Skip to main content
Glama
gebeer

conversation-search

by gebeer
README.md
# conversation-search

FTS5 full-text search over Claude Code conversation history. Indexes JSONL transcripts from `~/.claude/projects/` into a persistent SQLite FTS5 database and exposes them as searchable memory. Available as both an MCP server and a CLI tool.

Based on [Searchable Agent Memory in a Single File](https://eric-tramel.github.io/blog/2026-02-07-searchable-agent-memory/) by Eric Tramel.

## Development process

The initial implementation was produced by Claude Code following a structured pipeline:

1. **PRD** (`ai-docs/features/001-conversation-search-mcp.md`) — requirements and design decisions
2. **Build spec** (`specs/conversation-search-mcp.md`) — generated from the PRD, containing exact function signatures, filtering rules, acceptance criteria, and validation commands
3. **Implementation** (`conversation_search.py`) — written by Claude Code using the build spec as instructions

The search backend was later migrated from in-memory bm25s to SQLite FTS5 (`specs/fts5-migration.md`), peer-reviewed by Claude (Opus 4.6) and Codex (GPT-5.4), and executed as 5 sequential subagent tasks.

## How it works

Claude Code stores conversation transcripts as JSONL files under `~/.claude/projects/<encoded-dir>/`. This server:

1. Discovers matching project directories via glob pattern
2. Parses JSONL into turns (user message + assistant response + tool calls)
3. Builds an SQLite FTS5 index stored at `~/.cache/conversation-search/index.db`
4. On warm start, only reparses files whose mtime/size has changed (sub-second startup)
5. Watches the filesystem for changes and reindexes (60s debounce)
6. Serves 4 MCP tools via stdio (`serve`) or SSE (`daemon`)

## Requirements

- [`uv`](https://docs.astral.sh/uv/) (Python >= 3.10 is resolved automatically)

No venv or manual install needed. `uv run` handles `mcp`, `uvicorn`, and `watchdog` automatically. No `bm25s` or other search library is required — SQLite FTS5 is part of the Python standard library.

## Installation

Add to your MCP configuration — either project-level (`.mcp.json`) or global (`~/.claude.json` under the `mcpServers` key):

```json
{
  "mcpServers": {
    "conversation-search": {
      "command": "uvx",
      "args": [
        "--from", "git+https://github.com/gebeer/conversation-search.git",
        "conversation-search", "connect"
      ]
    }
  }
}
```

`connect` starts a shared daemon on first use and reuses it across sessions (see [Daemon Mode](#daemon-mode-recommended-for-multiple-sessions)). For standalone stdio mode (one index per session), replace `connect` with `serve`.

The `--pattern` flag controls which project directories under `~/.claude/projects/` are indexed. It defaults to `*` (all projects) if omitted. Patterns containing `/` are treated as filesystem paths and auto-converted to the encoded directory name format.

| Pattern | Scope |
|---------|-------|
| `*` | All projects |
| `~/repos/openclaw` | Single project |
| `~/repos/*` | All repos projects |
| `~/repos/open*` | Projects starting with "open" |
| `--pattern="-home-claude-repos-*"` | Encoded format (requires `=` syntax for leading `-`) |

Restart Claude Code after changing MCP configuration.

## CLI Usage

The tool can also be used directly from the command line for scripting and debugging:

```bash
uvx --from git+https://github.com/gebeer/conversation-search.git \
  conversation-search search --query "heartbeat" --limit 5

conversation-search list --project "claude" --limit 10

conversation-search read-turn --session-id "<uuid>" --turn 5

conversation-search read-conv --session-id "<uuid>" --offset 0 --limit 10
```

After the first `uvx` invocation, the `conversation-search` command is cached and can be called directly. Alternatively, use `uv run conversation_search.py <command>` from a local clone.

All CLI commands output pretty-printed JSON to stdout. Index progress is printed to stderr. Use `2>/dev/null` to suppress progress output when piping.

## Daemon Mode (Recommended for Multiple Sessions)

When running multiple Claude Code sessions simultaneously, use daemon mode to share a single
SQLite FTS5 index instead of opening separate DB connections per session.

### Setup

The default [installation](#installation) config already uses `connect`, which enables daemon mode automatically. On first session start, `connect` launches the daemon in the background. Subsequent sessions reuse it. The daemon exits after 15 minutes of inactivity.

### Manual daemon control

```bash
# Start daemon in foreground (useful for debugging)
conversation-search daemon

# Custom port and idle timeout
conversation-search daemon --port 9300 --idle-timeout 1800

# Stop daemon
kill $(cat ~/.cache/conversation-search/daemon.pid)
```

### Configuration

| Flag | Default | Description |
|------|---------|-------------|
| `--port` | 9237 | Localhost port for the SSE server |
| `--idle-timeout` | 900 | Seconds of inactivity before daemon exits |

Both flags work on `daemon` and `connect` subcommands.

### How it works

```
Claude Code session A ──┐
Claude Code session B ──┼── connect (stdio↔SSE bridge) ──► daemon (SSE on localhost:9237)
Claude Code session C ──┘                                       │
                                                          • one FTS5 index (~10 MB)
                                                          • one filesystem watcher
                                                          • one reindex loop
```

**Without daemon:** N sessions each open the same SQLite DB (WAL mode handles concurrent reads).
**With daemon:** Single writer/watcher; all sessions share one connection via SSE.

## Tools

### `search_conversations`

FTS5 full-text search across all indexed turns. All terms are implicitly ANDed.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | `str` | required | FTS5 search query. All terms must match (implicit AND). |
| `limit` | `int` | `10` | Max results |
| `session_id` | `str \| None` | `None` | Filter to one session |
| `project` | `str \| None` | `None` | Substring filter on project name |

Returns ranked results with `session_id`, `turn_number`, `score`, `snippet` (context window with `[[match]]` markers), `timestamp`.

#### Query syntax

| Syntax | Example | Meaning |
|--------|---------|---------|
| Keywords | `heartbeat timer` | Both must match (implicit AND) |
| Phrase | `"systemd timer"` | Exact phrase |
| Boolean | `heartbeat AND NOT clawd` | Boolean operators |
| Prefix | `buffer*` | Prefix matching |
| OR | `heartbeat OR cron` | Either term |
| Grouping | `(timer OR cron) AND heartbeat` | Grouped boolean |
| Literal | `literal:foo.bar()` | Code-like query, skips FTS5 syntax parsing |

### `list_conversations`

Browse indexed sessions with metadata.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `project` | `str \| None` | `None` | Substring filter on project name |
| `limit` | `int` | `50` | Max results |

Returns sessions sorted by `last_timestamp` desc, with `summary`, `turn_count`, `cwd`, `git_branch`.

### `read_turn`

Full-fidelity retrieval of a single turn. Re-parses the source JSONL (not the index).

| Parameter | Type | Description |
|-----------|------|-------------|
| `session_id` | `str` | Session UUID |
| `turn_number` | `int` | Zero-based turn index |

Returns complete `user_text`, `assistant_text`, and `tools_used` with rendered tool details.

### `read_conversation`

Paginated reading of consecutive turns from a session.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `session_id` | `str` | required | Session UUID |
| `offset` | `int` | `0` | Starting turn |
| `limit` | `int` | `10` | Number of turns |

## Usage pattern

The 4 tools are automatically exposed to the assistant via MCP — no extra instructions in `CLAUDE.md`, `AGENTS.md`, or similar files are needed. The server also provides `instructions` metadata through the MCP protocol to guide the assistant on effective usage.

Search wide, then read deep:

```
search_conversations("ProcessWire login redirect")  ->  find relevant turns
read_turn(session_id, turn_number)                   ->  get full context
read_conversation(session_id, offset, limit)         ->  read surrounding turns
```

FTS5 requires all query terms to match (implicit AND). Use specific keywords for best results. For either-or matching, use explicit `OR`. For code-like queries with special characters, use the `literal:` prefix.