- Update generated types from new openapi.yaml (capsule stats, usage, metrics, pause/resume lifecycle, host/channel management, auth flow) - Add Capsule pause/resume/ping/getMetrics lifecycle methods - Add Capsule.waitForReady abort signal support - Add PtyManager.connect and PtySession disposal - Fix HttpClient empty-body response handling (content-length: 0) - Add streamProcess() to CommandManager for background process streams - Add integration tests for capsule lifecycle, git, and PTY features - Add unit tests for AsyncQueue error paths, PtySession.close, Git.checkout without create, Git.add single string, Notebook.execCell error case, and PtyStartOptions fields
554 lines
12 KiB
Markdown
554 lines
12 KiB
Markdown
# 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
|