# Wrenn JavaScript SDK JavaScript and TypeScript client for the [Wrenn](https://wrenn.dev) microVM platform. Create isolated capsules, execute commands, manage files, run interactive terminals, and execute Python code -- all from Node.js. Designed as an e2b-style SDK. If you're migrating, `Sandbox` is available as a deprecated alias for `Capsule`. ## Installation ```bash npm install @wrenn/sdk ``` Requires Node.js 18+. ## Authentication Set the `WRENN_API_KEY` environment variable: ```bash export WRENN_API_KEY="wrn_your_api_key_here" ``` Optionally override the API base URL: ```bash export WRENN_BASE_URL="https://app.wrenn.dev/api" # default ``` You can also pass credentials directly: ```ts import { Capsule } from "@wrenn/sdk"; const capsule = await Capsule.create("minimal", { apiKey: "wrn_...", baseUrl: "https://app.wrenn.dev/api", }); ``` --- ## Wrenn Capsules ### Quick Start ```ts import { Capsule } from "@wrenn/sdk"; const capsule = await Capsule.create("minimal"); try { await capsule.waitForReady(); const result = await capsule.commands.exec("echo", { args: ["hello"] }); console.log(result.stdout); // "hello\n" } finally { await capsule.destroy(); } ``` ### Creating Capsules ```ts import { Capsule } from "@wrenn/sdk"; // Create with defaults: template="minimal", vcpus=1, memory_mb=512. const capsule = await Capsule.create(); // Create with an explicit template. const python = await Capsule.create("base-python"); // Create with resource and client options. const larger = await Capsule.create("minimal", { vcpus: 2, memory_mb: 1024, timeout_sec: 300, apiKey: "wrn_...", }); // Equivalent options-object form. const fromOptions = await Capsule.create({ template: "minimal", vcpus: 2, memory_mb: 1024, }); ``` ### Resource Cleanup Capsules created with `Capsule.create()` are owned by that SDK instance. When used with `await using`, the remote capsule is destroyed automatically when the block exits: ```ts await using capsule = await Capsule.create("minimal"); await capsule.waitForReady(); await capsule.commands.exec("echo", { args: ["work"] }); // capsule is automatically destroyed here ``` Capsules attached with `new Capsule(id)` or `Capsule.connect(id)` are not owned. `await using` a connected capsule only runs local cleanup and does not destroy the remote capsule. Use `destroy()` when you want to delete the remote capsule: ```ts const capsule = await Capsule.create("minimal"); try { await capsule.waitForReady(); await capsule.commands.exec("echo", { args: ["work"] }); } finally { await capsule.destroy(); } ``` ### Connecting to Existing Capsules Attach to an existing capsule by ID. This wraps the ID locally and does not fetch or validate it until you call an API method: ```ts const capsule = Capsule.connect("cl-abc123"); const info = await capsule.getInfo(); if (info.status === "paused") { await capsule.resume({ wait: true }); } const result = await capsule.commands.exec("echo", { args: ["still running"] }); console.log(result.stdout); ``` For code interpreter capsules: ```ts import { CodeInterpreter } from "@wrenn/sdk"; const interpreter = CodeInterpreter.connect("cl-abc123"); const result = await interpreter.notebook.execCell("print('reconnected')"); console.log(result.stdout); ``` ### Lifecycle Management ```ts // Instance methods. await capsule.pause(); // returns status like "pausing" await capsule.resume(); // returns status like "resuming" await capsule.resume({ wait: true }); await capsule.destroy(); await capsule.ping(); // reset inactivity timer await capsule.waitForReady(); const info = await capsule.getInfo(); console.log(info.status); // "running" const metrics = await capsule.getMetrics({ range: "10m" }); // Static helper. await Capsule.destroy("cl-abc123", { apiKey: "wrn_..." }); ``` ### Command Execution Commands are accessed via `capsule.commands`: ```ts // Foreground command. const result = await capsule.commands.exec("python3", { args: ["-c", "print(42)"], timeoutSec: 30, cwd: "/app", }); console.log(result.stdout); // "42\n" console.log(result.stderr); console.log(result.exit_code); // 0 // Background process. const process = await capsule.commands.start("python3", { args: ["server.py"], tag: "web-server", envs: { PORT: "8000" }, cwd: "/app", }); console.log(process.pid); console.log(process.tag); ``` #### Streaming Output ```ts // Stream a new command. for await (const event of capsule.commands.stream("python3", { args: ["-u", "train.py"], })) { if (event.type === "stdout") { process.stdout.write(String(event.data ?? "")); } if (event.type === "stderr") { process.stderr.write(String(event.data ?? "")); } if (event.type === "exit") { console.log("exited", event.exit_code); } } // Connect to a running background process stream. await using connection = await capsule.commands.streamProcess("web-server"); connection.send({ type: "ping" }); // connection is automatically closed here ``` #### Process Management ```ts const processes = await capsule.commands.list(); for (const proc of processes.processes ?? []) { console.log(proc.pid, proc.tag); } await capsule.commands.kill("web-server", "SIGTERM"); ``` ### Filesystem Files are accessed via `capsule.files`: ```ts // Write and read files. await capsule.files.write("/app/main.py", "print('hello')"); const content = await capsule.files.read("/app/main.py"); console.log(content.toString()); // List directory. const listing = await capsule.files.list("/app", { depth: 1 }); for (const entry of listing.entries ?? []) { console.log(entry.name, entry.type, entry.size); } // Create directory. await capsule.files.mkdir("/app/data"); // Remove file or directory. await capsule.files.remove("/app/old_data"); ``` #### Streaming Large Files ```ts await capsule.files.uploadStream( "/data/large.txt", Buffer.from("large file content"), ); const chunks: Buffer[] = []; for await (const chunk of capsule.files.downloadStream("/data/large.txt")) { chunks.push(chunk); } console.log(Buffer.concat(chunks).toString()); ``` ### Git Git operations are accessed via `capsule.git`. All commands execute the real `git` binary inside the capsule: ```ts // Clone a repository. await capsule.git.clone("https://github.com/org/repo.git", { path: "/app/repo", branch: "main", }); // Use repository-scoped commands. const status = await capsule.git.status({ cwd: "/app/repo" }); console.log(status.stdout); const log = await capsule.git.log({ cwd: "/app/repo", maxCount: 5 }); console.log(log.stdout); // Branches. await capsule.git.checkout("feature", { cwd: "/app/repo", create: true }); console.log((await capsule.git.branch({ cwd: "/app/repo" })).stdout); // Stage and commit. await capsule.git.add(["README.md", "src/index.ts"], { cwd: "/app/repo" }); await capsule.git.commit("initial commit", { cwd: "/app/repo" }); // Push and pull. await capsule.git.pull({ cwd: "/app/repo", remote: "origin", branch: "main" }); await capsule.git.push({ cwd: "/app/repo", remote: "origin", branch: "main" }); ``` Git helpers return command results. Check `exit_code`, `stdout`, and `stderr` for command-level failures. ### Interactive Terminal (PTY) ```ts await using term = await capsule.pty.start({ cmd: "/bin/bash", cols: 120, rows: 40, cwd: "/home/user", }); term.input("ls -la\n"); for await (const event of term.events) { if (event.type === "data") { process.stdout.write(String(event.data ?? "")); } if (event.type === "exit") { break; } } // terminal WebSocket is automatically closed here ``` Reconnect to a tagged session: ```ts await using term = await capsule.pty.connect("my-session-tag"); term.input("echo reconnected\n"); // terminal WebSocket is automatically closed here ``` **PtySession methods:** | Method | Description | |--------|-------------| | `input(data)` | Send text or bytes to stdin | | `resize(cols, rows)` | Resize the terminal | | `kill()` | Send a kill control message | | `close()` | Close the WebSocket connection | | `events` | Async iterator of PTY events | --- ## Code Interpreter `CodeInterpreter` creates or connects to a capsule intended for Python code execution. It uses the capsule command API to execute cells with `python3 -c`. ### Quick Start ```ts import { CodeInterpreter } from "@wrenn/sdk"; await using interpreter = await CodeInterpreter.create(); await interpreter.capsule.waitForReady(); const result = await interpreter.notebook.execCell("print('hello')"); console.log(result.stdout); // "hello\n" // interpreter.capsule is automatically destroyed here ``` Interpreters created with `CodeInterpreter.create()` own their capsule and destroy it on `await using` disposal. Interpreters attached with `CodeInterpreter.connect(id)` follow connected capsule semantics and leave the remote capsule running. ### Custom Templates By default, `CodeInterpreter.create()` uses the `jupyter` template. You can specify a custom template: ```ts const interpreter = await CodeInterpreter.create({ template: "my-custom-python-template", timeout_sec: 300, }); const result = await interpreter.notebook.execCell("print('custom template')", { timeoutSec: 60, }); ``` ### Code Interpreter + Commands/Files The code interpreter wrapper exposes the underlying standard capsule: ```ts const interpreter = await CodeInterpreter.create(); const { capsule } = interpreter; await interpreter.notebook.execCell("open('/tmp/data.csv', 'w').write('a,b\\n1,2')"); const content = await capsule.files.read("/tmp/data.csv"); console.log(content.toString()); const result = await capsule.commands.exec("wc", { args: ["-l", "/tmp/data.csv"] }); console.log(result.stdout); ``` --- ## Error Handling The SDK maps unsuccessful API responses to typed errors: ```ts import { AuthenticationError, BadRequestError, ConflictError, ForbiddenError, HostHasCapsulesError, NotFoundError, PayloadTooLargeError, ServerError, TimeoutError, WrennError, } from "@wrenn/sdk"; try { await Capsule.connect("missing").getInfo(); } catch (error) { if (error instanceof NotFoundError) { console.log(error.code); console.log(error.message); console.log(error.statusCode); // 404 } if (error instanceof WrennError) { console.log(error.body); } } ``` All SDK errors inherit from `WrennError` and expose `statusCode`, `code`, `message`, and `body`. --- ## Migrating from e2b Replace your imports and prefer `Capsule` for new code: ```ts // Before import { Sandbox } from "e2b"; const sandbox = await Sandbox.create(); // After import { Capsule } from "@wrenn/sdk"; const capsule = await Capsule.create(); ``` The `Sandbox` name is available as a deprecated alias: ```ts import { Sandbox } from "@wrenn/sdk"; const sandbox = await Sandbox.create("minimal"); ``` --- ## Low-Level Client For direct API access, use `WrennClient`: ```ts import { WrennClient } from "@wrenn/sdk"; const client = new WrennClient({ apiKey: "wrn_..." }); const capsule = await client.capsules.create({ template: "minimal", vcpus: 1, memory_mb: 512, timeout_sec: 300, }); await client.capsules.pause(capsule.id); await client.capsules.resume(capsule.id); await client.capsules.ping(capsule.id); await client.capsules.destroy(capsule.id); const templates = await client.snapshots.list(); console.log(templates); ``` Available resource groups: | Resource | Property | |----------|----------| | Auth | `client.auth` | | Account | `client.account` | | API keys | `client.apiKeys` | | Users | `client.users` | | Teams | `client.teams` | | Capsules | `client.capsules` | | Files | `client.files` | | Snapshots | `client.snapshots` | | Hosts | `client.hosts` | | Channels | `client.channels` | Generated OpenAPI types are exported from the package root: ```ts import type { components, operations, paths } from "@wrenn/sdk"; type CapsuleSchema = components["schemas"]["Capsule"]; type GetCapsuleOperation = operations["getCapsule"]; type CapsulePath = paths["/v1/capsules/{id}"]; ``` --- ## Development This project uses [Bun](https://bun.sh) for dependency management and script execution. The package build still uses `tsup`, and tests use Vitest. ```bash # Install dependencies bun install # Run linting make lint # Run unit tests make test # Build CJS, ESM, and declaration files make build # Run lint + unit tests make check ``` ### Running Integration Tests Integration tests require a live Wrenn server. Set credentials via environment variables: ```bash export WRENN_API_KEY="wrn_..." export WRENN_BASE_URL="https://app.wrenn.dev/api" # optional ``` Then run: ```bash make test-integration ``` Tests are automatically skipped when `WRENN_API_KEY` is not available. ## License MIT