#TypeScript Client

The @memory.build/client package provides programmatic access to Memory Engine from TypeScript and JavaScript.

#Install

npm install @memory.build/client

#Two clients

The package exposes two clients, matching the two API endpoints:

  • createMemoryClient — the space data plane plus space management. Talks to POST /api/v1/memory/rpc, carrying the active space in the X-Me-Space header. Authenticates with a session token, OAuth token, user PAT, or service-account API key. Namespaces: memory, principal, group, grant, invite.
  • createUserClient — user/account and service-account management. Talks to POST /api/v1/user/rpc. Authenticates with a session/OAuth token or the user's own PAT; service-account keys cannot manage accounts or mint keys. Methods: whoami, plus the serviceAccount, apiKey, and space namespaces.

This package does not provide login helpers — obtain a credential out of band (me login for a session/OAuth token, or me apikey create for a PAT/service-account key) and pass it to the client. (The web app signs in separately via better-auth/react's createAuthClient, which establishes an httpOnly cookie session.)

#Quick start

import { createMemoryClient } from "@memory.build/client";

const me = createMemoryClient({
  url: "https://api.memory.build",  // default
  token: sessionTokenOrApiKey,      // session token or "me.<lookupId>.<secret>"
  space: "abc123def456",            // the X-Me-Space slug
});

// Create a memory (tree is required — choose /share/* or ~/* deliberately)
await me.memory.create({
  content: "TypeScript was released in 2012",
  tree: "/share/knowledge/programming",
});

// Search
const { results } = await me.memory.search({
  semantic: "when was TypeScript created",
});

#Configuration

const me = createMemoryClient({
  url: "https://api.memory.build",  // default
  token: "me.xxx.yyy",              // session token or api key (format: "me.<lookupId>.<secret>")
  space: "abc123def456",            // active space slug (sent as X-Me-Space)
  timeout: 30000,                   // request timeout in ms (default: 30000)
  retries: 3,                       // automatic retries (default: 3)
});

The client retries on 429, 500, 502, 503, and 504 responses with exponential backoff and jitter, and respects the Retry-After header. The token and space can be swapped at runtime:

me.setToken("me.newkey.newsecret");
me.setSpace("otherslug1234");

#Memory operations

#create

tree is required. Use /share/* for memories the rest of the space should see, or ~/* for your private home. An optional name (a filename-like slug, unique within the tree) lets you address the memory by path. onConflict governs a clash on the idempotency key (the id if given, else the (tree, name) slot): "error" (default), "replace" (content-aware), or "ignore".

const memory = await me.memory.create({
  content: "The fact to remember",
  tree: "/share/work/projects/acme",   // required (leading slash optional on input)
  name: "kickoff",                     // optional, unique within the tree
  meta: { source: "meeting-notes" },   // optional JSON metadata
  temporal: {                          // optional time range
    start: "2025-01-01T00:00:00Z",
    end: "2025-01-31T23:59:59Z",
  },
  onConflict: "replace",               // optional; default "error"
});
// memory.id, memory.content, memory.tree, memory.name, memory.meta, ...

#batchCreate

Create up to 1,000 memories in a single call. Each memory requires a tree. A batch-level onConflict applies to every row (importers pass "replace" or "ignore").

const { results } = await me.memory.batchCreate({
  memories: [
    { content: "First memory", tree: "/share/notes" },
    { content: "Second memory", tree: "/share/notes", name: "second" },
  ],
  onConflict: "ignore", // optional; default "error"
});
// `results` has one { id, status } per input, in order — status is
// "inserted" | "updated" | "skipped". Filter by status for counts/ids:
const insertedIds = results
  .filter((r) => r.status === "inserted")
  .map((r) => r.id);

#get / getByPath

const memory = await me.memory.get({ id: "019..." });
// Or address a named memory by its tree/name path:
const byPath = await me.memory.getByPath({ path: "/share/auth/jwt-rotation" });

#update

Only provided fields are changed. Pass null to clear optional fields (e.g. name: null clears the name). Update is id-addressed.

const updated = await me.memory.update({
  id: "019...",
  content: "Updated content",
  name: "jwt-rotation",   // set/rename; null clears
  meta: { reviewed: true },
});

#delete / deleteByPath

const { deleted } = await me.memory.delete({ id: "019..." });
// Or delete a named memory by its tree/name path:
await me.memory.deleteByPath({ path: "/share/auth/jwt-rotation" });

#deleteTree

Delete all memories under a tree prefix.

const { count } = await me.memory.deleteTree({ tree: "/share/old/project", dryRun: true });
const { count: deleted } = await me.memory.deleteTree({ tree: "/share/old/project" });

#move

Move memories from one tree prefix to another, preserving subtree structure.

const { count } = await me.memory.move({
  source: "/share/drafts/api",
  destination: "/share/published/api",
});

#tree

View the hierarchical tree structure with counts at each node.

const { nodes } = await me.memory.tree();
// [{ path: "/share", count: 5 }, { path: "/share/work", count: 3 }, ...]

const { nodes } = await me.memory.tree({ tree: "/share/work", levels: 2 });

The search method supports keyword, semantic, and hybrid search with multiple filter types.

const { results } = await me.memory.search({
  // Search modes (use one or both for hybrid)
  semantic: "natural language meaning query",
  fulltext: "exact keyword BM25 match",

  // Filters (all optional, combined with AND)
  grep: "regex.*pattern",              // POSIX regex on content
  tree: "/share/work/projects/*",       // ltree/lquery filter
  meta: { source: "meeting-notes" },   // JSONB containment
  metaPredicate:                       // PostgreSQL JSONPath Boolean predicate
    '$.priority >= 3 && !exists($.archivedAt)',
  temporal: {                          // time-based filter
    after: "2025-06-01T00:00:00Z",
    before: "2025-07-01T00:00:00Z",   // every populated mode must match
    // contains: "2025-06-15T00:00:00Z"
    // overlaps: { start, end }
    // within: { start, end }
  },

  // Tuning
  limit: 10,                           // max results (1-1000)
  candidateLimit: 30,                  // candidates per mode before RRF fusion
  semanticThreshold: 0.7,              // optional min cosine similarity in [0,1] (higher = stricter; rejected if out of range)
  weights: { semantic: 0.7, fulltext: 0.3 },
  orderBy: "desc",                     // for filter-only queries (no search)
});

for (const { memory, score } of results) {
  console.log(score, memory.content);
}

#Space management

The memory client also exposes the in-space management namespaces. These require the appropriate authority (admin for roster/groups/invites; owner@path for grants). See Access Control.

#space — the active space

const { members } = await me.space.listMembers({ kind: "u" }); // any member

#principal — the roster

const { principals } = await me.principal.list();            // admin only
await me.principal.add({ principalId: "019..." });
await me.principal.remove({ principalId: "019..." });
const { principals } = await me.principal.resolve({ name: "[email protected]" });    // any member
const { principals } = await me.principal.lookup({ ids: ["019..."] });               // any member

#group

const group = await me.group.create({ name: "backend" });
const { groups } = await me.group.list();
await me.group.rename({ groupId: group.id, name: "backend-team" });
await me.group.delete({ groupId: group.id });
await me.group.addMember({ groupId: group.id, memberId: "019...", admin: false });
await me.group.removeMember({ groupId: group.id, memberId: "019..." });
const { members } = await me.group.listMembers({ groupId: group.id });
const { groups } = await me.group.listForMember({ memberId: "019..." });

#grant — tree access

Levels are 1 (read), 2 (write), 3 (owner).

await me.grant.set({ principalId: "019...", treePath: "/share/work", access: 2 });
await me.grant.remove({ principalId: "019...", treePath: "/share/work" });
const { grants } = await me.grant.list();                         // optionally { principalId } / { treePath }
// Enumerating others' grants needs admin / path owner; passing your own principalId
// is self-service — any member can list their own grants.

#invite

// shareAccess is a level number (1=read, 2=write, 3=owner) at the shared root; null/omit = none
const invite = await me.invite.create({ email: "[email protected]", admin: false, shareAccess: 1 });
const { invitations } = await me.invite.list();
await me.invite.revoke({ email: "[email protected]" });

#User-scoped operations

Use createUserClient for identity, service accounts, API keys, and space discovery. API-key minting and revocation still require a human session/OAuth credential; keys cannot mint or revoke other keys.

import { createUserClient } from "@memory.build/client";

const user = createUserClient({ token: sessionTokenOrUserPat });

// Identity
const me = await user.whoami();

// Spaces — discover and manage the spaces you belong to
const { spaces } = await user.space.list();
const space = await user.space.create({ name: "My Space" });   // → { id, slug }
await user.space.rename({ slug: space.slug, name: "Renamed" });
await user.space.delete({ slug: space.slug });

// Service accounts — team-owned operational identities in a space.
// User RPC service-account methods use the space id, not the slug.
const { serviceAccount: service } = await user.serviceAccount.create({
  spaceId: space.id,
  name: "deploy-bot",
  adminMembers: [{ memberId: "019...", admin: true }],
});
const { serviceAccounts } = await user.serviceAccount.list({ spaceId: space.id });
await user.serviceAccount.rename({ id: service.id, name: "deployer" });

// API keys — unrestricted by default
const { id, key } = await user.apiKey.create({
  memberId: me.id,
  name: "developer-machine",
  expiresAt: "2026-01-01T00:00:00Z",  // optional
});
console.log(key);  // "me.xxx.yyy" — full key returned once; only its hash is stored
const { apiKeys } = await user.apiKey.list({ memberId: me.id });
const apiKeyMeta = await user.apiKey.get({ id });
await user.apiKey.delete({ id });

// A restricted user PAT is capped to declared spaces and tree paths.
const scopedPat = await user.apiKey.create({
  memberId: me.id,
  name: "deployment-automation",
  access: [{
    spaceId: space.id,
    grants: [{ treePath: "/share/deploy", access: 2 }], // write
  }],
});
const scopedDetails = await user.apiKey.get({ id: scopedPat.id });

// Service-account keys target the service account. A
// restricted service-account key may declare only that account's native space.
const serviceKey = await user.apiKey.create({
  memberId: service.id,
  name: "production-deploy",
});

API keys are global per-principal credentials. An unrestricted key works in any space its principal has been admitted to (the space comes from X-Me-Space). Pass a non-empty access array when creating a user PAT or service-account key to make it restricted: each declaration names a direct-member space, optional tree grants, and an optional spaceAdmin cap. An empty grant array permits the holder's full live effective access in that one declared space. apiKey.get returns the persisted declarations; apiKey.list reports whether each key is restricted. Service accounts are themselves space-scoped, so a service-account key may declare only that service account's space.

#Error handling

The client throws RpcError for application errors. Each error has a numeric code and an optional string appCode for programmatic matching.

import { createMemoryClient, RpcError } from "@memory.build/client";

try {
  await me.memory.get({ id: "nonexistent" });
} catch (error) {
  if (error instanceof RpcError) {
    console.error(error.message);  // human-readable message
    if (error.is("NOT_FOUND")) {
      // handle missing memory
    }
  }
}

#Error codes

appCode Meaning
NOT_FOUND Resource doesn't exist (or not visible to you)
UNAUTHORIZED Missing or invalid session token / API key
FORBIDDEN Insufficient permissions
CONFLICT Duplicate or conflicting operation
LAST_ADMIN Operation would leave the space with no effective admin
RATE_LIMITED Too many requests
VALIDATION_ERROR Invalid input
QUERY_TIMEOUT Database statement timed out
LOCK_TIMEOUT Database lock wait timed out
TRANSACTION_TIMEOUT Database transaction timed out
EMBEDDING_NOT_CONFIGURED Semantic search without embedding provider
EMBEDDING_FAILED Embedding generation failed
INTERNAL_ERROR Server error

#Protocol

Both clients speak JSON-RPC 2.0 over HTTP. The memory client uses POST /api/v1/memory/rpc with Authorization: Bearer <token> and a required X-Me-Space: <slug> header; the user client uses POST /api/v1/user/rpc with a session/OAuth/PAT bearer for management operations. See the Access Control guide for the authority model behind the management namespaces.