# SynaptoMind [![CI](https://github.com/zumik3-del/synaptomind/actions/workflows/ci.yml/badge.svg)](https://github.com/zumik3-del/synaptomind/actions/workflows/ci.yml) [![CodeQL](https://github.com/zumik3-del/synaptomind/actions/workflows/codeql.yml/badge.svg)](https://github.com/zumik3-del/synaptomind/actions/workflows/codeql.yml) [![Security Audit](https://github.com/zumik3-del/synaptomind/actions/workflows/security.yml/badge.svg)](https://github.com/zumik3-del/synaptomind/actions/workflows/security.yml) [![Coverage Status](https://coveralls.io/repos/github/zumik3-del/synaptomind/badge.svg?branch=main)](https://coveralls.io/github/zumik3-del/synaptomind?branch=main) [![GitHub Release](https://img.shields.io/github/v/release/zumik3-del/synaptomind?label=latest)](https://github.com/zumik3-del/synaptomind/releases) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Docker](https://img.shields.io/badge/docker-ghcr.io-blue?logo=docker)](https://ghcr.io/zumik3-del/synaptomind) [![Bun](https://img.shields.io/badge/bun-runtime-%23000000?logo=bun)](https://bun.sh) **Local persistent memory for AI agents.** Your agent remembers decisions, goals, and context across sessions — over MCP (stdio or HTTP). Data stays on your machine. The HTTP listener on port 3005 is an ops plane (health and graph repair), not a second agent API.

SynaptoMind

**Works with** Claude Desktop · Cursor · OpenCode · Codex · any MCP client --- ## Quick Start ```bash curl -fsSL https://raw.githubusercontent.com/zumik3-del/synaptomind/main/deploy/install.sh | bash ``` A piped script has no sibling files, so the shared helpers and config are fetched from the published base by default — no environment variables are needed. `DEPLOY_RAW_URL` overrides that base (forks/mirrors); an explicit `APP_ENV_URL` or `LIB_RAW_URL` still wins. The installer downloads and extracts the release tarball (`DIST=binary`); the server starts on `http://127.0.0.1:3105` (HTTP ops) and `http://127.0.0.1:3106/mcp` (MCP). Config lives in `/var/lib/synaptomind/config.json`, reached via the `/opt/synaptomind/config.json` symlink. The installer seeds ports 3105/3106; the client-config and endpoint examples in this README use the built-in defaults (3005 HTTP ops / 3006 MCP). For the full install & update guide (flags, channels, rollback, uninstall), see [docs/DEPLOY.md](docs/DEPLOY.md). For the deployment convention shared with ziptask and subagentix — canonical layout, unit policy, the two distribution modes and the per-app `app.env` — see [docs/DEPLOY-LAYOUT.md](docs/DEPLOY-LAYOUT.md). Connect your client — add to Claude Desktop config (`claude_desktop_config.json`): ```json { "mcpServers": { "synaptomind": { "url": "http://127.0.0.1:3006/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ``` The installer generates `SYNAPTOMIND_SECRET` into `/opt/synaptomind/.env`; read it with `sudo grep SYNAPTOMIND_SECRET /opt/synaptomind/.env`. That's it — your agent now has persistent memory. --- ## Use Cases - **Coding agent memory** — architecture decisions, bug root causes, TODOs persist between sessions - **Project journal** — decisions and goals as a connected graph, not scattered notes - **Idea graph** — capture thoughts, let semantic search find connections you missed - **MCP memory backend** — plug persistent memory into any MCP-compatible agent --- ## Example: Auth Decision Across Sessions ``` You: "Remember: we chose JWT for auth, refresh tokens in httpOnly cookies" Agent: [memory_store action=create — tags: auth, jwt, security] --- new session --- You: "What did we decide about auth?" Agent: [memory_recall action=search "auth decision" → finds JWT thought with full context] You: "What should I do next?" Agent: [memory_status action=frontier → "Implement refresh token rotation" ranked #1] ``` Core loop: **capture → link → retrieve → act**. No manual organization — the graph connects related thoughts automatically. ```mermaid sequenceDiagram participant U as You participant A as Agent participant S as SynaptoMind U->>A: "Remember JWT for auth" A->>S: memory_store(action=create, content, tags) S-->>A: thought_id Note over U,A: --- new session --- U->>A: "What about auth?" A->>S: memory_recall(action=search, "auth") S-->>A: JWT thought + context chain A->>S: memory_status(action=frontier) S-->>A: ranked next actions ``` ## Features
Core concepts — what makes this more than a note app | Concept | What it does | |---------|-------------| | **Thoughts** | Individual notes/ideas. Each gets an embedding for semantic search and links to other thoughts. | | **Slots** | Context windows summarizing your state: pending items, project context, active goals and architecture decisions. Agents read these on startup. | | **Frontier** | Ranks "what to do next" over `directive`/`todo`/`pending` thoughts. Deferred pending items join once their `wake_days` delay has elapsed. | | **Primer** | Compact project summary for quick context injection. Promotes the most relevant thoughts into one document. | | **Clusters** | Group related thoughts; inspect and maintain their graph membership. |
Technical capabilities - **Graph storage** — thoughts, edges, projects, tags in SQLite - **Hybrid search** — vector (vec0) + BM25 (FTS5) via Reciprocal Rank Fusion - **Local embeddings** — `@huggingface/transformers`, no API keys - **MCP server** — stdio + HTTP transport - **Auto-clustering** — batch grouping by embedding proximity - **Background jobs** — decay, proposal retention
--- ## Architecture SynaptoMind runs as a single Bun process: the HTTP server on port 3005 (health and health-check ops endpoints), the MCP server on port 3006 (stdio or HTTP), and a child process that serves local embeddings over IPC. Every thought operation is served over MCP; the HTTP listener is an ops plane, documented in [docs/API.md](docs/API.md). Storage is one SQLite database using the `vec0` extension for vector search, FTS5 for full-text search, and an edges table for the graph. ```mermaid graph LR A[AI Agent] -->|MCP| B[SynaptoMind] O[Ops / monitoring] -->|HTTP health| B B --> C[(SQLite)] C --> D[vec0 — vector search] C --> E[FTS5 — full-text search] C --> F[Graph — edges & links] ``` --- ## Connecting MCP Clients
Claude Desktop Edit `~/Library/Application Support/Claude/claude_desktop_config.json`: ```json { "mcpServers": { "synaptomind": { "url": "http://127.0.0.1:3006/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ```
Cursor Add to `.cursor/mcp.json` in your project or global config: ```json { "mcpServers": { "synaptomind": { "url": "http://127.0.0.1:3006/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ```
OpenCode Add to `~/.config/opencode/opencode.json`: ```json { "mcp": { "synaptomind": { "type": "remote", "url": "http://127.0.0.1:3006/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ```
Stdio transport For clients that prefer stdio: ```json { "command": "bun", "args": ["run", "/path/to/synaptomind/src/index.ts", "--stdio"], "env": { "SYNAPTOMIND_SECRET": "your-token" } } ``` By default the stdio process is a **single-owner client**: it opens an MCP session against the shared database but does **not** start its own embedder child process or the decay / proposal-retention jobs at startup. The standalone HTTP server owns the scheduled jobs. An embedding request can still start a local child on demand; stdio does not reuse the HTTP server's embedder process. If you run stdio with no separate server, opt into local ownership with the `--stdio-standalone` flag (`args: ["run", "...", "--stdio", "--stdio-standalone"]`) or `SYNAPTOMIND_MCP_STDIO_STANDALONE=true` / `"mcp": { "stdioStandalone": true }` in `config.json`.
Codex (OpenAI) See [docs/codex-plugin.md](docs/codex-plugin.md) for installation and usage.
ChatGPT (OpenAI Secure MCP Tunnel) Connect web ChatGPT to a private SynaptoMind MCP server without exposing an inbound network port. See [docs/secure-mcp-tunnel.md](docs/secure-mcp-tunnel.md) for setup, validation, and the security boundary.
--- ## Configuration All settings in `config.json`. Priority: env vars > config.json > defaults. The HTTP listener port resolves in that order too: `SYNAPTOMIND_PORT` overrides `config.json` `server.port`, which overrides the built-in `3005` (default `src/config.ts:58`, env mapping `src/config.ts:138`, applied in `applyEnvOverrides()` `src/config.ts:283-301`). | Setting | Default | Description | |---------|---------|-------------| | `server.port` | 3005 | HTTP ops port (health, health-check) | | `server.host` | 127.0.0.1 | Bind address | | `mcp.httpPort` | 3006 | MCP HTTP transport port | | `mcp.stdioStandalone` | false | Let a `--stdio` process own the embedder + background jobs (default: shared HTTP server owns them) | | `embedder.model` | Xenova/multilingual-e5-small | HuggingFace embedding model | | `db.path` | ./data/synaptomind.db | SQLite database path | Auth tokens via env vars only: | Env var | Purpose | |---------|---------| | `SYNAPTOMIND_SECRET` | Bearer token in single-user mode (`users: []`, the default) | | `SYNAPTOMIND_USER_TOKEN_` | Per-user bearer token when `config.json` has a `users` list; `SYNAPTOMIND_SECRET` is then inert | In multi-user mode every HTTP request must present one of the configured users' tokens and is served that user's database. Without a token the server fails closed: authenticated API and MCP requests are rejected with `401` and a warning is logged at startup. For local development only, `SYNAPTOMIND_ALLOW_INSECURE=true` disables auth entirely in single-user mode. Multi-user: `config.json` may carry a `users` list — one SQLite database and one bearer token per user, all served by the same process. Adding, rotating and removing users, and switching an existing install over, is in [CONFIG.md § Users](docs/CONFIG.md#users-multi-user). See `config.json.example` for all options. Full reference: [docs/CONFIG.md](docs/CONFIG.md). Custom MCP instructions: [docs below](#custom-instructions). ---
Docker ### From source (development) ```bash docker compose up -d --build ``` Volumes mount `./data` and `./config.json`. ### Published image (production) The compose file resolves the image via the `SYNAPTOMIND_IMAGE` variable (default `:local`, built from source): ```bash SYNAPTOMIND_IMAGE=ghcr.io/zumik3-del/synaptomind:latest docker compose pull && docker compose up -d ``` Tagged releases publish `ghcr.io/zumik3-del/synaptomind:` (and `:latest` for non-prereleases) via the release workflow; the `deploy/` framework does not manage containers. ### Container user The container runs as a non-root user (uid/gid `10001`). Make sure the mounted paths are writable/readable by that uid: ```bash sudo chown -R 10001:10001 data # required once when upgrading from older (root-run) images chmod 644 config.json # config.json must be readable by uid 10001 ``` ### Auth ```bash echo "SYNAPTOMIND_SECRET=your-secret-token" > .env ``` See `.env.example` for all variables. Without a token (`SYNAPTOMIND_SECRET` in single-user mode, or a configured user's token when `config.json` has a `users` list) the server fails closed — authenticated requests are rejected with `401`. Use `SYNAPTOMIND_ALLOW_INSECURE=true` for local development only (single-user mode). For full Docker guide (updating, backup, troubleshooting), see [docs/DOCKER.md](docs/DOCKER.md).
Storing and recalling: MCP examples Every thought operation is an MCP tool call (stdio or `POST /mcp` on port 3006). The HTTP listener serves the ops endpoints only — `/health` and the two `/api/health-check` routes — and answers `404` for everything else; see [docs/API.md](docs/API.md). ```text Agent: [memory_store action=create content="First session went well" tags=["retrospective"] cwd="/path/to/project"] Agent: [memory_recall action=search query="session retrospective" limit=5 cwd="/path/to/project"] ``` Arguments, envelopes and per-action examples: [docs/mcp-tools.md](docs/mcp-tools.md). ### Health check ```bash curl http://127.0.0.1:3005/health ``` ### Graph health report ```bash curl "http://127.0.0.1:3005/api/health-check?severity=warning" \ -H "Authorization: Bearer $SYNAPTOMIND_SECRET" ```
MCP tools | Category | Tools | |----------|-------| | Recall | `memory_recall` (search, get, context, chain, clusters, list) | | Store | `memory_store` (create, update, link, link_batch, unlink, unlink_batch, retype, bulk_create) | | Supersede | `memory_supersede` (archive, archive_batch, merge) | | Status | `memory_status` (slots, frontier, config, health, propose) | | Projects | `memory_manage` (list, create, update, delete, resolve) | | Consolidate | `memory_crystallize` (graph, cluster, auto_cluster) | | Reflect | `memory_reflect` (reflect, timeline) | | Primers | `memory_primers` (list, delete) | | Review | `edge_review` (enqueue, list, apply, apply_batch, rollback, reject) | | Retention | `retention_review` (list, protect, delete, compact) | | Guide | `memory_guide` | Every action answers in one response envelope — `{ items, meta }` for collections, `{ item, meta }` for single objects — and the project-scoped actions take an optional `cwd` that narrows an answer to one project: without it a read answers across every project and says so in `meta`, while a created thought lands in the default project. `memory_status action=propose` is the exception among the readers: its plan stays pinned to the project of the thought it addresses. For action-level detail (parameters, the dry-run/confirm pattern, examples), see [docs/mcp-tools.md](docs/mcp-tools.md). **Breaking 0.x changes:** this release prunes several 0.x subsystems from the MCP tools, the HTTP surface and the configuration surface. The 0.20 release shrinks HTTP to the three ops endpoints: every `/api/*` thought route is gone and answers `404`, with the route → MCP tool map and the dropped capabilities in [docs/API.md](docs/API.md#removed-routes-020). [CHANGELOG.md](CHANGELOG.md) records every removal per release; the pages linked here document only the surface that still exists, and a configuration file that still carries retired keys loads unchanged and ignores them.
--- ## Custom Instructions The MCP server sends instructions to the AI agent on startup. To customize: 1. Create a markdown file (e.g., `instructions.md`) 2. Set the path in `config.json`: ```json "mcp": { "instructionsFile": "./instructions.md" } ``` Or via env var: ```bash export SYNAPTOMIND_MCP_INSTRUCTIONS_FILE=./instructions.md ``` Falls back to default instructions if file not found. --- ## Development ```bash bun run test # canonical suite (src/), sharded across parallel processes bun run test:unit # fast lane: everything except the entry-point/child-boot tests bun run test:integration # slow lane: those entry-point/child-boot tests only bun run test:serial # single-process oracle (`bun test src`), same file set bun run test:all # full suite (src/ + deploy/ + eval/ + scripts/) bun run test:deploy # deploy shell integration (deploy/) bun run test:eval # retrieval quality gates (eval/) bun run test:perf # perf scale (SYNAPTOMIND_PERF_SUITE=1) bun run test:isolated # subprocess-heavy embedder IPC + http transport lifecycle bun run eval # memory evaluation harness (recall@k, MRR, baselines) bunx biome check src/ eval/ # lint (advisory) bun run format # apply Biome formatting (src eval scripts deploy) bun run format:check # verify formatting — blocking CI gate ``` `bun test` (without a script) still runs the full suite; the canonical developer gate is `bun run test`, which runs exactly the same `src/**/*.test.ts` set as `bun test src` but splits the files across several `bun test` processes — Bun runs test files sequentially in one process, so sharding is what uses the extra cores. `bun run test:serial` is the single-process fallback for bisecting a failure, and `--shards N` (or a `--` passthrough, e.g. `bun run test -- --bail`) is honoured by the runner. Lanes and shard weights live in `src/test/lanes.ts`. `vec0.so` is installed by `bun install` into the checkout that ran it, so a fresh git worktree has no copy of its own. `bunfig.toml` preloads `src/test/preload.ts` for tests only: when the running checkout has no `vec0.so`, it points the test process at the main checkout's copy and prints one line naming the file it used. A local copy always wins (and `SYNAPTOMIND_VEC0_PATH` overrides everything), so a genuinely broken extension still fails loudly. For step-by-step usage scenarios, see [docs/SCENARIOS.md](docs/SCENARIOS.md). For performance benchmarks (search latency, write throughput, embedding speed), see [docs/BENCHMARK.md](docs/BENCHMARK.md). For the memory evaluation harness (metrics, datasets, thresholds), see [docs/EVAL.md](docs/EVAL.md). ### Releasing Releases are created by CI on `main`; the version is set explicitly, not derived from commit types. 1. From a clean `dev` tree, prepare the bump: `bash scripts/release.sh ` (or `bun run release `) — bumps `version` in `package.json` and regenerates the pending `CHANGELOG.md` section. It does not create a tag, commit, or push. 2. Commit the bump + changelog, push, and open a PR `dev` → `main`. 3. Merging the PR triggers `.github/workflows/release.yml` on `main`: it verifies the tree (`tsc` + `bun test`), creates the annotated tag `v` plus the GitHub Release (prerelease when the version contains `-`), and separately publishes the Docker image. --- ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). ## License [MIT](LICENSE) --- ## Topics `mcp` `mcp-server` `ai-memory` `agent-memory` `llm-memory` `persistent-memory` `self-hosted` `local-first` `knowledge-graph` `semantic-search` `ai-agents` `sqlite` `rag`