Skip to content

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.

install.sh
curl -fsSL https://pfmem-api.packagefactory.dk/install.sh | sh
pfmem init

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

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

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

Exit codes and what they mean
CodeMeaning
0The call succeeded.
1Anything else.
2The arguments were wrong. Nothing was sent.
3The key was missing or is not accepted.
4The key lacks the verb, or the target is out of its scope.
5The request was well-formed but failed validation.
6A quota or rate limit was hit.
7The service could not serve the request. Retryable.
8The request never reached the service.
9The 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.

pfmem init
cat key.txt | pfmem init
pfmem init --origin https://api.example.com

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

--all-subjects
# 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.

--app-id · --project-id
pfmem search --memory-type user --user-id u_42 --query "retry policy" --app-id support-bot
pfmem delete --created-before 1735689600000 --project-id billing

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

pfmem search --pretty
{
  "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.

Commands and what each one does
CommandWhat it does
addIngest messages for extraction. Asynchronous; returns a task id.
searchRank memories by relevance to a natural-language question.
getList a subject's memories, filtered and paginated.
editCorrect one memory in place.
deleteDelete memories by id, by session, or by age.
flushForce a session's pending extraction to run now.
task · tasks · retryRead 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 · versionLocal. They send no API request, though two of them still reach the network.
skill install|status|updateInstall, 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.

Flags accepted by every command
FlagWhat it does
--prettyIndent the JSON written to stdout.
--verboseRecord request diagnostics in the log file.
--origin <url>Override the API origin for this invocation.
--help, -hPrint help for the command and exit.
--version, -vPrint 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
pfmem logs --lines 50

Credentials 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
pfmem update --check

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

environment
PFMEM_NO_UPDATE_CHECK=1

Procedures

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-*
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
pfmem skill install
pfmem skill install --dir DIR
pfmem skill status
pfmem skill update

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