SDKs · 1. TypeScript SDK
@cuecrux/client is a hand-written, zero-dependency TypeScript client for the Crux Daemon's HTTP API. It exports two classes and thirty-three types, covers seventeen daemon endpoints, and requires Node 18 or a modern browser. This chapter is reference: every export, every signature, every thrown error. For the choice between this and the other clients, see chapter 0.
1.1 Install and version
npm install @cuecrux/client
This resolves to 0.1.0. The repository is at 0.2.0 and has not been tagged for release; the difference is three optional fields on VersionResponse.update, documented in 1.6 and flagged inline. Everything else in this chapter is present in the version you install.
| Fact | Value | Source |
|---|---|---|
| Package name | @cuecrux/client | package.json:2 |
| Licence | Apache-2.0: open source | package.json:5 |
| Module format | ESM only. exports["."] declares import and types, no require | package.json:8 |
| Runtime dependencies | None. Native fetch | package.json:20 |
| Minimum Node | 18 | package.json:23 |
| Typings | dist/index.d.ts, emitted by plain tsc with rootDir: ./src | package.json:7 |
There is no CommonJS build. require("@cuecrux/client") fails. If you are on CJS, use a dynamic import() or transpile.
Typings resolve correctly. Verified 2026-07-27: the published 0.1.0 tarball contains package/dist/index.d.ts at the path package.json declares, and the worked example in 1.11 compiles clean under TypeScript 5.7.3 with strict and moduleResolution: nodenext. This is worth stating because the sibling package @cuecrux/engine-client does not, see 6.1.
1.2 Constructing a client
import { CoreCruxClient } from "@cuecrux/client";
interface CoreCruxOptions {
/** Base URL of the CoreCrux daemon (e.g. `http://localhost:14800`). */
baseUrl: string;
/** Bearer token for authentication. */
token?: string;
}
class CoreCruxClient {
constructor(options: CoreCruxOptions);
}
baseUrl is required; there is no default. Trailing slashes are stripped (index.ts:56).
Three headers are set once in the constructor and sent on every request (index.ts:57): Content-Type: application/json, Accept: application/json, and, only when token is truthy, Authorization: Bearer <token> (index.ts:61).
The client reads no environment variables. If you want CRUX_AGENT_TOKEN behaviour you write it yourself; see chapter 3.
There is no option for a custom fetch, a timeout, an AbortSignal, a proxy, a retry policy or additional headers. Requests use the global fetch with no timeout (index.ts:242); a hung daemon hangs your call until the platform's own socket timeout fires.
1.3 Method inventory
Seventeen methods. All are async and return a Promise except subscribeEvents, which is synchronous and returns an EventSource.
| Method | HTTP | Returns | Throws |
|---|---|---|---|
healthz() | GET /healthz | HealthzResponse | CoreCruxError on non-2xx |
readyz() | GET /readyz | ReadyzResponse | CoreCruxError on non-2xx, including 503 |
version() | GET /v1/version | VersionResponse | CoreCruxError on non-2xx |
storeFact(fact) | PUT /v1/facts | Fact | CoreCruxError on non-2xx |
storeFacts(facts) | PUT /v1/facts/bulk | Fact[] | CoreCruxError on non-2xx |
getFact(factId) | GET /v1/facts/{factId} | Fact or null | CoreCruxError on non-2xx other than 404 |
deleteFact(factId) | DELETE /v1/facts/{factId} | boolean | CoreCruxError on non-2xx other than 404 |
getFactsByEntity(entity) | GET /v1/facts/entity/{entity} | { facts: Fact[] } | CoreCruxError on non-2xx |
queryFacts(options?) | GET /v1/facts | FactQueryResult | CoreCruxError on non-2xx |
exportFacts(options?) | GET /v1/facts/export | FactExportResult | CoreCruxError on non-2xx |
putSession(sessionId, state) | PUT /v1/sessions/{id}/state | SessionState | CoreCruxError on non-2xx |
getSession(sessionId) | GET /v1/sessions/{id}/state | SessionState or null | CoreCruxError on non-2xx other than 404 |
textSearch(options) | POST /v1/query/text-search | TextSearchResult | CoreCruxError on non-2xx |
textSearchExpand(options) | POST /v1/query/text-search/expand | TextSearchExpandResult | CoreCruxError on non-2xx |
graphExpand(options) | POST /v1/query/graph-expand | GraphExpandResult | CoreCruxError on non-2xx |
timeRange(options) | POST /v1/query/time-range | TimeRangeResult | CoreCruxError on non-2xx |
subscribeEvents(options?) | GET /v1/events/stream | EventSource | ReferenceError if EventSource is undefined |
Full signatures, verbatim (index.ts:69 onwards):
class CoreCruxClient {
constructor(options: CoreCruxOptions);
healthz(): Promise<HealthzResponse>;
readyz(): Promise<ReadyzResponse>;
version(): Promise<VersionResponse>;
storeFact(fact: StoreFact): Promise<Fact>;
storeFacts(facts: StoreFact[]): Promise<Fact[]>;
getFact(factId: string): Promise<Fact | null>;
deleteFact(factId: string): Promise<boolean>;
getFactsByEntity(entity: string): Promise<{ facts: Fact[] }>;
queryFacts(options?: FactQueryOptions): Promise<FactQueryResult>;
exportFacts(options?: FactExportOptions): Promise<FactExportResult>;
putSession(sessionId: string, state: unknown): Promise<SessionState>;
getSession(sessionId: string): Promise<SessionState | null>;
textSearch(options: TextSearchOptions): Promise<TextSearchResult>;
textSearchExpand(options: TextSearchExpandOptions): Promise<TextSearchExpandResult>;
graphExpand(options: GraphExpandOptions): Promise<GraphExpandResult>;
timeRange(options: TimeRangeOptions): Promise<TimeRangeResult>;
subscribeEvents(options?: { types?: string[] }): EventSource;
}
Four behaviours the signatures do not show
storeFactsunwraps,getFactsByEntitydoes not. The bulk route returns{"facts": [...]}andstoreFactsreturnsresult.facts(index.ts:92). The entity route also returns{"facts": [...]}andgetFactsByEntityreturns the wrapper (index.ts:122). Both match the daemon; the asymmetry is deliberate, and it is why you writeconst { facts } = await client.getFactsByEntity(...).- A 204 returns
undefined, whatever the type says. The request helper short-circuits on 204 and returnsundefined as unknown as T(index.ts:265). No route this client calls returns 204 today, but the cast means a future one would produceundefinedwhere the type promises aFact. - Path segments are escaped, query values are not hand-escaped.
factId,entityandsessionIdgo throughencodeURIComponent; query strings are built withURLSearchParams. deleteFactignores the response body. It returnstruefor any 2xx andfalseonly on a 404 (index.ts:109). The daemon's{"deleted": true}payload is discarded.
1.4 Fact types
interface Fact {
fact_id: string;
entity: string;
key: string;
value: string;
source_receipt: string | null;
confidence: number;
stored_at: string;
tokens: number;
deleted: boolean;
version: number;
supersedes: string | null;
private: boolean;
}
interface StoreFact {
entity: string;
key: string;
value: string;
source_receipt?: string;
confidence?: number;
private?: boolean;
}
interface FactQueryOptions {
query?: string;
entity?: string;
entity_prefix?: string;
top_k?: number;
token_budget?: number;
}
interface FactQueryResult {
facts: Fact[];
total_tokens: number;
}
interface FactExportOptions {
since?: string;
cursor?: string;
limit?: number;
}
interface FactExportResult {
facts: Fact[];
next_cursor: string | null;
has_more: boolean;
exported_at: string;
}
Source: types.ts:16 to types.ts:64.
Three things to know about StoreFact before you use it.
private: true is rejected on this route. The daemon returns 400 Bad Request with the detail private facts require MCP agent identity; HTTP /v1/facts does not support private=true (facts.rs:229). The bulk route rejects the same way (facts.rs:492). The field exists in the type because it appears in responses, not because you can set it here.
There is no horizon_class and no actor field. The daemon accepts both, SDKCrux's @cuecrux/memory sets them (5.3)
- but this SDK's
StoreFactdoes not carry them, so freshness-decay class and durable authorship
cannot be set from @cuecrux/client. Use a raw fetch if you need them.
Writing the same (entity, key) twice creates a second version, not an update in place. The store assigns version = previous + 1 and sets supersedes to the previous fact_id (fact_store.rs:882, fact_store.rs:1137). This is why storeFact is not safe to retry blindly, see 4.5.
1.5 Session types
interface SessionState {
session_id: string;
state: unknown;
updated_at: string;
total_tokens: number;
expires_at: string | null;
}
putSession(sessionId, state) takes state: unknown, any JSON-serialisable value. It is sent as the whole request body, not wrapped (index.ts:151). getSession returns null on 404 rather than throwing (index.ts:156).
Source: types.ts:68.
1.6 Health and version types
interface BuildInfo {
version: string;
commit: string;
}
interface HealthzResponse {
ok: boolean;
build: BuildInfo;
compat: Record<string, unknown>;
sdk_version: string;
routing: Record<string, unknown> | null;
valves: Record<string, unknown> | null;
}
interface ReadyzCheck {
name: string;
ok: boolean;
error: string | null;
}
interface ReadyzResponse {
ok: boolean;
checks?: ReadyzCheck[];
}
interface VersionResponse {
version: string;
commit: string;
msrv: string;
features: {
text_search: boolean;
graph_expand: boolean;
self_observe: boolean;
mcp: boolean;
};
sync?: {
mode: string;
configured: boolean;
background_sync_enabled: boolean;
remote_url: string;
api_key_configured: boolean;
degraded: boolean;
degraded_reason?: string | null;
};
update?: {
enabled: boolean;
state:
| "disabled"
| "current"
| "behind"
| "ahead"
| "diverged"
| "unavailable"
| "error";
remote: string;
ref: string;
tracking_ref: string;
repo_dir?: string | null;
current_commit?: string | null;
latest_commit?: string | null;
ahead_by: number;
behind_by: number;
checked_at?: string | null;
error?: string | null;
comparison_stale?: boolean;
basis?: "binary" | "checkout" | string; // added in 0.2.0, not yet published
binary_commit?: string | null; // added in 0.2.0, not yet published
checkout_commit?: string | null; // added in 0.2.0, not yet published
checkout_ahead_by?: number;
checkout_behind_by?: number;
upgrade_hint: string;
};
}
Source: types.ts:78 to types.ts:151.
The three fields marked as 0.2.0 additions are absent from the published 0.1.0 typings. They are optional, so code that does not reference them compiles against either version. basis tells you whether the ahead/behind counts describe the running binary or the source checkout; binary_commit and checkout_commit are admin-only and will be absent for an ordinary caller.
readyz() throws on failure. A not-ready daemon answers 503 with {"ok": false, "checks": [...]} (health.rs:74), and every non-2xx becomes a CoreCruxError. To read the failing checks you catch the error, not the return value. See 3.6.
Detail in /healthz and /readyz can be stripped by the operator: with CORECRUXD_PUBLIC_PROBES_MINIMAL set, the per-check breakdown is withheld and only ok: false survives (health.rs:64). Default is off, full detail. FLAG.
1.7 Query types
interface TextSearchOptions {
tenant_id: string;
query: string;
limit?: number;
token_budget?: number;
min_score?: number;
mode?: "normal" | "scan";
}
interface TextSearchHit {
segment_index: number;
doc_id: number;
score: number;
frame_offset: number;
token_count: number;
}
interface TextSearchGap {
query_terms: string[];
match_quality: string;
suggestion: string;
}
interface TextSearchCoverage {
score: number;
gaps: TextSearchGap[];
below_floor: number;
}
interface TextSearchResult {
results: TextSearchHit[];
coverage: TextSearchCoverage;
meta: {
backend: string;
took_ms: number;
segments_searched: number;
total_docs: number;
total_candidates?: number;
};
tokens_used?: number;
tokens_available?: number;
results_omitted?: number;
scan_mode?: boolean;
}
interface TextSearchExpandOptions {
tenant_id: string;
result_ids: { segment_index: number; doc_id: number }[];
}
interface TextSearchExpandResult {
chunks: {
segment_index: number;
doc_id: number;
frame_offset: number;
token_count: number;
}[];
tokens_loaded: number;
}
interface GraphExpandOptions {
tenant_id: string;
seed_artifact_ids: number[];
edge_types?: string[];
max_hops?: number;
budget?: number;
min_confidence?: number;
include_state?: boolean;
}
interface GraphExpandArtifact {
artifact_id: number;
score: number;
hop_distance: number;
edge_types_used: string[];
state?: {
living_status: string;
confidence: number;
updated_at_micros: number;
trunk_tier: number;
};
}
interface GraphExpandResult {
artifacts: GraphExpandArtifact[];
traversal_stats: {
nodes_visited: number;
hops_used: number;
budget_remaining: number;
edges_traversed: number;
};
}
interface TimeRangeOptions {
tenant_id: string;
start_micros: number;
end_micros: number;
artifact_ids?: number[];
include_relations?: boolean;
limit?: number;
}
interface TimeRangeArtifact {
artifact_id: number;
living_status: string;
confidence: number;
updated_at_micros: number;
relations_changed: {
src_artifact_id: number;
dst_artifact_id: number;
relation_type: string;
confidence: number;
created_at_micros: number;
updated_at_micros: number;
}[];
relation_change_count: number;
}
interface TimeRangeResult {
artifacts_changed: TimeRangeArtifact[];
scan_stats: {
artifacts_scanned: number;
relations_scanned: number;
total_changes: number;
};
}
Source: types.ts:155 to types.ts:286.
tenant_id is required on all four query calls. The options object is sent as the request body verbatim, the client does not rename, default or validate any field (index.ts:173); the daemon applies its own defaults for the optional ones.
textSearch is a lexical BM25 search over indexed segments. It is not a vector search. The coverage block is the honest part of the response: score and gaps tell you where the query terms went unmatched, which is more useful than the hit list when a search under-performs.
1.8 Event types
type CruxEventType = "fact.stored" | "fact.deleted" | "session.stored" | "session.deleted";
interface CruxEventFactStored {
type: "fact.stored";
fact_id: string;
entity: string;
key: string;
}
interface CruxEventFactDeleted {
type: "fact.deleted";
fact_id: string;
}
interface CruxEventSessionStored {
type: "session.stored";
session_id: string;
}
interface CruxEventSessionDeleted {
type: "session.deleted";
session_id: string;
}
type CruxEvent =
| CruxEventFactStored
| CruxEventFactDeleted
| CruxEventSessionStored
| CruxEventSessionDeleted;
Source: types.ts:290.
CruxEvent is a discriminated union on type, so a switch over it narrows correctly. Note that subscribeEvents returns a raw EventSource and does not apply these types, e.data is a string you parse and assert yourself.
1.9 Server-sent events, and why they are unauthenticated
const es = client.subscribeEvents({ types: ["fact.stored", "fact.deleted"] });
es.addEventListener("fact.stored", (e) => {
const event = JSON.parse(e.data) as CruxEventFactStored;
console.log(event.fact_id, event.entity, event.key);
});
es.onerror = () => console.error("SSE connection error");
es.close();
subscribeEvents builds GET /v1/events/stream, joins types with commas, and returns new EventSource(url) (index.ts:214).
The bearer token is not sent. Native EventSource cannot carry custom headers; the source says so at index.ts:222. Against a daemon that requires auth on the events route, this connection fails. Your options are a polyfill that supports headers, or a reverse proxy that injects the credential. There is no third option in this SDK.
EventSource is not global before Node 22. On Node 18 to 21 the call throws a ReferenceError unless you install a polyfill and assign it to globalThis.EventSource first.
1.10 The error model
class CoreCruxError extends Error {
readonly status: number;
readonly problem: ProblemDetails | null;
constructor(status: number, message: string, problem?: ProblemDetails | null);
// name === "CoreCruxError"
}
interface ProblemDetails {
type: string;
title: string;
status: number;
detail?: string;
instance?: string;
extensions?: Record<string, unknown>;
}
Source: index.ts:36 and types.ts:323.
problem is populated only when the response content-type contains json and the parsed body carries both title and status (index.ts:250). Otherwise it is null and message falls back to CoreCrux API error: <status> <statusText>. When a problem body is present, message becomes problem.detail ?? problem.title.
Two mismatches worth knowing, neither of which will break you.
The extensions field never populates. The daemon flattens problem extensions such as code and missingScopes to the top level of the JSON body, not into a nested object (corecrux-types/src/lib.rs:798). err.problem.extensions will therefore be undefined even when extension members are present. Read them by casting: (err.problem as Record<string, unknown>).code.
RFC 9457 versus RFC 7807. This SDK cites 9457 (index.ts:34); the daemon's developer portal cites 7807 (developer-portal.md:13). 9457 obsoletes 7807 and the wire format is identical. Nothing to do.
Error handling in practice, and which statuses mean what, is chapter 4.
1.11 A worked example that compiles
This is the exact file used to verify the published package on 2026-07-27. It compiles with zero errors under TypeScript 5.7.3, strict: true, module: nodenext, moduleResolution: nodenext, against @cuecrux/client@0.1.0 installed from npm.
import { CoreCruxClient, CoreCruxError } from "@cuecrux/client";
import type { Fact, FactQueryResult, TextSearchResult } from "@cuecrux/client";
const client = new CoreCruxClient({
baseUrl: process.env.CRUX_DAEMON_URL ?? "http://127.0.0.1:14800",
token: process.env.CRUX_AGENT_TOKEN,
});
async function main(): Promise<void> {
const stored: Fact = await client.storeFact({
entity: "execplan:sdk-docs",
key: "decision:client-choice",
value: "Use @cuecrux/client for daemon access.",
confidence: 0.9,
});
const hits: FactQueryResult = await client.queryFacts({
query: "client choice",
entity_prefix: "execplan:",
top_k: 5,
token_budget: 500,
});
console.log(hits.facts.length, hits.total_tokens);
const { facts } = await client.getFactsByEntity("execplan:sdk-docs");
console.log(facts.length);
const search: TextSearchResult = await client.textSearch({
tenant_id: "my-tenant",
query: "deployment architecture",
limit: 10,
token_budget: 4096,
});
console.log(search.coverage.score, search.meta.backend);
try {
await client.getFact(stored.fact_id);
} catch (err) {
if (err instanceof CoreCruxError) {
console.error(err.status, err.problem?.type);
}
throw err;
}
const removed: boolean = await client.deleteFact(stored.fact_id);
console.log(removed);
}
void main();
Note the two patterns that trip people up on the first attempt: getFactsByEntity is destructured because it returns a wrapper, and token is passed as string | undefined because CoreCruxOptions.token is optional, reading it from process.env needs no non-null assertion.
1.12 Versioning and release
| Rule | Detail | Source |
|---|---|---|
| Scheme | Strict semver, but pre-1.0: a minor may break, a patch is safe | sdk-release-lifecycle.md:27 |
| Coupling to the daemon | None. Daemon v* tags build the package but cannot publish it | sdk-release-lifecycle.md:17 |
| Publish trigger | Push of an sdk-typescript-vX.Y.Z tag, and the tag must equal package.json version or the run fails | sdk-typescript.yml:31 |
| Credentials | None held. npm Trusted Publishing via OIDC, id-token: write on the publish job only | sdk-typescript.yml:63 |
| Artifact identity | The published tarball is the exact npm pack output from the build job, downloaded as an artifact | sdk-typescript.yml:78 |
| Provenance | npm publish --provenance, verifiable with npm audit signatures | sdk-typescript.yml:79 |
| Immutability | Fail-closed. skip-existing is prohibited; a broken release is superseded by a patch, never unpublished | sdk-release-lifecycle.md:76 |
| Support window | Current and previous minor while 0.x; older lines get security fixes for six months | sdk-release-lifecycle.md:32 |
What this does not give you: the package carries no declaration of which daemon version it was tested against, in its README or its metadata, though the policy requires one (sdk-release-lifecycle.md:22). There is no compatibility matrix. If you need certainty, call version() and compare against the daemon you are pointed at.
Sources
- sdks/typescript/src/index.ts:36,
CoreCruxError - sdks/typescript/src/index.ts:54, constructor and headers
- sdks/typescript/src/index.ts:230, the request helper, error mapping, 204 cast
- sdks/typescript/src/types.ts:16, all 33 exported types
- sdks/typescript/package.json:8, ESM-only exports map
- .github/workflows/sdk-typescript.yml:31, tag-version gate
- crates/corecruxd/src/http/facts.rs:229, 400 on
private: true - crates/corecrux-memory/src/fact_store.rs:882, version chaining on repeat writes
- crates/corecruxd/src/http/health.rs:74, 503 from
/readyz - Compilation of 1.11 verified 2026-07-27 against
@cuecrux/client@0.1.0from npm, TypeScript 5.7.3

