Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@

A persistent memory system for AI coding agents that enables long-term context retention across sessions using local vector database technology.

**Multi-host (MCP):** one shared runtime for common coding-agent hosts:

```bash
npx -y opencode-mem install --host auto --cwd "$PWD" # or: --host all (--ide still works)
npx -y opencode-mem serve --cwd "$PWD"
npx -y opencode-mem status
```

Supports Cursor, Claude Code, Codex, Gemini/Antigravity, Windsurf, Kimi, OpenClaw, Goose, Warp, Copilot, Grok, plus native OpenCode (plugin + MCP). With `--cwd`, project-local MCP configs are written too. See [docs/mcp.md](docs/mcp.md).

## Core Features

Local Turso/libSQL database with native vector search, persistent project memories, automatic user profile learning, unified memory-prompt timeline, full-featured web UI, intelligent prompt-based memory extraction, multi-provider AI support (OpenAI, Anthropic), 12+ local embedding models, smart deduplication, and built-in privacy protection.
Expand Down Expand Up @@ -65,7 +75,7 @@ If a shard becomes incompatible (for example after changing `embeddingDimensions

## Schema migrations

Local Turso shards and auxiliary databases (`metadata.db`, `user-prompts.db`, `user-profiles.db`, `ai-sessions.db`) are upgraded with ordered `PRAGMA user_version` migrations in `src/services/turso/schema-migrations.ts`. Migrations are idempotent: starting the plugin applies only pending versions.
Local Turso shards and auxiliary databases (`metadata.db`, `user-prompts.db`, `user-profiles.db`, `ai-sessions.db`) are upgraded with ordered `PRAGMA user_version` migrations in `src/storage/turso/schema-migrations.ts`. Migrations are idempotent: starting the plugin applies only pending versions.

## Getting Started

Expand Down
155 changes: 154 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

185 changes: 185 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Multi-host MCP setup

opencode-mem wires common coding-agent **hosts** (Cursor, Claude, Codex, …) via MCP + one shared local runtime.

Related: [Issue #366](https://github.com/tickernelz/opencode-mem/issues/366).

> Naming: **host** = Cursor / Claude / … (this doc). OpenCode **session agents** (build, orchestrator, …) and the internal structured-output agent are separate concepts.

## Quick start

```bash
# Detect every installed host and write MCP (or OpenCode plugin) configs
npx -y opencode-mem install --host auto --cwd "$PWD"

# Or install every supported harness
npx -y opencode-mem install --host all --cwd "$PWD"

# One shared runtime for all hosts
npx -y opencode-mem serve --cwd "$PWD"
npx -y opencode-mem status
```

Restart IDEs after install. `status` shows detected vs configured hosts. OpenCode attaches to a healthy `serve` when `preferSharedRuntime` is on (default).

With `--cwd`, install also writes **project-local** MCP configs (and pins `OPENCODE_MEM_DIRECTORY` there only). User-global configs stay multi-project safe (no baked directory).

## Supported hosts

| Host / `--host` (or `--ide`) | Config written | `OPENCODE_MEM_PLATFORM` |
| ---------------------------- | ----------------------------------------------------------------------------------------- | ----------------------- |
| `cursor` | `~/.cursor/mcp.json` (+ project `.cursor/mcp.json`) | `cursor` |
| `claude` | `~/.claude.json` (+ project `.mcp.json`) | `claude` |
| `codex` | `~/.codex/config.toml` (+ project `.codex/config.toml`) | `codex` |
| `gemini` | `~/.gemini/settings.json` (+ project `.gemini/settings.json`) | `gemini` |
| `antigravity` | `~/.gemini/config/mcp_config.json` (+ project `.agents/mcp_config.json`) | `antigravity` |
| `opencode` | `~/.config/opencode/opencode.json` **plugin + MCP** (+ project `opencode.json`) | `opencode` |
| `windsurf` | `~/.codeium/windsurf/mcp_config.json` | `windsurf` |
| `kimi` | `~/.kimi-code/mcp.json` (+ project `.kimi-code/mcp.json`) | `kimi` |
| `openclaw` | `~/.openclaw/openclaw.json` → `mcp.servers` | `openclaw` |
| `goose` | `~/.config/goose/config.yaml` extension (`envs:`) | `goose` |
| `warp` | `~/.warp/.mcp.json` (+ project `.warp/.mcp.json`) | `warp` |
| `copilot` | VS Code User `mcp.json` (`servers`) + `~/.copilot/mcp-config.json` (+ `.vscode/mcp.json`) | `copilot` |
| `grok` | `~/.grok/config.toml` (+ project `.grok/config.toml`) | `grok` |
| `all` | every row above | per-ide |
| `auto` | only detected installs | per-ide |

Aliases: `claude-code`→`claude`, `codex-cli`→`codex`, `antigravity-cli`→`antigravity`, `github-copilot`→`copilot`.

## Architecture

```text
Cursor / Claude / Codex / Gemini / Windsurf / Kimi / …
→ MCP stdio (`opencode-mem mcp`)
→ shared serve (HTTP)
→ Turso + embeddings + Web UI

OpenCode plugin + MCP
→ preferSharedRuntime? attach to serve : in-process
→ MCP tools: memory_timeline / memory_search / memory_get / memory_write
```

### Shared-process boundary (`src/runtime/`)

`runtime/` owns the shared-process boundary — not domain storage:

| Piece | Role |
| -------------------------------- | -------------------------------------------------------- |
| `runtime/client.ts` | Discovery, auto-start `serve`, HTTP client |
| `runtime/bridge.ts` | OpenCode in-process attach pointer |
| `runtime/http/mcp-routes.ts` | `/api/mcp/*` — **compact** progressive MCP shapes |
| `runtime/http/runtime-routes.ts` | `/api/runtime/tool` — **full** plugin memory-tool shapes |

MCP hosts use `/api/mcp/*`. OpenCode attach uses `/api/runtime/tool` so search/list match in-process responses (full `content` / `similarity`, not snippets).

### Source layout

Thematic top-level modules (no `services/` catch-all):

```text
src/
shared/ hosts + platform-source + api schemas
cli/ serve / mcp / install / status
mcp/ stdio only
runtime/ client, bridge, http/ (MCP + REST/Web)
hosts/opencode/ OpenCode plugin orchestration
memory/ CRUD, capture, learning, tags, embedding, tool/, user-prompt/
storage/ turso/ + shard/migration services
ai/ providers, sessions, OpenCode AI helpers
user-profile/ profile manager + learning lock
infra/ logger, privacy, jsonc, onnx, secrets, …
config.ts root config
plugin.ts package plugin entry (V1+V2)
index.ts re-exports hosts/opencode helpers
v2/ OpenCode v2 adapter
types/ shared domain types
utils/ small pure helpers
```

Guardrails:

1. Import shared-process APIs from `runtime/client.js` and `runtime/bridge.js` only.
2. New HTTP endpoints for MCP / shared runtime go under `runtime/http/`.
3. Host detection, MCP config writers, and host platform labels live in `shared/hosts.ts` + `cli/install/` — never under `storage/` / `ai/` / `memory/` storage internals.
4. Priming and config formats stay under `cli/install/`.

### Progressive tools (token-aware)

| Tool | Role |
| ----------------- | ---------------------------------------------------------- |
| `memory_timeline` | Recent memories chronologically (session-start continuity) |
| `memory_search` | Compact topical index (+ `platformSource`) |
| `memory_get` | Full content for selected ids |
| `memory_write` | add / forget / profile |

Workflow: `memory_timeline` or `memory_search` → pick ids → `memory_get` (batch).

### Session priming (project `--cwd`)

For hosts without OpenCode-depth hooks, `install --cwd` also writes lightweight priming so the agent knows to call MCP at session start:

| Host | Priming file |
| -------------------- | ------------------------------------------------ |
| Cursor | `.cursor/rules/opencode-mem.mdc` (`alwaysApply`) |
| Claude | `CLAUDE.md` marked block |
| Windsurf | `.windsurf/rules/opencode-mem.md` |
| Gemini / Antigravity | `GEMINI.md` marked block |
| Kimi | `.kimi-code/rules/opencode-mem.md` |

OpenCode keeps native auto-capture / compaction inject — no priming file needed.

## Commands

```bash
opencode-mem serve [--host HOST] [--port PORT] [--cwd DIR]
opencode-mem mcp [--cwd DIR]
opencode-mem install --host <host[,host]|all|auto> [--cwd DIR]
opencode-mem status [--cwd DIR]
```

`--ide` remains a compatibility alias for install's `--host`.

## Env overrides

| Env | Purpose |
| ------------------------------------ | ---------------------------------------------- |
| `OPENCODE_MEM_DIRECTORY` | Project root for shards (project configs only) |
| `OPENCODE_MEM_PLATFORM` | Provenance stamp (set per IDE by `install`) |
| `OPENCODE_MEM_STORAGE_PATH` | Override data directory |
| `OPENCODE_MEM_PREFER_SHARED_RUNTIME` | OpenCode attach vs in-process |

## Single-owner playbook

1. `opencode-mem serve --cwd "$PWD"`
2. `opencode-mem install --host auto --cwd "$PWD"`
3. Open OpenCode → attaches to serve; other hosts via MCP → same serve
4. `opencode-mem status` → one healthy URL + host coverage

## Adding a host

New coding-agent hosts go through the existing install catalog — **not** through Turso, AI, or web-server special cases.

1. Add a `HostSpec` entry in [`src/shared/hosts.ts`](../src/shared/hosts.ts) (`HOST_IDS`, detect/config paths, optional priming, `configKind`).
2. Wire install under [`src/cli/install/`](../src/cli/install/):
- reuse a format in `formats/` when possible (`json`, `toml`, …), or add a small adapter under `hosts/` for one-off layouts;
- `installHost()` already dispatches on `spec.configKind`.
3. Optional project priming via `priming` on the host spec + `install/priming.ts`.
4. Cover detection/config in `tests/install-ide.test.ts` (and status coverage if needed).

Do **not** put host detection, MCP config writers, or `platformSource` labels under `storage/`, `ai/`, or low-level memory persistence modules.

## Follow-ups (optional)

Still deferred (not required for the thematic layout):

1. Drop leftover `--ide` / `InstallIde` aliases once callers use `--host` / `HostId`.
2. Split oversized files (`runtime/http/api-handlers.ts`, `user-profile/user-profile-manager.ts`, `config.ts`).

Do **not** big-bang rename `storage/turso/`, `memory/tool/`, or `cli/install/` — those boundaries already work.

## Notes

- This is **MCP-first** multi-host wiring (faster + cheaper to maintain than native hooks per host).
- Progressive recall mirrors claude-mem-style disclosure (`timeline`/`search` → `get`); capture outside OpenCode stays host-initiated (`memory_write`) plus project priming rules.
- OpenCode keeps deep auto-capture / compaction / profile learning; when attached, capture **writes** go through the shared runtime.
- Auth: `~/.opencode-mem/.auth-token`. Runtime pointer: `~/.opencode-mem/runtime.json`.
28 changes: 23 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{
"name": "opencode-mem",
"version": "2.28.3",
"description": "OpenCode plugin that gives coding agents persistent memory using local Turso/libSQL vector search",
"description": "Persistent local memory for coding agents (OpenCode plugin + MCP + standalone runtime) using Turso/libSQL vector search",
"type": "module",
"main": "dist/plugin.js",
"types": "dist/plugin.d.ts",
"bin": {
"opencode-mem": "dist/cli/index.js"
},
"exports": {
".": {
"import": "./dist/plugin.js",
Expand All @@ -19,15 +22,23 @@
"types": "./dist/v2/plugin.d.ts"
},
"./tags": {
"import": "./dist/services/tags.js",
"types": "./dist/services/tags.d.ts"
"import": "./dist/memory/tags.js",
"types": "./dist/memory/tags.d.ts"
},
"./mcp": {
"import": "./dist/mcp/server.js",
"types": "./dist/mcp/server.d.ts"
}
},
"scripts": {
"build": "rm -rf dist && bunx tsc && bun run web:build",
"build": "rm -rf dist && bunx tsc && bun run web:build && chmod +x dist/cli/index.js",
"web:dev": "cd web && bun run dev",
"web:build": "cd web && bun run build",
"dev": "tsc --watch",
"serve": "bun run src/cli/index.ts serve",
"mcp": "bun run src/cli/index.ts mcp",
"install:ides": "bun run src/cli/index.ts install",
"status": "bun run src/cli/index.ts status",
"test": "bun test",
"typecheck": "tsc --noEmit",
"lint": "eslint . --max-warnings=0",
Expand All @@ -44,7 +55,12 @@
"ai",
"coding-agent",
"local",
"standalone"
"standalone",
"mcp",
"model-context-protocol",
"cursor",
"claude-code",
"codex"
],
"author": "opencode-mem",
"license": "MIT",
Expand All @@ -58,6 +74,7 @@
"dependencies": {
"@huggingface/transformers": "^4.3.0",
"@libsql/client": "^0.18.0",
"@modelcontextprotocol/sdk": "^1.32.0",
"@opencode-ai/plugin": "^1.18.34",
"@opencode-ai/sdk": "^1.18.34",
"@opencode/plugin": "^2.0.21",
Expand Down Expand Up @@ -95,6 +112,7 @@
},
"files": [
"dist",
"docs",
"package.json"
],
"lint-staged": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/live-auto-update-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { tmpdir } from "node:os";

const root = join(import.meta.dirname, "..");
const { isVersionNewer, isAutoUpdatableSpec, updateRemoveDir, checkAutoUpdate, startAutoUpdate } =
await import(join(root, "src/services/auto-update.ts"));
await import(join(root, "src/infra/auto-update.ts"));

assert.equal(isVersionNewer("2.27.0", "2.26.0"), true);
assert.equal(isAutoUpdatableSpec("latest"), true);
Expand Down
30 changes: 13 additions & 17 deletions scripts/live-changed-surfaces.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,24 @@ function fail(name, error) {

const { CONFIG } = await import(join(root, "src/config.ts"));
const { resetTursoReady, ensureTursoReady } = await import(
join(root, "src/services/turso/ready.ts")
join(root, "src/storage/turso/ready.ts")
);
const { tursoShardManager } = await import(join(root, "src/services/turso/shard-manager.ts"));
const { tursoShardManager } = await import(join(root, "src/storage/turso/shard-manager.ts"));
const { tursoConnectionManager, resolveDatabaseEncryption } = await import(
join(root, "src/services/turso/connection-manager.ts")
join(root, "src/storage/turso/connection-manager.ts")
);
const { tursoVectorSearch } = await import(join(root, "src/services/turso/vector-search.ts"));
const { tursoVectorSearch } = await import(join(root, "src/storage/turso/vector-search.ts"));
const { runTursoEngineMigration } = await import(
join(root, "src/services/turso/engine-migrator.ts")
join(root, "src/storage/turso/engine-migrator.ts")
);
const { runDatabaseEncryptionMigration } = await import(
join(root, "src/services/turso/encryption-migrator.ts")
join(root, "src/storage/turso/encryption-migrator.ts")
);
const {
generateDatabaseEncryptionKeyFile,
isValidDatabaseEncryptionHexKey,
resolveOrCreateDatabaseEncryptionKey,
} = await import(join(root, "src/services/turso/encryption-key.ts"));
} = await import(join(root, "src/storage/turso/encryption-key.ts"));
const {
applySchemaMigrations,
USER_PROMPTS_MIGRATIONS,
Expand All @@ -63,20 +63,16 @@ const {
METADATA_DB_MIGRATIONS,
ensureUserPromptColumns,
memoryShardMigrations,
} = await import(join(root, "src/services/turso/schema-migrations.ts"));
const { TursoDb } = await import(join(root, "src/services/turso/turso-db.ts"));
} = await import(join(root, "src/storage/turso/schema-migrations.ts"));
const { TursoDb } = await import(join(root, "src/storage/turso/turso-db.ts"));
const { renameSqliteDatabase, copySqliteDatabase, removeSqliteDatabase } = await import(
join(root, "src/services/turso/sqlite-handle-release.ts")
join(root, "src/storage/turso/sqlite-handle-release.ts")
);
const { userPromptManager } = await import(
join(root, "src/services/user-prompt/user-prompt-manager.ts")
);
const { userProfileManager } = await import(
join(root, "src/services/user-profile/user-profile-manager.ts")
);
const { aiSessionManager } = await import(
join(root, "src/services/ai/session/ai-session-manager.ts")
join(root, "src/memory/user-prompt/user-prompt-manager.ts")
);
const { userProfileManager } = await import(join(root, "src/user-profile/user-profile-manager.ts"));
const { aiSessionManager } = await import(join(root, "src/ai/session/ai-session-manager.ts"));

const SCOPE = "a1b2c3d4e5f67890";
const SCOPE2 = "b2c3d4e5f6789012";
Expand Down
8 changes: 4 additions & 4 deletions scripts/test-auth-server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { WebServer } from "../src/services/web-server.js";
import { WebAuth } from "../src/services/web-auth.js";
import { resolveSecretValue } from "../src/services/secret-resolver.js";
import { WebServer } from "../src/runtime/http/web-server.js";
import { WebAuth } from "../src/runtime/http/web-auth.js";
import { resolveSecretValue } from "../src/infra/secret-resolver.js";

const PORT = Number(process.env.PORT ?? 14747);
const HOST = process.env.HOST ?? "127.0.0.1";

// Mirrors how src/index.ts plumbs the opencode-mem config through to WebAuth:
// Mirrors how src/hosts/opencode/plugin.ts plumbs the opencode-mem config through to WebAuth:
// both values are optional in the config file, and `webServerAuthPassword`
// accepts the same env:// / file:// shorthand as `memoryApiKey`.
const password = resolveSecretValue(process.env.WEB_AUTH_PASSWORD);
Expand Down
2 changes: 1 addition & 1 deletion scripts/verify-nested-onnxruntime-fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ async function main() {
const embeddingUrl = pathToFileURL(join(pluginRoot, "dist", "services", "embedding.js")).href;
const embeddingMod = await import(embeddingUrl);
if (typeof embeddingMod.loadLocalTransformersBackend !== "function") {
fail("dist/services/embedding.js does not export loadLocalTransformersBackend");
fail("dist/memory/embedding.js does not export loadLocalTransformersBackend");
}

const transformers = await embeddingMod.loadLocalTransformersBackend();
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client";
import { CONFIG } from "../../config.js";
import { CONFIG } from "../config.js";
import { loadOpencodeProvider } from "./opencode-provider-loader.js";

let _cachedClient: OpencodeClient | null = null;
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { applySafeExtraParams, BaseAIProvider, type ToolCallResult } from "./bas
import { AISessionManager } from "../session/ai-session-manager.js";
import { ToolSchemaConverter, type ChatCompletionTool } from "../tools/tool-schema.js";
import type { AIProviderType } from "../session/session-types.js";
import { log } from "../../logger.js";
import { log } from "../../infra/logger.js";
import { UserProfileValidator } from "../validators/user-profile-validator.js";

interface AnthropicMessage {
Expand Down
Loading
Loading