CLAUDE.md · git:20260310.6e74979 · 2026-03-10 · sha256 f82862f975fe31c1

CLAUDE.md git:20260310.6e74979A

Immutable. This exact content is served forever at /api/v1/blob/f82862f975fe31c1.

# CLAUDE.md — Agent context for mnemos

## What is this repo?

mnemos is cloud-persistent memory for AI agents. Plugins connect to mnemo-server (Go) for multi-agent, tenant-isolated memory with hybrid vector + keyword search.

Three components:
- `server/` — Go REST API (chi router, TiDB/MySQL, optional embedding)
- `openclaw-plugin/` — Agent plugin for OpenClaw (server backend)
- `claude-plugin/` — Claude Code plugin (bash hooks + skills)

## Commands

```bash
# Build server
cd server && go build ./cmd/mnemo-server

# Run server (requires MNEMO_DSN)
cd server && MNEMO_DSN="user:pass@tcp(host:4000)/mnemos?parseTime=true" go run ./cmd/mnemo-server

# Vet / lint
cd server && go vet ./...

# Run all checks
make build && make vet
```

## Project layout

```
server/cmd/mnemo-server/main.go     — Entry point, DI wiring, graceful shutdown
server/internal/config/             — Env var config loading (DB + embedding)
server/internal/domain/             — Core types (Memory with Metadata/Embedding/Score), errors
server/internal/embed/              — Embedding provider (OpenAI-compatible HTTP client)
server/internal/handler/            — HTTP handlers + chi router setup + JSON helpers
server/internal/middleware/         — Tenant resolution (tenantID → DB) + rate limiter
server/internal/repository/         — Repository interfaces + TiDB SQL (vector + keyword search)
server/internal/service/            — Business logic: upsert, LWW, hybrid search, embedding on write
server/schema.sql                   — Database DDL (control plane + tenant data plane)

openclaw-plugin/index.ts            — Tool registration via MemoryBackend interface
openclaw-plugin/backend.ts          — MemoryBackend interface (store/search/get/update/remove)
openclaw-plugin/server-backend.ts   — Server mode: fetch → mnemo API
openclaw-plugin/hooks.ts            — Lifecycle hooks (auto-recall, auto-capture, compact/reset)
openclaw-plugin/types.ts            — Shared TypeScript types

claude-plugin/hooks/common.sh            — Server helpers (REST API calls)
claude-plugin/hooks/session-start.sh     — Load recent memories → additionalContext
claude-plugin/hooks/stop.sh              — Save last response as memory
claude-plugin/hooks/user-prompt-submit.sh — System hint about available memory
claude-plugin/skills/memory-recall/      — On-demand search skill
claude-plugin/skills/memory-store/       — On-demand save skill
```

## Code style

- Go: standard `gofmt`, no ORM, raw `database/sql` with parameterized queries
- TypeScript: ESM modules, interface-based backend abstraction
- Bash hooks: `set -euo pipefail`, Python for JSON parsing (avoid shell injection)
- Layers: handler → service → repository (interfaces). Domain types imported by all layers.
- Errors: sentinel errors in `domain/errors.go`, mapped to HTTP status codes in `handler/handler.go`
- No globals. Manual DI in `main.go`. All constructors take interfaces.

## Key design decisions

- **Server-only architecture**: All plugins connect to mnemo-server via REST API. No direct DB access from plugins.
- **Plugin over skill**: Memory uses `kind: "memory"` plugin (automatic) not skill (agent-dependent)
- **Hooks over MCP tools**: Claude Code memory is via lifecycle hooks (guaranteed) not tools (optional)
- **Hybrid search**: Vector + keyword with graceful degradation. No embedder → keyword only.
- **Embedder nullable**: `embed.New()` returns nil when unconfigured. All code accepts nil embedder.
- **encoding_format: "float"**: Always set when calling embedding API (Ollama defaults to base64)
- **VEC_COSINE_DISTANCE**: Must appear identically in SELECT and ORDER BY for TiDB VECTOR INDEX
- **embedding IS NOT NULL**: Mandatory in vector search WHERE clause
- **3x fetch limit**: Both vector and keyword search fetch limit×3, merge after
- **Score**: `1 - distance` for vector results, `0.5` for keyword-only
- Upsert uses `INSERT ... ON DUPLICATE KEY UPDATE` (atomic, no race conditions)
- Version increment is atomic in SQL: `SET version = version + 1`
- Tags stored as JSON column, filtered with `JSON_CONTAINS`; empty tags stored as `[]` (not NULL)
- **No auth required**: Tenant ID in URL path is the only identification. No Bearer tokens, no API keys.

## Database schema

`server/schema.sql` contains DDL for two separate planes:

| Plane | Database | Tables |
|-------|----------|--------|
| **Control plane** | `MNEMO_DSN` (shared) | `tenants`, `upload_tasks` |
| **Tenant data plane** | Per-tenant DB provisioned by TiDB Zero | `memories` |

The `memories` table is **never created in the control plane DB**. It is created in each tenant's own dedicated database when a tenant is provisioned (automatically via TiDB Zero, or manually).

When deploying a new server environment, only run the control-plane tables against `MNEMO_DSN`:

```sql
-- Run against MNEMO_DSN only
CREATE TABLE IF NOT EXISTS tenants (...);
CREATE TABLE IF NOT EXISTS upload_tasks (...);
-- DO NOT run memories table here
```

The `memories` table DDL in `schema.sql` is reference only — used when manually setting up a tenant DB. For auto-embedding environments, use the generated column variant (see comments in `schema.sql`).

## Tenant provisioning

Provision a new tenant via the unauthenticated bootstrap endpoint:

```bash
curl -s -X POST http://<server>/v1alpha1/mem9s | jq .
# → { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "claim_url": "..." }
```

Save the returned `id` — this is the tenant ID used in all subsequent API calls.

All memory operations use tenant-scoped routes:
```
POST   /v1alpha1/mem9s/{tenantID}/memories          — Create
GET    /v1alpha1/mem9s/{tenantID}/memories           — Search/list
GET    /v1alpha1/mem9s/{tenantID}/memories/{id}      — Get by ID
PUT    /v1alpha1/mem9s/{tenantID}/memories/{id}      — Update
DELETE /v1alpha1/mem9s/{tenantID}/memories/{id}      — Delete
```

Agent identity is passed via the `X-Mnemo-Agent-Id` header.

## Metrics

Prometheus metrics are exposed at `GET /metrics` (no auth).

| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `mnemo_http_requests_total` | Counter | `method`, `route`, `status` | Total HTTP requests |
| `mnemo_http_request_duration_seconds` | Histogram | `method`, `route` | HTTP request latency |

`route` uses the chi route pattern (e.g. `/v1alpha1/mem9s/{tenantID}/memories`) — not the raw URL — to avoid high cardinality from tenant IDs.

Source: `server/internal/metrics/metrics.go`

## Installing the Claude Code plugin

If a user asks to install or set up the claude-plugin, read `claude-plugin/README.md` for the full guide.

**Marketplace install (recommended):**

1. In Claude Code: `/plugin marketplace add qiffang/mnemos`
2. Then: `/plugin install mnemo-memory@mnemos`
3. Accept the hook permissions when prompted
4. Add server credentials to `~/.claude/settings.json` under `env`:
   - Set `MNEMO_API_URL` and `MNEMO_TENANT_ID`
5. Restart Claude Code

**Manual install (alternative):**

1. `chmod +x claude-plugin/hooks/*.sh`
2. Copy skills: `cp -r claude-plugin/skills/memory-{recall,store} ~/.claude/skills/`
3. Add `env` + `hooks` entries to `~/.claude/settings.json` (see `claude-plugin/README.md` for full config)

## Installing the OpenCode plugin

If a user asks to install mnemo for OpenCode, read `opencode-plugin/README.md` for the full guide.

**npm plugin (recommended):**

1. Add to `opencode.json`: `{"plugin": ["mnemo-opencode"]}`
2. Set env vars: `MNEMO_API_URL` and `MNEMO_TENANT_ID`
3. Restart OpenCode — plugin auto-installs from npm and logs `[mnemo] Server mode...`

**From source (alternative):**

1. `cd opencode-plugin && npm install`
2. Add to `opencode.json`: `{"plugins": {"mnemo": {"path": "/absolute/path/to/mnemos/opencode-plugin"}}}`
3. Set env vars (same as above)

## Installing the OpenClaw plugin

If a user asks to install mnemo for OpenClaw, read `openclaw-plugin/README.md` for the full guide. Quick summary:

1. `cd openclaw-plugin && npm install`
2. Add to `openclaw.json`:
   - Set `plugins.slots.memory` to `"mnemo"`
   - Add `plugins.entries.mnemo` with `enabled: true` and config
   - Set `apiUrl` and `tenantID` in config
3. Plugin is `kind: "memory"` — OpenClaw framework manages the lifecycle automatically