plugmem-cli 0.7.0

plugmem command-line interface — bitemporal memory and hybrid retrieval over one local database.
Documentation

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 logical memory in a local database 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.

No embedding model is required. Of the four recall sources, only the vector one needs an embedder; text, graph and time work with nothing but the database. Configure [embedder] to add matching by meaning, or leave it out and match on words, entities and time.

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 local database, 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 agent, local-first app, or 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.
A memory in Python plugmem-py The engine as a CPython extension (PyO3), in-process; on PyPI 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 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

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.

command what it does
remember <TEXT> [--entity E] [--tag T]… [--link REL:ENTITY]… [--meta KEY=VALUE]… [--valid-from TS] [--vector F32,…] 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] [--token-budget N] [--ef N] [--graph-depth N] [--vector F32,…] ranked, token-budgeted block; sources compose. Each line is - [fN] text …N is the fact's id (see below). --token-budget caps the block (default 512), --ef widens the vector search beam, --graph-depth sets how far the graph walks from an anchor (default 2; 0 disables expansion)
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> [--provenance FACT_ID] upsert a typed edge between entities. --provenance records the fact the edge follows from, and graph recall returns it
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 memory as JSONL to stdout: every open fact, then every open edge, one per line, each tagged with kind. Streamed, so a large memory never has to fit in RAM
import <FILE> [--batch N] load a file written by export, streamed in batches — one embed round-trip and one fsync per batch. Edges are re-linked and their provenance retargeted to the ids this database assigns

--vector takes a comma-separated embedding (--vector 0.1,-0.2,…, or --vector "$(cat vec.txt)" for a real one). Given, it replaces the configured embedder for that call — nothing is sent to the provider — and its length must equal [engine].dim. Omit it and the engine embeds the text itself, which is what you want unless the vector already exists or your model is not an OpenAI-shaped HTTP endpoint.

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):
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 bounded for a prompt or another context consumer:
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

# Move a memory to another file. Both halves stream, so size does not matter:
plugmem-cli export > dump.jsonl
plugmem-cli --db other.plugmem import dump.jsonl --batch 256
# → "imported 128 facts and 14 edges"
#
# What crosses: text, entity, tags, metadata, valid_from, edges and each
# edge's provenance. What does not: closed revisions and vectors. This is a
# portable knowledge dump, not a backup — for a backup, copy the files.

# 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 logical memory backed by a local database layout. --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:

$ 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:

command what it does
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:

$ eval "$(plugmem-cli workspace use work)"     # sh / bash / zsh
$ plugmem-cli workspace use work | Invoke-Expression   # PowerShell

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".

$ 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 needs a published snapshot to map, so a database that has never been checkpointed is refused: run checkpoint in the writing process first. After that the writer may keep writing — a read-only session simply answers as of the checkpoint it pinned, until you refresh.

$ 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 for all fields and OS-specific paths.

[database]
path = "/path/to/memory.plugmem" # optional example; --db and PLUGMEM_DB win

[engine]
dim = 768              # embedding size (0 = vectors off); also max_bytes,
                       # max_text, max_blob. What the database is *built* with:
                       # changing one on an existing file is refused.

[recall]               # optional — every key has a tuned default
w_vec = 2.0            # weight of the vector source (0 turns it off)
half_life_days = 30    # age at which the recency discount has halved
                       # also: w_bm25, w_graph, w_time, w_recency, rrf_k,
                       # bm25_k1, bm25_b, graph_depth, graph_decay,
                       # hnsw_ef_search, similar_cos, similar_jaccard

[index]                # optional
flat_to_hnsw = 50000   # vectors before maintenance builds the HNSW graph
                       # also: hnsw_ef_construction

[embedder]             # default: none — lexical/tags/graph/time still work
kind = "ollama"        # ollama | openai | lmstudio | vllm | llamacpp | none
url = "http://localhost:11434/v1"
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.

[recall] and [index] are safe to change on an existing memory: reopening with different weights is how you change the ranking, and the next checkpoint records them in the file. Reach for them when a specific memory answers badly — the defaults are tuned, and w_bm25 = 0 (say) is mostly useful for asking what one source alone thinks.

A key nothing recognises is reported, not ignored, on stderr. Refusing it would mean an older binary could not read a newer config, but staying silent would leave you believing you had tuned something:

$ plugmem-cli stats
plugmem: unknown config section [engin] — did you mean `engine`?
plugmem: unknown setting [recall].w_vector — did you mean `w_vec`?
facts       0
...

Run plugmem-cli help settings for the complete catalogue with every default.

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 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.