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

@curvet/sdk

v0.4.1

Published

Official TypeScript SDK for the Curvet Unified Playground API (chat, image, and video generation across providers, one API key).

Readme

@curvet/sdk

Official TypeScript SDK for the Curvet Unified Playground API — chat, image, and video generation across multiple providers (OpenAI, Anthropic, Perplexity, DeepInfra) with one API key and one balance.

import { Curvet } from "@curvet/sdk";

const curvet = new Curvet({ appKey: process.env.CURVET_APP_KEY });

const { response } = await curvet.chat.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "Write a tagline for a freelance marketplace." }],
});
console.log(response);

Install

npm install @curvet/sdk

Requires Node 18+ (uses the built-in fetch). For older runtimes, pass a fetch implementation.

Authentication

The client accepts either or both of two credentials:

  • App Key (x-app-key) — for the Playground API (chat, image, video, audio, 3D, workflows, food, voice, balance, analytics). Get one from the Curvet Developer Portal with Playground access enabled. Env: CURVET_APP_KEY.
  • Enterprise API key (x-enterprise-key) — an org-scoped admin key for the Enterprise API (curvet.enterprise.* — members, invites, per-member credits). Env: CURVET_ENTERPRISE_KEY.

Provide whichever you need — app key only, enterprise key only, or both:

// Playground only
const curvet = new Curvet({ appKey: "cvt_app_..." });

// Enterprise only
const curvet = new Curvet({ enterpriseKey: "cvent_ent_..." });

// Both — playground + enterprise from a single client
const curvet = new Curvet({ appKey: "cvt_app_...", enterpriseKey: "cvent_ent_..." });

// …or via env (CURVET_APP_KEY / CURVET_ENTERPRISE_KEY), then: new Curvet()

The constructor throws only if neither is provided. Each credential unlocks its own methods — curvet.chat.* / media / workflows need the app key; curvet.enterprise.* needs the enterprise key. Calling a family you didn't supply a key for returns 401.

Usage

Chat

const res = await curvet.chat.create({
  model: "claude-sonnet-4-6",
  messages: [
    { role: "system", content: "You are concise." },
    { role: "user", content: "Explain vector databases in one sentence." },
  ],
  temperature: 0.7,
  maxTokens: 200,
});
console.log(res.response, res.usage.credits);

Image

const img = await curvet.image.generate({
  model: "flux-2-klein-4b",
  prompt: "An astronaut riding a unicorn on Mars, cinematic",
  size: "1024x1024", // single field — not width/height
});
console.log(img.imageUrl);

Video (async, handled for you)

Video runs as a background job. generate() submits and polls to completion — you just await it:

const video = await curvet.video.generate(
  { model: "wan-2.2", prompt: "Neon Tokyo street in the rain", mode: "text_to_video" },
  { onProgress: (pct) => console.log(`${pct}%`) },
);
console.log(video.mediaUrl);

Prefer fire-and-forget? Submit and poll yourself:

const job = await curvet.video.submit({ model: "wan-2.2", prompt: "..." });
if (job.status !== "completed") {
  const done = await curvet.jobs.handle(job.jobId!).wait();
  console.log(done.mediaUrl);
}
// ...or check once later:
const status = await curvet.jobs.retrieve(job.jobId!);

Audio & 3D (async, same as video)

const audio = await curvet.audio.generate({ model: "fish-audio", prompt: "Hello there" });
const mesh = await curvet.threeD.generate({ model: "meshy-3d", prompt: "a ceramic mug" });
console.log(audio.mediaUrl, mesh.mediaUrl);

Models, balance & analytics

const chatModels = await curvet.models.list({ type: "chat" });
const balance = await curvet.balance.get();
const analytics = await curvet.analytics.get({ startDate: "2026-01-01", endDate: "2026-02-01" });

Workflows

// JSON inputs:
const out = await curvet.workflows.run("workflowId", { inputs: { topic: "ai" } });

// With file inputs (multipart, handled for you):
await curvet.workflows.run("workflowId", {
  inputs: { caption: "hello" },
  files: { image: new Blob([bytes], { type: "image/png" }) },
});

For long workflows (video/audio/3D nodes), use the pollable API — submit and auto-poll to completion with live progress, instead of one long HTTP call:

const run = await curvet.workflows.runAndPoll(
  "workflowId",
  { inputs: { topic: "ai" } },
  { onProgress: (r) => console.log(r.status, r.progress + "%", r.currentNode?.label) },
);
console.log(run.result);

// Or fire-and-forget + poll yourself:
const { runId } = await curvet.workflows.submit("workflowId", { inputs: {} });
const status = await curvet.workflows.runs.retrieve(runId);

Food & speech-to-text

const dishes = await curvet.food.search("paneer", { limit: 5 });
const stt = await curvet.voice.stt({ audio: audioBytes, filename: "clip.wav" });
console.log(stt.text);

Enterprise (admin)

For enterprise customers: manage your organization with an enterprise key — invite members, set per-member credit allotments funded from your org pool, and read your dashboard. Requires enterpriseKey (org-scoped; sent as x-enterprise-key).

const curvet = new Curvet({ enterpriseKey: process.env.CURVET_ENTERPRISE_KEY });

// Dashboard: pool balance, seats, per-member usage
const overview = await curvet.enterprise.overview();
console.log(overview.pool.balance, overview.memberCount);

// Invite a teammate — single-use link; the allotment is reserved from your
// pool when they sign up, and they see only their own credits (never the pool).
const { url } = await curvet.enterprise.invites.create({
  allottedCredits: 1000,
  role: "member",      // or "admin"
  expiresInDays: 30,
});
await curvet.enterprise.invites.list();
await curvet.enterprise.invites.revoke(inviteId);

// Members
const members = await curvet.enterprise.members.list();
await curvet.enterprise.members.assignCredits(uid, 500);  // +assign / −reclaim (from pool)
await curvet.enterprise.members.setLimit(uid, 2000);      // monthly cap (0 = uncapped)
await curvet.enterprise.members.setRole(uid, "admin");
await curvet.enterprise.members.remove(uid);              // reclaims their credits

Assigning more than the pool holds throws InsufficientBalanceError (402) — top up the pool and retry.

Errors

Every failure throws a typed subclass of CurvetError:

import { InsufficientBalanceError, RateLimitError, AuthError } from "@curvet/sdk";

try {
  await curvet.image.generate({ model: "flux-2-klein-4b", prompt: "..." });
} catch (err) {
  if (err instanceof InsufficientBalanceError) {
    console.error(`Need ${err.required}, have ${err.available}`);
  } else if (err instanceof RateLimitError) {
    console.error(`Rate limited; resets at ${err.resetsAt}`);
  } else if (err instanceof AuthError) {
    console.error("Bad app key");
  } else {
    throw err;
  }
}

| Class | When | |---|---| | AuthError | 401 — missing/invalid app key or enterprise key | | PermissionError | 403 — app inactive, playground disabled, model/category not allowed | | BadRequestError | 400 — invalid/unknown model or payload | | InsufficientBalanceError | 402 — not enough credits (.required, .available) | | RateLimitError | 429 — rate/cost cap (.kind, .resetsAt, .retryAfterMs) | | NotFoundError | 404 — job/workflow not found | | APIError | 5xx — upstream error | | ConnectionError | network failure / timeout | | JobFailedError | async media job failed (.jobId) | | JobTimeoutError | async job exceeded poll timeout (.jobId) |

429 and 5xx (and pre-response network errors) are retried automatically with exponential backoff + jitter, respecting rate-limit resets. 4xx are never retried.

Configuration

new Curvet({
  appKey: "cvt_app_...",
  enterpriseKey: "cvent_ent_...", // optional — enables curvet.enterprise.*
  baseURL: "https://curvet.ai/api/v1/playground", // override for staging
  timeout: 60_000,            // per-request, ms
  maxRetries: 2,
  defaultPollIntervalMs: 2500, // async job polling
  defaultPollTimeoutMs: 180_000,
  fetch: customFetch,          // for Node < 18 / testing
});

Per-call overrides: every method accepts a final options arg ({ signal, timeout, maxRetries }; media methods also take { pollIntervalMs, pollTimeoutMs, onProgress }).

Development

npm install
npm run typecheck
npm test        # unit tests (mocked HTTP) — no network, no credits
npm run build   # ESM + CJS + d.ts via tsup

# live contract test (spends a few credits):
CURVET_TEST_APP_KEY=cvt_app_xxx npm test

Releasing

Publishing happens locally with the release script — no npm token, no CI. npm publish prompts for your passkey/2FA, which you approve in the browser.

./scripts/release.sh            # publish the current package.json version
./scripts/release.sh patch      # bump patch, then publish  (0.2.0 -> 0.2.1)
./scripts/release.sh minor      # bump minor, then publish
./scripts/release.sh major      # bump major, then publish

The script validates (typecheck + tests + build) before bumping, tags the version, publishes (you approve the passkey prompt), then pushes the tag and commit to GitHub. The order is deliberate — if the publish is cancelled, nothing is pushed; just re-run npm publish && git push --follow-tags origin main to finish.

A tokenless CI alternative using npm Trusted Publishing (OIDC) is also included at .github/workflows/publish.yml for when GitHub Actions is available.

Changelog

See CHANGELOG.md for release notes.

License

MIT