Metadata-Version: 2.4
Name: local-memory
Version: 0.2.0
Summary: Local-first long-term memory MCP server for AI agents. SQLite, offline, zero cloud.
Author: local-memory contributors
License: MIT
Project-URL: Repository, https://github.com/Sergey-Vladimirovich-Hankok/local-memory
Keywords: memory,mcp,sqlite,ai-agents,local-first,offline
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Database
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: numpy>=1.24
Requires-Dist: mcp<2,>=1.0
Provides-Extra: semantic
Requires-Dist: scikit-learn>=1.3; extra == "semantic"
Provides-Extra: vector
Requires-Dist: sqlite-vec>=0.1.6; extra == "vector"
Dynamic: license-file

# local-memory

**Local-first long-term memory for AI agents. SQLite, offline, zero cloud.**

> Your agent's memory stays on your machine. SQLite. No cloud. No API keys.
> Works offline.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![CI](https://img.shields.io/badge/CI-pytest-green.svg)](.github/workflows/ci.yml)

local-memory is a standalone [MCP](https://modelcontextprotocol.io) server
(stdio by default, plain REST and MCP-over-HTTP optional) that gives any AI
agent durable long-term memory: search over past sessions (keyword, semantic,
exact), a catalog of sessions, full session reads, and a write channel for new
memories. It works with Claude Code, opencode, Cursor, Cline, Codex, or any
other MCP client — or with plain `curl` over REST.

The engine is small and boring on purpose: one SQLite file, the Python
standard library, and numpy. The only other dependency is the `mcp` package,
which powers the MCP transports; the REST transport is stdlib-only.

---

## Why

AI agents are stateless by default. Every new session starts from zero: the
decisions made yesterday, the bugs already chased away, the project's
conventions, the exact error message you fixed last week — all of it is gone
unless you copy-paste it back in by hand.

The usual fix is a cloud memory API. But that means your conversations —
often your most sensitive work context — are stored, indexed, and processed on
someone else's servers. For local work that is a bad trade: the "memory" you
paid for is also the leak you did not want.

local-memory flips the trade. The memory is a plain SQLite database on your
machine, readable with any SQL client, back-upable with any backup tool, and
searchable with your own embeddings model (or without any model at all). The
agent gets the memory interface it wants; you keep the data you already had.

---

## How it works

```
                 +------------------------------------------------+
                 |              your machine (offline)            |
                 |                                                |
   MCP client     |   +----------------+          +-------------+  |
   (Claude Code,  |   |  local-memory  |  stdio   |   SQLite    |  |
    opencode,     +-->|  MCP server    |<-------->|  memory.db  |  |
    Cursor, ...)   |   |  or REST /    |          |  + FTS5     |  |
    via stdio,     |   |  MCP-HTTP     |          |  + vec tbl  |  |
    HTTP, MCP-HTTP |   +-------+-------+          +------+------+ |
                 |           |                              ^      |
                 |           | embeddings (optional,        |      |
                 |           | only if YOU configure it)    |      |
                 |           v                              |      |
                 |   +----------------+          +-------------+  |
                 |   | embeddings API |          | numpy matrix |  |
                 |   | (OpenAI-style) |          | cache (.npy) |  |
                 |   +----------------+          +-------------+  |
                 +-------------------------------------------------+
```

One database, three search engines, chosen automatically:

| Mode      | When used                              | How                                    |
|-----------|----------------------------------------|----------------------------------------|
| exact     | query looks like code (`a=b`, `fn(x)`) | literal LIKE, then FTS5                |
| keyword   | default for `memory_search`            | FTS5 with prefix expansion, LIKE safe  |
| semantic  | `memory_search_semantic`               | embeddings -> TF-IDF -> keyword        |

Every result carries a **context window**: the neighboring chunks around the
hit, so the agent sees what came before and after without an extra round
trip.

Transports: stdio (default), plain REST, or MCP streamable-http —
`serve --transport stdio|http|mcp-http`. Vector search: numpy matmul over
the `.npy` cache, or sqlite-vec inside the database (extra `[vector]`).

---

## Tools

| Tool                      | Args                                        | Returns |
|---------------------------|---------------------------------------------|---------|
| `memory_overview`         | `days=7`                                    | recent sessions: id, title, counts, times |
| `memory_search`           | `query`, `limit=5`                          | keyword hits + context, engine used      |
| `memory_search_semantic`  | `query`, `limit=5`                          | semantic hits + context, engine used     |
| `memory_fetch`            | `session_id`, `position=0`, `limit=20`      | consecutive chunks of one session        |
| `memory_status`           | —                                           | db statistics (counts, size, cache)      |
| `memory_ingest`           | `session_id`, `content`, `project`, `metadata` | write result (position, ok)          |

Result rows (a public contract — stable across versions):

```json
{
  "session_id": "alpha",
  "position": 3,
  "content": "the postgres connection pool timed out under load",
  "score": 0.83,
  "context": [
    {"position": 2, "content": "..."},
    {"position": 3, "content": "the postgres connection pool timed out under load"},
    {"position": 4, "content": "..."}
  ]
}
```

## Quickstart

### 1. Install

```bash
pip install local-memory
```

Want the optional extras?

```bash
pip install "local-memory[semantic]"   # + TF-IDF fallback (scikit-learn)
pip install "local-memory[vector]"     # + in-database vector search (sqlite-vec)
```

Both extras are optional; the core works with just numpy + `mcp`. Prefer a
local checkout? Clone and `pip install ".[semantic]"` from the repository
instead.

### 2. Initialize the database

```bash
local-memory init          # creates ~/.local/share/local-memory/memory.db
local-memory status        # {"exists": true, "sessions": 0, ...}
```

### 3. Write a memory

```bash
local-memory ingest --session-id demo --project myproject \
  --text "the postgres connection pool timed out under load, raised max size"
```

### 4. Search it

```bash
local-memory search "postgres connection pool" -k 5
local-memory search "pool timeout" --semantic
```

### 5. Connect an MCP client

MCP clients can run it with `uvx` — it fetches and caches the package for
you:

```bash
uvx local-memory serve
```

Claude Code:

```bash
claude mcp add local-memory -- uvx local-memory serve
```

opencode (`opencode.json`):

```json
{
  "mcp": {
    "local-memory": {
      "command": "uvx",
      "args": ["local-memory", "serve"]
    }
  }
}
```

Cursor / any generic MCP client (`mcpServers` snippet):

```json
{
  "mcpServers": {
    "local-memory": {
      "command": "uvx",
      "args": ["local-memory", "serve"]
    }
  }
}
```

> Already installed with `pip` in step 1? You can skip `uvx` and point
> `command` at the installed binary directly:
> `"command": "local-memory", "args": ["serve"]` (see `examples/generic_mcp.json`).

Ready-made snippets live in [`examples/`](examples/) — or generate one for
your client without copy-paste errors (tests guard the output against drift
from `examples/`):

```bash
local-memory config --client claude      # or: opencode | cursor | generic
local-memory config --client claude --setup --dry-run   # print the registration command
```

---

## Configuration

Everything is configured via environment variables. There is no config file —
one process, one database, zero moving parts.

| Variable             | Default                                  | Meaning |
|----------------------|------------------------------------------|---------|
| `MEMORY_DB_PATH`     | `~/.local/share/local-memory/memory.db`  | path to the SQLite database |
| `EMBED_API_URL`      | *(empty = embeddings disabled)*          | OpenAI-compatible `/v1/embeddings` endpoint |
| `EMBED_MODEL`        | `local-embedder`                         | model name sent to the embeddings API |
| `EMBED_OFF`          | *(unset)*                                | `1` forces keyword/TF-IDF only |
| `MEMORY_CTX_WINDOW`  | `3`                                      | context window width (neighbors per side) |
| `MEMORY_MAX_CHUNKS`  | `1000000`                                | cap on rows scanned per search |
| `MEMORY_MATRIX_CAP`  | `5000`                                   | max new chunks embedded per call |
| `MEMORY_HTTP_HOST`   | `127.0.0.1`                              | bind address for `serve --transport http` |
| `MEMORY_HTTP_PORT`   | `8787`                                   | port for `serve --transport http` |
| `MEMORY_HTTP_TOKEN`  | *(unset)*                                | bearer token for HTTP; **required** when binding off-loopback |
| `MEMORY_VECTOR_BACKEND` | `auto`                                | vector search backend: `auto` \| `npy` \| `sqlitevec` (extra `[vector]`) |

---

## Ingesting memory

`memory_ingest` / `local-memory ingest` is the write channel. A client hook or
plugin calls it once per message (or chunk) it wants the agent to remember.
Positions auto-increment per session; the FTS index is updated by a database
trigger, so ingested text is searchable immediately.

CLI:

```bash
local-memory ingest --session-id my-project --project my-project \
  --text "decided: use SQLite WAL mode for the memory db" \
  --metadata '{"source": "standup", "date": "2026-10-06"}'
local-memory ingest --session-id my-project --file notes.md
```

MCP tool:

```json
{
  "session_id": "my-project",
  "content": "decided: use SQLite WAL mode for the memory db",
  "project": "my-project",
  "metadata": {"source": "standup"}
}
```

Example hook script (call from your agent's post-message hook):

```bash
#!/usr/bin/env bash
# remember.sh — feed one message into local-memory
local-memory ingest \
  --session-id "project-${1}" \
  --text "${2}" \
  --project "hooks" >/dev/null
```

---

## Semantic search (optional)

`memory_search_semantic` tries, in order:

1. **Embeddings** — any OpenAI-compatible `/v1/embeddings` endpoint
   (llama.cpp `llama-server`, Ollama, vLLM, a local transformer service...).
   Point `EMBED_API_URL` at it; vectors are cached on disk next to the
   database (`embed_cache_*.npy`) and reused, so repeated queries are fast.
2. **TF-IDF** — character n-gram cosine similarity, offline, needs the
   optional `scikit-learn` extra.
3. **Keyword fallback** — the exact/FTS5/LIKE engines, with
   `"engine": "keyword-fallback"` in the response.

Prebuilding the cache (only needed for the embeddings path):

```bash
export EMBED_API_URL=http://localhost:8080/v1/embeddings
export EMBED_MODEL=my-local-embedding-model
local-memory build            # embed all chunks once
```

If no embeddings endpoint is configured, semantic search silently degrades to
TF-IDF/keyword. Nothing is sent over the network unless you configure a
`EMBED_API_URL`.

### Vector storage: `.npy` or sqlite-vec

The embedding matrix lives next to the database as
`embed_cache_<dbname>_<model>.{ids,mat}.npy` (numpy matmul — the permanent
fallback). The cache key includes the database filename, so multiple databases
in the same directory never share vectors.
With the optional `[vector]` extra, the same vectors can live **inside the
SQLite database** via [sqlite-vec](https://github.com/asg017/sqlite-vec):

```bash
pip install ".[vector]"                    # sqlite-vec extension
local-memory migrate-vectors               # copy the .npy cache into the vec table
local-memory migrate-vectors --remove-npy  # ...and drop the .npy files
```

Backend selection: `MEMORY_VECTOR_BACKEND=auto` (default — sqlite-vec when
installed and populated, otherwise `.npy`), `npy`, or `sqlitevec` (force;
fails fast if the extension is missing). New chunks are mirrored to the vec
table automatically. Both backends stay 100% local files.

---

## HTTP / REST mode

Besides stdio, `serve` can run over the network — two extra transports,
zero new dependencies (stdlib `ThreadingHTTPServer`):

```bash
local-memory serve --transport http      # plain REST (curl-friendly), port 8787
local-memory serve --transport mcp-http  # MCP streamable-http, port 8000
```

REST endpoints (JSON, same shapes as the CLI/MCP tools):

| Endpoint         | Description |
|------------------|-------------|
| `GET /health`    | liveness + version |
| `GET /status`    | database statistics |
| `GET /overview`  | recent sessions (`?days=7`) |
| `POST /search`   | `{"query", "limit"?, "semantic"?}` |
| `POST /ingest`   | `{"session_id", "content", "project"?, "metadata"?}` |
| `POST /fetch`    | `{"session_id", "position"?, "limit"?}` |
| `GET /tools`     | MCP tools manifest |

Security: loopback binds (`127.0.0.1`, `::1`, `localhost`) work without a
token. Binding any other address **requires** a token (`--token` or
`MEMORY_HTTP_TOKEN`) — otherwise the server refuses to start (fail fast).
With a token set, every request must send
`Authorization: Bearer <token>` or `X-Token: <token>` (constant-time
compare). No TLS — put a reverse proxy or SSH tunnel in front for anything
beyond loopback.

Example:

```bash
local-memory serve --transport http &
curl -s localhost:8787/health
curl -s -X POST localhost:8787/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "postgres connection pool", "limit": 5}'
```

---

## Privacy

- The database, FTS index, and embedding caches are plain files under
  `MEMORY_DB_PATH` (default `~/.local/share/local-memory/`). Back them up or
  delete them like any other file.
- No telemetry, no update checks, no phone-home of any kind.
- The only **outgoing** network access in the entire codebase is the
  embeddings call, and it only happens if you set `EMBED_API_URL` yourself.
- The HTTP/REST transport is inbound-only (you start it, you bind it) and
  defaults to loopback; off-loopback binding is refused without a token.
- `memory_ingest` stores content as-is, unencrypted, in SQLite. If your data
  is sensitive, protect the file with filesystem permissions.
- Nothing is ever sent to the authors of this project.

---

## Testing

The test suite runs fully offline against a synthetic in-memory-style fixture
database (three fake sessions, ~30 chunks). No real conversation data is read,
no network is touched.

```bash
python3 -m pytest tests/ -q
```

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). Short version: local-first is the
product promise; keep dependencies light; keep fixtures synthetic; add tests.

## Security

See [SECURITY.md](SECURITY.md). Report vulnerabilities privately via the
repository's Security tab — not in public issues.

## License

[MIT](LICENSE) — do what you want, just keep the notice.

Attribution — if you use or copy this code, keep the author notice in the
source headers, the repository link
(https://github.com/Sergey-Vladimirovich-Hankok/local-memory), and the
contact email (kokgfnu@gmail.com). MIT license covers permissions;
attribution keeps the work traceable. See [NOTICE](NOTICE).

---

## Roadmap

- PyPI release (for now the git repository is the only source).
- Qdrant backend for the embeddings matrix (sqlite-vec already ships —
  extra `[vector]`).
- Plugin packages for specific agents (Claude Code, opencode, Cursor) —
  `local-memory config --client ...` already generates the snippets.
- Session summaries via any local LLM (opt-in, offline).
- Done: REST mode (`serve --transport http` / `mcp-http`), sqlite-vec vector
  backend, per-client config generator.
- Not on the roadmap: cloud sync, web UI, Rust rewrites. If it must be local,
  it stays local.

## Disclaimer

local-memory is provided for educational and personal use. You are responsible
for what you ingest into it and for protecting the resulting database file.
The authors are not liable for lost, leaked, or misused data — the database is
yours, and so is the responsibility.

