# Recipes

> Six runnable examples, from a CLI chatbot to durable stream recovery.

Source: https://maincode.com/docs/client-sdk-recipes
Section: Client SDK · Matilda documentation

---

## Recipe 1: CLI chatbot

A complete interactive CLI chatbot with device-flow auth and streaming.

```ts title="cli-chatbot.ts"
import * as readline from 'node:readline/promises';
import { stdin, stdout } from 'node:process';
import Matilda from '@maincode-ai/matilda-client-sdk';
import { createFileTokenStore } from '@maincode-ai/matilda-client-sdk/auth/node';
import { homedir } from 'node:os';
import { join } from 'node:path';

const { store, lock } = createFileTokenStore(join(homedir(), '.matilda', 'tokens.json'));

const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' });

// Authenticate if needed
if (!(await client.auth.getTokens())) {
  console.log('Starting device flow authentication...');
  await client.auth.loginWithDeviceFlow({
    clientId: 'matilda-code',
    tokenStore: store,
    tokenLock: lock,
  });
  console.log('Authenticated!');
}

// Start chatting
const rl = readline.createInterface({ input: stdin, output: stdout });
const conversationId = crypto.randomUUID();

while (true) {
  const input = await rl.question('\nYou: ');
  if (!input.trim() || input.toLowerCase() === 'exit') break;

  process.stdout.write('Matilda: ');
  for await (const chunk of client.chat.streamText({ input, conversationId })) {
    process.stdout.write(chunk);
  }
  process.stdout.write('\n');
}

rl.close();
```

## Recipe 2: File Q\&A

Upload a document and ask questions about it.

```ts title="file-qa.ts"
import { readFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
import Matilda from '@maincode-ai/matilda-client-sdk';

const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' });

// Authenticate with device flow — token refresh is handled automatically
if (!(await client.auth.getTokens())) {
  await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code' });
}

// Upload a file
const buffer = readFileSync('./report.pdf');
const file = new File([buffer], 'report.pdf', { type: 'application/pdf' });
const upload = await client.files.upload(file, {
  onProgress: (pct) => process.stdout.write(`\rUploading: ${pct}%`),
});
console.log(`\nUploaded: ${upload.fileId} (${upload.status})`);

// A conversationId is required for follow-ups to share history — omitting it
// auto-creates a new conversation per call.
const conversationId = randomUUID();

// Ask a question about it
const response = await client.chat.create({
  input: 'Summarise the key findings in this report.',
  fileIds: [upload.fileId],
  conversationId,
});
console.log(response.outputText);

// Follow-up question in the same conversation
const followUp = await client.chat.create({
  input: 'What are the recommendations?',
  fileIds: [upload.fileId],
  conversationId,
});
console.log(followUp.outputText);
```

## Recipe 3: Conversation history browser

List, paginate, and inspect conversation history.

```ts title="history-browser.ts"
import Matilda from '@maincode-ai/matilda-client-sdk';

const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' });

// Authenticate with device flow — token refresh is handled automatically
if (!(await client.auth.getTokens())) {
  await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code' });
}

// List first page
let offset = 0;
const limit = 10;
let page = await client.conversations.list({ limit, offset });

console.log(`Total conversations: ${page.total}\n`);

for (const conv of page.conversations) {
  console.log(`[${conv.id}] ${conv.title}`);
  console.log(`  Updated: ${conv.updatedAt}`);
  console.log();
}

// Load next page
offset += limit;
if (offset < page.total) {
  page = await client.conversations.list({ limit, offset });
  for (const conv of page.conversations) {
    console.log(`[${conv.id}] ${conv.title}`);
  }
}

// Retrieve a full conversation
if (page.conversations.length > 0) {
  const full = await client.conversations.retrieve(page.conversations[0].id);
  console.log(`\n--- ${full.title} ---`);
  for (const msg of full.messages) {
    console.log(`\n[${msg.role.toUpperCase()}]`);
    console.log(msg.content);
    if (msg.feedback) {
      console.log(`  Feedback: ${msg.feedback}`);
    }
  }
}
```

## Recipe 4: Durable stream recovery

Start a stream, simulate a disconnect, and resume from the last cursor.

```ts title="stream-recovery.ts"
import Matilda from '@maincode-ai/matilda-client-sdk';

const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' });

// Authenticate with device flow — token refresh is handled automatically
if (!(await client.auth.getTokens())) {
  await client.auth.loginWithDeviceFlow({ clientId: 'matilda-code' });
}

const conversationId = crypto.randomUUID();
let streamId: string | null = null;
let lastEventId: string | undefined;
let receivedText = '';

// Start streaming — simulate disconnect after a few events
console.log('Starting stream...');
try {
  for await (const event of client.chat.stream({
    input: 'Write a very long, detailed essay about the history of computing.',
    conversationId,
  })) {
    if (event.type === 'response.created') streamId = event.streamId;
    if (event.type === 'response.cursor') lastEventId = event.lastEventId;
    if (event.type === 'response.output_text.delta') {
      receivedText += event.delta;
      // Simulate disconnect after 500 chars
      if (receivedText.length > 500) {
        console.log('\n--- Simulated disconnect ---');
        break;
      }
    }
    if (event.type === 'response.completed') {
      console.log('Stream completed naturally.');
    }
  }
} catch (err) {
  console.log('Disconnected:', err);
}

console.log(`Received ${receivedText.length} chars before disconnect.`);

// Check if the stream is still active
if (streamId) {
  const active = await client.chat.activeStream(conversationId);
  console.log(`Stream status: ${active.status}`);

  if (active.status === 'active') {
    console.log('\n--- Resuming ---');
    for await (const event of client.chat.resume({ streamId, lastEventId })) {
      if (event.type === 'response.output_text.delta') {
        receivedText += event.delta;
        process.stdout.write(event.delta);
      }
      if (event.type === 'response.completed') {
        console.log('\n\n--- Resume complete ---');
        console.log(`Total received: ${receivedText.length} chars`);
      }
    }
  }
}
```

## Recipe 5: Multi-environment setup

Run staging and production clients in the same process. Each instance manages its own auth independently.

```ts title="multi-env.ts"
import Matilda from '@maincode-ai/matilda-client-sdk';

const staging = new Matilda({ baseUrl: 'https://staging.matilda.maincode.com/api' });
const production = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' });

// Authenticate each instance independently — device flow or browser login
if (!(await staging.auth.getTokens())) {
  await staging.auth.loginWithDeviceFlow({
    clientId: 'matilda-code',
  });
}
if (!(await production.auth.getTokens())) {
  await production.auth.loginWithBrowser({ clientId: 'matilda-code' });
}

// Run the same prompt against both environments
const [stagingResponse, prodResponse] = await Promise.all([
  staging.chat.createText({ input: 'Explain quantum entanglement.' }),
  production.chat.createText({ input: 'Explain quantum entanglement.' }),
]);

console.log('Staging:', stagingResponse);
console.log('Production:', prodResponse);

// Instances are fully isolated — each manages its own token lifecycle
// Reconfiguring one never affects the other:
staging.configure({ baseUrl: 'https://override.example/api' });
// production.config.baseUrl is unchanged
```

## Recipe 6: Custom token store

Implement `StorageAdapter` to store tokens in a database or other custom backend.

```ts title="token-store.ts"
import Matilda from '@maincode-ai/matilda-client-sdk';
import type { StorageAdapter } from '@maincode-ai/matilda-client-sdk';

// Example: a database-backed token store
class DatabaseTokenStore implements StorageAdapter {
  constructor(private db: Database) {}

  async get(key: string): Promise<string | null> {
    const row = await this.db.query('SELECT value FROM tokens WHERE key = $1', [key]);
    return row?.value ?? null;
  }

  async set(key: string, value: string): Promise<void> {
    await this.db.query(
      'INSERT INTO tokens (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2',
      [key, value],
    );
  }

  async remove(key: string): Promise<void> {
    await this.db.query('DELETE FROM tokens WHERE key = $1', [key]);
  }
}

const tokenStore = new DatabaseTokenStore(myDatabase);

const client = new Matilda({ baseUrl: 'https://matilda.maincode.com/api' });

await client.auth.loginWithDeviceFlow({
  clientId: 'matilda-code',
  tokenStore,
  // No cross-process lock needed — the database handles concurrency
});

// Tokens are now persisted in the database and survive process restarts
const response = await client.chat.create({ input: 'Hello!' });
console.log(response.outputText);
```
