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

@puku-ai/sdk

v4.0.7

Published

PukuAI client SDK

Readme

@puku-ai/sdk

Official TypeScript SDK for the Puku AI client SDK.

The client class is PukuAI. Method names, request shapes, stream events, and error classes follow the Anthropic Messages API conventions.

import PukuAI from "@puku-ai/sdk";

const client = new PukuAI({
  apiKey: process.env.PUKU_API_KEY!,
  baseURL: process.env.PUKU_BASE_URL!,
});

const msg = await client.messages.create({
  model: "puku-ai-2.8",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello!" }],
});
console.log(msg.content[0].text);

Contents

Install

npm install @puku-ai/sdk
# or
pnpm add @puku-ai/sdk
# or
yarn add @puku-ai/sdk

The package targets Node.js ≥ 18 and ships with TypeScript declarations out of the box.

Configuration

Two environment variables, both required:

| Env var | Purpose | | --------------- | -------------------------------------- | | PUKU_API_KEY | Puku API key (sent as X-Api-Key) | | PUKU_BASE_URL | Puku API gateway URL (no fallback) |

const client = new PukuAI({
  apiKey: process.env.PUKU_API_KEY!,
  baseURL: process.env.PUKU_BASE_URL!,
  maxRetries: 2,
  timeout: 600_000,
  defaultHeaders: { "x-app": "my-app" },
});

Defaults: maxRetries = 2, timeout = 600_000 ms (10 min).

Supported models

Exactly three model names are accepted by the Puku gateway. Any other string returns a model-not-found error.

| Model | Family | Notes | |---------------|----------|-------| | puku-ai-2.7 | Puku AI | Cheaper, lower-latency. | | puku-ai-2.8 | Puku AI | Default for most workloads. |

Usage

Blocking

import PukuAI from "@puku-ai/sdk";

const client = new PukuAI();

const msg = await client.messages.create({
  model: "puku-ai-2.8",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello!" }],
});
console.log(msg.content[0].text);

Streaming

const stream = client.messages.stream({
  model: "puku-ai-2.8",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello!" }],
});

for await (const event of stream) {
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
    process.stdout.write(event.delta.text);
  }
}

// After the loop:
const fullText = await stream.finalText();
const finalMessage = await stream.finalMessage();

Errors

import PukuAI from "@puku-ai/sdk";

try {
  await client.messages.create(...);
} catch (err) {
  if (err instanceof PukuAI.RateLimitError) {
    // rate limited — back off and retry
  } else if (err instanceof PukuAI.AuthenticationError) {
    // bad api key
  } else if (err instanceof PukuAI.PukuError) {
    // any other SDK error (base class — all errors extend this)
    console.error(err.status, err.type, err.message);
  }
}

All error classes are static members of the PukuAI class,

  • PukuAI.PukuError (branded base — every error the SDK throws is an instance of this)
  • PukuAI.APIError
  • PukuAI.APIConnectionError
  • PukuAI.APIConnectionTimeoutError
  • PukuAI.APIUserAbortError
  • PukuAI.NotFoundError
  • PukuAI.ConflictError
  • PukuAI.RateLimitError
  • PukuAI.BadRequestError
  • PukuAI.AuthenticationError
  • PukuAI.InternalServerError
  • PukuAI.PermissionDeniedError
  • PukuAI.UnprocessableEntityError

Working features

Every feature below was exercised against the live Puku gateway at PUKU_BASE_URL= with a pk_live_* API key (see .env and .env.staging.example).

Verified against the live gateway

| Feature | Status | Where | |----------------------------------------------------|--------|-------| | Blocking messages.create + message.usage | ✅ works | client.messages.create() | | Streaming messages.stream() + SSE iteration | ✅ works | client.messages.stream() | | stream.finalText() | ✅ works | MessageStream | | stream.finalMessage() | ✅ works | MessageStream | | stream.on("text", cb) listener | ✅ works | MessageStream | | client.beta.messages.toolRunner(...).runUntilDone() | ✅ works | client.beta.messages | | client.messages.countTokens | ✅ works | count-tokens.ts | | client.messages.batches.create / retrieve / results | ✅ works | batch-results.ts (D1-backed storage) | | Server-side web_search (both streaming + non-streaming variants) | ✅ works | web-search.ts, web-search-stream.ts (gateway forwards tool_use to puku-research-gateway and maps back to web_search_tool_result) | | Structured outputs (output_config.format) — zod / json-schema / raw / standard-schema | ✅ works (non-streaming; raw occasionally flaky) | structured-outputs-*.ts | | ToolError class (exceptions from the runner) | ✅ works | helpers/beta | | MCP helpers (mcpTool, mcpTools, mcpMessage, mcpMessages, mcpResourceToContent, mcpResourceToFile) | ✅ exported | helpers/beta/mcp | | Agent toolset (betaAgentToolset20260401, betaBashTool, betaReadTool, betaWriteTool, betaEditTool, betaGlobTool, betaGrepTool) | ✅ exported | tools/agent-toolset | | Full error hierarchy (APIError, RateLimitError, AuthenticationError, …) | ✅ works | static on PukuAI |

Known gateway gaps

| Feature | Status | Note | |--------------------------------------|--------|------| | Structured-outputs JSON parser (output_config.format) — streaming variant | ⚠ flaky | Passes most runs but puku-ai-2.8 / puku-ai-2.7 occasionally emit a duplicate JSON key in the payload which the SDK's Zod parser rejects. Re-running usually passes — not a regression, just model-output variance. | | Structured-outputs JSON parser (output_config.format) — raw variant | ⚠ flaky | Same root cause: puku-ai-2.8 occasionally emits non-JSON prose (e.g. I computed …) or a malformed JSON payload (e.g. an unterminated string) inside the synthetic tool call, which the SDK's JSON.parse rejects. Re-running usually passes — not a regression, just model-output variance. |

Beta managed-agents surface (preview)

The Puku gateway serves Anthropic-shaped responses for the beta managed-agents namespace so SDK code that calls them does not 404. These exist so SDK examples round-trip against the live gateway while the full managed-agents runtime track is reopened — they are not a production control plane.

| Surface | Status | What you can do with it | |--------------------------------------|--------|-------------------------| | client.beta.sessions.* | ✅ works | Run a managed-agent loop — create session, post events.send, stream events.stream. The gateway drives the LLM turn and emits the same Anthropic-shaped SSE events the SDK's SessionToolRunner consumes. | | client.beta.agents.* | ⚠ limited | CRUD round-trip. agents.create returns a populated BetaAgent shape but no remote agent execution. | | client.beta.environments.* | ⚠ limited | POST /environments returns {id, status:'ready'} synchronously — no creating → ready polling. | | client.beta.environments.*.work.{poll,ack,heartbeat,stop,retrieve,update} | ⚠ limited | The work queue round-trips so WorkPoller/EnvironmentWorker terminate. One synthetic work item per session. | | client.beta.vaults.* | ⚠ limited | CRUD + credentials.create. Secret tokens are dropped — vault_ids passed to sessions.create are ignored (sessions don't need credentials in puku). | | client.beta.skills.{create,list,retrieve,delete} | ⚠ limited | Multipart upload + metadata only. Accepts files and files[] field names. | | client.beta.skills.versions.{create,list,retrieve,delete} | ⚠ limited | Multipart upload + metadata only. Idempotent on (skill_id, version) — 409 on duplicate. | | client.beta.skills.versions.download | ⚠ limited | Returns a 22-byte empty-zip body so response.blob() decodes a real Blob. Real archive bytes are not stored. | | client.files.{upload,list,retrieve,download,delete} (non-beta /v1/files) | ⚠ limited | Bytes held in-memory only — not persisted anywhere durable. |

Working example: managed-agent session with toolRunner

This is the loop client.beta.sessions.* actually drives for you. You create a session, post a user message, open the event stream, and the SDK's toolRunner dispatches any agent.tool_use events back through events.send automatically.

import PukuAI from "@puku-ai/sdk";

const client = new PukuAI();

// 1. Create an environment + an agent + a session.
const env = await client.beta.environments.create({ name: "demo" });
const agent = await client.beta.agents.create({
  name: "weather-bot",
  model: "puku-ai-2.8",
  environment: { type: "cloud", id: env.id },
  tools: [
    // Tools the gateway's tool registry will resolve at agent-turn time.
    { type: "agent_toolset_20260401" },
  ],
});
const session = await client.beta.sessions.create({
  agent_id: agent.id,
  environment_id: env.id,
});

// 2. Define a runnable tool the gateway will round-trip.
const getWeather = {
  name: "get_weather",
  description: "Return the current temperature for a city.",
  input_schema: {
    type: "object",
    properties: { city: { type: "string" } },
    required: ["city"],
  },
  run: async (input: { city: string }) =>
    JSON.stringify({ temp_c: 22.4, city: input.city }),
};

// 3. Post a user message and stream the agent turn.
await client.beta.sessions.events.send(session.id, {
  type: "user.message",
  content: [{ type: "text", text: "What's the weather in Tokyo?" }],
});

for await (const event of client.beta.sessions.events.stream(session.id)) {
  // `agent.message` is the final assistant turn; everything before it
  // is either tool_use / tool_result round-trips or status transitions.
  if (event.type === "agent.message") {
    console.log("agent:", event.content);
  }
}

// 4. Clean up.
await client.beta.sessions.delete(session.id);
await client.beta.agents.delete(agent.id);
await client.beta.environments.delete(env.id);

Working example: skill upload + version

The skills router accepts multipart upload and round-trips a single version per skill. Real archive bytes are not stored — versions.download returns a 22-byte empty zip so response.blob() succeeds.

import PukuAI from "@puku-ai/sdk";

const client = new PukuAI();

// Upload a skill.
const skill = await client.skills.create({
  display_name: "my-skill",
  files: [new File(["# SKILL"], "SKILL.md", { type: "text/markdown" })],
});

// Add a version (auto-generated timestamp, or pass one explicitly).
const v1 = await client.beta.skills.versions.create(skill.id, {
  files: [new File(["# v1"], "SKILL.md", { type: "text/markdown" })],
});

// List versions, retrieve one, download (returns an empty zip blob).
const page = await client.beta.skills.versions.list(skill.id);
const fetched = await client.beta.skills.versions.retrieve(skill.id, v1.version);
const blob = await client.beta.skills.versions.download(skill.id, v1.version);
console.log(page.data.length, fetched.version, blob.size); // 1, "20260101T000000Z", 22

// Cleanup — delete cascades to all versions.
await client.beta.skills.delete(skill.id);

Working example: file upload + download (non-beta /v1/files)

client.files.* is the non-beta Anthropic Files API stub. Bytes are held in memory and not persisted anywhere durable.

import PukuAI from "@puku-ai/sdk";

const client = new PukuAI();

const file = await client.files.upload({
  file: new File(["hello"], "hello.txt", { type: "text/plain" }),
});
console.log(file.id, file.filename, file.size_bytes); // file_xxx, "hello.txt", 5

const list = await client.files.list();
const blob = await client.files.download(file.id);
console.log(await blob.text()); // "hello"

await client.files.delete(file.id);

Example: full tool-runner loop

import PukuAI from "@puku-ai/sdk";

const client = new PukuAI({
  apiKey: process.env.PUKU_API_KEY!,
  baseURL: process.env.PUKU_BASE_URL!,
});

// A runnable tool. Puku-ai/sdk accepts the same BetaRunnableTool shape
//  (tool def + run + parse).
const getWeather = {
  name: "get_weather",
  description: "Get the current temperature for a city, in Celsius.",
  input_schema: {
    type: "object",
    properties: { city: { type: "string" } },
    required: ["city"],
  },
  run: async (input) => JSON.stringify({ temp_c: 22.4, city: input.city }),
  parse: (raw) => JSON.parse(raw),
};

const finalMessage = await client.beta.messages
  .toolRunner({
    model: "puku-ai-2.8",
    max_tokens: 512,
    messages: [{ role: "user", content: "What's the weather in Tokyo?" }],
    tools: [getWeather],
  })
  .runUntilDone();

console.log(finalMessage.content[0].text);
// -> "It looks like the weather service returned an error. Could you try
//     again in a moment? ..."  (or a successful summary, depending on
//     the model + tool)
console.log(finalMessage.usage);

Example: streaming with text listener

import PukuAI from "@puku-ai/sdk";

const client = new PukuAI();

const stream = client.messages.stream({
  model: "puku-ai-2.8",
  max_tokens: 128,
  messages: [{ role: "user", content: "Say hello in three languages." }],
});

let buf = "";
stream.on("text", (chunk) => { buf += chunk; });
stream.on("error", (err) => { console.error("stream error:", err); });

for await (const _ of stream) { /* drain */ }

const text = await stream.finalText();
const final = await stream.finalMessage();
console.log(text, final.usage);

Example: MCP helpers

import PukuAI, { mcpTools, mcpMessage, mcpMessages, mcpResourceToFile } from "@puku-ai/sdk";

const client = new PukuAI();

// `mcpTools` converts an MCP client from @modelcontextprotocol/sdk into


import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";

const mcp = new Client({ name: "demo", version: "0.0.0" }, { capabilities: {} });
const [clientT] = InMemoryTransport.createLinkedPair();
await mcp.connect(clientT);
const tools = await mcpTools({ mcpClient: mcp });

const reply = await client.beta.messages.create({
  model: "puku-ai-2.8",
  max_tokens: 512,
  mcp_servers: [{ type: "url", url: "https://mcp.example.com/sse", name: "example" }],
  messages: [{ role: "user", content: "Find any open PRs." }],
});

Example: agent toolset (bash / read / write / edit / glob / grep)

currently ignore this

The SDK ships the upstream tools/agent-toolset surface — the same BetaRunnableTool[] factory used by puku-cli's agent mode to give the model a sandboxed shell + filesystem.

import { betaAgentToolset20260401 } from "@puku-ai/sdk";

const tools = betaAgentToolset20260401({ workdir: "/tmp/sandbox" });
// -> [bash, read, write, edit, glob, grep]

const runner = client.beta.messages.toolRunner({
  model: "puku-ai-2.8",
  max_tokens: 1024,
  messages: [{ role: "user", content: "List /tmp/sandbox and grep for TODO" }],
  tools,
});
const final = await runner.runUntilDone();

Drop individual tools in or out:

const tools = betaAgentToolset20260401({ workdir: "/tmp/sandbox" })
  .filter((t) => t.name !== "bash"); // no shell access

Or pick the individual factories:

import { betaReadTool, betaBashTool } from "@puku-ai/sdk";

const tools = [betaReadTool({ workdir: "/tmp/sandbox" })];

setupSkills, resolveSkillVersion, and extractSkillArchive round-trip against /v1/skills/{id}/versions/{version} + /v1/sessions/{id}. The gateway now serves those endpoints (multipart upload + metadata in-memory, download returns a 22-byte empty zip), so the helpers no longer 404 — but extractSkillArchive will receive an empty zip body and cannot extract real archive contents. The pure archive-validation helpers (assertSafeMemberNames, assertNoSpecialMembers, runArchiveTool, archiveTopDir, readHead) stay available — they don't touch the gateway.

Namespace

Resource and helper types live on the PukuAI class so they can be used without re-importing:

import PukuAI from "@puku-ai/sdk";

type M     = PukuAI.Messages.Message;
type Mod   = PukuAI.Models.ModelInfo;
type Beta  = PukuAI.Beta.Messages.BetaMessage;
type Skill = PukuAI.Beta.Skills.SkillRetrieveResponse;

Static helpers:

PukuAI.HUMAN_PROMPT;     // "\n\nHuman:"
PukuAI.AI_PROMPT;        // "\n\nAssistant:"
PukuAI.toFile(...);      // multipart helper
PukuAI.VERSION;          // SDK version

ESM and CJS

The default export behaves differently depending on the module system.

ESM (import PukuAI from "@puku-ai/sdk"): the default export is the PukuAI class. Construct it with new:

import PukuAI from "@puku-ai/sdk";

const client = new PukuAI({ apiKey, baseURL });

CJS (require("@puku-ai/sdk")): the build applies a small callable shim (see scripts/build.ts → patchCallableCjs) that wraps module.exports in a function which delegates to new exports.default(...). This means both of these work:

const PukuAI = require("@puku-ai/sdk");

// Callable form (uses the shim):
const client = PukuAI({ apiKey, baseURL });

// Or as a class (`.default` is the same `PukuAI` class):
const client = new PukuAI({ apiKey, baseURL });

Use import PukuAI from "@puku-ai/sdk" in new code; the CJS callable form

License

MIT