Command line
Your memory, from a terminal
The same memory your assistants read, reachable from the place you already work. Capture the thing you just worked out, pipe a command’s output straight into it, and ask for any of it back weeks later. One command to install, three to be useful.
$ pm auth login
Opening your browser to sign in.
Signed in to https://api.persistmemory.com as profile "default".
# capture a decision, with the reasoning
$ pm remember "we chose Postgres with pgvector over Pinecone — cost and lock-in"
Accepted for processing. Job job_01j8x…
# and ask for it back, months later
$ pm search "why postgres"
SCORE TYPE TITLE CONTENT 0.914 decision Chose Postgres+pgvector Cost and lock-in were the deciding…
Install
Node 20 or newer is the only requirement, on every platform. The last one installs nothing at all.
macOS and Linux
shellcurl -fsSL https://persistmemory.com/install.sh | sh
Windows
PowerShellirm https://persistmemory.com/install.ps1 | iex
Any platform
npm, or nothing at allnpm install -g @persistmemory/cli # or run it without installing npx @persistmemory/cli auth login
Piping a script into a shell is worth being careful about, so read ours first — install.sh and install.ps1. Both refuse to run rather than guessing when they cannot find a supported platform or a recent enough Node, neither ever escalates to sudo or Administrator on your behalf, and both tell you what to do instead when npm’s global directory is not yours to write to.
pm auth login
Signing in
Two ways, and both are first-class. A browser when there is one, and a key when there is not — because a CLI whose only login needs a browser is a CLI that cannot run in a pipeline.
At a desk, with a browser
pm auth login
Opens your browser, asks you to approve four named permissions, and stores a token that renews itself. Over SSH, where nothing can open, it prints the URL and waits — so the same command works on a server.
In CI, with no browser
export PERSISTMEMORY_API_KEY=pm_live_… pm search "last release notes"
An environment variable outranks any stored login, so a runner authenticates without writing a file. Create a key on the settings page, or interactively with pm auth login --api-key, which prompts without echoing so the key never reaches your shell history.
The browser holds the password, not the CLI
Signing in opens your browser and uses OAuth 2.1 with PKCE. Your password is never typed into a terminal and never passes through the CLI. What comes back is a token scoped to what you approved on the consent screen.
A stolen code is worthless on its own
The CLI invents a one-time secret, sends only its hash to the browser, and reveals the secret when redeeming the code. Anything that intercepts the redirect on a shared machine gets a code it cannot exchange.
Credentials are kept apart from settings
Tokens live in ~/.persistmemory/credentials.json at mode 0600. The config file beside it holds only which server you talk to, so the file people paste into a bug report is the harmless one.
Commands
The grammar is pm <verb> <noun>, so a command you have not run yet is usually the one you would guess. pm --help has the full list.
Signing in
| pm auth login | open the browser and sign in |
| pm auth login --api-key | paste a key instead — for CI and headless machines |
| pm auth status | who am I, and does the server still accept it |
| pm auth logout | forget the stored credential |
Capturing
| pm remember "we chose Postgres" | capture a line |
| git log -1 | pm remember - | capture whatever is piped in |
| pm remember --file notes.md | capture a file |
A session
| pm | start a session — ask, read files, write answers back |
| pm chat --resume <id> | pick up where you left off |
| /read <path> | read a file into the conversation |
| /capture <path> | read a file AND remember it |
| /write <path> | write the last reply to a file — always asks first |
| /usage | what this session has cost |
Asking
| pm search "what did we decide about Postgres" | search your memory |
| pm list memories --limit 20 | the most recent memories |
| pm get memory mem_01j… | one memory, in full |
| pm list spaces | your Spaces |
Readable by a person, parseable by a script
Output is a table when it is going to a terminal and JSON when it is going to a pipe, so both work without your having to know a flag exists. Say --output yaml|csv|tsv when you want something else.
# a table, because this is a terminal $ pm list memories --limit 3 ID TYPE TITLE CONFIDENCE UPDATED mem_01j8x… decision Chose Postgres 0.91 2026-08-25 14:02 mem_01j8w… task Send Northwind PDF 0.78 2026-08-24 09:41
# json, because this is a pipe $ pm search "postgres" | jq -r '.results[].memory.title' Chose Postgres with pgvector # and the exit code says which failure it was $ pm search x; echo $? 3 # not signed in