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: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Engram

Self-curating coding preference memory for Claude Code.
Self-consolidating coding imprint memory for Claude Code.

Engram learns your coding preferences passively during Claude Code sessions, stores them with semantic deduplication, and automatically injects relevant preferences into future sessions — scoped to the language, framework, and repository you're working in.
Engram learns your coding imprints passively during Claude Code sessions, stores them with semantic deduplication, and automatically injects relevant imprints into future sessions — scoped to the language, framework, and repository you're working in.

## Quick Start

Expand All @@ -20,8 +20,8 @@ open http://localhost:3777
## How It Works

1. **During coding sessions** — when you give feedback like "don't mock the database" or "use frozen dataclasses", Claude Code stores it via the engram MCP server
2. **Between sessions** — engram deduplicates, resolves conflicts, and organizes preferences using semantic search and LLM-driven analysis
3. **At session start** — a Claude Code hook injects relevant preferences into your CLAUDE.md, scoped to the project's languages and repo
2. **Between sessions** — engram deduplicates, resolves conflicts, and consolidates imprints using semantic search and LLM-driven analysis
3. **At session start** — a Claude Code hook injects relevant imprints into your CLAUDE.md, scoped to the project's languages and repo

## Setup

Expand Down Expand Up @@ -90,8 +90,8 @@ storage:

Open `http://localhost:3777` to:

- **Browse** preferences with search, scope filtering, and tag filtering
- **Chat** with the curation agent for bulk cleanup, conflict review, and proactive suggestions
- **Browse** imprints with search, scope filtering, and tag filtering
- **Chat** with the collector agent for bulk cleanup, conflict review, and proactive suggestions
- **Configure** LLM provider and model settings

## Architecture
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[project]
name = "engramd"
version = "0.1.1"
description = "Self-curating coding preference memory for Claude Code"
description = "Self-consolidating coding imprint memory for Claude Code"
readme = "README.md"
license = "MIT"
requires-python = ">=3.11"
authors = [{ name = "DannyMor" }]
keywords = ["claude", "mcp", "preferences", "coding", "memory"]
keywords = ["claude", "mcp", "imprints", "coding", "memory"]
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3.11",
Expand Down
2 changes: 1 addition & 1 deletion src/engram/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""engram — Self-curating coding preference memory for Claude Code."""
"""engram — Self-consolidating coding imprint memory for Claude Code."""

__version__ = "0.1.0"
4 changes: 2 additions & 2 deletions src/engram/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

from engram.api.routes import chat, config, health, injection, preferences, scopes
from engram.api.routes import chat, config, health, imprints, injection, scopes


def create_api(app: FastAPI) -> None:
"""Mount all API routers onto the FastAPI app."""
app.include_router(health.router)
app.include_router(preferences.router)
app.include_router(imprints.router)
app.include_router(scopes.router)
app.include_router(injection.router)
app.include_router(config.router)
Expand Down
4 changes: 2 additions & 2 deletions src/engram/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from engram.core.models import EngramConfig
from engram.llm.base import LLMClient
from engram.storage.base import PreferenceStore
from engram.storage.base import ImprintStore


async def get_store(request: Request) -> PreferenceStore:
async def get_store(request: Request) -> ImprintStore:
return request.app.state.store


Expand Down
10 changes: 5 additions & 5 deletions src/engram/api/routes/chat.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
"""Chat endpoint — streaming curation agent responses."""
"""Chat endpoint — streaming collector agent responses."""

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse

from engram.api.dependencies import get_llm_client, get_store
from engram.api.models import ChatRequest
from engram.curator.agent import CurationAgent
from engram.collector.agent import ImprintCollector
from engram.llm.base import LLMClient
from engram.storage.base import PreferenceStore
from engram.storage.base import ImprintStore

router = APIRouter(tags=["chat"])


@router.post("/api/chat", response_class=StreamingResponse)
async def chat(
request: ChatRequest,
store: PreferenceStore = Depends(get_store),
store: ImprintStore = Depends(get_store),
llm_client: LLMClient | None = Depends(get_llm_client),
) -> StreamingResponse:
if llm_client is None:
Expand All @@ -25,7 +25,7 @@ async def chat(
" a Bedrock provider in ~/.engram/config.yaml",
)

agent = CurationAgent(llm=llm_client, store=store)
agent = ImprintCollector(llm=llm_client, store=store)
history = [{"role": m.role, "content": m.content} for m in request.history]

async def stream():
Expand Down
77 changes: 77 additions & 0 deletions src/engram/api/routes/imprints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Imprint CRUD endpoints."""

from fastapi import APIRouter, Depends, HTTPException, Response

from engram.api.dependencies import get_store
from engram.core.models import Imprint, ImprintCreate, ImprintUpdate
from engram.storage.base import ImprintStore

router = APIRouter(prefix="/api/imprints", tags=["imprints"])


@router.get("", response_model=list[Imprint])
async def list_imprints(
store: ImprintStore = Depends(get_store),
q: str | None = None,
scope: str | None = None,
repo: str | None = None,
tags: str | None = None,
) -> list[Imprint]:
if q is not None:
return await store.search(q, scope=scope, repo=repo)
tag_list = [t.strip() for t in tags.split(",")] if tags else None
return await store.get_all(scope=scope, repo=repo, tags=tag_list)


@router.post("", status_code=201, response_model=Imprint)
async def add_imprint(
body: ImprintCreate,
store: ImprintStore = Depends(get_store),
) -> Imprint:
return await store.add(body)


@router.get("/{imprint_id}", response_model=Imprint)
async def get_imprint(
imprint_id: str,
store: ImprintStore = Depends(get_store),
) -> Imprint:
try:
return await store.get(imprint_id)
except KeyError as err:
raise HTTPException(
status_code=404,
detail=f"Imprint not found: {imprint_id}",
) from err


@router.put("/{imprint_id}", response_model=Imprint)
async def update_imprint(
imprint_id: str,
body: ImprintUpdate,
store: ImprintStore = Depends(get_store),
) -> Imprint:
try:
return await store.update(
imprint_id, text=body.text, scope=body.scope, repo=body.repo, tags=body.tags
)
except KeyError as err:
raise HTTPException(
status_code=404,
detail=f"Imprint not found: {imprint_id}",
) from err


@router.delete("/{imprint_id}", status_code=204)
async def delete_imprint(
imprint_id: str,
store: ImprintStore = Depends(get_store),
) -> Response:
try:
await store.delete(imprint_id)
except KeyError as err:
raise HTTPException(
status_code=404,
detail=f"Imprint not found: {imprint_id}",
) from err
return Response(status_code=204)
24 changes: 12 additions & 12 deletions src/engram/api/routes/injection.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
"""Preference injection endpoint."""
"""Imprint injection endpoint."""

from fastapi import APIRouter, Depends, Query
from fastapi.responses import PlainTextResponse

from engram.api.dependencies import get_store
from engram.core.models import Preference
from engram.core.models import Imprint
from engram.injection import format_injection_block
from engram.storage.base import PreferenceStore
from engram.storage.base import ImprintStore

router = APIRouter(tags=["injection"])


@router.get("/api/inject", response_class=PlainTextResponse)
async def inject_preferences(
store: PreferenceStore = Depends(get_store),
async def inject_imprints(
store: ImprintStore = Depends(get_store),
scopes: str = Query(default="global"),
repo: str | None = None,
) -> str:
scope_list = [s.strip() for s in scopes.split(",")]
seen_ids: set[str] = set()
all_prefs: list[Preference] = []
all_imprints: list[Imprint] = []
for scope in scope_list:
prefs = await store.get_all(scope=scope, repo=repo)
for p in prefs:
if p.id not in seen_ids:
seen_ids.add(p.id)
all_prefs.append(p)
return format_injection_block(all_prefs)
imprints = await store.get_all(scope=scope, repo=repo)
for i in imprints:
if i.id not in seen_ids:
seen_ids.add(i.id)
all_imprints.append(i)
return format_injection_block(all_imprints)
77 changes: 0 additions & 77 deletions src/engram/api/routes/preferences.py

This file was deleted.

6 changes: 3 additions & 3 deletions src/engram/api/routes/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
from fastapi import APIRouter, Depends

from engram.api.dependencies import get_store
from engram.storage.base import PreferenceStore
from engram.storage.base import ImprintStore

router = APIRouter(tags=["metadata"])


@router.get("/api/scopes", response_model=list[str])
async def get_scopes(store: PreferenceStore = Depends(get_store)) -> list[str]:
async def get_scopes(store: ImprintStore = Depends(get_store)) -> list[str]:
return await store.get_scopes()


@router.get("/api/tags", response_model=list[str])
async def get_tags(store: PreferenceStore = Depends(get_store)) -> list[str]:
async def get_tags(store: ImprintStore = Depends(get_store)) -> list[str]:
return await store.get_tags()
6 changes: 3 additions & 3 deletions src/engram/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from engram.llm.anthropic import AnthropicLLMClient
from engram.llm.bedrock import BedrockLLMClient
from engram.mcp import create_mcp
from engram.storage.mem0 import Mem0PreferenceStore
from engram.storage.mem0 import Mem0ImprintStore

logger = logging.getLogger(__name__)

Expand All @@ -30,15 +30,15 @@ def create_llm_client(config: EngramConfig) -> AnthropicLLMClient | BedrockLLMCl

def create_app(
config: EngramConfig | None = None,
store: Mem0PreferenceStore | None = None,
store: Mem0ImprintStore | None = None,
) -> FastAPI:
"""Create and configure the full engram application."""
if config is None:
config = load_config()
setup_logging(config.logging.level)

if store is None:
store = Mem0PreferenceStore(config)
store = Mem0ImprintStore(config)

# MCP (must init before FastAPI for lifespan)
mcp = create_mcp(store)
Expand Down
2 changes: 1 addition & 1 deletion src/engram/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""
parser = argparse.ArgumentParser(
prog="engramd",
description="Self-curating coding preference memory for Claude Code",
description="Self-consolidating coding imprint memory for Claude Code",
)
parser.add_argument(
"--config",
Expand Down
6 changes: 6 additions & 0 deletions src/engram/collector/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Imprint collector — LLM-powered imprint management."""

from engram.collector.agent import ImprintCollector, build_system_prompt, build_tool_definitions
from engram.collector.tools import ToolCommand

__all__ = ["ImprintCollector", "ToolCommand", "build_system_prompt", "build_tool_definitions"]
Loading
Loading