Command line
pfmem
A single-file client for the memory API, written to be driven by an AI harness. It holds one key and calls the memory endpoints. It cannot mint, rotate or revoke a key, create a workspace, or configure a provider — those are administration, and they live behind the admin API on purpose.
Install
The installer fetches the published manifest, downloads the bundle, verifies it against the manifest's sha256, and installs a launcher. It compiles nothing and needs no root.
curl -fsSL https://pfmem-api.packagefactory.dk/install.sh | sh
pfmem initIt needs curl, Node 22 or newer, mktemp and sed, and says which one is missing rather than failing later with a syntax error.
Two environment variables move the installation: one relocates the whole thing, the other points it at a different deployment.
PFMEM_HOME="$HOME/.local/pfmem"
PFMEM_ORIGIN="https://pfmem-api.packagefactory.dk"The stdout contract
The primary caller is a model, so standard output is always exactly one JSON document. Everything written for a human — prompts, warnings, update notices — goes to standard error, where a parser never sees it.
{"ok":true,"request_id":"…","data":{…}}
{"ok":false,"error":{"code":"…","message":"…","request_id":"…"}}Check the success flag first. On a failure, switch on the error code and never on the message; messages are written for people and change between releases.
Three invocations are exempt, because a human is the reader: the reference page prints markdown, help prints help text, and the version flag prints a bare version string. Nothing else puts anything but one JSON document on stdout.
One flag indents the JSON. It changes the whitespace and nothing else.
Exit codes
Scripts switch on these numbers, so they are part of the interface.
| Code | Meaning |
|---|---|
0 | The call succeeded. |
1 | Anything else. |
2 | The arguments were wrong. Nothing was sent. |
3 | The key was missing or is not accepted. |
4 | The key lacks the verb, or the target is out of its scope. |
5 | The request was well-formed but failed validation. |
6 | A quota or rate limit was hit. |
7 | The service could not serve the request. Retryable. |
8 | The request never reached the service. |
9 | The thing addressed does not exist. |
The key, and where it lives
Give the key on standard input, or let it prompt when there is a terminal. Both keep the secret out of the process list, where ps shows it to every user on the machine, and out of your shell history. The flag that takes a key as an argument exists, and is the worst of the three.
cat key.txt | pfmem init
pfmem init --origin https://api.example.comThe read from standard input is bounded at 8 KB and ten seconds, so a harness that leaves the pipe open but idle gets a usage error naming its alternatives rather than hanging forever.
The key is read from the environment first, then from the config file, which is created readable only by its owner. It is never printed, never logged, and never echoed back by the diagnostic command.
A key read from that file is only ever sent to an https origin or to loopback. A stored credential rides along on every later invocation without anyone thinking about it, so an origin naming a plaintext host is refused rather than obeyed.
Overriding the origin for one call never repoints where the client fetches its own code. A per-invocation flag must not be able to nominate an installer.
Who this machine speaks for
Every memory belongs to a subject — a person, or an agent keeping notes for itself. Setup settles that once and records it, so later calls need not name it. Without that the subject is typed on every call and checked against nothing, and a second spelling of the same person is a separate, empty place that accepts writes and answers every later read with nothing.
Setup asks the server first. A key issued against one subject already carries it, and that answer is recorded without asking, because a write as anyone else is refused anyway. Otherwise it asks you, offering an address it finds in your git configuration, then your account name on the machine.
pfmem init
# Whose memories is this machine writing? [user/agent] (user)
# Which user id? ([email protected])
# or, with no terminal to ask at:
pfmem init --mode user --subject-id [email protected]An id only has to be stable and spelled the same way everywhere you call from. An email address is a fine choice, and is usually the one offered.
With no terminal nothing is asked, because a question nobody is there to answer waits forever. Give the mode and the id as flags instead; with neither, setup still stores the key, records no subject, and says so, and each call then names its own.
Afterwards the kind and the subject are both filled in for you when you leave them out. Anything you pass wins over what was recorded, and on a read there is a flag that widens back out to every subject the key is allowed to see. The diagnostic command reports what was recorded.
# the kind and the subject are filled in from what init recorded
pfmem search --query "how do they prefer to travel"
# widen back out to every subject this key may see
pfmem search --memory-type user --all-subjects --query "billing"What the client will not send
There is no flag for a workspace, an account, a filter or an embedding version, and a test asserts that none can be added. The key resolves to all of it server-side. Supplying any of them is precisely how a client would read someone else's memories.
Two scope flags exist, and they only ever narrow within the workspace the key already resolved to. Omitting them spans everything that key can already see; neither can widen anything.
pfmem search --memory-type user --user-id u_42 --query "retry policy" --app-id support-bot
pfmem delete --created-before 1735689600000 --project-id billingWhat a key is served
The CLI always writes to stdout with a key, so these rules are visible in what it prints. Unapproved content arrives marked, and anything withheld from the key is reported as a count rather than as a result.
{
"ok": true,
"request_id": "9130cb814f654e1ea042497d6362b08d",
"data": {
"hits": [
{
"record": {
"id": "a1b2c3",
"memory_type": "user",
"subject_id": "[email protected]",
"content": "[unverified] Prefers the night train to Berlin.",
"verification": "unverified",
"availability": "served"
},
"score": 0.0328,
"rank": 1
}
],
"method": "hybrid",
"embedding_version": "external_v1",
"took_ms": 41,
"withheld_count": 2
}
}The marker on unapproved content
[unverified]
Content no person has approved comes back with this prefix in front of its text. The prefix is added to the answer and never stored, so the same memory read by a signed-in person carries no prefix.
What was withheld, as a count
withheld_count
When a search or a listing leaves something out because it is withheld from this key, the count is in the JSON, and pfmem also writes it to stderr in human mode. A result set that quietly shrank is a model reading a smaller truth than the one that exists, and the count is the only warning it gets.
A procedure serves the version a person approved, when there is one
pending_version
A key is served the newest version a person has approved. When a key has written a newer version and nobody has approved it yet, the read answers with the approved version and names the waiting one in this field, so an agent follows a runbook somebody stood behind. When no version has ever been approved, the newest version is served with the marker instead: a procedure that only ever a key wrote is still usable, and nothing is held back until there is an approved version for it to fall behind.
Where an approval happens
Approving happens in the dashboard, on the page under Review — Flagged for review. Nothing a key sends can make an item trusted, and that is what keeps "a person vouched for this" a fact rather than a claim.
Commands
The reference command prints all of this in full — every flag, every constraint, every response shape — generated from the same specification the argument parser uses, so it cannot describe a flag the client would reject. That is the page to give a model.
| Command | What it does |
|---|---|
add | Ingest messages for extraction. Asynchronous; returns a task id. |
search | Rank memories by relevance to a natural-language question. |
get | List a subject's memories, filtered and paginated. |
edit | Correct one memory in place. |
delete | Delete memories by id, by session, or by age. |
flush | Force a session's pending extraction to run now. |
task · tasks · retry | Read one background task, list them, or run a failed one again. |
procedure-* | Write, read, version, retire, list and search procedures — the how-to memory a workspace keeps. Its own section is below. |
init · doctor · logs · docs · update · version | Local. They send no API request, though two of them still reach the network. |
skill install|status|update | Install, check or update the skill an agent reads. |
There is no command that stores raw text without extraction, and none that recalls a transcript. Reading before an edit is a listing, because an edit needs the memory's id and its version.
Global flags
These apply to every command.
| Flag | What it does |
|---|---|
--pretty | Indent the JSON written to stdout. |
--verbose | Record request diagnostics in the log file. |
--origin <url> | Override the API origin for this invocation. |
--help, -h | Print help for the command and exit. |
--version, -v | Print the version and exit. |
Logs
A JSONL file, readable only by its owner, recording each request's method, path, status, duration and request id, plus failures. It rotates at 1 MB and keeps one generation.
pfmem logs --lines 50Credentials are redacted on the way in: field names like authorization, api_key and token; any key string anywhere in a message, a URL or a nested value; and any bearer header.
Quote the request id from a failure. The service records the same id, which is what makes a problem traceable from your terminal to our logs.
Updates
At startup the client checks the release feed, cached for six hours. When a newer version exists it prompts only if both standard input and standard error are a terminal; otherwise it writes one line to standard error and carries on. A harness is never blocked, and an unreachable update service never fails a memory call.
pfmem update
pfmem update --checkAn update downloads, verifies the sha256, and replaces the bundle atomically. A check reports what is published and installs nothing.
Update and skill downloads require https, and the installer pins the protocol across redirects. A checksum published beside an artifact only proves the bytes match what that same server sent; over plaintext an attacker on the path supplies both, so it is the transport that makes the checksum mean anything. A plaintext origin is refused before any request is made.
One environment variable disables the check entirely.
PFMEM_NO_UPDATE_CHECK=1Procedures
A procedure is a how-to rather than a fact — how you ship, how you triage, what the steps actually are — and your assistant reads one before doing the work instead of rediscovering it. Writing one gives back an id every later call uses.
pfmem procedure-list --name-prefix release --limit 20
pfmem procedure-search --query "release freeze" --top-k 5
pfmem procedure-get --procedure-id pr_9f3c1a2e-5d47-4b8a-9c31-6e0d2f7b4a15
# that read came back at version 3, so append the next one conditionally
pfmem procedure-update --procedure-id pr_9f3c1a2e-5d47-4b8a-9c31-6e0d2f7b4a15 \
--content "Freeze at seven now." --expected-version 3 --reason "the freeze moved an hour"
pfmem procedure-versions --procedure-id pr_9f3c1a2e-5d47-4b8a-9c31-6e0d2f7b4a15
# the history lists versions without their text; read the one before the append
pfmem procedure-versions --procedure-id pr_9f3c1a2e-5d47-4b8a-9c31-6e0d2f7b4a15 --procedure-version 3
pfmem procedure-archive --procedure-id pr_9f3c1a2e-5d47-4b8a-9c31-6e0d2f7b4a15 --reason "replaced by a new runbook"
# retiring is one-way, so the way of working that replaces it is a new procedure
pfmem procedure-create --name nightly-release --visibility team \
--description "how we ship at night" \
--content "Freeze at seven, then {{shared:deploy-checks}}" \
--reason "replaces the retired nightly runbook"Search when you know a procedure probably exists and not what it was called; list when you want everything this key can reach. Both answer with summaries; only the search adds the passage that matched, with the matching words marked, and the text itself comes from the read.
An update appends a new version and never edits in place, so an old runbook is never lost under a new one. Pass the version you read and a change somebody else made first fails loudly instead of being buried. Retiring one takes it out of listing and search while its text and every version stay readable to anyone holding the id. Nothing here deletes, and nothing un-retires: a retired procedure takes no further version, so a replacement is a new procedure.
A procedure is private by default — its owning agent, and any key that reads the whole workspace. Sharing it with everyone in the workspace is a decision, not a default, and an update can move it either way. A key bound to no agent has nobody to keep a procedure private from, so it must share the ones it writes.
Only the subject that owns a procedure, or a key bound to no subject at all, may append to it or retire it. To everyone else it answers as one that does not exist — so a refusal on a procedure you can see in the listing means it is not yours to change, not that the id is wrong. A master key is not such a key here: it answers for an account rather than for one workspace, and this client never sends a workspace name, so every procedure command made with one is refused. Mint a workspace key and give the client that.
These have nothing to do with the skill commands below. The skill is one published file that teaches your assistant how to drive this client; a procedure is content your workspace owns and versions. Installing the skill stores no memory, and writing a procedure installs nothing.
The skill
The client ships a skill: one file teaching an agent when to reach for memory and how to call it. Install it and your assistant stops guessing at the interface.
pfmem skill install
pfmem skill install --dir DIR
pfmem skill status
pfmem skill updateWith no directory given, a project-level configuration found by walking up from the current directory wins over the user-level one — standing in a repository means that repository. When neither exists it reports both places it looked and asks for a directory rather than guessing.