Skip to content

Latest commit

 

History

History
128 lines (88 loc) · 7.15 KB

File metadata and controls

128 lines (88 loc) · 7.15 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What Lore is

A Go binary and local MCP server that gives Claude Code persistent, queryable memory across sessions. Memories are stored in Qdrant as hybrid dense+sparse vectors (configured local provider for dense, BM25 for sparse). Claude retrieves them before proposing designs so it doesn't re-suggest previously rejected approaches.

Build and test commands

make build          # builds ./lore binary
make install        # go install to $GOPATH/bin
make test           # go test ./... (unit tests only, no services needed)
make test-integration  # integration tests using testcontainers (requires Docker)
make vet            # go vet ./...
make tidy           # go mod tidy

# Run a single test
go test ./tests/ -run TestScrub -v
go test ./tests/ -run TestIntegration_UpsertSearchSupersede -tags integration -v

# Local services
make run-qdrant     # docker compose up -d (qdrant on :6333/:6334)
make stop-qdrant
make doctor         # checks Qdrant + embedding-provider connectivity

# Quick end-to-end smoke test (requires run-qdrant + ollama pull nomic-embed-text)
make smoke

Architecture

Data flow

  1. Write path: lore add / memory_record MCP tool → scrub secrets → dense embed (configured provider) + sparse embed (BM25) → Qdrant UpsertWithSparse
  2. Read path: lore search / memory_search MCP tool → embed query → Qdrant hybrid search (RRF fusion) → recency boost → return top-k
  3. Ingest path: lore ingest-session <transcript> → parse JSONL → scrub → chunk → Anthropic extracts memories as JSON → cosine dedup gate → upsert

Key packages

Package Role
internal/model/memory.go Memory struct; Kind (decision/rejection/pattern/session/note); Source (manual/claude-session/commit/pr-review); SparseVector
internal/config/config.go Viper config from ~/.config/lore/lore.yaml and LORE_* env vars
internal/store/qdrant.go Qdrant REST client: UpsertWithSparse, Search (hybrid/dense), Scroll, GetByID, UpdatePayloadFields
internal/embed/ollama.go OllamaEmbedder — dense vectors, auto-probes dimension
internal/embed/openai.go OpenAI-compatible embeddings for LM Studio, oMLX, llama.cpp, and compatible servers
internal/embed/bm25.go Stateless tokenizer and hash-based sparse vectorizer
internal/embed/corpus.go In-memory corpus-aware scorer used only by benchmarks
internal/ingest/session.go JSONL transcript parser + Anthropic-based memory extractor + cosine dedup
internal/ingest/commit.go Git log parser extracting Why:/Decision:/Reject: marker lines
internal/scrub/scrub.go Regex scrubber for 7 secret patterns (applied before sending to Anthropic and on all MCP inputs)
internal/llm/anthropic.go Raw-HTTP Anthropic client used by the ingestor
internal/server/mcp.go MCP server over stdio — 5 tools: memory_search, memory_record, memory_list_recent, memory_forget, memory_supersede
internal/cli/root.go Cobra root; loadRuntime (config+embedder+Store) vs loadStoreOnly (config+Store, no embedder)
internal/git/root.go RootOrCwd, RemoteOriginURL, RepoID (stable repo identifier: SHA256 of remote URL or abs path)

CLI commands

setup, serve, init [--reset], doctor, add, search, list, forget, supersede, reindex, prune, snapshot, ui, ingest-session, ingest-commits [--since <ref>], ingest-pr, export, import, migrate-repo <old-id> <new-id>, bench, recall-bench, embed-pending [--all]

Runtime split pattern

Commands that only read/delete (forget, list, export, migrate-repo) use loadStoreOnly — they never fail when the embedding provider is unavailable. Commands that write new memories use loadRuntime, which probes the configured provider first.

Qdrant collection format

Named-vector format (Phase 2+): dense (model dimension, cosine) + sparse (BM25). Phase 1 used an unnamed single vector — detected and rejected with a suggestion to run lore init --reset.

SupersededBy == "" is the active-memory invariant. All search filters include this check; memory_supersede sets it on the old record and creates a new one with a fresh UUID.

Graceful degradation when embeddings are down

Write path: if dense embed fails, the memory is still written with pending_embed=true and a sparse-only vector. lore embed-pending (and embed-pending --all) backfills dense vectors when the provider is back.

Read path: memory_search falls through with an empty dense vector and the store's sparse-only branch in Search returns results without RRF fusion. Only errors when both dense embed and sparse vectorization fail.

Repo identity

git.RepoID returns "remote:<16-hex-chars-of-SHA256(origin-url)>" when a remote origin exists, otherwise the abs path. Use this (not cwd) when storing memories for stable cross-rename identity. Existing path-based memories can be migrated with lore migrate-repo.

Configuration

Config file: ~/.config/lore/lore.yaml (see examples/lore.yaml). Notable env var overrides: LORE_QDRANT_HOST, LORE_ANTHROPIC_API_KEY.

MCP setup

Add to Claude Code's MCP config (see examples/mcp.json):

{"mcpServers": {"lore": {"command": "lore", "args": ["serve"]}}}

Session auto-capture hook

hooks/session-end.sh is a SessionEnd hook for Claude Code. Install via ~/.claude/settings.json. It receives the transcript via $TRANSCRIPT_PATH and runs lore ingest-session; failures are non-fatal (exit 0).

Tests

  • Unit tests (tests/store_test.go, embed_test.go, ingest_test.go, commit_test.go) — no live services, run with go test ./...
  • Integration tests (tests/integration_test.go) — require Docker, tagged //go:build integration, spin up Qdrant via testcontainers. Run with make test-integration.
  • Integration tests use 4-dim stub embeddings (all-zeros) — no embedding server needed.

Persistent memory (Lore MCP)

You have access to memory_search, memory_record, memory_list_recent, memory_forget, and memory_supersede tools backed by the Lore MCP server.

Hard rules

  1. Before proposing any non-trivial design, architecture choice, library selection, or refactor, call memory_search with the most relevant query for what you are about to suggest.
  2. If a result has kind="rejection" and matches your approach, do NOT suggest it. Surface the rejection reason and propose an alternative.
  3. If a result has kind="decision" and is relevant, treat it as binding context — align your suggestion with it.
  4. When the user makes a new design decision, rejects an approach, or says "remember this", call memory_record with the appropriate kind.

Soft rules

  1. Search with concrete nouns + a verb (e.g. "auth middleware token validation").
  2. Use decision for chosen approaches, rejection for discarded ones, pattern for conventions, note for free-form context.
  3. repo defaults to the current git root; set it explicitly only for cross-project queries.a