npm package: @memorymodel/client
Description: Official TypeScript SDK for MemoryModel API
Author: MemoryModel Team
Latest version: 0.5.0

# @memorymodel/client

Official TypeScript/Node.js SDK for [MemoryModel](https://memorymodel.dev).

## Installation

### From NPM 
```bash
npm install @memorymodel/client
```

## Quick Start

```typescript
import { MemoryClient } from '@memorymodel/client';

const client = new MemoryClient({
  apiKey: "sk_live_...",           // Required: Your Cluster Access Key
  defaultEndUserId: "user_123"     // Optional: Default user ID for all requests
});

// Add a memory
await client.add("Project deadline is Friday.");

// Search memories
const results = await client.search("When is the deadline?");
console.log(results);
```

---

## API Reference

### Constructor

```typescript
new MemoryClient(config: SDKConfig)
```

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `apiKey` | `string` | ✅ | - | Your cluster access key (`sk_live_...`) |
| `baseUrl` | `string` | ❌ | `https://api.memorymodel.dev` | Override API URL |
| `defaultEndUserId` | `string` | ❌ | - | Default user ID for requests |
| `timeout` | `number` | ❌ | `30000` | Request timeout in milliseconds |
| `maxRetries` | `number` | ❌ | `3` | Max retries for 5xx/429 errors |
| `apiVersion` | `string` | ❌ | `'v1'` | API version prefix |

---

### Methods

#### `add(content, options?)` — Ingest Text
```typescript
await client.add("Meeting moved to 3pm.", { 
  userContext: "Calendar update",  // Optional context
  endUserId: "user_456"            // Override default user
});
```

#### `addImage(imageData, options?)` — Ingest Image
```typescript
await client.addImage(base64String, { 
  userContext: "Screenshot of error" 
});
```

#### `search(query, options?)` — Semantic Search
```typescript
const results = await client.search("What time is the meeting?", {
  limit: 5,                        // Max results (default: 5)
  strategy: "centroid_aware"       // or "simple"
});
```

#### `list(options?)` — Get Memory History
```typescript
const memories = await client.list({ 
  limit: 10,
  memoryType: "event"              // Optional filter
});
```

#### `delete(memoryId)` — Delete Memory
```typescript
await client.delete("mem_abc123");
```

#### `searchImage(imageData, options?)` — Visual Similarity Search
```typescript
import * as fs from 'fs';

// Load and encode image
const imageBuffer = fs.readFileSync('query_image.jpg');
const base64Image = imageBuffer.toString('base64');

// Find visually similar memories
const results = await client.searchImage(base64Image, {
  limit: 5
});
```

> **Note**: Image search finds memories visually similar to the input image. Only memories ingested via `addImage()` will be matched.

#### `uploadDocument(fileData, options?)` — Upload & Process PDF

Upload a PDF file directly via the SDK. The file is sent as multipart/form-data, stored on our infrastructure, and automatically queued for processing.

```typescript
import * as fs from 'fs';

// Read the PDF file
const pdfBuffer = fs.readFileSync('path/to/contract.pdf');

// Upload and process in one step
const result = await client.uploadDocument(pdfBuffer, {
  fileName: 'contract.pdf'  // Optional, defaults to 'document.pdf'
});

console.log(result.storagePath); // Where the file was stored
console.log(result.jobId);       // Processing job ID
```

This method:
1. Uploads the PDF to MemoryModel's storage (max 20MB)
2. Automatically queues server-side processing
3. Extracts text from each page and creates linked memories

#### `addDocument(storagePath, options?)` — Process Pre-Uploaded PDF

If the PDF is already in MemoryModel storage (e.g. uploaded via console), trigger processing by path:

```typescript
await client.addDocument("projects/my-project/docs/contract.pdf");
```

> **Tip**: Most users should use `uploadDocument()`. Use `addDocument()` only if the file is already on our storage (e.g. uploaded via the Console UI).

---

## Validation

The SDK validates payloads **before** sending requests using Zod. Invalid data throws a `ZodError`:

```typescript
try {
  await client.add("");  // Empty content
} catch (e) {
  console.error(e.issues);  // [{ message: "Content cannot be empty" }]
}
```

You can also import schemas for custom validation:

```typescript
import { TextIngestPayloadSchema } from '@memorymodel/client';

const result = TextIngestPayloadSchema.safeParse(userInput);
if (!result.success) {
  // Handle validation errors
}
```

---

## Error Handling

The SDK throws `MemoryClientError` for API and network errors:

```typescript
import { MemoryClient, MemoryClientError } from '@memorymodel/client';

try {
  await client.search("test");
} catch (e) {
  if (e instanceof MemoryClientError) {
    console.log(e.message);      // Human-readable error
    console.log(e.status);       // HTTP status (401, 429, 500...)
    console.log(e.code);         // Error code from API
    console.log(e.isRetryable);  // true for 5xx/429
  }
}
```

**Automatic Retry**: The SDK automatically retries on 5xx and 429 errors with exponential backoff (1s → 2s → 4s).

---

## Environment Variables (Recommended)

```typescript
const client = new MemoryClient({
  apiKey: process.env.MEMORY_API_KEY!,
  defaultEndUserId: process.env.MEMORY_USER_ID
});
```

---

## Agent Integration Examples

### Example 1: Ingest Chat Messages (Memory Acquisition)

Store every user message as a memory for future context retrieval:

```typescript
import { MemoryClient } from '@memorymodel/client';
import { generateText } from 'ai'; // Vercel AI SDK example

const memory = new MemoryClient({
  apiKey: process.env.MEMORY_API_KEY!
});

async function handleUserMessage(userId: string, message: string) {
  // 1. Store the message as memory
  await memory.add(message, {
    endUserId: userId,
    userContext: "User chat message"
  });

  // 2. Generate AI response (your LLM logic)
  const response = await generateText({
    model: openai('gpt-4o'),
    prompt: message
  });

  return response.text;
}
```

---

### Example 2: Context-Aware Agent (Memory Retrieval)

Retrieve relevant memories before generating a response:

```typescript
async function handleUserQuery(userId: string, query: string) {
  // 1. Search for relevant memories
  const memories = await memory.search(query, {
    endUserId: userId,
    limit: 5
  });

  // 2. Build context from memories
  const context = memories
    .map(m => `- ${m.content}`)
    .join('\n');

  // 3. Generate response with context
  const response = await generateText({
    model: openai('gpt-4o'),
    system: `You are a helpful assistant. Use this context:\n${context}`,
    prompt: query
  });

  return response.text;
}
```

---

### Example 3: Full Agent Loop (Ingest + Retrieve)

Complete pattern for a memory-powered chatbot:

```typescript
async function agentLoop(userId: string, userMessage: string) {
  // A. Ingest the new message
  await memory.add(userMessage, { endUserId: userId });

  // B. Retrieve relevant past context
  const context = await memory.search(userMessage, {
    endUserId: userId,
    limit: 3
  });

  // C. Generate response with memory context
  const systemPrompt = context.length > 0
    ? `Context from past conversations:\n${context.map(m => m.content).join('\n')}`
    : "No prior context available.";

  const aiResponse = await yourLLM.generate({
    system: systemPrompt,
    user: userMessage
  });

  // D. Optionally ingest the AI response too
  await memory.add(aiResponse, {
    endUserId: userId,
    userContext: "AI response"
  });

  return aiResponse;
}
```

---

### Example 4: Image-Based Memory Search

Find memories visually similar to an uploaded image:

```typescript
import * as fs from 'fs';

async function findSimilarImages(userId: string, imagePath: string) {
  // 1. Load and encode the query image
  const imageBuffer = fs.readFileSync(imagePath);
  const base64Image = imageBuffer.toString('base64');

  // 2. Search for visually similar memories
  const similarMemories = await memory.searchImage(base64Image, {
    endUserId: userId,
    limit: 5
  });

  // 3. Each result contains source info if from a PDF
  similarMemories.forEach(mem => {
    console.log(`Found: ${mem.id}`);
    if (mem.source_doc) {
      console.log(`  → From: ${mem.source_doc}, Page: ${mem.page_number}`);
    }
  });

  return similarMemories;
}
```