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

calado

v0.4.1

Published

Official calado SDK — wrap your Anthropic or OpenAI client to capture conversations and agent definitions for behavioral analytics.

Readme

calado

Official SDK for calado — behavioral analytics for AI agents. Wrap your Anthropic or OpenAI client and calado captures conversations, system prompts, and tool schemas for analysis.

Install

npm install calado
# or
pnpm add calado
# or
yarn add calado

Quickstart

import Anthropic from "@anthropic-ai/sdk";
import { calado } from "calado";

calado.init(process.env.CALADO_API_KEY!);
const anthropic = calado.wrap(new Anthropic());

// Use the client normally — calado captures in the background.
await anthropic.messages.create({
  model: "claude-sonnet-4-5",
  max_tokens: 1024,
  system: "You are a helpful assistant.",
  messages: [{ role: "user", content: "hello" }],
});

OpenAI works the same way:

import OpenAI from "openai";
import { calado } from "calado";

calado.init(process.env.CALADO_API_KEY!);
const openai = calado.wrap(new OpenAI());

Get your API key from your agent's settings in the calado dashboard.

Configuration

calado.init(apiKey, {
  baseUrl: "https://app.calado.ai", // override for self-hosted
  batchSize: 10,                    // flush when queue reaches this
  flushInterval: 5_000,             // flush every N ms (0 disables timer)
  maxQueueEvents: 10_000,           // drop-oldest overflow threshold
  maxQueueBytes: 10 * 1024 * 1024,  // byte-based overflow threshold
  debug: false,                     // structured console logs
  mask: undefined,                  // (event) => event | null. See "Redacting sensitive data".
});

Redacting sensitive data

mask runs synchronously on every event before transport. Return a redacted copy, or return null / undefined to drop the event entirely. The callback runs inside your process, so raw values never leave the machine.

const EMAIL = /[\w.+-]+@[\w-]+\.[\w.-]+/g;
const PHONE = /\+?\d[\d\s().-]{7,}\d/g;
const scrub = (s: string) => s.replace(EMAIL, "[EMAIL]").replace(PHONE, "[PHONE]");

// content fields may be a string or an array of content blocks (Anthropic
// blocks, OpenAI multimodal parts, tool results). Handle both.
function scrubContent(value: unknown): unknown {
  if (typeof value === "string") return scrub(value);
  if (Array.isArray(value)) {
    for (const block of value) {
      if (block && typeof block === "object") {
        const b = block as { text?: unknown; content?: unknown };
        if (typeof b.text === "string") b.text = scrub(b.text);
        if (b.content !== undefined) b.content = scrubContent(b.content);
      }
    }
  }
  return value;
}

calado.init(process.env.CALADO_API_KEY!, {
  mask: (event) => {
    if (event.kind === "definition") {
      event.payload.content = scrub(event.payload.content);
      return event;
    }
    if (event.kind !== "conversation") return event;

    const req = event.payload.request as {
      messages?: Array<{ content?: unknown }>;
    };
    for (const m of req.messages ?? []) m.content = scrubContent(m.content);

    // Response shape differs per provider; switch on event.format.
    if (event.format === "anthropic") {
      const res = event.payload.response as { content?: unknown };
      res.content = scrubContent(res.content);
    } else if (event.format === "openai_chat") {
      const res = event.payload.response as {
        choices?: Array<{ message?: { content?: unknown } }>;
      };
      for (const c of res.choices ?? []) {
        if (c.message) c.message.content = scrubContent(c.message.content);
      }
    }
    return event;
  },
});

For stable per-conversation placeholders ([EMAIL_1], [EMAIL_2]), use the exported createPlaceholderTracker() helper. The full guide, including Presidio pairing, allowlist patterns, and CI guards, is at docs.calado.ai/ingestion/redaction.

Serverless (important)

calado batches in memory and flushes on a timer. Serverless runtimes freeze between invocations, so you must flush explicitly before your handler returns.

Next.js App Router

import { after } from "next/server";

export async function POST() {
  // ...
  after(() => calado.flush());
  return Response.json({ ok: true });
}

Vercel Edge

export default async function handler(req: Request, ctx: { waitUntil: (p: Promise<unknown>) => void }) {
  // ...
  ctx.waitUntil(calado.flush());
  return new Response("ok");
}

AWS Lambda / any serverless

export async function handler(event) {
  try {
    // ...
    return { statusCode: 200 };
  } finally {
    await calado.flush();
  }
}

Conversation context (Node.js)

In long-running Node processes, propagate a conversationId across async calls so calado groups multi-turn sessions correctly:

await calado.withConversation("conv_123", async () => {
  await anthropic.messages.create({ /* ... */ });
  await anthropic.messages.create({ /* ... */ }); // same session
});

withContext(convId, userId, fn) is the longer form. Both use AsyncLocalStorage and require Node.js — in Vercel Edge Runtime, use calado.conversationId = "..." with manual reset.

API

| Method | Purpose | |---|---| | calado.init(apiKey, options?) | Initialize. Throws on bad config. | | calado.wrap<T>(client) | Return a Proxy over your provider client. Unknown clients pass through unchanged. | | calado.flush() | Send queued data now. | | calado.shutdown() | Flush + clear timers. | | calado.status() | Return { enabled, queued, lastIngestAt, lastError, consecutive401s, maskConfigured, maskFailures, consecutiveMaskFailures, maskedEvents, definitionsDropped }. | | calado.withContext(convId, userId, fn) | Run fn with conversation context. | | calado.withConversation(id, fn) | Shorthand for withContext(id, undefined, fn). | | calado.conversationId | Get/set the active conversation id (scripts only). | | Configuration: mask | (event: IngestionEvent) => IngestionEvent \| null \| undefined. Synchronous redaction callback applied to every event before transport. See Redacting sensitive data. | | Configuration: createPlaceholderTracker() | Helper export. Returns a per-scope counter so the same raw value gets the same [CATEGORY_N] label across multi-turn conversations. |

What gets captured

  • Full request (model, messages, temperature, etc.) with system and tools stripped — those are sent separately as agent definitions.
  • Full response body (content blocks, tool calls, usage, stop reason).
  • Streaming: reconstructed final response. Aborted streams are marked metadata.partial = true.

Error philosophy

  • Init time: throws on bad config (invalid cl_ key, etc.).
  • Runtime: never throws. Your LLM calls always succeed even if calado's transport fails.
  • 401 responses: log a warning every time. After 3 consecutive 401s the transport auto-disables. Fix your API key and call init again.
  • Mask auto-disable: after 100 consecutive mask failures the transport auto-disables. status().lastError is set to 'mask_disabled_after_100_failures: call calado.init() to recover'. Fix your mask and call init again.
  • Retry: 5xx / network errors only. Two retries with jittered backoff. 4xx responses drop immediately.

Supported runtimes

  • Node.js 18+ (primary target)
  • Bun
  • Deno (via npm: specifier)
  • Vercel Edge Runtime — pass-through works; withContext requires Node

License

MIT