Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 0.2.0 (unreleased)

A write path, through an inbox you approve.

- New MCP tool `puremind_propose(title, content)`. An agent can propose a memory; it is saved in `~/.puremind/inbox/` and does nothing else. It is not indexed or searchable, and the vault is not touched, until you approve it. At most 200 characters of title, 20,000 of content and 100 waiting proposals; an identical proposal is kept once.
- New command `puremind inbox` with `list` (the default), `show ID`, `approve ID [--path P] [--yes]` and `reject ID`. Approving shows the proposal, asks at a terminal (without one it needs `--yes`) and creates a new note at `agent-memories/<date>-<title>.md`, or at `--path`. It never overwrites a note or edits one, refuses `..`, hidden and absolute paths, and does not write through a symlink at any level.
- `puremind serve --read-only`, or `[inbox] enabled = false` in `config.toml`, leaves out `puremind_propose`, so the server has only its three read-only tools. `[inbox] folder` sets where approved notes go. `puremind init --force` keeps both settings.
- A Homebrew formula in `packaging/homebrew/puremind.rb`, for use from a tap of your own (no public tap yet).
- README: results by question type for the benchmark (Recall@5 and judged QA for each of the six LongMemEval types).
- The MCP server's instructions and `puremind connect` now say that the server cannot write to the vault but accepts proposals. The test that listed exactly three tools now lists four.

## 0.1.6 (2026-10-22)

- `puremind_get` and the indexer now open a note through a chain of directory descriptors that refuse a symlink at every component. Before, only the last component was protected, so a folder swapped for a symlink after the path check could make `puremind_get` serve a file from outside the vault.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ CI also runs pip-audit on the dependencies a new install resolves, and the suite

## Rules

- The MCP server stays read-only. The write path is planned for v0.2 and will arrive with its own design discussion.
- The MCP server never writes to the vault. An agent can only leave a proposal in the inbox (`puremind_propose`), and only `puremind inbox approve`, run by the user, creates a note. A change that gives an MCP tool any other way to write needs a design discussion first.
- Keep the retrieval pipeline (BM25 top 50, dense top 50, RRF k=60, rerank over 50 with an 800-character window) unchanged unless a change is backed by a benchmark run.
- A new third-party import in `src/` needs a dependency in `pyproject.toml`, with a lower bound you have actually run; a test checks this.
- The version lives only in `src/puremind_mac/__init__.py`. A release bumps it and adds the matching heading to `CHANGELOG.md` in the same commit; a test checks that too.
Expand Down
63 changes: 57 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ On Linux without an NVIDIA GPU, add `--pip-args='--extra-index-url https://downl

Two models are downloaded into the Hugging Face cache the first time they are needed, about 3.5 GB in total: `Qwen/Qwen3-Embedding-0.6B` (1.2 GB, fetched by the first `puremind index`) and `BAAI/bge-reranker-v2-m3` (2.3 GB, fetched by the first search). Both are Apache-2.0. On Apple Silicon they run on the GPU through MPS; on Linux they use CUDA if present, else the CPU.

Homebrew users: there is no tap yet, but a formula is kept in this repository (`packaging/homebrew/puremind.rb`). To use it, put it in a tap of your own:

```bash
brew tap-new "$USER/puremind"
cp packaging/homebrew/puremind.rb "$(brew --repository "$USER/puremind")/Formula/"
brew install "$USER/puremind/puremind"
```

It builds the latest released tag in a Python 3.13 environment, the same as `pipx` does, and needs an Apple Silicon Mac.

## Quickstart

```bash
Expand All @@ -44,18 +54,38 @@ Re-run `puremind index` after editing notes, or run `puremind watch` to re-index

The snippet uses the absolute path of the `puremind` executable and carries any `PUREMIND_*`, `HF_HOME`, `HF_HUB_CACHE`, `HF_HUB_OFFLINE`, `TRANSFORMERS_OFFLINE` and `HF_ENDPOINT` variables set in your shell, because GUI apps such as Cursor do not inherit your shell's environment.

The MCP server is read-only and offers exactly three tools, all marked read-only for the client:
The MCP server offers four tools. Three only read your vault and are marked read-only for the client; the fourth, `puremind_propose`, cannot touch your notes (see Inbox below).

| Tool | Purpose |
|---|---|
| `puremind_search(query, limit=10)` | Hybrid search. Returns up to 50 matching passages, best first, each with its note path, heading path and score. |
| `puremind_get(path)` | Reads one note by the path search returned. It serves only the notes the indexer reads (see Vault below) and cuts text beyond 200,000 characters, saying so. |
| `puremind_recent(days=7, limit=20)` | Lists the notes modified in the last `days` days, newest first, as of the last index run. |
| `puremind_propose(title, content)` | Proposes a memory. It is saved in an inbox outside the vault and does nothing else until you approve it. |

Each agent session starts its own server process. Tool calls may overlap: every search is answered from one consistent snapshot of the index, even while `puremind watch` updates it.

**Scores.** With the reranker, a score is the cross-encoder's probability from 0 to 1. Without it (`--no-rerank`, or `rerank = false`), it is a reciprocal rank fusion value, at most about 0.033. `puremind search --lexical-only` prints BM25 keyword scores. Compare scores only within one result list. There is no relevance cut-off, so a search always returns up to `limit` passages, and weak ones are still listed.

## Inbox: agents propose, you approve

An agent can suggest that something is worth remembering, but it cannot write to your notes. `puremind_propose` saves the suggestion as a small file in `~/.puremind/inbox/` (mode 0600, outside the vault). A proposal is not indexed, not searchable and not readable through `puremind_get` until you approve it.

```bash
puremind inbox # list the proposals waiting
puremind inbox show 20261022-a1b2 # read one (an id, or the start of one)
puremind inbox approve 20261022-a1b2
puremind inbox reject 20261022-a1b2
```

`approve` prints the whole proposal and the note it would create, and asks before writing. The note goes to `agent-memories/<date>-<title>.md` in your vault (change the folder with `[inbox] folder` in `config.toml`, or pass `--path projects/grinder.md` for one note) and starts with the proposal's title as a `#` heading. It only ever creates a file: it never overwrites or edits an existing note (a name that is taken gets `-2`, `-3`, and so on, and an explicit `--path` that exists is an error), it refuses `..`, hidden folders and absolute paths, and it will not write through a symlink at any level. Run `puremind index` afterwards, or let `puremind watch` do it.

Limits: a title of at most 200 characters, content of at most 20,000, and 100 proposals waiting at a time; an identical proposal is kept once.

- **This is a review step, not a sandbox.** Approval asks you at a terminal, and without one needs `--yes`. An agent that can run shell commands as you could pass `--yes` itself. If your agent has a shell, treat the inbox as a safeguard against mistakes and prompt injection through the MCP server, not as a boundary against the agent, and read what you approve.
- **Read before you approve.** A proposal is text an agent wrote, perhaps from a web page or a file it read. Once approved it is indexed, and any agent may later be shown it as a search result.
- **Turn it off.** `puremind serve --read-only` (add it to the arguments of the server entry), or `enabled = false` under `[inbox]` in `config.toml`, leaves the server with its three read-only tools.

## How it works

- **Vault.** The indexer reads regular `.md` and `.markdown` files of at most 2 MiB. It skips hidden files and folders (such as `.obsidian` and `.git`), `node_modules` and `__pycache__` (in any letter case), symlinks that resolve outside the vault or to a skipped file, pipes, names that are not valid UTF-8, and paths inside the vault longer than 1,024 characters. A note that is replaced by such a symlink during a run is not read through it. A note it cannot read keeps its previous index entry. Notes below a folder that cannot be listed are kept in the index, nothing is removed when the vault looks empty while the index is not, and the run says why.
Expand All @@ -74,7 +104,7 @@ Each `puremind serve` process loads the models on its first search, which takes
| Resident memory per server | about 3 to 3.5 GB, up to about 3.8 GB while reranking | about 1.5 GB |
| A search after the first | 10 to 20 seconds | about 0.3 seconds |

The reranker costs about 0.2 seconds per candidate on a CPU, so with the default 50 candidates it takes most of the search time. On a machine without a GPU, use `--no-rerank`, or lower `candidates`, if that is too slow. On Apple Silicon both models run on the GPU, which we have not measured yet. An idle server that has not searched yet uses under 100 MB. Older transformers 4.x releases load the models as 32-bit floats and need about twice the memory.
The reranker costs about 0.2 seconds per candidate on a CPU, so with the default 50 candidates it takes most of the search time. On a machine without a GPU, use `--no-rerank`, or lower `candidates`, if that is too slow. On Apple Silicon both models run on the GPU (MPS). We have only a first check, on an Apple M5 Pro with a 4-note vault, and it is not a benchmark: the first search, which loads both models, took about 5 seconds and the next ones 0.2 seconds. Memory use on a Mac has not been measured. An idle server that has not searched yet uses under 100 MB. Older transformers 4.x releases load the models as 32-bit floats and need about twice the memory.

## Configuration

Expand All @@ -93,6 +123,8 @@ The reranker costs about 0.2 seconds per candidate on a CPU, so with the default
| Candidates from each retriever | `[retrieval] candidates` | | `50` |
| Reciprocal rank fusion constant | `[retrieval] rrf_k` | | `60` |
| Rerank the candidates | `[retrieval] rerank` | | `true` |
| Offer `puremind_propose` | `[inbox] enabled` | | `true` |
| Folder for approved proposals | `[inbox] folder` | | `agent-memories` |
| Characters of each passage the reranker reads | `[retrieval] rerank_chars` | | `800` |
| Config directory | | `PUREMIND_HOME` | `~/.puremind` |

Expand All @@ -106,6 +138,8 @@ The reranker costs about 0.2 seconds per candidate on a CPU, so with the default

Once both models are in the Hugging Face cache, pureMind makes no network requests: models load from the cache at their pinned revisions, and Hugging Face telemetry is turned off unless you have set `HF_HUB_DISABLE_TELEMETRY` yourself. The only network use is the one-time download, which sends your Hugging Face token if you have one configured. Set `HF_HUB_OFFLINE=1` to forbid downloads altogether; a model that is not downloaded then fails at once with a clear message. A cached copy that lacks a file the models need (after an interrupted download, or one made by another tool) is completed on the next run with network access, and reported by name when offline.

Proposals waiting in `~/.puremind/inbox/` are a copy of the text an agent proposed, kept until you approve or reject them; a rejected or approved one is deleted from the inbox.

The index is a second copy of your notes' text. It is created readable only by you (mode 0600 in a 0700 directory), and an index, `~/.puremind` directory or `config.toml` that an earlier version left more open is tightened when pureMind next uses it. When a note is deleted or edited, the index run that notices it removes the old text from the index file and its `-wal` file; if another process is reading the index at that moment, the `-wal` file is emptied by a later run. The keyword index keeps the old words a little longer under `puremind watch`: it is compacted once the vault has been quiet for a minute, and at the latest five minutes after the change, rather than after every save. A watch that stops first compacts on its way out, and one that crashes leaves a note in the index so the next `puremind index` or `puremind watch` does it. `puremind index` compacts at the end of every run. Text deleted while you used 0.1.1 or earlier is removed when 0.1.2 first opens the index. Copies of the index in backups or snapshots still hold it; to remove everything, delete `~/.puremind/index.sqlite` and the `-wal` and `-shm` files beside it, then run `puremind index` again.

## Command line
Expand All @@ -116,15 +150,16 @@ The index is a second copy of your notes' text. It is created readable only by y
| `puremind index [vault_dir] [--rebuild]` | Indexes the vault incrementally; `--rebuild` embeds every note again, and is needed after changing the embedding model. |
| `puremind watch [vault_dir] [--debounce SECONDS]` | Indexes, then re-indexes when notes change. |
| `puremind search <query> [--limit N] [--no-rerank] [--lexical-only]` | Searches from the terminal; `--limit` is 1 to 50. |
| `puremind serve [--no-rerank]` | Runs the MCP server on stdio. Your agent starts this for you. |
| `puremind serve [--no-rerank] [--read-only]` | Runs the MCP server on stdio. Your agent starts this for you. `--read-only` leaves out `puremind_propose`. |
| `puremind inbox [list\|show ID\|approve ID [--path P] [--yes]\|reject ID]` | Reviews the memories agents have proposed. |
| `puremind connect <claude-code\|codex\|cursor>` | Prints the setup snippet for an agent. |

Exit status: 0 on success, 1 when a search finds nothing, 2 for an error such as a missing vault, a broken config file, a model that cannot load or an index file that cannot be written (reported on an `error:` line, sometimes followed by a hint), and 130 after Ctrl-C.

## What it is not

- **Not a cloud service.** There is no account, no sync and no telemetry.
- **No write path yet.** The MCP server cannot create or edit notes. You write the notes; agents read them.
- **No agent write access.** The MCP server cannot create or edit notes. Agents can only propose, and you approve (see Inbox). You write the notes; agents read them.
- **No app yet.** v0.1 is a command line tool and a stdio server. There is no menu-bar app, no installer and no auto-start.
- **Not a capture tool.** It does not record agent sessions. It searches the notes you already have.

Expand All @@ -140,6 +175,22 @@ We measured the retrieval design in this repository on LongMemEval_s (500 questi
- **Session Recall@5** is the share of a question's evidence sessions that appear in the top 5 results, averaged over the 470 questions that have evidence. The other 30 are abstention questions with nothing to find.
- **Judged QA accuracy** covers all 500 questions. A reader model (`claude-sonnet-5-5`) answered from the top 10 retrieved exchanges, and a judge model (`claude-opus-5-5`) graded each answer with the official LongMemEval per-type prompts. A second judge agreed on 99 of 100 sampled verdicts. The QA difference is +12.2 percentage points (95% confidence interval +9.0 to +15.6).

### Results by question type

LongMemEval_s groups its questions into six types. The same run of this design (BM25, Qwen3-Embedding-0.6B, bge-reranker-v2-m3), split by type. The totals are the figures in the table above.

| Question type | Recall@5 (questions with evidence) | Judged QA accuracy |
|---|---|---|
| knowledge-update | 1.000 (72) | 93.6% (73 of 78) |
| multi-session | 0.951 (121) | 81.2% (108 of 133) |
| single-session-assistant | 1.000 (56) | 98.2% (55 of 56) |
| single-session-preference | 0.967 (30) | 93.3% (28 of 30) |
| single-session-user | 1.000 (64) | 98.6% (69 of 70) |
| temporal-reasoning | 0.946 (127) | 89.5% (119 of 133) |
| **All** | **0.971 (470)** | **90.4% (452 of 500)** |

Multi-session questions, which need evidence from several past conversations, and temporal-reasoning questions are the weakest types. Each type has between 30 and 133 questions, so differences of a few points between types are within noise. Recall@5 counts only the questions that have evidence sessions; the 30 abstention questions are part of the QA column, spread over the types they belong to. These are aggregates of the same exploratory run as above, with the same limits.

Limits:

- **Not this package on a Mac.** The design ran in our benchmark harness, with the models served from an NVIDIA GPU (the embedder in bfloat16, reading whole exchanges). No Mac hardware was used. `bench/longmemeval.py` reproduces the retrieval half with this package's code; it does not run the reader or the judge.
Expand All @@ -155,12 +206,12 @@ To run the retrieval benchmark yourself, see `bench/README.md`.

## Roadmap

- **v0.2:** a write path through a human-approved inbox. Agents propose memories, you approve them, and approved memories become Markdown files in your vault.
- **v0.2 (done, unreleased):** a write path through a human-approved inbox, and a Homebrew formula in `packaging/homebrew/`.
- **v0.3:** a menu-bar app that runs the watcher and server, with first-run setup for the supported agents.

## Development

CI runs the test suite on macOS (Apple Silicon) and Linux with Python 3.11, 3.12 and 3.13, and on Linux with 3.14. The tests use stand-in models, and a weekly job runs a smoke test with the real models on Linux. The full models have not yet been run on a Mac. See `CONTRIBUTING.md` to run the tests, the linter and the other CI checks yourself.
CI runs the test suite on macOS (Apple Silicon) and Linux with Python 3.11, 3.12 and 3.13, and on Linux with 3.14. The tests use stand-in models, and a weekly job runs a smoke test with the real models on Linux. The real models have been run by hand on an Apple M5 Pro (index and search), but CI does not run them on a Mac. See `CONTRIBUTING.md` to run the tests, the linter and the other CI checks yourself.

## Licence

Expand Down
Loading
Loading