Skip to main content
Glama
FloLey
by FloLey
README.md
# Personal Memory Wiki

A persistent personal memory system for Claude, exposed as an
[MCP](https://modelcontextprotocol.io) server. It accumulates, organizes, and
surfaces a single user's knowledge over time. All data is plain **Markdown in a
git repo**: no database, no embeddings, no RAG. Git history is the audit log and
the undo button.

The idea: while you talk to Claude, it captures notes into a fast "short-term"
layer. A nightly **dream** then distils those captures into curated long-term
pages, the same way memory consolidates during sleep. You can read and edit
everything through a private web console.

It is a standalone Python service: a Docker image, an HTTP MCP endpoint, and a
named volume holding the wiki. The code is path-agnostic via the `WIKI_ROOT`
environment variable, so it runs the same locally or on any host behind a
TLS-terminating reverse proxy.

## MCP tools (what Claude calls)

A deliberately small surface (a small surface is easier for the model to use
correctly):

- `prime()` - call FIRST: loads the grounding context (the `self/` pages, then
  the long-term and short-term indexes) in one call.
- `read(path)` - read any file: a page, an index, or a short-term entry.
  Tolerant of paths and suggests alternatives on a miss.
- `search(query_text, max_results?)` - full-text search, returning
  `path:line: text` matches.
- `remember(content, summary?, tags?, due?, type?)` - capture into short-term
  memory; `due`/`type` file dated items (todo, reminder, event) into `temporal/`.
- `GET /health` - liveness probe for the Docker healthcheck (public, not a tool).

Writing to **long-term** memory is intentionally **not** an MCP tool. It happens
through the web console or the nightly dream, never from a live conversation.
This keeps structural edits deliberate. All tools honour the owner allow-list
and a path guard, so `long_term/private/` is never read or searched.

## How memory is organized

- **short-term** (`short_term/`): the open, fast-to-write layer. Every
  `remember` appends an entry here.
- **long-term** (`long_term/`): curated pages, including a `self/` section
  (identity, grounding) and a `private/` section never exposed over MCP.
- **temporal** (`temporal/`): dated, transient items (todo / reminder / event)
  that live until their due date, then are archived. A durable fact with no
  expiry belongs on a long-term page instead.
- **index**: each layer has a one-line-per-page index, regenerated by code so it
  cannot drift from the contents.

## The dream (nightly consolidation)

The dream reads short-term memory plus a policy file (`DREAM.md`, editable in the
console) and decides how to fold captures into long-term pages. It runs as a
**three-stage pipeline** so no single call ever needs the whole memory (cost
scales with the number of captures, not the wiki size):

1. **triage** - cluster and route each capture;
2. **decide** - per unit, reading only the touched pages, choose the action;
3. **write** - produce each page's final content and its index line.

It has two modes, set from the console (`/ui/dream`):

- **dry-run** - stops after stage 2 and writes a report to `dream_reports/`,
  changing nothing.
- **execute** - runs stage 3 and applies everything in **one revertible git
  commit** (writes pages, files temporal items, drops consumed short-term
  entries, expires past-due items). It never deletes long-term content.

A built-in **scheduler thread** can run the dream once a night (`off` /
`dry-run` / `execute`, stored in `dream_schedule.json`). "Once a day" is enforced
by the presence of the day's report, so restarts never double-run and a missed
night is caught up. The model is `WIKI_DREAM_MODEL` (defaults to Opus,
overridable per stage via `_TRIAGE` / `_DECIDE` / `_WRITE`) and needs
`ANTHROPIC_API_KEY`. The three stage prompts are editable files
(`prompts/{triage,decide,write}.md`) viewable at `/ui/prompts`; the JSON schema
is injected by code, so editing the guidance cannot break the contract.

## Web console (`/ui`)

A private, browser-facing console served by the same process:

- `/ui` - lists every markdown file under the wiki (the structure).
- `/ui/page/{path}` - view a page rendered from markdown.
- `/ui/edit?path=...` - edit a page (or create one when path is empty).
- `POST /ui/save`, `POST /ui/delete` - write or soft-delete, committed with a
  `manual:` prefix (soft-delete keeps history).
- `/ui/dream`, `/ui/prompts` - run/schedule the dream and edit its prompts.

Markdown is rendered with raw HTML disabled, so stored content cannot inject
markup. Forms carry a signed CSRF token.

## Authentication: GitHub OAuth + owner allow-list

The server uses FastMCP's `GitHubProvider` (an OAuth proxy running the OAuth 2.1
+ PKCE flow Claude.ai expects). On top of "any valid GitHub login", an allow-list
middleware restricts access to a single account (`WIKI_ALLOWED_GITHUB_LOGIN`), so
the wiki stays private to its owner. The browser console uses the same OAuth app.

### Configuration (environment variables)

| Variable | Required | Purpose |
|---|---|---|
| `GH_OAUTH_CLIENT_ID` / `GH_OAUTH_CLIENT_SECRET` | yes (prod) | GitHub OAuth; the server refuses to start without them unless auth is disabled |
| `WIKI_PUBLIC_URL` | prod | the public base URL of the server, e.g. `https://YOUR_DOMAIN` |
| `WIKI_ALLOWED_GITHUB_LOGIN` | yes (prod) | the single GitHub login allowed to use the wiki |
| `WIKI_JWT_SIGNING_KEY` | recommended | stable random value so tokens survive restarts |
| `ANTHROPIC_API_KEY` | for the dream | the model calls in the consolidation pipeline |
| `WIKI_DREAM_MODEL` (+ `_TRIAGE`/`_DECIDE`/`_WRITE`) | no | dream model overrides |
| `WIKI_ROOT` | no | where the wiki lives (default `/srv/wiki`) |
| `WIKI_AUTH_DISABLED=1` | dev only | run open, no secrets needed |

Provide these through your deployment's environment (a `.env` file, container
secrets, an orchestrator, etc.). They are configuration, not committed to the
repo.

### GitHub OAuth app setup (one time)

Create a GitHub OAuth App (Settings -> Developer settings -> OAuth Apps) with:

- Homepage URL: `https://YOUR_DOMAIN`
- Authorization callback URL: `https://YOUR_DOMAIN/` (GitHub accepts any subpath,
  so both `/auth/callback` for Claude and `/ui/auth/callback` for the console
  work). Secret names must not start with `GITHUB_`, which GitHub reserves.

## Architecture

- **Transport:** Streamable HTTP, MCP mounted at `/mcp`, listening on `:8765`.
- **Data:** the wiki lives under `WIKI_ROOT` (default `/srv/wiki`), intended to
  be a persistent volume. The entrypoint seeds it from `seed/` only if empty,
  then `git init`s it, so restarts never clobber data.
- **Public URL:** `https://YOUR_DOMAIN/mcp`, behind a reverse proxy that
  terminates TLS (Caddy, nginx, Traefik, etc.).

```
memory-wiki/
  Dockerfile
  docker-entrypoint.sh    # seed-if-empty + git init, then run the server
  pyproject.toml
  src/wiki_server/
    server.py             # FastMCP app: tools + /health
    store.py, query.py    # read / search / write over the markdown tree
    paths.py              # path validation under WIKI_ROOT, refuses private/
    temporal.py           # dated items
    dream/                # the staged consolidation pipeline + scheduler
    ui.py, prompts.py     # web console and editable dream prompts
  seed/                   # initial wiki content, copied into the volume once
  tests/
```

## Run locally

The fast way, with auth disabled (no OAuth needed; the dream needs an Anthropic
key, but the read/write tools do not):

```sh
docker build -t memory-wiki .
docker run --rm -p 8765:8765 \
  -e WIKI_AUTH_DISABLED=1 \
  -v "$PWD/.localwiki:/srv/wiki" \
  memory-wiki
```

Then:

```sh
curl -s localhost:8765/health        # -> {"ok": true}

npx @modelcontextprotocol/inspector
#   connect to: http://localhost:8765/mcp
#   call prime(), then read("self/identity.md")
```

The local volume is seeded from `seed/` on first run, so you get a working wiki
to poke at.

## Tests

The logic (store, temporal items, the dream pipeline, path guard, prime) is
covered by a fast pytest suite that runs against a throwaway wiki, with git
commits and model calls stubbed. No API key or network needed. Runs in CI on
every push.

```sh
pip install pytest        # or: pip install -e ".[dev]"
pytest -q
```

## Deployment

The image is built from the included `Dockerfile`. A GitHub Actions workflow
(`.github/workflows/ci.yml`) runs the tests and, on `main`, builds and pushes
the image to GHCR. From there, deploy the container however you like: any Docker
host works, as long as the MCP endpoint sits behind a reverse proxy that
terminates HTTPS at your public URL and the env vars above are supplied.

Two things to remember in production:

- mount a **persistent volume** at `WIKI_ROOT` so the wiki survives restarts;
- point a DNS record for your domain at the host, and let the reverse proxy
  handle the certificate.

## Connect Claude.ai

1. In Claude.ai: Settings -> Connectors -> Add custom connector.
2. URL: `https://YOUR_DOMAIN/mcp`.
3. Claude redirects you to GitHub to log in and consent. Only the allow-listed
   GitHub account can use the tools.
4. Ask Claude to call `prime()`, then `read("self/identity.md")`.

## Roadmap

Built so far: OAuth, `remember()` plus the lean read/search tools, temporal
items, the web console, and the staged dream (dry-run, execute, editable
prompts, per-stage model, automatic nightly schedule).

Still open: a **weekly digest** (a periodic summary of what changed and what is
coming up). Optional later: notifications, and a "reorg" dream to fold residual
duplicate pages.