Skip to main content
Version: 7.0.0 (December 2025)
Target Audience: Developers building claude-mem integrations (VSCode extensions, IDE plugins, CLI tools)

Quick Reference

Worker Base URL

http://localhost:37777
Override with CLAUDE_MEM_WORKER_PORT

Health Check

GET /api/health
Returns { "status": "ok" }

Queue Observation

POST /api/sessions/observations
Pass claudeSessionId + tool data

SSE Stream

GET /stream
Real-time events for UI updates

Most Common Operations

// Health check
GET /api/health

// Create/get session and queue observation
POST /api/sessions/observations
Body: { claudeSessionId, tool_name, tool_input, tool_response, cwd }

// Queue summary
POST /api/sessions/summarize
Body: { claudeSessionId, last_user_message, last_assistant_message }

// Complete session
POST /api/sessions/complete
Body: { claudeSessionId }

// Search observations
GET /api/search?query=authentication&type=observations&format=index&limit=20

// Get recent context for project
GET /api/context/recent?project=my-project&limit=3

Environment Variables

CLAUDE_MEM_MODEL=claude-sonnet-4-5          # Model for observations/summaries
CLAUDE_MEM_CONTEXT_OBSERVATIONS=50          # Observations injected at SessionStart
CLAUDE_MEM_WORKER_PORT=37777                # Worker service port
CLAUDE_MEM_PYTHON_VERSION=3.13              # Python version for chroma-mcp

Build Commands (Local Development)

npm run build                 # Compile TypeScript (hooks + worker)
npm run sync-marketplace      # Copy to ~/.claude/plugins
npm run worker:restart        # Restart worker
npm run worker:logs           # View worker logs
npm run worker:status         # Check worker status

Worker Architecture

Request Flow

Platform Hook/Extension
  → HTTP Request to Worker (localhost:37777)
    → Route Handler (SessionRoutes/DataRoutes/SearchRoutes/etc.)
      → Domain Service (SessionManager/SearchManager/DatabaseManager)
        → Database (SQLite3 + Chroma vector DB)
          → SSE Broadcast (real-time UI updates)

Domain Services

DatabaseManager

SQLite connection management, initialization

SessionManager

Event-driven session lifecycle, message queues

SearchManager

Search orchestration (FTS5 + Chroma)

SSEBroadcaster

Server-Sent Events for real-time updates

SDKAgent

Claude Agent SDK for generating observations/summaries

PaginationHelper

Query pagination utilities

SettingsManager

User settings CRUD

FormattingService

Result formatting (index vs full)

TimelineService

Unified timeline generation

Route Organization

  • Health check endpoint
  • Viewer UI (React app)
  • SSE stream for real-time updates
  • Session lifecycle (init, observations, summarize, complete)
  • Privacy checks and tag stripping
  • Auto-start SDK agent generators
  • Data retrieval (observations, summaries, prompts, stats)
  • Pagination support
  • Processing status
  • All search operations
  • Unified search API
  • Timeline context
  • Semantic shortcuts
  • User settings
  • MCP toggle
  • Git branch switching

API Reference

Session Lifecycle (SessionRoutes)

Create/Get Session + Queue Observation

POST /api/sessions/observations
Content-Type: application/json

{
  "claudeSessionId": "abc123",
  "tool_name": "Bash",
  "tool_input": { "command": "ls" },
  "tool_response": { "stdout": "..." },
  "cwd": "/path/to/project"
}
Privacy Check: Skips if the user prompt was entirely wrapped in <private> tags.
Tag Stripping: Removes <private> and <claude-mem-context> tags before storage.
Auto-Start: Ensures SDK agent generator is running to process the queue.

Queue Summary

POST /api/sessions/summarize
Content-Type: application/json

{
  "claudeSessionId": "abc123",
  "last_user_message": "User's message",
  "last_assistant_message": "Assistant's response"
}

Complete Session

POST /api/sessions/complete
Content-Type: application/json

{
  "claudeSessionId": "abc123"
}
Effect: Stops SDK agent, marks session complete, broadcasts status change.

Legacy Endpoints (Still Supported)

POST /sessions/:sessionDbId/init
Body: { userPrompt, promptNumber }
New integrations should use /api/sessions/* endpoints with claudeSessionId.

Data Retrieval (DataRoutes)

Get Paginated Data

GET /api/observations?offset=0&limit=20&project=my-project
Response Format
{
  "items": [...],
  "hasMore": boolean,
  "offset": number,
  "limit": number
}

Get by ID

GET /api/observation/:id

Get Database Stats

GET /api/stats
Response
{
  "worker": {
    "version": "7.0.0",
    "uptime": 12345,
    "activeSessions": 2,
    "sseClients": 1,
    "port": 37777
  },
  "database": {
    "path": "~/.claude-mem/claude-mem.db",
    "size": 1048576,
    "observations": 500,
    "sessions": 50,
    "summaries": 25
  }
}

Get Projects List

GET /api/projects
Response
{
  "projects": ["claude-mem", "other-project"]
}

Get Processing Status

GET /api/processing-status
Response
{
  "isProcessing": boolean,
  "queueDepth": number
}

Search Operations (SearchRoutes)

GET /api/search?query=authentication&type=observations&format=index&limit=20
query
string
Search query text (optional, omit for filter-only)
type
string
default:"all"
"observations" | "sessions" | "prompts"
format
string
default:"index"
"index" | "full"
limit
number
default:20
Number of results
project
string
Filter by project name
obs_type
string
Filter by observation type: discovery, decision, bugfix, feature, refactor
concepts
string
Filter by concepts (comma-separated)
files
string
Filter by file paths (comma-separated)
dateStart
string
ISO timestamp (filter start)
dateEnd
string
ISO timestamp (filter end)
Response
{
  "observations": [...],
  "sessions": [...],
  "prompts": [...]
}
Format Options:
  • index: Minimal fields for list display (id, title, preview)
  • full: Complete entity with all fields

Unified Timeline

GET /api/timeline?anchor=123&depth_before=10&depth_after=10&project=my-project
anchor
string
required
Anchor point (observation ID, "S123" for session, or ISO timestamp)
depth_before
number
default:10
Records before anchor
depth_after
number
default:10
Records after anchor
project
string
Filter by project
Response
[
  { "type": "observation", "id": 120, "created_at_epoch": 1700000000000 },
  { "type": "session",     "id": 5,   "created_at_epoch": 1700000001000 },
  { "type": "observation", "id": 123, "created_at_epoch": 1700000002000 }
]

Semantic Shortcuts

Decisions

GET /api/decisions?format=index&limit=20

Changes

GET /api/changes?format=index&limit=20

How It Works

GET /api/how-it-works?format=index&limit=20

Search by Concept

GET /api/search/by-concept?concept=discovery&format=index&limit=10&project=my-project

Search by File Path

GET /api/search/by-file?filePath=src/services/worker-service.ts&format=index&limit=10

Search by Type

GET /api/search/by-type?type=bugfix&format=index&limit=10

Get Recent Context

GET /api/context/recent?project=my-project&limit=3
Response
{
  "summaries": [...],
  "observations": [...]
}

Context Preview (for Settings UI)

GET /api/context/preview?project=my-project
Returns plain text with ANSI colors for terminal display.

Context Injection (for Hooks)

GET /api/context/inject?project=my-project&colors=true
Returns a pre-formatted context string ready for display or system prompt injection.

Settings & Configuration (SettingsRoutes)

Get/Update User Settings

GET /api/settings

MCP Server Status/Toggle

GET /api/mcp/status

Git Branch Operations

GET /api/branch/status
{
  "current": "main",
  "remote": "origin/main",
  "ahead": 0,
  "behind": 0
}

Viewer & Real-Time Updates (ViewerRoutes)

Health Check

GET /api/health
Response
{ "status": "ok" }

Viewer UI

GET /
Returns the HTML shell for the React viewer app.

SSE Stream

GET /stream
Server-Sent Events streamEvent Types:
  • processing_status: { type, isProcessing, queueDepth }
  • session_started: { type, sessionDbId, project }
  • observation_queued: { type, sessionDbId }
  • summarize_queued: { type }
  • observation_created: { type, observation }
  • summary_created: { type, summary }
  • new_prompt: { type, id, claude_session_id, project, prompt_number, prompt_text, created_at_epoch }

Data Models

Active Session (In-Memory)

interface ActiveSession {
  sessionDbId: number;                    // Database ID (numeric)
  claudeSessionId: string;               // Claude session identifier (string)
  sdkSessionId: string | null;           // SDK session ID
  project: string;                       // Project name
  userPrompt: string;                    // Current user prompt text
  pendingMessages: PendingMessage[];     // Queue of pending operations
  abortController: AbortController;     // For cancellation
  generatorPromise: Promise<void> | null; // SDK agent promise
  lastPromptNumber: number;             // Last processed prompt number
  startTime: number;                    // Session start timestamp
  cumulativeInputTokens: number;        // Total input tokens
  cumulativeOutputTokens: number;       // Total output tokens
}

interface PendingMessage {
  type: 'observation' | 'summarize';
  tool_name?: string;
  tool_input?: any;
  tool_response?: any;
  prompt_number?: number;
  cwd?: string;
  last_user_message?: string;
  last_assistant_message?: string;
}

Database Entities

interface SDKSessionRow {
  id: number;
  claude_session_id: string;
  sdk_session_id: string;
  project: string;
  user_prompt: string;
  created_at_epoch: number;
  completed_at_epoch?: number;
}

Search Results

interface ObservationSearchResult {
  id: number;
  title: string;
  subtitle?: string;
  summary: string;
  facts: string[];         // Parsed from JSON
  concepts: string[];      // Parsed from JSON
  files_touched: string[]; // Parsed from JSON
  obs_type: string;
  project: string;
  created_at_epoch: number;
  prompt_number: number;
  rank?: number;           // FTS5 rank score
}

interface SessionSummarySearchResult {
  id: number;
  summary_text: string;
  facts: string[];
  concepts: string[];
  files_touched: string[];
  project: string;
  created_at_epoch: number;
  rank?: number;
}

interface UserPromptSearchResult {
  id: number;
  claude_session_id: string;
  project: string;
  prompt_number: number;
  prompt_text: string;
  created_at_epoch: number;
  rank?: number;
}

Timeline Item

interface TimelineItem {
  type: 'observation' | 'session' | 'prompt';
  id: number;
  created_at_epoch: number;
  // Entity-specific fields based on type
}

Integration Patterns

Mapping Claude Code Hooks to Worker API

1

SessionStart Hook

Not needed for the new API — sessions are auto-created on the first observation.
2

UserPromptSubmit Hook

No API call needed — the user prompt is captured by the first observation in the prompt.
3

PostToolUse Hook

async function onPostToolUse(context: HookContext) {
  const { session_id, tool_name, tool_input, tool_result, cwd } = context;

  const response = await fetch('http://localhost:37777/api/sessions/observations', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      claudeSessionId: session_id,
      tool_name,
      tool_input,
      tool_response: tool_result,
      cwd
    })
  });

  const result = await response.json();
  // result.status === 'queued' | 'skipped'
}
4

Summary Hook

async function onSummary(context: HookContext) {
  const { session_id, last_user_message, last_assistant_message } = context;

  await fetch('http://localhost:37777/api/sessions/summarize', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      claudeSessionId: session_id,
      last_user_message,
      last_assistant_message
    })
  });
}
5

SessionEnd Hook

async function onSessionEnd(context: HookContext) {
  const { session_id } = context;

  await fetch('http://localhost:37777/api/sessions/complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      claudeSessionId: session_id
    })
  });
}

VSCode Extension Integration

Language Model Tool Registration

import * as vscode from 'vscode';

const searchTool = vscode.lm.registerTool('claude-mem-search', {
  description: 'Search persistent memory for observations, sessions, and prompts',
  inputSchema: {
    type: 'object',
    properties: {
      query: { type: 'string', description: 'Search query text' },
      type: {
        type: 'string',
        enum: ['observations', 'sessions', 'prompts'],
        description: 'Type of results to return'
      },
      limit: { type: 'number', description: 'Maximum number of results', default: 10 }
    },
    required: ['query']
  },
  invoke: async (options, token) => {
    const { query, type, limit = 10 } = options.input;

    try {
      const response = await fetch(
        `http://localhost:37777/api/search?query=${encodeURIComponent(query)}&format=index&limit=${limit}`
      );

      if (!response.ok) {
        throw new Error(`Search failed: ${response.statusText}`);
      }

      const results = await response.json();

      return new vscode.LanguageModelToolResult([
        new vscode.LanguageModelTextPart(JSON.stringify(results, null, 2))
      ]);
    } catch (error) {
      return new vscode.LanguageModelToolResult([
        new vscode.LanguageModelTextPart(`Error: ${error.message}`)
      ]);
    }
  }
});

Chat Participant Implementation

const participant = vscode.chat.createChatParticipant('claude-mem', async (request, context, stream, token) => {
  const claudeSessionId = context.session.id;

  stream.markdown(`Searching memory for: ${request.prompt}\n\n`);

  const response = await fetch(
    `http://localhost:37777/api/search?query=${encodeURIComponent(request.prompt)}&format=index&limit=5`
  );

  const results = await response.json();

  if (results.observations?.length > 0) {
    stream.markdown('**Found observations:**\n');
    for (const obs of results.observations) {
      stream.markdown(`- ${obs.title} (${obs.project})\n`);
    }
  }

  return { metadata: { command: 'search' } };
});

package.json (VSCode Extension)

{
  "name": "claude-mem-vscode",
  "displayName": "Claude-Mem",
  "version": "1.0.0",
  "engines": { "vscode": "^1.95.0" },
  "activationEvents": ["onStartupFinished"],
  "main": "./dist/extension.js",
  "contributes": {
    "chatParticipants": [
      {
        "id": "claude-mem",
        "name": "memory",
        "description": "Search your persistent memory"
      }
    ],
    "languageModelTools": [
      {
        "name": "claude-mem-search",
        "displayName": "Search Memory",
        "description": "Search persistent memory for observations, sessions, and prompts"
      }
    ]
  },
  "scripts": {
    "build": "node build.js",
    "watch": "node build.js --watch",
    "package": "vsce package"
  },
  "devDependencies": {
    "@types/vscode": "^1.95.0",
    "esbuild": "^0.19.0",
    "typescript": "^5.3.0"
  }
}

Error Handling & Resilience

Connection Failures

async function callWorkerWithFallback<T>(
  endpoint: string,
  options?: RequestInit
): Promise<T | null> {
  try {
    const response = await fetch(`http://localhost:37777${endpoint}`, {
      ...options,
      signal: AbortSignal.timeout(5000) // 5s timeout
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return await response.json();
  } catch (error) {
    console.error(`Worker unavailable (${endpoint}):`, error);
    return null; // Graceful degradation
  }
}

Retry Logic with Exponential Backoff

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 100
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;

      const delay = baseDelay * Math.pow(2, attempt);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  throw new Error('Max retries exceeded');
}

Worker Health Check

async function isWorkerHealthy(): Promise<boolean> {
  try {
    const response = await fetch('http://localhost:37777/api/health', {
      signal: AbortSignal.timeout(2000)
    });
    return response.ok;
  } catch {
    return false;
  }
}

Privacy Tag Handling

The worker automatically strips privacy tags before storage:
  • <private>content</private> — User-level privacy control
  • <claude-mem-context>content</claude-mem-context> — System-level tag (prevents recursive storage)
Privacy Check: Observations/summaries are skipped if the entire user prompt was wrapped in <private> tags.

Custom Error Classes

class WorkerUnavailableError extends Error {
  constructor() {
    super('Claude-mem worker is not running or unreachable');
    this.name = 'WorkerUnavailableError';
  }
}

class WorkerTimeoutError extends Error {
  constructor(endpoint: string) {
    super(`Worker request timed out: ${endpoint}`);
    this.name = 'WorkerTimeoutError';
  }
}

SSE Stream Error Handling

function connectToSSE(onEvent: (event: any) => void) {
  const eventSource = new EventSource('http://localhost:37777/stream');

  eventSource.onmessage = (event) => {
    try {
      const data = JSON.parse(event.data);
      onEvent(data);
    } catch (error) {
      console.error('SSE parse error:', error);
    }
  };

  eventSource.onerror = (error) => {
    console.error('SSE connection error:', error);
    eventSource.close();

    // Reconnect after 5 seconds
    setTimeout(() => connectToSSE(onEvent), 5000);
  };

  return eventSource;
}

Development Workflow

Local Testing Loop

1

Terminal 1: Watch build

npm run watch
2

Terminal 2: Check worker status

npm run worker:status
npm run worker:logs
3

Terminal 3: Test API manually

curl http://localhost:37777/api/health
curl "http://localhost:37777/api/search?query=test&limit=5"
4

VSCode: Launch extension host

Press F5 to launch the extension host.

Complete WorkerClient Implementation

export class WorkerClient {
  private baseUrl: string;

  constructor(port: number = 37777) {
    this.baseUrl = `http://localhost:${port}`;
  }

  async isHealthy(): Promise<boolean> {
    try {
      const response = await fetch(`${this.baseUrl}/api/health`, {
        signal: AbortSignal.timeout(2000)
      });
      return response.ok;
    } catch {
      return false;
    }
  }

  async queueObservation(data: {
    claudeSessionId: string;
    tool_name: string;
    tool_input: any;
    tool_response: any;
    cwd?: string;
  }): Promise<{ status: string; reason?: string }> {
    const response = await fetch(`${this.baseUrl}/api/sessions/observations`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
      signal: AbortSignal.timeout(5000)
    });

    if (!response.ok) {
      throw new Error(`Failed to queue observation: ${response.statusText}`);
    }

    return await response.json();
  }

  async queueSummarize(data: {
    claudeSessionId: string;
    last_user_message?: string;
    last_assistant_message?: string;
  }): Promise<{ status: string; reason?: string }> {
    const response = await fetch(`${this.baseUrl}/api/sessions/summarize`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
      signal: AbortSignal.timeout(5000)
    });

    if (!response.ok) {
      throw new Error(`Failed to queue summary: ${response.statusText}`);
    }

    return await response.json();
  }

  async completeSession(claudeSessionId: string): Promise<void> {
    const response = await fetch(`${this.baseUrl}/api/sessions/complete`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ claudeSessionId }),
      signal: AbortSignal.timeout(5000)
    });

    if (!response.ok) {
      throw new Error(`Failed to complete session: ${response.statusText}`);
    }
  }

  async search(params: {
    query?: string;
    type?: 'observations' | 'sessions' | 'prompts';
    format?: 'index' | 'full';
    limit?: number;
    project?: string;
  }): Promise<any> {
    const queryString = new URLSearchParams(
      Object.entries(params)
        .filter(([_, v]) => v !== undefined)
        .map(([k, v]) => [k, String(v)])
    ).toString();

    const response = await fetch(
      `${this.baseUrl}/api/search?${queryString}`,
      { signal: AbortSignal.timeout(10000) }
    );

    if (!response.ok) {
      throw new Error(`Search failed: ${response.statusText}`);
    }

    return await response.json();
  }

  connectSSE(onEvent: (event: any) => void): EventSource {
    const eventSource = new EventSource(`${this.baseUrl}/stream`);

    eventSource.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        onEvent(data);
      } catch (error) {
        console.error('SSE parse error:', error);
      }
    };

    eventSource.onerror = (error) => {
      console.error('SSE connection error:', error);
    };

    return eventSource;
  }
}

Testing Strategy

Manual Testing Checklist

  • Worker starts successfully (npm run worker:status)
  • Health endpoint responds (curl http://localhost:37777/api/health)
  • SSE stream connects (curl http://localhost:37777/stream)
  • Queue observation creates session
  • Observation appears in database
  • Privacy tags are stripped
  • Private prompts are skipped
  • Queue summary creates summary
  • Complete session stops processing
  • Search observations by query
  • Search sessions by query
  • Search prompts by query
  • Get recent context for project
  • Get timeline around observation
  • Semantic shortcuts (decisions, changes, how-it-works)
  • SSE broadcasts processing status
  • SSE broadcasts new observations
  • SSE broadcasts new summaries
  • SSE broadcasts new prompts
  • Graceful degradation when worker unavailable
  • Timeout handling for slow requests
  • Retry logic for transient failures

Critical Implementation Notes

sessionDbId vs claudeSessionId

Use claudeSessionId (string) for new API endpoints, not sessionDbId (number).
  • sessionDbId — Numeric database ID (legacy endpoints only)
  • claudeSessionId — String identifier from the Claude platform (new endpoints)

JSON String Fields

Fields like facts, concepts, and files_touched are stored as JSON strings and require parsing:
const observation = await client.getObservationById(123);
const facts = JSON.parse(observation.facts);    // string[]
const concepts = JSON.parse(observation.concepts); // string[]

Timestamps

All created_at_epoch fields are in milliseconds, not seconds:
const date = new Date(observation.created_at_epoch);        // ✅ Correct
const date = new Date(observation.created_at_epoch * 1000); // ❌ Wrong

Asynchronous Processing

Workers process observations and summaries asynchronously. Results appear in the database 1–2 seconds after queuing. Use SSE events for real-time notifications.

Privacy Tags

Always wrap sensitive content in <private> tags to prevent storage:
const userMessage = '<private>API key: sk-1234567890</private>';
// This observation will be skipped (entire prompt is private)

Additional Resources

Documentation

Complete claude-mem documentation

GitHub

Source code and issue tracker

Worker Service

Worker architecture details

Database Schema

Database structure and queries

Build docs developers (and LLMs) love