Files
js-sdk/README.md
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

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