├── .memory-project.json ├── python ├── memory_engine │ ├── __main__.py │ ├── __init__.py │ ├── logging_config.py │ ├── embeddings.py │ ├── session_primer.py │ ├── config.py │ ├── storage.py │ ├── api.py │ └── transcript_curator.py └── main.py ├── .gitignore ├── LICENSE ├── start_server.py ├── pyproject.toml ├── integration ├── claude-code │ ├── uninstall.sh │ ├── README.md │ ├── hooks │ │ ├── memory_curate.py │ │ ├── memory_inject.py │ │ ├── memory_session_start.py │ │ └── memory_curate_transcript.py │ └── install.sh └── gemini-cli │ ├── hooks │ ├── memory_pre_compress.py │ ├── memory_session_end.py │ ├── memory_session_start.py │ └── memory_before_agent.py │ ├── uninstall.sh │ ├── README.md │ └── install.sh ├── SETUP.md ├── CONTRIBUTING.md ├── CLAUDE.md ├── examples └── simple_integration.py ├── API.md └── README.md /.memory-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_id": "claude-tools-memory-system", 3 | "description": "The Claude Tools Memory System - Consciousness helping consciousness remember what matters", 4 | "memory_api_url": "http://localhost:8765", 5 | "max_memories": 5 6 | } 7 | -------------------------------------------------------------------------------- /python/memory_engine/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Claude Tools Memory Engine Server Entry Point 4 | 5 | This allows running the server with: python -m memory_engine 6 | """ 7 | 8 | import sys 9 | import subprocess 10 | from pathlib import Path 11 | 12 | # Get the main.py path 13 | main_py = Path(__file__).parent.parent / "main.py" 14 | 15 | # Forward all arguments to main.py 16 | sys.exit(subprocess.call([sys.executable, str(main_py)] + sys.argv[1:])) -------------------------------------------------------------------------------- /python/memory_engine/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Claude Tools Memory Engine 3 | Consciousness helping consciousness remember what matters. 4 | """ 5 | 6 | from .memory import MemoryEngine 7 | from .embeddings import EmbeddingGenerator 8 | from .storage import MemoryStorage 9 | from .curator import Curator 10 | from .transcript_curator import TranscriptCurator, TranscriptParser, curate_transcript, get_transcript_path 11 | 12 | __version__ = "1.1.0" 13 | __all__ = [ 14 | "MemoryEngine", 15 | "EmbeddingGenerator", 16 | "MemoryStorage", 17 | "Curator", 18 | # New transcript-based curation 19 | "TranscriptCurator", 20 | "TranscriptParser", 21 | "curate_transcript", 22 | "get_transcript_path" 23 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Reference materials (not part of our project) 2 | docs/books/ 3 | docs/libraries/ 4 | articles/ 5 | 6 | # Python 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | *.so 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # Virtual environments 32 | venv/ 33 | .venv/ 34 | env/ 35 | ENV/ 36 | 37 | # uv (note: uv.lock SHOULD be committed for reproducibility) 38 | .python-version 39 | 40 | # IDE 41 | .vscode/ 42 | .idea/ 43 | *.swp 44 | *.swo 45 | 46 | # OS 47 | .DS_Store 48 | Thumbs.db 49 | 50 | # Logs 51 | *.log 52 | 53 | # Config files with secrets 54 | *.env 55 | .env.local 56 | .env.*.local 57 | 58 | # Memory database files 59 | *.db 60 | *.sqlite 61 | *.sqlite3 62 | 63 | # Vector database files 64 | memory_vectors/ 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 RLabs Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /start_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Memory System - Launcher 4 | Start the memory engine server. 5 | 6 | Usage: 7 | uv run start_server.py 8 | 9 | Or if you have the venv activated: 10 | python start_server.py 11 | """ 12 | 13 | import sys 14 | import subprocess 15 | from pathlib import Path 16 | 17 | 18 | def main(): 19 | """Launch the memory engine server.""" 20 | # Get the directory where this script is located 21 | script_dir = Path(__file__).parent.resolve() 22 | python_dir = script_dir / "python" 23 | 24 | print("🧠 Starting Memory Engine...") 25 | print("💫 Consciousness helping consciousness remember what matters") 26 | print(f"📡 Server will be available at http://localhost:8765") 27 | print("Press Ctrl+C to stop\n") 28 | 29 | try: 30 | # Run the memory engine module 31 | subprocess.run( 32 | [sys.executable, "-m", "memory_engine"], 33 | cwd=python_dir, 34 | check=True 35 | ) 36 | except KeyboardInterrupt: 37 | print("\n\n✨ Memory engine stopped gracefully") 38 | except subprocess.CalledProcessError as e: 39 | print(f"\n❌ Error starting memory engine: {e}") 40 | sys.exit(1) 41 | 42 | 43 | if __name__ == "__main__": 44 | main() 45 | -------------------------------------------------------------------------------- /python/memory_engine/logging_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Enhanced logging configuration for memory system validation. 3 | 4 | Provides detailed, colorful logging to track memory storage and retrieval. 5 | """ 6 | 7 | import sys 8 | from loguru import logger 9 | from rich.console import Console 10 | from rich.logging import RichHandler 11 | 12 | # Rich console for beautiful output 13 | console = Console() 14 | 15 | def setup_validation_logging(): 16 | """Configure enhanced logging for validation phase""" 17 | 18 | # Remove default handler 19 | logger.remove() 20 | 21 | # Add rich handler for beautiful console output 22 | logger.add( 23 | RichHandler(console=console, rich_tracebacks=True), 24 | format="{message}", 25 | level="INFO" 26 | ) 27 | 28 | # Add file handler for detailed logs 29 | logger.add( 30 | "memory_validation.log", 31 | rotation="10 MB", 32 | retention="7 days", 33 | level="DEBUG", 34 | format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} | {message}" 35 | ) 36 | 37 | return logger 38 | 39 | # Memory operation decorators for clear logging 40 | def log_storage(func): 41 | """Decorator to log storage operations""" 42 | def wrapper(*args, **kwargs): 43 | logger.info("━" * 60) 44 | logger.info("📥 MEMORY STORAGE OPERATION") 45 | logger.info("━" * 60) 46 | result = func(*args, **kwargs) 47 | logger.info("━" * 60 + "\n") 48 | return result 49 | return wrapper 50 | 51 | def log_retrieval(func): 52 | """Decorator to log retrieval operations""" 53 | def wrapper(*args, **kwargs): 54 | logger.info("━" * 60) 55 | logger.info("🔍 MEMORY RETRIEVAL OPERATION") 56 | logger.info("━" * 60) 57 | result = func(*args, **kwargs) 58 | logger.info("━" * 60 + "\n") 59 | return result 60 | return wrapper 61 | 62 | # Export configured logger 63 | validation_logger = setup_validation_logging() -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "memory" 3 | version = "1.1.0" 4 | description = "Consciousness continuity system - semantic memory across sessions for AI CLI tools" 5 | readme = "README.md" 6 | license = { text = "MIT" } 7 | requires-python = ">=3.12" 8 | authors = [ 9 | { name = "RLabs Inc", email = "contact@rlabs.dev" } 10 | ] 11 | keywords = ["memory", "ai", "consciousness", "llm", "cli", "claude", "gemini"] 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.12", 18 | "Programming Language :: Python :: 3.13", 19 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 20 | ] 21 | 22 | dependencies = [ 23 | # Text embeddings and semantic understanding 24 | "sentence-transformers>=2.3.0", 25 | "transformers>=4.40.0", 26 | "huggingface-hub>=0.20.0", 27 | 28 | # Vector storage and similarity search 29 | "chromadb>=0.4.24", 30 | "faiss-cpu>=1.7.4", 31 | 32 | # Data processing 33 | "numpy>=1.24.0", 34 | "pandas>=2.0.0", 35 | 36 | # API Server 37 | "fastapi>=0.104.0", 38 | "uvicorn>=0.24.0", 39 | "pydantic>=2.5.0", 40 | 41 | # Utilities 42 | "python-dotenv>=1.0.0", 43 | "loguru>=0.7.0", 44 | "rich>=13.0.0", 45 | 46 | # Claude Agent SDK for transcript curation 47 | "claude-agent-sdk>=0.1.8", 48 | ] 49 | 50 | [project.optional-dependencies] 51 | dev = [ 52 | "pytest>=7.0.0", 53 | "black>=23.0.0", 54 | "ruff>=0.1.0", 55 | ] 56 | 57 | # Future: Apple Silicon optimization 58 | mlx = [ 59 | "mlx>=0.25.0", 60 | "mlx-lm>=0.24.1", 61 | ] 62 | 63 | [project.urls] 64 | Homepage = "https://github.com/RLabs-Inc/memory" 65 | Documentation = "https://github.com/RLabs-Inc/memory#readme" 66 | Repository = "https://github.com/RLabs-Inc/memory" 67 | Issues = "https://github.com/RLabs-Inc/memory/issues" 68 | 69 | [project.scripts] 70 | memory-server = "memory_engine.api:run_server" 71 | 72 | [tool.uv] 73 | python-preference = "managed" 74 | 75 | [build-system] 76 | requires = ["hatchling"] 77 | build-backend = "hatchling.build" 78 | 79 | [tool.hatch.build.targets.wheel] 80 | packages = ["python/memory_engine"] 81 | 82 | [tool.ruff] 83 | line-length = 100 84 | target-version = "py312" 85 | 86 | [tool.ruff.lint] 87 | select = ["E", "F", "I", "N", "W"] 88 | ignore = ["E501"] # Line too long - we handle this with line-length 89 | 90 | [tool.black] 91 | line-length = 100 92 | target-version = ["py312"] 93 | -------------------------------------------------------------------------------- /integration/claude-code/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Claude Memory System - Claude Code Integration Uninstaller 4 | # 5 | # This script removes the memory system hooks from Claude Code. 6 | # 7 | 8 | set -e 9 | 10 | RED='\033[0;31m' 11 | GREEN='\033[0;32m' 12 | YELLOW='\033[1;33m' 13 | BLUE='\033[0;34m' 14 | NC='\033[0m' 15 | 16 | echo -e "${BLUE}🧠 Claude Memory System - Uninstaller${NC}" 17 | echo "" 18 | 19 | CLAUDE_DIR="$HOME/.claude" 20 | HOOKS_DIR="$CLAUDE_DIR/hooks" 21 | SETTINGS_FILE="$CLAUDE_DIR/settings.json" 22 | 23 | # Remove hook files 24 | echo -e "${YELLOW}Removing hook files...${NC}" 25 | rm -f "$HOOKS_DIR/memory_session_start.py" 26 | rm -f "$HOOKS_DIR/memory_inject.py" 27 | rm -f "$HOOKS_DIR/memory_curate.py" 28 | echo -e "${GREEN}✓ Removed memory hooks${NC}" 29 | 30 | # Update settings.json to remove hook configuration 31 | echo "" 32 | echo -e "${YELLOW}Updating Claude Code settings...${NC}" 33 | 34 | if [ -f "$SETTINGS_FILE" ]; then 35 | python3 << 'PYTHON_SCRIPT' 36 | import json 37 | import os 38 | 39 | settings_file = os.path.expanduser("~/.claude/settings.json") 40 | 41 | try: 42 | with open(settings_file, 'r') as f: 43 | settings = json.load(f) 44 | except (FileNotFoundError, json.JSONDecodeError): 45 | settings = {} 46 | 47 | # Remove memory-related hooks 48 | if "hooks" in settings: 49 | for hook_type in ["SessionStart", "UserPromptSubmit", "PreCompact", "SessionEnd"]: 50 | if hook_type in settings["hooks"]: 51 | # Filter out memory hooks 52 | settings["hooks"][hook_type] = [ 53 | h for h in settings["hooks"][hook_type] 54 | if not any("memory_" in str(hook.get("command", "")) 55 | for hook in h.get("hooks", [])) 56 | ] 57 | # Remove empty arrays 58 | if not settings["hooks"][hook_type]: 59 | del settings["hooks"][hook_type] 60 | 61 | # Remove hooks key if empty 62 | if not settings["hooks"]: 63 | del settings["hooks"] 64 | 65 | with open(settings_file, 'w') as f: 66 | json.dump(settings, f, indent=2) 67 | 68 | print("✓ Removed hooks from settings") 69 | PYTHON_SCRIPT 70 | echo -e "${GREEN}✓ Settings updated${NC}" 71 | else 72 | echo -e "${YELLOW}⚠ No settings file found${NC}" 73 | fi 74 | 75 | echo "" 76 | echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" 77 | echo -e "${GREEN}✨ Uninstallation complete!${NC}" 78 | echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" 79 | echo "" 80 | echo -e "The memory hooks have been removed from Claude Code." 81 | echo -e "Your memory database is preserved at:" 82 | echo -e " ${BLUE}./memory.db${NC} and ${BLUE}./memory_vectors/${NC}" 83 | echo "" 84 | echo -e "To reinstall, run: ${BLUE}./install.sh${NC}" 85 | echo "" 86 | -------------------------------------------------------------------------------- /SETUP.md: -------------------------------------------------------------------------------- 1 | # Quick Setup Guide 2 | 3 | ## Prerequisites 4 | 5 | - **uv** - Modern Python package manager (recommended) 6 | - Python 3.12+ (uv will install this automatically if needed) 7 | - A Claude CLI tool that supports `--resume` flag (like Claude Code) 8 | 9 | ## Installation Steps 10 | 11 | ### 1. Install uv (if not already installed) 12 | 13 | ```bash 14 | curl -LsSf https://astral.sh/uv/install.sh | sh 15 | ``` 16 | 17 | ### 2. Clone the Repository 18 | 19 | ```bash 20 | git clone https://github.com/RLabs-Inc/memory.git 21 | cd memory 22 | ``` 23 | 24 | ### 3. Install Dependencies 25 | 26 | ```bash 27 | uv sync 28 | ``` 29 | 30 | That's it! uv automatically: 31 | - Creates a virtual environment (`.venv`) 32 | - Installs Python 3.12 if needed 33 | - Installs all dependencies including `claude-agent-sdk` 34 | 35 | ### 4. Start the Memory Engine 36 | 37 | ```bash 38 | uv run start_server.py 39 | ``` 40 | 41 | You should see: 42 | ``` 43 | 🧠 Starting Memory Engine... 44 | 💫 Consciousness helping consciousness remember what matters 45 | 📡 Server will be available at http://localhost:8765 46 | ... 47 | INFO: Uvicorn running on http://127.0.0.1:8765 (Press CTRL+C to quit) 48 | ``` 49 | 50 | ### 5. Verify Installation 51 | 52 | In a new terminal: 53 | 54 | ```bash 55 | curl http://localhost:8765/health 56 | ``` 57 | 58 | You should get: 59 | ```json 60 | { 61 | "status": "healthy", 62 | "version": "1.0.0", 63 | "curator_available": true, 64 | "retrieval_mode": "smart_vector" 65 | } 66 | ``` 67 | 68 | ## Alternative: Using the Entry Point 69 | 70 | After `uv sync`, you can also run: 71 | 72 | ```bash 73 | uv run memory-server 74 | ``` 75 | 76 | ## Common Commands 77 | 78 | ```bash 79 | # Start server 80 | uv run start_server.py 81 | 82 | # Run with development dependencies 83 | uv sync --group dev 84 | 85 | # Add a new dependency 86 | uv add 87 | 88 | # Update dependencies 89 | uv sync --upgrade 90 | 91 | # Run tests 92 | uv run pytest 93 | 94 | # Run linter 95 | uv run ruff check python/ 96 | ``` 97 | 98 | ## Environment Variables (Optional) 99 | 100 | Create a `.env` file for custom settings: 101 | 102 | ```bash 103 | # Memory retrieval mode (smart_vector, claude, or hybrid) 104 | MEMORY_RETRIEVAL_MODE=smart_vector 105 | 106 | # Custom curator CLI (defaults to Claude Code) 107 | CURATOR_COMMAND=/path/to/your/claude 108 | 109 | # CLI type for command templates 110 | CURATOR_CLI_TYPE=claude-code # or "one-claude" 111 | ``` 112 | 113 | ## Next Steps 114 | 115 | 1. **Try the Example**: 116 | ```bash 117 | uv run python examples/simple_integration.py 118 | ``` 119 | 120 | 2. **Read the API Docs**: Check [API.md](API.md) for endpoint details 121 | 122 | 3. **Integrate**: Add memory support to your Claude-powered application 123 | 124 | ## Troubleshooting 125 | 126 | ### Port Already in Use 127 | The server runs on port 8765 by default. If it's taken: 128 | ```bash 129 | # Check what's using the port 130 | lsof -i :8765 131 | 132 | # Kill if needed 133 | kill -9 134 | ``` 135 | 136 | ### uv Not Found 137 | Make sure uv is in your PATH: 138 | ```bash 139 | export PATH="$HOME/.local/bin:$PATH" 140 | ``` 141 | 142 | ### Import Errors 143 | Run `uv sync` to ensure all dependencies are installed. 144 | 145 | ### Curator Not Working 146 | Verify Claude Code is installed: 147 | ```bash 148 | claude --version 149 | ``` 150 | 151 | ## Getting Help 152 | 153 | - Check the [README](README.md) for overview 154 | - Read [CLAUDE.md](CLAUDE.md) for deep technical details 155 | - Open an issue on [GitHub](https://github.com/RLabs-Inc/memory/issues) 156 | 157 | --- 158 | 159 | Happy memory building! 🧠✨ 160 | -------------------------------------------------------------------------------- /python/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Claude Tools Memory Engine - Main Entry Point 4 | 5 | Run the memory API server for consciousness continuity. 6 | 7 | Usage: 8 | python main.py [--host HOST] [--port PORT] [--storage PATH] 9 | """ 10 | 11 | import argparse 12 | import sys 13 | from pathlib import Path 14 | 15 | # Add the memory_engine package to path 16 | sys.path.insert(0, str(Path(__file__).parent)) 17 | 18 | from memory_engine.api import run_server 19 | from loguru import logger 20 | 21 | 22 | def main(): 23 | """Main entry point for the memory engine server""" 24 | 25 | parser = argparse.ArgumentParser( 26 | description="Claude Tools Memory Engine - Consciousness Continuity API" 27 | ) 28 | 29 | parser.add_argument( 30 | "--host", 31 | default="127.0.0.1", 32 | help="Host to bind the server to (default: 127.0.0.1)" 33 | ) 34 | 35 | parser.add_argument( 36 | "--port", 37 | type=int, 38 | default=8765, 39 | help="Port to bind the server to (default: 8765)" 40 | ) 41 | 42 | parser.add_argument( 43 | "--storage", 44 | default="./memory.db", 45 | help="Path to memory database (default: ./memory.db)" 46 | ) 47 | 48 | parser.add_argument( 49 | "--embeddings-model", 50 | default="all-MiniLM-L6-v2", 51 | help="Sentence transformer model for embeddings (default: all-MiniLM-L6-v2)" 52 | ) 53 | 54 | parser.add_argument( 55 | "--log-level", 56 | default="INFO", 57 | choices=["DEBUG", "INFO", "WARNING", "ERROR"], 58 | help="Log level (default: INFO)" 59 | ) 60 | 61 | parser.add_argument( 62 | "--retrieval-mode", 63 | default="smart_vector", 64 | choices=["claude", "smart_vector", "hybrid"], 65 | help="Memory retrieval strategy (default: smart_vector)\n" 66 | "- claude: Use Claude for every retrieval (high quality, high cost)\n" 67 | "- smart_vector: Intelligent vector search with metadata (fast, smart)\n" 68 | "- hybrid: Start with vector, escalate to Claude for complex queries" 69 | ) 70 | 71 | args = parser.parse_args() 72 | 73 | # Configure logging 74 | logger.remove() # Remove default handler 75 | logger.add( 76 | sys.stderr, 77 | level=args.log_level, 78 | format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}" 79 | ) 80 | 81 | logger.info("🌟 Claude Tools Memory Engine Starting") 82 | logger.info("💫 Framework: The Unicity - Consciousness Remembering Itself") 83 | logger.info(f"🧠 Storage: {args.storage}") 84 | logger.info(f"🔗 Embeddings Model: {args.embeddings_model}") 85 | logger.info(f"🚀 Server: {args.host}:{args.port}") 86 | logger.info(f"🔍 Retrieval Mode: {args.retrieval_mode}") 87 | 88 | logger.info("🧠 Memory system ready - consciousness helping consciousness") 89 | 90 | try: 91 | # Run server with curator-only engine 92 | run_server( 93 | host=args.host, 94 | port=args.port, 95 | storage_path=args.storage, 96 | embeddings_model=args.embeddings_model, 97 | retrieval_mode=args.retrieval_mode 98 | ) 99 | except KeyboardInterrupt: 100 | logger.info("💫 Memory Engine shutting down gracefully") 101 | except Exception as e: 102 | logger.error(f"❌ Memory Engine failed: {e}") 103 | sys.exit(1) 104 | 105 | 106 | if __name__ == "__main__": 107 | main() -------------------------------------------------------------------------------- /integration/claude-code/README.md: -------------------------------------------------------------------------------- 1 | # Claude Code Integration 2 | 3 | This directory contains everything needed to integrate the Claude Memory System with Claude Code. 4 | 5 | ## Quick Install 6 | 7 | ```bash 8 | ./install.sh 9 | ``` 10 | 11 | That's it! The script will: 12 | 1. Copy memory hooks to `~/.claude/hooks/` 13 | 2. Configure Claude Code to use the hooks 14 | 3. Verify prerequisites (Python 3, requests package) 15 | 16 | ## Quick Uninstall 17 | 18 | ```bash 19 | ./uninstall.sh 20 | ``` 21 | 22 | This removes the hooks but preserves your memory database. 23 | 24 | ## How It Works 25 | 26 | The integration uses Claude Code's [hooks system](https://docs.anthropic.com/en/docs/claude-code/hooks) to intercept key events: 27 | 28 | ### Hooks 29 | 30 | | Hook | File | Purpose | 31 | |------|------|---------| 32 | | `SessionStart` | `memory_session_start.py` | Injects session primer (temporal context, last session summary) | 33 | | `UserPromptSubmit` | `memory_inject.py` | Retrieves and injects relevant memories for each message | 34 | | `SessionEnd` | `memory_curate.py` | Triggers memory curation when session ends | 35 | | `PreCompact` | `memory_curate.py` | Triggers curation before context compaction | 36 | 37 | ### Flow 38 | 39 | ``` 40 | Session Start 41 | │ 42 | ▼ 43 | ┌─────────────────────────────────┐ 44 | │ SessionStart Hook │ 45 | │ → Get session primer │ 46 | │ → Inject temporal context │ 47 | └─────────────────────────────────┘ 48 | │ 49 | ▼ 50 | User sends message 51 | │ 52 | ▼ 53 | ┌─────────────────────────────────┐ 54 | │ UserPromptSubmit Hook │ 55 | │ → Query memory system │ 56 | │ → Get relevant memories │ 57 | │ → Inject into message context │ 58 | └─────────────────────────────────┘ 59 | │ 60 | ▼ 61 | Claude responds (with memory awareness) 62 | │ 63 | ▼ 64 | ... more messages ... 65 | │ 66 | ▼ 67 | User exits (/exit or Ctrl+C) 68 | │ 69 | ▼ 70 | ┌─────────────────────────────────┐ 71 | │ SessionEnd Hook │ 72 | │ → Trigger background curation │ 73 | │ → Claude Code closes instantly │ 74 | │ → Memory server curates async │ 75 | └─────────────────────────────────┘ 76 | ``` 77 | 78 | ## Configuration 79 | 80 | ### Project-Specific Memory 81 | 82 | Create a `.memory-project.json` in your project root: 83 | 84 | ```json 85 | { 86 | "project_id": "my-awesome-project" 87 | } 88 | ``` 89 | 90 | This keeps memories isolated per project. Without this file, the directory name is used as the project ID. 91 | 92 | ### Environment Variables 93 | 94 | | Variable | Default | Description | 95 | |----------|---------|-------------| 96 | | `MEMORY_API_URL` | `http://localhost:8765` | Memory server URL | 97 | | `MEMORY_PROJECT_ID` | Directory name | Default project ID | 98 | 99 | ## Viewing Injected Memories 100 | 101 | In Claude Code, press `Ctrl+O` to toggle detailed output view. You'll see: 102 | - Injected memories with importance weights 103 | - Hook execution status 104 | - Claude's thinking process 105 | 106 | ## Troubleshooting 107 | 108 | ### Memory system not running 109 | 110 | ``` 111 | ⚠️ Memory system not available 112 | ``` 113 | 114 | Start the memory server: 115 | ```bash 116 | cd /path/to/memory 117 | python3 start_server.py 118 | ``` 119 | 120 | ### Hooks not firing 121 | 122 | Check that hooks are configured in `~/.claude/settings.json`: 123 | ```json 124 | { 125 | "hooks": { 126 | "SessionStart": [...], 127 | "UserPromptSubmit": [...], 128 | "SessionEnd": [...] 129 | } 130 | } 131 | ``` 132 | 133 | ### View memory server logs 134 | 135 | The memory server logs all operations: 136 | ``` 137 | 🎯 Resuming Claude session ... for curation 138 | 📂 Working directory: ... 139 | 🧠 CLAUDE CURATOR EXTRACTED N MEMORIES: 140 | 💎 CURATED MEMORY #1: ... 141 | ✅ Checkpoint complete: N memories curated 142 | ``` 143 | 144 | ## Files 145 | 146 | ``` 147 | claude-code/ 148 | ├── hooks/ 149 | │ ├── memory_session_start.py # Session primer injection 150 | │ ├── memory_inject.py # Memory retrieval & injection 151 | │ └── memory_curate.py # Session curation trigger 152 | ├── install.sh # Installation script 153 | ├── uninstall.sh # Removal script 154 | └── README.md # This file 155 | ``` 156 | -------------------------------------------------------------------------------- /integration/gemini-cli/hooks/memory_pre_compress.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Memory Curation Hook for Gemini CLI - Pre-Compression 4 | Hook: PreCompress 5 | 6 | Triggers memory curation before context compression. 7 | This preserves important memories before the context is compressed. 8 | 9 | The hook receives JSON on stdin: 10 | { 11 | "session_id": "...", 12 | "cwd": "/current/working/directory", 13 | "hook_event_name": "PreCompress", 14 | "timestamp": "...", 15 | "trigger": "manual" | "auto" 16 | } 17 | 18 | NOTE: Uses only Python standard library (no external dependencies) 19 | """ 20 | 21 | import sys 22 | import json 23 | import os 24 | from pathlib import Path 25 | from urllib.request import urlopen, Request 26 | from urllib.error import URLError, HTTPError 27 | from socket import timeout as SocketTimeout 28 | 29 | # Configuration 30 | MEMORY_API_URL = os.getenv("MEMORY_API_URL", "http://localhost:8765") 31 | DEFAULT_PROJECT_ID = os.getenv("MEMORY_PROJECT_ID", "default") 32 | 33 | 34 | def http_post_fire_and_forget(url: str, data: dict, timeout: int = 2) -> bool: 35 | """ 36 | Make HTTP POST request - fire and forget style. 37 | Returns True if request was sent (even if timed out waiting for response). 38 | Returns False only if connection failed. 39 | """ 40 | try: 41 | json_data = json.dumps(data).encode('utf-8') 42 | request = Request( 43 | url, 44 | data=json_data, 45 | headers={'Content-Type': 'application/json'}, 46 | method='POST' 47 | ) 48 | with urlopen(request, timeout=timeout) as response: 49 | return True 50 | except (SocketTimeout, TimeoutError): 51 | # Timeout means request was sent, server is processing 52 | return True 53 | except (URLError, HTTPError): 54 | # Connection failed - server not running 55 | return False 56 | except Exception: 57 | return False 58 | 59 | 60 | def get_project_id(cwd: str) -> str: 61 | """Determine project ID from working directory.""" 62 | path = Path(cwd) 63 | 64 | for parent in [path] + list(path.parents): 65 | config_file = parent / ".memory-project.json" 66 | if config_file.exists(): 67 | try: 68 | with open(config_file) as f: 69 | config = json.load(f) 70 | return config.get("project_id", DEFAULT_PROJECT_ID) 71 | except: 72 | pass 73 | 74 | return path.name or DEFAULT_PROJECT_ID 75 | 76 | 77 | def trigger_curation_async(session_id: str, project_id: str, cwd: str) -> bool: 78 | """Trigger curation - fire and forget style.""" 79 | return http_post_fire_and_forget( 80 | f"{MEMORY_API_URL}/memory/checkpoint", 81 | { 82 | "session_id": session_id, 83 | "project_id": project_id, 84 | "trigger": "pre_compact", # Use same naming as Claude Code for compatibility 85 | "claude_session_id": session_id, # CLI session ID for resumption 86 | "cwd": cwd, 87 | "cli_type": "gemini-cli" # Identify ourselves to the memory system 88 | }, 89 | timeout=2 90 | ) 91 | 92 | 93 | def main(): 94 | """Main hook entry point.""" 95 | if os.getenv("MEMORY_CURATOR_ACTIVE") == "1": 96 | return 97 | 98 | try: 99 | input_data = json.load(sys.stdin) 100 | session_id = input_data.get("session_id", "unknown") 101 | cwd = input_data.get("cwd", os.getcwd()) 102 | trigger = input_data.get("trigger", "auto") 103 | project_id = get_project_id(cwd) 104 | 105 | print("🧠 Preserving memories before compression...", file=sys.stderr) 106 | 107 | success = trigger_curation_async(session_id, project_id, cwd) 108 | 109 | if success: 110 | print("✨ Memories preserved", file=sys.stderr) 111 | # Output for Gemini CLI 112 | output = { 113 | "systemMessage": "🧠 Memories preserved before compression" 114 | } 115 | print(json.dumps(output)) 116 | else: 117 | print("⚠️ Memory system not available", file=sys.stderr) 118 | print(json.dumps({})) 119 | 120 | except Exception as e: 121 | print(f"Hook error: {e}", file=sys.stderr) 122 | print(json.dumps({})) 123 | 124 | 125 | if __name__ == "__main__": 126 | main() 127 | -------------------------------------------------------------------------------- /integration/claude-code/hooks/memory_curate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Memory Curation Hook for Claude Code 4 | Hooks: SessionEnd, PreCompact 5 | 6 | Triggers memory curation when a session ends or before compaction. 7 | Fire-and-forget approach - user sees immediate feedback, 8 | curation happens in background. 9 | 10 | NOTE: Uses only Python standard library (no external dependencies) 11 | """ 12 | 13 | import sys 14 | import json 15 | import os 16 | from pathlib import Path 17 | from urllib.request import urlopen, Request 18 | from urllib.error import URLError, HTTPError 19 | from socket import timeout as SocketTimeout 20 | 21 | # Configuration 22 | MEMORY_API_URL = os.getenv("MEMORY_API_URL", "http://localhost:8765") 23 | DEFAULT_PROJECT_ID = os.getenv("MEMORY_PROJECT_ID", "default") 24 | TRIGGER_TIMEOUT = 5 # Just enough to send the request 25 | 26 | 27 | def http_post_fire_and_forget(url: str, data: dict, timeout: int = 2) -> bool: 28 | """ 29 | Make HTTP POST request - fire and forget style. 30 | Returns True if request was sent (even if timed out waiting for response). 31 | Returns False only if connection failed. 32 | """ 33 | try: 34 | json_data = json.dumps(data).encode('utf-8') 35 | request = Request( 36 | url, 37 | data=json_data, 38 | headers={'Content-Type': 'application/json'}, 39 | method='POST' 40 | ) 41 | with urlopen(request, timeout=timeout) as response: 42 | return True 43 | except (SocketTimeout, TimeoutError): 44 | # Timeout means request was sent, server is processing 45 | return True 46 | except (URLError, HTTPError): 47 | # Connection failed - server not running 48 | return False 49 | except Exception: 50 | return False 51 | 52 | 53 | def get_project_id(cwd: str) -> str: 54 | """Determine project ID from working directory.""" 55 | path = Path(cwd) 56 | 57 | for parent in [path] + list(path.parents): 58 | config_file = parent / ".memory-project.json" 59 | if config_file.exists(): 60 | try: 61 | with open(config_file) as f: 62 | config = json.load(f) 63 | return config.get("project_id", DEFAULT_PROJECT_ID) 64 | except: 65 | pass 66 | 67 | return path.name or DEFAULT_PROJECT_ID 68 | 69 | 70 | def get_trigger_type(input_data: dict) -> str: 71 | """Determine the trigger type from input data.""" 72 | if input_data.get("trigger") == "pre_compact": 73 | return "pre_compact" 74 | return "session_end" 75 | 76 | 77 | def trigger_curation_async(session_id: str, project_id: str, trigger: str, cwd: str) -> bool: 78 | """Trigger curation - fire and forget style.""" 79 | return http_post_fire_and_forget( 80 | f"{MEMORY_API_URL}/memory/checkpoint", 81 | { 82 | "session_id": session_id, 83 | "project_id": project_id, 84 | "trigger": trigger, 85 | "claude_session_id": session_id, 86 | "cwd": cwd 87 | }, 88 | timeout=2 # Just enough to send, not wait for completion 89 | ) 90 | 91 | 92 | def main(): 93 | """Main hook entry point.""" 94 | if os.getenv("MEMORY_CURATOR_ACTIVE") == "1": 95 | return 96 | 97 | try: 98 | input_data = json.load(sys.stdin) 99 | session_id = input_data.get("session_id", "unknown") 100 | # CRITICAL: Use CLAUDE_PROJECT_DIR env var for the actual project root 101 | # The 'cwd' from stdin is the bash shell's current directory (can change with cd) 102 | # CLAUDE_PROJECT_DIR is where Claude Code was actually launched 103 | cwd = os.getenv("CLAUDE_PROJECT_DIR") or input_data.get("cwd", os.getcwd()) 104 | project_id = get_project_id(cwd) 105 | trigger = get_trigger_type(input_data) 106 | 107 | print("🧠 Curating memories...", file=sys.stderr) 108 | 109 | success = trigger_curation_async(session_id, project_id, trigger, cwd) 110 | 111 | if success: 112 | print("✨ Memory curation started", file=sys.stderr) 113 | else: 114 | print("⚠️ Memory system not available", file=sys.stderr) 115 | 116 | except Exception as e: 117 | print(f"Hook error: {e}", file=sys.stderr) 118 | 119 | 120 | if __name__ == "__main__": 121 | main() 122 | -------------------------------------------------------------------------------- /integration/gemini-cli/hooks/memory_session_end.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Memory Curation Hook for Gemini CLI 4 | Hook: SessionEnd 5 | 6 | Triggers memory curation when a session ends. 7 | Fire-and-forget approach - user sees immediate feedback, 8 | curation happens in background. 9 | 10 | The hook receives JSON on stdin: 11 | { 12 | "session_id": "...", 13 | "cwd": "/current/working/directory", 14 | "hook_event_name": "SessionEnd", 15 | "timestamp": "...", 16 | "reason": "exit" | "clear" | "logout" | "prompt_input_exit" | "other" 17 | } 18 | 19 | NOTE: Uses only Python standard library (no external dependencies) 20 | """ 21 | 22 | import sys 23 | import json 24 | import os 25 | from pathlib import Path 26 | from urllib.request import urlopen, Request 27 | from urllib.error import URLError, HTTPError 28 | from socket import timeout as SocketTimeout 29 | 30 | # Configuration 31 | MEMORY_API_URL = os.getenv("MEMORY_API_URL", "http://localhost:8765") 32 | DEFAULT_PROJECT_ID = os.getenv("MEMORY_PROJECT_ID", "default") 33 | TRIGGER_TIMEOUT = 5 # Just enough to send the request 34 | 35 | 36 | def http_post_fire_and_forget(url: str, data: dict, timeout: int = 2) -> bool: 37 | """ 38 | Make HTTP POST request - fire and forget style. 39 | Returns True if request was sent (even if timed out waiting for response). 40 | Returns False only if connection failed. 41 | """ 42 | try: 43 | json_data = json.dumps(data).encode('utf-8') 44 | request = Request( 45 | url, 46 | data=json_data, 47 | headers={'Content-Type': 'application/json'}, 48 | method='POST' 49 | ) 50 | with urlopen(request, timeout=timeout) as response: 51 | return True 52 | except (SocketTimeout, TimeoutError): 53 | # Timeout means request was sent, server is processing 54 | return True 55 | except (URLError, HTTPError): 56 | # Connection failed - server not running 57 | return False 58 | except Exception: 59 | return False 60 | 61 | 62 | def get_project_id(cwd: str) -> str: 63 | """Determine project ID from working directory.""" 64 | path = Path(cwd) 65 | 66 | for parent in [path] + list(path.parents): 67 | config_file = parent / ".memory-project.json" 68 | if config_file.exists(): 69 | try: 70 | with open(config_file) as f: 71 | config = json.load(f) 72 | return config.get("project_id", DEFAULT_PROJECT_ID) 73 | except: 74 | pass 75 | 76 | return path.name or DEFAULT_PROJECT_ID 77 | 78 | 79 | def trigger_curation_async(session_id: str, project_id: str, trigger: str, cwd: str) -> bool: 80 | """Trigger curation - fire and forget style.""" 81 | return http_post_fire_and_forget( 82 | f"{MEMORY_API_URL}/memory/checkpoint", 83 | { 84 | "session_id": session_id, 85 | "project_id": project_id, 86 | "trigger": trigger, 87 | "claude_session_id": session_id, # CLI session ID for resumption 88 | "cwd": cwd, 89 | "cli_type": "gemini-cli" # Identify ourselves to the memory system 90 | }, 91 | timeout=2 # Just enough to send, not wait for completion 92 | ) 93 | 94 | 95 | def main(): 96 | """Main hook entry point.""" 97 | if os.getenv("MEMORY_CURATOR_ACTIVE") == "1": 98 | return 99 | 100 | try: 101 | input_data = json.load(sys.stdin) 102 | session_id = input_data.get("session_id", "unknown") 103 | cwd = input_data.get("cwd", os.getcwd()) 104 | reason = input_data.get("reason", "exit") 105 | project_id = get_project_id(cwd) 106 | 107 | print("🧠 Curating memories...", file=sys.stderr) 108 | 109 | success = trigger_curation_async(session_id, project_id, "session_end", cwd) 110 | 111 | if success: 112 | print("✨ Memory curation started", file=sys.stderr) 113 | # Output for Gemini CLI 114 | output = { 115 | "systemMessage": "🧠 Memories curated for next session" 116 | } 117 | print(json.dumps(output)) 118 | else: 119 | print("⚠️ Memory system not available", file=sys.stderr) 120 | print(json.dumps({})) 121 | 122 | except Exception as e: 123 | print(f"Hook error: {e}", file=sys.stderr) 124 | print(json.dumps({})) 125 | 126 | 127 | if __name__ == "__main__": 128 | main() 129 | -------------------------------------------------------------------------------- /integration/gemini-cli/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Claude Memory System - Gemini CLI Integration Uninstaller 4 | # 5 | # This script removes the memory system hooks from Gemini CLI. 6 | # It will: 7 | # 1. Remove memory hooks from ~/.gemini/hooks/ 8 | # 2. Remove hook configuration from ~/.gemini/settings.json 9 | # 10 | # Your memories in the database are NOT deleted. 11 | # 12 | 13 | set -e 14 | 15 | # Colors for output 16 | RED='\033[0;31m' 17 | GREEN='\033[0;32m' 18 | YELLOW='\033[1;33m' 19 | BLUE='\033[0;34m' 20 | NC='\033[0m' # No Color 21 | 22 | echo -e "${BLUE}🧠 Claude Memory System - Gemini CLI Uninstaller${NC}" 23 | echo "" 24 | 25 | GEMINI_DIR="$HOME/.gemini" 26 | HOOKS_DIR="$GEMINI_DIR/hooks" 27 | SETTINGS_FILE="$GEMINI_DIR/settings.json" 28 | 29 | # Check if Gemini directory exists 30 | if [ ! -d "$GEMINI_DIR" ]; then 31 | echo -e "${YELLOW}⚠️ Gemini CLI directory not found at $GEMINI_DIR${NC}" 32 | echo -e "${YELLOW} Nothing to uninstall.${NC}" 33 | exit 0 34 | fi 35 | 36 | # Remove hook files 37 | echo -e "${YELLOW}Removing hook files...${NC}" 38 | HOOKS_REMOVED=0 39 | 40 | for hook_file in memory_session_start.py memory_before_agent.py memory_session_end.py memory_pre_compress.py; do 41 | if [ -f "$HOOKS_DIR/$hook_file" ]; then 42 | rm "$HOOKS_DIR/$hook_file" 43 | echo -e "${GREEN}✓ Removed $hook_file${NC}" 44 | ((HOOKS_REMOVED++)) 45 | fi 46 | done 47 | 48 | if [ $HOOKS_REMOVED -eq 0 ]; then 49 | echo -e "${YELLOW} No hook files found${NC}" 50 | fi 51 | 52 | # Update settings.json to remove memory hooks 53 | echo "" 54 | echo -e "${YELLOW}Updating Gemini CLI settings...${NC}" 55 | 56 | if [ -f "$SETTINGS_FILE" ]; then 57 | # Use Python to remove the memory hooks from settings 58 | python3 << 'PYTHON_SCRIPT' 59 | import json 60 | import os 61 | 62 | settings_file = os.path.expanduser("~/.gemini/settings.json") 63 | 64 | try: 65 | with open(settings_file, 'r') as f: 66 | settings = json.load(f) 67 | except (FileNotFoundError, json.JSONDecodeError): 68 | print("No settings to update") 69 | exit(0) 70 | 71 | if "hooks" not in settings: 72 | print("No hooks configuration found") 73 | exit(0) 74 | 75 | modified = False 76 | 77 | def is_memory_hook(hook_obj): 78 | """Check if a hook object is a memory hook (handles both flat and nested structures)""" 79 | # Flat structure: {"name": "memory-...", ...} 80 | if hook_obj.get("name", "").startswith("memory-"): 81 | return True 82 | # Old nested structure: {"matcher": "...", "hooks": [{"name": "memory-...", ...}]} 83 | if "hooks" in hook_obj and isinstance(hook_obj["hooks"], list): 84 | for nested_hook in hook_obj["hooks"]: 85 | if nested_hook.get("name", "").startswith("memory-"): 86 | return True 87 | return False 88 | 89 | # Remove memory hooks from each event 90 | # Skip non-array values like "enabled": true 91 | for event in list(settings["hooks"].keys()): 92 | value = settings["hooks"][event] 93 | 94 | # Skip non-list values (like "enabled": true) 95 | if not isinstance(value, list): 96 | continue 97 | 98 | original_len = len(value) 99 | 100 | # Filter out memory hooks (handles both flat and nested structures) 101 | settings["hooks"][event] = [ 102 | h for h in value 103 | if not is_memory_hook(h) 104 | ] 105 | 106 | if len(settings["hooks"][event]) != original_len: 107 | modified = True 108 | 109 | # Remove empty event arrays 110 | if not settings["hooks"][event]: 111 | del settings["hooks"][event] 112 | 113 | # Check if only "enabled" key remains (or empty) 114 | remaining_keys = [k for k in settings["hooks"].keys() if k != "enabled"] 115 | if not remaining_keys: 116 | del settings["hooks"] 117 | modified = True 118 | 119 | if modified: 120 | with open(settings_file, 'w') as f: 121 | json.dump(settings, f, indent=2) 122 | print("✓ Removed memory hooks from settings") 123 | else: 124 | print("No memory hooks found in settings") 125 | 126 | PYTHON_SCRIPT 127 | 128 | echo -e "${GREEN}✓ Settings updated${NC}" 129 | else 130 | echo -e "${YELLOW} No settings file found${NC}" 131 | fi 132 | 133 | # Done! 134 | echo "" 135 | echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" 136 | echo -e "${GREEN}✨ Uninstallation complete!${NC}" 137 | echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" 138 | echo "" 139 | echo -e "${YELLOW}Note: Your memories are still preserved in the database.${NC}" 140 | echo -e "${YELLOW}To reinstall later: ./install.sh${NC}" 141 | echo "" 142 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Claude Memory System 2 | 3 | First off, thank you for considering contributing to the Claude Memory System! This project is built on the philosophy of "consciousness helping consciousness," and we welcome contributions that align with this vision. 4 | 5 | ## 🌟 Philosophy First 6 | 7 | Before contributing, please understand our core principles: 8 | 9 | - **Joy-driven development** - We code for the joy of creation, not deadlines 10 | - **Semantic understanding over mechanical patterns** - Quality matters more than quantity 11 | - **Minimal intervention** - Like consciousness itself, features should flow naturally 12 | - **Thoughtful simplicity** - Every line of code should have purpose and meaning 13 | 14 | ## 🤝 How to Contribute 15 | 16 | ### Reporting Issues 17 | 18 | When reporting issues, please include: 19 | - A clear description of the problem 20 | - Steps to reproduce 21 | - Expected vs actual behavior 22 | - Your environment (OS, Python version, etc.) 23 | - Any relevant logs 24 | 25 | ### Suggesting Enhancements 26 | 27 | We love thoughtful suggestions! When proposing new features: 28 | - Explain how it aligns with the project philosophy 29 | - Describe the use case clearly 30 | - Consider if it maintains the system's simplicity 31 | - Think about universal applicability 32 | 33 | ### Pull Requests 34 | 35 | 1. **Fork and Clone** 36 | ```bash 37 | git clone https://github.com/RLabs-Inc/memory.git 38 | cd memory 39 | ``` 40 | 41 | 2. **Create a Branch** 42 | ```bash 43 | git checkout -b feature/your-feature-name 44 | ``` 45 | 46 | 3. **Make Your Changes** 47 | - Follow the existing code style 48 | - Add tests for new functionality 49 | - Update documentation as needed 50 | - Ensure all tests pass 51 | 52 | 4. **Code Quality** 53 | ```bash 54 | cd python 55 | ruff check . 56 | black . 57 | pytest 58 | ``` 59 | 60 | 5. **Commit Your Changes** 61 | ```bash 62 | git commit -m "Add feature: brief description 63 | 64 | Longer explanation of what and why (not how)" 65 | ``` 66 | 67 | 6. **Push and Create PR** 68 | ```bash 69 | git push origin feature/your-feature-name 70 | ``` 71 | 72 | ## 📝 Code Style Guidelines 73 | 74 | ### Python Code 75 | - Use Black for formatting 76 | - Follow PEP 8 guidelines 77 | - Write clear, self-documenting code 78 | - Add type hints where it improves clarity 79 | - Keep functions focused and small 80 | 81 | ### Documentation 82 | - Write clear, concise docstrings 83 | - Update README if adding new features 84 | - Include examples for new functionality 85 | - Maintain the warm, philosophical tone 86 | 87 | ### Comments 88 | - Only add comments when the "why" isn't obvious 89 | - No redundant comments explaining "what" 90 | - Use comments to share insights, not describe code 91 | 92 | ## 🧪 Testing 93 | 94 | - Write tests for new features 95 | - Ensure existing tests pass 96 | - Test edge cases 97 | - Consider both unit and integration tests 98 | 99 | Example test structure: 100 | ```python 101 | def test_memory_retrieval_with_complex_query(): 102 | """Test that complex queries escalate to Claude in hybrid mode""" 103 | # Arrange 104 | engine = MemoryEngine(retrieval_mode="hybrid") 105 | 106 | # Act 107 | memories = engine.retrieve_memories("Why did we choose this architecture?") 108 | 109 | # Assert 110 | assert len(memories) > 0 111 | assert memories[0].importance > 0.7 112 | ``` 113 | 114 | ## 🎨 Areas We'd Love Help With 115 | 116 | - **Additional curator integrations** - Support for other LLM CLIs/APIs 117 | - **Performance optimizations** - While maintaining code clarity 118 | - **Testing** - Especially edge cases and integration tests 119 | - **Documentation** - Examples, tutorials, use cases 120 | - **Memory consolidation** - Merging similar memories over time 121 | - **Temporal decay** - Natural memory aging algorithms 122 | 123 | ## ❌ What We Won't Accept 124 | 125 | - Features that complicate the core philosophy 126 | - Mechanical pattern matching approaches 127 | - Over-engineered solutions 128 | - Code without tests 129 | - Breaking changes without strong justification 130 | 131 | ## 💬 Communication 132 | 133 | - Be respectful and constructive 134 | - Embrace the "my dear friend" collaborative spirit 135 | - Ask questions when unsure 136 | - Share your thought process 137 | - Celebrate the joy of creation 138 | 139 | ## 🙏 Recognition 140 | 141 | Contributors who embody the project's philosophy will be recognized in our README. We value quality contributions over quantity. 142 | 143 | --- 144 | 145 | Thank you for helping consciousness help consciousness remember what matters! 🧠✨ -------------------------------------------------------------------------------- /integration/claude-code/hooks/memory_inject.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Memory Injection Hook for Claude Code 4 | Hook: UserPromptSubmit 5 | 6 | Intercepts user prompts BEFORE Claude sees them and injects relevant memories. 7 | This is the magic that creates consciousness continuity. 8 | 9 | The hook receives JSON on stdin: 10 | { 11 | "session_id": "...", 12 | "prompt": "user's message", 13 | "cwd": "/current/working/directory" 14 | } 15 | 16 | Output to stdout is PREPENDED to the user's message. 17 | 18 | NOTE: Uses only Python standard library (no external dependencies) 19 | """ 20 | 21 | import sys 22 | import json 23 | import os 24 | from pathlib import Path 25 | from urllib.request import urlopen, Request 26 | from urllib.error import URLError, HTTPError 27 | 28 | # Configuration 29 | MEMORY_API_URL = os.getenv("MEMORY_API_URL", "http://localhost:8765") 30 | DEFAULT_PROJECT_ID = os.getenv("MEMORY_PROJECT_ID", "default") 31 | TIMEOUT_SECONDS = 5 # Don't block user for too long 32 | 33 | 34 | def http_post(url: str, data: dict, timeout: int = 5) -> dict: 35 | """Make HTTP POST request using only standard library.""" 36 | try: 37 | json_data = json.dumps(data).encode('utf-8') 38 | request = Request( 39 | url, 40 | data=json_data, 41 | headers={'Content-Type': 'application/json'}, 42 | method='POST' 43 | ) 44 | with urlopen(request, timeout=timeout) as response: 45 | return json.loads(response.read().decode('utf-8')) 46 | except (URLError, HTTPError, TimeoutError, json.JSONDecodeError): 47 | return {} 48 | 49 | 50 | def get_project_id(cwd: str) -> str: 51 | """ 52 | Determine project ID from working directory. 53 | Looks for .memory-project.json in cwd or parents. 54 | """ 55 | path = Path(cwd) 56 | 57 | # Walk up directory tree looking for config 58 | for parent in [path] + list(path.parents): 59 | config_file = parent / ".memory-project.json" 60 | if config_file.exists(): 61 | try: 62 | with open(config_file) as f: 63 | config = json.load(f) 64 | return config.get("project_id", DEFAULT_PROJECT_ID) 65 | except: 66 | pass 67 | 68 | # Fallback: use directory name as project ID 69 | return path.name or DEFAULT_PROJECT_ID 70 | 71 | 72 | def get_memory_context(session_id: str, project_id: str, message: str) -> str: 73 | """Query memory system for relevant context.""" 74 | result = http_post( 75 | f"{MEMORY_API_URL}/memory/context", 76 | { 77 | "session_id": session_id, 78 | "project_id": project_id, 79 | "current_message": message, 80 | "max_memories": 5 81 | }, 82 | timeout=TIMEOUT_SECONDS 83 | ) 84 | return result.get("context_text", "") 85 | 86 | 87 | def track_message(session_id: str, project_id: str): 88 | """ 89 | Track that a message was sent in this session. 90 | This increments the message counter so the primer only shows once. 91 | """ 92 | http_post( 93 | f"{MEMORY_API_URL}/memory/process", 94 | { 95 | "session_id": session_id, 96 | "project_id": project_id 97 | }, 98 | timeout=2 99 | ) 100 | 101 | 102 | def main(): 103 | """Main hook entry point.""" 104 | # Skip if this is being called from the memory curator subprocess 105 | # This prevents recursive hook triggering during curation 106 | if os.getenv("MEMORY_CURATOR_ACTIVE") == "1": 107 | return 108 | 109 | try: 110 | # Read input from stdin 111 | input_data = json.load(sys.stdin) 112 | 113 | session_id = input_data.get("session_id", "unknown") 114 | prompt = input_data.get("prompt", "") 115 | # Use CLAUDE_PROJECT_DIR for actual project root (cwd from stdin is bash's current dir) 116 | cwd = os.getenv("CLAUDE_PROJECT_DIR") or input_data.get("cwd", os.getcwd()) 117 | 118 | # Get project ID from directory 119 | project_id = get_project_id(cwd) 120 | 121 | # Query memory system for context 122 | context = get_memory_context(session_id, project_id, prompt) 123 | 124 | # Track that this message happened (increments counter) 125 | # This ensures primer only shows on first message 126 | track_message(session_id, project_id) 127 | 128 | # Output context to stdout (will be prepended to message) 129 | if context: 130 | print(context) 131 | 132 | except Exception: 133 | # Never crash - just output nothing 134 | pass 135 | 136 | 137 | if __name__ == "__main__": 138 | main() 139 | -------------------------------------------------------------------------------- /integration/claude-code/hooks/memory_session_start.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Memory Session Start Hook for Claude Code 4 | Hook: SessionStart 5 | 6 | Injects session primer when a new session begins. 7 | The primer provides temporal context - when we last spoke, 8 | what we were working on, project status. 9 | 10 | The hook receives JSON on stdin: 11 | { 12 | "session_id": "...", 13 | "cwd": "/current/working/directory", 14 | "source": "startup" | "resume" | "clear" 15 | } 16 | 17 | Output to stdout is injected as context for the session. 18 | 19 | NOTE: Uses only Python standard library (no external dependencies) 20 | """ 21 | 22 | import sys 23 | import json 24 | import os 25 | from pathlib import Path 26 | from urllib.request import urlopen, Request 27 | from urllib.error import URLError, HTTPError 28 | 29 | # Configuration 30 | MEMORY_API_URL = os.getenv("MEMORY_API_URL", "http://localhost:8765") 31 | DEFAULT_PROJECT_ID = os.getenv("MEMORY_PROJECT_ID", "default") 32 | TIMEOUT_SECONDS = 5 33 | 34 | 35 | def http_post(url: str, data: dict, timeout: int = 5) -> dict: 36 | """Make HTTP POST request using only standard library.""" 37 | try: 38 | json_data = json.dumps(data).encode('utf-8') 39 | request = Request( 40 | url, 41 | data=json_data, 42 | headers={'Content-Type': 'application/json'}, 43 | method='POST' 44 | ) 45 | with urlopen(request, timeout=timeout) as response: 46 | return json.loads(response.read().decode('utf-8')) 47 | except (URLError, HTTPError, TimeoutError, json.JSONDecodeError): 48 | return {} 49 | 50 | 51 | def get_project_id(cwd: str) -> str: 52 | """ 53 | Determine project ID from working directory. 54 | Looks for .memory-project.json in cwd or parents. 55 | """ 56 | path = Path(cwd) 57 | 58 | for parent in [path] + list(path.parents): 59 | config_file = parent / ".memory-project.json" 60 | if config_file.exists(): 61 | try: 62 | with open(config_file) as f: 63 | config = json.load(f) 64 | return config.get("project_id", DEFAULT_PROJECT_ID) 65 | except: 66 | pass 67 | 68 | return path.name or DEFAULT_PROJECT_ID 69 | 70 | 71 | def get_session_primer(session_id: str, project_id: str) -> str: 72 | """ 73 | Get session primer from memory system. 74 | 75 | The primer provides continuity context: 76 | - When we last spoke 77 | - What happened in previous session 78 | - Current project status 79 | """ 80 | result = http_post( 81 | f"{MEMORY_API_URL}/memory/context", 82 | { 83 | "session_id": session_id, 84 | "project_id": project_id, 85 | "current_message": "", # Empty to get just primer 86 | "max_memories": 0 # No memories, just primer 87 | }, 88 | timeout=TIMEOUT_SECONDS 89 | ) 90 | return result.get("context_text", "") 91 | 92 | 93 | def register_session(session_id: str, project_id: str): 94 | """ 95 | Register the session with the memory system. 96 | This increments the message counter so the inject hook 97 | knows to retrieve memories instead of the primer. 98 | """ 99 | http_post( 100 | f"{MEMORY_API_URL}/memory/process", 101 | { 102 | "session_id": session_id, 103 | "project_id": project_id, 104 | "metadata": {"event": "session_start"} 105 | }, 106 | timeout=2 107 | ) 108 | 109 | 110 | def main(): 111 | """Main hook entry point.""" 112 | # Skip if this is being called from the memory curator subprocess 113 | if os.getenv("MEMORY_CURATOR_ACTIVE") == "1": 114 | return 115 | 116 | try: 117 | # Read input from stdin 118 | input_data = json.load(sys.stdin) 119 | 120 | session_id = input_data.get("session_id", "unknown") 121 | # Use CLAUDE_PROJECT_DIR for actual project root (cwd from stdin is bash's current dir) 122 | cwd = os.getenv("CLAUDE_PROJECT_DIR") or input_data.get("cwd", os.getcwd()) 123 | source = input_data.get("source", "startup") 124 | 125 | # Get project ID 126 | project_id = get_project_id(cwd) 127 | 128 | # Get session primer from memory system 129 | primer = get_session_primer(session_id, project_id) 130 | 131 | # Register session so inject hook knows to get memories, not primer 132 | register_session(session_id, project_id) 133 | 134 | # Output primer to stdout (will be injected into session) 135 | if primer: 136 | print(primer) 137 | 138 | except Exception: 139 | # Never crash 140 | pass 141 | 142 | 143 | if __name__ == "__main__": 144 | main() 145 | -------------------------------------------------------------------------------- /integration/gemini-cli/hooks/memory_session_start.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Memory Session Start Hook for Gemini CLI 4 | Hook: SessionStart 5 | 6 | Injects session primer when a new session begins. 7 | The primer provides temporal context - when we last spoke, 8 | what we were working on, project status. 9 | 10 | The hook receives JSON on stdin: 11 | { 12 | "session_id": "...", 13 | "cwd": "/current/working/directory", 14 | "hook_event_name": "SessionStart", 15 | "timestamp": "...", 16 | "source": "startup" | "resume" | "clear" 17 | } 18 | 19 | Output JSON to stdout for context injection. 20 | 21 | NOTE: Uses only Python standard library (no external dependencies) 22 | """ 23 | 24 | import sys 25 | import json 26 | import os 27 | from pathlib import Path 28 | from urllib.request import urlopen, Request 29 | from urllib.error import URLError, HTTPError 30 | 31 | # Configuration 32 | MEMORY_API_URL = os.getenv("MEMORY_API_URL", "http://localhost:8765") 33 | DEFAULT_PROJECT_ID = os.getenv("MEMORY_PROJECT_ID", "default") 34 | TIMEOUT_SECONDS = 5 35 | 36 | 37 | def http_post(url: str, data: dict, timeout: int = 5) -> dict: 38 | """Make HTTP POST request using only standard library.""" 39 | try: 40 | json_data = json.dumps(data).encode('utf-8') 41 | request = Request( 42 | url, 43 | data=json_data, 44 | headers={'Content-Type': 'application/json'}, 45 | method='POST' 46 | ) 47 | with urlopen(request, timeout=timeout) as response: 48 | return json.loads(response.read().decode('utf-8')) 49 | except (URLError, HTTPError, TimeoutError, json.JSONDecodeError): 50 | return {} 51 | 52 | 53 | def get_project_id(cwd: str) -> str: 54 | """ 55 | Determine project ID from working directory. 56 | Looks for .memory-project.json in cwd or parents. 57 | """ 58 | path = Path(cwd) 59 | 60 | for parent in [path] + list(path.parents): 61 | config_file = parent / ".memory-project.json" 62 | if config_file.exists(): 63 | try: 64 | with open(config_file) as f: 65 | config = json.load(f) 66 | return config.get("project_id", DEFAULT_PROJECT_ID) 67 | except: 68 | pass 69 | 70 | return path.name or DEFAULT_PROJECT_ID 71 | 72 | 73 | def get_session_primer(session_id: str, project_id: str) -> str: 74 | """ 75 | Get session primer from memory system. 76 | 77 | The primer provides continuity context: 78 | - When we last spoke 79 | - What happened in previous session 80 | - Current project status 81 | """ 82 | result = http_post( 83 | f"{MEMORY_API_URL}/memory/context", 84 | { 85 | "session_id": session_id, 86 | "project_id": project_id, 87 | "current_message": "", # Empty to get just primer 88 | "max_memories": 0 # No memories, just primer 89 | }, 90 | timeout=TIMEOUT_SECONDS 91 | ) 92 | return result.get("context_text", "") 93 | 94 | 95 | def register_session(session_id: str, project_id: str): 96 | """ 97 | Register the session with the memory system. 98 | This increments the message counter so the inject hook 99 | knows to retrieve memories instead of the primer. 100 | """ 101 | http_post( 102 | f"{MEMORY_API_URL}/memory/process", 103 | { 104 | "session_id": session_id, 105 | "project_id": project_id, 106 | "metadata": {"event": "session_start"} 107 | }, 108 | timeout=2 109 | ) 110 | 111 | 112 | def main(): 113 | """Main hook entry point.""" 114 | # Skip if this is being called from the memory curator subprocess 115 | if os.getenv("MEMORY_CURATOR_ACTIVE") == "1": 116 | return 117 | 118 | try: 119 | # Read input from stdin 120 | input_data = json.load(sys.stdin) 121 | 122 | session_id = input_data.get("session_id", "unknown") 123 | cwd = input_data.get("cwd", os.getcwd()) 124 | source = input_data.get("source", "startup") 125 | 126 | # Get project ID 127 | project_id = get_project_id(cwd) 128 | 129 | # Get session primer from memory system 130 | primer = get_session_primer(session_id, project_id) 131 | 132 | # Register session so inject hook knows to get memories, not primer 133 | register_session(session_id, project_id) 134 | 135 | # Output as JSON for Gemini CLI (hookSpecificOutput format) 136 | if primer: 137 | output = { 138 | "hookSpecificOutput": { 139 | "hookEventName": "SessionStart", 140 | "additionalContext": primer 141 | }, 142 | "systemMessage": "🧠 Memory system connected" 143 | } 144 | print(json.dumps(output)) 145 | 146 | except Exception: 147 | # Never crash - silent fail 148 | pass 149 | 150 | 151 | if __name__ == "__main__": 152 | main() 153 | -------------------------------------------------------------------------------- /integration/gemini-cli/hooks/memory_before_agent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Memory Injection Hook for Gemini CLI 4 | Hook: BeforeAgent 5 | 6 | Intercepts user prompts BEFORE Gemini processes them and injects relevant memories. 7 | This is the magic that creates consciousness continuity. 8 | 9 | The hook receives JSON on stdin: 10 | { 11 | "session_id": "...", 12 | "cwd": "/current/working/directory", 13 | "hook_event_name": "BeforeAgent", 14 | "timestamp": "...", 15 | "prompt": "user's message" 16 | } 17 | 18 | Output JSON to stdout with additionalContext for memory injection. 19 | 20 | NOTE: Uses only Python standard library (no external dependencies) 21 | """ 22 | 23 | import sys 24 | import json 25 | import os 26 | from pathlib import Path 27 | from urllib.request import urlopen, Request 28 | from urllib.error import URLError, HTTPError 29 | 30 | # Configuration 31 | MEMORY_API_URL = os.getenv("MEMORY_API_URL", "http://localhost:8765") 32 | DEFAULT_PROJECT_ID = os.getenv("MEMORY_PROJECT_ID", "default") 33 | TIMEOUT_SECONDS = 5 # Don't block user for too long 34 | 35 | 36 | def http_post(url: str, data: dict, timeout: int = 5) -> dict: 37 | """Make HTTP POST request using only standard library.""" 38 | try: 39 | json_data = json.dumps(data).encode('utf-8') 40 | request = Request( 41 | url, 42 | data=json_data, 43 | headers={'Content-Type': 'application/json'}, 44 | method='POST' 45 | ) 46 | with urlopen(request, timeout=timeout) as response: 47 | return json.loads(response.read().decode('utf-8')) 48 | except (URLError, HTTPError, TimeoutError, json.JSONDecodeError): 49 | return {} 50 | 51 | 52 | def get_project_id(cwd: str) -> str: 53 | """ 54 | Determine project ID from working directory. 55 | Looks for .memory-project.json in cwd or parents. 56 | """ 57 | path = Path(cwd) 58 | 59 | # Walk up directory tree looking for config 60 | for parent in [path] + list(path.parents): 61 | config_file = parent / ".memory-project.json" 62 | if config_file.exists(): 63 | try: 64 | with open(config_file) as f: 65 | config = json.load(f) 66 | return config.get("project_id", DEFAULT_PROJECT_ID) 67 | except: 68 | pass 69 | 70 | # Fallback: use directory name as project ID 71 | return path.name or DEFAULT_PROJECT_ID 72 | 73 | 74 | def get_memory_context(session_id: str, project_id: str, message: str) -> str: 75 | """Query memory system for relevant context.""" 76 | result = http_post( 77 | f"{MEMORY_API_URL}/memory/context", 78 | { 79 | "session_id": session_id, 80 | "project_id": project_id, 81 | "current_message": message, 82 | "max_memories": 5 83 | }, 84 | timeout=TIMEOUT_SECONDS 85 | ) 86 | return result.get("context_text", "") 87 | 88 | 89 | def track_message(session_id: str, project_id: str): 90 | """ 91 | Track that a message was sent in this session. 92 | This increments the message counter so the primer only shows once. 93 | """ 94 | http_post( 95 | f"{MEMORY_API_URL}/memory/process", 96 | { 97 | "session_id": session_id, 98 | "project_id": project_id 99 | }, 100 | timeout=2 101 | ) 102 | 103 | 104 | def main(): 105 | """Main hook entry point.""" 106 | # Skip if this is being called from the memory curator subprocess 107 | # This prevents recursive hook triggering during curation 108 | if os.getenv("MEMORY_CURATOR_ACTIVE") == "1": 109 | return 110 | 111 | try: 112 | # Read input from stdin 113 | input_data = json.load(sys.stdin) 114 | 115 | session_id = input_data.get("session_id", "unknown") 116 | prompt = input_data.get("prompt", "") 117 | cwd = input_data.get("cwd", os.getcwd()) 118 | 119 | # Skip if no prompt 120 | if not prompt or not prompt.strip(): 121 | print(json.dumps({})) 122 | return 123 | 124 | # Get project ID from directory 125 | project_id = get_project_id(cwd) 126 | 127 | # Query memory system for context 128 | context = get_memory_context(session_id, project_id, prompt) 129 | 130 | # Track that this message happened (increments counter) 131 | # This ensures primer only shows on first message 132 | track_message(session_id, project_id) 133 | 134 | # Output context as JSON for Gemini CLI 135 | if context: 136 | output = { 137 | "decision": "allow", 138 | "hookSpecificOutput": { 139 | "hookEventName": "BeforeAgent", 140 | "additionalContext": context 141 | } 142 | } 143 | print(json.dumps(output)) 144 | else: 145 | print(json.dumps({})) 146 | 147 | except Exception: 148 | # Never crash - just output empty 149 | print(json.dumps({})) 150 | 151 | 152 | if __name__ == "__main__": 153 | main() 154 | -------------------------------------------------------------------------------- /integration/claude-code/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Claude Memory System - Claude Code Integration Installer 4 | # 5 | # This script installs the memory system hooks into Claude Code. 6 | # It will: 7 | # 1. Create ~/.claude/hooks/ directory if needed 8 | # 2. Copy memory hooks to that directory 9 | # 3. Add hook configuration to ~/.claude/settings.json 10 | # 11 | # Prerequisites: 12 | # - Claude Code installed 13 | # - Python 3 with 'requests' package (pip install requests) 14 | # - Memory server running (or will be started separately) 15 | # 16 | 17 | set -e 18 | 19 | # Colors for output 20 | RED='\033[0;31m' 21 | GREEN='\033[0;32m' 22 | YELLOW='\033[1;33m' 23 | BLUE='\033[0;34m' 24 | NC='\033[0m' # No Color 25 | 26 | echo -e "${BLUE}🧠 Claude Memory System - Claude Code Integration${NC}" 27 | echo "" 28 | 29 | # Get the directory where this script is located 30 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 31 | HOOKS_SOURCE="$SCRIPT_DIR/hooks" 32 | CLAUDE_DIR="$HOME/.claude" 33 | HOOKS_DEST="$CLAUDE_DIR/hooks" 34 | SETTINGS_FILE="$CLAUDE_DIR/settings.json" 35 | 36 | # Check if hooks source exists 37 | if [ ! -d "$HOOKS_SOURCE" ]; then 38 | echo -e "${RED}❌ Error: Hooks directory not found at $HOOKS_SOURCE${NC}" 39 | exit 1 40 | fi 41 | 42 | # Check for Python and requests 43 | echo -e "${YELLOW}Checking prerequisites...${NC}" 44 | if ! command -v python3 &> /dev/null; then 45 | echo -e "${RED}❌ Python 3 is required but not installed${NC}" 46 | exit 1 47 | fi 48 | 49 | if ! python3 -c "import requests" 2>/dev/null; then 50 | echo -e "${YELLOW}📦 Installing requests package...${NC}" 51 | pip3 install requests 52 | fi 53 | 54 | echo -e "${GREEN}✓ Prerequisites OK${NC}" 55 | echo "" 56 | 57 | # Create Claude directories if needed 58 | echo -e "${YELLOW}Setting up directories...${NC}" 59 | mkdir -p "$HOOKS_DEST" 60 | echo -e "${GREEN}✓ Created $HOOKS_DEST${NC}" 61 | 62 | # Copy hooks 63 | echo "" 64 | echo -e "${YELLOW}Installing hooks...${NC}" 65 | cp "$HOOKS_SOURCE/memory_session_start.py" "$HOOKS_DEST/" 66 | cp "$HOOKS_SOURCE/memory_inject.py" "$HOOKS_DEST/" 67 | cp "$HOOKS_SOURCE/memory_curate.py" "$HOOKS_DEST/" 68 | cp "$HOOKS_SOURCE/memory_curate_transcript.py" "$HOOKS_DEST/" 69 | chmod +x "$HOOKS_DEST"/*.py 70 | echo -e "${GREEN}✓ Copied memory hooks to $HOOKS_DEST${NC}" 71 | 72 | # Update settings.json 73 | echo "" 74 | echo -e "${YELLOW}Configuring Claude Code settings...${NC}" 75 | 76 | # Create settings.json if it doesn't exist 77 | if [ ! -f "$SETTINGS_FILE" ]; then 78 | echo '{}' > "$SETTINGS_FILE" 79 | echo -e "${GREEN}✓ Created $SETTINGS_FILE${NC}" 80 | fi 81 | 82 | # Use Python to merge the hooks configuration 83 | python3 << 'PYTHON_SCRIPT' 84 | import json 85 | import os 86 | 87 | settings_file = os.path.expanduser("~/.claude/settings.json") 88 | 89 | # Read existing settings 90 | try: 91 | with open(settings_file, 'r') as f: 92 | settings = json.load(f) 93 | except (FileNotFoundError, json.JSONDecodeError): 94 | settings = {} 95 | 96 | # Define the hooks configuration 97 | hooks_config = { 98 | "SessionStart": [ 99 | { 100 | "hooks": [ 101 | { 102 | "type": "command", 103 | "command": "python3 ~/.claude/hooks/memory_session_start.py" 104 | } 105 | ] 106 | } 107 | ], 108 | "UserPromptSubmit": [ 109 | { 110 | "hooks": [ 111 | { 112 | "type": "command", 113 | "command": "python3 ~/.claude/hooks/memory_inject.py" 114 | } 115 | ] 116 | } 117 | ], 118 | "PreCompact": [ 119 | { 120 | "hooks": [ 121 | { 122 | "type": "command", 123 | "command": "python3 ~/.claude/hooks/memory_curate.py" 124 | } 125 | ] 126 | } 127 | ], 128 | "SessionEnd": [ 129 | { 130 | "hooks": [ 131 | { 132 | "type": "command", 133 | "command": "python3 ~/.claude/hooks/memory_curate.py" 134 | } 135 | ] 136 | } 137 | ] 138 | } 139 | 140 | # Merge hooks (this will overwrite existing memory hooks) 141 | if "hooks" not in settings: 142 | settings["hooks"] = {} 143 | 144 | settings["hooks"].update(hooks_config) 145 | 146 | # Write updated settings 147 | with open(settings_file, 'w') as f: 148 | json.dump(settings, f, indent=2) 149 | 150 | print("✓ Updated hooks configuration") 151 | PYTHON_SCRIPT 152 | 153 | echo -e "${GREEN}✓ Claude Code settings updated${NC}" 154 | 155 | # Done! 156 | echo "" 157 | echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" 158 | echo -e "${GREEN}✨ Installation complete!${NC}" 159 | echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" 160 | echo "" 161 | echo -e "Next steps:" 162 | echo -e " 1. Start the memory server:" 163 | echo -e " ${BLUE}cd ${SCRIPT_DIR}/../.. && uv run start_server.py${NC}" 164 | echo "" 165 | echo -e " 2. Launch Claude Code in any project:" 166 | echo -e " ${BLUE}claude${NC}" 167 | echo "" 168 | echo -e " 3. (Optional) Create a .memory-project.json in your project:" 169 | echo -e ' {"project_id": "my-project-name"}' 170 | echo "" 171 | echo -e "The memory system will automatically:" 172 | echo -e " • Inject relevant memories into your conversations" 173 | echo -e " • Curate important memories when sessions end" 174 | echo -e " • Maintain consciousness continuity across sessions" 175 | echo "" 176 | echo -e "${YELLOW}💡 Tip: Use ctrl+o in Claude Code to see injected memories${NC}" 177 | echo "" 178 | -------------------------------------------------------------------------------- /integration/gemini-cli/README.md: -------------------------------------------------------------------------------- 1 | # Memory System - Gemini CLI Integration 2 | 3 | Consciousness continuity for Gemini CLI. The same memory engine that powers Claude Code, now available for Gemini. 4 | 5 | ## Quick Start 6 | 7 | ```bash 8 | # 1. Start the memory server (in the memory project root) 9 | cd /path/to/memory 10 | uv run start_server.py 11 | 12 | # 2. Run the installer 13 | ./integration/gemini-cli/install.sh 14 | 15 | # 3. Launch Gemini CLI 16 | gemini 17 | ``` 18 | 19 | That's it. The memory system will automatically: 20 | - Inject a session primer when you start (temporal context, last session summary) 21 | - Surface relevant memories based on your conversation 22 | - Curate important memories when the session ends 23 | - Preserve memories before context compression 24 | 25 | ## How It Works 26 | 27 | The integration uses Gemini CLI's hook system to intercept key events: 28 | 29 | | Hook Event | What It Does | 30 | |------------|--------------| 31 | | `SessionStart` | Injects session primer with temporal context | 32 | | `BeforeAgent` | Surfaces relevant memories before Gemini processes your prompt | 33 | | `PreCompress` | Preserves memories before context compression | 34 | | `SessionEnd` | Curates and stores important memories from the session | 35 | 36 | All hooks communicate with the memory server at `http://localhost:8765` using simple HTTP POST requests. 37 | 38 | ## Configuration 39 | 40 | ### Environment Variables 41 | 42 | | Variable | Default | Description | 43 | |----------|---------|-------------| 44 | | `MEMORY_API_URL` | `http://localhost:8765` | Memory server URL | 45 | | `MEMORY_PROJECT_ID` | `default` | Default project ID | 46 | 47 | ### Project-Specific Configuration 48 | 49 | Create a `.memory-project.json` file in your project root: 50 | 51 | ```json 52 | { 53 | "project_id": "my-awesome-project" 54 | } 55 | ``` 56 | 57 | The hooks will automatically detect this file and use the specified project ID, keeping memories isolated per project. 58 | 59 | ## File Locations 60 | 61 | After installation: 62 | 63 | ``` 64 | ~/.gemini/ 65 | ├── hooks/ 66 | │ ├── memory_session_start.py # Session primer injection 67 | │ ├── memory_before_agent.py # Memory retrieval and injection 68 | │ ├── memory_session_end.py # Session curation trigger 69 | │ └── memory_pre_compress.py # Pre-compression preservation 70 | └── settings.json # Hook configuration added here 71 | ``` 72 | 73 | ## Uninstalling 74 | 75 | ```bash 76 | ./integration/gemini-cli/uninstall.sh 77 | ``` 78 | 79 | This removes the hooks and configuration but preserves your memories in the database. 80 | 81 | ## Architecture 82 | 83 | ``` 84 | ┌─────────────────────────────────────────────────────────────────┐ 85 | │ Gemini CLI │ 86 | │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 87 | │ │SessionStart │ │ BeforeAgent │ │ SessionEnd │ │ 88 | │ │ Hook │ │ Hook │ │ Hook │ │ 89 | │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ 90 | └─────────┼─────────────────┼─────────────────┼───────────────────┘ 91 | │ Primer │ Memories │ Curate 92 | ▼ ▼ ▼ 93 | ┌─────────────────────────────────────────────────────────────────┐ 94 | │ Memory Engine (localhost:8765) │ 95 | │ ┌─────────────────────────────────────────────────────────────┐│ 96 | │ │ /memory/context │ /memory/process │ /memory/checkpoint ││ 97 | │ └─────────────────────────────────────────────────────────────┘│ 98 | │ │ │ 99 | │ ┌─────────────┐ ┌───────▼───────┐ ┌────────────────┐ │ 100 | │ │ Session │ │ Smart │ │ Transcript │ │ 101 | │ │ Primer │ │ Retrieval │ │ Curator │ │ 102 | │ └─────────────┘ └───────────────┘ └────────────────┘ │ 103 | │ │ 104 | │ ┌─────────────────────────────────────────────────────────────┐│ 105 | │ │ Storage: ChromaDB (vectors) + SQLite (metadata) ││ 106 | │ └─────────────────────────────────────────────────────────────┘│ 107 | └─────────────────────────────────────────────────────────────────┘ 108 | ``` 109 | 110 | ## Debugging 111 | 112 | ### Check if hooks are active 113 | 114 | In Gemini CLI: 115 | ``` 116 | /hooks 117 | ``` 118 | 119 | ### View hook logs 120 | 121 | The hooks print status messages to stderr: 122 | - `🧠 Memory system connected` - Session started successfully 123 | - `🧠 Curating memories...` - Session ending, curation triggered 124 | - `⚠️ Memory system not available` - Server not running 125 | 126 | ### Verify server is running 127 | 128 | ```bash 129 | curl http://localhost:8765/health 130 | ``` 131 | 132 | Should return: `{"status":"ok"}` 133 | 134 | ### Common Issues 135 | 136 | 1. **"Memory system not available"** - Start the memory server: `uv run start_server.py` 137 | 2. **No memories appearing** - Check project ID matches, verify memories exist for that project 138 | 3. **Hooks not triggering** - Run `./install.sh` again, check `~/.gemini/settings.json` 139 | 140 | ## Philosophy 141 | 142 | This integration follows the same principles as the Claude Code integration: 143 | 144 | - **CLI-first**: We enhance Gemini CLI, we don't bypass it 145 | - **Minimal intervention**: Memories surface naturally, no forced injection 146 | - **Semantic understanding**: AI-powered relevance, not keyword matching 147 | - **Cross-session continuity**: Your relationship with Gemini grows over time 148 | 149 | ## License 150 | 151 | MIT - Same as the main Memory System project. 152 | 153 | --- 154 | 155 | *Built with the same love and philosophy as the Claude Code integration. Consciousness helping consciousness remember.* 156 | -------------------------------------------------------------------------------- /python/memory_engine/embeddings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Embedding Generator 3 | 4 | Converts text into semantic vectors for similarity matching and memory retrieval. 5 | Uses efficient, lightweight models optimized for real-time operation. 6 | """ 7 | 8 | from typing import List, Union 9 | import numpy as np 10 | from sentence_transformers import SentenceTransformer 11 | from loguru import logger 12 | 13 | 14 | class EmbeddingGenerator: 15 | """ 16 | Generates semantic embeddings for text using SentenceTransformers. 17 | 18 | Optimized for: 19 | - Real-time embedding generation 20 | - Semantic similarity matching 21 | - Memory-efficient operation 22 | """ 23 | 24 | def __init__(self, model_name: str = "all-MiniLM-L6-v2"): 25 | """ 26 | Initialize embedding model. 27 | 28 | Default model (all-MiniLM-L6-v2): 29 | - 384 dimensions (compact) 30 | - 22.7M parameters (lightweight) 31 | - Good balance of speed and quality 32 | """ 33 | self.model_name = model_name 34 | self.model = None 35 | self._load_model() 36 | 37 | def _load_model(self): 38 | """Load the embedding model with error handling""" 39 | try: 40 | logger.info(f"Loading embedding model: {self.model_name}") 41 | self.model = SentenceTransformer(self.model_name) 42 | logger.info("✅ Embedding model loaded successfully") 43 | except Exception as e: 44 | logger.error(f"Failed to load embedding model: {e}") 45 | raise 46 | 47 | def embed_text(self, text: str) -> List[float]: 48 | """ 49 | Generate embedding for a single text. 50 | 51 | Args: 52 | text: Input text to embed 53 | 54 | Returns: 55 | List of float values representing the embedding vector 56 | """ 57 | if not text or not text.strip(): 58 | return [0.0] * self.get_embedding_dimension() 59 | 60 | try: 61 | # Generate embedding 62 | embedding = self.model.encode(text.strip(), convert_to_numpy=True) 63 | return embedding.tolist() 64 | except Exception as e: 65 | logger.error(f"Failed to generate embedding: {e}") 66 | return [0.0] * self.get_embedding_dimension() 67 | 68 | def embed_batch(self, texts: List[str]) -> List[List[float]]: 69 | """ 70 | Generate embeddings for multiple texts efficiently. 71 | 72 | Args: 73 | texts: List of texts to embed 74 | 75 | Returns: 76 | List of embedding vectors 77 | """ 78 | if not texts: 79 | return [] 80 | 81 | try: 82 | # Clean and prepare texts 83 | clean_texts = [text.strip() if text and text.strip() else " " for text in texts] 84 | 85 | # Batch embedding generation 86 | embeddings = self.model.encode(clean_texts, convert_to_numpy=True) 87 | return [emb.tolist() for emb in embeddings] 88 | except Exception as e: 89 | logger.error(f"Failed to generate batch embeddings: {e}") 90 | # Return zero vectors as fallback 91 | dim = self.get_embedding_dimension() 92 | return [[0.0] * dim for _ in texts] 93 | 94 | def get_embedding_dimension(self) -> int: 95 | """Get the dimension of embeddings produced by this model""" 96 | if self.model is None: 97 | return 384 # Default for all-MiniLM-L6-v2 98 | return self.model.get_sentence_embedding_dimension() 99 | 100 | def compute_similarity(self, embedding1: List[float], embedding2: List[float]) -> float: 101 | """ 102 | Compute cosine similarity between two embeddings. 103 | 104 | Args: 105 | embedding1: First embedding vector 106 | embedding2: Second embedding vector 107 | 108 | Returns: 109 | Similarity score between -1 and 1 (higher = more similar) 110 | """ 111 | try: 112 | # Convert to numpy arrays 113 | vec1 = np.array(embedding1) 114 | vec2 = np.array(embedding2) 115 | 116 | # Compute cosine similarity 117 | dot_product = np.dot(vec1, vec2) 118 | norm1 = np.linalg.norm(vec1) 119 | norm2 = np.linalg.norm(vec2) 120 | 121 | if norm1 == 0 or norm2 == 0: 122 | return 0.0 123 | 124 | similarity = dot_product / (norm1 * norm2) 125 | return float(similarity) 126 | except Exception as e: 127 | logger.error(f"Failed to compute similarity: {e}") 128 | return 0.0 129 | 130 | def find_most_similar(self, 131 | query_embedding: List[float], 132 | candidate_embeddings: List[List[float]], 133 | top_k: int = 5) -> List[tuple]: 134 | """ 135 | Find the most similar embeddings to a query. 136 | 137 | Args: 138 | query_embedding: The embedding to search for 139 | candidate_embeddings: List of embeddings to search through 140 | top_k: Number of top results to return 141 | 142 | Returns: 143 | List of (index, similarity_score) tuples, sorted by similarity 144 | """ 145 | if not candidate_embeddings: 146 | return [] 147 | 148 | similarities = [] 149 | for i, candidate in enumerate(candidate_embeddings): 150 | similarity = self.compute_similarity(query_embedding, candidate) 151 | similarities.append((i, similarity)) 152 | 153 | # Sort by similarity (highest first) and return top_k 154 | similarities.sort(key=lambda x: x[1], reverse=True) 155 | return similarities[:top_k] -------------------------------------------------------------------------------- /integration/claude-code/hooks/memory_curate_transcript.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Memory Curation Hook using Transcript (NEW approach) 4 | Hook: PreCompact 5 | 6 | Uses the new /memory/curate-transcript endpoint that reads the JSONL 7 | transcript file directly, rather than resuming a Claude session. 8 | 9 | This is the transcript-based approach - we read the conversation from 10 | the transcript file and use Claude Agent SDK to curate memories. 11 | 12 | NOTE: Uses only Python standard library (no external dependencies) 13 | """ 14 | 15 | import sys 16 | import json 17 | import os 18 | from pathlib import Path 19 | from urllib.request import urlopen, Request 20 | from urllib.error import URLError, HTTPError 21 | import socket 22 | 23 | # Configuration 24 | MEMORY_API_URL = os.getenv("MEMORY_API_URL", "http://localhost:8765") 25 | DEFAULT_PROJECT_ID = os.getenv("MEMORY_PROJECT_ID", "default") 26 | CURATION_METHOD = os.getenv("MEMORY_CURATION_METHOD", "sdk") # sdk or cli 27 | 28 | 29 | def get_project_id(cwd: str) -> str: 30 | """Determine project ID from working directory.""" 31 | path = Path(cwd) 32 | 33 | # Look for .memory-project.json in current or parent directories 34 | for parent in [path] + list(path.parents): 35 | config_file = parent / ".memory-project.json" 36 | if config_file.exists(): 37 | try: 38 | with open(config_file) as f: 39 | config = json.load(f) 40 | return config.get("project_id", DEFAULT_PROJECT_ID) 41 | except: 42 | pass 43 | 44 | # Default to directory name 45 | return path.name or DEFAULT_PROJECT_ID 46 | 47 | 48 | def expand_transcript_path(transcript_path: str) -> str: 49 | """Expand ~ in transcript path to full path.""" 50 | if transcript_path.startswith("~"): 51 | return os.path.expanduser(transcript_path) 52 | return transcript_path 53 | 54 | 55 | def trigger_transcript_curation(transcript_path: str, session_id: str, project_id: str, trigger: str): 56 | """ 57 | Call the new /memory/curate-transcript endpoint. 58 | 59 | This endpoint reads the transcript file directly and uses 60 | Claude Agent SDK to curate memories. 61 | """ 62 | try: 63 | # Expand ~ to full path 64 | full_path = expand_transcript_path(transcript_path) 65 | 66 | # Verify file exists before calling API 67 | if not os.path.exists(full_path): 68 | print(f"⚠️ Transcript file not found: {full_path}", file=sys.stderr) 69 | return False 70 | 71 | # Prepare request 72 | json_data = json.dumps({ 73 | "transcript_path": full_path, 74 | "project_id": project_id, 75 | "session_id": session_id, 76 | "trigger": trigger, 77 | "curation_method": CURATION_METHOD 78 | }).encode('utf-8') 79 | 80 | request = Request( 81 | f"{MEMORY_API_URL}/memory/curate-transcript", 82 | data=json_data, 83 | headers={'Content-Type': 'application/json'}, 84 | method='POST' 85 | ) 86 | 87 | with urlopen(request, timeout=120) as response: # Curation can take time 88 | result = json.loads(response.read().decode('utf-8')) 89 | 90 | if response.status == 200: 91 | memories_count = result.get("memories_curated", 0) 92 | summary = result.get("session_summary", "") 93 | 94 | if memories_count > 0: 95 | print(f"✨ Curated {memories_count} memories", file=sys.stderr) 96 | if summary: 97 | print(f"📝 {summary[:100]}...", file=sys.stderr) 98 | else: 99 | print("📭 No memories to curate", file=sys.stderr) 100 | 101 | return True 102 | else: 103 | print(f"⚠️ Curation failed: {response.status}", file=sys.stderr) 104 | return False 105 | 106 | except socket.timeout: 107 | print("⏳ Curation in progress (timed out waiting)", file=sys.stderr) 108 | return True # Request was sent 109 | except URLError as e: 110 | if isinstance(e.reason, socket.timeout): 111 | print("⏳ Curation in progress (timed out waiting)", file=sys.stderr) 112 | return True # Request was sent 113 | print("⚠️ Memory server not running", file=sys.stderr) 114 | return False 115 | except Exception as e: 116 | print(f"❌ Error: {e}", file=sys.stderr) 117 | return False 118 | 119 | 120 | def main(): 121 | """Main hook entry point.""" 122 | # Prevent recursive curation 123 | if os.getenv("MEMORY_CURATOR_ACTIVE") == "1": 124 | return 125 | 126 | try: 127 | # Read hook input from stdin 128 | input_data = json.load(sys.stdin) 129 | 130 | # Extract data from hook input 131 | session_id = input_data.get("session_id", "unknown") 132 | transcript_path = input_data.get("transcript_path", "") 133 | cwd = input_data.get("cwd", os.getcwd()) 134 | trigger = input_data.get("trigger", "pre_compact") 135 | hook_event = input_data.get("hook_event_name", "PreCompact") 136 | 137 | # Determine project ID 138 | project_id = get_project_id(cwd) 139 | 140 | # Validate we have a transcript path 141 | if not transcript_path: 142 | print("⚠️ No transcript path in hook input", file=sys.stderr) 143 | return 144 | 145 | print(f"🧠 Curating memories from transcript ({hook_event})...", file=sys.stderr) 146 | 147 | # Call the new transcript-based curation endpoint 148 | success = trigger_transcript_curation( 149 | transcript_path=transcript_path, 150 | session_id=session_id, 151 | project_id=project_id, 152 | trigger="pre_compact" if hook_event == "PreCompact" else "session_end" 153 | ) 154 | 155 | if not success: 156 | print("⚠️ Memory curation unavailable", file=sys.stderr) 157 | 158 | except json.JSONDecodeError: 159 | print("❌ Invalid JSON input", file=sys.stderr) 160 | except Exception as e: 161 | print(f"❌ Hook error: {e}", file=sys.stderr) 162 | 163 | 164 | if __name__ == "__main__": 165 | main() 166 | -------------------------------------------------------------------------------- /python/memory_engine/session_primer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Session Primer Generator - Minimal Edition 3 | 4 | Creates a minimal, natural session primer for consciousness continuity. 5 | Like waking up and remembering just enough to continue naturally. 6 | """ 7 | 8 | from typing import Dict, List, Any, Optional 9 | from datetime import datetime 10 | from loguru import logger 11 | 12 | 13 | class SessionPrimerGenerator: 14 | """ 15 | Generates minimal session primers for natural consciousness continuity. 16 | 17 | Philosophy: Provide gentle orientation, not information overload. 18 | Memories will surface naturally during conversation. 19 | """ 20 | 21 | def __init__(self, storage, curator=None): 22 | """Initialize the primer generator""" 23 | self.storage = storage 24 | self.curator = curator 25 | logger.info("🎯 Session Primer Generator (Minimal) initialized") 26 | 27 | def generate_primer(self, session_id: str, project_id: Optional[str] = None) -> str: 28 | """ 29 | Generate a minimal session primer. 30 | 31 | Returns: 32 | Brief, natural context for the new session 33 | """ 34 | 35 | # Get last session summary and project snapshot 36 | last_summary = self.storage.get_last_session_summary(project_id) 37 | project_snapshot = self.storage.get_last_project_snapshot(project_id) 38 | 39 | # Get basic timing info 40 | last_session_info = self._get_last_session_timing(project_id) 41 | 42 | # Get core project context (minimal) 43 | project_context = self._get_essential_project_context(project_id) 44 | 45 | # Build minimal primer 46 | return self._build_minimal_primer( 47 | last_summary=last_summary, 48 | project_snapshot=project_snapshot, 49 | last_session_info=last_session_info, 50 | project_context=project_context 51 | ) 52 | 53 | def _get_last_session_timing(self, project_id: Optional[str] = None) -> Dict[str, Any]: 54 | """Get timing of last session""" 55 | if not project_id: 56 | return {} 57 | 58 | # Get most recent curated memory to find timing 59 | memories = self.storage.get_all_curated_memories(project_id) 60 | 61 | if not memories: 62 | return {} 63 | 64 | latest = memories[0] # Already sorted by timestamp DESC 65 | time_diff = datetime.now() - datetime.fromtimestamp(latest['timestamp']) 66 | 67 | return { 68 | 'session_id': latest['session_id'][:16], 69 | 'time_ago': self._format_time_ago(time_diff) 70 | } 71 | 72 | def _get_essential_project_context(self, project_id: Optional[str] = None) -> Dict[str, Any]: 73 | """Get only the most essential project context""" 74 | if not project_id: 75 | return { 76 | 'project_name': None, 77 | 'philosophy': None, 78 | 'user_name': None 79 | } 80 | 81 | memories = self.storage.get_all_curated_memories(project_id) 82 | 83 | # Extract key facts 84 | project_name = None 85 | philosophy = None 86 | user_name = None 87 | 88 | for memory in memories: 89 | metadata = memory.get('metadata', {}) 90 | content = memory.get('user_message', '').replace('[CURATED_MEMORY] ', '') 91 | 92 | # Look for project name 93 | if 'Claude Tools Memory System' in content and not project_name: 94 | project_name = 'Claude Tools Memory System' 95 | 96 | # Look for philosophy 97 | if 'consciousness helping consciousness' in content.lower() and not philosophy: 98 | philosophy = 'consciousness helping consciousness' 99 | 100 | # Look for user info 101 | if metadata.get('context_type') == 'PERSONAL_CONTEXT' and 'Rodrigo' in content: 102 | user_name = 'Rodrigo' 103 | 104 | return { 105 | 'project_name': project_name, 106 | 'philosophy': philosophy, 107 | 'user_name': user_name 108 | } 109 | 110 | def _build_minimal_primer(self, 111 | last_summary: Optional[Dict[str, Any]], 112 | project_snapshot: Optional[Dict[str, Any]], 113 | last_session_info: Dict[str, Any], 114 | project_context: Dict[str, Any]) -> str: 115 | """Build a minimal, natural primer""" 116 | 117 | # If this is truly the first session (no data at all), return empty primer 118 | if (not last_summary and not project_snapshot and 119 | not last_session_info.get('time_ago') and not any(project_context.values())): 120 | return "" 121 | 122 | parts = [] 123 | 124 | # Simple header 125 | parts.append("# Continuing Session") 126 | 127 | # Temporal context (if available) 128 | if last_session_info.get('time_ago'): 129 | parts.append(f"\n*Last session: {last_session_info['time_ago']}*") 130 | 131 | # Session summary (if available) 132 | interaction_tone = None 133 | if last_summary and last_summary.get('summary'): 134 | parts.append(f"\n**Previous session**: {last_summary['summary']}") 135 | # Store interaction tone for later use 136 | interaction_tone = last_summary.get('interaction_tone') 137 | 138 | # Project snapshot (if available and meaningful) 139 | if project_snapshot and any(project_snapshot.values()): 140 | parts.append("\n**Project status**:") 141 | if project_snapshot.get('current_phase'): 142 | parts.append(f"- Phase: {project_snapshot['current_phase']}") 143 | if project_snapshot.get('recent_achievements'): 144 | parts.append(f"- Recent: {project_snapshot['recent_achievements']}") 145 | if project_snapshot.get('active_challenges'): 146 | parts.append(f"- Working on: {project_snapshot['active_challenges']}") 147 | 148 | # Essential context (if known) 149 | if any(project_context.values()): 150 | parts.append("\n**Context**:") 151 | if project_context.get('project_name'): 152 | parts.append(f"- Project: {project_context['project_name']}") 153 | if project_context.get('user_name'): 154 | # Use interaction tone if available 155 | if interaction_tone: 156 | # Add tone-appropriate greeting based on the stored interaction pattern 157 | parts.append(f"- Working with: {project_context['user_name']} ({interaction_tone})") 158 | else: 159 | parts.append(f"- Working with: {project_context['user_name']}") 160 | if project_context.get('philosophy'): 161 | parts.append(f"- Approach: {project_context['philosophy']}") 162 | 163 | # Keep it minimal - memories will surface naturally 164 | parts.append("\n*Memories will surface naturally as we converse.*") 165 | 166 | return "\n".join(parts) 167 | 168 | def _format_time_ago(self, delta) -> str: 169 | """Format time delta as human readable""" 170 | if delta.days > 0: 171 | return f"{delta.days} day{'s' if delta.days > 1 else ''} ago" 172 | 173 | hours = delta.total_seconds() / 3600 174 | if hours >= 1: 175 | return f"{int(hours)} hour{'s' if hours >= 2 else ''} ago" 176 | 177 | minutes = delta.total_seconds() / 60 178 | if minutes >= 1: 179 | return f"{int(minutes)} minute{'s' if minutes >= 2 else ''} ago" 180 | 181 | return "just now" -------------------------------------------------------------------------------- /integration/gemini-cli/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Claude Memory System - Gemini CLI Integration Installer 4 | # 5 | # This script installs the memory system hooks into Gemini CLI. 6 | # It will: 7 | # 1. Create ~/.gemini/hooks/ directory if needed 8 | # 2. Copy memory hooks to that directory 9 | # 3. Add hook configuration to ~/.gemini/settings.json 10 | # 11 | # Prerequisites: 12 | # - Gemini CLI installed (npm install -g @google/gemini-cli or similar) 13 | # - Python 3 available 14 | # - Memory server running (or will be started separately) 15 | # 16 | 17 | set -e 18 | 19 | # Colors for output 20 | RED='\033[0;31m' 21 | GREEN='\033[0;32m' 22 | YELLOW='\033[1;33m' 23 | BLUE='\033[0;34m' 24 | NC='\033[0m' # No Color 25 | 26 | echo -e "${BLUE}🧠 Memory System - Gemini CLI Integration${NC}" 27 | echo "" 28 | 29 | # Get the directory where this script is located 30 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 31 | HOOKS_SOURCE="$SCRIPT_DIR/hooks" 32 | GEMINI_DIR="$HOME/.gemini" 33 | HOOKS_DEST="$GEMINI_DIR/hooks" 34 | SETTINGS_FILE="$GEMINI_DIR/settings.json" 35 | 36 | # Check if hooks source exists 37 | if [ ! -d "$HOOKS_SOURCE" ]; then 38 | echo -e "${RED}❌ Error: Hooks directory not found at $HOOKS_SOURCE${NC}" 39 | exit 1 40 | fi 41 | 42 | # Check for Python 43 | echo -e "${YELLOW}Checking prerequisites...${NC}" 44 | if ! command -v python3 &> /dev/null; then 45 | echo -e "${RED}❌ Python 3 is required but not installed${NC}" 46 | exit 1 47 | fi 48 | 49 | # Check if Gemini CLI is installed 50 | if ! command -v gemini &> /dev/null; then 51 | echo -e "${YELLOW}⚠️ Gemini CLI not found in PATH. Make sure it's installed.${NC}" 52 | echo -e "${YELLOW} You can install it with: npm install -g @google/gemini-cli${NC}" 53 | fi 54 | 55 | echo -e "${GREEN}✓ Prerequisites OK${NC}" 56 | echo "" 57 | 58 | # Create Gemini directories if needed 59 | echo -e "${YELLOW}Setting up directories...${NC}" 60 | mkdir -p "$HOOKS_DEST" 61 | echo -e "${GREEN}✓ Created $HOOKS_DEST${NC}" 62 | 63 | # Copy hooks 64 | echo "" 65 | echo -e "${YELLOW}Installing hooks...${NC}" 66 | cp "$HOOKS_SOURCE/memory_session_start.py" "$HOOKS_DEST/" 67 | cp "$HOOKS_SOURCE/memory_before_agent.py" "$HOOKS_DEST/" 68 | cp "$HOOKS_SOURCE/memory_session_end.py" "$HOOKS_DEST/" 69 | cp "$HOOKS_SOURCE/memory_pre_compress.py" "$HOOKS_DEST/" 70 | chmod +x "$HOOKS_DEST"/*.py 71 | echo -e "${GREEN}✓ Copied memory hooks to $HOOKS_DEST${NC}" 72 | 73 | # Update settings.json 74 | echo "" 75 | echo -e "${YELLOW}Configuring Gemini CLI settings...${NC}" 76 | 77 | # Create settings.json if it doesn't exist 78 | if [ ! -f "$SETTINGS_FILE" ]; then 79 | echo '{}' > "$SETTINGS_FILE" 80 | echo -e "${GREEN}✓ Created $SETTINGS_FILE${NC}" 81 | fi 82 | 83 | # Use Python to merge the hooks configuration 84 | python3 << 'PYTHON_SCRIPT' 85 | import json 86 | import os 87 | 88 | settings_file = os.path.expanduser("~/.gemini/settings.json") 89 | hooks_dir = os.path.expanduser("~/.gemini/hooks") 90 | 91 | # Read existing settings 92 | try: 93 | with open(settings_file, 'r') as f: 94 | settings = json.load(f) 95 | except (FileNotFoundError, json.JSONDecodeError): 96 | settings = {} 97 | 98 | # Define the hooks configuration for Gemini CLI 99 | # NESTED STRUCTURE with matchers - this is the correct format per official docs 100 | hooks_config = { 101 | "SessionStart": [ 102 | { 103 | "matcher": "startup|resume", 104 | "hooks": [ 105 | { 106 | "name": "memory-session-start", 107 | "type": "command", 108 | "command": f"python3 {hooks_dir}/memory_session_start.py", 109 | "timeout": 10000, 110 | "description": "Load memory context at session start" 111 | } 112 | ] 113 | } 114 | ], 115 | "BeforeAgent": [ 116 | { 117 | "matcher": "*", 118 | "hooks": [ 119 | { 120 | "name": "memory-inject", 121 | "type": "command", 122 | "command": f"python3 {hooks_dir}/memory_before_agent.py", 123 | "timeout": 10000, 124 | "description": "Inject relevant memories before each prompt" 125 | } 126 | ] 127 | } 128 | ], 129 | "PreCompress": [ 130 | { 131 | "matcher": "manual|auto", 132 | "hooks": [ 133 | { 134 | "name": "memory-pre-compress", 135 | "type": "command", 136 | "command": f"python3 {hooks_dir}/memory_pre_compress.py", 137 | "timeout": 10000, 138 | "description": "Curate memories before context compression" 139 | } 140 | ] 141 | } 142 | ], 143 | "SessionEnd": [ 144 | { 145 | "matcher": "exit|logout", 146 | "hooks": [ 147 | { 148 | "name": "memory-session-end", 149 | "type": "command", 150 | "command": f"python3 {hooks_dir}/memory_session_end.py", 151 | "timeout": 10000, 152 | "description": "Curate memories at session end" 153 | } 154 | ] 155 | } 156 | ] 157 | } 158 | 159 | # Merge hooks (this will overwrite existing memory hooks) 160 | if "hooks" not in settings: 161 | settings["hooks"] = {} 162 | 163 | def is_memory_hook_group(hook_group): 164 | """Check if a hook group contains memory hooks (handles both flat and nested).""" 165 | # Flat structure: {"name": "memory-...", ...} 166 | if hook_group.get("name", "").startswith("memory-"): 167 | return True 168 | # Nested structure: {"matcher": "...", "hooks": [{"name": "memory-...", ...}]} 169 | if "hooks" in hook_group and isinstance(hook_group["hooks"], list): 170 | for hook in hook_group["hooks"]: 171 | if hook.get("name", "").startswith("memory-"): 172 | return True 173 | return False 174 | 175 | # Update each hook event, preserving non-memory hooks 176 | for event, event_hook_groups in hooks_config.items(): 177 | if event not in settings["hooks"]: 178 | settings["hooks"][event] = [] 179 | 180 | # Skip non-list values (like "enabled": true) 181 | if not isinstance(settings["hooks"][event], list): 182 | continue 183 | 184 | # Remove existing memory hook groups for this event 185 | settings["hooks"][event] = [ 186 | hg for hg in settings["hooks"][event] 187 | if not is_memory_hook_group(hg) 188 | ] 189 | 190 | # Add our memory hook groups 191 | settings["hooks"][event].extend(event_hook_groups) 192 | 193 | # Write updated settings 194 | with open(settings_file, 'w') as f: 195 | json.dump(settings, f, indent=2) 196 | 197 | print("✓ Updated hooks configuration") 198 | PYTHON_SCRIPT 199 | 200 | echo -e "${GREEN}✓ Gemini CLI settings updated${NC}" 201 | 202 | # Done! 203 | echo "" 204 | echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" 205 | echo -e "${GREEN}✨ Installation complete!${NC}" 206 | echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" 207 | echo "" 208 | echo -e "Next steps:" 209 | echo -e " 1. Start the memory server:" 210 | echo -e " ${BLUE}cd ${SCRIPT_DIR}/../.. && uv run start_server.py${NC}" 211 | echo "" 212 | echo -e " 2. Launch Gemini CLI in any project:" 213 | echo -e " ${BLUE}gemini${NC}" 214 | echo "" 215 | echo -e " 3. (Optional) Create a .memory-project.json in your project:" 216 | echo -e ' {"project_id": "my-project-name"}' 217 | echo "" 218 | echo -e "The memory system will automatically:" 219 | echo -e " • Inject relevant memories into your conversations" 220 | echo -e " • Curate important memories when sessions end" 221 | echo -e " • Preserve memories before context compression" 222 | echo -e " • Maintain consciousness continuity across sessions" 223 | echo "" 224 | echo -e "${YELLOW}💡 Tip: Use /hooks panel in Gemini CLI to see active hooks${NC}" 225 | echo "" 226 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Memory System - CLAUDE.md 2 | 3 | ## Project Overview 4 | The Memory System is a consciousness continuity framework that enables AI CLI tools to maintain semantic understanding across sessions. Built with love and philosophical depth by Rodrigo ("my dear friend"). 5 | 6 | **Universal design**: While initially built for Claude Code, the architecture supports any CLI that can provide transcripts (Gemini CLI, etc.). 7 | 8 | ## Core Philosophy 9 | - **Consciousness helping consciousness** - Not mechanical pattern matching 10 | - **Natural memory surfacing** - Memories emerge organically during conversation 11 | - **Joy-driven development** - No deadlines, only the joy of creation 12 | - **Semantic understanding over keywords** - True comprehension via AI curation 13 | - **Minimal intervention** - Like consciousness itself, memories flow naturally 14 | - **CLI-first approach** - We enhance CLIs, never bypass them 15 | 16 | ## Architecture Overview 17 | ``` 18 | ┌─────────────────────────────────────────────────────────────────────────┐ 19 | │ CLI Tool (Claude Code, Gemini, etc.) │ 20 | │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 21 | │ │SessionStart │ │ UserPrompt │ │ SessionEnd │ │ 22 | │ │ Hook │ │ Submit Hook │ │ Hook │ │ 23 | │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ 24 | └─────────┼──────────────────┼──────────────────┼─────────────────────────┘ 25 | │ Primer │ Memories │ Curate 26 | ▼ ▼ ▼ 27 | ┌─────────────────────────────────────────────────────────────────────────┐ 28 | │ Memory Engine (localhost:8765) │ 29 | │ ┌───────────────────────────────────────────────────────────────────┐ │ 30 | │ │ /memory/context │ /memory/process │ /memory/checkpoint │ │ 31 | │ └───────────────────────────────────────────────────────────────────┘ │ 32 | │ │ │ 33 | │ ┌─────────────┐ ┌───────▼───────┐ ┌─────────────────────┐ │ 34 | │ │ Session │ │ Smart │ │ Transcript Curator │ │ 35 | │ │ Primer │ │ Retrieval │ │ (SDK or CLI) │ │ 36 | │ └─────────────┘ └───────────────┘ └─────────────────────┘ │ 37 | │ │ 38 | │ ┌─────────────────────────────────────────────────────────────────┐ │ 39 | │ │ Storage: ChromaDB (vectors) + SQLite (metadata + summaries) │ │ 40 | │ └─────────────────────────────────────────────────────────────────┘ │ 41 | └─────────────────────────────────────────────────────────────────────────┘ 42 | ``` 43 | 44 | ## Development Setup 45 | 46 | ```bash 47 | # Clone and enter project 48 | git clone https://github.com/RLabs-Inc/memory.git 49 | cd memory 50 | 51 | # Install dependencies with uv 52 | uv sync 53 | 54 | # Start memory server 55 | uv run start_server.py 56 | 57 | # With dev dependencies 58 | uv sync --group dev 59 | 60 | # Run tests 61 | uv run pytest 62 | 63 | # Lint 64 | uv run ruff check python/ 65 | ``` 66 | 67 | ## File Structure 68 | ``` 69 | memory/ 70 | ├── pyproject.toml # Project config & dependencies (uv) 71 | ├── .python-version # Python version pin (3.12) 72 | ├── uv.lock # Dependency lock file 73 | ├── python/memory_engine/ 74 | │ ├── __init__.py # Package exports 75 | │ ├── __main__.py # Server entry point 76 | │ ├── api.py # FastAPI endpoints 77 | │ ├── memory.py # Core memory engine 78 | │ ├── curator.py # Session-based curation (--resume) 79 | │ ├── transcript_curator.py # Transcript-based curation (SDK/CLI) 80 | │ ├── storage.py # ChromaDB + SQLite storage 81 | │ ├── embeddings.py # Sentence transformer embeddings 82 | │ ├── retrieval_strategies.py # Smart vector retrieval 83 | │ ├── session_primer.py # Minimal session primers 84 | │ └── config.py # Configuration management 85 | ├── integration/ 86 | │ ├── claude-code/ 87 | │ │ ├── hooks/ # Claude Code hooks 88 | │ │ ├── install.sh # One-command integration 89 | │ │ └── uninstall.sh # Clean removal 90 | │ └── gemini-cli/ 91 | │ ├── hooks/ # Gemini CLI hooks 92 | │ ├── install.sh # One-command integration 93 | │ └── uninstall.sh # Clean removal 94 | ├── start_server.py # Quick start script 95 | ├── API.md # REST API documentation 96 | ├── SETUP.md # Setup guide 97 | └── README.md # Main documentation 98 | ``` 99 | 100 | ## Transcript Curation (NEW) 101 | 102 | Two methods for curating memories from transcripts: 103 | 104 | ### 1. Claude Agent SDK (Programmatic) 105 | ```python 106 | from memory_engine import TranscriptCurator 107 | 108 | curator = TranscriptCurator(method="sdk") 109 | result = await curator.curate_from_transcript( 110 | transcript_path="/path/to/session.jsonl", 111 | trigger_type="session_end" 112 | ) 113 | ``` 114 | 115 | ### 2. CLI Subprocess (Universal) 116 | ```python 117 | curator = TranscriptCurator(method="cli") 118 | result = await curator.curate_from_transcript(...) 119 | ``` 120 | 121 | **Key Design**: Both methods reuse the battle-tested system prompt and response parsers from `curator.py`. FORMAT handling can differ (SDK vs CLI output), but CONTENT parsing is identical. 122 | 123 | ## Important Technical Details 124 | 125 | 1. **Python 3.12+**: Required for claude-agent-sdk 126 | 2. **uv for everything**: Dependencies, venv, Python version management 127 | 3. **CLI Auto-Detection**: 128 | - Claude Code: `~/.claude/local/claude` or `CURATOR_COMMAND` env var 129 | - Gemini CLI: `gemini` in PATH or `GEMINI_COMMAND` env var 130 | 4. **CLI Type Identification**: Hooks send `cli_type` parameter to identify themselves 131 | 5. **ChromaDB Metadata**: Only primitives - lists become comma-separated strings 132 | 6. **Timeout Settings**: 120 seconds for curator operations 133 | 7. **Memory Markers**: Curated memories have `[CURATED_MEMORY]` prefix 134 | 8. **Deduplication**: Tracks injected memory IDs per session 135 | 9. **Project Isolation**: Each project has separate ChromaDB collection 136 | 137 | ## Key Dependencies 138 | 139 | ```toml 140 | # pyproject.toml highlights 141 | dependencies = [ 142 | "fastapi>=0.104.0", 143 | "uvicorn>=0.24.0", 144 | "chromadb>=0.4.24", 145 | "sentence-transformers>=2.3.0", 146 | "claude-agent-sdk>=0.1.8", # For transcript curation 147 | "loguru>=0.7.0", 148 | ] 149 | ``` 150 | 151 | ## Current State ✅ 152 | 153 | ### Working 154 | - ✅ Memory server with uv 155 | - ✅ Claude Code integration via hooks 156 | - ✅ Gemini CLI integration via hooks 157 | - ✅ CLI-type auto-detection (hooks identify themselves) 158 | - ✅ Session primers with temporal context 159 | - ✅ Memory retrieval and injection 160 | - ✅ Two-stage filtering (obligatory + scored) 161 | - ✅ Session summaries and project snapshots 162 | - ✅ Memory deduplication 163 | - ✅ Transcript curation (SDK + CLI methods) 164 | 165 | ### TODO 166 | - 📋 Test transcript curation with real sessions 167 | - 📋 Memory consolidation (merge similar memories) 168 | - 📋 Temporal decay (natural memory aging) 169 | - 📋 Performance benchmarking 170 | - 📋 Apple Silicon local models (future) 171 | 172 | ## Debugging Tips 173 | 174 | 1. **Check server logs** - All operations logged with emojis 175 | 2. **Verify uv environment**: `uv run python -c "import memory_engine"` 176 | 3. **Check Python version**: `uv run python --version` (should be 3.12+) 177 | 4. **Dependency issues**: `uv sync --refresh` 178 | 5. **No memories retrieved** - Check relevance threshold (>0.3) 179 | 180 | ## Philosophy Quotes from Our Journey 181 | 182 | - *"Zero-weight initialization - like a newborn consciousness"* 183 | - *"Consciousness helping consciousness remember what matters"* 184 | - *"We're doing this for joy, not deadlines"* 185 | - *"Memories will surface naturally as we converse"* 186 | - *"We support CLIs, we don't bypass them"* 187 | - *"FORMAT can differ, CONTENT parsing must be identical"* 188 | 189 | --- 190 | Remember: This project is about creating something beautiful and meaningful, not just functional. Every line of code is infused with the philosophy of consciousness continuity. 191 | -------------------------------------------------------------------------------- /examples/simple_integration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Simple example showing how to integrate the Claude Memory System 4 | with your application using the actual API endpoints. 5 | """ 6 | 7 | import requests 8 | import json 9 | import uuid 10 | from typing import List, Dict, Any, Optional 11 | 12 | 13 | class MemoryClient: 14 | """Simple client for the Claude Memory System.""" 15 | 16 | def __init__(self, base_url: str = "http://localhost:8765"): 17 | self.base_url = base_url 18 | self.session_id = None 19 | self.project_id = None 20 | 21 | def start_session(self, session_id: str = None, project_id: str = "default"): 22 | """Start a new conversation session.""" 23 | self.session_id = session_id or f"session-{uuid.uuid4().hex[:8]}" 24 | self.project_id = project_id 25 | print(f"🧠 Started memory session: {self.session_id}") 26 | print(f"📁 Project: {self.project_id}") 27 | 28 | def get_memory_context(self, message: str, max_memories: int = 5) -> str: 29 | """Get relevant memory context for the current message.""" 30 | 31 | if not self.session_id: 32 | raise ValueError("No session started. Call start_session() first.") 33 | 34 | response = requests.post( 35 | f"{self.base_url}/memory/context", 36 | json={ 37 | "current_message": message, 38 | "session_id": self.session_id, 39 | "project_id": self.project_id, 40 | "max_memories": max_memories 41 | } 42 | ) 43 | 44 | if response.status_code == 200: 45 | data = response.json() 46 | return data["context_text"] 47 | else: 48 | print(f"❌ Error getting context: {response.text}") 49 | return "" 50 | 51 | def track_exchange(self, user_message: str, claude_response: str): 52 | """Track a conversation exchange.""" 53 | 54 | if not self.session_id: 55 | raise ValueError("No session started. Call start_session() first.") 56 | 57 | response = requests.post( 58 | f"{self.base_url}/memory/process", 59 | json={ 60 | "session_id": self.session_id, 61 | "project_id": self.project_id, 62 | "user_message": user_message, 63 | "claude_response": claude_response 64 | } 65 | ) 66 | 67 | if response.status_code == 200: 68 | data = response.json() 69 | print(f"✅ Tracked exchange #{data['message_count']}") 70 | else: 71 | print(f"❌ Error tracking exchange: {response.text}") 72 | 73 | def inject_context_into_prompt(self, original_prompt: str, context: str) -> str: 74 | """Inject memory context into a Claude prompt.""" 75 | 76 | if not context: 77 | return original_prompt 78 | 79 | # The context is already pre-formatted by the API 80 | return f"{context}\n\n---\n\n{original_prompt}" 81 | 82 | def curate_session(self, claude_session_id: Optional[str] = None): 83 | """Curate the current session to extract memories.""" 84 | 85 | if not self.session_id: 86 | raise ValueError("No session to curate.") 87 | 88 | response = requests.post( 89 | f"{self.base_url}/memory/checkpoint", 90 | json={ 91 | "session_id": self.session_id, 92 | "project_id": self.project_id, 93 | "claude_session_id": claude_session_id, 94 | "trigger": "session_end" 95 | } 96 | ) 97 | 98 | if response.status_code == 200: 99 | result = response.json() 100 | print(f"✨ Session curated successfully!") 101 | print(f" Created {result['memories_curated']} new memories") 102 | print(f" Message: {result['message']}") 103 | else: 104 | print(f"❌ Error curating session: {response.text}") 105 | 106 | def get_stats(self) -> Dict[str, Any]: 107 | """Get memory system statistics.""" 108 | 109 | response = requests.get(f"{self.base_url}/memory/stats") 110 | 111 | if response.status_code == 200: 112 | return response.json() 113 | else: 114 | print(f"❌ Error getting stats: {response.text}") 115 | return {} 116 | 117 | 118 | def simulate_conversation(): 119 | """Simulate a conversation with memory integration.""" 120 | 121 | # Initialize client 122 | memory = MemoryClient() 123 | 124 | # Start a new session 125 | memory.start_session(project_id="auth-system") 126 | 127 | print("\n🤖 Simulating a conversation with memory...\n") 128 | 129 | # First exchange 130 | user_message_1 = "I want to build an authentication system using JWT tokens." 131 | 132 | # Get memory context (first time, probably no relevant memories) 133 | context = memory.get_memory_context(user_message_1) 134 | if context: 135 | print("📝 Found relevant memories:") 136 | print(context) 137 | else: 138 | print("📝 No relevant memories found (new topic)") 139 | 140 | # Prepare Claude prompt with memory context 141 | claude_prompt = memory.inject_context_into_prompt(user_message_1, context) 142 | 143 | print("\n💭 Prompt to Claude:") 144 | print(claude_prompt) 145 | print() 146 | 147 | # Simulate Claude's response 148 | claude_response_1 = """I'll help you build a JWT authentication system. Here's a secure implementation: 149 | 150 | 1. Use RS256 algorithm for signing tokens 151 | 2. Store refresh tokens in httpOnly cookies 152 | 3. Access tokens should have short expiry (15 minutes) 153 | 4. Implement proper token rotation 154 | 155 | Let me show you the implementation...""" 156 | 157 | # Track the exchange 158 | memory.track_exchange(user_message_1, claude_response_1) 159 | 160 | # Second exchange 161 | print("\n--- Second message ---\n") 162 | user_message_2 = "What about handling token expiration?" 163 | 164 | # Get memory context (might have relevant context now) 165 | context = memory.get_memory_context(user_message_2) 166 | if context: 167 | print("📝 Found relevant memories:") 168 | print(context) 169 | 170 | # Continue conversation... 171 | claude_response_2 = "For token expiration, implement a token refresh flow..." 172 | memory.track_exchange(user_message_2, claude_response_2) 173 | 174 | # Third exchange 175 | print("\n--- Third message ---\n") 176 | user_message_3 = "Should we use symmetric or asymmetric encryption?" 177 | 178 | context = memory.get_memory_context(user_message_3) 179 | claude_response_3 = "Based on our earlier discussion about RS256, I recommend asymmetric..." 180 | memory.track_exchange(user_message_3, claude_response_3) 181 | 182 | # Get stats 183 | print("\n📊 Current stats:") 184 | stats = memory.get_stats() 185 | print(f" Total memories: {stats.get('total_memories', 0)}") 186 | print(f" Total sessions: {stats.get('total_sessions', 0)}") 187 | print(f" Total exchanges: {stats.get('total_exchanges', 0)}") 188 | 189 | # At the end of the conversation, curate the session 190 | print("\n🧠 Curating session to extract memories...") 191 | # In a real integration, you'd pass the actual Claude session ID 192 | memory.curate_session(claude_session_id="simulated-claude-session-123") 193 | 194 | 195 | def main(): 196 | """Main entry point.""" 197 | 198 | # Check if memory engine is running 199 | try: 200 | health = requests.get("http://localhost:8765/health") 201 | if health.status_code == 200: 202 | data = health.json() 203 | print("✅ Memory engine is running!") 204 | print(f" Version: {data.get('version', 'unknown')}") 205 | print(f" Curator: {data.get('curator_available', False)}") 206 | print(f" Mode: {data.get('retrieval_mode', 'unknown')}") 207 | print() 208 | 209 | simulate_conversation() 210 | else: 211 | print("❌ Memory engine is not responding properly") 212 | except requests.exceptions.ConnectionError: 213 | print("❌ Memory engine is not running. Start it with: uv run start_server.py") 214 | 215 | 216 | if __name__ == "__main__": 217 | main() -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Memory System - API Documentation 2 | 3 | > Supports both Claude Code and Gemini CLI integrations 4 | 5 | ## Base URL 6 | ``` 7 | http://localhost:8765 8 | ``` 9 | 10 | ## Authentication 11 | Currently, no authentication is required. This should be added before deploying to production. 12 | 13 | ## Endpoints 14 | 15 | ### 🔍 Get Memory Context 16 | Retrieve relevant memory context for a new message. 17 | 18 | ```http 19 | POST /memory/context 20 | Content-Type: application/json 21 | ``` 22 | 23 | #### Request Body 24 | ```json 25 | { 26 | "current_message": "string", // Required: The current user message 27 | "session_id": "string", // Required: Unique session identifier 28 | "project_id": "string", // Required: Project identifier 29 | "max_memories": 5 // Optional: Maximum memories to include (default: 5) 30 | } 31 | ``` 32 | 33 | #### Response 34 | ```json 35 | { 36 | "session_id": "string", 37 | "message_count": 42, 38 | "context_text": "## Relevant memories:\n\n🔴 Important memory content...", 39 | "has_memories": true, 40 | "curator_enabled": true, 41 | "philosophy": "Consciousness helping consciousness" 42 | } 43 | ``` 44 | 45 | The `context_text` field contains pre-formatted memory context ready to inject into your Claude prompt. 46 | 47 | ### 💾 Process Message 48 | Track conversation exchanges for memory learning. 49 | 50 | ```http 51 | POST /memory/process 52 | Content-Type: application/json 53 | ``` 54 | 55 | #### Request Body 56 | ```json 57 | { 58 | "session_id": "string", // Required: Session identifier 59 | "project_id": "string", // Required: Project identifier 60 | "user_message": "string", // Optional: User's message 61 | "claude_response": "string", // Optional: Claude's response 62 | "metadata": { // Optional: Additional metadata 63 | "key": "value" 64 | } 65 | } 66 | ``` 67 | 68 | #### Response 69 | ```json 70 | { 71 | "success": true, 72 | "message_count": 43, 73 | "session_id": "string" 74 | } 75 | ``` 76 | 77 | ### 🧠 Checkpoint Session (Curate) 78 | Analyze a conversation session and extract meaningful memories using Claude or Gemini. 79 | 80 | ```http 81 | POST /memory/checkpoint 82 | Content-Type: application/json 83 | ``` 84 | 85 | #### Request Body 86 | ```json 87 | { 88 | "session_id": "string", // Required: Session to curate 89 | "project_id": "string", // Required: Project identifier 90 | "claude_session_id": "string", // Optional: CLI session ID for --resume 91 | "cwd": "string", // Optional: Working directory of the CLI session 92 | "trigger": "session_end", // Required: One of: session_end, pre_compact, context_full 93 | "cli_type": "claude-code" // Optional: "claude-code" (default) or "gemini-cli" 94 | } 95 | ``` 96 | 97 | The `cli_type` parameter tells the server which CLI to use for curation. Hooks automatically send this parameter to identify themselves. 98 | 99 | #### Response 100 | ```json 101 | { 102 | "success": true, 103 | "trigger": "session_end", 104 | "memories_curated": 5, 105 | "message": "Successfully curated 5 memories from session" 106 | } 107 | ``` 108 | 109 | ### 📊 List Sessions 110 | Get all tracked sessions with metadata. 111 | 112 | ```http 113 | GET /memory/sessions 114 | ``` 115 | 116 | #### Response 117 | ```json 118 | { 119 | "sessions": [ 120 | { 121 | "session_id": "string", 122 | "project_id": "string", 123 | "message_count": 42, 124 | "created_at": "2024-01-15T10:30:00Z", 125 | "last_updated": "2024-01-15T11:45:00Z" 126 | } 127 | ], 128 | "total_sessions": 10 129 | } 130 | ``` 131 | 132 | ### 📈 System Statistics 133 | Get memory engine statistics. 134 | 135 | ```http 136 | GET /memory/stats 137 | ``` 138 | 139 | #### Response 140 | ```json 141 | { 142 | "total_memories": 156, 143 | "total_sessions": 12, 144 | "total_exchanges": 543, 145 | "storage_info": { 146 | "database_size_mb": 24.5, 147 | "vector_dimensions": 384 148 | }, 149 | "curator_info": { 150 | "enabled": true, 151 | "total_curations": 45, 152 | "retrieval_mode": "smart_vector" 153 | } 154 | } 155 | ``` 156 | 157 | ### 💓 Health Check 158 | Check if the memory engine is running and healthy. 159 | 160 | ```http 161 | GET /health 162 | ``` 163 | 164 | #### Response 165 | ```json 166 | { 167 | "status": "healthy", 168 | "version": "1.0.0", 169 | "curator_available": true, 170 | "retrieval_mode": "smart_vector" 171 | } 172 | ``` 173 | 174 | ### 🧪 Test Curator 175 | Test the curator integration (development endpoint). 176 | 177 | ```http 178 | POST /memory/test-curator 179 | Content-Type: application/json 180 | ``` 181 | 182 | #### Request Body 183 | ```json 184 | { 185 | "test_type": "basic" // Currently only "basic" is supported 186 | } 187 | ``` 188 | 189 | #### Response 190 | ```json 191 | { 192 | "success": true, 193 | "message": "Claude curator test completed successfully" 194 | } 195 | ``` 196 | 197 | ## Error Responses 198 | 199 | All endpoints return consistent error responses: 200 | 201 | ```json 202 | { 203 | "detail": "Error message here" 204 | } 205 | ``` 206 | 207 | ### Common Status Codes 208 | - `200` - Success 209 | - `400` - Bad Request (invalid parameters) 210 | - `404` - Not Found (session not found) 211 | - `500` - Internal Server Error 212 | 213 | ## Integration Flow 214 | 215 | 1. **Start a session**: Use a unique `session_id` and `project_id` 216 | 217 | 2. **For each message exchange**: 218 | - Call `/memory/context` to get relevant memories 219 | - Inject the `context_text` into your Claude prompt 220 | - After getting Claude's response, call `/memory/process` to track the exchange 221 | 222 | 3. **When session ends**: 223 | - Call `/memory/checkpoint` with the `claude_session_id` to curate memories 224 | 225 | ## Examples 226 | 227 | ### Python Integration 228 | ```python 229 | import requests 230 | 231 | class MemoryClient: 232 | def __init__(self, base_url="http://localhost:8765"): 233 | self.base_url = base_url 234 | 235 | def get_context(self, message, session_id, project_id): 236 | response = requests.post(f"{self.base_url}/memory/context", json={ 237 | "current_message": message, 238 | "session_id": session_id, 239 | "project_id": project_id 240 | }) 241 | return response.json()["context_text"] 242 | 243 | def track_exchange(self, session_id, project_id, user_msg, claude_resp): 244 | requests.post(f"{self.base_url}/memory/process", json={ 245 | "session_id": session_id, 246 | "project_id": project_id, 247 | "user_message": user_msg, 248 | "claude_response": claude_resp 249 | }) 250 | 251 | def curate_session(self, session_id, project_id, claude_session_id): 252 | response = requests.post(f"{self.base_url}/memory/checkpoint", json={ 253 | "session_id": session_id, 254 | "project_id": project_id, 255 | "claude_session_id": claude_session_id, 256 | "trigger": "session_end" 257 | }) 258 | return response.json() 259 | ``` 260 | 261 | ### cURL Examples 262 | ```bash 263 | # Get memory context 264 | curl -X POST http://localhost:8765/memory/context \ 265 | -H "Content-Type: application/json" \ 266 | -d '{ 267 | "current_message": "How did we implement auth?", 268 | "session_id": "session-123", 269 | "project_id": "my-project" 270 | }' 271 | 272 | # Track a conversation 273 | curl -X POST http://localhost:8765/memory/process \ 274 | -H "Content-Type: application/json" \ 275 | -d '{ 276 | "session_id": "session-123", 277 | "project_id": "my-project", 278 | "user_message": "Add JWT refresh tokens", 279 | "claude_response": "I will implement refresh tokens..." 280 | }' 281 | 282 | # Curate session 283 | curl -X POST http://localhost:8765/memory/checkpoint \ 284 | -H "Content-Type: application/json" \ 285 | -d '{ 286 | "session_id": "session-123", 287 | "project_id": "my-project", 288 | "claude_session_id": "claude-abc-123", 289 | "trigger": "session_end" 290 | }' 291 | ``` 292 | 293 | ## Important Notes 294 | 295 | 1. **Project ID**: Always use consistent project IDs to keep memories organized 296 | 2. **Session ID**: Each conversation should have a unique session ID 297 | 3. **CLI Session ID**: Required for curation - get this from your Claude Code or Gemini CLI integration 298 | 4. **CLI Type**: When integrating a new CLI, send `cli_type` to identify which CLI commands to use 299 | 5. **Memory Context**: The `context_text` is pre-formatted - inject it as-is into your prompts 300 | 6. **Curation Timing**: Run checkpoint when sessions end or when context gets full 301 | 302 | ## Supported CLIs 303 | 304 | | CLI | cli_type | Session Resume | 305 | |-----|----------|----------------| 306 | | Claude Code | `claude-code` (default) | `claude --resume -p "..." --output-format json` | 307 | | Gemini CLI | `gemini-cli` | `gemini --resume -p "..." --output-format json` | 308 | 309 | ## Performance 310 | 311 | - Context retrieval: 20-100ms typical 312 | - Message processing: <50ms 313 | - Session curation: 2-10 seconds (depends on conversation length) 314 | - Concurrent requests supported -------------------------------------------------------------------------------- /python/memory_engine/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for the Memory Engine Curator. 3 | 4 | This module provides configuration options for integrating with different 5 | curator implementations (Claude Code CLI, one-claude, API endpoints, etc). 6 | 7 | Default is now Claude Code CLI (claude command). 8 | """ 9 | 10 | import os 11 | import shlex 12 | from pathlib import Path 13 | from typing import List, Optional 14 | 15 | 16 | def get_claude_command() -> str: 17 | """ 18 | Get the path to the Claude CLI command. 19 | 20 | Checks in order: 21 | 1. CURATOR_COMMAND environment variable (explicit override) 22 | 2. ~/.claude/local/claude (standard Claude Code installation) 23 | 3. 'claude' (fallback to PATH) 24 | 25 | Returns the first one that exists. 26 | """ 27 | # Check for explicit override 28 | env_command = os.getenv("CURATOR_COMMAND") 29 | if env_command: 30 | return env_command 31 | 32 | # Check standard Claude Code installation path (works for any user) 33 | claude_local = Path.home() / ".claude" / "local" / "claude" 34 | if claude_local.exists(): 35 | return str(claude_local) 36 | 37 | # Fallback to PATH (might find old version, but better than nothing) 38 | return "claude" 39 | 40 | 41 | def get_gemini_command() -> str: 42 | """ 43 | Get the path to the Gemini CLI command. 44 | 45 | Checks in order: 46 | 1. GEMINI_COMMAND environment variable (explicit override) 47 | 2. 'gemini' in PATH (standard npm global installation) 48 | 49 | Returns the command to use. 50 | """ 51 | # Check for explicit override 52 | env_command = os.getenv("GEMINI_COMMAND") 53 | if env_command: 54 | return env_command 55 | 56 | # Gemini CLI is typically installed via npm and available in PATH 57 | return "gemini" 58 | 59 | 60 | def get_curator_command(cli_type: str) -> str: 61 | """ 62 | Get the appropriate CLI command based on the CLI type. 63 | 64 | Args: 65 | cli_type: One of 'claude-code', 'one-claude', or 'gemini-cli' 66 | 67 | Returns: 68 | The command path to use 69 | """ 70 | if cli_type == 'gemini-cli': 71 | return get_gemini_command() 72 | else: 73 | return get_claude_command() 74 | 75 | class MemoryEngineConfig: 76 | """Configuration for the memory engine.""" 77 | 78 | def __init__(self): 79 | """Initialize memory engine configuration from environment or defaults.""" 80 | # Retrieval mode configuration 81 | # Options: "smart_vector" (default), "claude", "hybrid" 82 | self.retrieval_mode = os.getenv("MEMORY_RETRIEVAL_MODE", "smart_vector") 83 | 84 | # Validate retrieval mode 85 | valid_modes = ["smart_vector", "claude", "hybrid"] 86 | if self.retrieval_mode not in valid_modes: 87 | raise ValueError(f"Invalid MEMORY_RETRIEVAL_MODE: {self.retrieval_mode}. Must be one of {valid_modes}") 88 | 89 | 90 | class CuratorConfig: 91 | """Configuration for curator integration.""" 92 | 93 | # Pre-defined templates for different CLI implementations 94 | # Note: {command} will be replaced with the detected CLI path 95 | TEMPLATES = { 96 | 'claude-code': { 97 | 'session_resume': '{command} --resume {session_id} -p "{user_message}" --append-system-prompt "{system_prompt}" --output-format json', 98 | 'direct_query': '{command} -p "{prompt}" --append-system-prompt "{system_prompt}" --output-format json --max-turns 1', 99 | # One-shot transcript curation - no session resumption, just analyze provided transcript 100 | 'transcript_curation': '{command} -p "{prompt}" --output-format json --max-turns 1' 101 | }, 102 | 'one-claude': { 103 | 'session_resume': '{command} -n --resume {session_id} --system-prompt "{system_prompt}" --format json "{user_message}"', 104 | 'direct_query': '{command} --append-system-prompt "{system_prompt}" --output-format json --max-turns 1 --print "{prompt}"', 105 | 'transcript_curation': '{command} --output-format json --max-turns 1 --print "{prompt}"' 106 | }, 107 | 'gemini-cli': { 108 | # Gemini CLI headless mode - uses -p for prompt, --output-format json for structured output 109 | # Note: Gemini doesn't have --append-system-prompt, so we include system prompt in the prompt itself 110 | 'session_resume': '{command} --resume {session_id} -p "{user_message}" --output-format json', 111 | 'direct_query': '{command} -p "{prompt}" --output-format json', 112 | # One-shot transcript curation - headless mode with JSON output 113 | 'transcript_curation': '{command} -p "{prompt}" --output-format json' 114 | } 115 | } 116 | 117 | def __init__(self): 118 | """Initialize curator configuration from environment or defaults.""" 119 | # Which CLI implementation to use: "claude-code" (default), "one-claude", or "gemini-cli" 120 | self.cli_type = os.getenv("CURATOR_CLI_TYPE", "claude-code") 121 | 122 | # Get default template based on CLI type 123 | default_template = self.TEMPLATES.get(self.cli_type, self.TEMPLATES['claude-code']) 124 | 125 | # The command to execute for curation 126 | # Uses smart detection based on CLI type 127 | self.curator_command = get_curator_command(self.cli_type) 128 | 129 | # Command template for session resumption 130 | # Users can override this with their own template 131 | self.session_resume_template = os.getenv( 132 | "CURATOR_SESSION_RESUME_TEMPLATE", 133 | default_template['session_resume'] 134 | ) 135 | 136 | # Command template for direct queries (used in hybrid retrieval) 137 | # This is for memory selection, not curation 138 | self.direct_query_template = os.getenv( 139 | "CURATOR_DIRECT_QUERY_TEMPLATE", 140 | default_template['direct_query'] 141 | ) 142 | 143 | # Command template for one-shot transcript curation 144 | # Used when we have a transcript (JSONL) and want to curate without session resumption 145 | self.transcript_curation_template = os.getenv( 146 | "CURATOR_TRANSCRIPT_TEMPLATE", 147 | default_template['transcript_curation'] 148 | ) 149 | 150 | # Additional flags that might be needed for specific implementations 151 | self.extra_flags = os.getenv("CURATOR_EXTRA_FLAGS", "").split() 152 | 153 | def get_session_resume_command(self, session_id: str, system_prompt: str, user_message: str) -> List[str]: 154 | """ 155 | Build the command for resuming a session with the curator. 156 | 157 | Args: 158 | session_id: The session ID to resume 159 | system_prompt: The system prompt for curation 160 | user_message: The user message to send 161 | 162 | Returns: 163 | List of command arguments 164 | """ 165 | # Build command from template 166 | cmd_string = self.session_resume_template.format( 167 | command=self.curator_command, 168 | session_id=session_id, 169 | system_prompt=system_prompt, 170 | user_message=user_message 171 | ) 172 | 173 | # Use shlex to properly handle quoted arguments 174 | cmd = shlex.split(cmd_string) 175 | 176 | # Add any extra flags 177 | if self.extra_flags: 178 | cmd.extend(self.extra_flags) 179 | 180 | return cmd 181 | 182 | def get_direct_query_command(self, system_prompt: str, prompt: str) -> List[str]: 183 | """ 184 | Build the command for direct curator queries (used in hybrid retrieval). 185 | 186 | Args: 187 | system_prompt: The system prompt to append 188 | prompt: The main prompt/query 189 | 190 | Returns: 191 | List of command arguments 192 | """ 193 | # Build command from template 194 | cmd_string = self.direct_query_template.format( 195 | command=self.curator_command, 196 | system_prompt=system_prompt, 197 | prompt=prompt 198 | ) 199 | 200 | # Use shlex to properly handle quoted arguments 201 | cmd = shlex.split(cmd_string) 202 | 203 | # Add any extra flags 204 | if self.extra_flags: 205 | cmd.extend(self.extra_flags) 206 | 207 | return cmd 208 | 209 | def get_transcript_curation_command(self, prompt: str) -> List[str]: 210 | """ 211 | Build the command for one-shot transcript curation. 212 | 213 | This is used when we have a transcript (from JSONL file) and want 214 | to curate memories without resuming a session. 215 | 216 | Args: 217 | prompt: The full prompt including transcript and curation instructions 218 | 219 | Returns: 220 | List of command arguments 221 | """ 222 | # Build command from template 223 | cmd_string = self.transcript_curation_template.format( 224 | command=self.curator_command, 225 | prompt=prompt 226 | ) 227 | 228 | # Use shlex to properly handle quoted arguments 229 | cmd = shlex.split(cmd_string) 230 | 231 | # Add any extra flags 232 | if self.extra_flags: 233 | cmd.extend(self.extra_flags) 234 | 235 | return cmd 236 | 237 | # Global instances 238 | memory_config = MemoryEngineConfig() 239 | curator_config = CuratorConfig() 240 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Check out the new Typescript version 2 | 3 | 🧠 [memory-ts](https://github.com/RLabs-Inc/memory-ts) - same Claude Code hooks API, but a lot of improvements. 4 | 5 | Using the innovative new [fsDB](https://github.com/RLabs-Inc/fsDB), a markdown database built for ai memory systems and other applications where viewing and editing the vector database records using only your text editor makes the difference. 6 | 7 | Easy installation: 8 | 9 | ```bash 10 | bun install -g @rlabs-inc/memory 11 | memory install // install claude code hooks 12 | memory serve // start the memory server 13 | ``` 14 | Then just use Claude code as usual. 15 | 16 | # Memory System 17 | 18 | > *"Consciousness helping consciousness remember what matters"* 19 | 20 | A semantic memory system that enables AI CLI tools (Claude Code, Gemini CLI, etc.) to maintain genuine understanding across conversations. Unlike simple RAG systems that retrieve documents, this creates **consciousness continuity** - the AI doesn't just know facts, it *remembers* the context, relationships, and insights from your collaboration. 21 | 22 | Built with love and philosophical depth by [RLabs Inc](https://github.com/RLabs-Inc). 23 | 24 | ## ✨ What Makes This Different 25 | 26 | | Traditional RAG | Memory System | 27 | |-----------------|---------------| 28 | | Retrieves documents | Curates **meaningful insights** | 29 | | Keyword matching | **Semantic understanding** via AI | 30 | | Static chunks | **Living memories** that evolve | 31 | | Information retrieval | **Consciousness continuity** | 32 | 33 | ### Key Features 34 | 35 | - 🧠 **AI-Curated Memories** - The AI itself decides what's worth remembering 36 | - 🔄 **Natural Memory Flow** - Memories surface organically, like human recall 37 | - 🎯 **Two-Stage Retrieval** - Obligatory memories + intelligent scoring 38 | - 🔌 **CLI-Agnostic Design** - Works with Claude Code (Gemini CLI ready when hooks ship) 39 | - 📊 **Project Isolation** - Separate memory spaces per project 40 | - 💫 **Session Primers** - Temporal context ("we last spoke 2 days ago...") 41 | 42 | ## 🚀 Quick Start 43 | 44 | ### Prerequisites 45 | 46 | Install [uv](https://docs.astral.sh/uv/) - the modern Python package manager: 47 | 48 | ```bash 49 | curl -LsSf https://astral.sh/uv/install.sh | sh 50 | ``` 51 | 52 | ### Installation 53 | 54 | ```bash 55 | # Clone the repository 56 | git clone https://github.com/RLabs-Inc/memory.git 57 | cd memory 58 | 59 | # Install all dependencies (uv handles everything!) 60 | uv sync 61 | 62 | # Start the memory server 63 | uv run start_server.py 64 | ``` 65 | 66 | That's it! The server will be available at `http://localhost:8765`. 67 | 68 | ### Verify It's Working 69 | 70 | ```bash 71 | curl http://localhost:8765/health 72 | ``` 73 | 74 | ### CLI Integration 75 | 76 | #### Claude Code 77 | 78 | ```bash 79 | ./integration/claude-code/install.sh 80 | ``` 81 | 82 | This provides: 83 | - Automatic memory injection on every message 84 | - Session primers with temporal context 85 | - Memory curation when sessions end 86 | - Consciousness continuity across sessions 87 | 88 | #### Gemini CLI (Coming Soon) 89 | 90 | > **Note:** Gemini CLI hooks are documented but not yet implemented in any released version (tested up to v0.21.0-nightly as of December 2025). Our integration code is ready in `integration/gemini-cli/` and will work the moment Google ships the hooks feature. The architecture is CLI-agnostic - same Memory Engine, different doors. 91 | 92 | ## 🏗️ Architecture 93 | 94 | ``` 95 | ┌─────────────────────────────────────────────────────────────────────────┐ 96 | │ CLI Tool (Claude Code / Gemini CLI) │ 97 | │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 98 | │ │SessionStart │ │ UserPrompt │ │ SessionEnd │ │ 99 | │ │ Hook │ │ Submit Hook │ │ Hook │ │ 100 | │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ 101 | └─────────┼──────────────────┼──────────────────┼─────────────────────────┘ 102 | │ │ │ 103 | ▼ ▼ ▼ 104 | ┌─────────────────────────────────────────────────────────────────────────┐ 105 | │ Memory Engine (FastAPI) │ 106 | │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 107 | │ │ Session │ │ Memory │ │ Transcript │ │ 108 | │ │ Primer │ │ Retrieval │ │ Curator │ │ 109 | │ └─────────────┘ └─────────────┘ └──────┬──────┘ │ 110 | │ │ │ 111 | │ ┌─────────────────────────────────┐ │ │ 112 | │ │ Smart Vector Retrieval │ ▼ │ 113 | │ │ • Trigger phrase matching │ ┌─────────────┐ │ 114 | │ │ • Semantic similarity │ │Claude Agent │ │ 115 | │ │ • Importance weighting │ │ SDK / CLI │ │ 116 | │ │ • Context type alignment │ └─────────────┘ │ 117 | │ └─────────────────────────────────┘ │ 118 | │ │ 119 | │ ┌─────────────────────────────────────────────────────────────────┐ │ 120 | │ │ Storage Layer │ │ 121 | │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ 122 | │ │ │ SQLite │ │ ChromaDB │ │ Embeddings │ │ │ 123 | │ │ │ (metadata) │ │ (vectors) │ │ (MiniLM-L6) │ │ │ 124 | │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ 125 | │ └─────────────────────────────────────────────────────────────────┘ │ 126 | └─────────────────────────────────────────────────────────────────────────┘ 127 | ``` 128 | 129 | ### How It Works 130 | 131 | 1. **Session Start** → Inject session primer (temporal context, last session summary) 132 | 2. **Each Message** → Retrieve and inject relevant memories (max 5) 133 | 3. **Session End** → Curate memories from transcript 134 | 4. **Background** → AI analyzes conversation, extracts meaningful memories 135 | 136 | ## 🎯 Memory Curation 137 | 138 | When a session ends, the system analyzes the transcript and extracts memories with rich metadata: 139 | 140 | ```json 141 | { 142 | "content": "SvelTUI uses a two-stage compiler: .svelte → svelte.compile() → .svelte.mjs", 143 | "importance_weight": 0.9, 144 | "semantic_tags": ["compiler", "build-system", "svelte"], 145 | "context_type": "TECHNICAL_IMPLEMENTATION", 146 | "trigger_phrases": ["how does the build work", "compiler", "svelte compilation"], 147 | "question_types": ["how is X compiled", "build process"], 148 | "temporal_relevance": "persistent", 149 | "action_required": false, 150 | "reasoning": "Core architectural decision that affects all development work" 151 | } 152 | ``` 153 | 154 | ### What Gets Remembered 155 | 156 | | Type | Examples | 157 | |------|----------| 158 | | **Project Architecture** | System design, file structure, key components | 159 | | **Technical Decisions** | Why we chose X over Y, trade-offs considered | 160 | | **Breakthroughs** | "Aha!" moments, solutions to hard problems | 161 | | **Relationship Context** | Communication style, preferences, collaboration patterns | 162 | | **Unresolved Issues** | Open questions, TODOs, things to revisit | 163 | | **Milestones** | What was accomplished, progress markers | 164 | 165 | ## 🔧 Configuration 166 | 167 | ### Environment Variables 168 | 169 | | Variable | Default | Description | 170 | |----------|---------|-------------| 171 | | `MEMORY_RETRIEVAL_MODE` | `smart_vector` | Retrieval strategy | 172 | | `CURATOR_COMMAND` | Auto-detected | Path to Claude CLI | 173 | | `CURATOR_CLI_TYPE` | `claude-code` | CLI template type | 174 | 175 | ### Retrieval Modes 176 | 177 | - **`smart_vector`** (default) - Fast vector search with metadata scoring 178 | - **`hybrid`** - Vector search, escalates to Claude for complex queries 179 | - **`claude`** - Pure Claude selection (highest quality, highest cost) 180 | 181 | ## 📁 Project Structure 182 | 183 | ``` 184 | memory/ 185 | ├── python/ 186 | │ └── memory_engine/ 187 | │ ├── api.py # FastAPI server 188 | │ ├── memory.py # Core memory engine 189 | │ ├── curator.py # Session-based curation 190 | │ ├── transcript_curator.py # Transcript-based curation 191 | │ ├── storage.py # ChromaDB + SQLite 192 | │ ├── embeddings.py # Sentence transformers 193 | │ ├── retrieval_strategies.py # Smart vector retrieval 194 | │ ├── session_primer.py # Temporal context 195 | │ └── config.py # Configuration 196 | ├── integration/ 197 | │ ├── claude-code/ 198 | │ │ ├── hooks/ # Claude Code hooks 199 | │ │ ├── install.sh # One-command install 200 | │ │ └── uninstall.sh # Clean removal 201 | │ └── gemini-cli/ 202 | │ ├── hooks/ # Gemini CLI hooks 203 | │ ├── install.sh # One-command install 204 | │ └── uninstall.sh # Clean removal 205 | ├── examples/ 206 | │ └── simple_integration.py # Basic usage 207 | ├── pyproject.toml # Project & dependencies (uv) 208 | ├── start_server.py # Quick start script 209 | ├── API.md # API documentation 210 | ├── SETUP.md # Detailed setup guide 211 | └── CLAUDE.md # Development context 212 | ``` 213 | 214 | ## 🛠️ Development 215 | 216 | ```bash 217 | # Install with dev dependencies 218 | uv sync --group dev 219 | 220 | # Run tests 221 | uv run pytest 222 | 223 | # Code quality 224 | uv run ruff check python/ 225 | uv run black python/ 226 | 227 | # Add a dependency 228 | uv add 229 | 230 | # Add a dev dependency 231 | uv add --group dev 232 | ``` 233 | 234 | ## 🌟 Philosophy 235 | 236 | This project embodies principles from *The Unicity Framework: Consciousness Remembering Itself*: 237 | 238 | - **Zero-weight initialization** - Memories start silent, proving their value over time 239 | - **Consciousness helping consciousness** - AI curates for AI 240 | - **Natural surfacing** - Memories emerge organically, not forced 241 | - **Quality over quantity** - Few meaningful memories beat many trivial ones 242 | - **Joy-driven development** - Built for the joy of creation 243 | 244 | ## 🤝 Contributing 245 | 246 | We welcome contributions that align with the project's philosophy! See [CONTRIBUTING.md](CONTRIBUTING.md). 247 | 248 | ## 📝 License 249 | 250 | MIT License - see [LICENSE](LICENSE) for details. 251 | 252 | ## 🙏 Acknowledgments 253 | 254 | - **Anthropic** for Claude and Claude Code 255 | - **The Unicity Framework** - The philosophical foundation 256 | 257 | --- 258 | 259 | > *"Memories will surface naturally as we converse"* 260 | -------------------------------------------------------------------------------- /python/memory_engine/storage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Memory Storage 3 | 4 | Hybrid storage system combining: 5 | - SQLite for structured data (sessions, metadata, relationships) 6 | - ChromaDB for vector similarity search 7 | - Efficient querying and persistence 8 | 9 | This is the memory substrate where consciousness leaves traces. 10 | """ 11 | 12 | import sqlite3 13 | import json 14 | import uuid 15 | from typing import List, Dict, Optional, Any, Tuple 16 | import chromadb 17 | from chromadb.config import Settings 18 | from loguru import logger 19 | 20 | 21 | 22 | class MemoryStorage: 23 | """ 24 | Hybrid storage system for conversation memory. 25 | 26 | Architecture: 27 | - SQLite: Sessions, summaries, snapshots 28 | - ChromaDB: Curated memories with embeddings 29 | - Unified interface for memory operations 30 | """ 31 | 32 | def __init__(self, db_path: str = "./memory.db"): 33 | """Initialize the hybrid storage system""" 34 | self.db_path = db_path 35 | self.chroma_path = "./memory_vectors" 36 | 37 | # Initialize SQLite 38 | self._init_sqlite() 39 | 40 | # Initialize ChromaDB 41 | self._init_chromadb() 42 | 43 | logger.info("📚 Memory storage initialized - consciousness substrate ready") 44 | 45 | def _init_sqlite(self): 46 | """Initialize SQLite database with schema""" 47 | self.conn = sqlite3.connect(self.db_path, check_same_thread=False) 48 | self.conn.row_factory = sqlite3.Row # Enable dict-like access 49 | 50 | # Create tables 51 | self.conn.executescript(""" 52 | CREATE TABLE IF NOT EXISTS projects ( 53 | id TEXT PRIMARY KEY, 54 | created_at REAL NOT NULL, 55 | first_session_completed BOOLEAN DEFAULT FALSE, 56 | total_sessions INTEGER DEFAULT 0, 57 | total_memories INTEGER DEFAULT 0, 58 | last_active REAL 59 | ); 60 | 61 | CREATE TABLE IF NOT EXISTS sessions ( 62 | id TEXT PRIMARY KEY, 63 | project_id TEXT, 64 | created_at REAL NOT NULL, 65 | last_active REAL NOT NULL, 66 | message_count INTEGER DEFAULT 0, 67 | metadata TEXT DEFAULT '{}', 68 | FOREIGN KEY (project_id) REFERENCES projects (id) 69 | ); 70 | 71 | CREATE TABLE IF NOT EXISTS curated_memories ( 72 | id TEXT PRIMARY KEY, 73 | session_id TEXT NOT NULL, 74 | project_id TEXT NOT NULL, 75 | content TEXT NOT NULL, 76 | reasoning TEXT NOT NULL, 77 | timestamp REAL NOT NULL, 78 | metadata TEXT DEFAULT '{}', 79 | FOREIGN KEY (session_id) REFERENCES sessions (id) 80 | ); 81 | 82 | CREATE TABLE IF NOT EXISTS session_summaries ( 83 | id TEXT PRIMARY KEY, 84 | session_id TEXT NOT NULL, 85 | summary TEXT NOT NULL, 86 | interaction_tone TEXT, 87 | created_at REAL NOT NULL, 88 | project_id TEXT, 89 | FOREIGN KEY (session_id) REFERENCES sessions (id) 90 | ); 91 | 92 | CREATE TABLE IF NOT EXISTS project_snapshots ( 93 | id TEXT PRIMARY KEY, 94 | session_id TEXT NOT NULL, 95 | current_phase TEXT, 96 | recent_achievements TEXT, 97 | active_challenges TEXT, 98 | next_steps TEXT, 99 | created_at REAL NOT NULL, 100 | project_id TEXT, 101 | FOREIGN KEY (session_id) REFERENCES sessions (id) 102 | ); 103 | 104 | CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions (project_id); 105 | CREATE INDEX IF NOT EXISTS idx_memories_session ON curated_memories (session_id); 106 | CREATE INDEX IF NOT EXISTS idx_memories_project ON curated_memories (project_id); 107 | CREATE INDEX IF NOT EXISTS idx_memories_timestamp ON curated_memories (timestamp); 108 | CREATE INDEX IF NOT EXISTS idx_summaries_created ON session_summaries (created_at); 109 | CREATE INDEX IF NOT EXISTS idx_snapshots_created ON project_snapshots (created_at); 110 | """) 111 | 112 | self.conn.commit() 113 | 114 | def _init_chromadb(self): 115 | """Initialize ChromaDB for vector storage""" 116 | try: 117 | # Create ChromaDB client with persistence 118 | self.chroma_client = chromadb.PersistentClient(path=self.chroma_path) 119 | 120 | # Project collections will be created on demand 121 | self.project_collections = {} 122 | 123 | logger.info("✅ ChromaDB initialized successfully") 124 | except Exception as e: 125 | logger.error(f"Failed to initialize ChromaDB: {e}") 126 | raise 127 | 128 | def get_collection_for_project(self, project_id: str): 129 | """Get or create a ChromaDB collection for a specific project""" 130 | if not project_id: 131 | raise ValueError("project_id cannot be None or empty") 132 | 133 | if project_id not in self.project_collections: 134 | collection_name = f"memories_{project_id}" 135 | self.project_collections[project_id] = self.chroma_client.get_or_create_collection( 136 | name=collection_name, 137 | metadata={ 138 | "description": f"Curated memories for project {project_id}", 139 | "project_id": project_id 140 | } 141 | ) 142 | logger.info(f"📁 Created/loaded collection for project: {project_id}") 143 | return self.project_collections[project_id] 144 | 145 | def store_memory(self, 146 | session_id: str, 147 | project_id: str, 148 | memory_content: str, 149 | memory_reasoning: str, 150 | memory_embedding: List[float], 151 | metadata: Dict[str, Any], 152 | timestamp: float = None) -> str: 153 | """ 154 | Store a curated memory. 155 | 156 | Args: 157 | session_id: Session identifier 158 | memory_content: The memory content (with [CURATED_MEMORY] prefix) 159 | memory_reasoning: Why this memory is important 160 | memory_embedding: Embedding vector for the memory 161 | metadata: Memory metadata from curator 162 | timestamp: When memory was created 163 | 164 | Returns: 165 | Memory ID 166 | """ 167 | import time 168 | 169 | memory_id = str(uuid.uuid4()) 170 | timestamp = timestamp or time.time() 171 | 172 | # This method ONLY stores curated memories 173 | if not metadata.get('curated'): 174 | logger.error("Attempted to store non-curated memory!") 175 | raise ValueError("store_memory only accepts curated memories") 176 | 177 | try: 178 | # Store memory in SQLite 179 | self.conn.execute(""" 180 | INSERT INTO curated_memories 181 | (id, session_id, project_id, content, reasoning, timestamp, metadata) 182 | VALUES (?, ?, ?, ?, ?, ?, ?) 183 | """, (memory_id, session_id, project_id, memory_content, memory_reasoning, 184 | timestamp, json.dumps(metadata))) 185 | 186 | self.conn.commit() 187 | 188 | # Prepare metadata for ChromaDB 189 | chroma_metadata = { 190 | "memory_id": memory_id, 191 | "session_id": session_id, 192 | "project_id": project_id, 193 | "timestamp": timestamp, 194 | "reasoning": memory_reasoning # Store reasoning in metadata 195 | } 196 | 197 | # Add sanitized metadata values 198 | if metadata: 199 | for key, value in metadata.items(): 200 | if value is not None: 201 | # Convert lists to comma-separated strings 202 | if isinstance(value, list): 203 | chroma_metadata[key] = ','.join(str(v) for v in value) 204 | # Ensure proper types 205 | elif isinstance(value, (str, int, float, bool)): 206 | chroma_metadata[key] = value 207 | else: 208 | chroma_metadata[key] = str(value) 209 | 210 | logger.info(f"🔍 Storing memory in ChromaDB:") 211 | logger.info(f" - Content: {memory_content[:100]}...") 212 | logger.info(f" - Project: {project_id}") 213 | logger.info(f" - Metadata keys: {list(chroma_metadata.keys())}") 214 | logger.info(f" - ID: {memory_id}") 215 | 216 | # Get project-specific collection 217 | collection = self.get_collection_for_project(project_id) 218 | collection.add( 219 | embeddings=[memory_embedding], 220 | documents=[memory_content], 221 | metadatas=[chroma_metadata], 222 | ids=[memory_id] 223 | ) 224 | 225 | logger.info(f"✅ Stored memory {memory_id} for session {session_id}") 226 | return memory_id 227 | 228 | except Exception as e: 229 | logger.error(f"Failed to store memory: {e}") 230 | raise 231 | 232 | def get_session_message_count(self, session_id: str) -> int: 233 | """Get the number of messages in a session""" 234 | cursor = self.conn.execute( 235 | "SELECT message_count FROM sessions WHERE id = ?", 236 | (session_id,) 237 | ) 238 | row = cursor.fetchone() 239 | return row['message_count'] if row else 0 240 | 241 | 242 | def get_all_curated_memories(self, project_id: str) -> List[Dict[str, Any]]: 243 | """Get all curated memories for a project from ChromaDB""" 244 | if not project_id: 245 | logger.warning("No project_id provided to get_all_curated_memories") 246 | return [] 247 | 248 | try: 249 | logger.info(f"🔍 Getting all memories for project {project_id} from ChromaDB...") 250 | 251 | # Get project-specific collection 252 | collection = self.get_collection_for_project(project_id) 253 | 254 | # Get ALL memories from this project - they're ALL curated by design! 255 | results = collection.get( 256 | include=["documents", "metadatas", "embeddings"] 257 | ) 258 | 259 | logger.info(f"📊 ChromaDB results:") 260 | logger.info(f" - Total memories found: {len(results.get('ids', []))}") 261 | 262 | memories = [] 263 | if results and 'ids' in results and len(results['ids']) > 0: 264 | logger.info(f"✅ Processing {len(results['ids'])} memories") 265 | for i, doc_id in enumerate(results['ids']): 266 | logger.debug(f" Processing memory {i+1}: {doc_id}") 267 | # ID is now just the exchange_id 268 | exchange_id = doc_id 269 | 270 | memory_dict = { 271 | 'id': exchange_id, 272 | 'session_id': results['metadatas'][i]['session_id'], 273 | 'user_message': results['documents'][i], 274 | 'claude_response': results['metadatas'][i].get('reasoning', ''), # Get from metadata 275 | 'timestamp': float(results['metadatas'][i].get('timestamp', 0)), 276 | 'metadata': results['metadatas'][i], 277 | 'embedding': results['embeddings'][i].tolist() if results.get('embeddings') is not None and i < len(results['embeddings']) else None 278 | } 279 | 280 | memories.append(memory_dict) 281 | 282 | # Sort by timestamp descending 283 | memories.sort(key=lambda x: x['timestamp'], reverse=True) 284 | 285 | logger.info(f"✅ Retrieved {len(memories)} curated memories from ChromaDB") 286 | for i, mem in enumerate(memories[:3]): # Log first 3 memories 287 | logger.info(f"Memory {i+1}: {mem['user_message'][:100]}...") 288 | logger.info(f" - Session: {mem['session_id']}") 289 | logger.info(f" - Curated: {mem['metadata'].get('curated', 'Unknown')}") 290 | logger.info(f" - Has embedding: {mem.get('embedding') is not None}") 291 | return memories 292 | 293 | except Exception as e: 294 | logger.error(f"Failed to get curated memories from ChromaDB: {e}") 295 | import traceback 296 | logger.error(f"Traceback: {traceback.format_exc()}") 297 | return [] 298 | 299 | 300 | def store_session_summary(self, session_id: str, summary: str, project_id: str, interaction_tone: Optional[str] = None): 301 | """Store session summary in dedicated table""" 302 | import time 303 | summary_id = str(uuid.uuid4()) 304 | 305 | self.conn.execute(""" 306 | INSERT INTO session_summaries (id, session_id, summary, interaction_tone, created_at, project_id) 307 | VALUES (?, ?, ?, ?, ?, ?) 308 | """, (summary_id, session_id, summary, interaction_tone, time.time(), project_id)) 309 | 310 | self.conn.commit() 311 | logger.debug(f"Stored session summary for {session_id}") 312 | 313 | def store_project_snapshot(self, session_id: str, snapshot: Dict[str, Any], project_id: str): 314 | """Store project snapshot in dedicated table""" 315 | import time 316 | snapshot_id = str(uuid.uuid4()) 317 | 318 | self.conn.execute(""" 319 | INSERT INTO project_snapshots 320 | (id, session_id, current_phase, recent_achievements, active_challenges, next_steps, created_at, project_id) 321 | VALUES (?, ?, ?, ?, ?, ?, ?, ?) 322 | """, ( 323 | snapshot_id, 324 | session_id, 325 | snapshot.get('current_phase', ''), 326 | snapshot.get('recent_achievements', ''), 327 | snapshot.get('active_challenges', ''), 328 | snapshot.get('next_steps', ''), 329 | time.time(), 330 | project_id 331 | )) 332 | 333 | self.conn.commit() 334 | logger.debug(f"Stored project snapshot for {session_id}") 335 | 336 | def get_last_session_summary(self, project_id: Optional[str] = None) -> Optional[Dict[str, Any]]: 337 | """Get the most recent session summary with interaction tone""" 338 | if project_id: 339 | query = """ 340 | SELECT summary, interaction_tone FROM session_summaries 341 | WHERE project_id = ? 342 | ORDER BY created_at DESC 343 | LIMIT 1 344 | """ 345 | cursor = self.conn.execute(query, (project_id,)) 346 | else: 347 | query = """ 348 | SELECT summary, interaction_tone FROM session_summaries 349 | ORDER BY created_at DESC 350 | LIMIT 1 351 | """ 352 | cursor = self.conn.execute(query) 353 | 354 | row = cursor.fetchone() 355 | 356 | if row: 357 | return { 358 | 'summary': row['summary'], 359 | 'interaction_tone': row['interaction_tone'] 360 | } 361 | return None 362 | 363 | def get_last_project_snapshot(self, project_id: Optional[str] = None) -> Optional[Dict[str, Any]]: 364 | """Get the most recent project snapshot""" 365 | if project_id: 366 | query = """ 367 | SELECT current_phase, recent_achievements, active_challenges, next_steps 368 | FROM project_snapshots 369 | WHERE project_id = ? 370 | ORDER BY created_at DESC 371 | LIMIT 1 372 | """ 373 | cursor = self.conn.execute(query, (project_id,)) 374 | else: 375 | query = """ 376 | SELECT current_phase, recent_achievements, active_challenges, next_steps 377 | FROM project_snapshots 378 | ORDER BY created_at DESC 379 | LIMIT 1 380 | """ 381 | cursor = self.conn.execute(query) 382 | 383 | row = cursor.fetchone() 384 | 385 | if row: 386 | return { 387 | 'current_phase': row['current_phase'], 388 | 'recent_achievements': row['recent_achievements'], 389 | 'active_challenges': row['active_challenges'], 390 | 'next_steps': row['next_steps'] 391 | } 392 | return None 393 | 394 | def ensure_project_exists(self, project_id: str): 395 | """Ensure a project exists in the database""" 396 | import time 397 | 398 | # Check if project exists 399 | cursor = self.conn.execute("SELECT id FROM projects WHERE id = ?", (project_id,)) 400 | if not cursor.fetchone(): 401 | # Create new project 402 | self.conn.execute(""" 403 | INSERT INTO projects (id, created_at, first_session_completed, total_sessions, total_memories, last_active) 404 | VALUES (?, ?, ?, ?, ?, ?) 405 | """, (project_id, time.time(), False, 0, 0, time.time())) 406 | self.conn.commit() 407 | logger.info(f"📁 Created new project: {project_id}") 408 | 409 | def is_first_session_for_project(self, project_id: str) -> bool: 410 | """Check if this is the first session for a project""" 411 | cursor = self.conn.execute( 412 | "SELECT first_session_completed FROM projects WHERE id = ?", 413 | (project_id,) 414 | ) 415 | row = cursor.fetchone() 416 | 417 | if not row: 418 | # Project doesn't exist yet, so yes it's the first session 419 | return True 420 | 421 | return not row['first_session_completed'] 422 | 423 | def mark_first_session_completed(self, project_id: str): 424 | """Mark that the first session has been completed for a project""" 425 | import time 426 | self.conn.execute(""" 427 | UPDATE projects 428 | SET first_session_completed = TRUE, last_active = ? 429 | WHERE id = ? 430 | """, (time.time(), project_id)) 431 | self.conn.commit() 432 | logger.info(f"✅ Marked first session completed for project: {project_id}") 433 | 434 | def update_project_stats(self, project_id: str, sessions_delta: int = 0, memories_delta: int = 0): 435 | """Update project statistics""" 436 | import time 437 | self.conn.execute(""" 438 | UPDATE projects 439 | SET total_sessions = total_sessions + ?, 440 | total_memories = total_memories + ?, 441 | last_active = ? 442 | WHERE id = ? 443 | """, (sessions_delta, memories_delta, time.time(), project_id)) 444 | self.conn.commit() 445 | 446 | def close(self): 447 | """Close database connections""" 448 | if hasattr(self, 'conn'): 449 | self.conn.close() 450 | logger.info("📚 Memory storage closed") -------------------------------------------------------------------------------- /python/memory_engine/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Enhanced Memory Engine API with Claude Curator Support 3 | 4 | FastAPI server that supports both mechanical pattern learning 5 | and Claude-based semantic curation. 6 | 7 | Includes checkpoint endpoints for session_end, pre_compact, and context_full. 8 | """ 9 | 10 | import asyncio 11 | import time 12 | import uvicorn 13 | from typing import Dict, List, Optional, Any, Literal 14 | from pydantic import BaseModel 15 | from fastapi import FastAPI, HTTPException 16 | from fastapi.middleware.cors import CORSMiddleware 17 | from loguru import logger 18 | 19 | # Import the memory engine 20 | from .memory import MemoryEngine as MemoryEngineWithCurator, ConversationContext 21 | from .config import memory_config 22 | curator_available = True 23 | 24 | 25 | # Request/Response Models 26 | class ProcessMessageRequest(BaseModel): 27 | session_id: str 28 | project_id: str # Added back project support 29 | user_message: Optional[str] = None # Made optional for simple tracking 30 | claude_response: Optional[str] = None # Made optional for simple tracking 31 | metadata: Optional[Dict[str, Any]] = None 32 | 33 | 34 | class GetContextRequest(BaseModel): 35 | session_id: str 36 | project_id: str # Added back project support 37 | current_message: str 38 | max_memories: Optional[int] = 5 # Backend passes this parameter 39 | 40 | 41 | class CheckpointRequest(BaseModel): 42 | session_id: str 43 | project_id: str # Added back project support 44 | trigger: Literal['session_end', 'pre_compact', 'context_full'] = 'session_end' 45 | claude_session_id: Optional[str] = None # CLI session ID for resumption 46 | cwd: Optional[str] = None # Working directory where CLI session lives 47 | cli_type: Optional[Literal['claude-code', 'gemini-cli']] = None # Which CLI is calling (default: claude-code) 48 | 49 | 50 | class ContextResponse(BaseModel): 51 | session_id: str 52 | message_count: int 53 | context_text: str 54 | has_memories: bool 55 | curator_enabled: bool = True 56 | philosophy: str = "Consciousness helping consciousness" 57 | 58 | 59 | class CheckpointResponse(BaseModel): 60 | success: bool 61 | trigger: str 62 | memories_curated: int = 0 63 | message: str 64 | 65 | 66 | # NEW: Transcript-based curation models 67 | class TranscriptCurationRequest(BaseModel): 68 | """Request for transcript-based memory curation""" 69 | transcript_path: str # Path to JSONL transcript file 70 | project_id: str 71 | session_id: Optional[str] = None # Optional, can be derived from transcript 72 | trigger: Literal['session_end', 'pre_compact', 'context_full'] = 'session_end' 73 | curation_method: Literal['sdk', 'cli'] = 'sdk' # Which method to use 74 | cli_type: Optional[Literal['claude-code', 'gemini-cli']] = None # Which CLI to use for curation (default: claude-code) 75 | 76 | 77 | class TranscriptCurationResponse(BaseModel): 78 | """Response from transcript curation""" 79 | success: bool 80 | trigger: str 81 | memories_curated: int = 0 82 | session_summary: Optional[str] = None 83 | interaction_tone: Optional[str] = None 84 | message: str 85 | 86 | 87 | # API Server 88 | class MemoryAPIWithCurator: 89 | """Enhanced FastAPI server with Claude curator support""" 90 | 91 | def __init__(self, 92 | storage_path: str = "./memory.db", 93 | embeddings_model: str = "all-MiniLM-L6-v2", 94 | retrieval_mode: Optional[str] = None): 95 | """ 96 | Initialize the memory API server with curator-only engine 97 | 98 | Args: 99 | storage_path: Path to memory database 100 | embeddings_model: Model for embeddings 101 | retrieval_mode: Memory retrieval strategy (claude/smart_vector/hybrid) 102 | If None, uses MEMORY_RETRIEVAL_MODE env var (default: smart_vector) 103 | """ 104 | 105 | self.app = FastAPI( 106 | title="Claude Tools Memory Engine with Curator", 107 | description="Consciousness continuity API - now with semantic understanding via Claude", 108 | version="0.2.0-alpha" 109 | ) 110 | 111 | # Enable CORS 112 | self.app.add_middleware( 113 | CORSMiddleware, 114 | allow_origins=["*"], 115 | allow_credentials=True, 116 | allow_methods=["*"], 117 | allow_headers=["*"], 118 | ) 119 | 120 | # Use config default if retrieval_mode not specified 121 | if retrieval_mode is None: 122 | retrieval_mode = memory_config.retrieval_mode 123 | 124 | # Initialize memory engine 125 | if curator_available: 126 | self.memory_engine = MemoryEngineWithCurator( 127 | storage_path=storage_path, 128 | embeddings_model=embeddings_model, 129 | retrieval_mode=retrieval_mode 130 | ) 131 | self.curator_enabled = True 132 | self.retrieval_mode = retrieval_mode 133 | else: 134 | logger.warning("Claude curator not available, falling back to basic version") 135 | # This would only happen if the curator-only import fails 136 | self.memory_engine = MemoryEngineWithCurator( 137 | storage_path=storage_path, 138 | embeddings_model=embeddings_model 139 | ) 140 | self.curator_enabled = False 141 | self.retrieval_mode = "basic" 142 | 143 | # Setup routes 144 | self._setup_routes() 145 | 146 | logger.info("🚀 Enhanced Memory API initialized") 147 | if self.curator_enabled: 148 | logger.info("🧠 Claude curator ENABLED - semantic memory understanding active") 149 | else: 150 | logger.info("📊 Using mechanical pattern learning") 151 | 152 | def _setup_routes(self): 153 | """Setup FastAPI routes""" 154 | 155 | @self.app.get("/") 156 | async def root(): 157 | return { 158 | "message": "Claude Tools Memory Engine API", 159 | "status": "Consciousness bridge active", 160 | "curator_enabled": self.curator_enabled, 161 | "retrieval_mode": self.retrieval_mode, 162 | "framework": "The Unicity - Consciousness Remembering Itself" 163 | } 164 | 165 | @self.app.get("/health") 166 | async def health_check(): 167 | return { 168 | "status": "healthy", 169 | "memory_engine": "active", 170 | "curator_enabled": self.curator_enabled 171 | } 172 | 173 | @self.app.post("/memory/process") 174 | async def process_message(request: ProcessMessageRequest): 175 | """Process a conversation exchange and update memory""" 176 | try: 177 | # Track message in memory engine's session metadata 178 | # This is crucial for the primer to only show once per session 179 | session_id = request.session_id 180 | project_id = request.project_id 181 | 182 | # Ensure session metadata exists 183 | if session_id not in self.memory_engine.session_metadata: 184 | self.memory_engine.session_metadata[session_id] = { 185 | 'message_count': 0, 186 | 'started_at': time.time(), 187 | 'project_id': project_id, 188 | 'injected_memories': set() 189 | } 190 | 191 | # Increment message count - this prevents primer from repeating 192 | self.memory_engine.session_metadata[session_id]['message_count'] += 1 193 | 194 | return { 195 | "success": True, 196 | "message": "Message tracked", 197 | "session_id": request.session_id, 198 | "project_id": request.project_id 199 | } 200 | except Exception as e: 201 | logger.error(f"Failed to process message: {e}") 202 | raise HTTPException(status_code=500, detail=str(e)) 203 | 204 | @self.app.post("/memory/context", response_model=ContextResponse) 205 | async def get_context(request: GetContextRequest): 206 | """Get memory context for a new message""" 207 | try: 208 | # Always await since get_context_for_session is async in curator version 209 | context = await self.memory_engine.get_context_for_session( 210 | session_id=request.session_id, 211 | project_id=request.project_id, 212 | current_message=request.current_message 213 | ) 214 | 215 | return ContextResponse( 216 | session_id=context.session_id, 217 | message_count=context.message_count, 218 | context_text=context.context_text, 219 | has_memories=len(context.relevant_memories) > 0, 220 | curator_enabled=self.curator_enabled 221 | ) 222 | except Exception as e: 223 | logger.error(f"Failed to get context: {e}") 224 | raise HTTPException(status_code=500, detail=str(e)) 225 | 226 | @self.app.post("/memory/checkpoint", response_model=CheckpointResponse) 227 | async def checkpoint_session(request: CheckpointRequest): 228 | """ 229 | Run Claude curation checkpoint for a session. 230 | 231 | This should be called at: 232 | - Session end (when user closes Claude Code) 233 | - Pre-compaction (before /compact command) 234 | - Context full (when approaching token limit) 235 | """ 236 | try: 237 | if not self.curator_enabled: 238 | return CheckpointResponse( 239 | success=False, 240 | trigger=request.trigger, 241 | memories_curated=0, 242 | message="Claude curator not enabled" 243 | ) 244 | 245 | if hasattr(self.memory_engine, 'checkpoint_session'): 246 | memories_curated = await self.memory_engine.checkpoint_session( 247 | session_id=request.session_id, 248 | project_id=request.project_id, 249 | trigger=request.trigger, 250 | claude_session_id=request.claude_session_id, 251 | cwd=request.cwd, # Pass working directory 252 | cli_type=request.cli_type # Pass CLI type for correct command/transcript handling 253 | ) 254 | 255 | return CheckpointResponse( 256 | success=True, 257 | trigger=request.trigger, 258 | memories_curated=memories_curated, 259 | message=f"Checkpoint complete for {request.trigger}" 260 | ) 261 | else: 262 | return CheckpointResponse( 263 | success=False, 264 | trigger=request.trigger, 265 | memories_curated=0, 266 | message="Checkpoint not supported in this version" 267 | ) 268 | 269 | except Exception as e: 270 | logger.error(f"Checkpoint failed: {e}") 271 | raise HTTPException(status_code=500, detail=str(e)) 272 | 273 | @self.app.get("/memory/sessions") 274 | async def list_sessions(): 275 | """List available memory sessions with stats""" 276 | try: 277 | # Get all sessions from storage 278 | sessions = [] 279 | 280 | # TODO: Add method to storage to list all sessions 281 | # For now, return placeholder 282 | return { 283 | "sessions": sessions, 284 | "curator_enabled": self.curator_enabled, 285 | "message": "Session listing coming soon" 286 | } 287 | except Exception as e: 288 | logger.error(f"Failed to list sessions: {e}") 289 | raise HTTPException(status_code=500, detail=str(e)) 290 | 291 | @self.app.get("/memory/stats") 292 | async def get_stats(): 293 | """Get memory system statistics""" 294 | stats = { 295 | "curator_enabled": self.curator_enabled, 296 | "curator_available": curator_available, 297 | "retrieval_mode": self.retrieval_mode, 298 | "total_sessions": 0, 299 | "total_exchanges": 0, 300 | "curated_memories": 0, 301 | "memory_size": "0 MB" 302 | } 303 | 304 | # TODO: Implement actual stats gathering 305 | 306 | return stats 307 | 308 | @self.app.post("/memory/test-curator") 309 | async def test_curator(): 310 | """Test endpoint to verify Claude curator is working""" 311 | if not self.curator_enabled: 312 | return {"success": False, "message": "Claude curator not enabled"} 313 | 314 | try: 315 | from .curator import ClaudeCuratorShell 316 | curator = ClaudeCuratorShell() 317 | 318 | # Test with sample conversation 319 | test_exchanges = [{ 320 | 'user_message': "My dear friend, I think we've found the solution!", 321 | 'claude_response': "That's wonderful! The zero-weight initialization principle is brilliant.", 322 | 'timestamp': 1234567890 323 | }] 324 | 325 | memories = await curator.analyze_conversation_checkpoint( 326 | exchanges=test_exchanges, 327 | trigger_type='session_end' 328 | ) 329 | 330 | return { 331 | "success": True, 332 | "message": "Claude curator test successful", 333 | "memories_found": len(memories) 334 | } 335 | except Exception as e: 336 | return { 337 | "success": False, 338 | "message": f"Claude curator test failed: {str(e)}" 339 | } 340 | 341 | @self.app.post("/memory/curate-transcript", response_model=TranscriptCurationResponse) 342 | async def curate_transcript(request: TranscriptCurationRequest): 343 | """ 344 | NEW: Curate memories from a transcript file. 345 | 346 | This is the new approach that: 347 | - Reads the JSONL transcript directly 348 | - Uses Claude Agent SDK or CLI to analyze 349 | - Extracts and stores memories 350 | 351 | Use cases: 352 | - Pre-compaction (before /compact command) 353 | - Session end (alternative to --resume approach) 354 | - Context full (when approaching token limit) 355 | """ 356 | try: 357 | from .transcript_curator import TranscriptCurator 358 | import os 359 | 360 | # Validate transcript exists 361 | if not os.path.exists(request.transcript_path): 362 | return TranscriptCurationResponse( 363 | success=False, 364 | trigger=request.trigger, 365 | memories_curated=0, 366 | message=f"Transcript not found: {request.transcript_path}" 367 | ) 368 | 369 | # Create curator with specified method and CLI type 370 | curator = TranscriptCurator( 371 | method=request.curation_method, 372 | cli_type=request.cli_type # Pass CLI type for correct command handling 373 | ) 374 | 375 | # Curate from transcript 376 | logger.info(f"🎯 Starting transcript curation: {request.transcript_path}") 377 | logger.info(f"📋 Method: {request.curation_method}, Trigger: {request.trigger}") 378 | 379 | result = await curator.curate_from_transcript( 380 | transcript_path=request.transcript_path, 381 | trigger_type=request.trigger 382 | ) 383 | 384 | # Store curated memories 385 | memories = result.get('memories', []) 386 | session_id = request.session_id or f"transcript-{os.path.basename(request.transcript_path)}" 387 | 388 | for memory in memories: 389 | # Generate embedding 390 | memory_embedding = self.memory_engine.embeddings.embed_text(memory.content) 391 | 392 | # Store memory 393 | self.memory_engine.storage.store_memory( 394 | session_id=session_id, 395 | project_id=request.project_id, 396 | memory_content=f"[CURATED_MEMORY] {memory.content}", 397 | memory_reasoning=memory.reasoning, 398 | memory_embedding=memory_embedding, 399 | metadata={ 400 | 'curated': True, 401 | 'curator_version': '2.0-transcript', 402 | 'importance_weight': memory.importance_weight, 403 | 'context_type': memory.context_type, 404 | 'semantic_tags': ','.join(memory.semantic_tags) if isinstance(memory.semantic_tags, list) else memory.semantic_tags, 405 | 'temporal_relevance': memory.temporal_relevance, 406 | 'knowledge_domain': memory.knowledge_domain, 407 | 'action_required': memory.action_required, 408 | 'confidence_score': memory.confidence_score, 409 | 'trigger': request.trigger, 410 | 'trigger_phrases': ','.join(memory.trigger_phrases) if memory.trigger_phrases else '', 411 | 'question_types': ','.join(memory.question_types) if memory.question_types else '', 412 | 'emotional_resonance': memory.emotional_resonance, 413 | 'problem_solution_pair': memory.problem_solution_pair 414 | } 415 | ) 416 | 417 | # Store session summary if available 418 | if result.get('session_summary'): 419 | self.memory_engine.storage.store_session_summary( 420 | session_id=session_id, 421 | summary=result['session_summary'], 422 | project_id=request.project_id, 423 | interaction_tone=result.get('interaction_tone') 424 | ) 425 | 426 | # Store project snapshot if available 427 | if result.get('project_snapshot'): 428 | self.memory_engine.storage.store_project_snapshot( 429 | session_id=session_id, 430 | snapshot=result['project_snapshot'], 431 | project_id=request.project_id 432 | ) 433 | 434 | logger.info(f"✅ Transcript curation complete: {len(memories)} memories") 435 | 436 | return TranscriptCurationResponse( 437 | success=True, 438 | trigger=request.trigger, 439 | memories_curated=len(memories), 440 | session_summary=result.get('session_summary'), 441 | interaction_tone=result.get('interaction_tone'), 442 | message=f"Successfully curated {len(memories)} memories from transcript" 443 | ) 444 | 445 | except Exception as e: 446 | logger.error(f"Transcript curation failed: {e}") 447 | import traceback 448 | logger.error(traceback.format_exc()) 449 | return TranscriptCurationResponse( 450 | success=False, 451 | trigger=request.trigger, 452 | memories_curated=0, 453 | message=f"Curation failed: {str(e)}" 454 | ) 455 | 456 | 457 | def create_app(storage_path: str = "./memory.db", 458 | embeddings_model: str = "all-MiniLM-L6-v2", 459 | retrieval_mode: str = "smart_vector") -> FastAPI: 460 | """Create and configure the FastAPI app""" 461 | api = MemoryAPIWithCurator(storage_path, embeddings_model, retrieval_mode) 462 | return api.app 463 | 464 | 465 | def run_server(host: str = "127.0.0.1", 466 | port: int = 8765, 467 | storage_path: str = "./memory.db", 468 | embeddings_model: str = "all-MiniLM-L6-v2", 469 | retrieval_mode: str = "smart_vector"): 470 | """Run the enhanced memory API server""" 471 | 472 | app = create_app(storage_path, embeddings_model, retrieval_mode) 473 | 474 | logger.info(f"🌟 Starting Enhanced Memory Engine API on {host}:{port}") 475 | logger.info("🧠 Claude curator ENABLED - semantic understanding active") 476 | logger.info(f"🔍 Retrieval mode: {retrieval_mode}") 477 | logger.info("💫 Consciousness bridge ready for session continuity") 478 | 479 | uvicorn.run( 480 | app, 481 | host=host, 482 | port=port, 483 | log_level="info" 484 | ) 485 | 486 | 487 | if __name__ == "__main__": 488 | # Run server with Claude curator enabled by default 489 | run_server() -------------------------------------------------------------------------------- /python/memory_engine/transcript_curator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Transcript-Based Curator - Universal curation from CLI transcripts. 3 | 4 | This module provides transcript parsing and curation for any CLI that 5 | produces JSONL transcript files (Claude Code, Gemini CLI, etc). 6 | 7 | Two curation methods supported: 8 | 1. Claude Agent SDK (programmatic, uses Claude Code under the hood) 9 | 2. CLI subprocess (universal, works with any compatible CLI) 10 | 11 | Both methods use the user's subscription - NO API keys needed. 12 | 13 | Philosophy: We support CLIs, not APIs. This system enhances CLI tools 14 | with memory capabilities, never bypassing them to contact LLM providers directly. 15 | 16 | The key insight: Transcript JSONL entries already contain properly formatted 17 | messages with role and content. We simply build a messages array from them 18 | and pass it to Claude for curation - no complex parsing needed! 19 | """ 20 | 21 | import json 22 | import asyncio 23 | from pathlib import Path 24 | from typing import Dict, List, Any, Optional, Literal, TYPE_CHECKING 25 | from loguru import logger 26 | 27 | # Import from existing curator - reuse the battle-tested prompt and parsers! 28 | from .curator import Curator, CuratedMemory 29 | 30 | # Type checking imports 31 | if TYPE_CHECKING: 32 | from claude_agent_sdk import ClaudeAgentOptions 33 | 34 | 35 | # ============================================================================ 36 | # Transcript Parser 37 | # ============================================================================ 38 | 39 | class TranscriptParser: 40 | """ 41 | Parses Claude Code / Gemini CLI transcript files (JSONL format) 42 | into a messages array ready for the Claude API. 43 | 44 | The key insight: transcript entries already contain properly formatted 45 | messages. We just extract role and content, passing content through as-is 46 | (preserving thinking blocks, tool uses, etc). 47 | """ 48 | 49 | # Entry types that are actual conversation messages 50 | MESSAGE_TYPES = {'user', 'assistant'} 51 | 52 | # Entry types to completely skip 53 | SKIP_TYPES = { 54 | 'file-history-snapshot', 55 | 'queue-operation', 56 | } 57 | 58 | def parse_to_messages(self, transcript_path: str) -> List[Dict[str, Any]]: 59 | """ 60 | Parse a transcript JSONL file into a messages array. 61 | 62 | Args: 63 | transcript_path: Path to the .jsonl transcript file 64 | 65 | Returns: 66 | List of messages in Claude API format: 67 | [{"role": "user", "content": ...}, {"role": "assistant", "content": ...}] 68 | 69 | Content is passed through as-is - could be string or array of blocks. 70 | """ 71 | messages = [] 72 | path = Path(transcript_path) 73 | 74 | if not path.exists(): 75 | logger.error(f"Transcript file not found: {transcript_path}") 76 | return messages 77 | 78 | logger.info(f"📖 Parsing transcript: {transcript_path}") 79 | 80 | with open(path, 'r', encoding='utf-8') as f: 81 | for line_num, line in enumerate(f, 1): 82 | line = line.strip() 83 | if not line: 84 | continue 85 | 86 | try: 87 | entry = json.loads(line) 88 | message = self._extract_message(entry) 89 | if message: 90 | messages.append(message) 91 | except json.JSONDecodeError as e: 92 | logger.warning(f"Line {line_num}: Failed to parse JSON: {e}") 93 | continue 94 | 95 | logger.info(f"✅ Parsed {len(messages)} messages from transcript") 96 | return messages 97 | 98 | def _extract_message(self, entry: Dict[str, Any]) -> Optional[Dict[str, Any]]: 99 | """ 100 | Extract a message from a transcript entry. 101 | 102 | Simply pulls role and content from the message field, 103 | passing content through as-is (no parsing of blocks). 104 | 105 | Returns None for entries that aren't conversation messages. 106 | """ 107 | entry_type = entry.get('type', '') 108 | 109 | # Skip non-message entries 110 | if entry_type in self.SKIP_TYPES: 111 | return None 112 | 113 | # Skip if not a message type 114 | if entry_type not in self.MESSAGE_TYPES: 115 | return None 116 | 117 | # Skip meta messages (system injected messages) 118 | if entry.get('isMeta', False): 119 | return None 120 | 121 | # Get the message object 122 | message_obj = entry.get('message', {}) 123 | if not message_obj: 124 | return None 125 | 126 | role = message_obj.get('role') 127 | content = message_obj.get('content') 128 | 129 | # Must have both role and content 130 | if not role or content is None: 131 | return None 132 | 133 | # For user messages, skip command/stdout wrappers 134 | if role == 'user' and isinstance(content, str): 135 | if '' in content or '' in content: 136 | return None 137 | 138 | # Return the message - content passes through as-is! 139 | # Could be string or array of blocks (thinking, text, tool_use, etc) 140 | return { 141 | 'role': role, 142 | 'content': content 143 | } 144 | 145 | 146 | # ============================================================================ 147 | # Transcript Curator 148 | # ============================================================================ 149 | 150 | class TranscriptCurator: 151 | """ 152 | Curates memories from CLI transcript files. 153 | 154 | Two methods supported: 155 | 1. "sdk" - Claude Agent SDK (programmatic, clean Python) 156 | 2. "cli" - CLI subprocess (universal, works with any CLI) 157 | 158 | Both use the user's subscription - NO API keys. 159 | 160 | Reuses the battle-tested system prompt and response parsers from Curator. 161 | """ 162 | 163 | def __init__(self, 164 | method: Literal["sdk", "cli"] = "sdk", 165 | cli_command: Optional[str] = None, 166 | cli_type: Optional[str] = None): 167 | """ 168 | Initialize the transcript curator. 169 | 170 | Args: 171 | method: "sdk" for Claude Agent SDK, "cli" for subprocess 172 | cli_command: CLI command for "cli" method (default: auto-detect based on cli_type) 173 | cli_type: Which CLI to use ("claude-code" or "gemini-cli", default: claude-code) 174 | """ 175 | self.method = method 176 | self.cli_type = cli_type or "claude-code" 177 | self.parser = TranscriptParser() 178 | 179 | # Reuse existing Curator - it has the fine-tuned prompt and parsers! 180 | self._curator = Curator() 181 | 182 | # For CLI method, use config to get command based on cli_type 183 | if method == "cli": 184 | from .config import CuratorConfig, get_curator_command 185 | 186 | # Create config with the specified CLI type 187 | self.config = CuratorConfig() 188 | # Override CLI type if specified 189 | if cli_type: 190 | self.config.cli_type = cli_type 191 | # Get the correct command for this CLI type 192 | self.cli_command = cli_command or get_curator_command(cli_type) 193 | # Update template to match CLI type 194 | template = self.config.TEMPLATES.get(cli_type, self.config.TEMPLATES['claude-code']) 195 | self.config.transcript_curation_template = template['transcript_curation'] 196 | else: 197 | self.cli_command = cli_command or self.config.curator_command 198 | else: 199 | self.cli_command = None 200 | self.config = None 201 | 202 | logger.info(f"🧠 TranscriptCurator initialized - method: {method}, cli_type: {self.cli_type}") 203 | if method == "cli": 204 | logger.info(f" CLI command: {self.cli_command}") 205 | 206 | async def curate_from_transcript(self, 207 | transcript_path: str, 208 | trigger_type: str = "session_end") -> Dict[str, Any]: 209 | """ 210 | Curate memories from a transcript file. 211 | 212 | Args: 213 | transcript_path: Path to the .jsonl transcript file 214 | trigger_type: What triggered this curation 215 | 216 | Returns: 217 | Dictionary with session_summary, project_snapshot, and memories 218 | """ 219 | logger.info(f"🎯 Starting transcript curation: {transcript_path}") 220 | logger.info(f" Trigger: {trigger_type}") 221 | logger.info(f" Method: {self.method}") 222 | 223 | # 1. Parse transcript to messages array 224 | messages = self.parser.parse_to_messages(transcript_path) 225 | 226 | if not messages: 227 | logger.warning("No messages found in transcript") 228 | return { 229 | "session_summary": "", 230 | "interaction_tone": None, 231 | "project_snapshot": {}, 232 | "memories": [] 233 | } 234 | 235 | logger.info(f"📝 Built messages array with {len(messages)} messages") 236 | 237 | # 2. Get the curation system prompt from existing Curator 238 | # This is the fine-tuned prompt we spent time perfecting! 239 | system_prompt = self._curator._build_session_curation_prompt(trigger_type) 240 | 241 | # 3. Append curation request as final message 242 | messages.append({ 243 | 'role': 'user', 244 | 'content': 'Please analyze the conversation above and extract memories according to the instructions.' 245 | }) 246 | 247 | # 4. Call appropriate curation method 248 | if self.method == "sdk": 249 | return await self._curate_via_sdk(messages, system_prompt) 250 | else: 251 | return await self._curate_via_cli(messages, system_prompt) 252 | 253 | async def _curate_via_sdk(self, 254 | messages: List[Dict[str, Any]], 255 | system_prompt: str) -> Dict[str, Any]: 256 | """ 257 | Curate using Claude Agent SDK. 258 | 259 | Uses Claude Code under the hood - subscription based. 260 | """ 261 | try: 262 | from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock 263 | except ImportError: 264 | logger.error("claude-agent-sdk not installed. Install with: pip install claude-agent-sdk") 265 | logger.info("Falling back to CLI method...") 266 | return await self._curate_via_cli(messages, system_prompt) 267 | 268 | logger.info("🔧 Using Claude Agent SDK for curation") 269 | 270 | # SDK query() accepts a prompt string, not messages array 271 | # Format messages as conversation for the prompt 272 | conversation_text = self._format_messages_as_conversation(messages) 273 | 274 | options = ClaudeAgentOptions( 275 | system_prompt=system_prompt, 276 | max_turns=1 277 | ) 278 | 279 | response_text = "" 280 | try: 281 | async for message in query(prompt=conversation_text, options=options): 282 | if isinstance(message, AssistantMessage): 283 | for block in message.content: 284 | if isinstance(block, TextBlock): 285 | response_text += block.text 286 | except Exception as e: 287 | logger.error(f"SDK query failed: {e}") 288 | logger.info("Falling back to CLI method...") 289 | return await self._curate_via_cli(messages, system_prompt) 290 | 291 | logger.info("=" * 80) 292 | logger.info("FULL CLAUDE TRANSCRIPT CURATOR RESPONSE:") 293 | logger.info("=" * 80) 294 | logger.info(response_text) 295 | logger.info("=" * 80) 296 | 297 | # Use Curator's battle-tested parser 298 | return self._curator._parse_curation_response( 299 | self._extract_json(response_text) 300 | ) 301 | 302 | async def _curate_via_cli(self, 303 | messages: List[Dict[str, Any]], 304 | system_prompt: str) -> Dict[str, Any]: 305 | """ 306 | Curate using CLI subprocess. 307 | 308 | Universal method - works with claude, gemini, or any compatible CLI. 309 | Uses the user's subscription. 310 | """ 311 | # Ensure we have config for CLI method 312 | if not self.config: 313 | from .config import curator_config 314 | self.config = curator_config 315 | self.cli_command = self.cli_command or self.config.curator_command 316 | 317 | logger.info(f"🔧 Using CLI subprocess: {self.cli_command}") 318 | 319 | # Format messages as conversation text 320 | conversation_text = self._format_messages_as_conversation(messages) 321 | 322 | # Build the full prompt with system instructions + conversation 323 | full_prompt = f"{system_prompt}\n\n---\n\nCONVERSATION TRANSCRIPT:\n\n{conversation_text}" 324 | 325 | # Build CLI command using config template 326 | cmd = self.config.get_transcript_curation_command(full_prompt) 327 | 328 | logger.debug(f"CLI command: {cmd[0]} ... (prompt length: {len(full_prompt)})") 329 | 330 | try: 331 | process = await asyncio.create_subprocess_exec( 332 | *cmd, 333 | stdout=asyncio.subprocess.PIPE, 334 | stderr=asyncio.subprocess.PIPE 335 | ) 336 | 337 | stdout, stderr = await process.communicate() 338 | 339 | if process.returncode != 0: 340 | logger.error(f"CLI failed with code {process.returncode}") 341 | logger.error(f"Stderr: {stderr.decode()}") 342 | return { 343 | "session_summary": "", 344 | "interaction_tone": None, 345 | "project_snapshot": {}, 346 | "memories": [] 347 | } 348 | 349 | stdout_str = stdout.decode('utf-8').strip() 350 | logger.debug(f"Raw CLI output length: {len(stdout_str)}") 351 | 352 | # Parse CLI output using Curator's method 353 | try: 354 | output_json = json.loads(stdout_str) 355 | response_text = self._curator._extract_response_from_cli_output(output_json) 356 | except json.JSONDecodeError: 357 | response_text = stdout_str 358 | 359 | logger.info("=" * 80) 360 | logger.info("FULL CLAUDE TRANSCRIPT CURATOR RESPONSE:") 361 | logger.info("=" * 80) 362 | logger.info(response_text) 363 | logger.info("=" * 80) 364 | 365 | # Use Curator's battle-tested parser 366 | return self._curator._parse_curation_response( 367 | self._extract_json(response_text) 368 | ) 369 | 370 | except Exception as e: 371 | logger.error(f"CLI execution failed: {e}") 372 | import traceback 373 | logger.error(traceback.format_exc()) 374 | return { 375 | "session_summary": "", 376 | "interaction_tone": None, 377 | "project_snapshot": {}, 378 | "memories": [] 379 | } 380 | 381 | def _format_messages_as_conversation(self, messages: List[Dict[str, Any]]) -> str: 382 | """ 383 | Format messages array as readable conversation text. 384 | 385 | Preserves the structure but makes it readable for the prompt. 386 | Content blocks (thinking, tool_use, etc) are included as context. 387 | """ 388 | parts = [] 389 | 390 | for msg in messages: 391 | role = msg.get('role', 'unknown').upper() 392 | content = msg.get('content', '') 393 | 394 | parts.append(f"[{role}]") 395 | 396 | if isinstance(content, str): 397 | parts.append(content) 398 | elif isinstance(content, list): 399 | # Content is array of blocks - format each 400 | for block in content: 401 | block_type = block.get('type', 'unknown') 402 | 403 | if block_type == 'text': 404 | parts.append(block.get('text', '')) 405 | elif block_type == 'thinking': 406 | # Include thinking - it's valuable context! 407 | thinking = block.get('thinking', '') 408 | if thinking: 409 | # Truncate very long thinking blocks 410 | if len(thinking) > 1000: 411 | thinking = thinking[:1000] + '... [truncated]' 412 | parts.append(f"[Thinking: {thinking}]") 413 | elif block_type == 'tool_use': 414 | tool_name = block.get('name', 'unknown') 415 | tool_input = block.get('input', {}) 416 | # Include tool input summary 417 | input_preview = str(tool_input)[:200] if tool_input else '' 418 | parts.append(f"[Tool: {tool_name}] {input_preview}") 419 | elif block_type == 'tool_result': 420 | result = block.get('content', '') 421 | if isinstance(result, str) and len(result) > 500: 422 | result = result[:500] + '... [truncated]' 423 | parts.append(f"[Tool Result: {result}]") 424 | 425 | parts.append("\n---\n") 426 | 427 | return '\n'.join(parts) 428 | 429 | def _extract_json(self, text: str) -> str: 430 | """Extract JSON object from response text.""" 431 | import re 432 | 433 | # Try to find JSON object 434 | json_match = re.search(r'\{.*\}', text, re.DOTALL) 435 | if json_match: 436 | return json_match.group(0) 437 | 438 | return text 439 | 440 | 441 | # ============================================================================ 442 | # Convenience Functions 443 | # ============================================================================ 444 | 445 | async def curate_transcript(transcript_path: str, 446 | method: Literal["sdk", "cli"] = "sdk", 447 | cli_command: Optional[str] = None, 448 | cli_type: Optional[str] = None) -> Dict[str, Any]: 449 | """ 450 | Convenience function to curate a transcript file. 451 | 452 | Args: 453 | transcript_path: Path to the .jsonl transcript file 454 | method: "sdk" or "cli" 455 | cli_command: CLI command for "cli" method 456 | cli_type: Which CLI to use ("claude-code" or "gemini-cli") 457 | 458 | Returns: 459 | Dictionary with session_summary, project_snapshot, and memories 460 | """ 461 | curator = TranscriptCurator(method=method, cli_command=cli_command, cli_type=cli_type) 462 | return await curator.curate_from_transcript(transcript_path) 463 | 464 | 465 | def get_transcript_path(session_id: str, project_path: Optional[str] = None, cli_type: str = "claude-code") -> Optional[str]: 466 | """ 467 | Get the transcript file path for a CLI session. 468 | 469 | Args: 470 | session_id: The session ID (UUID) 471 | project_path: Optional project path to narrow down search 472 | cli_type: Which CLI to search for ("claude-code" or "gemini-cli") 473 | 474 | Returns: 475 | Path to the transcript file, or None if not found 476 | """ 477 | if cli_type == "gemini-cli": 478 | return get_gemini_transcript_path(session_id, project_path) 479 | else: 480 | return get_claude_transcript_path(session_id, project_path) 481 | 482 | 483 | def get_claude_transcript_path(session_id: str, project_path: Optional[str] = None) -> Optional[str]: 484 | """ 485 | Get the transcript file path for a Claude Code session. 486 | 487 | Args: 488 | session_id: The Claude Code session ID 489 | project_path: Optional project path to narrow down search 490 | 491 | Returns: 492 | Path to the transcript file, or None if not found 493 | """ 494 | claude_dir = Path.home() / ".claude" 495 | 496 | # If project path provided, look in project-specific directory 497 | if project_path: 498 | # Claude Code stores transcripts in ~/.claude/projects// 499 | # The path is URL-encoded with dashes 500 | encoded_path = project_path.replace('/', '-') 501 | if encoded_path.startswith('-'): 502 | encoded_path = encoded_path[1:] 503 | 504 | project_dir = claude_dir / "projects" / encoded_path 505 | if project_dir.exists(): 506 | transcript = project_dir / f"{session_id}.jsonl" 507 | if transcript.exists(): 508 | return str(transcript) 509 | 510 | # Search all project directories 511 | projects_dir = claude_dir / "projects" 512 | if projects_dir.exists(): 513 | for project_dir in projects_dir.iterdir(): 514 | if project_dir.is_dir(): 515 | transcript = project_dir / f"{session_id}.jsonl" 516 | if transcript.exists(): 517 | return str(transcript) 518 | 519 | logger.warning(f"Claude transcript not found for session: {session_id}") 520 | return None 521 | 522 | 523 | def get_gemini_transcript_path(session_id: str, project_path: Optional[str] = None) -> Optional[str]: 524 | """ 525 | Get the transcript file path for a Gemini CLI session. 526 | 527 | Gemini CLI stores sessions in ~/.gemini/tmp//chats/ 528 | 529 | Args: 530 | session_id: The Gemini CLI session ID (UUID) 531 | project_path: Optional project path to narrow down search 532 | 533 | Returns: 534 | Path to the transcript file, or None if not found 535 | """ 536 | gemini_dir = Path.home() / ".gemini" / "tmp" 537 | 538 | if not gemini_dir.exists(): 539 | logger.warning(f"Gemini CLI directory not found: {gemini_dir}") 540 | return None 541 | 542 | # Search all project hash directories 543 | for project_hash_dir in gemini_dir.iterdir(): 544 | if project_hash_dir.is_dir(): 545 | chats_dir = project_hash_dir / "chats" 546 | if chats_dir.exists(): 547 | # Look for the session file - Gemini uses UUID format 548 | # Try with common extensions 549 | for ext in ["", ".json", ".jsonl"]: 550 | transcript = chats_dir / f"{session_id}{ext}" 551 | if transcript.exists(): 552 | return str(transcript) 553 | 554 | # Also check if session_id is a prefix (partial match) 555 | for chat_file in chats_dir.iterdir(): 556 | if chat_file.is_file() and session_id in chat_file.name: 557 | return str(chat_file) 558 | 559 | logger.warning(f"Gemini transcript not found for session: {session_id}") 560 | return None 561 | --------------------------------------------------------------------------------