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

@insforge/instabox-sdk

v0.5.0

Published

TypeScript SDK for instabox: create sandboxes on Firecracker dev machines, run commands, move files, pause, resume, snapshot and SSH in.

Readme

@insforge/instabox-sdk

TypeScript SDK for InstaBox: create sandboxes on Firecracker dev machines, run commands in them, read and write files, pause and resume them, save and restore snapshots and SSH in.

  • Zero runtime dependencies. Uses the global fetch (Node 18+, Bun, Deno).
  • ESM and CommonJS builds with type declarations.

Install

npm install @insforge/instabox-sdk

Authentication

Create an API key on the API Keys page of the instabox dashboard (or instabox keys create in the CLI), then either set it in the environment:

export INSTABOX_API_KEY=...
export INSTABOX_API_URL=https://api.instabox.sh   # optional, this is the default

or pass it in code (options win over env vars):

const sandbox = await Sandbox.create('universal', { apiKey: '...', apiUrl: 'https://...' })

The key is sent as the X-API-Key header.

Scopes

| Scope | Allows | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | machines:read | Sandbox.list, getInfo, isRunning, vault | | machines:write | create, connect (resume), pause, resume, stop, setTimeout, setMetadata, disableVault, pushVault, commands, files, sshCredentials | | machines:delete | kill | | images:read | Resolve private snapshot names, listSnapshots, getSnapshot | | images:write | createSnapshot | | images:delete | deleteSnapshot |

:write and :delete imply :read. Creating from a template or a snapshot ID needs only machine scopes. Creating from a snapshot name also needs images:read. Snapshot permissions are named images:* by the current API. Add images:write and images:delete if your code saves and deletes snapshots. A call outside the key's scopes throws InstaboxAuthError (403 insufficient_scope).

Quickstart

import { Sandbox } from '@insforge/instabox-sdk'

const sandbox = await Sandbox.create() // agent-browser
const r = await sandbox.commands.run('node -v')
console.log(r.stdout)
await sandbox.kill()

Sandbox.create(template?, opts?) takes a template ID (universal), a private snapshot name (my-setup) or a snapshot ID (sn_...). It defaults to agent-browser (universal with a desktop and browser preinstalled, which the dashboard's Computer tab shows; universal on a server without it). Template IDs take precedence over snapshot names. An unknown name or an ambiguous snapshot name throws before creating a machine; use an ID to select a specific snapshot.

const sandbox = await Sandbox.create('universal', {
  name: 'my-box', // default sbx-<random>; lowercase letters, digits and '-'
  envs: { NODE_ENV: 'development' }, // applied to every commands.run of this object
  metadata: { userId: 'u_42' }, // labels stored with the sandbox (plaintext: not for secrets)
  vault: false, // for a sandbox made for someone else: none of the account's Vault items
  timeoutMs: 30 * 60_000, // server pauses it 30 min from now (60 s to 24 h)
  archiveMs: 24 * 60 * 60_000, // stops it at this deadline, independent of timeout
  onTimeout: 'pause', // or 'kill' to delete it instead
  autoSuspendMs: 10 * 60_000, // pauses it after 10 idle minutes (default 30; 0: never)
})
console.log(sandbox.sandboxId) // m_...

Sizes are fixed by the service: 4 vCPU, 8 GiB memory, a 16 GiB root disk and a 20 GiB /data disk. The vcpus, memMiB, diskGiB and rootDiskGiB options are for other deployments; api.instabox.sh refuses them (400 spec_not_allowed).

create waits for the create operation to succeed. getInfo().machine.runtime.ready reports guest readiness. archiveMs is optional (60 seconds to 24 hours) and becomes getInfo().archiveAt; its deadline is independent of the pause/kill timeout.

Commands

const r = await sandbox.commands.run('npm test', {
  cwd: '/data/home/app',
  stdin: '',
  envs: { CI: '1' },
  timeoutMs: 120_000,
  onStdout: (out) => process.stdout.write(out),
  onStderr: (err) => process.stderr.write(err),
})
// r: { stdout, stderr, exitCode, timedOut, truncated }

A non-zero exit throws CommandExitError, which carries stdout, stderr and exitCode:

import { CommandExitError } from '@insforge/instabox-sdk'

try {
  await sandbox.commands.run('exit 3')
} catch (e) {
  if (e instanceof CommandExitError) console.log(e.exitCode, e.stderr)
}

The API returns timedOut and truncated flags. A command that runs past timeoutMs throws CommandExitError with timedOut: true and exitCode 124. Each output stream is limited to 1 MiB, and stdin to 1 MiB (UTF-8; a longer one throws before the request). Command timeouts are 1–600 seconds; the default HTTP request deadline allows an additional 30 seconds for the result. An explicit requestTimeoutMs overrides that default. Output is not streamed: onStdout and onStderr are called once with the full output after the command finishes.

background: true starts the command detached, in its own process group, and returns { pid, kill() } right away. kill() sends SIGTERM to the command and everything it started; it returns false if they had all exited. Background commands need setsid in the sandbox.

const server = await sandbox.commands.run('node server.js', { background: true })
await server.kill()

Files

await sandbox.files.write('/data/home/app.js', 'console.log(1)') // string or Uint8Array -> { path }
const text = await sandbox.files.read('/data/home/app.js') // string
const bytes = await sandbox.files.read('/data/home/logo.png', { format: 'bytes' })
const entries = await sandbox.files.list('/data/home') // [{ name, path, type: 'file' | 'dir' }]
await sandbox.files.exists('/data/home/app.js') // boolean
await sandbox.files.remove('/data/home/tmp') // files or directories

Files go over the command channel as base64 in bounded chunks. Each read or write is capped at 10 MiB. Truncated file output raises an error. write creates parent directories and replaces the file atomically; writing to a path that is a directory throws InstaboxError with code is_a_directory. Relative paths are relative to HOME. HOME is /data/home, on the persistent disk.

Lifecycle

await sandbox.pause() // snapshot memory and suspend
await sandbox.resume() // restore the snapshot (or start a stopped machine)
const again = await Sandbox.connect(id) // attach; resumes a paused or stopped sandbox
const noWake = await Sandbox.connect(id, { resume: false })

const info = await sandbox.getInfo() // { sandboxId, templateId, snapshotId?, name, metadata, vault, startedAt, endAt?, archiveAt?, onTimeout, autoSuspendMs?, state, machine }
await sandbox.isRunning() // boolean

const all = await Sandbox.list() // your sandboxes, as SandboxInfo[]
const mine = await Sandbox.list({ metadata: { userId: 'u_42' } }) // all pairs must match

await sandbox.setTimeout(10 * 60_000) // pause 10 min from now; setTimeout(0) clears it
await sandbox.setTimeout(60_000, { onTimeout: 'kill' })
await sandbox.setAutoSuspend(10 * 60_000) // pause after 10 idle minutes; 0 keeps it on
await sandbox.setMetadata({ userId: 'u_42', stage: 'review' }) // replaces it; {} clears

await sandbox.kill() // delete the machine and its disks; false if already gone
await Sandbox.kill(id)

Every lifecycle call waits for its operation to finish (default limit 10 minutes, operationTimeoutMs to change it).

A timeout is enforced by the server: at endAt it suspends the sandbox (onTimeout: 'pause', resume it with Sandbox.connect) or deletes it ('kill'). It fires once and is then cleared, so a resumed sandbox runs without a timeout until you set one again. Without timeoutMs there is no deadline.

Idle pause: a new sandbox pauses after 30 idle minutes (no agent working, no terminal use, little CPU) and resumes on the next commands.run, file read or SSH sign-in, processes and memory intact. autoSuspendMs: 0 makes a sandbox that stays on (a server that must keep answering), another value changes the idle time, setAutoSuspend(ms) changes it later and getInfo().autoSuspendMs shows it. Unlike a timeout the setting stays across pause and resume.

Vault

The Vault holds the account's secrets on the control plane (API keys for model providers, a .env, a service-account file) and writes them into every sandbox of the account when it boots or resumes: env items become environment variables of every login shell (so of commands.run too), file items become files under the home directory. Items are managed by a person, on the dashboard's Vault page or with instabox vault set; an API key cannot read, create or change them.

What an API key can do is keep a sandbox out of it, which a program creating sandboxes for other people must do: anything running in a sandbox can read what the Vault writes into it.

const tenant = await Sandbox.create('universal', { vault: false }) // never gets the account's items
await sandbox.disableVault() // for good, in any state

vault: false is for good: such a sandbox never gets items. On a sandbox that takes the Vault:

const v = await sandbox.vault() // { enabled, items: [{ name, kind, path? }], applied: { at, env, files, removed?, kept?, error? } | null }
const applied = await sandbox.pushVault() // write the current items now (the sandbox must be running)
console.log((await sandbox.getInfo()).vault) // true

An item's selector limits it to sandboxes whose metadata has those labels, so metadata at creation chooses which items a sandbox gets. A write happens at every boot, start and resume and on pushVault(); after a resume a changed env item reaches new login shells only. vault() and pushVault() throw not_found on a server without the Vault.

Snapshots

Save a sandbox's root and /data disks as a private snapshot. The sandbox may be running or stopped. From a running sandbox the snapshot comes back with status: 'pending' and turns ready once saved; Sandbox.create(snapshot.id) waits for that (it retries the server's 409 operation_conflict "still being saved", up to operationTimeoutMs). rootOnly: true saves only the root disk. Snapshot methods use /v1/snapshots and /v1/machines/{id}/snapshots.

const snapshot = await sandbox.createSnapshot({ name: 'my-setup', description: 'Prepared workspace' })
const restored = await Sandbox.create(snapshot.id)
try {
  console.log((await restored.getInfo()).snapshotId)
  console.log(await Sandbox.listSnapshots())
  console.log(await Sandbox.getSnapshot(snapshot.id))
} finally {
  await restored.kill()
  await Sandbox.deleteSnapshot(snapshot.id)
}

deleteSnapshot returns false if the snapshot is already absent. Snapshots do not save running processes; use pause/resume for memory preservation.

To copy a sandbox, save a snapshot and create from it, as above (InstaBox no longer has a fork call). The copy does not inherit the source's settings: vault: false and other Sandbox.create options apply to it as to any new sandbox, so a copy of a sandbox without the Vault needs Sandbox.create(snapshot.id, { vault: false }).

SSH

import { readFileSync, writeFileSync, appendFileSync } from 'node:fs'
import { homedir } from 'node:os'

const pub = readFileSync(`${homedir()}/.ssh/id_ed25519.pub`, 'utf8')
const ssh = await sandbox.sshCredentials(pub, { ttlSeconds: 3600 })
// { host, port, username, certificate, knownHostsLine, expiresAt, connectionId }

writeFileSync(`${homedir()}/.ssh/id_ed25519-cert.pub`, ssh.certificate + '\n')
appendFileSync(`${homedir()}/.ssh/known_hosts`, ssh.knownHostsLine + '\n')
// ssh -p <port> <username>@<host>
console.log(`ssh -p ${ssh.port} ${ssh.username}@${ssh.host}`)

The sandbox must be running. Certificates last 1 hour by default; ttlSeconds takes 1 to 86 400 (24 hours), other values throw before the request. Pausing or stopping prevents connections while the sandbox is not running; an unexpired certificate can be used again after resume or start. Deleting the sandbox or revoking the API key that issued the certificate revokes access.

Desktop: see it and control it

Sandboxes from the default agent-browser template have a desktop (Xfce and Chromium on display :1). sandbox.desktop starts it, gives an agent a mouse, a keyboard and screenshots, and makes a link people can watch it through.

await sandbox.desktop.start() // { installed, running, width: 1280, height: 800 }; a no-op when running
await sandbox.desktop.open('https://example.com')
const png = await sandbox.desktop.screenshot() // Uint8Array, PNG

await sandbox.desktop.click(640, 400) // { button: 'left' | 'right' | 'middle', double: true }
await sandbox.desktop.move(100, 200)
await sandbox.desktop.drag([100, 200], [300, 400])
await sandbox.desktop.scroll({ direction: 'down', amount: 3 })
await sandbox.desktop.type('Hello "world" ✓\n') // literal text, sent through stdin
await sandbox.desktop.press('Enter')
await sandbox.desktop.press(['ctrl', 'l']) // a chord
const { x, y } = await sandbox.desktop.cursorPosition()

// A page that shows the desktop full-screen: open it, or put it in an <iframe>.
const { url, expiresAt } = await sandbox.desktop.streamUrl({ viewOnly: true, ttlSeconds: 3600 })
await sandbox.desktop.revokeStreamUrls() // every link stops working; open viewers are disconnected

start({ width, height }) sizes the screen (800-2560 x 600-1600) of a desktop it starts; on other templates it installs the desktop first, about half a minute once. Coordinates are screen pixels from the top left. Keys take common names (Enter, Esc, Tab, Backspace, arrows, F1-F24, ctrl, alt, shift, cmd) or X keysym names. Input and screenshots run xdotool and scrot in the sandbox, so they need the machines:exec (or machines:write) scope; a failing one throws CommandExitError.

streamUrl needs no instabox account to open: anyone with the link can connect until expiresAt (60 s to 24 h, default 1 h). Expiry stops new connections; revoke ends open sessions: a viewer already watching stays until they leave or you call revokeStreamUrls(), which ends every link of the sandbox and disconnects its viewers within a few seconds. Revoke when someone's access should end, for example when a tenant's session or subscription ends. Links are view-only by default, enforced by the server; viewOnly: false lets the viewer use the mouse and keyboard. The sandbox and its desktop must be running when the link is opened.

Metrics and usage

// CPU and memory, one sample per 30 s, oldest first (default: the last 5 minutes, at most 24 h back)
const samples = await sandbox.getMetrics({ since: new Date(Date.now() - 15 * 60_000) })
// [{ timestamp: Date, cpuUsedPct: 12.5, cpuCount: 4, memUsed, memTotal, diskTotal }]

// What the sandbox used, for billing your own customers (default: this calendar month, UTC, to now)
const usage = await Sandbox.getUsage(sandbox.sandboxId, { from: new Date('2026-10-01T00:00:00Z') })
// { from, to, runningSeconds, vcpuSeconds, memGiBSeconds, diskGiBSeconds }

// Your account: counts, stored bytes (per sandbox in storage.machines, measured hourly) and limits
const account = await Sandbox.usage()

All three need machines:read (Sandbox.usage() also works with images:read).

  • Metrics are measured on the host: cpuUsedPct is CPU time over the sandbox's vCPUs (0-100), memUsed the guest memory it has touched, in bytes. diskUsed is not reported yet. The list is empty while the sandbox is not running.
  • runningSeconds counts time running, not paused or stopped; vcpuSeconds and memGiBSeconds multiply it by the sandbox's size. diskGiBSeconds is its disk size (system disk plus /data) times the time it existed in the window, paused or stopped included. Running time is recorded from the server release that added this route; earlier time is not counted. A deleted sandbox's usage stays readable by its ID.

Errors

| Class | When | | ---------------------- | ------------------------------------------------------------------------------------------------------------- | | InstaboxError | Base class. status, code, message, requestId, retryable, operationId from the API error envelope. | | InstaboxAuthError | 401 / 403: invalid or revoked API key, or no access. No API key at all: code missing_api_key, status 0. | | InstaboxQuotaError | 429 quota_exceeded, for example the machine limit. | | InstaboxTimeoutError | A request or an operation took too long. | | CommandExitError | A command exited non-zero. Carries stdout, stderr, exitCode. |

A failed operation (for example no_capacity while creating) throws InstaboxError with the operation's code and operationId.

Reads, and mutations (which always carry an Idempotency-Key), are retried up to 3 times with backoff on 502, 503, 504 and network errors. Commands send no Idempotency-Key and are not retried.

Limits and what is not there yet

  • Quotas are account-specific; inspect GET /v1/usage. Exceeding a quota throws InstaboxQuotaError.
  • The timeout is a deadline from when it was set, not an inactivity timer.
  • Command output is not streamed.

Agent skill

skills/instabox/SKILL.md teaches a coding agent to use instabox with an API key: the CLI, REST, SSH certificates and this SDK. It ships in the package: copy its skills/instabox folder into your agent's skills directory (for Claude Code, ~/.claude/skills/ or .claude/skills/ in a repo). With npm it is node_modules/@insforge/instabox-sdk/skills/instabox; with any package manager, dirname $(node -p "require.resolve('@insforge/instabox-sdk/package.json')")/skills/instabox.

Development

Use Node 22 or newer for the development tools.

npm install
npm run build
npm test
INSTABOX_LIVE=1 INSTABOX_API_KEY=... npm run test:live   # creates and deletes one real machine

Releasing

Bump version in package.json in a PR. After it merges, tag that commit and push the tag:

git tag v0.4.0 && git push origin v0.4.0

The Release workflow checks that the tag is the package's version and that it is not on npm yet, runs the checks and publishes through npm trusted publishing (OIDC): no token is stored anywhere. Once, a maintainer adds the trusted publisher on npmjs.com (the package's Settings, Trusted Publisher, GitHub Actions: organization InsForge, repository instabox-sdk, workflow release.yml, no environment).