withruntime 0.2.1 → 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 (45) hide show
  1. package/README.md +68 -4
  2. package/dist/cli.d.ts +9 -0
  3. package/dist/cli.js +1262 -0
  4. package/dist/client.d.ts +118 -0
  5. package/dist/client.js +160 -0
  6. package/dist/credentials.d.ts +44 -0
  7. package/dist/credentials.js +180 -0
  8. package/dist/errors.d.ts +58 -0
  9. package/dist/errors.js +106 -0
  10. package/dist/index.d.ts +36 -0
  11. package/dist/index.js +25 -0
  12. package/dist/login.d.ts +119 -0
  13. package/dist/login.js +367 -0
  14. package/dist/mcp.d.ts +15 -0
  15. package/dist/mcp.js +221 -0
  16. package/dist/page.d.ts +13 -0
  17. package/dist/page.js +39 -0
  18. package/dist/products/desktop.d.ts +103 -0
  19. package/dist/products/desktop.js +38 -0
  20. package/dist/products/images.d.ts +121 -0
  21. package/dist/products/images.js +64 -0
  22. package/dist/products/index.d.ts +29 -0
  23. package/dist/products/index.js +36 -0
  24. package/dist/products/interpreter.d.ts +88 -0
  25. package/dist/products/interpreter.js +81 -0
  26. package/dist/products/network.d.ts +47 -0
  27. package/dist/products/network.js +19 -0
  28. package/dist/products/previews.d.ts +51 -0
  29. package/dist/products/previews.js +30 -0
  30. package/dist/products/referrals.d.ts +36 -0
  31. package/dist/products/referrals.js +8 -0
  32. package/dist/products/volumes.d.ts +47 -0
  33. package/dist/products/volumes.js +27 -0
  34. package/dist/sandbox.d.ts +192 -0
  35. package/dist/sandbox.js +628 -0
  36. package/dist/snapshots.d.ts +50 -0
  37. package/dist/snapshots.js +55 -0
  38. package/dist/tar.d.ts +3 -0
  39. package/dist/tar.js +116 -0
  40. package/dist/transport.d.ts +65 -0
  41. package/dist/transport.js +261 -0
  42. package/dist/types.d.ts +175 -0
  43. package/dist/types.js +1 -0
  44. package/package.json +53 -8
  45. package/cli.js +0 -8
package/dist/mcp.js ADDED
@@ -0,0 +1,221 @@
1
+ import process from "node:process";
2
+ import { resolveCredential } from "./credentials.js";
3
+ import { RuntimeError } from "./errors.js";
4
+ import { beginDeviceLogin } from "./login.js";
5
+ import { apiOrigin, VERSION } from "./transport.js";
6
+ const CONNECT_INSTRUCTIONS = "Runtime is not connected on this machine yet. Call runtime_connect: it gives you a link and a code for the person you work for to approve in their browser (no key to copy). Once they have, call runtime_connect again; Runtime's tools then appear (sandboxes, commands, files and the rest).";
7
+ const connectTool = {
8
+ name: "runtime_connect",
9
+ title: "Connect this machine to Runtime",
10
+ description: "Connect Runtime on this machine with a browser approval. The first call returns a link and a code: show them to the person you work for and ask them to approve. Call again after they have; it waits up to 45 seconds for the approval, then Runtime's tools appear.",
11
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
12
+ };
13
+ /** Bridges an MCP client that speaks stdio to Runtime's hosted MCP endpoint.
14
+ * Newline-delimited JSON-RPC in, the same out; every message is forwarded as
15
+ * it is, so the server's tools, schemas and `instructions` reach the client
16
+ * unchanged, and nothing here keeps a second catalogue. No dependencies:
17
+ * starting it costs a process and one TLS connection.
18
+ *
19
+ * With no key on this machine it does not fail: it serves one tool,
20
+ * runtime_connect, that walks the agent and its person through the browser
21
+ * approval, then connects to Runtime and tells the client its tools changed.
22
+ * So `claude mcp add runtime -- npx -y withruntime mcp` is the whole
23
+ * setup. */
24
+ export async function serveMcp(env = process.env, options = {}) {
25
+ const fetcher = options.fetch ?? fetch;
26
+ let key;
27
+ try {
28
+ key = await resolveCredential(env);
29
+ }
30
+ catch (error) {
31
+ if (!(error instanceof RuntimeError && error.code === "missing_api_key"))
32
+ throw error;
33
+ }
34
+ const origin = apiOrigin(env.RUNTIME_API_URL ?? "https://api.withruntime.com");
35
+ const endpoint = new URL("/mcp", origin).href;
36
+ let protocolVersion;
37
+ let session;
38
+ let initialize;
39
+ let pending;
40
+ const inFlight = new Set();
41
+ const write = (message) => {
42
+ process.stdout.write(`${JSON.stringify(message)}\n`);
43
+ };
44
+ const failure = (id, message, data) => ({
45
+ jsonrpc: "2.0",
46
+ id: id ?? null,
47
+ error: { code: -32000, message, ...(data === undefined ? {} : { data }) },
48
+ });
49
+ /** One message to the hosted endpoint, and the messages it answered. */
50
+ async function post(body) {
51
+ const response = await fetcher(endpoint, {
52
+ method: "POST",
53
+ redirect: "error",
54
+ headers: {
55
+ authorization: `Bearer ${key}`,
56
+ "content-type": "application/json",
57
+ accept: "application/json, text/event-stream",
58
+ "x-runtime-client": `mcp-bridge/${VERSION}`,
59
+ ...(protocolVersion ? { "mcp-protocol-version": protocolVersion } : {}),
60
+ ...(session ? { "mcp-session-id": session } : {}),
61
+ },
62
+ body,
63
+ signal: AbortSignal.timeout(600_000),
64
+ });
65
+ session = response.headers.get("mcp-session-id") ?? session;
66
+ const text = await response.text();
67
+ const messages = [];
68
+ if (response.ok && response.status !== 202 && response.status !== 204) {
69
+ if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
70
+ for (const block of text.split(/\n\n/))
71
+ for (const data of block.split("\n").filter((l) => l.startsWith("data:")))
72
+ messages.push(JSON.parse(data.slice(5).trim()));
73
+ }
74
+ else if (text.trim())
75
+ messages.push(JSON.parse(text));
76
+ }
77
+ for (const reply of messages) {
78
+ const result = reply.result;
79
+ if (result?.protocolVersion)
80
+ protocolVersion = result.protocolVersion;
81
+ }
82
+ return { status: response.status, messages, text };
83
+ }
84
+ async function forward(line, id, isRequest) {
85
+ let answer;
86
+ try {
87
+ answer = await post(line);
88
+ }
89
+ catch {
90
+ if (isRequest)
91
+ write(failure(id, "Runtime could not be reached. Check the network and retry."));
92
+ return;
93
+ }
94
+ if (answer.status >= 400) {
95
+ let detail;
96
+ try {
97
+ detail = JSON.parse(answer.text).error;
98
+ }
99
+ catch {
100
+ detail = answer.text.slice(0, 500);
101
+ }
102
+ const hint = answer.status === 401
103
+ ? "The key was refused. Run `npx -y withruntime login` again, or set RUNTIME_API_KEY."
104
+ : `Runtime answered ${answer.status}.`;
105
+ if (isRequest)
106
+ write(failure(id, hint, detail));
107
+ return;
108
+ }
109
+ for (const reply of answer.messages)
110
+ write(reply);
111
+ }
112
+ /** Not connected yet: answer the client here, with one tool that connects. */
113
+ async function connectMode(message, id) {
114
+ const method = message.method;
115
+ const reply = (result) => write({ jsonrpc: "2.0", id, result });
116
+ if (method === "initialize") {
117
+ initialize = message.params ?? {};
118
+ return reply({
119
+ protocolVersion: initialize.protocolVersion ?? "2025-06-18",
120
+ capabilities: { tools: { listChanged: true } },
121
+ serverInfo: { name: "runtime-cloud", version: VERSION },
122
+ instructions: CONNECT_INSTRUCTIONS,
123
+ });
124
+ }
125
+ if (typeof method === "string" && method.startsWith("notifications/"))
126
+ return;
127
+ if (method === "ping")
128
+ return reply({});
129
+ if (method === "tools/list")
130
+ return reply({ tools: [connectTool] });
131
+ if (method !== "tools/call" ||
132
+ message.params?.name !== "runtime_connect")
133
+ return write(failure(id, "Runtime is not connected yet: call runtime_connect first."));
134
+ const text = (words) => reply({ content: [{ type: "text", text: words }] });
135
+ try {
136
+ if (!pending) {
137
+ pending = await beginDeviceLogin(env, env.RUNTIME_AGENT_NAME ?? "MCP agent", {
138
+ ...(options.fetch ? { fetch: options.fetch } : {}),
139
+ });
140
+ if (!env.RUNTIME_NO_BROWSER && !pending.resumed)
141
+ await (options.openBrowser ?? openBrowser)(pending.url).catch(() => undefined);
142
+ return text(`Ask the person you work for to open ${pending.url} (it may already be open in their browser), sign in to Runtime, check that it shows the code ${pending.userCode}, and choose Connect agent. Then call runtime_connect again.`);
143
+ }
144
+ const until = Math.min(pending.expiresAt, Date.now() + 45_000);
145
+ while (Date.now() < until) {
146
+ const outcome = await pending.check();
147
+ if (outcome === "expired")
148
+ break;
149
+ if (outcome !== "pending") {
150
+ key = await resolveCredential(env);
151
+ pending = undefined;
152
+ await post(JSON.stringify({
153
+ jsonrpc: "2.0",
154
+ id: "runtime-connect",
155
+ method: "initialize",
156
+ params: initialize ?? {},
157
+ }));
158
+ await post(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }));
159
+ write({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
160
+ return text(`Connected as ${outcome.agentName}. Runtime's tools are available now: list the tools again, then start with runtime_sandboxes_create.`);
161
+ }
162
+ await new Promise((resolve) => setTimeout(resolve, 3000));
163
+ }
164
+ if (Date.now() >= pending.expiresAt) {
165
+ pending = undefined;
166
+ return text("The link expired before it was approved. Call runtime_connect for a new one.");
167
+ }
168
+ return text(`Not approved yet. The person you work for should open ${pending.url} and choose Connect agent; then call runtime_connect again.`);
169
+ }
170
+ catch (error) {
171
+ pending = undefined;
172
+ return write(failure(id, error instanceof Error
173
+ ? error.message
174
+ : "The connection failed. Call runtime_connect again."));
175
+ }
176
+ }
177
+ async function handle(line) {
178
+ let message;
179
+ try {
180
+ message = JSON.parse(line);
181
+ }
182
+ catch {
183
+ write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
184
+ return;
185
+ }
186
+ const first = Array.isArray(message) ? message[0] : message;
187
+ const id = first?.id;
188
+ const isRequest = id !== undefined && first.method !== undefined;
189
+ if (key === undefined && first && typeof first === "object")
190
+ return connectMode(first, id);
191
+ return forward(line, id, isRequest);
192
+ }
193
+ let buffer = "";
194
+ process.stdin.setEncoding("utf8");
195
+ await new Promise((resolve) => {
196
+ process.stdin.on("data", (chunk) => {
197
+ buffer += chunk;
198
+ let newline = buffer.indexOf("\n");
199
+ while (newline >= 0) {
200
+ const line = buffer.slice(0, newline).trim();
201
+ buffer = buffer.slice(newline + 1);
202
+ if (line) {
203
+ const task = handle(line).finally(() => inFlight.delete(task));
204
+ inFlight.add(task);
205
+ }
206
+ newline = buffer.indexOf("\n");
207
+ }
208
+ });
209
+ const finish = () => void Promise.allSettled([...inFlight]).then(() => resolve());
210
+ process.stdin.once("end", finish);
211
+ process.once("SIGINT", finish);
212
+ process.once("SIGTERM", finish);
213
+ });
214
+ }
215
+ async function openBrowser(url) {
216
+ const { execFile } = await import("node:child_process");
217
+ const { promisify } = await import("node:util");
218
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
219
+ const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
220
+ await promisify(execFile)(command, args, { timeout: 10_000 });
221
+ }
package/dist/page.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /** A list you can await for one page or iterate for every item. */
2
+ export declare class Page<T> implements AsyncIterable<T> {
3
+ readonly data: T[];
4
+ readonly nextCursor: string | null;
5
+ private readonly fetchPage;
6
+ constructor(data: T[], nextCursor: string | null, fetchPage: (cursor: string) => Promise<Page<T>>);
7
+ get hasMore(): boolean;
8
+ next(): Promise<Page<T> | null>;
9
+ /** Every page, fetched as you go. */
10
+ pages(): AsyncGenerator<Page<T>>;
11
+ [Symbol.asyncIterator](): AsyncGenerator<T>;
12
+ toArray(limit?: number): Promise<T[]>;
13
+ }
package/dist/page.js ADDED
@@ -0,0 +1,39 @@
1
+ /** A list you can await for one page or iterate for every item. */
2
+ export class Page {
3
+ data;
4
+ nextCursor;
5
+ fetchPage;
6
+ constructor(data, nextCursor, fetchPage) {
7
+ this.data = data;
8
+ this.nextCursor = nextCursor;
9
+ this.fetchPage = fetchPage;
10
+ }
11
+ get hasMore() {
12
+ return this.nextCursor !== null;
13
+ }
14
+ async next() {
15
+ return this.nextCursor === null ? null : this.fetchPage(this.nextCursor);
16
+ }
17
+ /** Every page, fetched as you go. */
18
+ async *pages() {
19
+ yield this;
20
+ let page = await this.next();
21
+ while (page) {
22
+ yield page;
23
+ page = await page.next();
24
+ }
25
+ }
26
+ async *[Symbol.asyncIterator]() {
27
+ for await (const page of this.pages())
28
+ yield* page.data;
29
+ }
30
+ async toArray(limit = 10_000) {
31
+ const all = [];
32
+ for await (const item of this) {
33
+ all.push(item);
34
+ if (all.length >= limit)
35
+ break;
36
+ }
37
+ return all;
38
+ }
39
+ }
@@ -0,0 +1,103 @@
1
+ import type { Sandbox } from "../sandbox.js";
2
+ import type { RequestOptions, Transport } from "../transport.js";
3
+ import type { Preview } from "./previews.js";
4
+ export type MouseButton = "left" | "middle" | "right";
5
+ export type DesktopWindow = {
6
+ id: string;
7
+ desktop: number;
8
+ pid: number;
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ className: string;
14
+ title: string;
15
+ };
16
+ export type DesktopStart = {
17
+ ok: boolean;
18
+ display: string;
19
+ size: string;
20
+ /** Opens the live desktop (noVNC) in a browser; carries a private preview token. */
21
+ streamUrl: string;
22
+ preview: Preview;
23
+ };
24
+ /** `sbx.desktop`: a Linux desktop in the sandbox, driven like a person would.
25
+ *
26
+ * const { streamUrl } = await sbx.desktop.start();
27
+ * await sbx.desktop.open("https://example.com");
28
+ * await sbx.desktop.click(640, 400);
29
+ * await sbx.desktop.type("hello");
30
+ * const png = await sbx.desktop.screenshot();
31
+ *
32
+ * Coordinates are pixels from the top left of the screen. */
33
+ export declare function sandboxDesktop(t: Transport, sandbox: Sandbox): {
34
+ start: (size?: {
35
+ width?: number;
36
+ height?: number;
37
+ }, options?: RequestOptions) => Promise<DesktopStart>;
38
+ stop: (options?: RequestOptions) => Promise<{
39
+ ok: boolean;
40
+ }>;
41
+ /** PNG bytes, or JPEG with `format: "jpeg"`. */
42
+ screenshot: (input?: {
43
+ format?: "png" | "jpeg";
44
+ quality?: number;
45
+ }, options?: RequestOptions) => Promise<Uint8Array<ArrayBufferLike>>;
46
+ move: (x: number, y: number, options?: RequestOptions) => Promise<{
47
+ ok: true;
48
+ }>;
49
+ click: (x?: number, y?: number, input?: {
50
+ button?: MouseButton;
51
+ double?: boolean;
52
+ }, options?: RequestOptions) => Promise<{
53
+ ok: true;
54
+ }>;
55
+ doubleClick: (x?: number, y?: number, options?: RequestOptions) => Promise<{
56
+ ok: true;
57
+ }>;
58
+ rightClick: (x?: number, y?: number, options?: RequestOptions) => Promise<{
59
+ ok: true;
60
+ }>;
61
+ mouseDown: (button?: MouseButton, options?: RequestOptions) => Promise<{
62
+ ok: true;
63
+ }>;
64
+ mouseUp: (button?: MouseButton, options?: RequestOptions) => Promise<{
65
+ ok: true;
66
+ }>;
67
+ drag: (from: [number, number], to: [number, number], options?: RequestOptions) => Promise<{
68
+ ok: true;
69
+ }>;
70
+ /** Wheel clicks: positive `dy` scrolls down, positive `dx` right. */
71
+ scroll: (dy: number, input?: {
72
+ dx?: number;
73
+ x?: number;
74
+ y?: number;
75
+ }, options?: RequestOptions) => Promise<{
76
+ ok: true;
77
+ }>;
78
+ type: (text: string, input?: {
79
+ delayMs?: number;
80
+ }, options?: RequestOptions) => Promise<{
81
+ ok: true;
82
+ }>;
83
+ /** xdotool key names, space separated: "ctrl+l", "Return", "alt+Tab". */
84
+ press: (keys: string, options?: RequestOptions) => Promise<{
85
+ ok: true;
86
+ }>;
87
+ cursor: (options?: RequestOptions) => Promise<{
88
+ x: number;
89
+ y: number;
90
+ }>;
91
+ windows: (options?: RequestOptions) => Promise<DesktopWindow[]>;
92
+ focus: (windowId: string, options?: RequestOptions) => Promise<{
93
+ ok: true;
94
+ }>;
95
+ /** Opens `url` in Firefox on the desktop. */
96
+ open: (url: string, options?: RequestOptions) => Promise<{
97
+ ok: true;
98
+ }>;
99
+ /** Starts a program on the desktop, detached. */
100
+ launch: (argv: string[], options?: RequestOptions) => Promise<{
101
+ ok: true;
102
+ }>;
103
+ };
@@ -0,0 +1,38 @@
1
+ /** `sbx.desktop`: a Linux desktop in the sandbox, driven like a person would.
2
+ *
3
+ * const { streamUrl } = await sbx.desktop.start();
4
+ * await sbx.desktop.open("https://example.com");
5
+ * await sbx.desktop.click(640, 400);
6
+ * await sbx.desktop.type("hello");
7
+ * const png = await sbx.desktop.screenshot();
8
+ *
9
+ * Coordinates are pixels from the top left of the screen. */
10
+ export function sandboxDesktop(t, sandbox) {
11
+ const base = () => `/v1/sandboxes/${encodeURIComponent(sandbox.id)}/desktop`;
12
+ const act = (body, options) => t.json({ method: "POST", path: `${base()}:act`, body, ...options });
13
+ return {
14
+ start: (size = {}, options) => t.json({ method: "POST", path: `${base()}:start`, body: size, ...options }),
15
+ stop: (options) => t.json({ method: "POST", path: `${base()}:stop`, ...options }),
16
+ /** PNG bytes, or JPEG with `format: "jpeg"`. */
17
+ screenshot: (input = {}, options) => t.bytes({ method: "GET", path: `${base()}/screenshot`, query: input, ...options }),
18
+ move: (x, y, options) => act({ action: "move", x, y }, options),
19
+ click: (x, y, input = {}, options) => act({ action: "click", ...(x === undefined ? {} : { x, y }), ...input }, options),
20
+ doubleClick: (x, y, options) => act({ action: "click", double: true, ...(x === undefined ? {} : { x, y }) }, options),
21
+ rightClick: (x, y, options) => act({ action: "click", button: "right", ...(x === undefined ? {} : { x, y }) }, options),
22
+ mouseDown: (button = "left", options) => act({ action: "mouseDown", button }, options),
23
+ mouseUp: (button = "left", options) => act({ action: "mouseUp", button }, options),
24
+ drag: (from, to, options) => act({ action: "drag", from, to }, options),
25
+ /** Wheel clicks: positive `dy` scrolls down, positive `dx` right. */
26
+ scroll: (dy, input = {}, options) => act({ action: "scroll", dy, ...input }, options),
27
+ type: (text, input = {}, options) => act({ action: "type", text, ...input }, options),
28
+ /** xdotool key names, space separated: "ctrl+l", "Return", "alt+Tab". */
29
+ press: (keys, options) => act({ action: "key", keys }, options),
30
+ cursor: (options) => act({ action: "cursor" }, options),
31
+ windows: async (options) => (await act({ action: "windows" }, options)).windows,
32
+ focus: (windowId, options) => act({ action: "focus", windowId }, options),
33
+ /** Opens `url` in Firefox on the desktop. */
34
+ open: (url, options) => act({ action: "open", url }, options),
35
+ /** Starts a program on the desktop, detached. */
36
+ launch: (argv, options) => act({ action: "launch", argv }, options),
37
+ };
38
+ }
@@ -0,0 +1,121 @@
1
+ import { Page } from "../page.js";
2
+ import type { RequestOptions, Transport } from "../transport.js";
3
+ /** A file for a Dockerfile's build context or a recipe. */
4
+ export type ImageFile = {
5
+ path: string;
6
+ content: string;
7
+ encoding?: "utf8" | "base64";
8
+ mode?: number;
9
+ };
10
+ export type ImageRecipe = {
11
+ /** "runtime" (the default) or a public image reference. */
12
+ base?: string;
13
+ apt?: string[];
14
+ pip?: string[];
15
+ npm?: string[];
16
+ commands?: string[];
17
+ env?: Record<string, string>;
18
+ files?: ImageFile[];
19
+ workdir?: string;
20
+ };
21
+ export type ImageBuildLimits = {
22
+ vcpu?: number;
23
+ memoryMiB?: number;
24
+ diskMiB?: number;
25
+ maxImageMiB?: number;
26
+ timeoutSeconds?: number;
27
+ };
28
+ /** Exactly one of `image`, `dockerfile` or `recipe`. */
29
+ export type CreateImage = {
30
+ name?: string;
31
+ labels?: Record<string, string>;
32
+ region?: string;
33
+ build?: ImageBuildLimits;
34
+ env?: Record<string, string>;
35
+ } & ({
36
+ image: string;
37
+ dockerfile?: never;
38
+ recipe?: never;
39
+ } | {
40
+ dockerfile: string;
41
+ files?: ImageFile[];
42
+ buildArgs?: Record<string, string>;
43
+ image?: never;
44
+ recipe?: never;
45
+ } | {
46
+ recipe: ImageRecipe;
47
+ image?: never;
48
+ dockerfile?: never;
49
+ });
50
+ export type ImageState = "queued" | "building" | "ready" | "failed" | "deleting" | "deleted";
51
+ export type Image = {
52
+ id: string;
53
+ kind: "image";
54
+ status: string;
55
+ state: ImageState;
56
+ name: string | null;
57
+ labels: Record<string, string>;
58
+ region: string;
59
+ source: {
60
+ kind: "dockerfile" | "oci" | "recipe";
61
+ digest: string;
62
+ base: unknown;
63
+ steps: number;
64
+ };
65
+ build: ImageBuildLimits & {
66
+ startedAt: string | null;
67
+ endedAt: string | null;
68
+ cachedSteps: number;
69
+ builderVersion: string | null;
70
+ };
71
+ sizeBytes: number | null;
72
+ sizeMiB: number | null;
73
+ storedBytes: number | null;
74
+ rootfsSha256: string | null;
75
+ env: Record<string, string>;
76
+ workdir: string;
77
+ error: string | null;
78
+ logs: {
79
+ bytes: number;
80
+ lines: number;
81
+ truncated: boolean;
82
+ };
83
+ createdAt: string;
84
+ readyAt: string | null;
85
+ /** What the source asked for that an image does not carry (on create). */
86
+ notes?: string[];
87
+ };
88
+ export type ImageLogLine = {
89
+ seq: number;
90
+ at: string;
91
+ stream: "build" | "system";
92
+ text: string;
93
+ };
94
+ export type ImageLogs = {
95
+ lines: ImageLogLine[];
96
+ nextAfter: number;
97
+ state: ImageState;
98
+ truncated: boolean;
99
+ done: boolean;
100
+ };
101
+ /** Custom images: `runtime.images.build({ recipe: { pip: ["pandas"] } })`, then
102
+ * `runtime.sandboxes.create({ image: image.id })`. */
103
+ export declare function images(t: Transport): {
104
+ /** Queue a build and return at once, state "queued". */
105
+ create: (input: CreateImage, options?: RequestOptions) => Promise<Image>;
106
+ /** Build and wait until the image is ready or failed, streaming log lines
107
+ * to `onLog`. Throws when the build fails, with the build's own error. */
108
+ build(input: CreateImage, options?: RequestOptions & {
109
+ onLog?: (line: ImageLogLine) => void;
110
+ pollMs?: number;
111
+ }): Promise<Image>;
112
+ get: (id: string, options?: RequestOptions) => Promise<Image>;
113
+ logs: (id: string, after?: number, options?: RequestOptions) => Promise<ImageLogs>;
114
+ list: (query?: {
115
+ state?: ImageState;
116
+ name?: string;
117
+ limit?: number;
118
+ cursor?: string;
119
+ }, options?: RequestOptions) => Promise<Page<Image>>;
120
+ delete: (id: string, options?: RequestOptions) => Promise<Image>;
121
+ };
@@ -0,0 +1,64 @@
1
+ import { Page } from "../page.js";
2
+ /** Custom images: `runtime.images.build({ recipe: { pip: ["pandas"] } })`, then
3
+ * `runtime.sandboxes.create({ image: image.id })`. */
4
+ export function images(t) {
5
+ const get = (id, options) => t.json({ method: "GET", path: `/v1/images/${encodeURIComponent(id)}`, ...options });
6
+ const logs = (id, after = 0, options) => t.json({
7
+ method: "GET",
8
+ path: `/v1/images/${encodeURIComponent(id)}/logs`,
9
+ query: { after },
10
+ ...options,
11
+ });
12
+ const list = async (query = {}, options) => {
13
+ const page = await t.json({
14
+ method: "GET",
15
+ path: "/v1/images",
16
+ query,
17
+ ...options,
18
+ });
19
+ return new Page(page.data, page.nextCursor, (cursor) => list({ ...query, cursor }, options));
20
+ };
21
+ return {
22
+ /** Queue a build and return at once, state "queued". */
23
+ create: (input, options) => t.json({ method: "POST", path: "/v1/images", body: input, ...options }),
24
+ /** Build and wait until the image is ready or failed, streaming log lines
25
+ * to `onLog`. Throws when the build fails, with the build's own error. */
26
+ async build(input, options = {}) {
27
+ const { onLog, pollMs = 1000, ...request } = options;
28
+ let image = await t.json({
29
+ method: "POST",
30
+ path: "/v1/images",
31
+ body: input,
32
+ ...request,
33
+ });
34
+ let after = 0;
35
+ for (;;) {
36
+ if (onLog) {
37
+ const page = await logs(image.id, after, { signal: request.signal });
38
+ for (const line of page.lines)
39
+ onLog(line);
40
+ after = page.nextAfter;
41
+ }
42
+ image = await get(image.id, { signal: request.signal });
43
+ if (image.state !== "queued" && image.state !== "building")
44
+ break;
45
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
46
+ }
47
+ if (onLog)
48
+ for (const line of (await logs(image.id, after)).lines)
49
+ onLog(line);
50
+ if (image.state !== "ready")
51
+ throw new Error(`Image ${image.id} ${image.state}: ${image.error ?? "no error given"}`);
52
+ return image;
53
+ },
54
+ get,
55
+ logs,
56
+ list,
57
+ delete: (id, options) => t.json({
58
+ method: "POST",
59
+ path: `/v1/images/${encodeURIComponent(id)}:delete`,
60
+ body: {},
61
+ ...options,
62
+ }),
63
+ };
64
+ }
@@ -0,0 +1,29 @@
1
+ import type { Transport } from "../transport.js";
2
+ import type { Sandbox } from "../sandbox.js";
3
+ import { images } from "./images.js";
4
+ import { sandboxDesktop } from "./desktop.js";
5
+ import { sandboxInterpreter } from "./interpreter.js";
6
+ import { sandboxNetwork } from "./network.js";
7
+ import { sandboxPreviews } from "./previews.js";
8
+ import { referrals } from "./referrals.js";
9
+ import { volumes } from "./volumes.js";
10
+ export declare const clientExtensions: {
11
+ images: typeof images;
12
+ volumes: typeof volumes;
13
+ referrals: typeof referrals;
14
+ };
15
+ export declare const sandboxExtensions: {
16
+ interpreter: typeof sandboxInterpreter;
17
+ previews: typeof sandboxPreviews;
18
+ network: typeof sandboxNetwork;
19
+ desktop: typeof sandboxDesktop;
20
+ };
21
+ export type ClientExtensions = {
22
+ readonly [K in keyof typeof clientExtensions]: ReturnType<(typeof clientExtensions)[K]>;
23
+ };
24
+ export type SandboxExtensions = {
25
+ readonly [K in keyof typeof sandboxExtensions]: ReturnType<(typeof sandboxExtensions)[K]>;
26
+ };
27
+ /** The factories as plain pairs, for the constructors that install them. */
28
+ export declare function clientFactories(): Array<[string, (t: Transport) => unknown]>;
29
+ export declare function sandboxFactories(): Array<[string, (t: Transport, sandbox: Sandbox) => unknown]>;
@@ -0,0 +1,36 @@
1
+ import { images } from "./images.js";
2
+ import { sandboxDesktop } from "./desktop.js";
3
+ import { sandboxInterpreter } from "./interpreter.js";
4
+ import { sandboxNetwork } from "./network.js";
5
+ import { sandboxPreviews } from "./previews.js";
6
+ import { referrals } from "./referrals.js";
7
+ import { volumes } from "./volumes.js";
8
+ /* HOW A LANE ADDS A PRODUCT TO THE SDK
9
+ *
10
+ * 1. Write src/products/<name>.ts exporting a factory:
11
+ * export function images(t: Transport) {
12
+ * return {
13
+ * create: (input: CreateImage, options?: RequestOptions) =>
14
+ * t.json<Image>({ method: "POST", path: "/v1/images", body: input, ...options }),
15
+ * ...
16
+ * };
17
+ * }
18
+ * and, for methods that belong on a sandbox (sbx.previews.create(3000)):
19
+ * export function sandboxPreviews(t: Transport, sandbox: Sandbox) { return { ... } }
20
+ * 2. Add ONE line to the matching object below. The Runtime client and every
21
+ * Sandbox pick it up, typed, with the shared auth, retries, keys and errors.
22
+ */
23
+ export const clientExtensions = { images, volumes, referrals };
24
+ export const sandboxExtensions = {
25
+ interpreter: sandboxInterpreter,
26
+ previews: sandboxPreviews,
27
+ network: sandboxNetwork,
28
+ desktop: sandboxDesktop,
29
+ };
30
+ /** The factories as plain pairs, for the constructors that install them. */
31
+ export function clientFactories() {
32
+ return Object.entries(clientExtensions);
33
+ }
34
+ export function sandboxFactories() {
35
+ return Object.entries(sandboxExtensions);
36
+ }