peachmem (0.2.1)

Published 2026-08-11 14:50:49 +00:00 by kerryhatcher in kerryhatcher/peachmem

Installation

[registries.forgejo]
index = "sparse+" # Sparse index
# index = "" # Git

[net]
git-fetch-with-cli = true
cargo add peachmem@0.2.1 --registry forgejo

About this package

A markdown-as-memory MCP server: notes stored in Postgres, full-text searchable, linked as a graph via [[wikilinks]].

peachmem

a mem system for ai agents

peachmem is an MCP server that gives an agent durable memory: notes are written as markdown, stored in Postgres, full-text searchable, and linked into a graph via [[wikilink]] syntax in the body.

How it works

  • Storage: one row per note (id, slug, title, body_md, tags) in Postgres. slug is derived from the title once, at creation, and never changes — so links keep resolving even after a title is edited.

  • Full-text search: a generated tsvector column (title weighted above body) with a GIN index, ranked with ts_rank_cd and highlighted with ts_headline. If a query matches nothing, search falls back to trigram similarity on the title (via pg_trgm) so typos and near-misses still surface something.

  • Graph linking: write [[Note Title]] or [[Note Title|display text]] anywhere in a note's body. On save, peachmem extracts those references into a links table (source → target slug), resolving target_id when a matching note exists. Links to notes that don't exist yet are kept as "unresolved" and resolve automatically once that note is created — Obsidian-style. If a target note is later deleted, its incoming links revert to unresolved rather than disappearing.

  • Projects: notes are grouped by project, a durable identity that is deliberately decoupled from any single checkout. A project is reachable through any number of aliases:

    • git remotes — several remotes can point at one project, so a frontend and backend repo can share notes.
    • hostname + local path pairs — worktrees, or the same repo cloned more than once, on any number of machines. A NULL hostname means "this path on any machine".

    Because resolution is remote-first, two workstations that clone the same repo to different paths converge on the same project automatically, and a git worktree resolves to its main checkout's project. Each new location is recorded as another alias as it's seen. A non-git directory still gets a project, keyed by its path.

    On creation a note attaches to the project peachmem is running in (set once, like the slug). Notes can also belong to several projects, or to none — attach_workspace: false makes a global note, for things that aren't about any one codebase. list_notes/search_notes filter by project using any alias; list_notes also filters by path.

    Auto-detection can't infer everything, so link_project is the manual escape hatch: rename/describe a project, register extra remotes or paths as aliases, or merge two projects that turned out to be the same thing (notes, aliases and paths all move over). list_projects shows every project with its full alias set and note count.

  • Session summaries: a SessionStart hook asynchronously summarizes the previous Claude Code session in a project into a note, so it never delays session start. It only fires on startup (not resume, clear, compact, or fork — those continue an existing session), finds the previous session's transcript in ~/.claude/projects/<cwd-slug>/, and reduces it to a digest of user prompts, assistant prose, and which tools ran — transcripts are megabytes, mostly tool output, so the raw file is never sent to a model. That digest goes to Claude Haiku, which returns three things: a short markdown worklog, a work classification (feature, bug, docs, research, refactor, test, ops, or chore), and a one-sentence high-level description. The result is saved as a note tagged work_log, plus the classification as a second tag, plus session:<id> as an idempotency key so a session is never summarized twice. The note body leads with the description as a > blockquote, then the worklog, then a metadata footer. The one-sentence description is also injected back into the new session as context — arriving on the next turn, since the hook is async — so each session opens knowing what the last one did. Find these later with list_notes(tag: "work_log") or search_notes.

Tools

Tool Description
save_note Create a note, or update one in place by passing id. Optionally set tags, paths, projects, attach_workspace.
get_note Fetch a note by id or slug, with its forward links, backlinks, projects, and paths.
search_notes Ranked full-text search with snippets, fuzzy fallback; filter by tag or project.
list_notes Browse notes, most recently updated first; filter by tag, project, and/or path.
delete_note Delete a note by id or slug.
list_projects List projects with their aliases (remotes, hostname/path pairs) and note counts.
link_project Rename/describe a project, add remote or path aliases, and/or merge projects together.

Configuration

All Postgres connection info — host, port, dbname, user, password — lives in ~/.peachmem/config.toml, created automatically with defaults the first time peachmem runs, if it doesn't already exist. The defaults match docker-compose.yml's static credentials, so a fresh checkout works end to end with zero setup: no secrets, no env vars, nothing beyond the compiled binary itself. This is a local-only dev database (bound to localhost on a nonstandard port), so static well-known credentials are an intentional tradeoff for zero-config, cross-machine simplicity.

DATABASE_URL, if set, bypasses config.toml entirely (handy for CI or a throwaway test database).

RUST_LOG controls log verbosity (unset → info); logs go to stderr since stdout is reserved for the MCP protocol stream.

Migrations run automatically on startup.

The same file also has an optional [worklog] table configuring the session-summary hook described above: which model summarizes, how much of the transcript digest it's allowed to read, and time/cost ceilings. It's entirely optional — an existing config.toml with only a [postgres] table keeps working unchanged and gets these defaults:

[worklog]
enabled = true
model = "claude-haiku-4-5"
max_digest_chars = 40000
timeout_secs = 180
max_cost_usd = 0.20

Set enabled = false to turn the feature off without touching the plugin itself.

Cost scales with max_digest_chars, and the digest is billed roughly twice because the structured answer costs a second tool-call turn. Measured with Haiku: about $0.015 of fixed overhead plus $0.001 per 1,000 digest characters, so the 40,000-char default lands near $0.055 per session start. Raising the limit buys detail on long sessions at proportionally more cost — a 3.7 MB transcript digests to about 84,000 characters and costs about $0.10 to summarize in full.

max_cost_usd is a safety net rather than a target, which is why it sits well above the expected cost: exceeding it makes the CLI exit non-zero and the worklog is dropped, so a ceiling set close to normal cost would quietly disable the feature for exactly the long sessions most worth summarizing.

The summarizer shells out to the claude CLI, so it reuses your existing Claude Code authentication — there's no API key to configure. If claude isn't on PATH, the hook does nothing and the session is unaffected. The same is true of every other failure path: if Postgres is down, the model call fails, or anything else goes wrong, the hook exits quietly rather than surfacing an error, and the session proceeds normally either way.

Claude Code runs an installed plugin from a cached checkout under ~/.claude/plugins/cache/, not from this working tree, so a freshly added or changed hook won't take effect until the plugin is updated/reinstalled from its marketplace.

Running locally

just start     # starts postgres (docker compose, port 55433)
cargo run
Command What it does
just start Start postgres (creates the container on first run).
just stop Stop postgres without removing the container.
just restart Fully tear down and recreate the postgres container.

Point an MCP client at the built binary — nothing else required:

cargo build --release
./target/release/peachmem

Installing as a plugin

This repo is a self-contained plugin/marketplace for two ecosystems at once:

  • agent-plugins.org (vendor-neutral): plugin.json + mcp.json at the repo root.
  • Claude Code: .claude-plugin/plugin.json + .claude-plugin/marketplace.json — the marketplace lists this same repo (source: ".") as its one plugin.

Both point their MCP server's command directly at the peachmem binary, so it must already be installed and on PATH — see Installing as a crate below. (Postgres itself still needs to be reachable per Configuration above; the plugin doesn't manage that.)

# Install the binary first so it's on PATH
cargo install --registry kerryhatcher peachmem

# Claude Code
claude plugin marketplace add /path/to/peachmem
claude plugin install peachmem@peachmem

Installing as a crate

Published to a self-hosted Forgejo Cargo registry rather than crates.io. Consumers add the registry to ~/.cargo/config.toml:

[registries.kerryhatcher]
index = "sparse+https://code.kerryhatcher.com/api/packages/kerryhatcher/cargo/"
cargo install --registry kerryhatcher peachmem

The registry is readable anonymously (auth-required: false), so installing needs no token.

Publishing a release

.cargo/config.toml in this repo already defines the registry, and publish = ["kerryhatcher"] in Cargo.toml means a bare cargo publish fails rather than reaching for crates.io. Only the token is per-machine — generate one in Forgejo with the write:packages scope and add it to ~/.cargo/credentials.toml, keeping Forgejo's literal Bearer prefix:

[registries.kerryhatcher]
token = "Bearer <token>"
cargo publish --registry kerryhatcher --dry-run   # packages and verify-builds, no upload
cargo publish --registry kerryhatcher

Bump version in three places together — Cargo.toml, .claude-plugin/plugin.json, and plugin.json — since the crate and both plugin manifests are released as one thing; claude plugin validate . checks the manifests agree. A given name/version can't be re-uploaded without deleting the existing package first, so dry-run before publishing for real.

Development

cargo test      # unit tests (wikilink parsing, slugify, repo detection, worklog digest)
cargo clippy

Dependencies

ID Version
anyhow ^1
chrono ^0.4
dirs ^6
dotenvy ^0.15
once_cell ^1
regex ^1
rmcp ^3.1
schemars ^1
serde ^1
serde_json ^1
sqlx ^0.9
tokio ^1
toml ^1
tracing ^0.1
tracing-subscriber ^0.3
uuid ^1
whoami ^2

Keywords

mcp memory notes postgres knowledge-graph
Details
Cargo
2026-08-11 14:50:49 +00:00
2
MIT
56 KiB
Assets (1)
Versions (2) View all
0.3.0 2026-08-12
0.2.1 2026-08-11