--- 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 ``` - `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 `` 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: - `` — inline 3-step flow (phone → OTP → profile review). Best for embedding in your own UI. - `` — 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, 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 ``` 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, 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 My Agent

My Agent

Enrich your profile

``` 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 `` 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 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 | |-----------|---------|-----------| | `` | Inline auth (phone → OTP → profile review) | `configure:authenticated` | | `` | Modal auth (phone → OTP → connections → profile editor) | `configure:authenticated` | | `` | Tool connection UI | `configure:tool-connect` | | `` | Chat history import | `configure:import-complete` | | `` | Profile data review + permissions | — | | `` | 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