---
title: "TypeScript SDK"
description: "Getting started with the Meerkat TypeScript SDK: sessions, turns, streaming events, and capabilities."
icon: "js"
---

The TypeScript SDK (`@rkat/sdk`) is a thin wrapper over Meerkat's settled runtime-backed contracts. It spawns a local `rkat-rpc` subprocess and exposes the same session lifecycle used by the CLI, REST, JSON-RPC, and MCP surfaces as TypeScript-native `Session` and `DeferredSession` handles.

## Getting started

<Steps>
  <Step title="Install the SDK">
    ```bash
    npm install @rkat/sdk
    ```
  </Step>
  <Step title="Install the RPC binary">
    ```bash
    cargo build -p meerkat-rpc --release
    # Ensure target/release/rkat-rpc is on your $PATH
    ```

    By default the SDK can also auto-resolve and download `rkat-rpc` for the current platform, so a manual binary install is optional unless you want to control the binary path yourself.

    You also need an API key for at least one LLM provider (e.g. `ANTHROPIC_API_KEY`).
  </Step>
  <Step title="Configure tsconfig">
    The SDK is ESM. Your `tsconfig.json` must use Node16 module resolution:

    ```json
    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "Node16",
        "moduleResolution": "Node16",
        "strict": true
      }
    }
    ```
  </Step>
  <Step title="Connect and run">
    ```typescript
    import { MeerkatClient } from "@rkat/sdk";

    const client = new MeerkatClient();
    await client.connect();

    const session = await client.createSession("What is the capital of Sweden?");
    console.log(session.text);
    console.log(session.id);

    const result = await session.turn("And what is its population?");
    console.log(result.text);

    await session.archive();
    await client.close();
    ```
  </Step>
</Steps>

---

## Method overview

### MeerkatClient methods

| Method | Description |
|--------|-------------|
| `connect(options?)` | Spawn `rkat-rpc`, handshake, fetch capabilities |
| `close()` | Kill the subprocess |
| `createSession(prompt, options?)` | Create a session, run the first turn, return a `Session` |
| `createSessionStreaming(prompt, options?)` | Create a session and stream typed events from the first turn |
| `listSessions()` | List active sessions |
| `readSession(sessionId)` | Read typed session metadata (`SessionInfo`) |
| `readSessionHistory(sessionId, options?)` | Read committed session transcript history |
| `sendExternalEvent(sessionId, eventType, payload, options?)` | Inject a canonical external event into a session |
| `injectContext(sessionId, text, options?)` | Append runtime system context |
| `getModelsCatalog()` | Read config-backed model catalog |
| `createSchedule/getSchedule/listSchedules/updateSchedule/pauseSchedule/resumeSchedule/deleteSchedule/listScheduleOccurrences/listScheduleTools/callScheduleTool` | Schedule API wrappers |
| `createMob(options?)` | Create a mob, return a `Mob` |
| `createDeferredSession(prompt, options?)` | Create a session without running the first turn; returns a `DeferredSession` |
| `listMobs()` | List mobs |
| `getConfig()` | Read runtime configuration |
| `setConfig(config)` | Replace runtime configuration |
| `patchConfig(patch)` | Merge-patch runtime configuration |
| `liveOpen/liveStatus/liveClose/liveSendInput/liveCommitInput/liveInterrupt/liveTruncate/liveRefresh` | Direct wrappers for the `live/*` RPC surface |

### Auth wrappers

The SDK also exposes the auth-profile wrappers from the RPC surface:

- `authProfileList(...)`, `authProfileGet(...)`, `authProfileCreate(...)`, `authProfileDelete(...)`
- `authLoginStart(...)`, `authLoginComplete(...)`, `authLoginDeviceStart(...)`, `authLoginDeviceComplete(...)`, `authLoginProvisionApiKey(...)`
- `authStatusGet(...)`, `authLogout(...)`
- `realmList(...)`, `realmGet(...)`
- `sendPeerResponseTerminal(...)`
- `mcpAdd(...)`, `mcpRemove(...)`, `mcpReload(...)`
- `getBlob(...)`, `listSkills()`

### Generated assistant images

When a session model calls the built-in `generate_image` tool, generated images appear in committed history as `SessionAssistantBlock` entries with `blockType === "image"`. The SDK preserves `imageId`, `blobId`, `mediaType`, `width`, `height`, `revisedPrompt`, and provider metadata.

```typescript
const history = await session.history();
const image = history.messages
  .flatMap((message) => [...message.blocks])
  .find((block) => block.blockType === "image");
const payload = image?.blobId ? await client.getBlob(image.blobId) : undefined;
```

Image-generation wire contracts are exported for integrations that inspect tool results directly: `WireGenerateImageRequest`, `WireGenerateImageExecutionPlan`, `WireImageGenerationToolResult`, `WireImageOperationPhase`, and `WireAssistantImageRef`.

### Capability methods

| Method | Description |
|--------|-------------|
| `client.capabilities` | Read-only array of all `Capability` objects |
| `client.hasCapability(id)` | Returns `true` if capability is `"Available"` |
| `client.requireCapability(id)` | Throws `CapabilityUnavailableError` if unavailable |

### Session methods

| Method | Description |
|--------|-------------|
| `session.turn(prompt, options?)` | Run another turn (non-streaming), returns `RunResult` |
| `session.stream(prompt, options?)` | Run another turn with streaming, returns `EventStream` |
| `session.history(options?)` | Read committed transcript history for this session |
| `session.injectContext(text, options?)` | Append runtime system context |
| `session.interrupt()` | Cancel the currently running turn |
| `session.archive()` | Remove the session from the server |
| `session.invokeSkill(skillRef, prompt)` | Invoke a skill in this session |
| `session.send(command)` | Send a typed comms command scoped to this session |
| `session.peers()` | List peers visible to this session's comms runtime |
| `session.subscribeEvents()` | Open a standalone event subscription; returns `EventSubscription<AgentEventEnvelope>` |

### Live channel helper

`RealtimeChannel` and the old realtime convenience helpers are no longer part of
the public SDK. Use `LiveChannel.session(client, session.id, options?)` for a
session-bound wrapper around the `live/*` methods. The SDK returns the transport
bootstrap from `live/open`; callers still own the WebSocket connection to
`openResult.transport.url`.

```typescript
import { LiveChannel, MeerkatClient } from "@rkat/sdk";

const client = new MeerkatClient();
await client.connect({ liveWs: true });

const session = await client.createSession("Open a live channel");
const channel = LiveChannel.session(client, session.id, {
  turningMode: "explicit_commit",
});

const openResult = await channel.open();
// Connect to openResult.transport.url externally.
await channel.sendInputText("hello");
await channel.commitInput("text");
await channel.close();
```

### Mob methods

| Method | Description |
|--------|-------------|
| `mob.mobId` | The mob's identifier |
| `mob.status()` | Get mob status |
| `mob.lifecycle(action)` | Stop, resume, complete, destroy, or reset the mob |
| `mob.spawn(spec)` | Spawn a member |
| `mob.spawnMany(specs)` | Batch-spawn members |
| `mob.retire(agentIdentity)` | Retire a member |
| `mob.respawn(agentIdentity)` | Respawn a member |
| `mob.wire(a, b)` | Wire two members |
| `mob.unwire(a, b)` | Remove wiring |
| `mob.listMembers()` | List mob members |
| `mob.member(agentIdentity).send(content, handlingMode?)` | Send work to a member |
| `mob.readEvents(options?)` | Read mob event history (`mob/events`) |
| `mob.listFlows()` | List available flows |
| `mob.runFlow(flowId, params)` | Start a flow run |
| `mob.flowStatus(runId)` | Get flow run status |
| `mob.cancelFlow(runId)` | Cancel a flow run |
| `mob.subscribeMemberEvents(agentIdentity)` | Subscribe to member events; returns `EventSubscription<AgentEventEnvelope>` |
| `mob.subscribeEvents()` | Subscribe to mob-wide events; returns `EventSubscription<AttributedMobEvent>` |

---

## MeerkatClient

### Constructor

```typescript
new MeerkatClient(rkatPath?: string)
```

Defaults to searching `$PATH` for `rkat-rpc`. If not found, automatically downloads the correct binary for the current platform and caches it in `~/.cache/meerkat/bin/rkat-rpc`. Pass an explicit path or set `MEERKAT_BIN_PATH` to override.

### connect()

```typescript
async connect(options?: ConnectOptions): Promise<this>
```

Spawns `rkat-rpc`, performs `initialize` handshake, checks contract version compatibility, and fetches capabilities. Returns `this` for chaining.

```typescript
export interface ConnectOptions {
  realmId?: string;
  instanceId?: string;
  realmBackend?: "jsonl" | "sqlite";
  isolated?: boolean;
  stateRoot?: string;
  contextRoot?: string;
  userConfigRoot?: string;
  liveWs?: boolean;
}
```

`realmId` and `isolated` are mutually exclusive. If `realmId` is omitted and `isolated` is not set, the spawned `rkat-rpc` process uses its normal isolated-mode default rather than a shared default realm. Reuse `realmId` explicitly to share sessions and config across processes or surfaces.
Set `liveWs: true` when you need the `live/*` adapter surface; the SDK starts
`rkat-rpc` with an ephemeral live WebSocket listener and `live/open` returns the
transport URL/token for your client to connect.

### createSession()

```typescript
async createSession(
  prompt: string | ContentBlock[],
  options?: SessionOptions,
): Promise<Session>
```

Creates a new session, runs the first turn with `prompt`, and returns a `Session` object. The `Session` holds the last `RunResult` and exposes convenience accessors for the most recent text, usage, and tool call counts.

```typescript
const session = await client.createSession("Summarise this project", {
  model: "claude-sonnet-4-6",
  systemPrompt: "You are a senior engineer.",
  maxTokens: 4096,
});

console.log(session.text);
console.log(session.usage.inputTokens);
```

### createSessionStreaming()

```typescript
createSessionStreaming(
  prompt: string | ContentBlock[],
  options?: SessionOptions,
): EventStream
```

Creates a new session and returns an `EventStream` for the first turn. Iterate the stream to receive typed events as they arrive. The final `RunResult` is available on `stream.result` after iteration completes.

```typescript
const stream = client.createSessionStreaming("Explain async/await in TypeScript");

for await (const event of stream) {
  if (event.type === "text_delta") {
    process.stdout.write(event.delta);
  }
}

console.log("\nTokens used:", stream.result.usage.outputTokens);
```

### SessionOptions

All fields are camelCase:

```typescript
interface SessionOptions {
  model?: string;
  provider?: string;
  systemPrompt?: string;
  maxTokens?: number;
  outputSchema?: Record<string, unknown>;
  structuredOutputRetries?: number;
  hooksOverride?: Record<string, unknown>;
  enableBuiltins?: boolean;
  enableShell?: boolean;
  enableMemory?: boolean;
  enableMob?: boolean;
  keepAlive?: boolean;
  commsName?: string;
  peerMeta?: Record<string, unknown>;
  budgetLimits?: Record<string, unknown>;
  providerParams?: Record<string, unknown>;
  preloadSkills?: SkillRef[];
  skillRefs?: SkillRef[];
  labels?: Record<string, string>;
  additionalInstructions?: string[];
  appContext?: unknown;
  shellEnv?: Record<string, string>;
  externalTools?: Record<string, unknown>[];
}
```

---

## Session

`createSession()` and `createSessionStreaming()` both produce a `Session` object that acts as the handle for all subsequent turns on the same conversation. The SDK does not own a second execution path; it only wraps the canonical runtime session identity.

### Identity

```typescript
session.id    // stable UUID string
session.ref   // optional human-readable reference (string | undefined)
```

### Last-result shortcuts

These accessors always reflect the most recent completed turn:

```typescript
session.text            // assistant text from the last turn
session.usage           // Usage from the last turn
session.turns           // number of LLM turns in the last run
session.toolCalls       // number of tool calls in the last run
session.structuredOutput // structured output from the last run (unknown | undefined)
session.lastResult      // the full RunResult object
```

### turn()

```typescript
async turn(
  prompt: string | ContentBlock[],
  options?: {
    skillRefs?: SkillRef[];
    flowToolOverlay?: TurnToolOverlay;
    additionalInstructions?: string[];
    keepAlive?: boolean;
    model?: string;
    provider?: string;
    maxTokens?: number;
    systemPrompt?: string;
    outputSchema?: Record<string, unknown>;
    structuredOutputRetries?: number;
    providerParams?: Record<string, unknown>;
  },
): Promise<RunResult>
```

Sends another turn to the session and returns the `RunResult`. Also updates the session's last-result shortcuts.

```typescript
const result = await session.turn("Now explain the second point in more detail");
console.log(result.text);
```

### stream()

```typescript
stream(
  prompt: string | ContentBlock[],
  options?: {
    skillRefs?: SkillRef[];
    flowToolOverlay?: TurnToolOverlay;
    additionalInstructions?: string[];
    keepAlive?: boolean;
    model?: string;
    provider?: string;
    maxTokens?: number;
    systemPrompt?: string;
    outputSchema?: Record<string, unknown>;
    structuredOutputRetries?: number;
    providerParams?: Record<string, unknown>;
  },
): EventStream
```

Sends another turn and returns an `EventStream`. Iterating it yields typed `AgentEvent` objects. The session's last-result shortcuts are updated when iteration completes.

```typescript
for await (const event of session.stream("Elaborate on point three")) {
  if (event.type === "text_delta") {
    process.stdout.write(event.delta);
  }
}
// session.text is now updated
```

### interrupt()

```typescript
async interrupt(): Promise<void>
```

Sends `turn/interrupt` for this session. Has no effect if no turn is running.

### archive()

```typescript
async archive(): Promise<void>
```

Removes this session from the server. The `Session` object should not be used after calling `archive()`.

### invokeSkill()

```typescript
async invokeSkill(skillRef: SkillRef, prompt: string): Promise<RunResult>
```

Calls `requireCapability("skills")`, then runs a turn with the provided structured `SkillKey` injected:

```typescript
const result = await session.invokeSkill(
  { sourceUuid: "abc123", skillName: "code-review" },
  "Review this function for safety issues",
);
```

### send() and peers()

```typescript
async send(command: CommsCommand): Promise<CommsSendReceipt>
async peers(): Promise<Array<Record<string, unknown>>>
```

Scope comms operations to this session. Requires the `comms` capability.

```typescript
await session.send({
  kind: "peer_message",
  to: "agent-b",
  body: "Hello!",
  handling_mode: "queue",
});
const peers = await session.peers();
```

### subscribeEvents()

```typescript
async subscribeEvents(): Promise<EventSubscription<AgentEventEnvelope>>
```

Open a standalone event subscription for this session. Returns an async-iterable `EventSubscription` that yields typed `AgentEventEnvelope` objects.

```typescript
const sub = await session.subscribeEvents();
for await (const envelope of sub) {
  console.log(envelope.payload.type, envelope.seq);
}
await sub.close();
```

---

## Capabilities

Capabilities are fetched automatically during `connect()`. Use them to guard code paths that depend on optional features.

```typescript
// Read all capabilities
console.log(client.capabilities);
// [{ id: "sessions", description: "...", status: "Available" }, ...]

// Check a single capability
if (client.hasCapability("comms")) {
  await session.send({
    kind: "peer_message",
    to: "agent-b",
    body: "Hi",
    handling_mode: "queue",
  });
}

// Assert and throw if unavailable
client.requireCapability("skills");  // throws CapabilityUnavailableError if not available
```

<Accordion title="Known capability IDs">
| ID | Description |
|---|-------------|
| `sessions` | Session lifecycle |
| `streaming` | Real-time event streaming |
| `structured_output` | JSON schema structured output |
| `hooks` | Lifecycle hooks |
| `builtins` | Built-in tools |
| `shell` | Shell tool |
| `comms` | Inter-agent communication |
| `memory_store` | Semantic memory |
| `session_store` | Session persistence |
| `session_compaction` | Context compaction |
| `skills` | Skill loading |
</Accordion>

---

## Config management

```typescript
const config = await client.getConfig();
await client.setConfig({ ...config.config, model: "claude-opus-4-6" });
const updated = await client.patchConfig({ max_tokens: 8192 });
console.log(updated.config.max_tokens);
```

---

## Examples

<Accordion title="Structured output">
```typescript
import { MeerkatClient } from "@rkat/sdk";

const client = new MeerkatClient();
await client.connect();

const session = await client.createSession("List three European capitals", {
  outputSchema: {
    type: "object",
    properties: {
      capitals: { type: "array", items: { type: "string" } },
    },
    required: ["capitals"],
  },
  structuredOutputRetries: 3,
});

console.log(session.structuredOutput);
// { capitals: ["Paris", "Berlin", "Madrid"] }

await client.close();
```
</Accordion>

<Accordion title="Multi-turn conversation">
```typescript
import { MeerkatClient } from "@rkat/sdk";

const client = new MeerkatClient();
await client.connect();

const session = await client.createSession("My name is Alice.", {
  model: "claude-sonnet-4-6",
});

const result = await session.turn("What is my name?");
console.log(result.text);  // Mentions "Alice"

await session.archive();
await client.close();
```
</Accordion>

<Accordion title="Streaming a turn">
```typescript
import { MeerkatClient } from "@rkat/sdk";

const client = new MeerkatClient();
await client.connect();

const session = await client.createSession("You are a helpful assistant.", {
  systemPrompt: "Be concise.",
});

for await (const event of session.stream("Explain Rust lifetimes in two sentences")) {
  switch (event.type) {
    case "text_delta":
      process.stdout.write(event.delta);
      break;
    case "turn_completed":
      console.log(`\nStop reason: ${event.stopReason}`);
      console.log(`Tokens: ${event.usage.inputTokens} in / ${event.usage.outputTokens} out`);
      break;
  }
}

await client.close();
```
</Accordion>

<Accordion title="Invoking a skill">
```typescript
import { MeerkatClient } from "@rkat/sdk";

const client = new MeerkatClient();
await client.connect();

client.requireCapability("skills");

const session = await client.createSession("Let's review some code.");

const result = await session.invokeSkill(
  { sourceUuid: "abc123", skillName: "code-review" },
  "Review this function for correctness and safety",
);

console.log(result.text);

await client.close();
```
</Accordion>

<Accordion title="Multimodal prompt (image input)">
```typescript
import { MeerkatClient, ContentBlock } from "@rkat/sdk";
import { readFileSync } from "fs";

const client = new MeerkatClient();
await client.connect();

const imageData = readFileSync("screenshot.png").toString("base64");

const session = await client.createSession([
  { type: "text", text: "What do you see in this image?" },
  { type: "image", media_type: "image/png", data: imageData },
]);

console.log(session.text);
await client.close();
```
</Accordion>

<Accordion title="Using collectText() for simple streaming">
```typescript
import { MeerkatClient } from "@rkat/sdk";

const client = new MeerkatClient();
await client.connect();

const stream = client.createSessionStreaming("Write a haiku about Rust");
const [fullText, result] = await stream.collectText();

console.log(fullText);
console.log("Input tokens:", result.usage.inputTokens);

await client.close();
```
</Accordion>

---

## See also

- [TypeScript SDK reference](/sdks/typescript/reference) - types, events, errors, and version compatibility
- [Rust SDK overview](/rust/overview) - Rust library API
- [RPC reference](/api/rpc) - JSON-RPC protocol specification
