@contextq/sdk
v0.3.0
Published
Official TypeScript client for the ContextQ knowledge-management API — memory, contexts, search, and agent boot in a few lines.
Downloads
232
Maintainers
Readme
@contextq/sdk
Official TypeScript client for the ContextQ knowledge-management API — a thin, typed wrapper over the REST surface with retries and zero runtime dependencies.
Targets the ContextQ API surface as of 2026-07-03 (server commit 0f8a6b6, the T338 conversation-native memory
API on development). See "Version note" below.
Install
npm install @contextq/sdkRequires Node 18+ (uses the global fetch/AbortController; no polyfills bundled).
Auth
Every request is authenticated with a ContextQ API key (sk_live_<40 hex>), sent as Authorization: Bearer.
A new account's first key is shown once on the dashboard's Connect an Agent page; create more at API Keys
(POST /dashboard/api-keys if you're driving the dashboard API directly). Every plan can create keys up to its active-key cap: Free 3, Pro 20, Team and Enterprise unlimited (keys that back MCP OAuth connectors do not count). Keys carry scopes (read, write, admin) — the default key scopes (read, write)
cover every method in this SDK.
import { ContextQ } from '@contextq/sdk';
const ctx = new ContextQ({
apiKey: process.env.CONTEXTQ_API_KEY!,
baseUrl: process.env.CONTEXTQ_BASE_URL ?? 'https://app.contextq.dev', // or your self-hosted URL
});| Option | Default | Notes |
|---|---|---|
| apiKey | required | sk_live_... |
| baseUrl | https://app.contextq.dev | point at http://localhost:38200 for local dev, or your self-hosted instance |
| maxRetries | 3 | retry attempts after the initial try, for 429/5xx/network failures |
| timeoutMs | 30000 | per-request timeout; 0 disables |
| headers | {} | merged into every request (e.g. a reverse-proxy auth header) |
| fetch | global fetch | override for tests / non-Node runtimes |
The 5 core flows
1. Memory (Mem0-parity conversational memory)
// Extract durable facts from a raw conversation and upsert them, deduped, per end-user.
const result = await ctx.memory.add(
[
{ role: 'user', content: "Hi, I'm Alex. My timezone is PST." },
{ role: 'assistant', content: 'Got it, Alex.' },
],
{ subjectId: 'user-42', workspace: 'acme' }, // Mem0's user_id — scopes dedup + every created memory to this end-user
);
// result: { created: [...], updated: [...], archived: [...], skipped: [...], extractedClaims, dryRun }
// Conversations over the server's async threshold (default 8 messages) return
// { jobId, statusUrl } instead — check `'jobId' in result`.
// Look memories back up for that end-user.
const found = await ctx.memory.search('timezone', { subjectId: 'user-42' });2. Contexts (direct CRUD, when you're not going through the conversational memory API)
const note = await ctx.contexts.create({
name: 'Q3 roadmap decision',
description: 'Why we picked option B',
content: '...',
type: 'reference',
workspace: 'acme',
tags: ['roadmap'],
});
const fetched = await ctx.contexts.get(note.id);
const updated = await ctx.contexts.update(note.id, { description: 'Updated rationale' });
const archived = await ctx.contexts.archive(note.id); // convenience for update(id, { lifecycle: 'archived' })
await ctx.contexts.delete(note.id); // hard delete3. Search (full hybrid search: PG tsvector + Elasticsearch BM25 + RRF fusion)
const hits = await ctx.search.query('dark mode preference', {
workspace: 'acme',
tags: ['ui'],
});4. Agent boot (token-budgeted context pack for an autonomous agent session)
const pack = await ctx.agent.boot('my-agent-slug', {
goal: 'Ship the billing migration',
workspace: 'acme',
tokenBudget: 4000,
});
// pack.resume, pack.lessons, pack.facts, pack.repo_map, ...5. Retrieve (passages + memories + citations, one round-trip — for RAG/chatbot backends)
const result = await ctx.retrieve.query('refund policy', {
workspace: 'acme',
subjectId: 'user-42', // scopes the separate `memories` array only, never `passages`
top_k: 8,
});
// result: { passages, memories, groupMemories, traceId }
// Each passage/memory carries a `citation` (source_type, title, heading_path, page/sheet/table, ...).Documents & batch/zip import
ctx.documents wraps the document-intake API (POST /api/documents, POST /api/documents/batch, and
GET /api/documents/batches[/:id]). Every uploaded file is parsed, chunked, and embedded asynchronously by the
server — upload()/uploadZip()/importFolder() all return immediately (202 Accepted) with job/batch ids to
poll.
// A handful of files -- goes through POST /api/documents (max 20 files/request).
const uploaded = await ctx.documents.upload(
[{ filename: 'handbook.pdf', data: fs.readFileSync('./handbook.pdf') }],
{ workspace: 'acme' },
);
// uploaded.jobs[0].statusUrl -> poll with ctx documents' underlying ingest-jobs route, or just poll the batch below.
// A whole local folder -- reads every file under it, bundles them into an
// in-memory zip (no temp files, no external zip binary), and uploads via
// POST /api/documents/batch in one call. Node-only (uses node:fs/promises).
const batchUpload = await ctx.documents.importFolder('./knowledge-base', {
workspace: 'acme',
});
console.log(batchUpload.batchId, batchUpload.jobs.length, batchUpload.rejected);
// Poll the whole batch's progress with ONE id instead of N per-file ids.
const status = await ctx.documents.getBatch(batchUpload.batchId);
// status: { id, status: 'processing'|'completed'|'completed_with_errors'|'failed',
// totalFiles, processedFiles, failedFiles, createdAt, completedAt }
// Already have a zip in memory (e.g. built by another tool)? Skip importFolder
// and upload it directly -- up to 50 files per archive, 50MB per file.
const zipBytes = fs.readFileSync('./export.zip');
const fromZip = await ctx.documents.uploadZip(zipBytes, { workspace: 'acme', filename: 'export.zip' });
// List recent batches for the authenticated tenant.
const recent = await ctx.documents.listBatches();Instead of polling, subscribe to the ingest.completed webhook event so your own backend gets pinged the moment
every file in a batch finishes (parsed, failed, deduped, or rejected):
// One-time setup via the REST API directly (no dedicated SDK helper yet):
await fetch(`${baseUrl}/api/subscriptions`, {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'batch-import-notifier',
transport: 'webhook',
webhook_url: 'https://your-service.example.com/hooks/contextq',
topic_filter: 'ingest.completed',
}),
});Your webhook receives an HMAC-signed (X-ContextQ-Signature: sha256=...) POST with a body shaped like:
{
"event": "ingest.completed",
"topic": "ingest.completed",
"resource": "document_batch:123",
"data": {
"batchId": 123,
"source": "knowledge-base.zip",
"status": "completed_with_errors",
"totalFiles": 50,
"processedFiles": 47,
"failedFiles": 3
},
"emitted_at": "2026-07-06T12:00:00.000Z"
}Errors
Non-2xx responses (after retries) throw ContextQError with status, body, code, and path. Pure network
failures throw ContextQNetworkError.
import { ContextQ, ContextQError } from '@contextq/sdk';
try {
await ctx.contexts.get(999999);
} catch (err) {
if (err instanceof ContextQError && err.status === 404) {
console.log('not found');
} else {
throw err;
}
}Retries & idempotency
429, 500, 502, 503, 504, and network errors are retried with exponential backoff + jitter (base 300ms,
capped at 8s), up to maxRetries (default 3) additional attempts. A Retry-After response header (seconds or an
HTTP-date) takes precedence over the computed backoff.
Retries apply to every HTTP method, including POST. The server has no idempotency-key mechanism yet, so in the
rare case where a POST /api/memory or POST /api/contexts request succeeds server-side but the response is lost
to a network error, a retry could create a duplicate. memory.add's dedup pass (kNN + diff) mitigates this in
practice for the memory flow; for contexts.create, prefer contexts.update-style idempotent flows where
duplication would matter.
Examples
See examples/:
quickstart.mjs— add a memory, search for it, fetch it back (the 5-line core flow).contexts-and-agent.mjs— direct context CRUD (create/update/archive), search, and agent boot.
Run either after building:
npm run build
CONTEXTQ_API_KEY=sk_live_... CONTEXTQ_BASE_URL=http://localhost:38200 node examples/quickstart.mjsVersion note
Types in src/types.ts are hand-mirrored from the server's Zod schemas and TypeScript interfaces
(src/routes/memory.ts, src/routes/contexts.ts, src/routes/search.ts, src/routes/agent.ts,
src/services/memory.service.ts, src/services/agent-boot.service.ts) as of 2026-07-03 — there is no shared
package or codegen step between the server and this SDK yet. If the server's request/response shapes change,
this file needs a matching update; it is not automatically kept in sync.
Build
Dual ESM + CJS via tsup, zero runtime dependencies (only the global fetch).
npm install
npm run build # emits dist/index.js (ESM), dist/index.cjs (CJS), and .d.ts/.d.cts
npm run typecheck
npm pack # produces contextq-sdk-0.1.0.tgz