npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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

Quick 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-sandbox uses for eve's prewarm.
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 live

Unused 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 restored

Templates

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

License

MIT