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

@sercha-ai/client

v0.6.0

Published

TypeScript client for the Sercha Enterprise API

Readme

@sercha-ai/client

TypeScript client for the Sercha Enterprise API.

npm install @sercha-ai/client

Node 18+. ESM and CommonJS. No runtime dependencies.

Quick start

import { SerchaClient } from '@sercha-ai/client';

const sercha = new SerchaClient({
  baseUrl: process.env.SERCHA_BASE_URL!,
  auth: {
    clientId: process.env.SERCHA_CLIENT_ID!,
    clientSecret: process.env.SERCHA_CLIENT_SECRET!,
  },
});

const { rows } = await sercha.query('SELECT _id, status FROM claims.Claim');

Credentials come from a Sercha service account: an admin creates one under Settings → Service Accounts, and the secret is shown once. The client exchanges them for a short-lived access token and refreshes it automatically.

Construct the client once and share it. It caches tokens, so building one per request mints a new token every time.

Querying

query() runs one SerchaQL statement.

interface Claim {
  _id: string;
  claim_id: string;
  status: string;
  days_open: number;
}

const { rows, stats, columns } = await sercha.query<Claim>(
  'SELECT _id, claim_id, status, days_open FROM claims.Claim WHERE status = "open"',
);

The type parameter asserts the row shape. It is not validated at runtime: the server sends no schema, so there is nothing to check against. Use catalogue.entityProperties() if you need the real schema.

Large result sets

The API has no HTTP pagination and applies a 30-second deadline to the query endpoint, so a single unbounded SELECT over a large entity will fail rather than stream. paginate() pages in-language:

for await (const claim of sercha.paginate<Claim>(
  'SELECT _id, status FROM claims.Claim ORDER BY _id',
)) {
  await handle(claim);
}

The statement must have an ORDER BY. Paging with OFFSET over an unordered result is not stable: rows move between pages, so some come back twice and others never arrive. This is silent, so the client rejects an unordered statement rather than letting it through. Order by something unique, usually _id.

all() collects into an array; one() runs a statement expected to return exactly one row and throws otherwise.

Plugin confirmation

A statement calling an enrichment plugin may exceed a soft call limit. The server answers 202 rather than running it, and the client raises:

import { PluginConfirmationRequiredError } from '@sercha-ai/client';

try {
  await sercha.query('SELECT enrich(abn) FROM suppliers.Supplier');
} catch (error) {
  if (error instanceof PluginConfirmationRequiredError) {
    console.log(`${error.estimate.uncached_calls} calls, about ` +
                `${error.estimate.estimated_wait_seconds}s`);
    await sercha.query('SELECT enrich(abn) FROM suppliers.Supplier', { confirm: true });
  }
}

Ignoring this error means the query silently never runs.

Genie

Genie is the conversational agent. Turns stream over one held-open connection; there is no run ID to poll.

const { id } = await sercha.genie.createConversation('Q3 review');

for await (const event of sercha.stream(id, 'which claims breached SLA?')) {
  if (event.type === 'thinking') process.stdout.write('.');
  if (event.type === 'answer') console.log(event.text);
}

ask() accumulates a whole turn when the progress is not needed:

const result = await sercha.ask(id, 'which claims breached SLA?');
console.log(result.text);
console.log(result.queries.map((q) => q.serchaql));

result.kind is answer, question or error. A question means Genie needs clarification before it can answer — send another turn.

If the connection drops mid-turn the events are lost, but the turn is persisted server-side; getConversation() recovers what it produced.

Stateless turns

streamMessages() and askMessages() take the whole transcript and Sercha stores nothing. Use them when your application owns its own conversation history: ownership, retention and scoping then stay somewhere you can enforce them, rather than being split across two systems joined by an id.

const messages = [
  { role: 'user', content: 'which claims breached SLA?' },
  { role: 'assistant', content: 'Four did, all in March.' },
  { role: 'user', content: 'which of them were reopened?' },
];

for await (const event of sercha.genie.streamMessages(messages)) {
  if (event.type === 'answer') console.log(event.text);
}

Prior context exists only because you send it: nothing is remembered between calls, and no conversation_id is emitted. A first turn is legitimately a single message.

Requires Sercha 0.11.1 or newer. These methods send a stateless flag that older servers ignore, and an older server reads a single message as the start of a new conversation and persists it — creating conversations your application never asked for and cannot reach. Against a server older than 0.11.1, send at least two messages.

Runs

const run = await sercha.waitForRun(runId, { timeoutMs: 900_000 });
if (run.status === 'failed') throw new Error(run.error);

waitForRun() returns a failed run rather than throwing, because failure is an outcome to inspect. It throws SerchaRunTimeoutError only when the budget expires, and the run keeps executing server-side in that case.

Triggering runs (runs.trigger()) requires an admin token. A default service account can read runs but not start them.

Discovery

const tree = await sercha.catalogueTree({ queryable: true });
console.log(tree.corpuses.map((c) => c.name));

With queryable, corpuses are filtered by the token's grants — the authoritative answer to what this token can query. A corpus missing here will fail at query time whether or not it exists.

Packs and cleanup

Corpora organised by structure (Sercha 0.16.3+) expose the Pack Builder surface; Sercha 0.17+ adds the cleanup surface: corpus document listings with content hashes and partition keys, human flag confirmation (retire-without-delete) and the archive log.

// The rooms index: partitions plus the room badge, one call per corpus.
const partitions = await sercha.corpusPartitions(corpusId);
// partitions.structure_state === 'needs_review', partitions.tray_count === 2

// A pack's members: filter the document listing by its partition key.
const invoices = await sercha.corpusDocuments(corpusId, {
  partitionKey: 'finance/invoices',
});

// Retire a duplicate in favour of its primary. Locks, leaves every
// pack-scoped query, deletes nothing.
await sercha.confirmFlag(corpusId, {
  document_id: copy.id,
  flag: 'duplicate_of',
  target_document_id: primary.id,
  rationale: 'Confirmed identical in review.',
});

// The archive log, and the undo: a human re-assignment restores.
const archived = await sercha.structureArchived(corpusId);
await sercha.assignDocument(corpusId, { document_id: copy.id, container_id: packId });

Two auth tiers, and applications should degrade between them rather than fault: structure, structureTray, assignDocument, rerunStructure, confirmFlag and the corpus CRUD are admin-gated — a default service account receives 403, which means "hide the surface", not "error". corpusDocuments, corpusPartitions and structureArchived need a select grant on the corpus and answer 404 without one, deliberately indistinguishable from a missing corpus. A corpus that does not organise by structure answers 409 with the envelope's code — a property of the corpus, not a fault; route the user elsewhere instead of retrying.

When clustering duplicates by content_hash, an empty or absent hash means "not yet computed" — never treat two empties as a match.

Testing

@sercha-ai/client/testing provides an in-memory implementation of the same interface, for development without a running Sercha and for tests that should not touch the network.

import type { Sercha } from '@sercha-ai/client';
import { StubSercha } from '@sercha-ai/client/testing';

const sercha: Sercha = new StubSercha({
  queries: {
    'SELECT _id, status FROM claims.Claim ORDER BY _id': [
      { _id: '1', status: 'open' },
      { _id: '2', status: 'closed' },
    ],
  },
});

An unconfigured statement throws rather than returning empty: an empty result and a missing fixture are different situations, and conflating them lets a test pass against a stub that was never asked what the code actually queries. Supply onQuery for a catch-all. stub.executed records every statement run, for asserting what the code queried.

Type your application against the Sercha interface rather than SerchaClient, and the stub substitutes with no call-site changes.

Configuration

new SerchaClient({
  baseUrl: 'https://api.acme.sercha.cloud',
  auth: { clientId, clientSecret, scopes: ['query:read', 'genie:use'] },
  timeoutMs: 35_000,        // per request; server deadline is 30s
  streamTimeoutMs: 300_000, // Genie turns; server budget is 5 min
  retry: { attempts: 3, baseDelayMs: 250, maxDelayMs: 10_000 },
  fetch: customFetch,       // defaults to globalThis.fetch
  headers: { 'X-Request-Id': id },
  userAgent: 'acme-app/1.2.3',
});

Pass auth: { token } instead if you manage the token lifecycle yourself.

Retries cover 429 and transient 5xx with exponential backoff and jitter, plus one retry on 401 that re-mints the token first. 4xx and malformed responses are not retried: replaying them cannot succeed and only delays the error.

Errors

Everything extends SerchaError.

| Error | Meaning | | --- | --- | | SerchaConfigError | Unusable configuration; thrown from the constructor. | | SerchaAuthError | Token exchange failed. Carries the OAuth error code. | | SerchaHttpError | Non-2xx. Has status, and code/objectRef on query errors. | | SerchaDecodeError | A 2xx whose body would not parse. | | SerchaTimeoutError | Request exceeded its timeout. | | SerchaRunTimeoutError | A run did not finish in the budget. It is still running. | | PluginConfirmationRequiredError | Re-run with confirm: true to proceed. |

Query errors carry a structured reference to the object that failed:

catch (error) {
  if (error instanceof SerchaHttpError && error.code === 'corpus_not_found') {
    console.error(`No such corpus: ${error.objectRef?.ref}`);
  }
}

Versioning

0.x — the Sercha API is still evolving and minor versions may contain breaking changes. Pin exactly if that matters to you.

Licence

Apache 2.0. Copyright © 2026 Custodia Labs Pty Ltd (ABN 89 688 480 391).

Support: [email protected]