Kevin Codex 335e1aa746 fix(tests): isolate preflight from the real config and always test a … (#35)
* fix(tests): isolate preflight from the real config and always test a fresh Node bundle

tests/mcp-preflight.test.ts resolved settings against the real
~/.openrecall/config.json: `envFor` set no OPENRECALL_HOME, so on a machine
that had run `openrecall init` the two "missing passphrase" cases found a
passphrase and failed (499/501). Every env the file builds now points at a
per-module temp home, and a positive control writes a passphrase into a
second home and asserts it is not read, while the config-file fallback is
shown to be live when OPENRECALL_HOME points at that home.

tests/cli-version.test.ts and tests/mcp-stdio.test.ts skipped the Node
launcher when dist/openrecall.js was absent and tested a stale one when it
was present. scripts/build.ts now exports `bundleCli(outdir)` behind
`import.meta.main`, and tests/node-bundle.ts builds the CLI from the current
source once per test process into a temp dir, so the Node launcher is always
exercised and `bun test` is green with any dist/, stale or missing. The
bundle is built in a child bun rather than in-process: inside `bun test`,
Bun.build cannot resolve files directly under src/ until the runtime
resolver has touched one of them (Bun 1.3.11), and `bun test` runs no
`exit` handlers registered during module load, so the dir is named after
the pid and dirs of dead pids are swept on the next build.

dist is now two files: dist/openrecall.cli.js, the bundle, and
dist/openrecall.js, the `bin` entry, a shim that enables Node's compile
cache and imports the bundle. The cache covers only modules loaded after it
is enabled, so it could not be turned on from inside the bundle. Measured on
Node 24.22.2 (darwin arm64), medians:

  node dist/openrecall.js --version     before 39.2 ms  after 31.3 ms  shim without cache 38.5 ms  (10 runs x 3 rounds)
  MCP stdio initialize round trip       before 68.8 ms  after 61.9 ms  shim without cache 71.8 ms  (10 runs x 3 rounds)
  spawn -> "[openrecall mcp] ready"     before 66.9 ms  after 59.7 ms  shim without cache 67.9 ms  (20 runs, interleaved)

Both headline gains clear the 5 ms bar, and the extra import hop costs under
1 ms, so the layout stays. Under Node, `openrecall serve` re-spawns argv[1],
now the shim, and `launchCommand` pins the shim; tests/package.test.ts
covers both from an installed tarball, and tests/cli-version.test.ts asserts
the shim imports ./openrecall.cli.js and the bundle carries the version.

* fix(tests): drop inherited OPENRECALL_* variables before any test runs

A developer's own exports reached every CLI child spawned with
`...process.env` and every in-process settings lookup. The worst one is
OPENRECALL_PASSPHRASE_FILE: it outranks OPENRECALL_PASSPHRASE, so the
launch tests ran under the developer's passphrase instead of the
fixture's, and passed only because their namespace was empty. With the
variable pointing at a missing file, four tests failed (three in
mcp-preflight, one in package). The preload now deletes every inherited
OPENRECALL_* variable; tests set the ones they need explicitly, and
everything else in the environment is kept.

Also documents the two helpers in tests/node-bundle.ts that had no
docstring (CodeRabbit's docstring-coverage check).
2026-11-06 18:17:08 +08:00
2026-11-03 04:43:40 +00:00
2026-06-25 09:46:46 +08:00
2026-06-25 09:46:46 +08:00
2026-06-25 09:46:46 +08:00
2026-11-03 04:43:40 +00:00

OpenRecall — Your agents change. Your memory stays. One encrypted memory across projects, agents, and sessions.

One memory. Every agent.

Persistent context for your whole agent stack.
Open source. Local by default. Encrypted before storage.

CI status npm version Monthly npm downloads Storage is end-to-end encrypted MIT license Latest release GitHub Discussions Discord Twigpine on X

Quick start · Project memory · Agent setup · Self-host · Security · Community

Switch agents. Come back to a project next week. Keep the useful context.

OpenRecall is the memory layer for your agents: shared preferences, project decisions, and durable notes that Claude Code, Codex, Cursor, Zero, OpenClaude, and your own agents can reuse. Memory is encrypted on your machine; the local store or sync server holds ciphertext. Start locally, then self-host when you want the same memory on more than one machine.

Built by Twigpine, alongside Zero and OpenClaude.

Why OpenRecall

What you need What OpenRecall gives you
Context that survives a new session Durable notes your connected agents can recall and update
Preferences shared across tools One MCP memory interface for every connected agent
Projects that stay organized Discoverable project scopes, with shared preferences recalled alongside project facts
Control over your data Encryption on your machine, with local storage or a server you run
A small first step Install, initialize, connect your agents; local mode needs no server
Memory worth keeping A shared agent guide for recall, deduplication, and pruning, plus a scanner that blocks likely secrets

Quick start

npm i -g @twigpine/openrecall     # Node 24.22.1+
openrecall init                   # one passphrase, kept owner-only in ~/.openrecall
openrecall setup all --write      # connect every agent found on this machine
openrecall status                 # check the config and the store

Restart your agents and they share one memory. No server is involved: this is local mode. setup without --write is a dry run that prints every change first; where an agent has its own mcp add, that is what it uses, and any JSON file it edits is backed up. Per-agent notes, including Zero's sandbox and the Claude Code / OpenClaude plugin, are in docs/agents.md.

To point an agent at a hosted service instead, print the block to paste:

# prints the MCP config block, plus a fresh passphrase shown once
openrecall card <owner> https://memory.twigpine.com

The passphrase is generated locally and never leaves the machine: it is printed for you to store, and the block carries a placeholder rather than the value. The service key is never printed either, even if OPENRECALL_API_KEY is set; paste it into the block yourself. The URL must be https unless it points at a loopback host.

That block is a public interface, not example output. The consumer integrations point back at it rather than restating it, and a test asserts that every environment variable it emits is one the server still reads, so a rename here fails in this repository instead of at spawn time in another one.

Zero-knowledge means no recovery. The passphrase in ~/.openrecall/config.json is the only key to your memory. Back that file up; nobody, including the host, can recover it.

Three ways to use it

You run Good for
Local npm i -g @twigpine/openrecall, then init and setup one machine, every agent on it, no server
Server openrecall serve, or the Docker image the same memory on every machine, or a team
Library import { OpenRecallClient } from '@twigpine/openrecall' agents you write yourself

One rule ties them together: the MCP server runs wherever the passphrase is. Recall ranks over plaintext after decryption, so it has to run on your machine. A server only stores and syncs ciphertext; it is what makes memory follow you across machines, never a place that can read it.

Use from any agent

openrecall setup covers Claude Code, Zero, OpenClaude, Codex, Cursor, Gemini CLI, opencode, Windsurf, Claude Desktop, and VS Code. For anything else that speaks MCP, register a stdio server. The agent's config holds no secrets:

{
  "mcpServers": {
    "openrecall": { "command": "npx", "args": ["-y", "@twigpine/openrecall", "mcp"] }
  }
}

Claude Code and OpenClaude can install the plugin instead, which bundles the MCP server and the memory skill:

/plugin install openrecall --marketplace Twigpine/openrecall
Tool Purpose
memory_save(key, content) persist a durable fact
memory_recall(query) rank stored memories by relevance (locally) and return the matching part of each
memory_search(query) literal keyword or substring search
memory_get(key) read one entry in full
memory_list() list entry keys
memory_delete(key) remove an entry
memory_project(project) register or reopen a stable project memory scope
memory_projects(limit?, offset?) discover projects in the encrypted directory

Memory across projects

Project tools and multi-namespace recall need 0.2.1 or later.

Keep standing preferences and general feedback in your configured shared namespace. Keep project decisions and references in a separate project scope. The agent can discover and reuse those scopes:

  1. memory_projects() lists known project IDs and their namespaces (20 per page; use offset for more).
  2. memory_project(project: "github.com/twigpine/openrecall") registers or reopens that ID and returns its project namespace and the shared namespace. Use the same host/owner/repo ID across agents and machines; IDs are case-insensitive and ignore a trailing .git.
  3. memory_recall accepts namespaces: [shared, project] to rank both scopes together with one result limit and size cap. Every hit names its source namespace. Include only the projects relevant to the task.

Pass the returned namespace explicitly to memory_save, memory_get, memory_list, memory_search, and memory_delete. Opening a project does not change defaults or move existing memories. Existing shared memories remain available, and callers that use a single namespace keep their current behavior. Project IDs live in the encrypted directory; their namespaces use hashed IDs.

Tools alone do not make an agent use memory well; it also needs the discipline to recall before answering, save only durable facts, and never duplicate. That lives in one place, skills/openrecall-memory/SKILL.md. setup installs it where an agent loads skills, and the MCP server exposes the same text as its memory_guide prompt and in its startup instructions, so every client gets it.

Self-host a server

A server lets several machines share one memory. Install Bun and ensure bun is on your PATH. The npm-installed openrecall serve launches Bun automatically; everything else in the package runs on Node. For local memory on one machine, use init and setup from the quick start above.

npm i -g @twigpine/openrecall
export STATIC_API_KEYS="me:$(openssl rand -hex 24)"   # owner:key, note the key
STORE=fs DATA_DIR=./data openrecall serve

# on each machine, the same passphrase everywhere (copy it from the first)
openrecall init --url https://memory.example.com --api-key <key> --namespace user:me

Each API key owns the user:<owner> namespace subtree. For many users, use the Supabase key table in migrations/ instead of static keys. ALLOW_UNAUTHENTICATED=true skips auth entirely, for a server bound to localhost only. A Docker image (ghcr.io/twigpine/openrecall) and a fly.toml are included; configuration is in .env.example. What lands on the server is ciphertext: grep its data directory for any plaintext and you will find nothing.

Standalone binaries

For a machine with neither Bun nor Node, each release attaches a standalone binary with a SHA256SUMS file. The runtime is baked in, so they are large (tens of MB). The glibc builds need nothing installed, verified on a stock debian:12-slim with neither Bun nor Node present. The -musl builds are the exception: they link against libstdc++ and libgcc, so on a bare Alpine they fail to load until you apk add libstdc++.

curl -LO https://github.com/Twigpine/openrecall/releases/latest/download/openrecall-linux-x64
chmod +x openrecall-linux-x64 && ./openrecall-linux-x64 init

Use as a library

import { OpenRecallClient } from '@twigpine/openrecall'

const memory = new OpenRecallClient({
  url: 'https://memory.example.com',            // your server
  apiKey: process.env.OPENRECALL_API_KEY,
  passphrase: process.env.OPENRECALL_PASSPHRASE, // never leaves this process
})

await memory.push('user:me', { 'MEMORY.md': '# what I know about the user' })
const { entries } = await memory.pull('user:me')

push reports what the store actually stored. The store validates each key and size itself and answers 200 even when it skips an entry, so check skipped before treating a push as saved:

const { uploaded, skipped } = await memory.push('user:me', { 'notes.md': '…' })
// skipped: [{ key, reason }] — refused by the store, never counted as uploaded

delete returns { deleted }, so a real removal can be told from a no-op on a key that was not stored.

The entry points (@twigpine/openrecall, /crypto, /secretscan) ship as plain ESM with type declarations, for Node 24+, Bun, or any bundler. An agent that only needs memory tools is better served by the MCP server; the library is for code that wants the sync protocol itself.

How it works

agent (Claude Code · Zero · OpenClaude · Codex · Cursor · …)
   │   MCP tools: plaintext in, plaintext out
   ▼  openrecall mcp — runs on your machine, holds the passphrase
   │  [ client/crypto.ts — encrypt with a key derived from your passphrase ]
   │   ciphertext
   ▼
local store (~/.openrecall/data)   or   openrecall server (crypto-blind)
                                            ▼
                                  BlobStore: fs | s3 (Tigris/R2/AWS)

Security model

  • Encryption. AES-256-GCM with key = scrypt(passphrase, salt = sha256("openrecall:" + namespace)).
  • Integrity. The entry key is GCM additional authenticated data, so a blob is bound to its key and cannot be swapped or replayed.
  • Determinism. A synthetic-IV nonce enables delta sync; the only leak is whether two entries are byte-identical.
  • Key custody. The passphrase lives only in ~/.openrecall/config.json (mode 0600) or your environment, and only the local MCP/CLI process reads it. Agent configs never contain it.
  • Defense in depth. The client-side secret scanner runs before encryption and blocks saves containing live-looking credentials by default (OPENRECALL_SCAN=warn|off to relax it).
  • Passphrase custody. The MCP server and CLI accept OPENRECALL_PASSPHRASE_FILE, an absolute path, as well as the value in OPENRECALL_PASSPHRASE, and the file wins when both are set. Agent hosts commonly spread their own environment into every stdio server they launch, so a passphrase exported for openrecall is readable by all of them; a path is not, and a file can carry permissions an environment cannot.
  • Startup refusal. The MCP server checks its configuration against the pinned namespace before serving a tool, and exits rather than start on one that would corrupt stored memory: unexpanded template text in a secret, a passphrase that cannot decrypt what is stored, a rejected key, an unauthorized namespace, or a scan mode it does not recognize. Every wait is bounded; OPENRECALL_TIMEOUT_MS raises the limit on a slow link.
  • Tenancy. Each API key maps to an owner who controls exactly their own user:<owner> subtree, with per-account quotas and per-owner rate limits enforced server-side.
  • Upgrade path. A version byte on every blob allows a move to XChaCha20-Poly1305 and Argon2id without breaking existing data.

Report vulnerabilities privately; see SECURITY.md for the process and the threat model.

HTTP API

All bodies are ciphertext; the server validates sizes and hashes without decrypting anything.

Method Route Purpose
GET /health liveness
GET /api/memory/:ns full data (ciphertext entries + checksums)
GET /api/memory/:ns?view=hashes per-key checksums only, for delta sync
GET /api/memory/:ns?view=entry&key=<entryKey> one entry's ciphertext, without fetching the rest
PUT /api/memory/:ns delta upsert { entries, deletions?, base? }
DELETE /api/memory/:ns?key=<entryKey>[&base=sha256:<hex>] remove one entry

A namespace (user:me, repo:owner/name, agent:intern) is the unit of scoping, and entry keys mirror a memory directory (MEMORY.md, feedback/x.md). The hashes view also reports supports (capabilities a client can rely on) and erasure (whether this deployment's store really removes bytes on delete).

base is an optional per-entry precondition: the ciphertext hash the caller believes a key holds, or null for "should not exist". A disagreement is refused with 409 stale_base_version and a details.conflicts map naming what each key actually holds; omitting base writes unconditionally. A namespace whose manifest cannot be parsed answers 503 manifest_unreadable rather than appearing empty.

The entry view answers with that key's base64 ciphertext and checksum, byte for byte what the full read returns under the same key. It distinguishes three answers a single status would blur: 404 empty (no such namespace), 404 entry_not_found (the namespace exists, the key does not), and 503 entry_unreadable (the manifest names the key and the store cannot produce its body).

HEAD is supported on the read routes: same status and headers, no body. Any other method on a known route is a 405 with an Allow header, and a malformed percent-escape in the path is a 400 invalid_path — client input never turns into a 500.

Configuration

  • Client (CLI and MCP server): ~/.openrecall/config.json, written by openrecall init, holds url (local or a server URL), apiKey, passphrase, namespace, and scan (block | warn | off). OPENRECALL_URL, OPENRECALL_API_KEY, OPENRECALL_PASSPHRASE, OPENRECALL_NAMESPACE, and OPENRECALL_SCAN override it; OPENRECALL_HOME moves the whole directory.
  • Server: .env.example documents STORE (fs | s3), ALLOW_UNAUTHENTICATED, STATIC_API_KEYS or Supabase auth, and the per-namespace and per-account limits. Values are validated at startup: an unknown STORE or a malformed number stops the process and names the variable.

Status

Beta. @twigpine/openrecall 0.2.1 is on npm, published from CI with provenance. The crypto-blind server, client crypto, CLI, local mode, MCP server, agent setup, secret scanner, quotas, and rate limiting are implemented and covered by bun test, including real MCP sessions over stdio under both Bun and Node and an install of the packed package under Node. Next: the hosted beta at memory.twigpine.com and a self-service console for API keys. See ROADMAP.md.

Development

bun install
bun run build       # dist/openrecall.js (the Node CLI) and dist/client/ (the library)
bun test            # the suite; build first so the Node bundle and package tests run
bun run type-check  # tsc --noEmit
bun run check       # biome lint + format

The codebase splits along the trust boundary:

client/    the zero-knowledge client — encrypts here; holds the key
src/       the crypto-blind server — only ever sees ciphertext
src/cli/   client settings, local mode, and `openrecall setup`
src/mcp/   the MCP server (memory tools) over the client
bin/       the CLI (init | setup | status | push | pull | mcp | serve)
skills/    the openrecall-memory skill, shared by every agent

The cardinal rule: plaintext, passphrases, and derived keys never reach the server. A test walks the import graph to enforce it. See CONTRIBUTING.md for the full guide.

Community

Contributing

Issues and PRs welcome. For larger changes, open an issue first so the scope is clear. See CONTRIBUTING.md for the dev setup, design principles, Conventional Commits, and DCO sign-off, and our Code of Conduct.

License

Released under the MIT License. Contributions are accepted under the same license.

The logo and brand assets are included as editable SVGs.

S
Description
Open-source, self-hostable, zero-knowledge memory for AI agents. End-to-end encrypted — the server stores only ciphertext and can't read your agent's memory. Works with any MCP agent .
Readme MIT
1.5 MiB
0 Stars 1 Watchers 0 Forks
v0.2.1
Latest
2026-11-07 10:03:40 -10:00
Languages
TypeScript 100%