Skip to main content
Glama
README.md
# Forge — local RAG over your meeting notes

Forge turns a folder of Markdown meeting notes into a searchable memory that an
AI assistant can query. Search runs **entirely on your machine**: the embedding
model is local, the vectors live in a SQLite file, and no part of the search path
makes a network call.

It exposes one MCP tool, `vault_search`, so from Claude Code (or any MCP client)
you can ask things like *"what did we decide about rate limits, and who owns the
follow-up?"* and get pointers to the exact sections of the exact notes.

```
You:    Use vault_search to find what we decided about API rate limits.
Claude: [searches] → 3 sections across 2 notes → reads them → answers with
        the decision, the owner, and the date.
```

Optionally, two LLM agents read your raw notes and distil them into durable
per-theme and per-customer summaries. Those are the only parts that call out to
a model provider, and they are opt-in.

---

## Contents

- [How it works](#how-it-works)
- [Requirements](#requirements)
- [Install](#install)
- [60-second demo](#60-second-demo)
- [Using it with your own notes](#using-it-with-your-own-notes)
- [The note format](#the-note-format)
- [Connect it to Claude Code](#connect-it-to-claude-code)
- [The `forge` CLI](#the-forge-cli)
- [The sync agents (optional)](#the-sync-agents-optional)
- [Import from Notion (optional)](#import-from-notion-optional)
- [Normalise mis-transcribed names (optional)](#normalise-mis-transcribed-names-optional)
- [Tests](#tests)
- [Troubleshooting](#troubleshooting)
- [Privacy](#privacy)
- [Design notes](#design-notes)

---

## How it works

```
your notes (.md)  →  index.py  →  .forge-index/index.sqlite  →  server.py  →  MCP client
                     chunk by ##     vectors + FTS5 keyword       hybrid          (Claude Code)
                     embed local     index + metadata             retrieval
```

1. **`index.py`** walks your vault, splits each note into one chunk per `##`
   section, embeds each chunk locally with `bge-small-en-v1.5`, and stores the
   vectors, a keyword (FTS5) index, and metadata in one SQLite file.
2. **`server.py`** answers `vault_search(query, top_k)` by running a semantic
   search and a keyword search in parallel and blending the scores. It returns
   *pointers* — file path, section heading, date, relevance, short preview — and
   the client reads the full files itself.
3. **`./forge`** is a small CLI that fronts everything, so you never have to
   remember individual script paths.

`ARCHITECTURE.md` has the full diagram and component map.

---

## Requirements

- **Python 3.11+**
- **[`uv`](https://github.com/astral-sh/uv)** (or use `python -m venv` and `pip`)
- ~150 MB disk for the embedding model, downloaded once on first index

### Your Python must support SQLite extensions

Forge uses [`sqlite-vec`](https://github.com/asg017/sqlite-vec), which needs
`sqlite3.Connection.enable_load_extension`. Several common Python builds omit it,
and you will get `AttributeError: 'sqlite3.Connection' object has no attribute
'enable_load_extension'`. Check first:

```bash
python3 -c "import sqlite3; sqlite3.connect(':memory:').enable_load_extension"
```

Silence means you are fine. If it raises:

- **macOS** — the system Python and most `pyenv` builds are compiled without it.
  Use Homebrew's:
  ```bash
  brew install python@3.13 sqlite
  uv venv --python /opt/homebrew/bin/python3.13 .venv
  ```
- **Linux** — install a distro Python built with `--enable-loadable-sqlite-extensions`
  (Debian/Ubuntu's `python3` is; a hand-built one may not be), or rebuild with
  `PYTHON_CONFIGURE_OPTS="--enable-loadable-sqlite-extensions" pyenv install 3.13`.

---

## Install

```bash
git clone https://github.com/blittlemore/forge-rag.git
cd forge-rag
uv venv .venv
uv pip install -r requirements.txt
```

You do **not** need to activate the venv — `./forge`, `index.py`, and `server.py`
each re-exec themselves under `.venv` automatically.

---

## 60-second demo

The repo ships a synthetic sample vault (six invented notes for a fictional
company) so you can see it work before writing anything of your own:

```bash
FORGE_VAULT_ROOT=examples ./forge run index
FORGE_VAULT_ROOT=examples ./forge run search "api rate limits"
```

The first index downloads the embedding model (~130 MB); after that everything is
offline. Other queries worth trying: `single sign-on`, `sprint capacity`,
`hybrid search`, `service account blocker`. See
[`examples/README.md`](examples/README.md) for what each note contains.

---

## Using it with your own notes

Two options.

**A: keep notes inside the repo** (simplest). Create `meetings/` and put your
`.md` files in it — it is git-ignored, so your notes are never committed:

```bash
mkdir -p meetings/2026/07
# add your notes, then:
./forge run index
./forge run search "whatever you discussed"
```

**B: point Forge at an existing vault** (e.g. an Obsidian folder), leaving your
notes where they are:

```bash
export FORGE_VAULT_ROOT=~/Documents/MyVault   # must contain a meetings/ dir
./forge run index
```

Forge indexes `meetings/`, plus `workstreams/` and `customers/` if the agents
have created them. The SQLite index always lives in `.forge-index/` inside the
repo, not in your vault.

---

## The note format

YAML frontmatter, then `##` sections:

```markdown
---
title: "Reporting Pipeline Weekly"
date: "6 Jul 2026"
meeting_type: "Team sync"
tags: [reporting, pipeline]
---

# Reporting Pipeline Weekly

## Summary

Weekly sync on the nightly reporting pipeline...

## Decisions

- Ship the export job behind a feature flag.

## Action items

- Jordan to request a service account. Due 8 Jul.
```

What matters:

- **One chunk per `##` section** — sections are the retrieval unit, so keep each
  one about one thing. Adjacent chunks overlap slightly so a fact spanning a
  boundary still appears intact somewhere.
- **`title` and `date` come from frontmatter.** `date` parses as `D MMM YYYY`.
  `meeting_type` and `tags` are optional. Only `title` and `date` are used for
  display.
- **Name files `YYYY-MM-DD HH.MM - Title.md`.** The agents read the date prefix
  to find "today's" notes; it takes precedence over the frontmatter `date`.
- **Text before the first `##` is not indexed** — the H1 is only a title
  fallback, so put real content under a heading.
- **`meetings/_transcripts/` is skipped**, so raw transcripts can sit next to
  the summaries without polluting results.

Notes written by [Minute](https://minute.app) already match this format. So does
anything the included Notion importer produces.

---

## Connect it to Claude Code

Register the MCP server once, from the repo root:

```bash
claude mcp add forge -- "$(pwd)/.venv/bin/python" "$(pwd)/server.py"
```

Then in any Claude Code session:

```text
Use vault_search to find meeting sections about onboarding blockers,
then read the top file and summarise the decisions.
```

`vault_search(query, top_k=5)` returns the most relevant chunks with file path,
section heading, date, and relevance score. It makes **no LLM calls** — the
client does the reading and reasoning.

It also **refreshes the index lazily on every query**: edit a note and search
immediately, and the change is picked up (detected by content hash) before
results are returned. You rarely need to run `index` manually.

---

## The `forge` CLI

```bash
./forge list                    # every capability
./forge info <name>             # usage, flags, whether it's safe unattended
./forge run <name> [args...]    # run it; extra args pass through
./forge interactive             # REPL with history and tab-completion
```

| Capability | Kind | What it does |
|---|---|---|
| `index` | core | Index the vault into the local RAG database |
| `search` | core | Search from the CLI, no MCP client needed |
| `test` | core | Run the pytest suite |
| `workstream-sync` | agent | Distil notes into per-theme workstream files |
| `customer-sync` | agent | Distil notes into per-customer profiles |
| `notion-import` | helper | Import a Notion database into the vault |
| `fix-names` | helper | Normalise mis-transcribed names |

Useful `index` flags:

- `--force` — re-index every file even if unchanged
- `--rebuild` — drop and recreate the DB (required after changing the embedding
  model; the indexer warns you when it detects a stale one)
- `--check` — exit `0` if the index is current, `1` with stale paths on stderr.
  No embedding, no writes — good for a pre-commit hook or CI.

Indexing is **incremental by content hash**, so a `git checkout` that reverts a
file is caught, and `touch` is a no-op. Deleted files are pruned.

Optionally put it on your `PATH`:

```bash
ln -s "$(pwd)/forge" /usr/local/bin/forge
```

Adding a capability means adding one entry to `REGISTRY` in `cli/registry.py` —
nothing else in the CLI changes.

---

## The sync agents (optional)

Everything above is free and offline. These two agents are the exception: they
send note content to a Claude model and **cost money per note**.

They read raw meeting notes and merge the durable facts into living summaries:

- **`workstream-sync`** → `workstreams/NN-theme.md`, one file per theme, so a
  project's current state is in one place instead of scattered across 30 notes.
- **`customer-sync`** → `customers/<slug>.md`, one profile per customer, with
  issues, requests, action items, and sentiment.

Both re-index afterwards, so their output is searchable through `vault_search`
like any other note.

### Setup

```bash
cp .env.example .env
# add ANTHROPIC_API_KEY=sk-ant-...
```

```bash
./forge run customer-sync --latest 1 --dry-run   # compute only, write nothing
./forge run customer-sync --latest 1             # for real
./forge run workstream-sync --all --limit 5      # today's notes, capped
```

**Start with `--dry-run`.** It runs the full pipeline and prints what would
change without writing.

### Providers

| `FORGE_PROVIDER` | Credential | Notes |
|---|---|---|
| `anthropic` (default) | `ANTHROPIC_API_KEY` | The Claude API directly |
| `bedrock` | `BEDROCK_API_KEY` | Claude on Amazon Bedrock, so note content stays inside your own AWS tenant |

Override models with `FORGE_WORKER_MODEL` / `FORGE_GUARD_MODEL` (they take
LiteLLM-prefixed ids, e.g. `anthropic/claude-sonnet-5`). Defaults are Sonnet 5
for the extraction and merge work, Haiku 4.5 for the cheap guardrail checks.

### Guardrails

Because these agents write files you will later trust, they are wrapped in
checks: a relevance check skips non-work notes before the expensive model runs, a
template check rejects output that breaks the file structure, and a grounding
check rejects claims that do not trace back to the source note or the previous
version of the file. Blocked output is not written, and the note stays unmarked
so a later run retries it.

---

## Import from Notion (optional)

```bash
cp helpers/notion_import/.env.example .env    # add NOTION_TOKEN + NOTION_DATABASE_ID
./forge run notion-import --dry-run           # preview
./forge run notion-import                     # 10 most recent
./forge run notion-import --all               # everything
./forge run index
```

Re-runs are idempotent (existing files are skipped). See
[`helpers/README.md`](helpers/README.md) for creating the integration and sharing
the database with it.

---

## Normalise mis-transcribed names (optional)

Transcription mangles names ("Jordyn" for "Jordan"). `fix-names` rewrites known
variants across the vault, and runs automatically as part of `index`.

It is **off until you configure it** — without `people.json` it does nothing, so
you can ignore it entirely:

```bash
cp helpers/fix_names/people.json.example helpers/fix_names/people.json
# {"Jordan": ["Jordan", "Jordyn", "Jordanne"]}
```

`people.json` is git-ignored, since it holds real names.

---

## Tests

```bash
./forge run test
```

63 offline tests covering the Markdown chunker, hybrid score blending,
incremental indexing, the lazy server refresh, the REPL, and the agent helpers.
No credentials needed and no network calls.

The LLM evals under `agents/*/evals/` are separate — they make real model calls,
so they are skipped unless a credential is set:

```bash
.venv/bin/python -m pytest agents/customer_sync/evals/ -v
```

---

## Troubleshooting

**`AttributeError: ... 'enable_load_extension'`** — your Python was built without
SQLite extension support. See [Requirements](#requirements).

**Search returns nothing** — check the index has content:

```bash
./forge run index --check
sqlite3 .forge-index/index.sqlite "select count(*) from chunks;"
```

Zero chunks usually means your notes are not under a `meetings/` directory inside
`FORGE_VAULT_ROOT`, or the content sits above the first `##` heading.

**First index is slow** — that is the one-time model download (~130 MB).
Subsequent runs are incremental and only re-embed changed files.

**MCP server won't start in Claude Code** — the registered command must use
absolute paths. Re-run the `claude mcp add` line from the repo root so `$(pwd)`
expands, then check with `claude mcp list`. Test the server standalone with
`./forge run search "test"`.

**Agent fails with a missing-key error** — it names the variable it wants.
`FORGE_PROVIDER` decides which: `ANTHROPIC_API_KEY` by default,
`BEDROCK_API_KEY` for Bedrock. It reads `.env` at the repo root.

**Stale-model warning after upgrading** — run `./forge run index -- --rebuild`.
Vectors from a different model are not comparable.

---

## Privacy

Forge is built to keep meeting content private.

- **Search is fully local.** Embedding, vector search, and keyword search all run
  on your machine. The only network call in the search path is the one-time model
  download.
- **Your notes are never committed.** `meetings/`, `workstreams/`, `customers/`,
  `owner.md`, `.env`, and `helpers/fix_names/people.json` are all git-ignored.
  Only the synthetic sample vault under `examples/` is tracked.
- **The agents are the exception.** They send note content to whichever provider
  you configure. Use `FORGE_PROVIDER=bedrock` to keep that traffic inside your own
  AWS tenant. If you never run the agents, nothing leaves your machine.
- **Agent tracing is off by default.** The OpenAI Agents SDK can upload traces,
  but only if you set `OPENAI_API_KEY`. Even then, note text and model responses
  are excluded from spans — metadata only.

---

## Design notes

- **Retrieval, not generation.** `server.py` returns pointers and previews; the
  client reads the files. This keeps the server cheap, deterministic, and free of
  any LLM dependency.
- **Hybrid retrieval.** Semantic search catches "same idea, different words";
  BM25 keyword search catches exact product names and error codes. Running both
  and blending (weighted toward semantic) covers both failure modes. Falls back
  to vector-only against an older index with no FTS5 table.
- **One SQLite file, no services.** Vectors in a `sqlite-vec` `vec0` virtual
  table, keywords in FTS5, metadata in a normal table. No vector database or
  search process to run.
- **Content-hash incrementality.** Re-indexing keys on the sha256 of file
  content, not mtime — so mtime-preserving tools and `git` operations are handled
  correctly in both directions.
- **Why not approximate nearest neighbours?** At ~3,000 chunks the vector search
  takes 2.9 ms and query embedding dominates. `docs/scaling-notes.md` has the
  measurements and the ~50,000-chunk threshold where this should be revisited.

---

## Licence

MIT — see [LICENSE](LICENSE).

Maintenance

ActivityStale
ResponsivenessNo issues