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

@kortix/froglet-sdk

v0.1.0

Published

TypeScript client for the Froglet API.

Downloads

80

Readme

@kortix/froglet-sdk — TypeScript SDK

Programmatic access to the Froglet API. Boot sandboxes, exec commands, manage templates, attach volumes, run backups — all from Node, Bun, Deno, or the browser. Node 18+ or anywhere fetch is global; no Node-specific imports.

It mirrors the @kortix/froglet-cli command-line tool one-for-one. If the CLI has a command, the SDK has the method.

Install

npm  install @kortix/froglet-sdk
pnpm add @kortix/froglet-sdk

Quickstart

import { FrogletClient } from '@kortix/froglet-sdk';

const froglet = new FrogletClient({
  apiKey: process.env.FROGLET_API_KEY,
  // baseUrl: defaults to https://api.froglet.sh
});

// 1. boot a sandbox
const created = await froglet.sandboxes.create({
  templateSlug: 'ubuntu-24.04',
  size: 'xs',
  slug: 'demo',
});

// 2. grab a handle with the id pre-bound
const sb = froglet.sandbox(created.id);
await sb.waitForState('running');

// 3. run something inside it
const result = await sb.exec('uname -a');
console.log(result.stdout);

// 4. tear it down
await sb.destroy();

You don't have to use the handle — every method is also available flat on the resource (froglet.sandboxes.exec(id, 'uname -a')). The handle just removes the repetition when you're doing several things with the same sandbox.

Client options

new FrogletClient({
  apiKey: 'fl_…', // required
  baseUrl: 'https://api.froglet.sh', // default
  fetch: globalThis.fetch, // override for testing
  userAgent: '@kortix/froglet-sdk/0.1.0', // sent on every request
  timeoutMs: 60_000, // per-request timeout
});

For a session-cookie auth flow (i.e. from the browser, talking to your own backend), pass fetch: customFetch that forwards the cookie. The SDK doesn't care how Authorization gets onto the request as long as the response shape is { data: T } | { error: { code, message } }.

Concepts in 60 seconds

  • Template — a rootfs image. Every workspace gets ubuntu-24.04 for free; build your own from any Docker image with templateBuilds.create.
  • Sandbox — one running VM. Boots from a template; has its own IP, CPU, memory, disk. Identified by a UUID; the short slug shown in sandboxes.list (hbuh, smwlk) is just a friendlier alias.
  • Volume — persistent storage that outlives sandboxes. Attached at a mount path, read-write or read-only.
  • Backup — R2-uploaded full sandbox snapshot (VM RAM + dirty disk blocks). Restoring drops the sandbox back to the exact instant the backup was taken. Different from sandboxes.snapshot, which is a fast in-place checkpoint that is not uploaded anywhere.
  • Port — a public HTTPS route to an internal port inside a sandbox (e.g. https://web--demo.froglet.sh → :8080 in the guest).
  • SSH access — a workspace-scoped JWT for the bastion at ssh.froglet.sh:2222.

Recipes

Build a template from a Docker image, then use it

const build = await froglet.templateBuilds.create({
  name: 'acme-app',
  slug: 'acme-app',
  sourceKind: 'image',
  sourceRef: 'ghcr.io/acme/app:1.2',
});

// poll for state=ready (or hit the SSE stream)
let row;
do {
  await new Promise((r) => setTimeout(r, 5_000));
  const builds = await froglet.templateBuilds.list();
  row = builds.find((b) => b.id === build.id);
} while (row && row.state !== 'ready' && row.state !== 'error');

if (row?.state === 'ready') {
  const sb = await froglet.sandboxes.create({ templateSlug: 'acme-app', size: 's' });
  await froglet.sandboxes.waitForState(sb.id, 'running');
}

Exec with custom cwd + stdin

const r = await froglet.sandboxes.exec(
  sb.id,
  'python -c "import sys; print(len(sys.stdin.read()))"',
  {
    cwd: '/tmp',
    stdin: 'hello\nworld\n',
    timeoutMs: 30_000,
  },
);
console.log(r.exit_code, r.stdout, r.stderr);

Upload + download a file

await froglet.sandboxes.uploadFile(
  sb.id,
  '/data/input.txt',
  new TextEncoder().encode('hello from node'),
);

const bytes = await froglet.sandboxes.downloadFile(sb.id, '/data/output.txt');
const text = new TextDecoder().decode(bytes);

Expose a port

const port = await froglet.ports.create(sb.id, {
  internalPort: 8080,
  label: 'web', // public route lives at https://web--<slug>.froglet.sh
  // public: false       // gated route (token required) — defaults to true
  // ttlSec: 600         // auto-revoke after 10 min
});
console.log(port.url);

For a one-time share link:

const { token, expires_at } = await froglet.sandboxes.mintPortShareToken(sb.id, port.id, 300);

SSH into a sandbox

const access = await froglet.sshAccess.create(sb.id, { name: 'laptop', ttlHours: 4 });
console.log(access.command);
//   ssh -p 2222 <jwt>@ssh.froglet.sh

Snapshot, mutate, restore

const backup = await froglet.sandboxes.backup(sb.id, 'before-experiment');

// ... break things ...

const job = await froglet.sandboxes.restore(sb.id, backup.id);
// state=in_progress; poll listRestoreJobs to track

Mount a persistent volume

const vol = await froglet.volumes.create({ name: 'shared-data', size_gb: 25 });

await froglet.volumes.attach(sb.id, {
  volume_id: vol.id,
  mount_path: '/data',
  // mode: 'ro' for read-only
});

// later, on a different sandbox:
await froglet.volumes.attach(otherSb.id, { volume_id: vol.id, mount_path: '/data' });

Wait for a sandbox to reach a state

await froglet.sandboxes.waitForState(sb.id, 'running', { timeoutMs: 30_000 });

Stream a PTY into the browser

const ticket = await froglet.sandboxes.ptyTicket(sb.id);
const ws = new WebSocket(ticket.url); // wss://…?ticket=<jwt>
ws.onmessage = (ev) => term.write(ev.data);
term.onData((data) => ws.send(data));

Working with a single sandbox

client.sandbox(id) returns a SandboxHandle whose methods are pre-bound to that id. Everything you can do to a sandbox is on the handle:

const sb = froglet.sandbox(sandboxId);

// status + lifecycle
const { sandbox, events } = await sb.fetch();
await sb.start();
await sb.stop();
await sb.destroy();
await sb.waitForState('running');

// exec + files
const r = await sb.exec('uname -a');
await sb.uploadFile('/tmp/x', new TextEncoder().encode('hi'));
const bytes = await sb.downloadFile('/tmp/x');
await sb.listFiles('/etc');
const tkt = await sb.ptyTicket();

// snapshots + backups
await sb.snapshot('checkpoint');
const b = await sb.backup('nightly');
await sb.listBackups();
await sb.restore(b.id);
await sb.setBackupPolicy({
  enabled: true,
  intervalMinutes: 60,
  retainCount: 3,
  retainDays: 7,
  storage: 'r2',
});

// ports + ssh access
const port = await sb.exposePort({ internalPort: 8080, label: 'web' });
await sb.revokePort(port.id);
const t = await sb.mintPortShareToken(port.id, 300);
const access = await sb.createSshAccess({ name: 'laptop', ttlHours: 4 });
await sb.revokeSshAccess(access.id);

// volumes
await sb.attachVolume({ volume_id: vol.id, mount_path: '/data' });
await sb.attachedVolumes();
await sb.detachVolume(vol.id);

The handle is purely a convenience — it forwards to the same client.sandboxes, client.ports, client.sshAccess, and client.volumes resources. Use whichever style fits the call site better.

API surface

Top-level namespaces on FrogletClient:

| Namespace | What it covers | | ---------------- | ------------------------------------------------------------------------- | | me | Who am I (workspace, role, scopes). | | sandboxes | Lifecycle, exec, files, PTY, snapshots, backups, restore. | | templates | Catalog + publish/unpublish/delete. | | templateBuilds | Build templates from Docker images. | | ports | Public routes per sandbox. | | sshAccess | SSH bastion tokens per sandbox. | | volumes | Persistent storage + attach/detach/search. | | keys | Workspace API keys. | | members | Workspace member management. | | quotas | Usage + limits + raise-quota requests. | | admin | Platform-admin operations (hosts, orders, cells). Workspace keys get 403. |

Each namespace returns plain TypeScript objects whose shapes are exported alongside the resource (e.g. Sandbox, Backup, BackupPolicy, SandboxPort, Volume, Member, ApiKey). Import them when you need to annotate variables:

import type { Sandbox, ExecResult, Backup } from '@kortix/froglet-sdk';

Errors

Every request returns the unwrapped data field on success and throws FrogletApiError on 4xx/5xx with the structured body:

import { FrogletApiError } from '@kortix/froglet-sdk';

try {
  await froglet.sandboxes.create({ templateSlug: 'unknown', size: 'm' });
} catch (e) {
  if (e instanceof FrogletApiError) {
    console.log(e.status, e.code, e.message);
    // 404 not_found "template 'unknown' not found"
  }
  throw e;
}

Common codes:

| Status | Code | Meaning | | ------ | ------------------------- | ------------------------------------------------------ | | 401 | unauthorized | Missing / wrong API key. | | 403 | forbidden_scope | Token doesn't carry the scope this route needs. | | 403 | platform_admin_required | You tried an admin/* route with a workspace key. | | 404 | not_found | Resource doesn't exist or your token can't see it. | | 429 | quota_exceeded | Workspace at the limit. Inspect error.payload. | | 422 | backup_failed | Backup pipeline rejected the request (e.g. host loss). |

Browser usage

The SDK has no Node-specific deps — globalThis.fetch, Uint8Array, and a small structured-clone-safe envelope decoder are the only platform surfaces. The one caveat: don't put your raw API key in a browser bundle. Either use a session cookie + your own fetch wrapper, or talk to a Vercel/Cloudflare Worker function that forwards requests with the key server-side.

Gotchas

  • sandboxes.create takes slug, not name — the field that becomes the short human alias.
  • sandboxes.exec and sandboxes.listFiles return snake_case fields (exit_code, modified_ms) — the rest of the API uses camelCase. Already reflected in the SDK types.
  • volumes.create, volumes.attach, volumes.detach use snake_case input (size_gb, volume_id, mount_path). The SDK input types match.
  • sandboxes.stop can exceed 60s because the host takes an auto-snapshot before suspending. For hot sandboxes, bump the client timeout: new FrogletClient({ ..., timeoutMs: 180_000 }).
  • volumes.search returns 503 on prod today — the API process is missing FROGLET_VOLUMED_URL env vars. Operator-side fix.
  • templateBuilds.list may return state 'ready' (template usable) or 'error'. 'done' is the in-between state after the build finishes but before the template row is created — usually transient.

Compared to the CLI

| What you want | CLI | SDK | | ------------------ | -------------------------------------------------------------- | ------------------------------------- | ------------------------------ | | Try things by hand | froglet sb new --template ubuntu-24.04 --size xs --name demo | not the right tool | | Script lifecycle | bash + --json | jq | client.sandboxes.create(...) | | Build into an app | doesn't fit | the whole point | | Snapshot ID lookup | shown short in ls, full in --json | always full UUIDs in returns | | Slug vs UUID | accepts either (resolves via list) | use UUIDs returned by list/create |

Both ship one-for-one — the SDK is what @kortix/froglet-cli shells out to conceptually, and the dashboard uses the same client.

Source layout

clients/sdk/typescript/
  src/
    client.ts             FrogletClient + options
    core/
      envelope.ts         { data: T } | { error: { code, message } } unwrap
      errors.ts           FrogletApiError
      http.ts             HttpClient (Bearer, JSON, timeout, abort)
    resources/
      sandboxes/          everything per-sandbox: lifecycle, exec, files, PTY, backups
      templates/
      template-builds/
      ports/
      ssh-access/
      volumes/
      keys/
      members/
      me/
      quotas/
      admin/              hosts/orders/cells (platform-admin only)
    index.ts              barrel — re-exports every resource + types
    version.ts            VERSION string baked into the User-Agent
  tests/
    client.test.ts        unit tests for HttpClient + envelope
    resources.test.ts     unit tests for every resource (mocked fetch)

Unit tests (pnpm test) run in <1s with mocked fetch.