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 — a thin shell around
plugmem-host 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
tap; brew upgrade / brew uninstall then manage it like any formula:
$ brew install m62624/plugmem/plugmem-cli
Installer script (no Rust toolchain)
latest always points at the newest tag on the
Releases page:
# Linux / macOS (POSIX sh)
$ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/m62624/plugmem/releases/latest/download/plugmem-cli-installer.sh | sh
# 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. Double-click to
install; it registers in "Add or remove programs" for normal upgrades and
uninstalls.
cargo binstall
cargo-binstall downloads the prebuilt binary instead of compiling — it just works on every OS/arch above:
$ cargo binstall plugmem-cli
From source
Needs a Rust toolchain. From crates.io:
$ cargo install plugmem-cli
…or from a local checkout of this repo:
$ 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 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.
| You want | Use | Why |
|---|---|---|
| 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 (std) |
The engine plus files, locking, mmap, HTTP embedders, integrity, concurrency. |
The engine with no std or your own storage |
plugmem-core (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):
| Source | Algorithm | What it finds |
|---|---|---|
| Lexical | BM25 (Robertson idf) over a Unicode (UAX #29) tokenizer | exact terms / keyword overlap |
| Semantic | int8-quantized cosine — flat two-phase below a threshold, an HNSW graph above | meaning / nearest neighbours |
| Graph | entity graph with typed edges, breadth-first from query anchors | relational knowledge |
| Temporal | range scans over a recorded_at-ordered index; bitemporal validity |
"what was true then", time windows |
Usage
plugmem-cli [--db PATH] [--json] <command>
The database is chosen by, in order: --db PATH, the PLUGMEM_DB
environment variable, or ./plugmem.db (created on first write). The
engine keeps no clock, so now comes from the system clock at each call.
| command | what it does |
|---|---|
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 |
show <ID> |
one fact's full card — text, both time axes, state |
stats |
engine size counters |
maintain |
purge tombstones, compact, build HNSW past the threshold (disk-first) |
checkpoint |
flush the journal into a fresh snapshot and clear it (leaves the database checkpointed) |
verify |
check content integrity (text UTF-8, vector↔fact consistency); 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
# Remember, with a subject entity, a tag, and opaque metadata (a URI to the
# real payload in another store — the engine never interprets it):
# Recall — a ranked block ready to paste into a prompt:
# Bitemporal: correct a fact, then ask what was true earlier.
# Machine-readable output for tooling / agents:
# Reclaim space held by forgotten facts. `recall` shows the id as `[fN]`;
# use that N to forget, then compact:
# Human-readable backup and restore. export streams line by line; import
# streams the file back in batches (one embed round-trip + one fsync each):
# Integrity & recovery (exit 2 on corruption — scriptable as a gate):
&&
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".
$ 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:
| verb | what it does |
|---|---|
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.
$ 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 $XDG_CONFIG_HOME/plugmem/config.toml (all optional — the CLI works
with none). Precedence overall is flag > environment > config file >
default.
[]
= 768 # embedding size (0 = vectors off); other Config
# size fields: max_bytes, max_text, shards_* …
[] # default: none — lexical/tags/graph/time still work
= "ollama" # ollama | openai | lmstudio | vllm | llamacpp | none
= "http://localhost:11434/v1"
= "nomic-embed-text"
= "OPENAI_API_KEY" # env var holding the bearer token (openai)
[]
= 1024
= 4194304
= 100 # optional auto-purge
= 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:
| code | meaning |
|---|---|
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'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'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.
License
MIT.