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

@darqlabs/curator-sdk

v0.1.4

Published

Official Curator SDK — talk to your deployed agents from server-side JavaScript or TypeScript.

Readme

@darqlabs/curator-sdk

Official Curator SDK.

Integrate with your deployed agents. Works in Node, the browser, and modern edge runtimes (anywhere fetch exists).

const reply = await curator.agent("support-bot").run("How do I reset my password?")
//console.log(reply.content)

Integration guide

Before you start

You'll need three things from your Curator dashboard:

  1. An API key. Generate one at Settings → Developer. Each key is scoped to a single environment (production, staging, or development) — pick the one you're integrating against.
  2. Your project slug.
  3. A deployed agent's identifier slug.

1. Install

npm install @darqlabs/curator-sdk
# or
pnpm add @darqlabs/curator-sdk
# or
yarn add @darqlabs/curator-sdk

Requires Node 18+ or any runtime with a global fetch. Ships ESM + CommonJS — modern bundlers tree-shake it automatically.

2. Initialize the client

Create one Curator instance and reuse it for the lifetime of your process.

import { Curator } from "@darqlabs/curator-sdk"

export const curator = new Curator({
  apiKey: "CURATOR_API_KEY",
  project: "default",          // your project slug
  environment: "production",   // 'production' | 'staging' | 'development'
})

3. Send your first message

For a single round-trip, use agent.run():

const reply = await curator
  .agent("support-bot")
  .run("Hi, I lost my receipt — can you resend it for order #4823?")

console.log(reply.content)
// reply.conversationId  — store this if you want to follow up
// reply.runId           — useful for log correlation
// reply.sequence        — per-conversation message index
// reply.metadata        — structured result from the agent, or null (see below)

4. Multi-turn conversations

For ongoing chat, use agent.chat():

const chat = curator.agent("support-bot").chat()

await chat.send("I'd like to upgrade my plan.")
await chat.send("Actually, what's included in the Pro tier?")

console.log(chat.id) // "conv_8f2a1c4b9e0d"

5. Resume a conversation later

Store chat.id somewhere durable, then reattach with .conversation(id):

const chat = curator.agent("support-bot").conversation(conversationId)
const reply = await chat.send("Following up on what we discussed yesterday…")

You can also fetch the prior message history:

const page = await chat.history({ limit: 50 })
for (const msg of page.messages) {
  console.log(`[${msg.role}] ${msg.content}`)
}
if (page.hasMore) {
  const next = await chat.history({ limit: 50, beforeSequence: page.oldestSequence! })
}

6. Handle errors

The SDK throws typed errors for non-success states. Catch the ones you care about:

import {
  CuratorApprovalRequiredError,
  CuratorAgentError,
  CuratorTimeoutError,
  CuratorAuthError,
  CuratorNotFoundError,
} from "@darqlabs/curator-sdk"

try {
  const reply = await chat.send("Refund my last order.")
  // …use reply.content
} catch (err) {
  if (err instanceof CuratorApprovalRequiredError) {
    // The agent paused waiting for human approval. Resolution happens in
    // the Curator dashboard — surface a friendly message to your user.
    showToUser(
      `That action needs administrator approval. Please contact your ` +
      `administrator to review the request for "${err.approval.tool_name}".`,
    )
  } else if (err instanceof CuratorTimeoutError) {
    // The wait expired but the agent is still working in the background.
    // Either give the user a "still thinking…" message, or keep waiting:
    const reply = await err.continueWaiting(180000)
  } else if (err instanceof CuratorAgentError) {
    // Terminal agent failure — LLM quota, provider auth, etc.
    // err.failureClass is one of: provider_quota_exhausted | provider_auth |
    // provider_bad_request | provider_rate_limited | provider_unavailable |
    // internal | unknown. err.isPermanent tells you whether retrying helps.
    log.error({ err }, "agent run failed")
  } else if (err instanceof CuratorAuthError) {
    // 401 — bad / revoked API key.
  } else if (err instanceof CuratorNotFoundError) {
    // 404 — wrong deployment slug, project, or conversation id.
  } else {
    throw err
  }
}

All errors inherit from CuratorError, so a single catch (err: CuratorError) works as a backstop if you'd rather handle them generically.

7. Attach files

Give the SDK the bytes, get back a reference ready to attach:

const fileRef = await curator.files.upload({
  filename: "sales-2026-q2.csv",
  mimeType: "text/csv",
  data: csvBlob, // Blob, File, ArrayBuffer, Uint8Array, or Buffer
})

const reply = await chat.send("What does this CSV show?", {
  files: [fileRef],
})

Two-step — useful when you want to drive the PUT yourself (progress UI, resumable uploads, custom transport):

const { uploadUrl, headers, fileReference } = await curator.files.createUpload({
  filename: "sales-2026-q2.csv",
  mimeType: "text/csv",
  size: csvBlob.size,
})

await fetch(uploadUrl, { method: "PUT", headers, body: csvBlob })

const reply = await chat.send("What does this CSV show?", {
  files: [fileReference],
})

Common patterns

Stateful chat in a web app

const chat = userSession.conversationId
  ? curator.agent("support-bot").conversation(userSession.conversationId)
  : curator.agent("support-bot").chat()

const reply = await chat.send(userInput)
if (!userSession.conversationId) {
  await persistConversationId(userSession.id, chat.id!)
}
return reply.content

Long-running runs

Raise the timeout per-call (server cap 5 minutes):

const reply = await chat.send("Do a deep research pass on this brief.", {
  timeoutMs: 300000,
})

Structured results (metadata)

An agent whose instructions define a structured output — a booking draft, an extracted form, a plan — returns that object on reply.metadata alongside the conversational reply.content. It's null for plain conversational agents. The shape is whatever the agent emits (schema-agnostic); validate it in your app (e.g. with a zod schema).

const reply = await chat.send("Book Acme from Fontana to Ontario on July 1.")
console.log(reply.content)   // "I've started the booking draft…"
console.log(reply.metadata)  // { bookingDraft: { origin: {…}, … }, changedFields: [...], clarification: null }

Live updates: chat.send() returns only the final snapshot. To render progress as the agent fills the object in — turn by turn, before it replies — stream instead and handle state events:

for await (const event of chat.stream("Book Acme from Fontana to Ontario.")) {
  if (event.type === "delta") appendText(event.text)          // prose tokens
  else if (event.type === "state") renderDraft(event.metadata) // live structured snapshot, every turn
  else if (event.type === "done") finalize(event.message)      // message.metadata = final snapshot
}

Using React? @darqlabs/curator-sdk-react's <CuratorChat onState={…} /> wires this up for you.


Using from a browser

CORS: browser requests hit the Curator API directly, so your origin must be on the allowlist. Add it in Settings → Developer.


Optional: verified end-user identity

You don't need this to get started. An API key + project is all it takes to talk to your agents — everything above works without any user-level identity.

Turn this on only when you want Curator to know which of your users is driving a conversation — for per-user policies, audit attribution, or tenant isolation. It's opt-in per org: until an operator enables it in Settings → Developer → End-user auth, the SDK ignores endUserJwt entirely and requests authenticate on the API key alone.

When enabled, your backend mints a short-lived JWT per user and hands it to the SDK. The server verifies it against your configured JWKS endpoint:

const curator = new Curator({
  apiKey: process.env.CURATOR_API_KEY!,
  project: "default",
  // Static token, or a function the SDK calls per request to refresh it.
  endUserJwt: () => mintEndUserJwtForCurrentUser(),
})

Standing up a JWKS endpoint is not required for MVP — skip this section and revisit it when you need verified identity.


API reference

new Curator(options)

| Option | Type | Default | Notes | | --- | --- | --- | --- | | apiKey | string | — | Required. curator_live_* (production) or curator_test_* (staging / development), from Settings → Developer. | | project | string | — | Default project slug. Required unless every call passes { system: true }. | | environment | 'production' \| 'staging' \| 'development' | 'production' | Default environment. Must match the environment your apiKey was issued for. | | baseUrl | string | managed host | Override for self-hosted. | | defaultTimeoutMs | number | 120000 | Server-capped at 300000. | | defaultHeaders | Record<string,string> | {} | Sent on every request. | | fetch | typeof fetch | global | Inject a custom fetch. | | endUserJwt | string \| () => string \| Promise<string> | — | Optional. Only needed if your org has turned on verified end-user identity (see below). Leave it unset otherwise. |

curator.agent(slug, locator?)

Returns an Agent bound to the given deployment. locator accepts { project?, environment?, system? } to override the constructor defaults for this address.

agent.run(content, opts?) → Promise<AssistantMessage>

Send a single message and return the reply. Equivalent to agent.chat().send(content, opts).

agent.chat() → Chat

Returns a new chat handle for multi-turn conversations.

agent.conversation(id) → Chat

Reattach to an existing conversation by id.

chat.send(content, opts?) → Promise<AssistantMessage>

opts.files?: FileAttachment[] — attachments. opts.timeoutMs?: number — override the wait timeout for this call.

Returns the final assistant message. message.metadata holds the agent's structured result (or null).

chat.stream(content, opts?) → AsyncIterable<StreamEvent>

Same send, streamed. Yields events as they arrive:

| event.type | Payload | Terminal | | --- | --- | --- | | delta | text — incremental prose tokens | no | | state | metadata — complete structured snapshot for this turn | no | | tool_call.started / tool_call.completed | tool call id / name | no | | done | message (with message.metadata) | yes | | error | failureClass, message | yes | | paused_for_approval | approval | yes |

state fires on every turn the agent reports structured state — see Structured results.

chat.history(opts?) → Promise<HistoryPage>

opts.limit?: number — page size (server cap 500). opts.beforeSequence?: number — cursor; pass page.oldestSequence from the previous page.

chat.id

Conversation id, or null until the first send.

curator.files.upload(input) → Promise<FileAttachment>

One-shot upload. input is { filename, mimeType, data, size? } where data is a Blob, File, ArrayBuffer, Uint8Array, or Buffer. Returns the reference to pass into chat.send(..., { files: [...] }).

curator.files.createUpload(input) → Promise<PresignUploadResult>

Two-step: returns { uploadUrl, method, headers, expiresAt, fileReference }. PUT the bytes to uploadUrl with the returned headers, then attach fileReference to a send.

Errors

| Class | Thrown when | | --- | --- | | CuratorAuthError | 401 — bad / revoked API key. | | CuratorForbiddenError | 403 — permission denied. | | CuratorNotFoundError | 404 — unknown deployment / conversation. | | CuratorDeploymentRetiredError | 410 — deployment retired. | | CuratorRateLimitError | 429 — back off. | | CuratorApprovalRequiredError | Run paused for human approval. | | CuratorAgentError | Terminal agent failure (LLM quota, etc.). failureClass + isPermanent. | | CuratorTimeoutError | Wait expired. continueWaiting() resumes. | | CuratorError | Base class — everything above extends this. |


Troubleshooting

CuratorAuthError: Invalid or revoked API key — double-check the key in Settings → Developer. The SDK sends it as Authorization: Bearer <key>.

CuratorNotFoundError: Deployment not found — verify (project slug, environment, deployment slug) all match what the Deployments tab shows. Missing one of these is the #1 cause.

CuratorTimeoutError on every call — your agent is taking longer than defaultTimeoutMs. Raise the timeout (new Curator({ defaultTimeoutMs: 300000 })) or call err.continueWaiting() once.

Browser requests fail with a CORS error — add your origin to the allowlist in Settings → Developer. The SDK isn't doing anything unusual; the request gets blocked before it reaches Curator.

Approval-required pauses on every send — your agent has tools that require human approval. Resolve them once in the dashboard and adjust the deployment's policy if you don't want this gate in your environment.