# plugmem-cli
> ⚠️ Experimental. plugmem is mostly an AI-built experiment — written with
> the help of a small local model (Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf) and various
> Claude models, in roughly equal measure. Expect non-professional design
> choices, rough edges, broken behavior, or mistakes. Use it at your own risk.
`plugmem-cli` is the command-line surface over the plugmem
[temporal-memory engine](https://docs.rs/plugmem-core/latest) — a thin shell around
[`plugmem-host`](https://docs.rs/plugmem-host/latest) that lets you (or an agent's
launcher) keep a memory in a single file from a terminal or a shell script. Each
one-shot command parses arguments, calls one engine verb, and prints the result
(a human report, `--json` for tooling); the interactive `plugmem repl` keeps the
engine open across commands for host speed. No memory logic lives here.
The installed binary is **`plugmem-cli`**.
## Install
Prebuilt for **Linux, Windows and macOS (x64 & arm64)** on every tagged release.
**Pick one method — you don't need more than one; they install the same
`plugmem-cli` binary.**
### Homebrew (macOS / Linux)
From the [`m62624/homebrew-plugmem`](https://github.com/m62624/homebrew-plugmem)
tap; `brew upgrade` / `brew uninstall` then manage it like any formula:
```console
$ brew install m62624/plugmem/plugmem-cli
```
### Installer script (no Rust toolchain)
`latest` always points at the newest tag on the
[Releases page](https://github.com/m62624/plugmem/releases):
```console
# Linux / macOS (POSIX sh)
```powershell
# Windows (PowerShell) — alternative to the .msi
> powershell -ExecutionPolicy Bypass -c "irm https://github.com/m62624/plugmem/releases/latest/download/plugmem-cli-installer.ps1 | iex"
```
### Windows `.msi`
Download `plugmem-cli-*.msi` from the
[Releases page](https://github.com/m62624/plugmem/releases). Double-click to
install; it registers in **"Add or remove programs"** for normal upgrades and
uninstalls.
### `cargo binstall`
[cargo-binstall](https://github.com/cargo-bins/cargo-binstall) downloads the
prebuilt binary instead of compiling — it just works on every OS/arch above:
```console
$ cargo binstall plugmem-cli
```
### From source
Needs a Rust toolchain. From crates.io:
```console
$ cargo install plugmem-cli
```
…or from a local checkout of this repo:
```console
$ cargo install --path crates/plugmem-cli
# or, to build without installing:
$ cargo build --release -p plugmem-cli # binary at target/release/plugmem-cli
```
### Uninstall
`cargo uninstall plugmem-cli` (for `cargo install`/`binstall`);
`brew uninstall plugmem-cli` (Homebrew); "Add or remove programs" (`.msi`). The
shell/PowerShell installers ship no uninstaller — remove `~/.cargo/bin/plugmem-cli`
and `~/.config/plugmem-cli` (Windows: `%USERPROFILE%\.cargo\bin\plugmem-cli.exe`
and `%LOCALAPPDATA%\plugmem-cli`) by hand. See the
[workspace README](https://github.com/m62624/plugmem#install) for the full matrix.
## Which crate do I need?
The CLI is the **human/scripting door**. A Rust program links the library
instead; an agent or another language comes in over a protocol.
| A memory from a **terminal or shell script** | **`plugmem-cli`** (this binary) | One file, no server; `plugmem repl` keeps the engine open for host speed. |
| **A memory in a Rust program** — the common case | [`plugmem-host`](https://docs.rs/plugmem-host/latest) (`std`) | The engine plus files, locking, mmap, HTTP embedders, integrity, concurrency. |
| The engine with **no `std`** or **your own storage** | [`plugmem-core`](https://docs.rs/plugmem-core/latest) (`no_std`) | Engine only; you bring persistence. |
| A memory for an **LLM agent** or a **non-Rust program** | `plugmem-mcp` | Long-lived stdio JSON-RPC; language-independent — the door for programmatic / cross-language access (the CLI is the human one). |
| A memory in **JavaScript / TypeScript** (Node) | `plugmem-napi` | The engine as a native Node addon (napi-rs), in-process; on npm as `plugmem`. |
## What recall does
Recall fuses four sources by reciprocal-rank fusion with a recency boost (tags
filter; they are not a source):
| **Lexical** | [BM25](https://en.wikipedia.org/wiki/Okapi_BM25) (Robertson idf) over a Unicode ([UAX #29](https://unicode.org/reports/tr29/)) tokenizer | exact terms / keyword overlap |
| **Semantic** | int8-quantized cosine — flat two-phase below a threshold, an [HNSW](https://arxiv.org/abs/1603.09320) graph above | meaning / nearest neighbours |
| **Graph** | entity graph with current typed edges on the hot path; `--as-of` walks edge history | relational knowledge |
| **Temporal** | range scans over a `recorded_at`-ordered index; bitemporal validity | "what was true *then*", time windows |
## Usage
```text
plugmem-cli [--db PATH] [--json] <command>
```
The database is chosen by, in order: `--db PATH`, the `PLUGMEM_DB`
environment variable, or the platform data path (created on first write). The
engine keeps no clock, so `now` comes from the system clock at each call.
| `remember <TEXT> [--entity E] [--tag T]… [--link REL:ENTITY]… [--meta KEY=VALUE]… [--valid-from TS]` | store a fact; prints its id and any similar/conflicting facts. `--meta` is repeatable (opaque key→value, e.g. a URI; last value wins per key) |
| `recall [QUERY] [--tag T]… [--entity E]… [--as-of TS] [--range FROM TO] [-k N] [--closed]` | ranked, token-budgeted block; sources compose. Each line is `- [fN] text …` — `N` is the fact's id (see below) |
| `revise <ID> <TEXT> [same flags as remember]` | close the old fact, record the successor |
| `forget <ID>` | tombstone a fact (purged physically at the next `maintain`) |
| `link <SRC> <REL> <DST>` | upsert a typed edge between entities |
| `unlink <SRC> <REL> <DST>` | close the current typed edge while preserving `--as-of` history |
| `show <ID>` | one fact's full card — text, both time axes, state |
| `stats` | engine size counters |
| `maintain [--mode M]` | policy-driven maintenance: cheap no-op, tombstone compaction, text reindex or bounded HNSW work. `M` is `auto` (default), `compact`, `reindex-text`, `optimize-vectors` or `full`; only `full` repacks the edge arenas, and no mode drops history |
| `checkpoint` | flush the journal into a fresh snapshot and clear it (leaves the database checkpointed) |
| `verify` | check integrity an open defers: text UTF-8, metadata, vector↔fact consistency, and that the edge graph agrees with itself; exit 2 on damage |
| `scrub` | check the snapshot's byte-level container checksums; exit 2 on the first damaged section |
| `recover <DST>` | salvage a content-corrupt database into a clean `DST`; the source (`--db`) is left untouched |
| `export` | dump the currently-open facts as JSONL (one per line) to stdout |
| `import <FILE> [--batch N]` | load facts from a JSONL file (as written by `export`), streamed in batches — one embed round-trip and one fsync per batch |
**Fact ids.** A fact's id is how you address it in `forget`, `revise`, and
`show`. `remember` prints it on store (`remembered fact 3`), and `recall`
carries it on every line: the human block renders each fact as `- [fN] text …`
where `N` is the id (so `[f3]` → `forget 3`), and `--json` exposes the same
value as a plain `"id"` field on each fact. You don't guess an id — you read it
back from a `recall` (or `show`), then act on it, the usual "find, then change"
flow.
Read-only commands (`recall`, `show`, `stats`, `export`) open the snapshot
**zero-copy over an mmap** (a shared lock, so several may run at once and
the whole file is not loaded) — falling back to a normal open if the
journal is un-checkpointed. `recall` uses that fast path only when no
embedder is configured, because embedding the query needs the read-write
handle. `scrub` also takes the shared mmap open, so it needs a checkpointed
database — run `checkpoint` (or `maintain`) first if the journal is dirty.
### Examples
```sh
# Remember, with a subject entity, a tag, and opaque metadata (a URI to the
# real payload in another store — the engine never interprets it):
plugmem-cli remember "prefers tokio with pinned versions" --entity user --tag pref \
--meta source=chat --meta uri=s3://bucket/note.txt
# Recall — a ranked block ready to paste into a prompt:
plugmem-cli recall "which runtime"
# Bitemporal: correct a fact, then ask what was true earlier.
plugmem-cli remember "lives in Moscow" --entity user # → fact 0
plugmem-cli revise 0 "lives in Berlin" --entity user
plugmem-cli recall "lives" --entity user --as-of 1700000000000 # → Moscow
# Machine-readable output for tooling / agents:
plugmem-cli --json recall "runtime" --tag pref
plugmem-cli --json stats
# Reclaim space held by forgotten facts. `recall` shows the id as `[fN]`;
# use that N to forget, then compact:
plugmem-cli recall "old runtime note" # → - [f3] prefers deno …
plugmem-cli forget 3 # the 3 from [f3]
plugmem-cli maintain
# Human-readable backup and restore. export streams line by line; import
# streams the file back in batches (one embed round-trip + one fsync each):
plugmem-cli export > backup.jsonl
plugmem-cli --db other.plugmem import backup.jsonl --batch 256
# Integrity & recovery (exit 2 on corruption — scriptable as a gate):
plugmem-cli verify # content consistency
plugmem-cli checkpoint && plugmem-cli scrub # flush journal, then byte-level checksums
plugmem-cli recover agent.recovered.plugmem # salvage into a clean copy
```
## Many memories in one directory (optional)
**Default: one memory, one file.** `--db path/to/memory.plugmem` and nothing
here applies. Reach for this only when you keep several independent
memories — one per project, per client, per agent — and want to address them by
name instead of remembering where each file is.
Point the CLI at a directory and `--db` starts taking a name:
```console
$ export PLUGMEM_WORKSPACE=~/memories # or --workspace, or [workspace].dir
$ plugmem-cli --db work remember "the release branch is integration/0.3.0"
$ plugmem-cli --db personal recall "release branch" # sees nothing: separate memory
```
The rule is one line: a name has no separator and no dot, so `work` is a name
while `work.plugmem`, `./work` and `/srv/work` stay paths. A name resolves to
`<dir>/db/<name>.plugmem` and nowhere else — it is not a path and cannot become
one. A first write to an unused name creates that memory; there is no
registration step.
Memories are **independent**: no search spans them, and there is no way to link
an entity across them. That is the point (one memory answering for another is
what makes a shared store useless), and it is also the thing to decide up front
— a fact filed in the wrong memory is not merely misplaced, it is unreachable
from the other.
### The `workspace` command group
Administrative, and none of it is needed for everyday use:
| `workspace list` | every memory on disk, with its description when it has one |
| `workspace find <QUERY> [-k N]` | which memory is the one about… — searches descriptions, and a person's name finds what they own |
| `workspace describe <NAME> <TEXT> [--tag T]… [--owner WHO]` | say what a memory is for; creates it if absent |
| `workspace archive <NAME>` | label it archived (it stays where it is and stays openable) |
| `workspace reindex` | rebuild the registry from the memories' own descriptions |
| `workspace verify` | report where the registry and the directory disagree; exit `1` if they do |
| `workspace use <NAME>` | print the shell line that points **this terminal** at a memory |
The description lives in two places: inside the memory itself, and in a registry
(`<dir>/registry.plugmem`, an ordinary plugmem database). The directory is the
truth and the registry is only a searchable index over it — delete it and
`workspace reindex` rebuilds it from the memories; what you lose is search, not
data.
`workspace use` writes **nothing to disk**. It prints a line for the shell:
```console
$ eval "$(plugmem-cli workspace use work)" # sh / bash / zsh
The selection then lives in that terminal, so a second window is unaffected — a
state file shared by every window would let one of them silently redirect a
script running in another.
## Interactive mode (`repl`)
`plugmem repl` opens the database **once** and runs commands from stdin (one
per line, the same subcommand grammar), keeping the engine resident so each
command is host speed instead of a per-command reload. `help` lists the verbs,
`exit`/`quit` (or EOF) leaves, and the session checkpoints on exit. This is the
read-write session: it holds the writer lock and sees its own writes instantly
(read-your-writes), so there is never anything to "refresh".
```text
$ plugmem-cli repl
plugmem> remember "prefers tokio"
plugmem> recall runtime
plugmem> exit
```
`plugmem repl --read-only` is a **separate, observe-only** session for watching
a database that **another process** is writing. It opens a shared, zero-copy
mmap over the last published snapshot (it does not take the writer lock and does
not write), so only the read verbs run — `recall`, `show`, `stats`, `export`,
`verify`. It adds two cross-process freshness verbs:
| `generation` | print the snapshot generation this session is pinned to (a number that a writer's checkpoint bumps) |
| `refresh` | advance to the writer's latest published checkpoint, if any — prints `refreshed → generation N` or `already current → generation N` |
**These two verbs exist only in `--read-only`.** A normal (writer) `repl` and
every one-shot command already see the newest data — read-your-writes, or a
fresh open per command — so refreshing there is meaningless and is not offered.
`--read-only` requires a checkpointed database (an un-checkpointed writer is
refused): run `checkpoint` in the writing process first.
```text
$ plugmem-cli repl --read-only # in a second terminal, while a writer runs
plugmem(ro)> generation
generation 7
plugmem(ro)> recall runtime # answers as of generation 7
plugmem(ro)> refresh # the writer has checkpointed since
refreshed → generation 9
plugmem(ro)> recall runtime # now answers as of generation 9
plugmem(ro)> exit
```
## Configuration
Optional `config.toml`, found by `--config PATH`, then `$PLUGMEM_CONFIG`, then
the platform config directory (all optional — the CLI works with none).
Precedence overall is **explicit path/flag > environment > config file >
platform default**. See the [full settings reference](https://github.com/m62624/plugmem/blob/main/crates/plugmem-host/SETTINGS.md)
for all fields and OS-specific paths.
```toml
[database]
path = "/path/to/memory.plugmem" # optional example; --db and PLUGMEM_DB win
[engine]
dim = 768 # embedding size (0 = vectors off); other Config
# size fields: max_bytes, max_text, shards_* …
[embedder] # default: none — lexical/tags/graph/time still work
model = "nomic-embed-text"
api_key_env = "OPENAI_API_KEY" # env var holding the bearer token (openai)
[maintenance]
snapshot_every_ops = 1024
snapshot_journal_bytes = 4194304
maintain_every_forgets = 100 # optional auto-purge
batch_size = 128 # facts per `import` batch (--batch overrides)
```
`import` streams the file in batches of `batch_size` (default 128; `--batch N`
overrides it): each batch is a single embedder round-trip and a single journal
fsync, so a bulk load with an embedder makes one HTTP call per batch instead of
one per fact, and the file is never fully read into memory. Larger batches mean
fewer round-trips but a bigger request body and more memory per batch.
The embedder is what unlocks the **vector** recall source: with `kind =
"none"` (the default) `remember`/`recall` still answer from lexical, tag,
graph and temporal evidence, but no embeddings are computed. One
OpenAI-compatible client covers Ollama, OpenAI, LM Studio, vLLM and
llama.cpp-server. `$PLUGMEM_EMBEDDER` overrides `[embedder].kind`.
## Exit codes
Scriptable as a gate:
| `0` | success |
| `1` | a soft miss — the target fact does not exist (`show`), or the database is locked by another process |
| `2` | a usage error (bad arguments) or a runtime error (I/O, a corrupt image) |
## Lifecycle — open per command
Each invocation is a **short-lived process**: it opens the database file,
runs one command, and exits — the process *is* the session boundary, so
there is no explicit open/close and nothing to keep open between calls.
This is the same model as `sqlite3`, `git` and most file-backed tools:
run → one operation → done. Two invocations that happen to overlap in that
brief window collide on the lock (the second gets exit `1`); back to back,
they never do.
Opening reads the snapshot into memory and replays the journal, so on a
large database each command pays that load. For a memory of tens of
thousands of facts it is milliseconds; if you need many operations against
a big memory without re-loading each time, that is the **long-lived
handle** case — embed [`plugmem-host`](https://docs.rs/plugmem-host/latest)'s
`Database` in your process (open once, call many verbs, all in RAM), or
run the MCP server, which keeps the memory resident. The CLI deliberately
does not: it trades a per-command load for a stateless, scriptable tool.
How much a memory weighs — the byte cost of a fact, an edge or a vector,
and where each structure tops out — is tabulated in
[`plugmem-core`](https://docs.rs/plugmem-core/latest)'s *Capacity — what weighs
what*; it applies verbatim to a file the CLI opens.
## Concurrency
One database file is a single-writer resource: `plugmem-cli` takes an
exclusive lock for the length of the (short-lived) command, so a second
`plugmem-cli` — or an MCP server holding the same file — is refused with
exit `1` rather than corrupting it. See the
[host concurrency model](https://docs.rs/plugmem-host/latest).
## License
MIT.