Tasnim Kabir Sadik 349b230913 feat: align SDK with updated OpenAPI spec and add missing unit tests
- 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
2026-05-16 19:14:55 +06:00
2026-05-06 09:50:32 +00:00
2026-05-09 16:32:41 +06:00

Wrenn JavaScript SDK

JavaScript and TypeScript client for the Wrenn 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

npm install @wrenn/sdk

Requires Node.js 18+.

Authentication

Set the WRENN_API_KEY environment variable:

export WRENN_API_KEY="wrn_your_api_key_here"

Optionally override the API base URL:

export WRENN_BASE_URL="https://app.wrenn.dev/api"  # default

You can also pass credentials directly:

import { Capsule } from "@wrenn/sdk";

const capsule = await Capsule.create("minimal", {
	apiKey: "wrn_...",
	baseUrl: "https://app.wrenn.dev/api",
});

Wrenn Capsules

Quick Start

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

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:

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:

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:

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:

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

// 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:

// 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

// 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

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:

// 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

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:

// 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)

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:

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

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:

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:

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:

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:

// 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:

import { Sandbox } from "@wrenn/sdk";

const sandbox = await Sandbox.create("minimal");

Low-Level Client

For direct API access, use WrennClient:

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:

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 for dependency management and script execution. The package build still uses tsup, and tests use Vitest.

# 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:

export WRENN_API_KEY="wrn_..."
export WRENN_BASE_URL="https://app.wrenn.dev/api"  # optional

Then run:

make test-integration

Tests are automatically skipped when WRENN_API_KEY is not available.

License

MIT

Description
No description provided
Readme MIT 312 KiB