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

@kanyun-ai-infra/sandbox

v0.3.8

Published

TypeScript SDK for Kanyun Sandbox v2

Readme

@kanyun-ai-infra/sandbox

TypeScript SDK for the Kanyun Sandbox v2 control plane.

Install

npm install @kanyun-ai-infra/sandbox

Install a specific release:

npm install @kanyun-ai-infra/[email protected]

Quick Start

import { Client } from "@kanyun-ai-infra/sandbox";

const client = new Client({
  controlPlaneUrl: "http://127.0.0.1:8080",
  apiKey: "kanyun_xxx",
});

// Create a sandbox from an existing template
const handle = await client.createSandbox({
  templateName: "my-devbox",
  ttlSeconds: 3600,
});

console.log(`Sandbox: ${handle.info.claimName}`);
console.log(`Status:  ${handle.info.status}`);

// When done
await handle.destroy();

Typical Workflow

A common usage flow looks like this:

准备镜像 → 查看可用资源 → 创建模板 → (可选) 创建预热池 → 创建 Sandbox → 使用 → 销毁

Step 1: Explore Available Resources

Before creating resources, check what clusters and runtime profiles are available:

// List clusters
const clusters = await client.listClusters();
for (const c of clusters) {
  console.log(`  id=${c.id}, name=${c.displayName}, status=${c.status}`);
}

// List runtime profiles (defines where sandboxes run)
const profiles = await client.listRuntimeProfiles();
for (const p of profiles) {
  console.log(`  id=${p.id}, cluster=${p.clusterId}, ns=${p.namespace}`);
}

Step 2: Create a Template

You need a container image first (built by yourself or via the platform's image build system). Then create a template that references it.

Before creating a template, you need to know which runtime profile to use. A runtime profile defines which cluster and namespace your sandboxes will run in:

// List available runtime profiles
const profiles = await client.listRuntimeProfiles();
for (const p of profiles) {
  console.log(`  id=${p.id}, cluster=${p.clusterId}, ns=${p.namespace}`);
}

// Use the first available profile
const runtimeProfileId = profiles[0].id;

Then create the template:

const template = await client.applyTemplate("my-devbox", {
  runtimeProfileId,
  displayName: "My Dev Box",
  description: "Custom development environment",
  defaultTTLSeconds: 3600,
  maxTTLSeconds: 86400,
  enabled: true,
  podTemplate: {
    spec: {
      containers: [{
        name: "main",
        image: "registry.example.com/my-org/my-devbox:latest",
        resources: {
          requests: { cpu: "1", memory: "2Gi" },
          limits: { cpu: "2", memory: "4Gi" },
        },
      }],
    },
  },
  moxt: {
    enabled: true,
    apiKey: "moxt_api_key",
  },
});
console.log(`Template created: ${template.name}`);

When Moxt mount is enabled, the platform injects the sidecar and the main container can access the mounted content at /moxt/data.

Step 3: Create a Warm Pool (Optional)

If you need sandboxes to start quickly, pre-warm a pool of standby instances:

const warmPool = await client.applyWarmPool("my-devbox-pool", {
  templateName: "my-devbox",
  desiredReplicas: 3,
});
console.log(`Warm pool: ${warmPool.name}, ready: ${warmPool.readyReplicas}/${warmPool.desiredReplicas}`);

Step 4: Create a Sandbox

const handle = await client.createSandbox({
  templateName: "my-devbox",
  ttlSeconds: 3600,
  metadata: { user: "alice", purpose: "code-review" },
});

console.log(`Sandbox: ${handle.info.claimName}`);
console.log(`IP:      ${handle.info.ip}`);
console.log(`Expires: ${handle.info.expiresAt}`);

If you did not create a warm pool, the sandbox starts cold (Pod is created on-demand). You may need to wait for it to become Running:

const handle = await client.createSandbox({
  templateName: "my-devbox",
  ttlSeconds: 3600,
});

// Cold start: poll until Running
while (handle.info.status.toLowerCase() !== "running") {
  await new Promise((r) => setTimeout(r, 3000));
  await handle.refresh();
  console.log(`  status=${handle.info.status}`);
}

console.log(`Sandbox ready! IP: ${handle.info.ip}`);

With a warm pool, createSandbox typically returns immediately with status=Running since a pre-warmed Pod is adopted instantly.

Step 5: Manage Sandbox Lifecycle

// Extend TTL by another 30 minutes
await handle.extend(1800);

// Refresh to get latest status
await handle.refresh();
console.log(`Status: ${handle.info.status}`);

// Get detailed info (includes metadata)
const detail = await handle.detail();
console.log(`Metadata:`, detail.metadata);

// List events
const events = await handle.events();
for (const event of events) {
  console.log(`  ${event.eventType} at ${event.occurredAt}`);
}

// Destroy when done
await handle.destroy();

Client Options

const client = new Client({
  controlPlaneUrl: "http://127.0.0.1:8080",
  apiKey: "kanyun_xxx",
  timeoutMs: 15_000,       // HTTP timeout (default: 15s)
});

API Reference

Sandbox

| Method | Description | |--------|-------------| | createSandbox(request) | Create a sandbox, returns SandboxHandle | | getSandbox(id) | Get sandbox info by ID | | listSandboxes() | List all running sandboxes | | getSandboxDetail(id) | Get detailed sandbox info (includes metadata) | | listSandboxEvents(id) | List sandbox lifecycle events | | extendSandbox(id, ttlSeconds) | Extend a running sandbox's TTL | | deleteSandbox(id) | Delete a sandbox | | connectSandbox(id) | Connect to an existing sandbox, returns SandboxHandle |

Template

| Method | Description | |--------|-------------| | listTemplates() | List all templates | | getTemplate(name) | Get template by name | | applyTemplate(name, request) | Create or update a template | | deleteTemplate(name) | Delete a template | | listTemplateMaterializations(name) | List template materializations across clusters | | resyncTemplateMaterialization(name) | Trigger re-sync of a template materialization |

Warm Pool

| Method | Description | |--------|-------------| | listWarmPools() | List all warm pools | | getWarmPool(name) | Get warm pool by name | | applyWarmPool(name, request) | Create or update a warm pool | | deleteWarmPool(name) | Delete a warm pool |

SandboxHandle

| Method | Description | |--------|-------------| | handle.info | Current SandboxInfo snapshot | | handle.daemon | Direct-connect daemon client for process, file, git, and info APIs | | handle.extend(ttlSeconds) | Extend sandbox TTL from now | | handle.refreshActivity() | Explicitly refresh sandbox activity while using direct-connect daemon APIs | | handle.refresh() | Refresh sandbox info from server | | handle.detail() | Get sandbox detail | | handle.events() | List sandbox events | | handle.destroy() | Delete the sandbox |

Direct-Connect Daemon

When daemon injection is enabled, sandbox responses include daemonEndpoint and daemonAuthToken. The SDK wraps those credentials in handle.daemon, so callers do not need to build daemon URLs or authorization headers by hand.

Since 0.3.6, handle.daemon covers:

  • info: version(), health(), workDir()
  • process: one-shot command execution plus persistent shell sessions
  • files: list/info/download/upload, binary-safe Uint8Array/Buffer/Blob upload, bulk upload/download, folder/delete/move/search/find/replace/permissions
  • git: clone/status/add/commit/push/pull

In 0.3.7, streamSessionCommandLogs() adds true chunk-by-chunk log following for asynchronous session commands.

Prefer await handle.daemon.info.workDir() over hard-coding /workspace; the control plane can override the daemon work directory with KANYUN_TOOLBOX_DAEMON_WORK_DIR.

const handle = await client.createSandbox({
  templateName: "aio-devbox",
  ttlSeconds: 3600,
});

const workDir = await handle.daemon.info.workDir();
const result = await handle.daemon.process.executeCommand({
  command: "pwd && whoami",
  cwd: workDir,
  timeout: 10,
});

console.log(result.stdout);

await handle.daemon.files.upload(`${workDir}/hello.txt`, "Hello from SDK!\n");
await handle.daemon.files.upload(`${workDir}/blob.bin`, new Uint8Array([0, 1, 2, 255]));
await handle.daemon.files.bulkUpload([
  { path: `${workDir}/a.txt`, content: "alpha" },
  { path: `${workDir}/b.txt`, content: new Blob(["beta"]) },
]);
const downloaded = await handle.daemon.files.bulkDownload([`${workDir}/a.txt`, `${workDir}/b.txt`]);
console.log(await downloaded.get(`${workDir}/a.txt`)?.text());
const files = await handle.daemon.files.list(workDir);
console.log(files.map((file) => file.name));

const status = await handle.daemon.git.status(workDir);
console.log(status.branch);

// Direct daemon calls do not pass through the control plane, so refresh activity explicitly.
await handle.refreshActivity();

Persistent sessions keep shell state between commands:

const session = await handle.daemon.process.createSession({ cwd: workDir });
await handle.daemon.process.executeInSession(session.id!, {
  command: "export DEMO=direct && mkdir -p demo",
});
const asyncCommand = await handle.daemon.process.executeInSession(session.id!, {
  command: "printf \"$DEMO\\n\" && pwd",
  async: true,
});
if (!("commandId" in asyncCommand) || !asyncCommand.commandId) {
  throw new Error("async command did not return a command id");
}
for await (const chunk of handle.daemon.process.streamSessionCommandLogs(session.id!, asyncCommand.commandId)) {
  process.stdout.write(chunk);
}
await handle.daemon.process.deleteSession(session.id!);

handle.daemon throws SandboxDaemonUnavailableError when the sandbox does not have daemonEndpoint or daemonAuthToken. Create the sandbox with daemon injection enabled and wait until it is running before using protected daemon APIs.

Session

const session = await client.newSession({
  templateName: "my-devbox",
  ttlSeconds: 600,
});
try {
  console.log(session.info.ip);
} finally {
  await session.close(); // sandbox auto-destroyed
}

Or with a callback:

const result = await client.runSession(
  { templateName: "my-devbox", ttlSeconds: 600 },
  async (session) => {
    return session.info.ip;
  },
);

History

| Method | Description | |--------|-------------| | listSandboxHistory(params) | Query sandbox history (paginated) | | getSandboxHistory(id) | Get historical sandbox detail with events |

Platform Profiles

| Method | Description | |--------|-------------| | listClusters() / getCluster(id) / createCluster(req) / upsertCluster(id, req) / deleteCluster(id) | Cluster management | | listRuntimeProfiles() / getRuntimeProfile(id) / createRuntimeProfile(req) / upsertRuntimeProfile(id, req) / deleteRuntimeProfile(id) | Runtime profile management | | listBuildProfiles() / getBuildProfile(id) / createBuildProfile(req) / upsertBuildProfile(id, req) / deleteBuildProfile(id) | Build profile management | | listRegistryProfiles() / getRegistryProfile(id) / createRegistryProfile(req) / upsertRegistryProfile(id, req) / deleteRegistryProfile(id) | Registry profile management | | listSecretProfiles() / getSecretProfile(id) / createSecretProfile(req) / upsertSecretProfile(id, req) / deleteSecretProfile(id) | Secret profile management | | listSecretMaterializations(profileId) / resyncSecretMaterialization(profileId, matId) / rotateSecretProfile(profileId, req) | Secret materialization |

Image Build

| Method | Description | |--------|-------------| | createImageBuild(req) / listImageBuilds() / getImageBuild(id) | Image build management | | listImageBuildLogs(buildId) / cancelImageBuild(buildId) | Build logs and cancellation | | listImageAssets() / createImageAsset(req) / getImageAsset(id) / upsertImageAsset(id, req) / deleteImageAsset(id) | Reusable image assets |

Error Handling

| Error | Cause | |-------|-------| | ValidationError | Invalid request (400) | | AuthenticationError | API key invalid (401) | | NotFoundError | Resource not found (404) | | ConflictError | State conflict (409) | | APIError | Other control-plane errors | | SandboxDaemonUnavailableError | .daemon accessed without daemon direct-connect credentials |

Authentication

All control-plane requests use X-API-Key header.