Skip to main content

TypeScript SDK

@mindd/client is a thin TypeScript/JavaScript client. It wraps the generated protobuf-es / Connect stubs with idiomatic types and injects the capability token on every call. It speaks standard gRPC over HTTP/2, so it talks to the same server as mindctl, the Go stubs, and the Python SDK.

Source lives in sdk/typescript/.

Install

The package is ESM only and targets Node.js 20 or newer (the gRPC transport uses node:http2). It ships type declarations.

npm install @mindd/client

Or from the repository:

cd sdk/typescript
npm install
npm run build

Runtime dependencies are @bufbuild/protobuf, @connectrpc/connect and @connectrpc/connect-node. ai is an optional peer dependency, needed only for the Vercel AI SDK adapter under the /ai subpath.

The SDK versions and releases independently of the server, from the typescript-sdk-v* tag prefix.

Hello world

import { MindD } from "@mindd/client";

const m = new MindD("127.0.0.1:7777", { token: process.env.MINDD_TOKEN! });
const enc = new TextEncoder();
const dec = new TextDecoder();

await m.kv.put("scratchpad", "hello", enc.encode("world"), { ttlSeconds: 300 });
const rec = await m.kv.get("scratchpad", "hello");
console.log(dec.decode(rec.value)); // "world"

address is a host:port or a full http(s):// URL.

OptionDefaultMeaning
tokennone, requiredcapability token sent on every call
tlsfalseuse https/h2 instead of plaintext h2c
interceptors[]extra Connect interceptors, applied after the capability one
const m = new MindD("mem.internal:443", { token, tls: true });

The token is shared across the seven sub-clients: m.kv, m.episodic, m.semantic, m.artifact, m.lease, m.graph and m.admin. The transport is on m.transport if you need to build a raw Connect client.

Per-block surface

Values are Uint8Array. Durations are plain numbers of seconds (ttlSeconds, waitForSeconds), not Duration messages. Streaming reads are AsyncIterable, so for await works directly.

KV

await m.kv.put(ns, key, value, { ttlSeconds, contentType, metadata, ifVersion });
const rec = await m.kv.get(ns, key); // { found, value, version, ... }
const items = await m.kv.multiGet(ns, ["k1", "k2"]);
await m.kv.delete(ns, key, { ifVersion });

for await (const item of m.kv.scan(ns, { keyPrefix: "", limit: 100, startAfter: "" })) {
// startAfter is the keyset resume cursor
}

Episodic

const ev = await m.episodic.append("events", "tool_call", {
payload: enc.encode("..."),
role: "assistant",
sessionId: "sess-1",
dedupKey: "msg-42", // idempotent under retry
supersedes: [],
source: "",
});

for await (const e of m.episodic.range("events", { limit: 10, sessionId: "sess-1" })) { }
for await (const e of m.episodic.tail("events", { includeHistorical: true, afterCursor: 0n })) { }

const affected = await m.episodic.expire("events", {
beforeCursor: 1000n,
action: EpisodicExpireAction.SOFT_DELETE,
maxRows: 500,
});

Semantic

import { SearchMode, PredicateOp } from "@mindd/client";

await m.semantic.upsert("notes", [{ id: "a", content: "apple" }]);

const hits = await m.semantic.search("notes", {
queryText: "apple",
topK: 3,
mode: SearchMode.HYBRID,
rerankCandidateK: 50,
});

await m.semantic.delete("notes", "a"); // soft delete by default
await m.semantic.delete("notes", "a", { hard: true });
const n = await m.semantic.expire("notes", { filter: { topic: "food" }, action, maxRows: 100 });

The lifecycle fields (validFrom, validTo, supersedes, source, ifVersion) go on the record objects passed to upsert; asOf and includeInvalidated go on search. See Semantic.

Artifact

const ref = await m.artifact.put("blobs", bytes, { contentType: "image/png", id: "render-1" });
const got = await m.artifact.get("blobs", ref.id, { offset: 0, length: 0 });
const meta = await m.artifact.stat("blobs", ref.id);
await m.artifact.delete("blobs", ref.id);

for await (const meta of m.artifact.list("blobs", { filter: { kind: "render" }, limit: 100 })) { }

put handles the client-streaming chunking; get reassembles the server stream into one Uint8Array.

Lease

const handle = await m.lease.acquire("locks", "job-42", { ttlSeconds: 30 });
await m.lease.renew(handle.holderId, "locks", "job-42", { ttlSeconds: 300 });
await m.lease.release(handle.holderId, "locks", "job-42");

await m.lease.inspect("locks", "job-42");
await m.lease.list("locks");

Graph

import { Direction } from "@mindd/client";

await m.graph.upsertNodes("knowledge", [{ id: "alice", labels: ["Person"] }]);
await m.graph.upsertEdges("knowledge", [
{ id: "e1", type: "AUTHORED", from: "alice", to: "doc-1" },
]);

const node = await m.graph.getNode("knowledge", "alice");
const nb = await m.graph.neighbors("knowledge", "alice", {
edgeTypes: ["AUTHORED"],
direction: Direction.OUT,
limit: 50,
});
const sub = await m.graph.traverse("knowledge", "alice", { depth: 2, maxNodes: 100 });

await m.graph.deleteNode("knowledge", "doc-1", { cascade: true });
await m.graph.deleteEdge("knowledge", "e1");

Unlike Python, from is not a reserved word here, so EdgeInput uses a plain from field.

Admin

const resp = await m.admin.listNamespaces(); // requires the admin.inspect op
console.log(resp.server?.version);

Vercel AI SDK adapter

@mindd/client/ai implements the AI SDK's chat-persistence convention on the kv block: one key per chat holding the full UIMessage[]. Importing the base client never pulls in ai.

import { MindD } from "@mindd/client";
import { createChatStore } from "@mindd/client/ai";

const store = createChatStore(new MindD("127.0.0.1:7777", { token }), {
namespace: "chats",
});

const chatId = await store.createChat();
const messages = await store.loadChat(chatId);

Wire saveChat into a streamed response so every turn is persisted:

import { streamText, convertToModelMessages } from "ai";

const result = streamText({ model, messages: convertToModelMessages(messages) });
return result.toUIMessageStreamResponse({
originalMessages: messages,
onFinish: ({ messages }) => store.saveChat({ chatId, messages }),
});

The store also exposes deleteChat(id) and listChats(). It needs a token with kv.get, kv.put, kv.delete and kv.scan on the chat namespace. See Framework adapters.

Exports

Beyond MindD, the package exports capabilityInterceptor and CAPABILITY_HEADER for building your own Connect clients, the runtime enums SearchMode, PredicateOp, ExpireAction, EpisodicExpireAction and Direction, and the generated message types (KVItem, Event, SemanticRecord, Hit, ArtifactRef, ArtifactMeta, LeaseHandle, GraphNode, GraphEdge, Subgraph, NamespaceInfo, and others).

Regenerating stubs

The stubs under src/gen come from proto/ via the buf-native protobuf-es plugin:

make proto-ts # buf generate --template buf.gen.ts.yaml

Test

cd sdk/typescript
npm test # builds, then runs the offline smoke test