withruntime 0.6.1 → 0.7.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 (46) hide show
  1. package/E2B.md +32 -32
  2. package/README.md +3 -3
  3. package/dist/ai/index.d.ts +21 -5
  4. package/dist/ai/index.js +14 -21
  5. package/dist/ai-harness/index.js +15 -2
  6. package/dist/billing-cli.d.ts +21 -0
  7. package/dist/billing-cli.js +158 -0
  8. package/dist/cli-extras.d.ts +15 -0
  9. package/dist/cli-extras.js +60 -4
  10. package/dist/cli-name.d.ts +6 -0
  11. package/dist/cli-name.js +8 -0
  12. package/dist/cli.d.ts +17 -1
  13. package/dist/cli.js +535 -130
  14. package/dist/client.d.ts +35 -6
  15. package/dist/client.js +23 -0
  16. package/dist/credentials.js +1 -2
  17. package/dist/daytona/filesystem.d.ts +2 -0
  18. package/dist/daytona/filesystem.js +9 -7
  19. package/dist/e2b/filesystem.js +3 -2
  20. package/dist/e2b/sandbox.d.ts +9 -6
  21. package/dist/e2b/sandbox.js +31 -7
  22. package/dist/errors.d.ts +5 -0
  23. package/dist/errors.js +7 -0
  24. package/dist/index.d.ts +8 -6
  25. package/dist/index.js +1 -0
  26. package/dist/keys.js +1 -1
  27. package/dist/login.d.ts +2 -6
  28. package/dist/login.js +3 -8
  29. package/dist/network-cli.d.ts +1 -1
  30. package/dist/network-cli.js +64 -2
  31. package/dist/products/billing.d.ts +35 -0
  32. package/dist/products/billing.js +13 -0
  33. package/dist/products/index.d.ts +4 -1
  34. package/dist/products/index.js +4 -1
  35. package/dist/products/network-products.d.ts +45 -0
  36. package/dist/products/network-products.js +27 -0
  37. package/dist/products/previews.d.ts +1 -2
  38. package/dist/products/secrets.d.ts +14 -0
  39. package/dist/sandbox.d.ts +17 -3
  40. package/dist/sandbox.js +233 -62
  41. package/dist/transport.d.ts +17 -1
  42. package/dist/transport.js +124 -7
  43. package/dist/types.d.ts +0 -2
  44. package/dist/wallet.d.ts +30 -0
  45. package/dist/wallet.js +48 -0
  46. package/package.json +2 -2
package/E2B.md CHANGED
@@ -50,38 +50,38 @@ machine. An E2B key (`e2b_...`) is never sent anywhere. If one is passed as
50
50
 
51
51
  ## Mapping
52
52
 
53
- | E2B | On Runtime |
54
- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
55
- | `Sandbox.create()`, `Sandbox.create(template, opts)` | `sandboxes.create`. `base` and `code-interpreter-v1` are Runtime's stock image. Any other name must be a ready Runtime image with that name. A UUID is an image or a snapshot. |
56
- | `timeoutMs` (Python `timeout`, seconds) | `timeoutSeconds`, rounded up. Under 60 s becomes 60 s, with a warning. Over an hour is refused. |
57
- | `metadata` | labels (at most 32). |
58
- | `envs` | Given to every command and code run through this object. |
59
- | `allowInternetAccess: false` | `network: { internet: false }`. |
60
- | `lifecycle.onTimeout` | `onLeaseEnd`: `kill` is `stop`, `pause` is `pause`. |
61
- | `commands.run(cmd, { cwd, envs, timeoutMs, onStdout, onStderr })` | `exec`, streamed, so the whole output comes back however large. Default timeout 60 s. `0` means no limit (Runtime's longest, 24 hours). |
62
- | Non-zero exit | Throws `CommandExitError` with `exitCode`, `stdout`, `stderr` and `error` (`exit status N`). A command killed by a signal has `exitCode` -1. |
63
- | Command past its timeout | Throws `TimeoutError`. |
64
- | `commands.run(cmd, { background: true })` | `spawn`. Returns a `CommandHandle` with `pid`, `wait()`, `kill()` (SIGKILL), `sendStdin`, `closeStdin`, `disconnect`, `stdout`, `stderr`, `exitCode`. |
65
- | `commands.list`, `kill(pid)`, `sendStdin(pid)`, `closeStdin(pid)`, `connect(pid)` | Runtime's processes. A pid is a number derived from Runtime's process id, not the Linux pid. |
66
- | `files.read(path, { format })` | `files.read`. `text`, `bytes`, `blob` and `stream` (Python: `text`, `bytes`, `stream`). |
67
- | `files.write(path, data)`, `write([...])`, `writeFiles` | `files.write`. Parent directories are made, and a file that exists is replaced. |
68
- | `files.list(path, { depth })` | `files.list`, hidden files included. `owner` and `group` are always `""`. |
69
- | `files.makeDir`, `exists`, `getInfo`, `rename`, `remove` | `files.stat`, `mkdir`, `rename` (replacing a file that exists) and `remove` (recursive). A missing file throws `FileNotFoundError`. |
70
- | Relative paths | Resolved against the home directory, `/workspace` on Runtime. |
71
- | `/home/user` (E2B's home) | The first time a path, `cwd` or command names it, it becomes a link to `/workspace`, unless something is already there. |
72
- | `sandbox.kill()`, `Sandbox.kill(id)` | `stop`, without waiting. `false` when the sandbox was not found or had already ended. |
73
- | `sandbox.setTimeout(ms)`, `Sandbox.setTimeout(id, ms)` | `extend`: the end moves to now + ms. Moving it earlier is refused. |
74
- | `Sandbox.connect(id, { timeoutMs })`, `sandbox.connect()` | `get`. A paused sandbox is woken. A running one's end moves later, never earlier. An ended one throws `SandboxNotFoundError`. |
75
- | `sandbox.pause()`, `betaPause`, `Sandbox.pause(id)` | `pause`, keeping memory and files. `false` when it was already paused. |
76
- | `getInfo`, `isRunning` | `get`. `templateId` is `base` or the image or snapshot id. `envdVersion` is `runtime`. |
77
- | `Sandbox.list({ query: { metadata, state }, limit })` | `sandboxes.list` by labels and state, oldest first, with E2B's paginator (`hasNext`, `nextItems()`). |
78
- | `fork`, `createSnapshot`, `deleteSnapshot` | Runtime's forks and snapshots. While Runtime has them switched off, they throw `NotSupportedError` with Runtime's own message. |
79
- | `getHost(port)` | Refused, because E2B answers it without a call. `await sandbox.getPublicHost(port)` shares the port as a public Runtime preview and returns its host. |
80
- | `runCode(code, { language, context, onStdout, onStderr, onResult, onError, timeoutMs })` | Runtime's interpreter, Python and JavaScript. The sandbox's `envs` reach code through a context made once for them. `onResult` is called after the run ends. |
81
- | `Execution`, `Result`, `Logs`, `ExecutionError`, `OutputMessage` | The same shapes. Results that are too large to send inline are fetched and inlined. `logs.stdout` has one entry per line. |
82
- | `createCodeContext`, `listCodeContexts`, `restartCodeContext`, `removeCodeContext` | Runtime's interpreter contexts. |
83
- | `getMetrics({ start, end })`, `Sandbox.getMetrics(id)` | Runtime's measured CPU and memory, one entry per host reading (every minute by default). `diskUsed` is null: Runtime does not read disk use inside the sandbox. |
84
- | Error classes | E2B's names and parents. Each also carries Runtime's `code`, `hint` and `requestId`, with the original error as `cause`. |
53
+ | E2B | On Runtime |
54
+ | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | `Sandbox.create()`, `Sandbox.create(template, opts)` | `sandboxes.create`. `base` and `code-interpreter-v1` are Runtime's stock image. Any other name must be a ready Runtime image with that name. A UUID is an image or a snapshot. |
56
+ | `timeoutMs` (Python `timeout`, seconds) | `timeoutSeconds`, rounded up. Under 60 s becomes 60 s, with a warning. Over an hour is refused. |
57
+ | `metadata` | labels (at most 32). |
58
+ | `envs` | Given to every command and code run through this object. |
59
+ | `allowInternetAccess: false` | `network: { internet: false }`. |
60
+ | `lifecycle.onTimeout` | `onLeaseEnd`: `kill` is `stop`, `pause` is `pause`. |
61
+ | `commands.run(cmd, { cwd, envs, timeoutMs, onStdout, onStderr })` | `exec`, streamed, so the whole output comes back however large. Default timeout 60 s. `0` means no limit (Runtime's longest, 24 hours). |
62
+ | Non-zero exit | Throws `CommandExitError` with `exitCode`, `stdout`, `stderr` and `error` (`exit status N`). A command killed by a signal has `exitCode` -1. |
63
+ | Command past its timeout | Throws `TimeoutError`. |
64
+ | `commands.run(cmd, { background: true })` | `spawn`. Returns a `CommandHandle` with `pid`, `wait()`, `kill()` (SIGKILL), `sendStdin`, `closeStdin`, `disconnect`, `stdout`, `stderr`, `exitCode`. |
65
+ | `commands.list`, `kill(pid)`, `sendStdin(pid)`, `closeStdin(pid)`, `connect(pid)` | Runtime's processes. A pid is a number derived from Runtime's process id, not the Linux pid. |
66
+ | `files.read(path, { format })` | `files.read`. `text`, `bytes`, `blob` and `stream` (Python: `text`, `bytes`, `stream`). |
67
+ | `files.write(path, data)`, `write([...])`, `writeFiles` | `files.write`. Parent directories are made, and a file that exists is replaced. |
68
+ | `files.list(path, { depth })` | `files.list`, hidden files included. `owner` and `group` are always `""`. |
69
+ | `files.makeDir`, `exists`, `getInfo`, `rename`, `remove` | `files.stat`, `mkdir`, `rename` (replacing a file that exists) and `remove` (recursive). A missing file throws `FileNotFoundError`. |
70
+ | Relative paths | Resolved against the home directory, `/workspace` on Runtime. |
71
+ | `/home/user` (E2B's home) | The first time a path, `cwd` or command names it, it becomes a link to `/workspace`, unless something is already there. |
72
+ | `sandbox.kill()`, `Sandbox.kill(id)` | `stop`, without waiting. `false` when the sandbox was not found or had already ended. |
73
+ | `sandbox.setTimeout(ms)`, `Sandbox.setTimeout(id, ms)` | `extend`: the end moves to now + ms. Moving it earlier is refused. |
74
+ | `Sandbox.connect(id, { timeoutMs })`, `sandbox.connect()` | `get`. A paused sandbox is woken. A running one's end moves later, never earlier. An ended one throws `SandboxNotFoundError`. |
75
+ | `sandbox.pause()`, `betaPause`, `Sandbox.pause(id)` | `pause`, keeping memory and files. `false` when it was already paused. |
76
+ | `getInfo`, `isRunning` | `get`. `templateId` is `base` or the image or snapshot id. `envdVersion` is `runtime`. |
77
+ | `Sandbox.list({ query: { metadata, state }, limit })` | `sandboxes.list` by labels and state, oldest first, with E2B's paginator (`hasNext`, `nextItems()`). |
78
+ | `fork`, `createSnapshot`, `deleteSnapshot` | Runtime's forks and snapshots. While Runtime has them switched off, they throw `NotSupportedError` with Runtime's own message. |
79
+ | `getHost(port)` | `<port>-<id>.runtimehost.com` at once, as E2B answers it, and the port is shared as a public Runtime preview beside the caller (the sync Python sandbox shares it before returning). `await sandbox.getPublicHost(port)` returns once the share has landed. |
80
+ | `runCode(code, { language, context, onStdout, onStderr, onResult, onError, timeoutMs })` | Runtime's interpreter, Python and JavaScript. The sandbox's `envs` reach code through a context made once for them. `onResult` is called after the run ends. |
81
+ | `Execution`, `Result`, `Logs`, `ExecutionError`, `OutputMessage` | The same shapes. Results that are too large to send inline are fetched and inlined. `logs.stdout` has one entry per line. |
82
+ | `createCodeContext`, `listCodeContexts`, `restartCodeContext`, `removeCodeContext` | Runtime's interpreter contexts. |
83
+ | `getMetrics({ start, end })`, `Sandbox.getMetrics(id)` | Runtime's measured CPU and memory, one entry per host reading (every minute by default). `diskUsed` is null: Runtime does not read disk use inside the sandbox. |
84
+ | Error classes | E2B's names and parents. Each also carries Runtime's `code`, `hint` and `requestId`, with the original error as `cause`. |
85
85
 
86
86
  ## Gaps
87
87
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Runtime Cloud SDK and CLI
2
2
 
3
- One client for every Runtime Cloud product, and the `runtime` CLI. Node.js 22
4
- or later, or Bun. Its one dependency, undici (Node's own HTTP client), is
3
+ One client for every Runtime Cloud product, and the `runtime` CLI. Node.js
4
+ 22.12 or later, or Bun. Its one dependency, undici (Node's own HTTP client), is
5
5
  loaded only behind a proxy.
6
6
 
7
7
  ```bash no-run
@@ -27,7 +27,7 @@ console.log(result.exitCode, result.stdout);
27
27
  `Sandbox.create()` needs no arguments and returns once the sandbox is running;
28
28
  `await using` stops it when the block ends (Node 24, Bun or TypeScript; on
29
29
  Node 22 call `await sbx.stop()`). With no arguments you get the free
30
- trial while it lasts (50 free hours, up to eight sandboxes running at once), 2 vCPU, 4 GiB of
30
+ trial while it lasts (100 free hours, up to eight sandboxes running at once), 2 vCPU, 4 GiB of
31
31
  memory and a 4 GiB disk. The current default image includes NumPy, pandas and
32
32
  matplotlib; see the [sandbox environment](https://withruntime.com/docs/sandbox-environment).
33
33
 
@@ -9,21 +9,37 @@
9
9
  * await generateText({ model, tools: runtimeTools(sbx), stopWhen: stepCountIs(10), prompt });
10
10
  *
11
11
  * Needs the `ai` package (version 5 or newer) beside this one. */
12
- import { type Tool } from "ai";
12
+ import { type Schema } from "ai";
13
13
  import type { Sandbox } from "../sandbox.js";
14
14
  import { type ExecInput, type ExecOutput, type ListEntry, type SandboxToolOptions } from "../tools/index.js";
15
15
  export type { SandboxToolOptions } from "../tools/index.js";
16
+ /** One tool, typed so that both the AI SDK's `tools` and Mastra's accept it.
17
+ *
18
+ * ai 7's own `Tool` type is not assignable to Mastra's `tools` (checked with
19
+ * @mastra/core 1.71.0): ai 7 lets a description be a function, forbids an `id`
20
+ * and wants `context` in the call options, and Mastra wants a string
21
+ * description, an `id` or the older `parameters` field, and passes no
22
+ * `context`. This shape is the part both agree on. `parameters` is the same
23
+ * schema object as `inputSchema`: Mastra reads whichever it finds and the AI
24
+ * SDK reads only `inputSchema`. */
25
+ export type RuntimeTool<I, O> = {
26
+ type?: undefined;
27
+ description: string;
28
+ inputSchema: Schema<I>;
29
+ parameters: Schema<I>;
30
+ execute: (input: I, options?: unknown) => Promise<O>;
31
+ };
16
32
  /** `{ runtime_exec, runtime_read_file, runtime_write_file, runtime_list_files }`, bound to `sandbox`. */
17
33
  export type RuntimeTools = {
18
- runtime_exec: Tool<ExecInput, ExecOutput>;
19
- runtime_read_file: Tool<{
34
+ runtime_exec: RuntimeTool<ExecInput, ExecOutput>;
35
+ runtime_read_file: RuntimeTool<{
20
36
  path: string;
21
37
  }, string>;
22
- runtime_write_file: Tool<{
38
+ runtime_write_file: RuntimeTool<{
23
39
  path: string;
24
40
  content: string;
25
41
  }, string>;
26
- runtime_list_files: Tool<{
42
+ runtime_list_files: RuntimeTool<{
27
43
  path?: string;
28
44
  depth?: number;
29
45
  }, ListEntry[] | {
package/dist/ai/index.js CHANGED
@@ -9,30 +9,23 @@
9
9
  * await generateText({ model, tools: runtimeTools(sbx), stopWhen: stepCountIs(10), prompt });
10
10
  *
11
11
  * Needs the `ai` package (version 5 or newer) beside this one. */
12
- import { jsonSchema, tool } from "ai";
12
+ import { jsonSchema } from "ai";
13
13
  import { sandboxTools, } from "../tools/index.js";
14
+ function aiTool(source) {
15
+ const schema = jsonSchema(source.inputSchema);
16
+ return {
17
+ description: source.description,
18
+ inputSchema: schema,
19
+ parameters: schema,
20
+ execute: (input) => source.execute(input),
21
+ };
22
+ }
14
23
  export function runtimeTools(sandbox, options = {}) {
15
24
  const [exec, read, write, list] = sandboxTools(sandbox, options);
16
25
  return {
17
- runtime_exec: tool({
18
- description: exec.description,
19
- inputSchema: jsonSchema(exec.inputSchema),
20
- execute: (input) => exec.execute(input),
21
- }),
22
- runtime_read_file: tool({
23
- description: read.description,
24
- inputSchema: jsonSchema(read.inputSchema),
25
- execute: (input) => read.execute(input),
26
- }),
27
- runtime_write_file: tool({
28
- description: write.description,
29
- inputSchema: jsonSchema(write.inputSchema),
30
- execute: (input) => write.execute(input),
31
- }),
32
- runtime_list_files: tool({
33
- description: list.description,
34
- inputSchema: jsonSchema(list.inputSchema),
35
- execute: (input) => list.execute(input),
36
- }),
26
+ runtime_exec: aiTool(exec),
27
+ runtime_read_file: aiTool(read),
28
+ runtime_write_file: aiTool(write),
29
+ runtime_list_files: aiTool(list),
37
30
  };
38
31
  }
@@ -31,6 +31,19 @@ const DEFAULT_COMMAND_MS = 3_600_000;
31
31
  /** Exit codes for a command that ended without one, as coreutils `timeout` and SIGKILL give. */
32
32
  const TIMED_OUT_EXIT = 124;
33
33
  const KILLED_EXIT = 137;
34
+ /** The exit code a shell would report, which harnesses expect. Runtime's API
35
+ * gives a command killed by a signal as the negative signal number (a command
36
+ * past its limit is killed with SIGKILL and reads -9); a shell says 128 plus
37
+ * the signal, so -15 is 143. A command past its limit exits 124, whatever
38
+ * killed it, as `timeout(1)` does. */
39
+ function shellExitCode(exit) {
40
+ if (exit?.timedOut)
41
+ return TIMED_OUT_EXIT;
42
+ const code = exit?.exitCode;
43
+ if (code === undefined || code === null)
44
+ return KILLED_EXIT;
45
+ return code < 0 ? 128 - code : code;
46
+ }
34
47
  /** A harness sandbox provider backed by Runtime Cloud. Construct it once, at
35
48
  * module scope; nothing is created until a session is. */
36
49
  export function createRuntimeSandbox(settings = {}) {
@@ -187,7 +200,7 @@ export class RuntimeSandboxSession {
187
200
  throw abortSignal.reason;
188
201
  const timedOut = exit?.timedOut ?? false;
189
202
  return {
190
- exitCode: exit?.exitCode ?? (timedOut ? TIMED_OUT_EXIT : KILLED_EXIT),
203
+ exitCode: shellExitCode(exit),
191
204
  stdout: out.stdout,
192
205
  stderr: timedOut
193
206
  ? `${out.stderr}${out.stderr && !out.stderr.endsWith("\n") ? "\n" : ""}Timed out: the command ran past its limit and was ended.\n`
@@ -266,7 +279,7 @@ export class RuntimeSandboxSession {
266
279
  if (abortSignal?.aborted)
267
280
  throw abortSignal.reason;
268
281
  return {
269
- exitCode: exit?.exitCode ?? (exit?.timedOut ? TIMED_OUT_EXIT : KILLED_EXIT),
282
+ exitCode: shellExitCode(exit),
270
283
  };
271
284
  },
272
285
  kill: async () => {
@@ -0,0 +1,21 @@
1
+ import type { Runtime } from "./client.js";
2
+ import type { claimWalletAccount, openWalletAccount } from "./wallet.js";
3
+ type Out = {
4
+ json: boolean;
5
+ write: (text: string) => void;
6
+ error: (text: string) => void;
7
+ };
8
+ type Args = {
9
+ positional: string[];
10
+ flags: Map<string, string[]>;
11
+ };
12
+ export declare const BILLING_HELP = "runtime billing <command>\n\n topup <usd> [--wait] Buy credit with stablecoins (USDC on Base, Solana or Tempo).\n Prints an address per network and the exact amount to send.\n Send exactly that, from any wallet, before the time shown.\n --wait keeps checking until the credit lands.\n status <id> [--wait] Whether a top-up's payment has arrived\n\n No account yet? An agent can open one with a wallet alone:\n topup <usd> --new-account --accept-terms [--wait]\n Opens an account and its first top-up. Prints a claim code,\n shown once. --wait saves the key here once the money lands.\n claim <code> [--wait] Exchange a paid account's claim code for its key, saved here\n\n $10 to $10,000 a top-up; $500 in an account's first week. By card:\n https://withruntime.com/account/billing\n";
13
+ /** Wallet-account commands need no key, and the claim saves the one they get. */
14
+ export type WalletDeps = {
15
+ open: typeof openWalletAccount;
16
+ claim: typeof claimWalletAccount;
17
+ /** Saves a claimed key as this machine's connection, as `login --with-key` does. */
18
+ save: (key: string, orgId: string) => Promise<void>;
19
+ };
20
+ export declare function billingCommand(argv: string[], args: Args, out: Out, client: () => Promise<Runtime>, sleep?: (ms: number) => Promise<void>, wallet?: WalletDeps): Promise<number>;
21
+ export {};
@@ -0,0 +1,158 @@
1
+ import { RuntimeError } from "./errors.js";
2
+ import { named } from "./cli-name.js";
3
+ export const BILLING_HELP = `runtime billing <command>
4
+
5
+ topup <usd> [--wait] Buy credit with stablecoins (USDC on Base, Solana or Tempo).
6
+ Prints an address per network and the exact amount to send.
7
+ Send exactly that, from any wallet, before the time shown.
8
+ --wait keeps checking until the credit lands.
9
+ status <id> [--wait] Whether a top-up's payment has arrived
10
+
11
+ No account yet? An agent can open one with a wallet alone:
12
+ topup <usd> --new-account --accept-terms [--wait]
13
+ Opens an account and its first top-up. Prints a claim code,
14
+ shown once. --wait saves the key here once the money lands.
15
+ claim <code> [--wait] Exchange a paid account's claim code for its key, saved here
16
+
17
+ $10 to $10,000 a top-up; $500 in an account's first week. By card:
18
+ https://withruntime.com/account/billing
19
+ `;
20
+ function usage(message) {
21
+ return new RuntimeError({
22
+ message,
23
+ code: "usage",
24
+ status: 0,
25
+ hint: `Run \`${named("runtime")} billing help\`.`,
26
+ });
27
+ }
28
+ const DONE = new Set(["paid", "failed", "expired"]);
29
+ function describe(topup) {
30
+ const me = named("runtime");
31
+ if (topup.status === "paid")
32
+ return `Paid: $${topup.amountUsd} of credit is on the account.`;
33
+ if (topup.status === "expired")
34
+ return `Expired: no payment arrived in time. Open a new one with \`${me} billing topup ${topup.amountUsd}\`.`;
35
+ if (topup.status === "failed")
36
+ return "Failed. Nothing was credited.";
37
+ if (topup.status === "pending")
38
+ return `Stripe has seen the payment and is confirming it. Check again with \`${me} billing status ${topup.purchaseId}\`.`;
39
+ const lines = [`Send exactly ${topup.amount} of one of these tokens, on one network:`, ""];
40
+ for (const network of topup.networks)
41
+ for (const token of network.tokens)
42
+ lines.push(` ${network.network.padEnd(7)} ${token.currency.toUpperCase().padEnd(6)} to ${network.address}`, ` ${"".padEnd(7)} ${"".padEnd(6)} token ${token.contract}`);
43
+ lines.push("", ...(topup.payBy ? [`Send before ${topup.payBy}.`] : []), "A different amount, token or network cannot be matched or returned automatically.", `The credit lands a few minutes after the transfer: \`${me} billing status ${topup.purchaseId}\`.`);
44
+ return lines.join("\n");
45
+ }
46
+ const FLAGS = new Set(["wait", "new-account", "accept-terms"]);
47
+ export async function billingCommand(argv, args, out, client, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), wallet) {
48
+ const [verb, value] = args.positional;
49
+ const has = (name) => args.flags.get(name)?.at(-1) === "true";
50
+ const wait = has("wait");
51
+ for (const name of args.flags.keys())
52
+ if (!FLAGS.has(name))
53
+ throw usage(`Unknown option --${name}.`);
54
+ if (!verb || verb === "help") {
55
+ out.write(BILLING_HELP);
56
+ return 0;
57
+ }
58
+ if ((verb === "topup" && has("new-account")) || verb === "claim") {
59
+ if (!wallet)
60
+ throw usage("Wallet accounts are not available here.");
61
+ return walletCommand(verb, value, has("accept-terms"), wait, out, sleep, wallet);
62
+ }
63
+ const api = (await client()).billing;
64
+ let topup;
65
+ if (verb === "topup") {
66
+ topup = await api.topup({ usd: dollarsOf(value) }, { idempotencyKey: crypto.randomUUID() });
67
+ }
68
+ else if (verb === "status") {
69
+ if (!value)
70
+ throw usage("Give the top-up's id.");
71
+ topup = await api.topupStatus(value);
72
+ }
73
+ else {
74
+ throw usage(`Unknown billing command ${verb}.`);
75
+ }
76
+ const shown = new Set();
77
+ const show = (current) => {
78
+ if (out.json)
79
+ out.write(JSON.stringify(current));
80
+ else if (!shown.has(current.status))
81
+ out.write(describe(current));
82
+ shown.add(current.status);
83
+ };
84
+ show(topup);
85
+ if (!wait)
86
+ return 0;
87
+ /* Stripe takes a few minutes to see a transfer; checking every ten seconds
88
+ is quick enough and costs nothing. Stops once the top-up has an end, or
89
+ an hour after its deadline. */
90
+ const until = (topup.payBy ? Date.parse(topup.payBy) : Date.now()) + 60 * 60_000;
91
+ while (!DONE.has(topup.status) && Date.now() < until) {
92
+ await sleep(10_000);
93
+ topup = await api.topupStatus(topup.purchaseId);
94
+ show(topup);
95
+ }
96
+ return topup.status === "paid" ? 0 : 1;
97
+ }
98
+ function dollarsOf(value) {
99
+ const usd = Number(value?.replace(/^\$/, ""));
100
+ if (!value || !Number.isFinite(usd) || usd <= 0)
101
+ throw usage("Say how many dollars of credit to buy, such as `billing topup 20`.");
102
+ return usd;
103
+ }
104
+ /** `topup <usd> --new-account` and `claim <code>`: an account with no key. */
105
+ async function walletCommand(verb, value, acceptTerms, wait, out, sleep, wallet) {
106
+ const me = named("runtime");
107
+ let code;
108
+ let payBy = null;
109
+ if (verb === "topup") {
110
+ if (!acceptTerms)
111
+ throw usage("Opening an account means agreeing to https://withruntime.com/legal/terms. Add --accept-terms once the person you work for agrees.");
112
+ const opened = await wallet.open({ usd: dollarsOf(value), acceptTerms: true });
113
+ code = opened.claimCode;
114
+ payBy = opened.payBy;
115
+ if (out.json)
116
+ out.write(JSON.stringify(opened));
117
+ else
118
+ out.write([
119
+ describe(opened),
120
+ "",
121
+ `Claim code (shown once, keep it secret): ${code}`,
122
+ `Once paid: \`${me} billing claim ${code}\` gives this machine the account's key.`,
123
+ "A person can take the account over at https://withruntime.com/account/claim with the same code.",
124
+ ].join("\n"));
125
+ if (!wait)
126
+ return 0;
127
+ }
128
+ else {
129
+ if (!value)
130
+ throw usage("Give the claim code.");
131
+ code = value;
132
+ }
133
+ const until = (payBy ? Date.parse(payBy) : Date.now()) + 60 * 60_000;
134
+ for (let first = verb === "claim";; first = false) {
135
+ if (!first)
136
+ await sleep(10_000);
137
+ const claim = await wallet.claim(code);
138
+ if (claim.status === "paid") {
139
+ await wallet.save(claim.apiKey, claim.orgId);
140
+ out.write(out.json
141
+ ? JSON.stringify({ status: "paid", orgId: claim.orgId, connected: true })
142
+ : "Paid. This machine is connected to the new account; its key is saved, not shown.");
143
+ return 0;
144
+ }
145
+ if (claim.status === "expired" || claim.status === "failed") {
146
+ out.write(out.json
147
+ ? JSON.stringify(claim)
148
+ : `The top-up ${claim.status}: nothing was credited. Open a new one with \`${me} billing topup <usd> --new-account --accept-terms\`.`);
149
+ return 1;
150
+ }
151
+ if (!wait || Date.now() >= until) {
152
+ out.write(out.json
153
+ ? JSON.stringify(claim)
154
+ : `Not paid yet (${claim.status}). Run \`${me} billing claim ${code}\` again once the transfer has gone through.`);
155
+ return wait ? 1 : 0;
156
+ }
157
+ }
158
+ }
@@ -1,5 +1,6 @@
1
1
  import type { CliKit } from "./cli.js";
2
2
  import type { Sandbox } from "./sandbox.js";
3
+ import type { Snapshot } from "./snapshots.js";
3
4
  type Out = {
4
5
  json: boolean;
5
6
  write: (text: string) => void;
@@ -21,4 +22,18 @@ export declare function lostOutput(result: {
21
22
  stdoutTruncated?: boolean;
22
23
  stderrTruncated?: boolean;
23
24
  }, id: string): string | undefined;
25
+ /** Feeds `runtime sandbox shell` from standard input. When piped input ends,
26
+ * the shell is told so the way a terminal says it, Ctrl-D, after a newline
27
+ * if the last line had none: `echo ls | runtime sandbox shell <id>` runs `ls`
28
+ * and exits, as ssh does, where it used to wait forever (25 September 2026).
29
+ * A keyboard never ends, so a terminal's input is left alone. Returns the
30
+ * function that unhooks it. */
31
+ export declare function pipeShellInput(stdin: NodeJS.ReadableStream & {
32
+ isTTY?: boolean;
33
+ }, terminal: {
34
+ write(data: Uint8Array | string): void;
35
+ }): () => void;
36
+ /** `runtime snapshot get`, as `runtime sandbox get` reads: one fact a line.
37
+ * It printed raw JSON until 25 September 2026; `--json` still does. */
38
+ export declare function describeSnapshot(s: Snapshot, table: (rows: string[][]) => string): string;
24
39
  export {};
@@ -1,5 +1,5 @@
1
1
  import { writeFile } from "node:fs/promises";
2
- import { me } from "./login.js";
2
+ import { me } from "./cli-name.js";
3
3
  export async function recordCommand(sbx, argv, out, kit) {
4
4
  const { parse, integer, need, table, usage } = kit;
5
5
  const [action, ...rest] = argv;
@@ -7,7 +7,7 @@ export async function recordCommand(sbx, argv, out, kit) {
7
7
  const r = sbx.desktop.recordings;
8
8
  switch (action) {
9
9
  case "start": {
10
- const args = parse(rest);
10
+ const args = parse(rest, [], ["fps", "max-seconds", "max-mib"]);
11
11
  const rec = await r.start({
12
12
  ...(integer(args, "fps") ? { fps: integer(args, "fps") } : {}),
13
13
  ...(integer(args, "max-seconds") ? { maxSeconds: integer(args, "max-seconds") } : {}),
@@ -51,7 +51,7 @@ export async function recordCommand(sbx, argv, out, kit) {
51
51
  }
52
52
  export async function watchCommand(sbx, argv, out, kit) {
53
53
  const { parse, flag, has, need } = kit;
54
- const args = parse(argv, ["recursive"]);
54
+ const args = parse(argv, ["recursive"], ["events", "include", "exclude"]);
55
55
  const path = need(args.positional[0], "a directory, e.g. /workspace");
56
56
  const events = flag(args, "events")?.split(",");
57
57
  const watch = await sbx.files.watch(path, (event) => {
@@ -108,7 +108,7 @@ export async function mcpCommand(sbx, argv, out, kit) {
108
108
  };
109
109
  switch (action) {
110
110
  case "start": {
111
- const args = parse(rest, ["replace"]);
111
+ const args = parse(rest, ["replace"], ["secret", "env", "option", "port"]);
112
112
  if (!args.positional.length)
113
113
  throw usage("Name the servers: runtime sandbox mcp <id> start github fetch");
114
114
  const scoped = (name) => {
@@ -203,3 +203,59 @@ export function lostOutput(result, id) {
203
203
  return undefined;
204
204
  return `runtime: some of the command's output was lost: it wrote faster than it was read, and the sandbox keeps the latest 1 MiB. Write it to a file and copy it out: ${me} sandbox cp ${id}:/workspace/out.txt .`;
205
205
  }
206
+ /** Feeds `runtime sandbox shell` from standard input. When piped input ends,
207
+ * the shell is told so the way a terminal says it, Ctrl-D, after a newline
208
+ * if the last line had none: `echo ls | runtime sandbox shell <id>` runs `ls`
209
+ * and exits, as ssh does, where it used to wait forever (25 September 2026).
210
+ * A keyboard never ends, so a terminal's input is left alone. Returns the
211
+ * function that unhooks it. */
212
+ export function pipeShellInput(stdin, terminal) {
213
+ let last = 10;
214
+ const onData = (chunk) => {
215
+ const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
216
+ if (bytes.length)
217
+ last = bytes[bytes.length - 1];
218
+ terminal.write(new Uint8Array(bytes));
219
+ };
220
+ const onEnd = () => {
221
+ if (stdin.isTTY)
222
+ return;
223
+ terminal.write(last === 10 ? "\x04" : "\n\x04");
224
+ };
225
+ stdin.on("data", onData);
226
+ stdin.on("end", onEnd);
227
+ return () => {
228
+ stdin.off("data", onData);
229
+ stdin.off("end", onEnd);
230
+ };
231
+ }
232
+ /** `runtime snapshot get`, as `runtime sandbox get` reads: one fact a line.
233
+ * It printed raw JSON until 25 September 2026; `--json` still does. */
234
+ export function describeSnapshot(s, table) {
235
+ const mib = (bytes) => bytes === null ? "-" : `${Math.ceil(bytes / 1_048_576).toLocaleString("en-US")} MiB`;
236
+ const labels = (s.labels ?? {});
237
+ return table([
238
+ ["id", s.id],
239
+ ["name", s.name ?? "-"],
240
+ ["state", s.state],
241
+ ["from", s.sourceSandboxId],
242
+ ["shape", `${s.shape.vcpu} vCPU, ${s.shape.memoryMiB} MiB memory, ${s.shape.diskMiB} MiB disk`],
243
+ ["stored", mib(s.storedBytes)],
244
+ ["billed for", mib(s.meteredBytes)],
245
+ [
246
+ "backed up",
247
+ s.backedUp
248
+ ? `yes${s.durability.durableAt ? `, ${s.durability.durableAt}` : ""}`
249
+ : s.durability.state,
250
+ ],
251
+ ["created", s.createdAt],
252
+ ["kept until", `${s.expiresAt} (${s.retentionDays} days)`],
253
+ [
254
+ "labels",
255
+ Object.entries(labels)
256
+ .map(([k, v]) => `${k}=${v}`)
257
+ .join(", ") || "-",
258
+ ],
259
+ ...(s.error ? [["error", s.error]] : []),
260
+ ]);
261
+ }
@@ -0,0 +1,6 @@
1
+ /** How this CLI was started, so what it tells you to type works: through npx
2
+ * the command is `npx withruntime`, installed it is `runtime`. Its own module
3
+ * so a command that only names itself does not load the login flow. */
4
+ export declare const me: string;
5
+ /** Text that names a command, with the command spelled the way it was run. */
6
+ export declare function named(text: string, name?: string): string;
@@ -0,0 +1,8 @@
1
+ /** How this CLI was started, so what it tells you to type works: through npx
2
+ * the command is `npx withruntime`, installed it is `runtime`. Its own module
3
+ * so a command that only names itself does not load the login flow. */
4
+ export const me = /[\\/]_npx[\\/]/.test(process.argv[1] ?? "") ? "npx withruntime" : "runtime";
5
+ /** Text that names a command, with the command spelled the way it was run. */
6
+ export function named(text, name = me) {
7
+ return name === "runtime" ? text : text.replace(/(^|[\s`(])runtime(?= [a-z<[])/g, `$1${name}`);
8
+ }
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { RuntimeError } from "./errors.js";
3
+ import type { Usage } from "./types.js";
4
+ import type { SecretRule } from "./products/secrets.js";
3
5
  type Out = {
4
6
  json: boolean;
5
7
  write: (text: string) => void;
@@ -12,7 +14,7 @@ type Args = {
12
14
  };
13
15
  /** What the commands in cli-extras.ts use of this file. */
14
16
  declare const kit: {
15
- parse: (argv: string[], booleans?: string[]) => Args;
17
+ parse: (argv: string[], booleans: readonly string[], values: readonly string[]) => Args;
16
18
  flag: (args: Args, name: string) => string | undefined;
17
19
  has: (args: Args, name: string) => boolean;
18
20
  integer: (args: Args, name: string) => number | undefined;
@@ -25,6 +27,20 @@ declare const kit: {
25
27
  };
26
28
  export type CliKit = typeof kit;
27
29
  export declare function run(argv: string[], env?: NodeJS.ProcessEnv, out?: Out): Promise<number>;
30
+ /** `runtime usage` for a person: the balance, the trial and what each kind
31
+ * of resource was charged, in dollars. --json keeps every figure, and each
32
+ * resource with its rates and CPU time. */
33
+ /** One row per resource, for a spreadsheet: when it ran, how big it was, how
34
+ * long it ran, the CPU it used and what it cost. Money is exact dollars to the
35
+ * microdollar (the API's integer microdollars moved six places), so a column
36
+ * sums to the ledger to the last digit. */
37
+ export declare function usageCsv(u: Usage): string;
38
+ /** `--allow` values as secret rules: each is `[METHOD[,METHOD]...] PATH`, and
39
+ * rules naming the same methods share one rule's paths. Only the shape is read
40
+ * here; the API holds every rule to the same check the host makes
41
+ * (ARCHITECTURE.md section 10, "Per-path rules on an egress secret") and says
42
+ * which one it refused. */
43
+ export declare function parseSecretRules(values: string[]): SecretRule[];
28
44
  export declare function secretsCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out,
29
45
  /** Tests give the value here; the CLI reads standard input. */
30
46
  read?: () => Promise<string>): Promise<number>;