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

@caruuto/caruuto-js

v0.9.9

Published

A high-level library for interacting with Caruuto

Downloads

1,632

Readme

Caruuto JS

The official JavaScript SDK for Caruuto — a high-level client for querying content collections, sending AI chat messages, managing media, queueing email, and issuing tokens.


Installation

npm install @caruuto/caruuto-js

Requires Node.js 18 or later.


Initialisation

Use createClient for all production use — it returns a client with all extensions (ai, email, tokens) attached.

const CaruutoClient = require('@caruuto/caruuto-js')

const client = CaruutoClient.createClient(
  'https://your-instance.caruuto.com',
  'your-api-key'
)

The bare constructor (new CaruutoClient(url, key)) creates a client without extensions. Use it only when you need the Content API alone and want to avoid loading extension code.

Options

CaruutoClient.createClient(url, apiKey, {
  // Override the built-in undici fetch with your own implementation.
  fetch: myCustomFetch,

  // Properties that are stripped before create/update calls.
  // Defaults to ['created_at', 'created_by', 'updated_at', 'updated_by', 'v'].
  reservedProperties: ['_id', 'created_at']
})

Content API

The Content API follows a chainable builder pattern. Filter methods set state on the client and return this; a terminator (find, create, update, delete) fires the request and returns a Promise. The state is reset automatically after each request.

// Collection → filter → terminator
const result = await client
  .in('articles')
  .whereFieldIsEqualTo('status', 'published')
  .limitTo(10)
  .sortBy('published_at', 'desc')
  .find()

Query filters

All filter methods return this and can be chained freely.

in(collection)

Select the target collection.

client.in('products')

where(query)

Pass a raw MongoDB-style query object directly.

client.where({ status: 'active', price: { $gt: 100 } })

whereFieldIsEqualTo(field, value, caseInsensitive?)

Exact match. Pass true as the third argument for a case-insensitive match.

client.whereFieldIsEqualTo('slug', 'hello-world')
client.whereFieldIsEqualTo('email', '[email protected]', true)

whereFieldIsNotEqualTo(field, value)

Negated match. Strings produce a case-insensitive regex negation; numbers use $ne.

client.whereFieldIsNotEqualTo('status', 'archived')

whereFieldIsGreaterThan(field, value)

client.whereFieldIsGreaterThan('price', 50)

whereFieldIsGreaterThanOrEqualTo(field, value)

client.whereFieldIsGreaterThanOrEqualTo('stock', 1)

whereFieldIsLessThan(field, value)

client.whereFieldIsLessThan('age', 18)

whereFieldIsLessThanOrEqualTo(field, value)

client.whereFieldIsLessThanOrEqualTo('score', 100)

whereFieldContains(field, value)

Case-insensitive substring match (/value/i).

client.whereFieldContains('title', 'caruuto')

whereFieldBeginsWith(field, value)

Case-insensitive prefix match (/^value/i).

client.whereFieldBeginsWith('slug', 'product-')

whereFieldEndsWith(field, value)

Case-insensitive suffix match (/value$/i).

client.whereFieldEndsWith('email', '@example.com')

whereFieldDoesNotContain(field, value)

Negated substring match.

client.whereFieldDoesNotContain('title', 'draft')

whereFieldExists(field)

Match documents where the field is not null.

client.whereFieldExists('publishedAt')

whereFieldDoesNotExist(field)

Match documents where the field is null or absent.

client.whereFieldDoesNotExist('deletedAt')

whereFieldIsOneOf(field, values)

$in — field value must be one of the provided array.

client.whereFieldIsOneOf('category', ['news', 'opinion'])

whereFieldIsNotOneOf(field, values)

$nin — field value must not appear in the provided array.

client.whereFieldIsNotOneOf('status', ['draft', 'archived'])

Pagination & sorting

Two pagination modes are available: offset-based (page/limit) and cursor-based (afterId). They are mutually exclusive — when afterId is set the server ignores goToPage.

limitTo(n)

Limit the number of returned documents. Works with both pagination modes.

client.in('posts').limitTo(20).find()

goToPage(n)

Select a page (1-indexed, offset-based). Works together with limitTo. Not compatible with afterId — the server ignores page when a cursor is active.

client.in('posts').limitTo(20).goToPage(3).find()

afterId(id)

Cursor-based pagination. Pass the id of the last document from the previous page to fetch the next page. This is the required pagination mode for the members collection and will be supported by other content types.

When an afterId cursor is active the response includes both count (documents on this page) and totalCount (total across all pages).

// First page — no cursor
const page1 = await client.in('members').limitTo(50).find()
const lastId = page1.results.at(-1).id

// Next page — pass the last ID as the cursor
const page2 = await client.in('members').limitTo(50).afterId(lastId).find()

sortBy(field, order)

Sort on a field. order is 'asc' (default) or 'desc'.

client.in('posts').sortBy('published_at', 'desc').find()

Multiple sorts can be chained:

client
  .in('posts')
  .sortBy('category', 'asc')
  .sortBy('published_at', 'desc')
  .find()

useFields(fields)

Restrict the fields returned. Pass an array of field names.

client.in('articles').useFields(['title', 'slug', 'excerpt']).find()

useVersion(version)

Override the API version for this request. Defaults to 'v1'.

client.in('articles').useVersion('v2').find()

find

Retrieve documents matching the current query state.

const result = await client
  .in('articles')
  .whereFieldIsEqualTo('status', 'published')
  .find()

// result.results — array of documents
// result.metadata — pagination info (when extractMetadata is true)

Pass { extractMetadata: true } to also fetch document count and pagination metadata:

const result = await client
  .in('articles')
  .limitTo(10)
  .find({ extractMetadata: true })

// result.metadata.totalCount
// result.metadata.totalPages

create

Create one or more documents in a collection. Reserved properties (created_at, updated_at, etc.) are stripped automatically.

// Single document
const doc = await client.in('articles').create({
  title: 'Hello World',
  status: 'published'
})

// Multiple documents
const docs = await client.in('articles').create([
  { title: 'Post One' },
  { title: 'Post Two' }
])

update

Update documents matching the current query. A query must be set before calling update.

await client
  .in('articles')
  .whereFieldIsEqualTo('_id', '64a1b2c3d4e5f6a7b8c9d0e1')
  .update({ status: 'archived' })

delete

Delete documents matching the current query. A query must be set before calling delete.

await client
  .in('articles')
  .whereFieldIsEqualTo('status', 'draft')
  .delete()

getCollections

Return the list of all content type definitions registered in the Caruuto instance.

const collections = await client.getCollections()

syncSchema

Sync a collection's schema definition with the server. Useful for automated schema migrations.

await client.in('articles').syncSchema({
  fields: [
    { key: 'title', type: 'String', required: true },
    { key: 'body', type: 'String' }
  ]
})

search

Full-text search across a collection.

const results = await client
  .in('articles')
  .search({ phrase: 'caruuto headless' })
  .find()

Media API

uploadFile

Upload a file to the project's media bucket. The storage bucket is resolved server-side from the API key.

const fs = require('fs')

const buffer = fs.readFileSync('/path/to/image.png')

const result = await client.uploadFile(
  'images/avatars',   // directory within the bucket
  'avatar.png',       // file name
  'image/png',        // MIME type
  buffer,             // Buffer or ArrayBuffer
  buffer.byteLength   // content length in bytes
)

getSignedUrl

Obtain a signed URL for a direct client-side upload (e.g. from a browser).

const { url, fields } = await client.getSignedUrl({
  fileName: 'photo.jpg',
  mimeType: 'image/jpeg',
  directory: 'uploads'
})

AI extension

The AI extension is available on clients created with createClient. All methods are on client.ai.


ai.query

Send a message and receive a single blocking response from the AI.

const response = await client.ai.query({
  message: 'What are your opening hours?'
})

console.log(response.answer)
console.log(response.conversation_id) // use to continue the conversation

Options

| Parameter | Type | Required | Description | |---|---|---|---| | message | string | Yes | The user's message | | projectId | string | No | Caruuto project UUID. Not required when authenticating with an API key (the key's project is used). | | conversationId | string | No | Continue an existing conversation | | clientSessionId | string | No | Anonymous user token for session continuity | | source | string | No | 'widget', 'api', or 'embed' | | referrerUrl | string | No | URL where the chat widget is embedded | | utmSource | string | No | UTM source from the page URL | | utmMedium | string | No | UTM medium from the page URL | | utmCampaign | string | No | UTM campaign from the page URL | | utmTerm | string | No | UTM term from the page URL | | utmContent | string | No | UTM content from the page URL | | leadEmail | string | No | Capture a lead email on this turn | | leadName | string | No | Lead name (used together with leadEmail) | | type | string | No | Restrict knowledge retrieval to a single type | | maxItems | number | No | Maximum knowledge items to retrieve (1–10) |

Response

{
  answer: string,
  conversation_id: string,
  context: [],        // knowledge items used
  entities: [],       // entities detected
  _meta: {},
  from_cache: boolean // present when the answer was served from cache
}

ai.stream

Send a message and receive a streaming response. Returns an async generator — iterate with for await.

for await (const chunk of client.ai.stream({ message: 'Tell me about your products' })) {
  if (chunk.type === 'text') {
    process.stdout.write(chunk.text)
  } else if (chunk.type === 'done') {
    console.log('\nconversation_id:', chunk.conversation_id)
  }
}

Yields two event types:

| type | Fields | Description | |---|---|---| | 'text' | text: string | A token or token batch from the LLM | | 'done' | conversation_id, context, entities, _meta, from_cache? | Final event, mirrors query() response fields |

Accepts the same options as ai.query.


ai.context

Retrieve the assembled context for a message without calling the LLM. Use this when you want to drive your own LLM call (e.g. with the AI SDK) and need Caruuto to handle retrieval, entity detection, and prompt assembly.

const ctx = await client.ai.context({
  message: 'What are your pricing tiers?',
  conversationId: existingConversationId
})

if (ctx.from_cache) {
  // serve the cached answer directly
  return ctx.cached_answer
}

// ctx.system_prompt — ready to pass to your LLM
// ctx.history       — prior conversation turns
// ctx.context       — knowledge items
// ctx.entities      — detected entities
// ctx.conversation_id

Accepts the same options as ai.query (excluding leadEmail/leadName).


ai.saveTurn

Persist a completed turn (user message + assistant response) after your own LLM call. Call this once your stream is complete with the full response text and token usage.

await client.ai.saveTurn({
  conversationId: ctx.conversation_id,
  userContent: 'What are your pricing tiers?',
  assistantContent: fullAssistantText,
  modelId: 'claude-sonnet-4-6',
  inputTokens: 512,
  outputTokens: 128,
  cachedTokens: 0,
  contextItems: ctx.context,
  entitiesReferenced: ctx.entities.map(e => e.id)
})

Options

| Parameter | Type | Required | Description | |---|---|---|---| | conversationId | string | Yes | From context() or a prior query() response | | userContent | string | Yes | The user's message text | | assistantContent | string | Yes | The full assistant response text | | projectId | string | No | Not required when authenticating with an API key | | modelId | string | No | Model identifier (e.g. 'claude-sonnet-4-6') | | inputTokens | number | No | | | outputTokens | number | No | | | cachedTokens | number | No | | | fromCache | boolean | No | Set true when assistantContent came from a context() cache hit | | contextItems | object[] | No | Knowledge items from context() response | | entitiesReferenced | string[] | No | Entity IDs from context() response | | leadEmail | string | No | | | leadName | string | No | |

Response

{ ok: boolean, turn_count: number }

ai.endConversation

Explicitly end a conversation, marking its status as 'ended'. Call this when the user closes the chat widget or navigates away.

const summary = await client.ai.endConversation({
  conversationId: 'conv_abc123'
})

// summary.id
// summary.status         → 'ended'
// summary.turn_count
// summary.total_tokens
// summary.started_at
// summary.ended_at

ai.loadConversation

Load a conversation and its messages formatted as AI SDK UIMessage objects. Use this to restore a prior conversation into your chat UI.

const conversation = await client.ai.loadConversation({
  conversationId: 'conv_abc123'
})

// conversation.id
// conversation.status
// conversation.turn_count
// conversation.started_at
// conversation.ended_at
// conversation.messages   ← UIMessage[]

ai.forkConversation

Fork an existing conversation or question-cache entry into a new conversation. Returns the new conversation ID.

const { id } = await client.ai.forkConversation({
  sourceId: 'conv_abc123',
  source: 'conversation'    // 'conversation' (default) | 'question_cache'
})

ai.shareConversation

Set a conversation's visibility to 'public' or 'private'. Public conversations are accessible via a share URL without authentication.

const result = await client.ai.shareConversation({
  conversationId: 'conv_abc123',
  visibility: 'public'
})

// result.id
// result.visibility
// result.share_url

ai.getPublicConversation

Fetch a publicly shared conversation without authentication. Returns null if the conversation is private or not found.

const conversation = await client.ai.getPublicConversation({
  conversationId: 'conv_abc123'
})

if (!conversation) {
  // private or not found
}

// conversation.id
// conversation.turn_count
// conversation.started_at
// conversation.ended_at
// conversation.messages

ai.trackLinkClick

Record a click on a link surfaced in an assistant response. Powers the CTA-click and conversion panels in the analytics dashboard.

await client.ai.trackLinkClick({
  conversationId: 'conv_abc123',
  url: 'https://example.com/pricing',
  linkLabel: 'View pricing',
  messageIndex: 2
})

Options

| Parameter | Type | Required | Description | |---|---|---|---| | conversationId | string | Yes | The conversation the link appeared in | | url | string | Yes | The full clicked URL, exactly as rendered | | projectId | string | No | Not required when authenticating with an API key | | linkLabel | string | No | The link's visible text | | messageIndex | number | No | Zero-based index of the assistant message the link appeared in |

Response

{ ok: boolean }

Email extension

Manage email records via client.email. All methods return a Promise.

email.find(query)

Retrieve email records matching the given query.

const result = await client.email.find({ status: 'queued' })

email.queue(data)

Queue an email for sending.

await client.email.queue({
  to: '[email protected]',
  subject: 'Welcome',
  templateId: 'welcome-v1'
})

email.update(data)

Update an existing email record.

await client.email.update({ id: 'email_123', status: 'cancelled' })

email.delete(data)

Delete an email record.

await client.email.delete({ id: 'email_123' })

Tokens extension

Issue and verify short-lived tokens via client.tokens. Also accessible as top-level methods client.createToken() and client.verifyToken().

tokens.create(data) / createToken(data)

Create a new token.

const token = await client.tokens.create({ userId: 'user_abc', scope: 'reset' })
// or
const token = await client.createToken({ userId: 'user_abc', scope: 'reset' })

tokens.verify(token) / verifyToken(token)

Verify a token string.

const result = await client.tokens.verify('tok_abc123')
// or
const result = await client.verifyToken('tok_abc123')

Error handling

All terminator and extension methods return Promises. Failed requests (HTTP 4xx/5xx) reject with an Error object that carries additional properties:

try {
  await client.in('articles').whereFieldIsEqualTo('_id', 'bad-id').find()
} catch (err) {
  console.error(err.message) // message from the API response body, if present
  console.error(err.status)  // HTTP status code
  console.error(err.errors)  // validation errors array, if present
}

Custom fetch

By default the SDK uses undici's fetch. Pass your own implementation via the fetch option to use a different HTTP client, add middleware, or enable instrumentation:

const client = CaruutoClient.createClient(url, apiKey, {
  fetch: (url, options) => {
    console.log('→', options.method, url)
    return globalThis.fetch(url, options)
  }
})

The custom fetch function receives the same (url, init) arguments as the Fetch API and must return a Response-compatible object.