@easybits.cloud/sdk
v0.36.4
Published
Typed HTTP client for the EasyBits cloud for AI agents — sandboxes, web, files, SQL databases, documents and app hosting
Downloads
3,325
Maintainers
Readme
@easybits.cloud/sdk
The typed HTTP client for the EasyBits cloud for AI agents — sandboxes, web, files, SQL databases, documents and app hosting.
EasyBits is the cloud AI agents already know how to use: run code in an isolated Firecracker microVM, search and read the web, store and serve files, query a SQL database, generate documents and deploy apps — via SDK, MCP and REST API. Priced in MXN.
Install
npm install @easybits.cloud/sdkQuick Start
import { EasybitsClient } from "@easybits.cloud/sdk";
const eb = new EasybitsClient({ apiKey: "eb_sk_live_..." });
// Upload a file
const { file, putUrl } = await eb.uploadFile({
fileName: "photo.jpg",
contentType: "image/jpeg",
size: 1024000,
});
// Upload bytes to the presigned URL
await fetch(putUrl, { method: "PUT", body: fileBuffer });
// Confirm upload
await eb.updateFile(file.id, { status: "DONE" });Authentication
Get your API key from the Developer Dashboard.
const eb = new EasybitsClient({ apiKey: process.env.EASYBITS_API_KEY! });Or use automatic resolution from ~/.easybitsrc or env vars:
import { createClientFromEnv } from "@easybits.cloud/sdk";
const eb = await createClientFromEnv();Methods
Files
| Method | Description |
|--------|-------------|
| listFiles(params?) | List your files (paginated) |
| getFile(fileId) | Get file details + download URL |
| uploadFile(params) | Create file record + get upload URL |
| updateFile(fileId, params) | Update name, access, metadata, or status |
| deleteFile(fileId) | Soft-delete a file (7-day retention) |
| restoreFile(fileId) | Restore a soft-deleted file |
| listDeletedFiles(params?) | List deleted files with days until purge |
| searchFiles(query) | AI-powered natural language file search |
| bulkUploadFiles(items) | Create up to 20 file records + upload URLs |
| bulkDeleteFiles(fileIds) | Soft-delete up to 100 files at once |
| duplicateFile(fileId, name?) | Copy an existing file (new storage object) |
| listPermissions(fileId) | List sharing permissions for a file |
Images
| Method | Description |
|--------|-------------|
| optimizeImage(params) | Convert to WebP/AVIF (creates new file) |
| transformImage(params) | Resize, crop, rotate, flip, grayscale |
Sharing
| Method | Description |
|--------|-------------|
| shareFile(params) | Share a file with another user by email |
| generateShareToken(fileId, expiresIn?) | Generate a temporary download URL |
| listShareTokens(params?) | List share tokens (paginated) |
Webhooks
| Method | Description |
|--------|-------------|
| listWebhooks() | List your configured webhooks |
| createWebhook(params) | Create a webhook (returns secret, shown once) |
| getWebhook(webhookId) | Get webhook details |
| updateWebhook(webhookId, params) | Update URL, events, or status |
| deleteWebhook(webhookId) | Permanently delete a webhook |
Websites
| Method | Description |
|--------|-------------|
| listWebsites() | List your static websites |
| createWebsite(name, { slug? }) | Create a new website (optional custom slug) |
| getWebsite(websiteId) | Get website details |
| updateWebsite(websiteId, params) | Update website name, slug or status (old slug 301s to the new one) |
| deleteWebsite(websiteId) | Delete website and its files |
Workspaces
Namespaced, quota-bounded containers of files. Create one per tenant, mint a workspace-scoped key, and that key can only ever touch its own workspace's files.
| Method | Description |
|--------|-------------|
| listWorkspaces(params?) | List workspaces (cursor paginated) |
| createWorkspace({ name, slug?, quotaBytes? }) | Create a workspace |
| getWorkspace(workspaceId) | Get workspace details |
| updateWorkspace(workspaceId, params) | Update name/status/quota |
| deleteWorkspace(workspaceId) | Delete workspace and its files |
| getWorkspaceUsage(workspaceId) | Get { usedBytes, quotaBytes, fileCount } |
| createWorkspaceKey(workspaceId, params?) | Mint a workspace-scoped API key (raw returned once) |
Documents
| Method | Description |
|--------|-------------|
| listDocuments() | List your documents |
| getDocument(id) | Get document with all pages |
| createDocument(params) | Create a document |
| updateDocument(id, params) | Update document metadata (name, theme, colors) |
| deleteDocument(id) | Delete a document |
| deployDocument(id) | Publish as live website |
| unpublishDocument(id) | Unpublish document |
| generateDocument(id, params) | AI-generate pages (parallel, streaming) |
| refineDocument(id, params) | Surgical AI edits to a page |
| regenerateDocumentPage(id, params) | Redesign a page keeping content |
| enhanceDocumentPrompt(name, prompt?) | Auto-describe or enhance a prompt |
| getDocumentDirections(prompt, opts?) | Get 4 design directions (fonts, colors, mood) |
Account
| Method | Description |
|--------|-------------|
| getUsageStats() | Storage used/limit, file counts, plan info |
| listProviders() | List storage providers |
| listKeys() | List your API keys |
Webhooks
EasyBits sends POST requests to your URL when events occur. Payloads are signed with HMAC SHA-256.
// Create a webhook
const webhook = await eb.createWebhook({
url: "https://your-server.com/webhooks/easybits",
events: ["file.created", "file.deleted"],
});
// Save the secret — it's only shown once
console.log(webhook.secret); // whsec_...Events
| Event | Trigger |
|-------|---------|
| file.created | File uploaded or duplicated |
| file.updated | File name, access, or metadata changed |
| file.deleted | File soft-deleted |
| file.restored | File restored from trash |
| website.created | Website created |
| website.updated | Website name or slug changed |
| website.deleted | Website deleted |
| workspace.created | Workspace created |
| workspace.deleted | Workspace deleted |
Verifying signatures
import { createHmac } from "crypto";
function verifyWebhook(body: string, signature: string, secret: string): boolean {
const expected = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
return signature === expected;
}
// In your webhook handler:
const signature = req.headers["x-easybits-signature"];
const event = req.headers["x-easybits-event"];
const isValid = verifyWebhook(rawBody, signature, webhook.secret);Payload format
{
"event": "file.created",
"timestamp": "2026-02-26T12:00:00.000Z",
"data": {
"id": "abc123",
"name": "photo.jpg",
"size": 1024000,
"contentType": "image/jpeg",
"access": "private"
}
}Auto-pause
Webhooks are automatically paused after 5 consecutive delivery failures. Reactivate with:
await eb.updateWebhook(webhookId, { status: "ACTIVE" });Examples
Bulk upload files
const { items } = await eb.bulkUploadFiles([
{ fileName: "a.pdf", contentType: "application/pdf", size: 50000 },
{ fileName: "b.png", contentType: "image/png", size: 120000 },
]);
for (const { file, putUrl } of items) {
await fetch(putUrl, { method: "PUT", body: buffers[file.name] });
await eb.updateFile(file.id, { status: "DONE" });
}Check account usage
const stats = await eb.getUsageStats();
console.log(`${stats.storage.usedGB}/${stats.storage.maxGB} GB used`);
console.log(`${stats.counts.files} files, ${stats.counts.webhooks} webhooks`);Duplicate a file
const copy = await eb.duplicateFile("abc123", "photo-backup.jpg");Pagination
Every paginated list returns one envelope: { items, nextCursor, hasMore } (some
also include total). When hasMore is true, pass nextCursor back as cursor
to fetch the next page:
let cursor: string | undefined;
do {
const { items, nextCursor, hasMore } = await eb.listFiles({ limit: 50, cursor });
process(items);
cursor = nextCursor ?? undefined;
if (!hasMore) break;
} while (cursor);Error Handling
All errors share one shape: a JSON body { "error": "message" }, sometimes with
extra fields like code or status.
import { EasybitsError } from "@easybits.cloud/sdk";
try {
await eb.getFile("nonexistent");
} catch (err) {
if (err instanceof EasybitsError) {
console.log(err.status); // 404
console.log(err.body); // '{"error":"File not found"}'
}
}Sandboxes
Run code in isolated Firecracker microVMs — execute code with a persistent Jupyter kernel, manage files, run background processes, and expose ports as public HTTPS URLs.
import { EasybitsClient } from "@easybits.cloud/sdk";
const eb = new EasybitsClient({ apiKey: process.env.EASYBITS_API_KEY });
// Create a sandbox (waits until it's running)
const sbx = await eb.sandboxes.create({ template: "code-interpreter" });
// Persistent Python kernel — state survives between calls
await sbx.runCell("import pandas as pd; df = pd.read_csv('sales.csv')");
const out = await sbx.runCell("df.groupby('month').total.sum()");
console.log(out.stdout);
// matplotlib charts come back as image/png in results[]
const chart = await sbx.runCell("df.plot(); plt.show()");
const png = chart.results.find((r) => r.type === "image/png")?.data; // base64
// Run a server and get a public URL. Write the command as `exec <program>`:
// the shell is REPLACED by your process instead of staying as its parent, so
// signals and logs go straight to it. (Do NOT try to background something with
// nohup or & inside exec(): that call is synchronous and its shell dies with
// the response, taking the child with it. That is what execBackground is for.)
const { execId } = await sbx.execBackground("exec python3 -m http.server 3000");
const { url } = await sbx.exposePort(3000);
console.log(url); // https://sb-...-3000.sandboxes.easybits.cloud
// Poll it, list them, kill it. bgStatus returns the ACCUMULATED stdout, so
// live logs are just `st.stdout.slice(seen)` — no websockets needed.
const st = await sbx.bgStatus(execId); // { status, exitCode?, stdout, stderr }
const { processes } = await sbx.bgList(); // lost the execId? it is in here
await sbx.bgKill(execId, { graceSeconds: 2 });
// bgKill signals the whole process GROUP (SIGTERM, then SIGKILL after the
// grace), so a dev server's forked children die too instead of surviving and
// holding the port. Killing an already-finished process succeeds and reports
// alreadyExited.
// exposePort is HTTP-only (22/23/25/445/3389 are rejected). For a raw L4 port:
const fwd = await sbx.exposeRawPort(22, "tcp");
console.log(fwd.endpoint); // cname.sandboxes.easybits.cloud:49123 — dial this
// hostPort comes from a pool, differs per box and is released on destroy:
// read it back, never hardcode it. Close it with unexposeRawPort(22, "tcp").
// Only templates that declare the port work; a 403 is permanent, not transient.
// SSH: injects the key, restarts the box sshd and opens 22, in one call.
// The box sshd is fail-closed, so the key has to go in before the port opens.
const ssh = await sbx.enableSsh(["ssh-ed25519 AAAA... me@laptop"]);
console.log(ssh.command); // ssh -p 49002 root@<host of THAT box>
// Key-only, as root. disableSsh() closes the port but does NOT revoke the key.
// PREFER THE TUNNEL over that command. A high port does not survive office
// networks or corporate VPNs, and that failure reaches you as an
// unreproducible "it won't connect". The tunnel rides the same 443 as the web:
//
// npm i -g @easybits.cloud/cli && easybits login # or: easybits login - < key.txt
// easybits ssh-key # your public key; created once, never leaves the box
//
// # ~/.ssh/config
// Host *.ghosty
// ProxyCommand easybits ssh-proxy %h
// User root
//
// ssh my-box.ghosty # the `name` you gave it at create time
// ssh sb_abc123.ghosty # the id works too
// ssh my-box.ghosty "cd /data/work && ghosty serve --acp" # remote ACP
//
// enableSsh() is still needed once, to inject the key (the box sshd is
// fail-closed). The tunnel moves opaque bytes and does NOT authenticate: the
// SSH session authenticates end-to-end against the box's sshd, so a bug in the
// tunnel cannot let anyone in. sshTicket() mints the short-lived signed ticket
// the proxy uses — you rarely call it yourself.
// Files
await sbx.files.write("/tmp/data.json", JSON.stringify({ ok: true }));
const { content } = await sbx.files.read("/tmp/data.json");
// Lifecycle
await sbx.extend(600); // add 10 min to the TTL
await sbx.destroy();Snapshot & fork (copy-on-write clone)
Freeze a running box into a named image, then boot N children from it — each an independent sandbox with its own IP. Prep the environment once (deps installed, project set up), snapshot it, and fork in parallel to try N variants without repeating the setup.
// Base box: install deps once
const base = await eb.sandboxes.create({ template: "node" });
await base.exec("npm i -g cowsay");
// Snapshot the ready state — the box keeps running
const snap = await base.snapshot("deps-ready");
// Fork into 3 children that run in parallel (each inherits the disk)
const kids = await base.fork({ count: 3 });
for (const k of kids) {
await k.waitUntilReady();
console.log(k.sandboxId, (await k.exec("cowsay hi")).stdout);
}
// Reuse the snapshot later, without the base box
const more = await eb.sandboxes.forkFromSnapshot(snap.snapshotId, { count: 2 });
// Catalog + cleanup
await eb.sandboxes.snapshots.list();
await eb.sandboxes.snapshots.delete(snap.snapshotId);Derived templates (template-snapshot)
Prepare one box (npm ci, seeds, skills), capture it once under a content
key (key, hash), and every box created with that pair is born with the
bootstrap done — no fork, no per-child copy; the host keeps only the disk delta
(12-22 MB). Measured: capture ~0.5-1 s, create from the derived template ~0.6 s
- ~2 s boot. This is what
@easybits.cloud/eve-sandboxuses for eve'sprewarm.
const base = await eb.sandboxes.create({ template: "node" });
await base.exec("cd /workspace && npm ci");
// Idempotent per (key, hash): a second call returns reused: true
const dt = await base.templateSnapshot({ key: "agent", hash: "3f9c" }); // { derivedId, reused, sizeBytes }
// Children never inherit the source env (secrets are scrubbed): pass their own
const child = await eb.sandboxes.create({ templateKey: "agent", templateHash: "3f9c", env: { FOO: "bar" } });
await eb.sandboxes.templateSnapshots.get({ key: "agent", hash: "3f9c" }); // 404 DerivedTemplateNotProvisioned → capture first
await eb.sandboxes.templateSnapshots.list();
await eb.sandboxes.templateSnapshots.delete(dt.derivedId); // 409 DerivedTemplateInUse while children liveUnused for 30 days, a derived template deletes itself. If the base template is
rebaked, create answers 409 DerivedTemplateStale: capture again.
Network policy (per-box egress)
await sbx.setNetworkPolicy("deny-all");
await sbx.setNetworkPolicy({ allow: { "api.github.com": [], "registry.npmjs.org": [] } }); // no wildcards
await sbx.setNetworkPolicy("allow-all");Resolved to IPs by the host (DNS refresh), persisted with the box and re-applied
on resume. transform (header injection) is not supported.
Sleep / wake (survive quiet periods)
Without suspendOnIdle, the box is destroyed when timeoutSeconds (default
300 s) elapses. For anything you will talk to later, sleep it instead: a
suspended box is a Firecracker snapshot, resumes in ~1 s and keeps disk and memory.
// At creation
const sbx = await eb.sandboxes.create({ template: "node", timeoutSeconds: 600, suspendOnIdle: true });
// Or on an existing box (e.g. one you created without it)
await sbx.setIdlePolicy({ suspendOnIdle: true, idleTtlSeconds: 600, hardTtlSeconds: 7 * 24 * 3600 });
await sbx.suspend(); // sleep now; TTL is paused
await sbx.resume(); // wake; the remaining lifetime is restoredTemplates
Use eb.listTemplates() for the live catalog with required env. Kinds: base
(run code), agent (ready-made agent), service (platform boxes started with
service_start), internal (fleet workers created by the platform).
| Template | Kind | Description |
|---|---|---|
| ubuntu | base | Full Linux. Install packages, compile, run servers. |
| python | base | Python runtime; each run-code is a fresh process. |
| node | base | Node 24 + typescript, tsx, pnpm, git and python3; each run-code is a fresh process. |
| bun | base | Bun runtime. |
| dev-box | base | Clean work box (git, curl, build-essential, Node 22); the recommended one for SSH. |
| code-interpreter | base | Python + persistent Jupyter kernel (sandbox_run_cell): variables and charts survive between cells. |
| eve-nitro | base | Self-hosted eve (Vercel) server: Node 24, pnpm, eve CLI 0.65; persistent /data, port 3000. |
| node-agent | agent | Node + Claude Agent SDK pre-baked (agent_run). |
| claude-code | agent | Claude Agent SDK loop; per-token billing. |
| goose | agent | goose (AAIF), coding agent with native ACP. |
| ghostyclaw | agent | Always-on Ghosty daemon (WhatsApp, Slack, Telegram) with Docker and admin-api. |
| ghosty-lite | agent | Lightweight Rust ACP agent, multi-provider; your EasyBits key can be its brain. |
| open-ghosty | agent | Ghosty on open models, SSE web chat. |
| lang-ghosty | agent | Ghosty on LangChain, SSE web chat. |
| rust-ghosty | agent | DeepSeek-first Ghosty (CodeWhale/Rust) with SSE web chat and WhatsApp. |
| ghosty-gc | agent | Ghosty for teams (GTeams): threads, artifacts, collaborative editor. |
| ghosty-chat | agent | Persistent Ghosty chat (Express + SSE). |
| cagent-ghosty | agent | Ghosty on cagent (Docker), SSE web chat. |
| openclaw | agent | OpenClaw, always-on personal AI. |
| chat-openai | agent | Persistent Express+SSE chat on OpenAI; create it with agent_create. |
| chat-anthropic | agent | Persistent Express+SSE chat on Anthropic; create it with agent_create. |
| ghosty-studio | agent | Ghosty Studio: agent control plane inside a box. |
| desktop-ghosty | agent | Linux desktop with Ghosty (noVNC). |
| computer-ghosty | agent | Computer-use with XFCE desktop + public noVNC. |
| computer-ghosty-gemini | agent | Computer-use on Gemini. |
| livekit-svc | service | Video call room + HD recording (Studio). |
| whisper-svc | service | whisper STT; part of the voice box. |
| kokoro-svc | service | kokoro TTS; part of the voice box. |
| voice-svc | service | Voice (STT + TTS) for the fleet; service_start('voice'). |
| render-svc | service | Chromium for PDF/PNG/audits; service_start('render'). |
| collab-svc | service | GTeams collaborative editor (Yjs). |
| hyperframes-svc | service | HyperFrames video rendering. |
| claude-worker | internal | Fleet worker (Claude). Created by the platform. |
| codex-worker | internal | Fleet worker (Codex). Created by the platform. |
For one-off snippets without persistent state, use sbx.runCode(code, { lang }).
Building agents with eve? @easybits.cloud/eve-sandbox turns
these boxes into eve's SandboxBackend.
Fleet Agents
Elastic fleet agents route conversations to ephemeral workers (WhatsApp, WABA,
web). Create/list/delete authenticate with the client credential (a user OAuth JWT
with WRITE scope); every config and message call takes the per-agent token
returned by create() — persist { id, token } and reuse the token as the
second argument. One client instance can configure many agents.
// Lifecycle — auth = client credential (pass the user JWT as apiKey)
const eb = new EasybitsClient({ apiKey: userJwt });
const { fleetAgent } = await eb.fleet.create({ name: "Tania", systemPrompt: "...", model: "claude-sonnet-5" });
const { id, token } = fleetAgent; // persist BOTH
// Pick a non-default engine (DeepSeek/Codex/…). Some engines need a provider
// secret — set it right after with setSecret (e.g. DEEPSEEK_API_KEY).
const ds = await eb.fleet.create({ engine: "deepseek", name: "Vendedor", systemPrompt: "..." });
await eb.fleet.setSecret(ds.fleetAgent.id, ds.fleetAgent.token, { name: "DEEPSEEK_API_KEY", value: "sk-..." });
await eb.fleet.list(); // { pools: [...] }
await eb.fleet.delete(id);
// Read config — auth = per-agent token
const caps = await eb.fleet.getCapabilities(id, token);
caps.agent.model; // "claude-sonnet-5"
caps.agent.effort; // "medium"
caps.skills; // [{ id, name, enabled, ... }]
// Agent-level config
await eb.fleet.setName(id, token, "Tania");
await eb.fleet.setAgentPrompt(id, token, "New base instructions...");
await eb.fleet.setModel(id, token, "claude-opus-4-8");
await eb.fleet.setEffort(id, token, "high"); // low|medium|high|xhigh|max
await eb.fleet.toggleOwnNumber(id, token, true);
await eb.fleet.addMcp(id, token, { name: "stripe", pkg: "@stripe/mcp", requiredSecret: "STRIPE_KEY" });
await eb.fleet.toggleSkill(id, token, { skillId: "abc", on: true });
// Per-channel config (groupId; "*" = the agent's default)
await eb.fleet.setGroupPrompt(id, token, "*", "Extra per-channel instructions");
await eb.fleet.setToolGroup(id, token, "*", { buckets: ["documentos", "db", "db-write"] });
await eb.fleet.setCapLevel(id, token, "*", { cap: "denik", level: "write" });
// Messaging
const { reply } = await eb.fleet.message(id, token, { groupId: "web-123", text: "Hola" });
// WhatsApp (Baileys) connection — auth = client credential (owner), NOT the per-agent token.
// Link a PERSONAL number (never a Business/WABA number). Omit pairingPhone → QR; pass it → code.
await eb.fleet.connect(id); // then poll:
const { baileys } = await eb.fleet.connectionState(id); // { status, qr?, pairingCode?, pairBlockedUntil? }
// status: qr_pending | pairing | connecting | connected | failed | disconnected. Poll ~2.5s.
const { groups } = await eb.fleet.listGroups(id); // [{ groupId, subject, enabled, isMain }] (on-demand)
await eb.fleet.toggleGroup(id, groups[0].groupId, true); // answer here
await eb.fleet.setMain(id, groups[0].groupId); // admin/main channel
await eb.fleet.disconnect(id);getCapabilities returns the full catalog + current state (builtins, capabilities,
buckets, bucketTools, models, skills, per-group config). See the type
FleetCapabilities for the shape.
MCP Integration
For AI agents, the same sandboxes are available as MCP tools (the agent calls them itself — no code needed):
npx -y @easybits.cloud/mcpLicense
MIT
