npm.io
3.1.1 • Published yesterday

xmemory

Licence
MIT
Version
3.1.1
Deps
0
Size
67 kB
Vulns
0
Weekly
0

xmemory

TypeScript/JavaScript client library for the xmemory API.

Installation

npm install xmemory

Quick start

import { XmemoryClient } from "xmemory";

const xm = new XmemoryClient({
  url: "https://api.xmemory.ai",  // or set XMEM_API_URL env var
  apiKey: "<your-api-key>",       // or set XMEM_API_KEY env var
});

// Write and read from an existing instance
const inst = xm.instance("<your-instance-id>");

await inst.write("Alice is a software engineer who loves TypeScript.");
const result = await inst.read("What does Alice do?");
console.log(result.reader_result);

Configuration

Parameter Env var Default Description
url XMEM_API_URL https://api.xmemory.ai Base URL of the xmemory API
apiKey XMEM_API_KEY undefined Bearer API key for authentication
timeoutMs 60000 Default request timeout in milliseconds

The legacy token option and XMEM_AUTH_TOKEN env var are still accepted for backwards compatibility but are deprecated and will be removed in a future release. Using them prints a deprecation warning. If both the new and legacy values are set, the new ones win.

Creating a client

import { XmemoryClient, xmemoryInstance } from "xmemory";

// Option 1: constructor (no health check)
const xm1 = new XmemoryClient({ apiKey: "..." });

// Option 2: factory with health check
const xm2 = await XmemoryClient.create({ apiKey: "..." });

// Option 3: convenience function (same as Option 2)
const xm3 = await xmemoryInstance({ apiKey: "..." });

Admin operations

All cluster and instance management lives under client.admin.

Clusters
const clusters = await xm.admin.listClusters();
const cluster = await xm.admin.getCluster(clusterId);
Create an instance
import { SchemaType } from "xmemory";

const inst = await xm.admin.createInstance(
  clusterId,
  "my-instance",
  schemaYml,
  SchemaType.YML,
  { description: "User profiles" },
);

// inst is an InstanceHandle — use it directly for data operations
await inst.write("Alice joined the team.");
List and get instances
const instances = await xm.admin.listInstances();
const info = await xm.admin.getInstance(instanceId);
Schema operations
const schema = await xm.admin.getInstanceSchema(instanceId);
await xm.admin.updateInstanceSchema(instanceId, newYml, SchemaType.YML);
Generate schema
const result = await xm.admin.generateSchema(clusterId, "Track user profiles and preferences");
console.log(result.data_schema);
Update metadata and delete
await xm.admin.updateInstanceMetadata(instanceId, "new-name", "new description");
const deletedIds = await xm.admin.deleteInstance(instanceId);

Instance data operations

Get a handle to an instance and use it for reads, writes, and extractions.

const inst = xm.instance("<instance-id>");
inst.write(text, options?)WriteResult

Extract and store structured objects from text.

const result = await inst.write("Bob is a designer based in Berlin.");
console.log(result.write_id, result.trace_id);
console.log(result.changes); // what the write created / updated / deleted

Options: { extractionLogic?, diffEngine?, timeoutMs? }extractionLogic defaults to "fast".

inst.writeAsync(text, options?)AsyncWriteResult

Start an asynchronous write. Returns a write_id for tracking.

const { write_id } = await inst.writeAsync("Carol manages the London office.");
inst.writeStatus(writeId, options?)WriteStatusResult

Poll the status of an async write.

const status = await inst.writeStatus(write_id);
console.log(status.write_status); // "queued" | "processing" | "completed" | "failed" | "not_found"
inst.read(query, options?)ReadResult

Query the instance.

const result = await inst.read("Who is on the team?");
console.log(result.reader_result);

Options: { readMode?, scope?, traceId?, timeoutMs? }readMode defaults to "single-answer".

Scoped reads

By default a read may draw on the whole instance. Pass a scope to restrict it to a set of concrete objects — useful for grounding an answer in exactly the records you care about, or for keeping a per-user / per-entity read from leaking into unrelated data.

Each object in the scope is identified by its type (the PascalCase class name or snake_case table name) plus its user-defined primary key (a mapping of primary-key field name to value):

const result = await inst.read("What do we know about these people?", {
  scope: {
    objects: [
      { type: "Person", key: { full_name: "Alice Smith" } },
      { type: "Person", key: { full_name: "Bob Jones" } },
    ],
    relationsScope: "all_relations", // default: "no_relations"
  },
});

relationsScope controls relation traversal: "no_relations" (the default) restricts the read to the listed objects only, while "all_relations" also exposes the relations among the in-scope objects.

inst.extract(text, options?)ExtractResult

Extract objects from text without storing them.

const result = await inst.extract("Dave is an engineer in Tokyo.");
console.log(result.objects_extracted);
inst.getSchema(options?)InstanceSchemaInfo
const schema = await inst.getSchema();
console.log(schema.data_schema);
inst.describe(options?)DescribeResult

Get agent-facing tool descriptions enriched with the instance's schema.

const desc = await inst.describe();
console.log(desc.asText());              // plain text for system prompts
const tools = desc.asAnthropicTools();   // Anthropic tool-use format
const tools = desc.asOpenaiTools();      // OpenAI function-calling format

Results are cached for 5 minutes. Call inst.clearDescribeCache() to force a refresh.

Schema evolution

Schemas can change after creation. xmemory supports safe, data-preserving migrations (rename / remove / type change) driven by structured migration ops, plus a suggestion engine that proposes improvements from real read traffic. This is purely additive — existing methods are unchanged.

See the Schema evolution section of the API reference for the conceptual model, and the TypeScript guide for full walkthroughs.

Suggestion-engine flow (review → decide → apply)

The engine surfaces a single rolling proposal per instance. The minimum flow is three calls — review, decide (in bulk), apply:

import { XmemoryClient, type DecisionInput } from "xmemory";

const xm = new XmemoryClient({ apiKey: "..." });
const inst = xm.instance("<instance-id>");

// 1. Review — get the proposal + its concurrency token.
const review = await inst.reviewSuggestions();
if (review.status === "evolution_in_progress") {
  console.log(`A migration is in flight; retry in ${review.retry_after_seconds}s`);
} else if (review.proposal) {
  const proposal = review.proposal;
  for (const item of proposal.items) {
    console.log(item.item_fingerprint, item.rationale, item.op);
  }

  // 2. Decide — accept / reject / defer per item, in one batch.
  const decisions: DecisionInput[] = proposal.items.map((item) => ({
    item_fingerprint: item.item_fingerprint,
    decision: "accept",
  }));
  const decided = await inst.decideSuggestions(proposal.proposal_version, decisions);

  // 3. Apply — commit accepted decisions as one migration.
  const applied = await inst.applyPendingDecisions(decided.next_proposal_version);
  console.log(applied.status, applied.summary); // e.g. "ok" "added 1 field"
}

When status === "evolution_in_progress", back off for retry_after_seconds and retry instead of blocking.

Direct migration flow (enhance → dry-run → update)

Drive a migration yourself — ask the server to enhance the current schema, preview the DDL, then apply it:

import { XmemoryClient, SchemaType } from "xmemory";
import yaml from "js-yaml";

const xm = new XmemoryClient({ apiKey: "..." });
const current = (await xm.admin.getInstanceSchema("<instance-id>")).data_schema;

// 1. Enhance — new schema + an executor-ready migration plan.
const enhanced = await xm.admin.enhanceSchema(
  "<cluster-id>",
  "Rename Person.mail to Person.email.",
  yaml.dump(current),
);
console.log(enhanced.summary, enhanced.migration_plan?.ops);

const newYaml = yaml.dump(enhanced.data_schema);

// 2. Dry-run — preview the DDL without applying anything.
const preview = await xm.admin.dryRunMigration("<instance-id>", newYaml, SchemaType.YML, {
  migrationPlan: enhanced.migration_plan ?? undefined,
});
console.log(preview.statements);

// 3. Update — apply. confirmDestructive is required for ops that drop data.
const info = await xm.admin.updateInstanceSchema("<instance-id>", newYaml, SchemaType.YML, {
  migrationPlan: enhanced.migration_plan ?? undefined,
  confirmDestructive: false,
});
console.log(info.migration_id, info.prior_version, "->", info.new_version);
Migration history
const page = await xm.admin.listMigrations("<instance-id>", { limit: 20 });
for (const record of page.items) {
  console.log(record.id, record.source, record.prior_version, "->", record.new_version);
}

const detail = await xm.admin.getMigration("<instance-id>", "<migration-id>", { includeYaml: true });
console.log(detail.yaml_before, detail.yaml_after);

Migration ops are exported as discriminated-union types (MigrationPlan, MigrationOp, AddField, RenameField, RemoveObject, …) keyed on op_type. ProposalItem.op and MigrationRecord.ops are raw dicts for forward compatibility — narrow them to MigrationOp when needed.

Runnable end-to-end examples live in examples/.

Error handling

All errors throw XmemoryAPIError. Health check failures throw XmemoryHealthCheckError (a subclass).

import { XmemoryClient, XmemoryAPIError, XmemoryHealthCheckError } from "xmemory";

try {
  const xm = await XmemoryClient.create({ apiKey: "..." });
} catch (e) {
  if (e instanceof XmemoryHealthCheckError) {
    console.error("Server unreachable:", e.message);
  }
}

try {
  await inst.read("query");
} catch (e) {
  if (e instanceof XmemoryAPIError) {
    console.error(`API error (HTTP ${e.status}): ${e.message}`);
  }
}

XmemoryAPIError carries status (HTTP status), code (structured error code, when the server returned one), details (an optional structured payload), and retryAfter (the Retry-After header in seconds, when the server sent one).

Branch on code, not on the bare HTTP status — the same status can mean different things. In particular HTTP 402 Payment Required is now overloaded:

HTTP code Meaning Retryable?
402 QUOTA_EXCEEDED Tenant exhausted its plan/usage allowance (a daily or monthly token quota). No. Wait for the window to reset.
402 TRIAL_ENDED Trial over / subscription lapsed. No. Needs a plan change.
429 RATE_LIMITED Genuine velocity / rate limit. Yes, with backoff.

For QUOTA_EXCEEDED, details carries kind ("daily_quota_exceeded" | "monthly_quota_exceeded") and retry_after_seconds (number | null); when the window is resettable the server also sends a Retry-After header, surfaced as retryAfter (seconds). TRIAL_ENDED may carry details.kind === "trial_exceeded" or no details. RATE_LIMITED is retryable — honour retryAfter (or Retry-After) for backoff. The client never retries on its own; it only surfaces the value.

try {
  await inst.write("...");
} catch (e) {
  if (!(e instanceof XmemoryAPIError)) throw e;
  switch (e.code) {
    case "QUOTA_EXCEEDED": {
      // Non-retryable: plan/usage allowance exhausted.
      const kind = (e.details as { kind?: string } | null)?.kind; // daily_ | monthly_quota_exceeded
      console.error(`Quota exhausted (${kind}); resets in ${e.retryAfter ?? "?"}s`);
      break;
    }
    case "TRIAL_ENDED":
      // Non-retryable: trial over / subscription lapsed — upgrade required.
      console.error("Trial ended — upgrade the plan.");
      break;
    case "RATE_LIMITED":
      // Retryable: back off and retry, honouring Retry-After.
      console.error(`Rate limited; retry after ${e.retryAfter ?? "a short delay"}s`);
      break;
    default:
      console.error(`API error (HTTP ${e.status}, code ${e.code}): ${e.message}`);
  }
}

The schema-evolution endpoints return codes you can pattern match on via .code the same way — for example stale_proposal_version, dependency_closure_failed, destructive_confirmation_required, non_additive_change_requires_plan, stale_schema_version, migration_not_found, instance_not_initialised:

try {
  await inst.applyPendingDecisions(token);
} catch (e) {
  if (e instanceof XmemoryAPIError && e.code === "stale_proposal_version") {
    const review = await inst.reviewSuggestions(); // re-review and retry
  }
}

All timeouts are per-request

Every method accepts an optional timeoutMs in its options bag, overriding the client default.

const result = await inst.read("query", { timeoutMs: 120_000 });

Mastra integration

You can also use xmemory as an MCP server within Mastra.ai.

First, create a local Mastra instance:

npm create mastra@latest mastra-with-xmemory

From within this example mastra-with-xmemory directory, first give it some LLM key:

echo "export ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY_FOR_MASTRA" >>.env

Then you may want to add MCP support to Mastra first, for its hot reload to pick up xmemory right away.

npm i @mastra/mcp

Then fire up the AI-assisted IDE of your choice and give it this prompt.

We need to integrate the `xmemory` MCP server with the Mastra instance running from this directory.

To do this we need to add the `xmemory` Agent, alongside the Weather Agent, and the `xmemory` MCP server to use the Tools from it.

The `xmemory` Agent setup is straightforward, just clone what the Weather Agent has, with `xmemory`-specific instructions. Use the following instructions:

~ ~ ~

> You are the xmemory assistant. You help users manage and query their xmemory instance:
> - Create and configure new instances, generate or enhance schemas, connect and disconnect from instances.
> - Use the xmemory_admin_* tools to perform administrative and schema operations as requested.
> - Be concise and confirm what you did after each action.

~ ~ ~

For the MCP server, you need to add `@mastra/mcp` into `package.json` if it's not already there.

And then make changes along these lines:

new file mode 100644
--- /dev/null
+++ b/src/mastra/mcp-clients.ts
@@ -0,0 +1,19 @@
+import { MCPClient } from '@mastra/mcp';
+
+if (!process.env.XMEM_MCP_BEARER_TOKEN) {
+  throw new Error('XMEM_MCP_BEARER_TOKEN environment variable is required');
+}
+
+export const xmemoryMcp = new MCPClient({
+  id: 'xmemory',
+  servers: {
+    xmemory: {
+      url: new URL('https://dk-mcp.xmemory.ai'),
+      requestInit: {
+        headers: {
+          Authorization: `Bearer ${process.env.XMEM_MCP_BEARER_TOKEN}`,
+        },
+      },
+    },
+  },
+});


new file mode 100644
--- /dev/null
+++ b/src/mastra/xmemory-tools.ts
@@ -0,0 +1,10 @@
+import { xmemoryMcp } from './mcp-clients';
+
+let xmemoryTools: Record<string, any> = {};
+try {
+  xmemoryTools = await xmemoryMcp.listTools();
+} catch (err) {
+  console.error('Failed to load xmemory MCP tools:', err);
+}
+
+export { xmemoryTools };

~ ~ ~

Commit the above as "Added `xmemory` to Mastra."

Keywords