# @surrealdb/spectron

Typed REST client for the [Spectron](https://surrealdb.com/platform/spectron) API. It is lightweight, uses your platform `fetch`, and ships no runtime dependencies.

## Install

Run the following command to add the SDK to your project:

```sh
# using npm
npm i @surrealdb/spectron

# or using pnpm
pnpm i @surrealdb/spectron

# or using yarn
yarn add @surrealdb/spectron

# or using bun
bun add @surrealdb/spectron
```

## Quick start

```ts
import { Spectron } from "@surrealdb/spectron";

// Create a new Spectron client (pinned to one context)
const client = new Spectron({
  endpoint: process.env.SPECTRON_ENDPOINT!,
  context: "acme-prod",
  apiKey: process.env.SPECTRON_API_KEY!,
});

// Upload a document
const document = await client.documents.upload({
  file: documentFile,
  title: "Handbook",
});

// Remember a fact and recall it
await client.remember("I just got promoted to CTO", { scopes: "user/tobie" });
const hits = await client.recall("What is Tobie's role?", { k: 10 });

// Chat (server-driven memory loop)
const { reply } = await client.chat("What do you know about me?");
```

## Memory operations

```ts
// Persist facts from free text and/or caller-supplied triples (idempotent).
await client.remember("Tobie prefers dark mode", { infer: "full" });

// Persist a batch of conversation messages.
await client.rememberMany([
  { role: "user", content: "I moved to Lisbon" },
  { role: "assistant", content: "Noted." },
]);

// Recall, context, reflection, and forgetting.
await client.recall("Where does Tobie live?", { k: 5 });
await client.context("Summarise preferences", { k: 5 });
await client.reflect("What changed this week?", { persist: true });
await client.forget("Remove old project notes", { purge: true });

// Snapshots and maintenance.
await client.state();
await client.profile();
await client.whoami();
await client.consolidate({ dryRun: true });
await client.elaborate({ entityRef: "person:tobie" });
await client.fsck();
await client.inspect("person:tobie");
await client.audit({ limit: 50 });

// Self-service API keys.
const minted = await client.keys.create({ name: "ci", ttlSeconds: 3600 });
await client.keys.list();
await client.keys.rotate("ci");
await client.keys.delete("ci");
```

### Streaming chat

```ts
const stream = await client.chat("Tell me a story", { stream: true });
for await (const chunk of stream) {
  process.stdout.write(chunk.delta);
}
```

## Namespaces

| Namespace | Highlights |
| --- | --- |
| `client.documents` | `upload`, `reprocess`, `get`, `raw`, `chunks`, `list`, `delete`, `query`, `recomputeLinks`, `keywords.*` |
| `client.entities` | `list`, `get`, `history`, `delete` |
| `client.sessions` | `create` → `Session` (`turns`, `context`, `close`) |
| `client.lifecycle` | `expire`, `decay` |
| `client.traces` | `list`, `get`, `stats` |
| `client.principals` | `list`, `get`, `effective`, `grant`, `revoke` |
| `client.scopes` | `list`, `register`, `delete`, `forget` |
| `client.keys` | `create`, `list`, `delete`, `rotate` |

## Delegation

`client.onBehalfOf(principalId)` returns a new client whose every request carries the `X-Spectron-On-Behalf-Of` header, so calls run with that principal's authorisation. This requires the `manage` grant. The original client is left unchanged.

```ts
const asAlex = client.onBehalfOf("principal:alex");
await asAlex.remember("Reviewed the Q3 plan");
await asAlex.recall("What did Alex review?");
```

## Errors

| Class | Typical cause |
| --- | --- |
| `AuthError` | 401 |
| `ScopeError` | 403 |
| `NotFoundError` | 404 |
| `ValidationError` | 400 / 422 |
| `RateLimitError` | 429 (`retryAfter` when provided) |
| `ServerError` | 5xx |
| `ConnectionError` | Network / timeout |

Catch subclasses of `SpectronError`, or use `errorFromResponse` directly.

## Retries & idempotency

Idempotent `GET` requests retry on `5xx` and connection failures with backoff `250ms`, `500ms`, `1000ms` (up to `maxRetries`, default `3`). The `remember` and `rememberMany` writes carry an `Idempotency-Key` derived from the request and a 30-second window, so they are retried safely too; other mutating methods are not retried automatically.

## Scope

Write and session calls accept `scopes?: Scope`, and read calls accept the same shape as `lens?: Scope`. The wire format is a `ScopeSets`: a DNF (disjunctive-normal-form) selector, `string[][]`. The outer array is an OR of clauses; each inner array is an AND of `key/value` slash-paths. So `[["team/a"], ["team/b", "clearance/secret"]]` means `team/a OR (team/b AND clearance/secret)`.

For ergonomics a bare string is a single-path clause and a flat string array is an OR of single-path clauses, and the two mix. All forms normalise to the wire shape via `normaliseScope`. Empty paths and empty clauses are dropped; omit `scopes` entirely to use the key's default write region.

```ts
client.remember("...", { scopes: "team/eng" }); // -> [["team/eng"]]
client.remember("...", { scopes: ["team/eng", "org/acme"] }); // OR -> [["team/eng"], ["org/acme"]]
client.remember("...", { scopes: [["team/eng", "org/acme"]] }); // AND -> [["team/eng", "org/acme"]]
```
