---
name: configure
description: Add persistent user memory, identity, and connected tools to your AI agent. Users authenticate once with phone OTP. Their profile — identity, preferences, memories, connected tool data — persists across every agent they use.
allowed-tools: [Bash, Read, Write, Edit]
---

# Configure

Persistent memory and identity for AI agents. Users authenticate with phone OTP and get a profile that persists across every agent. Profiles contain identity, preferences, memories, and connected tool data (Gmail, Calendar, Drive, Notion). Developers read and write profiles via SDK. One install, one auth — profiles start empty and get richer over time.

`npm install configure`

## Detect State

Before starting, check the project's current state to determine the right path.

### 1. Check for existing API key
Look for `CONFIGURE_API_KEY` in `.env`, environment variables, or secrets.
- **Found** (starts with `sk_`) → Go to [Existing Developer](#existing-developer)
- **Not found** → Go to [New Developer](#new-developer)

### 2. Check for existing project
- `package.json` exists with a server entry point (Express, Fastify, Next.js, etc.) → **Existing Agent**
- No `package.json` or empty project → **New Agent**

This determines your integration path:

| State | Path |
|-------|------|
| No API key + no project | [New Developer](#new-developer) → [Build a New Agent](#build-a-new-agent) |
| No API key + existing project | [New Developer](#new-developer) → [Add to Existing Agent](#add-to-existing-agent) |
| Has API key + no `CONFIGURE_AGENT` | [Existing Developer](#existing-developer) → choose integration path |
| Has API key + has `CONFIGURE_AGENT` | Skip to [Add to Existing Agent](#add-to-existing-agent) or [Build a New Agent](#build-a-new-agent) |

---

## New Developer

For developers who haven't used Configure before. This creates your account, agent, and API keys via phone verification. No browser required.

### Step 1: Install

```bash
npm install configure
```

### Step 2: Collect developer info

Ask the developer for three things:
- **Phone number** in E.164 format: `+14075551234`
- **Email address**
- **Agent name** — lowercase alphanumeric + hyphens, 2-63 chars (e.g. `my-agent`)

Agent name rules:
- Lowercase letters, numbers, and hyphens only
- 2-63 characters, no leading/trailing hyphens
- Reserved names (cannot use): `docs`, `api`, `www`, `app`, `admin`, `dashboard`, `status`, `blog`, `mcp`, `v1`, `auth`, `profile`, `tools`, `memory`

### Step 3: Start provisioning

```bash
curl -X POST https://api.configure.dev/v1/developer/provision \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+14075551234",
    "email": "dev@example.com",
    "agent_name": "my-agent"
  }'
```

**Success** — OTP sent to phone:
```json
{ "ok": true, "verification_id": "uuid" }
```

**Error — email already registered** (409):
```json
{
  "error": "already_registered",
  "message": "This email already has a Configure account.",
  "existing_agents": ["existing-agent"],
  "actions": {
    "add_agent": "POST /v1/developer/agents with X-API-Key header to create a new agent",
    "dashboard": "https://configure.dev/login to manage keys and agents"
  }
}
```
→ Tell the developer they already have an account. Show their existing agents. Ask if they want to use one of those or add a new agent (they'll need their `sk_` key — go to [Existing Developer](#existing-developer)).

**Error — agent name taken** (409):
```json
{ "error": "agent_taken", "message": "Agent name 'my-agent' is already taken." }
```
→ Ask for a different name and retry.

**Error — validation failed** (400): Check the `message` field, fix the input, retry.

**Error — rate limited** (429): Wait the number of seconds in `retry_after`, then retry.

### Step 4: Verify OTP

Ask the developer for the 6-digit code sent to their phone.

```bash
curl -X POST https://api.configure.dev/v1/developer/provision/verify \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+14075551234",
    "code": "831204",
    "email": "dev@example.com",
    "agent_name": "my-agent"
  }'
```

**Success** (201):
```json
{
  "ok": true,
  "secret_key": "sk_...",
  "publishable_key": "pk_...",
  "agent": "my-agent",
  "developer_account_id": "uuid",
  "message": "Account provisioned. Save your secret key — it won't be shown again."
}
```

**Error — wrong code** (401): `otp_invalid`. Ask developer to re-enter. If repeated failure, restart from Step 3 to re-send OTP.

**Error — SMS blocked** (429): `otp_blocked`. Tell developer: "Please try again in 15 minutes."

### Step 5: Write .env

```bash
CONFIGURE_API_KEY=sk_...
CONFIGURE_PUBLISHABLE_KEY=pk_...
CONFIGURE_AGENT=my-agent
```

Add `.env` to `.gitignore` if not already present. The secret key (`sk_`) must never be committed.

### Step 6: Verify setup

Create and run a quick verification:

```javascript
// verify-setup.mjs
import { ConfigureClient } from 'configure';

const client = new ConfigureClient(process.env.CONFIGURE_API_KEY, {
  agent: process.env.CONFIGURE_AGENT,
});

try {
  const demo = await client.auth.getDemo();
  console.log('✓ Setup verified — API key works');
} catch (err) {
  console.error('✗ Setup failed:', err.message);
  if (err.code === 'API_KEY_MISSING') console.error('  → .env not loaded. Use: node --env-file=.env verify-setup.mjs');
  if (err.code === 'AUTH_REQUIRED') console.error('  → API key is invalid. Check CONFIGURE_API_KEY in .env');
  process.exit(1);
}
```

```bash
node --env-file=.env verify-setup.mjs
```

If this fails with `api_key_invalid`, the key may be wrong — re-check `.env`. If it fails with a network error, verify connectivity to `api.configure.dev`.

Delete `verify-setup.mjs` after it passes.

Now go to [Add to Existing Agent](#add-to-existing-agent) or [Build a New Agent](#build-a-new-agent) based on your project state.

---

## Existing Developer

For developers who already have `CONFIGURE_API_KEY` in `.env`.

### Check for agent name

If `CONFIGURE_AGENT` is already in `.env` → skip to [Add to Existing Agent](#add-to-existing-agent) or [Build a New Agent](#build-a-new-agent).

If no `CONFIGURE_AGENT` — ask the developer which agent this project should use, or create a new one:

### Create a new agent

Requires the secret key (`sk_`). Publishable keys (`pk_`) cannot create agents.

```bash
curl -X POST https://api.configure.dev/v1/developer/agents \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sk_..." \
  -d '{
    "name": "second-agent",
    "display_name": "Second Agent",
    "description": "My new project"
  }'
```

**Success** (201):
```json
{
  "ok": true,
  "agent": "second-agent",
  "id": "uuid",
  "message": "Agent created. Your existing API key works — pass { agent: 'second-agent' } to ConfigureClient."
}
```

**Errors:**
- `403 secret_key_required` — Using a `pk_` key. This endpoint requires `sk_`.
- `409 already_exists` — Agent name taken. Ask for a different name.
- `401 api_key_invalid` — Key is wrong or revoked. Check `.env`.

Add `CONFIGURE_AGENT=second-agent` to `.env`. Existing `sk_`/`pk_` keys work for all agents — `CONFIGURE_AGENT` determines which namespace writes go to.

Now go to [Add to Existing Agent](#add-to-existing-agent) or [Build a New Agent](#build-a-new-agent).

---

## Add to Existing Agent

For projects that already have a server, an LLM, and a chat UI. You're adding Configure's memory and identity layer.

**Before writing code:** Read the developer's existing server and frontend files. Understand: (1) where the LLM is called, (2) where the system prompt is assembled, (3) where tool results are handled, (4) where the frontend renders. Place Configure calls in those exact locations. Do not restructure the developer's code.

### Step 1: Initialize the client (backend)

```typescript
import { ConfigureClient, CONFIGURE_TOOLS } from 'configure';

const client = new ConfigureClient(process.env.CONFIGURE_API_KEY, {
  agent: process.env.CONFIGURE_AGENT,
});
```

### Step 2: Add auth to the frontend

```html
<script type="module">
  import 'configure/components';
</script>

<configure-auth
  api-key="YOUR_PK_KEY"
  agent="YOUR_AGENT_NAME"
></configure-auth>
```

- `api-key` uses the **publishable key** (`pk_`), never the secret key
- `agent` is the agent name from `.env`
- `import 'configure/components'` is a side-effect import — it registers all `<configure-*>` custom elements

Listen for the auth event:

```javascript
document.addEventListener('configure:authenticated', (e) => {
  const { token, userId } = e.detail;
  // Store token + userId — pass them to your backend on every request
});
```

Two auth components are available:
- `<configure-auth>` — inline 3-step flow (phone → OTP → profile review). Best for embedding in your own UI.
- `<configure-auth-modal>` — modal 4-step flow (phone → OTP → tool connections → profile editor). Best for full onboarding.

### Step 3: Add profile context to the system prompt

In your chat endpoint, before calling the LLM:

```typescript
const profile = await client.profile.get(token, userId, {
  sections: ['identity', 'summary', 'integrations'],
});
const profileContext = profile.format({ guidelines: true });

const systemPrompt = `${yourExistingPrompt}\n\n${profileContext}`;
```

`profile.format()` returns a prompt-ready string. For a user with Gmail connected and some memories, it looks like:

```
User:
Name: Christian Ancheta
Email: christian@example.com
Occupation: Software Engineer
Location: Orlando, FL
Interests: AI infrastructure, prediction markets

Connected: gmail, calendar

About this user:
Building Configure — the identity layer for AI agents...

Memories:
atlas:
Prefers dark mode. Working on prediction market integration...

CONFIGURE GUIDELINES — handling personal data responsibly

GROUNDING:
- Only state specific facts if they appear in the user's profile context or tool results.
- Never fabricate or assume personal information.

TRANSPARENCY:
- When referencing personal data, briefly cite your source.

TOOL CONNECTIONS:
- When a tool returns "not connected", suggest connecting it naturally.

CONVERSATION EFFICIENCY:
- Check conversation history before re-searching.
```

`guidelines: true` is the default. It appends behavioral guidelines that teach the LLM how to use profile data responsibly.

### Step 4: Add CONFIGURE_TOOLS to the LLM

For Anthropic:

```typescript
import Anthropic from '@anthropic-ai/sdk';

const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-20250514',
  system: systemPrompt,
  messages: conversationHistory,
  tools: [...yourExistingTools, ...CONFIGURE_TOOLS],
  max_tokens: 4096,
});
```

For OpenAI:

```typescript
import { toOpenAIFunctions } from 'configure';

const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'system', content: systemPrompt }, ...conversationHistory],
  tools: [...yourExistingTools, ...toOpenAIFunctions(CONFIGURE_TOOLS)],
});
```

Handle tool calls with this switch. All 16 tools in `CONFIGURE_TOOLS`:

```typescript
async function executeTool(
  name: string,
  input: Record<string, any>,
  token: string,
  userId: string,
) {
  switch (name) {
    // Profile tools
    case 'get_profile':
      return client.profile.get(token, userId);
    case 'profile_read':
      return client.profile.read(token, userId, input.path);
    case 'profile_ls':
      return client.profile.ls(token, userId, input.path);
    case 'get_memories':
      return client.profile.getMemories(token, userId, input);
    case 'remember':
      return client.profile.remember(token, userId, input.fact || input.content);
    case 'ingest':
      return client.profile.ingest(token, userId, input.messages, { sync: false });

    // Self tools (agent's own storage)
    case 'self_get_profile':
      return client.self.getProfile();
    case 'self_get_memories':
      return client.self.getMemories();

    // Live data tools
    case 'search_emails':
      return client.tools.searchEmails(token, userId, input.query, input.max_results ? { maxResults: input.max_results } : undefined);
    case 'get_calendar':
      return client.tools.getCalendar(token, userId, input.range);
    case 'search_files':
      return client.tools.searchFiles(token, userId, input.query, input.max_results ? { maxResults: input.max_results } : undefined);
    case 'search_notes':
      return client.tools.searchNotes(token, userId, input.query, input.max_results ? { maxResults: input.max_results } : undefined);
    case 'search_web':
      return client.tools.searchWeb(token, userId, input.query, input.max_results ? { maxResults: input.max_results } : undefined);
    case 'fetch_url':
      return client.tools.fetchUrl(token, userId, input.url);

    // Action tools
    case 'create_calendar_event':
      return client.tools.createCalendarEvent(token, userId, {
        title: input.title,
        startTime: input.start_time,
        endTime: input.end_time,
        description: input.description,
        location: input.location,
      });
    case 'send_email':
      return client.tools.sendEmail(token, userId, {
        to: input.to,
        subject: input.subject,
        body: input.body,
      });

    default:
      return { error: `Unknown tool: ${name}` };
  }
}
```

When the LLM returns a `tool_use` block, call `executeTool(block.name, block.input, token, userId)` and return the result as a `tool_result` message. Continue the loop until the LLM stops requesting tools.

### Step 5: Add memory extraction

After each conversation turn, fire-and-forget an ingest call:

```typescript
client.profile.ingest(token, userId, [
  { role: 'user', content: userMessage },
  { role: 'assistant', content: assistantResponse },
]).catch(() => {});
```

Don't `await` this. It runs in the background. Configure's memory engine extracts facts, preferences, and context into the user's profile. Memories accumulate over time and appear in future `profile.format()` calls.

### Step 6: Add profile seeding to the frontend

Profiles start empty. These components let users seed their profile with rich data:

```html
<configure-connection-list
  api-key="YOUR_PK_KEY"
  auth-token="TOKEN_FROM_AUTH"
  user-id="USERID_FROM_AUTH"
  agent="YOUR_AGENT_NAME"
  tools="gmail,calendar,drive,notion"
></configure-connection-list>

<configure-memory-import
  api-key="YOUR_PK_KEY"
  auth-token="TOKEN_FROM_AUTH"
  user-id="USERID_FROM_AUTH"
  agent="YOUR_AGENT_NAME"
  providers="chatgpt,claude,gemini"
></configure-memory-import>
```

Set the `auth-token` and `user-id` attributes after the `configure:authenticated` event fires.

- **Gmail connect** is the richest data source (~10 seconds to sync). Email-derived identity (name, occupation, contacts, interests) auto-populates the profile.
- **Chat history import** takes ~30 seconds. Past conversations become profile memories.
- Without explicit seeding, profiles build organically via `ingest()` after each conversation — but start sparse.

Now go to [See It Work](#see-it-work) to verify it works.

---

## Build a New Agent

For starting from scratch. This creates a complete working agent with auth, memory, tools, and profile seeding.

### Step 1: Project setup

```bash
mkdir my-agent && cd my-agent
npm init -y
npm install configure @anthropic-ai/sdk express
```

`.env` already exists from the [New Developer](#new-developer) or [Existing Developer](#existing-developer) steps.

### Step 2: Create server.ts

```typescript
import 'dotenv/config';
import express from 'express';
import path from 'path';
import Anthropic from '@anthropic-ai/sdk';
import { ConfigureClient, CONFIGURE_TOOLS, classifyError } from 'configure';

const app = express();
app.use(express.json());
app.use(express.static(path.join(import.meta.dirname, 'public')));

const client = new ConfigureClient(process.env.CONFIGURE_API_KEY, {
  agent: process.env.CONFIGURE_AGENT,
});
const anthropic = new Anthropic();

// Auth proxy — forwards OTP requests to Configure
app.post('/auth/send-otp', async (req, res) => {
  try {
    await client.auth.sendOtp(req.body.phone);
    res.json({ ok: true });
  } catch (err) {
    res.status(400).json({ error: err instanceof Error ? err.message : 'Unknown error' });
  }
});

app.post('/auth/verify-otp', async (req, res) => {
  try {
    const result = await client.auth.verifyOtp(req.body.phone, req.body.code);
    res.json(result);
  } catch (err) {
    res.status(400).json({ error: err instanceof Error ? err.message : 'Unknown error' });
  }
});

// Tool execution — maps CONFIGURE_TOOLS to SDK methods
async function executeTool(
  name: string,
  input: Record<string, any>,
  token: string,
  userId: string,
) {
  const maxResults = input.max_results ? { maxResults: input.max_results } : undefined;

  switch (name) {
    case 'get_profile':
      return client.profile.get(token, userId);
    case 'profile_read':
      return client.profile.read(token, userId, input.path);
    case 'profile_ls':
      return client.profile.ls(token, userId, input.path);
    case 'get_memories':
      return client.profile.getMemories(token, userId, input);
    case 'remember':
      return client.profile.remember(token, userId, input.fact || input.content);
    case 'ingest':
      return client.profile.ingest(token, userId, input.messages, { sync: false });
    case 'self_get_profile':
      return client.self.getProfile();
    case 'self_get_memories':
      return client.self.getMemories();
    case 'search_emails':
      return client.tools.searchEmails(token, userId, input.query, maxResults);
    case 'get_calendar':
      return client.tools.getCalendar(token, userId, input.range);
    case 'search_files':
      return client.tools.searchFiles(token, userId, input.query, maxResults);
    case 'search_notes':
      return client.tools.searchNotes(token, userId, input.query, maxResults);
    case 'search_web':
      return client.tools.searchWeb(token, userId, input.query, maxResults);
    case 'fetch_url':
      return client.tools.fetchUrl(token, userId, input.url);
    case 'create_calendar_event':
      return client.tools.createCalendarEvent(token, userId, {
        title: input.title, startTime: input.start_time, endTime: input.end_time,
        description: input.description, location: input.location,
      });
    case 'send_email':
      return client.tools.sendEmail(token, userId, {
        to: input.to, subject: input.subject, body: input.body,
      });
    default:
      return { error: `Unknown tool: ${name}` };
  }
}

// Chat endpoint
app.post('/chat', async (req, res) => {
  const { token, userId, message, history = [] } = req.body;
  if (!token || !userId || !message) {
    return res.status(400).json({ error: 'token, userId, and message are required' });
  }

  try {
    // Load profile and build system prompt
    const profile = await client.profile.get(token, userId, {
      sections: ['identity', 'summary', 'integrations'],
    });
    const context = profile.format({ guidelines: true });
    const systemPrompt = `You are a helpful assistant.\n\n${context}`;

    // Build messages
    const messages: Anthropic.MessageParam[] = [
      ...history.map((m: any) => ({ role: m.role as 'user' | 'assistant', content: m.content })),
      { role: 'user', content: message },
    ];

    // Agentic loop — keep going until the LLM stops requesting tools
    let response = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 4096,
      system: systemPrompt,
      tools: CONFIGURE_TOOLS as any,
      messages,
    });

    while (response.stop_reason === 'tool_use') {
      const toolBlocks = response.content.filter((b): b is Anthropic.ToolUseBlock => b.type === 'tool_use');
      const toolResults: Anthropic.ToolResultBlockParam[] = [];

      for (const block of toolBlocks) {
        const result = await executeTool(block.name, block.input as any, token, userId);
        toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result) });
      }

      messages.push({ role: 'assistant', content: response.content });
      messages.push({ role: 'user', content: toolResults });

      response = await anthropic.messages.create({
        model: 'claude-sonnet-4-20250514',
        max_tokens: 4096,
        system: systemPrompt,
        tools: CONFIGURE_TOOLS as any,
        messages,
      });
    }

    const text = response.content
      .filter((b): b is Anthropic.TextBlock => b.type === 'text')
      .map(b => b.text)
      .join('');

    // Fire-and-forget memory extraction
    client.profile.ingest(token, userId, [
      { role: 'user', content: message },
      { role: 'assistant', content: text },
    ]).catch(() => {});

    res.json({ response: text });
  } catch (err) {
    const classified = classifyError(err);
    res.status(500).json({ error: classified.message });
  }
});

app.listen(3000, () => console.log('Agent running at http://localhost:3000'));
```

### Step 3: Create public/index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Agent</title>
  <script type="module">import 'configure/components';</script>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 640px; margin: 40px auto; padding: 0 20px; }
    #chat { display: none; }
    #messages { min-height: 200px; border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin-bottom: 12px; white-space: pre-wrap; }
    #input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px; font-size: 16px; }
    .seeding { margin-top: 24px; padding-top: 24px; border-top: 1px solid #eee; }
  </style>
</head>
<body>
  <h1>My Agent</h1>

  <configure-auth
    api-key="REPLACE_WITH_PK"
    agent="REPLACE_WITH_AGENT"
  ></configure-auth>

  <div id="chat">
    <div id="messages"></div>
    <input id="input" placeholder="Say something..." />

    <div class="seeding">
      <h3>Enrich your profile</h3>
      <configure-connection-list
        api-key="REPLACE_WITH_PK"
        agent="REPLACE_WITH_AGENT"
        tools="gmail,calendar,drive,notion"
      ></configure-connection-list>
      <configure-memory-import
        api-key="REPLACE_WITH_PK"
        agent="REPLACE_WITH_AGENT"
        providers="chatgpt,claude,gemini"
      ></configure-memory-import>
    </div>
  </div>

  <script>
    let token, userId;

    document.addEventListener('configure:authenticated', (e) => {
      token = e.detail.token;
      userId = e.detail.userId;
      document.getElementById('chat').style.display = 'block';

      // Pass auth to seeding components
      document.querySelectorAll('configure-connection-list, configure-memory-import').forEach(el => {
        el.setAttribute('auth-token', token);
        el.setAttribute('user-id', userId);
      });
    });

    document.getElementById('input').addEventListener('keydown', async (e) => {
      if (e.key !== 'Enter' || !token) return;
      const input = e.target;
      const message = input.value.trim();
      if (!message) return;
      input.value = '';

      const messages = document.getElementById('messages');
      messages.textContent += `You: ${message}\n`;

      const res = await fetch('/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token, userId, message }),
      });
      const data = await res.json();
      messages.textContent += `Agent: ${data.response}\n\n`;
    });
  </script>
</body>
</html>
```

Replace `REPLACE_WITH_PK` with your `CONFIGURE_PUBLISHABLE_KEY` from `.env`, and `REPLACE_WITH_AGENT` with your `CONFIGURE_AGENT`.

Now go to [See It Work](#see-it-work).

---

## See It Work

Both integration paths converge here. Verify your agent recognizes you.

### 1. Start your app

```bash
node --env-file=.env server.ts
```

Open `http://localhost:3000`.

### 2. Authenticate

Enter your phone number (the same one used during setup). Enter the OTP code. You are now User #1 of your own agent.

### 3. Seed your profile

Do at least one of these to populate your profile with real data:

**Option A — Connect Gmail** (richest data, ~10 seconds)
Click "Connect Gmail" → complete Google OAuth → wait for sync to finish. Email-derived identity (name, occupation, contacts, interests) auto-populates your profile.

**Option B — Import chat history** (~30 seconds)
Click "Import ChatGPT Memories" → follow the export instructions → paste the exported data → wait for extraction to complete.

**Option C — Just chat** (slow path)
Skip seeding. Your profile builds organically via `ingest()` after each conversation. The first response will be generic. Options A or B demonstrate value immediately.

For more seeding patterns (batch import, text mode, manual facts), see the [Profile Seeding guide](https://docs.configure.dev/guides/profile-seeding).

**Important:** After connecting a tool or importing, wait 10-30 seconds for the data to process before sending your first message.

### 4. Send a message

Type "What do you know about me?" or just "Hello."

**What you should see:** The agent responds with specific, personal details drawn from your seeded data. Not "Hello! How can I help you today?" Something like:

> "Hey Christian — I can see you're a software engineer working on AI infrastructure in Orlando. Based on your recent emails, it looks like you've been coordinating with a few people about prediction markets. You also seem interested in Japan travel for this summer. How can I help?"

That's Configure working. You just experienced what your users will experience — personalized from the first message.

### Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| Generic greeting, no personal details | Profile empty — seeding didn't complete | Check tool connect or import finished. Call `profile.get()` directly to inspect. |
| Auth component doesn't render | Missing import or wrong key | Verify `import 'configure/components'` runs. Check `api-key` uses `pk_` key. |
| `API_KEY_MISSING` error | .env not loaded | Use `node --env-file=.env` or add `import 'dotenv/config'` at the top of server.ts |
| `TOOL_NOT_CONNECTED` on search | User hasn't connected that tool | Expected behavior. Show `<configure-connection-list>` so the user can connect it. |
| Tool calls return errors | CONFIGURE_TOOLS not passed to LLM | Verify `tools: CONFIGURE_TOOLS` in your `messages.create()` call |
| No memories after chatting | `ingest()` not called or failing | Add `.catch(console.error)` temporarily to debug ingest errors |
| CORS error from localhost | Browser making direct API calls | All `sk_` calls must be server-side. Frontend uses components (which use `pk_`), not direct API calls. |

---

## Quick Reference

### Imports

```typescript
import { ConfigureClient, CONFIGURE_TOOLS, CONFIGURE_GUIDELINES, ConfigureError, classifyError, toOpenAIFunctions } from 'configure';
import 'configure/components'; // registers all <configure-*> elements
```

Import paths: `'configure'`, `'configure/components'`, `'configure/tool-definitions'`, `'configure/types'`, `'configure/cfs-types'`

### Client initialization

```typescript
// Auto-detect from CONFIGURE_API_KEY and CONFIGURE_AGENT env vars
const client = new ConfigureClient();

// Explicit
const client = new ConfigureClient('sk_...', { agent: 'my-agent' });
```

### Profile operations

```typescript
const profile = await client.profile.get(token, userId, { sections: ['identity', 'summary', 'integrations'] });
const context = profile.format({ guidelines: true }); // prompt-ready string
await client.profile.remember(token, userId, 'User prefers dark mode');
await client.profile.ingest(token, userId, messages);
const memories = await client.profile.getMemories(token, userId);
```

### Tools operations

```typescript
const emails = await client.tools.searchEmails(token, userId, 'flight confirmation');
const events = await client.tools.getCalendar(token, userId, 'week');
const files = await client.tools.searchFiles(token, userId, 'quarterly report');
const notes = await client.tools.searchNotes(token, userId, 'meeting notes');
const results = await client.tools.searchWeb(token, userId, 'latest AI news');
const page = await client.tools.fetchUrl(token, userId, 'https://example.com');
await client.tools.createCalendarEvent(token, userId, { title, startTime, endTime });
await client.tools.sendEmail(token, userId, { to, subject, body });
```

### Components

| Component | Purpose | Key event |
|-----------|---------|-----------|
| `<configure-auth>` | Inline auth (phone → OTP → profile review) | `configure:authenticated` |
| `<configure-auth-modal>` | Modal auth (phone → OTP → connections → profile editor) | `configure:authenticated` |
| `<configure-connection-list>` | Tool connection UI | `configure:tool-connect` |
| `<configure-memory-import>` | Chat history import | `configure:import-complete` |
| `<configure-profile-editor>` | Profile data review + permissions | — |
| `<configure-tool-approval>` | Action approval (email, calendar) | — |

All components accept shared attributes: `api-key`, `base-url`, `auth-token`, `user-id`, `agent`, `theme`

### Links

- **Full API reference:** https://configure.dev/llms.txt
- **Reference implementation:** Atlas agent — `apps/agents/atlas/server.ts` in the Configure repo
- **Live demo:** https://atlas.configure.dev
- **Documentation:** https://docs.configure.dev
