Skip to main content
Glama
README.md
# mem-port

[![npm version](https://img.shields.io/npm/v/@rsl-innovation/mem-port.svg)](https://www.npmjs.com/package/@rsl-innovation/mem-port)

**[mem-port.com](https://mem-port.com/)**

A local MCP (Model Context Protocol) server for portable, long-term agentic memory — a thumb drive for your AI context.

Every AI copilot (Claude Code, Cursor, Windsurf, ...) keeps its own memory, siloed to that tool. The usual workaround — copy-pasting context, summaries, or exported notes from one agent into another — only captures a snapshot frozen at the moment you made it. From there the copies drift: each agent keeps learning on its own, nothing keeps the copies in sync, and the longer you go the more your copilots disagree about what's actually true. mem-port runs as a single local daemon that any number of copilots can connect to, backed by an embedded knowledge graph (entities, episodes, memories, skills, architectural decision records, and the relations between them) that survives restarts and can be exported to a portable file and moved anywhere. Every connected copilot reads and writes the same graph, so there's nothing to paste and nothing to drift.

Unlike other memory-for-agents projects, mem-port needs **no external services** — no Postgres, no Qdrant, no Neo4j. It's one process, one embedded [SurrealDB](https://surrealdb.com) instance combining graph storage and vector search, and zero-config local semantic search (no API key required).

Connecting a client (below) gives it the *ability* to use mem-port; for more detailed, tunable instructions on what it should actually save and when — including keeping personal/team/project memory in separate scopes — see **[MEMORY_GUIDE.md](./MEMORY_GUIDE.md)**.

## Quick start

```bash
npx @rsl-innovation/mem-port serve
```

This starts a daemon on `http://127.0.0.1:8787/mcp`. Point any MCP client at it over Streamable HTTP, with a `library-id` header identifying your workspace. Every copilot that connects with the same `library-id` shares the same memory; different `library-id`s are fully isolated from each other (each maps to its own SurrealDB namespace/database) — there's no cross-tenant leakage.

`npx` re-checks the registry on every invocation. If you'll be running mem-port commands often, install it globally instead so `mem-port` is a plain command on your PATH:

```bash
npm install -g @rsl-innovation/mem-port
mem-port serve
```

The rest of this README uses `mem-port <command>` for brevity. If you didn't install globally, substitute `npx @rsl-innovation/mem-port <command>` wherever you see that — it works identically, just slower to start.

### Connecting from Claude Code

Easiest: use the CLI (`--header` accepts any number of `Key: Value` pairs). Add `--scope user` so the server is available in every project on this machine, not just the one you happen to be in when you run the command — the default `local` scope ties it to a single project directory:

```bash
claude mcp add --transport http mem-port http://127.0.0.1:8787/mcp \
  --header "library-id: my-personal-workspace" \
  --scope user
```

Or add it directly to `~/.claude.json` (user scope, applies everywhere) or `.mcp.json` (project scope, shareable via version control with that repo's team). The `type` field is required — an entry with a `url` but no `type` is treated as a misconfigured stdio server:

```json
{
  "mcpServers": {
    "mem-port": {
      "type": "http",
      "url": "http://127.0.0.1:8787/mcp",
      "headers": { "library-id": "my-personal-workspace" }
    }
  }
}
```

Run `/mcp` inside Claude Code to confirm it shows `mem-port` as connected.

### Connecting from the Claude Code VS Code extension

The extension shares the exact same MCP configuration as the CLI (`.mcp.json` / `~/.claude.json`) — there's no separate settings UI to add a server from. Open the integrated terminal (`` Ctrl+` `` / `` Cmd+` ``) and run the same command as above:

```bash
claude mcp add --transport http mem-port http://127.0.0.1:8787/mcp \
  --header "library-id: my-personal-workspace" \
  --scope user
```

This requires the [standalone `claude` CLI](https://code.claude.com/docs/en/setup) to be installed — the extension bundles its own private copy for the chat panel and does *not* put `claude` on your terminal PATH, so `claude mcp add` won't work in the integrated terminal until you install the CLI separately. Editing `.mcp.json` directly (the JSON block above) works too and doesn't need the CLI.

Once added, type `/mcp` in the chat panel to confirm mem-port shows as connected, or to enable/disable/reconnect it.

### Connecting from other MCP clients

Any client that supports Streamable HTTP with custom headers can connect the same way. Clients that only support stdio-based servers (some Claude Desktop configurations, for example) need a stdio-to-HTTP bridge such as [`mcp-remote`](https://www.npmjs.com/package/mcp-remote):

```json
{
  "mcpServers": {
    "mem-port": {
      "command": "npx",
      "args": ["-y", "mcp-remote@latest", "http://127.0.0.1:8787/mcp", "--header", "library-id:my-personal-workspace"]
    }
  }
}
```

### Getting your copilot to use it proactively

Connecting the server gives your copilot the *ability* to save/recall memory — the server's MCP `instructions` and tool descriptions already nudge any client toward using it proactively. For more explicit control (and for keeping personal/organizational/project memory in separate `library-id` scopes instead of one bucket), see [MEMORY_GUIDE.md](./MEMORY_GUIDE.md) for instructions to paste into your copilot's own custom-instructions file.

## Tools

| Tool | Purpose |
|---|---|
| `save_memory` | Save a fact/preference/decision/task/reference, optionally linked to entities |
| `search_memory` | Semantic (vector) search over memories |
| `save_episode` | Record a raw interaction/event that memories can be derived from |
| `list_episodes` | List recorded episodes, filterable by time range/source |
| `save_skill` | Save a reusable procedure, optionally linked to entities |
| `search_skills` | Semantic (vector) search over skills, by task/situation — descriptions only |
| `list_skills` | List saved skills, filterable by tag/source — descriptions only |
| `get_skill` | Look up a skill by exact name or id |
| `forget_skill` | Soft-archive (default) or permanently delete a skill |
| `save_adr` | Record an architectural decision, optionally superseding an earlier one |
| `search_adrs` | Semantic (vector) search over ADRs, by problem or area |
| `list_adrs` | List the ADR log, filterable by status/tag/source |
| `get_adr` | Look up one ADR in full, by number or id |
| `forget_adr` | Soft-archive (default) or permanently delete an ADR |
| `get_entity` | Look up an entity plus everything that mentions or relates to it |
| `relate_entities` | Create a graph relation between two entities |
| `forget_memory` | Soft-archive (default) or permanently delete a memory |
| `export_library` | Export this library to a portable `.memport.json` bundle |
| `import_library` | Import a `.memport.json` bundle, merging or overwriting |

### Read-only connections

Ten of those tools only read: `search_memory`, `list_episodes`, `get_entity`, `search_skills`, `list_skills`, `get_skill`, `search_adrs`, `list_adrs`, `get_adr` and `export_library`. The other nine can change the library.

A connection can be limited to the read half, and the write tools are then **not registered at all** — a `tools/call` for `save_memory` comes back as an unknown tool, because the server built for that request never had it. Two independent things can ask for this, and the more restrictive wins:

```
grant is read-only   ──▶ read-only, always  (set by an admin; the member cannot opt out)
read-only: 1 header  ──▶ read-only          (set by the client, on itself)
otherwise            ──▶ read-write
```

**Per member.** With [authentication](#authentication-and-the-admin-panel) on, every workspace grant in the admin panel is read-write or read-only. This is the one to reach for when someone should consult a curated library without adding to it — their copilot is never offered the tools, so it cannot write to the workspace even if it decides to.

**Per client.** Any client can drop its own write tools with a `read-only: 1` header next to `library-id`, the same way `mcp-apps: 0` turns off rendered results:

```json
{
  "mcpServers": {
    "mem-port": {
      "type": "http",
      "url": "http://127.0.0.1:8787/mcp",
      "headers": { "library-id": "my-personal-workspace", "read-only": "1" }
    }
  }
}
```

That is useful for a CI job, a shared machine, or a copilot you would rather kept its hands off a library you curate by hand. It can only ever remove tools: a read-only grant stays read-only however the client is configured, and there is no environment variable that turns the whole daemon read-only — with `MEM_PORT_AUTH=off` there are no grants at all, so the header is the only thing in play.

### Rendered results (MCP Apps)

The nine read tools — `search_memory`, `list_episodes`, `search_skills`, `list_skills`, `get_skill`, `search_adrs`, `list_adrs`, `get_adr`, `get_entity` — render their results as cards in hosts that support [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview), instead of showing you the JSON your copilot reads. Lists come back as result cards; `get_*` as a detail view.

Each read tool declares `_meta.ui.resourceUri` pointing at a single `ui://mem-port/results.html` resource. The host fetches that page, renders it in a sandboxed iframe, and pushes the tool result into it — the view model rides on the result's `_meta`, so the card you see and the JSON the model reads come from the same description and cannot disagree.

Supported by [Claude and Claude Desktop](https://claude.com/blog/interactive-tools-in-claude), VS Code Copilot, ChatGPT, Cursor, Goose and others — see the [client matrix](https://modelcontextprotocol.io/extensions/client-matrix). **Claude Code is not among them**, so results stay as text there. The text block is unchanged and always first, so a host that doesn't render MCP Apps behaves exactly as it did.

It is **on by default**. To turn it off, add an `mcp-apps: 0` header next to `library-id` where you configure the client:

```json
{
  "mcpServers": {
    "mem-port": {
      "type": "http",
      "url": "http://127.0.0.1:8787/mcp",
      "headers": { "library-id": "my-personal-workspace", "mcp-apps": "0" }
    }
  }
}
```

Or turn it off for every client at once by starting the daemon with `MCP_APPS=0 mem-port serve`. An explicit `mcp-apps` header wins over the environment in both directions, so one client can opt back in on a daemon that has it off.

## Memories and episodes

**Memories** are the core unit — one durable, self-contained statement worth recalling in a later session that starts from zero context ("User prefers dark mode in all editors"). Each carries a `memory_type` (`fact`, `preference`, `decision`, `task`, or `reference`) that `search_memory` can filter on, and an `importance` from 0 to 1. The type is worth picking deliberately: it's the difference between a searchable library and a flat pile of text — see [MEMORY_GUIDE.md](./MEMORY_GUIDE.md) for how to choose, and for what doesn't belong in memory at all.

**Episodes** are the raw material memories get derived from — a conversation, a debugging session, a meeting — recorded with a `title`, `content`, a `source` (which copilot recorded it) and `occurred_at`. Where a memory is a distilled claim, an episode is an unedited record of something that happened. `save_memory` takes a `source_episode_id`, so a memory can point back at the episode it came from and keep its provenance.

The two answer different questions, which is why both exist: *"what's true about this project?"* is a semantic search over memories, while *"what happened last Tuesday?"* is a chronological read of episodes via `list_episodes` (filterable by time range and source). Memories are what you search; episodes are what you replay.

**Entities** — people, projects, tools — are the connective tissue. Passing `entity_refs` when saving anything links it to those entities, creating them on first mention. `get_entity` then returns every memory, episode, skill, and ADR that mentions the entity plus its related entities, which makes "tell me everything relevant to checkout-service" one lookup instead of several searches. `relate_entities` adds typed edges between entities themselves (`Alice` —leads→ `mem-port`).

## Skills memory

Alongside episodes and memories, mem-port stores **skills** — reusable procedures for recurring tasks (e.g. "how to debug a flaky test in this repo," "the deploy steps for checkout-service"). A skill has a `name`, a `description` (the trigger condition — when a copilot should reach for it, matched by `search_skills`), and `content` (the actual instructions).

`search_skills` and `list_skills` return descriptions and metadata but **not** `content`, and `get_skill` serves the body for the one skill you picked. Since both are meant to be called proactively at the start of a task, returning every procedure body would put the whole library into the model's context to answer "is there a skill for this?" — measured at 68 kB for 21 skills, against 12 kB for the same call now.

Skills are what makes "porting common skills across AI" work with no extra machinery: since they live in the same shared knowledge graph as everything else, a skill saved by Claude Code is immediately visible to Cursor or Windsurf the moment they connect with the same `library-id` — no file format conversion needed. `export_library`/`import_library` carry skills between machines exactly like entities, episodes, and memories.

## ADR log

mem-port also keeps an **ADR log** — architectural decision records, the consequential technical choices whose reasoning matters months later. Each ADR gets a sequential number within its library (`ADR-0001`, `ADR-0002`, ...) and holds the four things a decision record needs: the `context` that forced the decision, the `decision` itself, its `consequences`, and the `alternatives` that lost.

This is deliberately not the same as `save_memory(memory_type: "decision")`. A memory records *that* something was decided; an ADR keeps the problem framing and the rejected options, which is what you actually need when someone proposes the rejected option again a year later. `search_adrs` matches against title + context + decision, so "why aren't we using Postgres?" finds the record even when it shares no words with it.

Decisions get reversed, so ADRs have a lifecycle (`proposed` → `accepted`, then `superseded` or `deprecated`) and a supersede chain. Passing `supersedes` when recording a newer decision — as a record id, a number, or its display form like `ADR-0003` — automatically marks the older one `superseded` and links the two, so the log stays readable from either end rather than accumulating contradictory records.

Prefer superseding an ADR over `forget_adr` — a decision that was reversed is usually worth keeping on the record.

## Porting memory between machines

Same-machine sharing across copilots needs no extra step — they just connect to the same daemon with the same `library-id`. `export_library`/`import_library` solve a different problem: moving to a new machine, backing up, versioning (the bundle is plain JSON — commit it to a private git repo if you like), or handing a curated slice of memory to someone else.

```bash
# on the old machine
mem-port export --library-id my-personal-workspace
# -> writes <data-dir>/exports/my-personal-workspace-<timestamp>.memport.json

# on the new machine, after copying the file over
mem-port import --library-id my-personal-workspace --in ./my-personal-workspace-....memport.json
```

`import` defaults to `--mode merge` (dedupes entities by name+type, memories/episodes/skills/ADRs by content hash — importing the same bundle twice is a no-op). Imported ADRs are renumbered onto the end of the target library's sequence rather than colliding with its existing numbers; supersede links are carried across by record reference, so a chain survives renumbering intact. Pass `--mode overwrite` to wipe the target library first, or `--dry-run` to see what would happen without writing anything.

## Running persistently

`mem-port serve` runs in the foreground — it's a long-lived daemon, not a one-shot command, so it blocks whatever terminal started it and dies when that terminal closes. If your MCP client can't connect (`ECONNREFUSED 127.0.0.1:8787`), that's almost always the reason: nothing is actually listening. Check with `lsof -i :8787`.

For a quick session, background it: `mem-port serve &` (or `nohup mem-port serve > ~/.mem-port.log 2>&1 &` to survive closing the terminal). For something that survives reboots and restarts itself if it ever crashes, set it up as a proper background service.

### macOS (launchd)

```bash
which node        # note this path
which mem-port     # note this path too, then resolve the symlink:
readlink -f "$(which mem-port)"   # -> .../lib/node_modules/@rsl-innovation/mem-port/bin/mem-port.js
```

Write `~/Library/LaunchAgents/com.rsl-innovation.mem-port.plist`, substituting the two paths above. **Invoke `node` directly with the resolved script path — don't point `ProgramArguments` at the `mem-port` shim itself.** `launchd` doesn't inherit your shell's PATH, so the shim's `#!/usr/bin/env node` shebang fails with `env: node: No such file or directory` when launchd runs it:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.rsl-innovation.mem-port</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/node</string>
        <string>/usr/local/lib/node_modules/@rsl-innovation/mem-port/bin/mem-port.js</string>
        <string>serve</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/YOUR_USERNAME/Library/Logs/mem-port.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/YOUR_USERNAME/Library/Logs/mem-port.error.log</string>
</dict>
</plist>
```

```bash
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.rsl-innovation.mem-port.plist   # start now + on every login
launchctl bootout gui/$(id -u)/com.rsl-innovation.mem-port                                  # stop and unregister
tail -f ~/Library/Logs/mem-port.log ~/Library/Logs/mem-port.error.log                       # logs
```

To pick up a new version after `npm install -g @rsl-innovation/mem-port`, restart the running job in place — no need to unload/reload the plist:

```bash
launchctl kickstart -k gui/$(id -u)/com.rsl-innovation.mem-port
```

To stop it and start it again later, use `bootout`/`bootstrap` (above) rather than `launchctl stop` — this plist's `KeepAlive` is unconditionally `true`, so a plain `stop` gets immediately relaunched by launchd. `bootout` actually unregisters the job, and `bootstrap` registers and starts it again.

### Linux (systemd --user)

```ini
# ~/.config/systemd/user/mem-port.service
[Unit]
Description=mem-port

[Service]
ExecStart=/usr/bin/node /path/to/lib/node_modules/@rsl-innovation/mem-port/bin/mem-port.js serve
Restart=on-failure

[Install]
WantedBy=default.target
```

```bash
systemctl --user enable --now mem-port
journalctl --user -u mem-port -f
```

## Configuration

| Env var | Default | Purpose |
|---|---|---|
| `MEM_PORT_PORT` | `8787` | HTTP port |
| `MEM_PORT_DATA_DIR` | OS-appropriate app data dir | Where the SurrealDB store and cached embedding model live |
| `MEM_PORT_EMBEDDING_MODEL` | `Xenova/all-MiniLM-L6-v2` | Local embedding model id (reserved for future use) |
| `MEM_PORT_MODEL_CACHE_DIR` | `<data-dir>/models` | Override the embedding model cache location |
| `MCP_APPS` / `MEM_PORT_MCP_APPS` | on | Set to `0` to stop the read tools declaring an [MCP Apps](#rendered-results-mcp-apps) UI |

By default all state lives under one data directory — the SurrealDB store (`surrealkv://`, persistent across restarts) and the cached local embedding model. Delete the data dir to fully reset.

### Using a hosted SurrealDB

Point mem-port at an existing SurrealDB server instead of the embedded engine:

| Env var | Default | Purpose |
|---|---|---|
| `MEM_PORT_DB_URL` | `surrealkv://<data-dir>/memport.db` | Database URL. A `ws://` or `wss://` URL selects the hosted driver |
| `MEM_PORT_DB_NAMESPACE` | `memport` | Namespace holding one database per library-id |
| `MEM_PORT_DB_USER` / `MEM_PORT_DB_PASS` | — | Credentials. Required for a hosted server |
| `MEM_PORT_DB_TOKEN` | — | Bearer token, as an alternative to user/password |
| `MEM_PORT_DB_PREFIX` | none | Prefix for tenant database names, on a cluster shared with other apps |
| `MEM_PORT_DB_MAX_SESSIONS` | `256` | Cached per-library sessions before the least recently used is closed |

```bash
MEM_PORT_DB_URL=wss://your-instance.surreal.cloud \
MEM_PORT_DB_USER=root \
MEM_PORT_DB_PASS=... \
  mem-port serve
```

Three constraints worth knowing before you point this at a cluster. Each is checked at startup, so a misconfiguration fails once with an explanation rather than on every tool call:

- **SurrealDB 3.0 or newer.** Sessions and transactions are both server-side 3.0 features, and mem-port needs both — a forked session per library-id for tenancy, and a transaction for `import_library`. A 2.x server connects fine and then fails on every request.
- **WebSocket only.** SurrealDB's HTTP engine supports neither of those features regardless of server version, so an `http(s)://` URL is rejected.
- **The user must be root- or namespace-level.** mem-port creates a database per library-id inside its namespace and defines that database's schema on first use, which a database-scoped user cannot do.

### Accounts and the admin panel

By default mem-port has no accounts: it binds loopback, and the operating system
is the boundary. That is right for a personal daemon and wrong the moment the
daemon is reachable from anywhere else, so authentication switches on with
exposure — off on loopback, required on any other interface, and
`MEM_PORT_AUTH` overrides either way.

With auth on, an admin panel is served at `/admin`. Sign in with the bootstrap
admin (`MEM_PORT_ADMIN_USER` / `MEM_PORT_ADMIN_PASSWORD`, used only while no
admin exists) and from there:

- **create workspaces** — a workspace is one isolated knowledge graph, and its
  name is what clients send as `library-id`
- **create users**, and issue each an API key (shown once; only a hash is kept)
  with revocation when a key needs rotating
- **grant a user access to specific workspaces**, each grant read-write or
  read-only — a read-only member is served only the ten read tools, so their
  copilot has no way to write to that workspace (see
  [Read-only connections](#read-only-connections)). Each user also carries a
  default level that pre-selects the choice when you grant them a workspace.
- **explore a workspace's graph** — a read-only view of what a workspace holds
  and how its entities connect, which is the quickest way to check whether a
  newly connected client is actually writing anything

The panel also serves its own documentation at `/admin/docs`, covering both the
portal and the product, with copy-pasteable client configuration for the URL the
admin actually reached it on.

Clients then send two headers, and nothing else changes:

```
Authorization: Bearer <the user's key>
library-id: <a workspace they were granted>
```

Upgrading an existing deployment changes nothing on its own: grants that predate
this keep read-write access until an admin says otherwise.

Being an admin does not confer data access — admins decide who may reach what,
which is a different power from reading it, so an admin who wants a workspace
grants it to themselves.

### Deploying

Container image, a local Compose stack, and Cloud Run manifests live in
[`deployments/`](deployments/). Configuration is documented in
[`.env.example`](.env.example).

Two things change when mem-port stops running on localhost, both covered there:
a hosted database becomes required (the embedded engine loses data on ephemeral
filesystems and lets replicas diverge), and the loopback bind that currently
serves as the security boundary goes away — mem-port has no authentication of
its own, so something else has to provide one. The supplied manifests default to
closed for that reason.

### Using Postgres instead

mem-port ships two storage drivers. The default is embedded SurrealDB, which
needs nothing installed. The alternative is Postgres with
[pgvector](https://github.com/pgvector/pgvector):

```bash
npm install pg                      # optional dependency, only for this driver
MEM_PORT_DB_URL=postgres://user:pass@host:5432/memport mem-port serve
```

Each workspace gets its own Postgres schema, so isolation is structural rather
than a `WHERE` clause. pgvector is required — every search mem-port offers is a
cosine similarity over an embedding — and mem-port attempts `CREATE EXTENSION`
itself, which works on most managed services where it is available but not
enabled.

The two drivers are interchangeable, and that is enforced rather than claimed:
[`test/crossDriver.test.ts`](test/crossDriver.test.ts) seeds the same fixture
through both and asserts every read tool returns **byte-identical** output.

### Adding another database

Storage sits behind a contract in [`src/interfaces/`](src/interfaces/), expressed in domain terms — `store.skills.search(vector, filter)`, `store.entities.detail({ name })` — with no query language, record-id objects or graph syntax in it. Each engine is confined to its own directory (`src/db/surreal/`, `src/db/postgres/`); nothing under `src/mcp/`, `src/port/` or `src/services/` imports a driver.

To add one:

1. Implement [`LibraryStore`](src/interfaces/store.interface.ts) and its seven sub-stores, plus [`StoreProvider`](src/interfaces/provider.interface.ts), under `src/db/<engine>/`.
2. Add a case to [`createStoreProvider`](src/db/createStoreProvider.ts) and a driver value in [`src/config.ts`](src/config.ts).

The obligations worth reading twice are the ones the Postgres driver had to be careful about: ids and timestamps cross the boundary as strings; **unset optional fields stay absent rather than becoming `null`** (SurrealDB returns `undefined`, Postgres returns an explicit null, and `JSON.stringify` treats them differently); `search` ranks by cosine similarity and excludes rows with no embedding; `entities.detail` answers a four-way fan-in without an N+1; and `transaction(fn)` rolls back on `Rollback` while still returning its payload.

Point `crossDriver.test.ts` at a new driver and it will tell you whether the contract actually holds.

## Development

```bash
npm install
npm run dev        # start the daemon with tsx, no build step
npm test           # vitest: golden output, tenancy, skills, ADRs, export/import round-trip
                   # the hosted-SurrealDB suite needs Docker; it skips (with a warning) without it
npm run typecheck
npm run build       # tsup -> dist/, what npx @rsl-innovation/mem-port actually runs
```

`scripts/smoke.sh` is a plain-curl smoke test against a already-running daemon (no Node/Inspector dependency, usable in CI):

```bash
npm run dev &
./scripts/smoke.sh
```

### Releasing

Releases are automated. Bumping the version creates the commit and the `vX.Y.Z` tag; pushing the tag is what triggers everything else:

```bash
npm version patch   # or minor / major — runs typecheck + tests first
git push --follow-tags
```

The `Release` workflow then verifies the tag matches `package.json`, re-runs typecheck and tests, publishes to npm (with provenance, via OIDC trusted publishing — no token stored anywhere), and creates the GitHub Release with notes generated from the commits since the previous tag.

Do not run `npm publish` by hand; a tag push is the only supported path.

## Gotchas

- **mem-port is a localhost server — it only works with clients running on the same machine.** Web/cloud-hosted chat sessions (e.g. chatgpt.com or claude.ai in a browser tab) run server-side and have no route to `127.0.0.1` on your computer, so they can't reach mem-port no matter how it's configured. To connect ChatGPT, Claude, or similar tools, install their **desktop app** and add mem-port there — the desktop app runs locally and can reach the daemon, whereas the same account's web session cannot.
- Claude Code's CLI and VS Code extension both run locally already, so they work out of the box (see the connection instructions above) — this gotcha mainly matters for tools you might otherwise only use through a browser.

## Known limitations (v1)

- Vector search is brute-force (no HNSW/DISKANN index yet) — fine at personal-memory-store scale, revisit if a library grows very large.
- `export_library`'s scope filtering supports `memory_types` and `since`; filtering by `entity_ids` isn't implemented yet.
- No authentication — the daemon binds to `127.0.0.1` only and trusts anything running locally on your machine.
- `@huggingface/transformers`' bundled `onnxruntime-node`/`sharp` carry known transitive advisories (ZIP/image parsing libs) with no upstream fix yet. mem-port never feeds them untrusted input, but `npm audit` will flag them.