# EverMem Plugin for Claude Code Persistent memory for Claude Code. Automatically saves and recalls context from past coding sessions. > Compatibility note: this folder documents a legacy EverMem Cloud > plugin. It still uses the old cloud `/api/v1/memories/*` routes and > should not be treated as the canonical local EverOS 1.0.0 OSS API. > New integrations should follow > [EverOS 1.0.0 migration notes](../../docs/migration-to-1.0.0.md) and > [the API reference](../../docs/api.md). ![Memory Hub Screenshot](https://github.com/user-attachments/assets/af37c1f6-7ba5-430c-b99d-2a7e7eac618f) ## Features - **Automatic Memory Save** - Conversations are saved when Claude finishes responding - **Automatic Memory Retrieval** - Relevant memories are retrieved when you submit a prompt - **Session Context** - Recent work summary loaded on session start - **Memory Search** - Manually search your memory history - **Memory Hub** - Visual dashboard to explore and manage memories ## Quick Install ```bash curl -fsSL https://raw.githubusercontent.com/EverMind-AI/evermem-claude-code/main/install.sh | bash ``` This will: 1. Prompt for your EverMem API key 2. Save it to your shell profile 3. Install the plugin via Claude Code's plugin system **Get your API key:** [console.evermind.ai](https://console.evermind.ai/) ## Manual Installation ### 1. Get Your API Key Visit [console.evermind.ai](https://console.evermind.ai/) to create an account and get your API key. ### 2. Configure Environment Variable Add to your shell profile (`~/.zshrc` or `~/.bashrc`): ```bash export EVERMEM_API_KEY="your-api-key-here" ``` Reload your shell: ```bash source ~/.zshrc # or source ~/.bashrc ``` ### 3. Install the Plugin ```bash # Add marketplace from GitHub (tracks updates automatically) claude plugin marketplace add https://github.com/EverMind-AI/evermem-claude-code # Install the plugin claude plugin install evermem@evermem --scope user ``` To update the plugin later: ```bash claude plugin marketplace update evermem claude plugin update evermem@evermem ``` ### 4. Verify Installation Run `/evermem:help` to check if the plugin is configured correctly. ## Usage ### Commands | Command | Description | |---------|-------------| | `/evermem:help` | Show setup status and available commands | | `/evermem:search ` | Search your memories for specific topics | | `/evermem:ask ` | Ask about past work (combines memory + context) | | `/evermem:hub` | Open the Memory Hub dashboard | | `/evermem:debug` | View debug logs for troubleshooting | | `/evermem:projects` | View your Claude Code projects table | ### Automatic Behavior The plugin works automatically in the background: **On Session Start:** ``` 💡 EverMem: Last session (2h ago): "Implementing JWT authentication..." | 3 memories ``` Recent memories and last session summary are loaded to provide context. **On Prompt Submit:** ``` You: "How should I handle authentication?" ↓ 📝 Memory Retrieved (2): • [0.85] (2 days ago) Discussion about JWT token implementation • [0.72] (1 week ago) Auth middleware setup decisions ↓ Claude receives the relevant context and responds accordingly ``` **On Response Complete:** ``` 💾 EverMem: Memory saved (4 messages) ``` ### Memory Hub The Memory Hub provides a visual interface to explore your memories: - Activity heatmap (GitHub-style, 6 months) - Memory statistics (Total, Projects, Active Days, Avg/Day, Avg/Project) - Last 7 Days growth chart - Project-based memory grouping with expandable cards - Timeline view within each project (grouped by date) - Load more pagination for large projects To use the hub, run `/evermem:hub` and follow the instructions. ## Configuration ### Environment Variables | Variable | Description | Required | |----------|-------------|----------| | `EVERMEM_API_KEY` | Your EverMem API key | Yes | ### Project-Specific Settings Create `.claude/evermem.local.md` in your project root for per-project configuration: ```markdown --- group_id: "my-project" --- Project-specific notes here. ``` ## Troubleshooting ### API Key Not Configured ```bash # Check if the key is set echo $EVERMEM_API_KEY # If empty, add to your shell profile and reload export EVERMEM_API_KEY="your-key-here" source ~/.zshrc ``` ### No Memories Found 1. Memories are only recalled after you've had previous conversations 2. Short prompts (less than 3 words) are skipped 3. Check that your API key is valid at [console.evermind.ai](https://console.evermind.ai/) ### API Errors - **403 Forbidden**: Invalid or expired API key - **502 Bad Gateway**: Server temporarily unavailable, try again ### Debug Mode Enable debug logging to troubleshoot issues: ```bash # Set environment variable export EVERMEM_DEBUG=1 # View logs in real-time tail -f /tmp/evermem-debug.log # Clear logs > /tmp/evermem-debug.log ``` Run `/evermem:debug` to view recent debug logs directly. ## Links - **Console**: [console.evermind.ai](https://console.evermind.ai/) - **API Documentation**: [docs.evermind.ai](https://docs.evermind.ai) - **Issues**: [GitHub Issues](https://github.com/EverMind-AI/evermem-claude-code/issues) ## License MIT --- # Technical Details The following sections explain how EverMem works internally. This is useful for developers who want to understand the implementation or contribute to the project. ## How It Works ``` ┌─────────────────────────────────────────────────────────────┐ │ Session Start │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ SessionStart Hook │ │ • Fetches recent memories from EverMem Cloud │ │ • Loads last session summary from local storage │ │ • Injects session context into Claude's prompt │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Your Prompt │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ UserPromptSubmit Hook │ │ • Searches EverMem Cloud for relevant memories │ │ • Displays memory summary to user │ │ • Injects context into Claude's prompt │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Claude Response │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Stop Hook │ │ • Extracts conversation from transcript │ │ • Sends to EverMem Cloud for storage │ │ • Server generates summary and stores memory │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Session End │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ SessionEnd Hook │ │ • Parses transcript to extract first user prompt │ │ • Saves session summary to local storage │ │ • No AI calls - pure local data extraction │ └─────────────────────────────────────────────────────────────┘ ``` ## Claude Code Hooks Mechanism > Reference: [Claude Code Hooks Documentation](https://docs.anthropic.com/en/docs/claude-code/hooks) Claude Code provides a **hooks system** that allows plugins to execute custom scripts at specific lifecycle events. Hooks are **event-driven** - they don't run continuously but are triggered by Claude Code at specific moments. ### How Hooks Work ``` ┌─────────────────────────────────────────────────────────────────┐ │ Claude Code (Main Process) │ │ │ │ 1. Event occurs (e.g., user sends message, Claude responds) │ │ 2. Claude Code reads hooks.json │ │ 3. Finds matching hooks for the event │ │ 4. Spawns child process: node │ │ 5. Sends JSON data via stdin pipe ─────────────┐ │ │ 6. Reads response from stdout │ │ └─────────────────────────────────────────────────│───────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Hook Script (Child Process) │ │ │ │ // Read JSON from stdin (sent by Claude Code) │ │ let input = ''; │ │ for await (const chunk of process.stdin) { │ │ input += chunk; │ │ } │ │ const hookInput = JSON.parse(input); │ │ │ │ // Process and return result via stdout │ │ console.log(JSON.stringify({ ... })); │ └─────────────────────────────────────────────────────────────────┘ ``` ### Hook Events | Event | Trigger | Use Case | |-------|---------|----------| | `SessionStart` | Claude Code starts | Load context, setup environment | | `UserPromptSubmit` | User sends a message | Validate prompt, inject context | | `PreToolUse` | Before tool execution | Approve/deny/modify tool calls | | `PostToolUse` | After tool execution | Validate results, run linters | | `Stop` | Claude finishes responding | Save conversation, cleanup | | `Notification` | System notification | Custom alerts | ### Plugin hooks.json Configuration ```json { "hooks": { "EventName": [ { "matcher": "*", // Pattern to match (for tool events) "hooks": [ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/my-hook.js", "timeout": 30 // Timeout in seconds } ] } ] } } ``` **Environment Variables:** - `${CLAUDE_PLUGIN_ROOT}` - Plugin directory path (for plugins) - `${CLAUDE_PROJECT_DIR}` - Project root directory ### EverMem Plugin Hooks ```json { "hooks": { "SessionStart": [...], // Load session context + track groups locally "UserPromptSubmit": [...], // Search & inject memories "Stop": [...], // Save conversation to cloud "SessionEnd": [...] // Save session summary locally } } ``` ## SessionStart Hook The SessionStart hook runs when Claude Code starts a new session. It loads recent memories from the cloud and last session summary from local storage. ### Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Claude Code Session Start │ │ │ │ 1. Claude Code spawns: session-context-wrapper.sh │ │ 2. Wrapper checks npm dependencies │ │ 3. Wrapper executes: node session-context.js │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ session-context.js │ │ │ │ 1. Read hook input from stdin (contains cwd) │ │ 2. Save group to local storage (groups.jsonl) │ │ 3. Fetch recent memories from EverMem API (limit: 100) │ │ 4. Take the 5 most recent memories │ │ 5. Get last session summary from sessions.jsonl │ │ 6. Output systemMessage + systemPrompt via stdout │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Claude Code Receives │ │ │ │ • systemMessage: "💡 EverMem: Last session (2h ago): \"...\" | 5 memories"│ │ • systemPrompt: ... │ │ │ │ The systemPrompt is injected into Claude's context window │ └─────────────────────────────────────────────────────────────────┘ ``` ### Hook Input (stdin) ```json { "session_id": "", "cwd": "/path/to/your/project", "permission_mode": "default", "hook_event_name": "SessionStart" } ``` ### Hook Output (stdout) ```json { "continue": true, "systemMessage": "💡 EverMem: Last session (2h ago): \"Implementing JWT authentication...\" | 5 memories", "systemPrompt": "\nLast session (2h ago, 5 turns): Implementing JWT authentication for the API\n\nRecent memories (5):\n\n[1] (2/9/2026) JWT token implementation\n...\n" } ``` ### Output Fields | Field | Description | |-------|-------------| | `continue` | Always `true` - never block session start | | `systemMessage` | Displayed to user in terminal | | `systemPrompt` | Injected into Claude's context (invisible to user) | ### Data Sources The hook combines two data sources: 1. **Cloud Memories** - Recent memories from EverMem API (5 most recent) 2. **Local Session Summary** - Last session from `data/sessions.jsonl` (saved by SessionEnd hook) No AI summarization is used - pure local data extraction for zero latency and no additional API costs. ### Error Handling | Error Type | User Message | |------------|--------------| | Network error | "Cannot reach EverMem server. Check your internet connection." | | Timeout | "EverMem server is slow or unreachable." | | 401/Unauthorized | "Authentication failed. Check your EVERMEM_API_KEY." | | 404 | "API endpoint not found. Check EVERMEM_BASE_URL." | | Module not found | "Missing dependency. Run: npm install" | All errors return `continue: true` to ensure session starts normally. ### Node.js Version Check The hook requires Node.js 18+ for ES modules support. If an older version is detected: ```json { "continue": true, "systemMessage": "⚠️ EverMem: Node.js 16.x is too old. Please upgrade to Node.js 18+." } ``` ## SessionEnd Hook The SessionEnd hook runs when a Claude Code session ends. It saves a session summary to local storage for use by the SessionStart hook. ### Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Claude Code Session End │ │ │ │ Triggers: /exit, closing terminal, idle timeout │ │ Claude Code spawns: node session-summary.js │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ session-summary.js │ │ │ │ 1. Read hook input from stdin (contains transcript_path) │ │ 2. Check if session already summarized (skip if yes) │ │ 3. Parse transcript JSONL file │ │ 4. Extract: first user prompt, turn count, timestamps │ │ 5. Save to data/sessions.jsonl │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Local Storage │ │ │ │ data/sessions.jsonl: │ │ {"sessionId":"abc","groupId":"...","summary":"First user │ │ prompt truncated to 200 chars","turnCount":5,...} │ └─────────────────────────────────────────────────────────────────┘ ``` ### Hook Input (stdin) ```json { "session_id": "", "transcript_path": "~/.claude/projects//.jsonl", "cwd": "/path/to/your/project", "reason": "user_exit", "hook_event_name": "SessionEnd" } ``` ### Hook Output (stdout) ```json { "systemMessage": "📝 Session saved (5 turns): Implementing JWT authentication for the..." } ``` ### Session Summary Format Each session is saved as a single line in `data/sessions.jsonl`: ```json { "sessionId": "", "groupId": "claude-code:/path/to/project", "summary": "First user prompt truncated to 200 characters", "turnCount": 5, "reason": "user_exit", "startTime": "2026-02-09T10:00:00.000Z", "endTime": "2026-02-09T10:30:00.000Z", "timestamp": "2026-02-09T10:30:05.000Z" } ``` ### Fields | Field | Description | |-------|-------------| | `sessionId` | Unique session identifier (from Claude Code) | | `groupId` | Project identifier (based on working directory) | | `summary` | First user prompt (truncated to 200 chars) | | `turnCount` | Number of conversation turns | | `reason` | Why session ended (user_exit, idle_timeout, etc.) | | `startTime` | First message timestamp | | `endTime` | Last message timestamp | | `timestamp` | When summary was saved | ### Deduplication Each session is only saved once. Before saving, the hook checks if the sessionId already exists in `sessions.jsonl`. ### No AI Summarization The SessionEnd hook uses a simple approach: the first user prompt becomes the session summary. This provides: - **Zero latency** - No API calls needed - **Zero cost** - No Haiku or other model usage - **Reliability** - Works offline, no external dependencies The first user prompt typically describes what the user wanted to accomplish, making it a natural summary of the session's purpose. ### Design Philosophy: Deferred Display Pattern The SessionEnd and SessionStart hooks work together using a **"save now, display later"** pattern: ``` ┌─────────────────────────────────────────────────────────────────┐ │ Session A (ending) │ │ │ │ SessionEnd Hook: │ │ • Extracts first user prompt, turn count, duration │ │ • Saves to sessions.jsonl │ │ • Output NOT displayed (session already closed) │ └─────────────────────────────────────────────────────────────────┘ │ │ sessions.jsonl (local storage) │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Session B (starting) │ │ │ │ SessionStart Hook: │ │ • Reads last session from sessions.jsonl │ │ • Displays: "Last (2h ago, 5 turns): Your question..." │ │ • Provides continuity across sessions │ └─────────────────────────────────────────────────────────────────┘ ``` **Why this design?** 1. **SessionEnd can't display messages** - When a session ends (`/exit`, `Ctrl+D`), the terminal is closing. Any `systemMessage` output would be lost or not visible to the user. 2. **SessionStart is the right moment** - The next time the user opens Claude Code, they see what they were working on. This creates a natural "welcome back" experience. 3. **Local-first architecture** - Session summaries are stored locally in `sessions.jsonl`, not in the cloud. This ensures: - Instant access (no API latency) - Works offline - No additional API costs - Privacy (session data stays on your machine) 4. **Graceful degradation** - If SessionEnd fails to run (e.g., `Ctrl+C` force quit), the next SessionStart still works with cloud memories. No single point of failure. **Data Flow Summary:** | Event | Action | Storage | Display | |-------|--------|---------|---------| | SessionEnd | Save summary | Local (sessions.jsonl) | None | | SessionStart | Read summary | Local + Cloud | Yes | ## Local Groups Tracking The SessionStart hook automatically records project groups to `data/groups.jsonl` (JSONL format): ```jsonl {"keyId":"9a823d2f8ea5","groupId":"claude-code:/path/to/project-a","name":"project-a","path":"/path/to/project-a","timestamp":"2026-02-09T06:00:00Z"} {"keyId":"9a823d2f8ea5","groupId":"claude-code:/path/to/api-server","name":"api-server","path":"/path/to/api-server","timestamp":"2026-02-09T08:00:00Z"} ``` **Fields:** - `keyId`: SHA-256 hash (first 12 chars) of the API key - associates groups with accounts - `groupId`: Unique identifier based on working directory, format: `claude-code:{path}` - `name`: Project folder name - `path`: Full path to the project - `timestamp`: When the group was first recorded **Deduplication:** Each `keyId + groupId` combination is stored only once (no duplicates). View tracked projects with `/evermem:projects` command. ## Stop Hook: Conversation Flow Claude Code stores all conversations locally in JSONL (JSON Lines) format. The EverMem plugin reads this transcript and uploads the latest Q&A pair to the cloud. ### Hook Input When Claude finishes responding, the Stop hook receives input like this: ```json { "session_id": "", "transcript_path": "~/.claude/projects//.jsonl", "cwd": "/path/to/your/project", "permission_mode": "default", "hook_event_name": "Stop", "stop_hook_active": false } ``` ### Transcript File Format The transcript file (`*.jsonl`) contains one JSON object per line, recording every message and event in the session. **Important:** A single Claude response may span multiple lines with different content types. **Common Fields:** | Field | Description | |-------|-------------| | `type` | Line type: `user`, `assistant`, `progress`, `system`, `file-history-snapshot` | | `uuid` | Unique message ID | | `parentUuid` | Parent message ID (for threading) | | `timestamp` | ISO 8601 timestamp | | `sessionId` | Session UUID | | `message.role` | `user` or `assistant` | | `message.content` | String or array of content blocks | **Content Block Types (in `message.content` array):** | Type | Description | |------|-------------| | `text` | Final text response to user | | `thinking` | Claude's internal reasoning (extended thinking) | | `tool_use` | Tool invocation (Read, Write, Bash, etc.) | | `tool_result` | Result returned from tool execution | **Complete Conversation Example:** A single Q&A turn generates multiple JSONL lines: ```jsonl // 1. User message {"type":"user","message":{"role":"user","content":"debug.js 如何使用"},"uuid":"696034a3-...","timestamp":"2026-02-09T02:20:16.540Z"} // 2. Assistant thinking (extended thinking mode) {"type":"assistant","message":{"role":"assistant","content":[{"type":"thinking","thinking":"用户希望了解 debug.js 的使用方法...","signature":"EuAC..."}]},"uuid":"b375ff09-...","timestamp":"2026-02-09T02:20:26.866Z"} // 3. Assistant tool use (e.g., Read file) {"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_01Qur8BnkKD9t53JSSorDLbm","name":"Read","input":{"file_path":"/path/to/README.md"}}]},"uuid":"f01ec15c-..."} // 4. Progress event (hook execution) {"type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read"},"uuid":"f4219b83-..."} // 5. Tool result (returned as user message) {"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Qur8BnkKD9t53JSSorDLbm","type":"tool_result","content":"file contents here..."}]},"uuid":"f5c5f7c6-..."} // 6. Assistant final text response {"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"完成!README 已更新..."}]},"uuid":"cae1b79c-..."} // 7. System events (stop hook, timing) {"type":"system","subtype":"stop_hook_summary","hookCount":1,"hasOutput":true,"uuid":"25a25edf-..."} {"type":"system","subtype":"turn_duration","durationMs":81371,"uuid":"55418b2c-..."} ``` **Simplified View:** ``` User Input ↓ [thinking] → [tool_use] → [tool_result] → [tool_use] → ... → [text] ↓ System Events (hooks, timing) ``` ### Turn Boundary & Segmentation **Session Level:** One JSONL file = One Session (filename is session ID) **Turn Level:** A "Turn" = User sends message → Claude fully responds **Turn boundary marker (ONLY this one):** ```json {"type":"system","subtype":"turn_duration","durationMs":30692} ``` > **Note:** `file-history-snapshot` is NOT a turn boundary. It's a session-level marker that can appear anywhere in the file. **JSONL Structure:** ``` Line 1: file-history-snapshot ← Session marker (NOT turn boundary) Line 2-21: Turn 1 Line 22: turn_duration ← Turn 1 end ✓ Line 23: file-history-snapshot ← Can appear mid-session (NOT turn boundary) Line 24-43: Turn 2 Line 44: turn_duration ← Turn 2 end ✓ ... ``` **Message Chain (parentUuid):** ``` user (uuid: aaa, parent: None) ← Turn start ↓ assistant/thinking (parent: aaa) ↓ assistant/tool_use (parent: ...) ↓ user/tool_result (parent: ...) ← NOT user input, skip! ↓ assistant/text (parent: ...) ← Final response ↓ system/turn_duration (parent: ...) ← Turn end ``` ### Memory Extraction The `store-memories.js` hook extracts the **last complete Turn**: 1. **Wait for completion** - Retry reading file until `turn_duration` marker appears (indicates turn is complete) 2. **Find turn boundaries** - Start after last `turn_duration`, end at current `turn_duration` - **ONLY** `turn_duration` is used as boundary (NOT `file-history-snapshot`) 3. **Collect user text** - Original input only (skip `tool_result`) 4. **Collect assistant text** - All `text` blocks (skip `thinking`, `tool_use`) 5. **Merge content** - Join scattered text blocks with `\n\n` separator 6. **Upload to cloud** - Send both user and assistant content to EverMem API **Race Condition Handling:** The Stop hook runs before `turn_duration` is written. To ensure complete content extraction: ```javascript // Retry until turn_duration appears (max 5 attempts, 100ms delay) async function readTranscriptWithRetry(path) { for (let attempt = 1; attempt <= 5; attempt++) { const lines = readFile(path); const lastLine = JSON.parse(lines[lines.length - 1]); // turn_duration = turn complete if (lastLine.type === 'system' && lastLine.subtype === 'turn_duration') { return lines; } await sleep(100); // Wait and retry } } ``` **Why merge?** A single Claude response spans multiple JSONL lines: - `thinking` → `tool_use` → `tool_result` → ... → `text` (final response) The hook merges all `text` blocks to capture the complete response. ### API Upload Each message is sent to `POST /api/v1/memories/group` (or `POST /api/v1/memories` for personal memories): ```json { "group_id": "claude-code:/path/to/project", "messages": [ { "sender_id": "claude-code-user", "role": "user", "timestamp": 1770367656189, "content": "How do I add authentication?" } ], "async_mode": true } ``` Response on success: ```json { "message": "Message accepted and queued for processing", "request_id": "", "status": "queued" } ``` ### Hook Output (stdout) The hook returns JSON via stdout to communicate with Claude Code: ```json { "systemMessage": "💾 Memory saved (2) [user: 59, assistant: 127]" } ``` This message is displayed to the user after Claude finishes responding. ## Memory Hub Implementation The `/evermem:hub` command opens a web dashboard for visualizing memories. Due to browser limitations (GET requests can't have body), a local proxy server bridges the dashboard and EverMem API. ### Architecture ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ /evermem:hub Command │ │ 1. Start proxy server: node server/proxy.js & │ │ 2. Generate URL: http://localhost:3456/?key=${EVERMEM_API_KEY} │ │ 3. User opens URL in browser │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ Browser (dashboard.html) │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Stats │ │ Heatmap │ │ 7-Day │ │ Project │ │ │ │ Cards │ │ (6 months) │ │ Chart │ │ Cards │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ Data Flow: │ │ 1. GET /api/groups → Local groups.jsonl (filtered by keyId) │ │ 2. For each group: POST /api/v1/memories/get → Fetch memories │ │ 3. Render dashboard with aggregated data │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ Proxy Server (localhost:3456) │ │ │ │ Routes: │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ GET / → Serve dashboard.html │ │ │ │ GET /api/groups → Read groups.jsonl, filter by keyId │ │ │ │ POST /api/v1/memories/search → Forward to EverMind API │ │ │ │ POST /api/v1/memories/get → Forward to EverMind API │ │ │ │ GET /health → Health check │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ Why Proxy? │ │ - The proxy forwards browser calls to the EverMind API and serves the │ │ dashboard. │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ EverMem Cloud API │ │ https://api.evermind.ai │ │ │ │ POST /api/v1/memories/search { query, filters, method, memory_types, top_k } │ │ POST /api/v1/memories/get { memory_type, filters, page, page_size, ... } │ │ Response: { data: { episodes[], total_count, count } } │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### Proxy Server (`server/proxy.js`) ```javascript // Key function: Convert API key to keyId (for groups filtering) function computeKeyId(apiKey) { const hash = createHash('sha256').update(apiKey).digest('hex'); return hash.substring(0, 12); // First 12 chars of SHA-256 } // Key function: Read groups.jsonl and filter by keyId function getGroupsForKey(keyId) { const content = readFileSync(GROUPS_FILE, 'utf8'); const lines = content.trim().split('\n'); const groupMap = new Map(); for (const line of lines) { const entry = JSON.parse(line); if (entry.keyId !== keyId) continue; // Filter by current API key // Aggregate: count sessions, track first/last seen // ... } return Array.from(groupMap.values()); } // Key route: Forward POST requests to EverMind API // Browser sends: POST /api/v1/memories/search { query, filters, ... } // Browser sends: POST /api/v1/memories/get { memory_type, filters, ... } // Proxy forwards to upstream API with Authorization header ``` ### Dashboard (`dashboard/dashboard.html`) **Data Loading Flow:** ```javascript async function loadGroups() { // 1. Fetch groups from local storage (via proxy) const groupsData = await fetch('/api/groups', { headers: { 'Authorization': `Bearer ${apiKey}` } }); // 2. For each group, fetch memories with pagination for (const group of groups) { const data = await fetch('/api/v1/memories/get', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ memory_type: 'episodic_memory', filters: { group_id: group.id }, page: 1, page_size: 100, rank_by: 'timestamp', rank_order: 'desc' }) }); // Store: memories[], totalCount, hasMore, offset groupMemories[group.id] = { ... }; } // 3. Render dashboard renderDashboard(totalMemories); } ``` **UI Components:** | Component | Description | |-----------|-------------| | Stats Grid | 5 cards: Total Memories, Projects, Active Days, Avg/Day, Avg/Project | | Heatmap | GitHub-style 6-month activity grid with tooltips | | Growth Chart | Last 7 days bar chart | | Project Cards | Expandable cards showing memories per project | | Timeline | Within each project, memories grouped by date | | Load More | Pagination button when `has_more: true` | **Timeline within Project:** ``` 📁 evermem-claude-code (25 memories) ├── ● Sun, Feb 9 [Today] 3 memories │ ├── 💭 Discussion about JWT... 10:30 AM │ ├── 🔧 Fixed authentication... 09:15 AM │ └── ✨ Created new API endpoint 08:00 AM │ ├── ● Sat, Feb 8 5 memories │ ├── 📝 Updated README... 16:20 PM │ └── ... │ └── [Load more (17 remaining)] ``` ## Debug Logging Both `inject-memories.js` and `store-memories.js` use a shared debug utility: ```javascript import { debug, setDebugPrefix } from './utils/debug.js'; setDebugPrefix('inject'); // Log lines will show [inject] prefix debug('hookInput:', data); // Only writes when EVERMEM_DEBUG=1 ``` **Debug output by script:** | Script | Prefix | Debug Points | |--------|--------|--------------| | `inject-memories.js` | `[inject]` | hookInput, search query, search results, filtered/selected memories, output | | `store-memories.js` | `[store]` | hookInput, read attempts, turn range, line types, extracted content, results | **Example debug log:** ```log # Memory injection (UserPromptSubmit hook) [2026-02-06T08:47:30.100Z] [inject] hookInput: { "prompt": "How do I add auth?", ... } [2026-02-06T08:47:30.150Z] [inject] searching memories for prompt: How do I add auth? [2026-02-06T08:47:30.500Z] [inject] search results: {"total": 5, "memories": [...]} [2026-02-06T08:47:30.520Z] [inject] selected memories: [{"score": 0.85, "subject": "JWT implementation"}] # Memory storage (Stop hook) [2026-02-06T08:47:36.184Z] [store] hookInput: { "transcript_path": "...jsonl", ... } # Retry logic - waiting for turn_duration [2026-02-06T08:47:36.200Z] [store] read attempt 1: { "totalLines": 525, "isComplete": false, "lastLineType": "progress" } [2026-02-06T08:47:36.201Z] [store] turn not complete, waiting 100ms before retry... [2026-02-06T08:47:36.310Z] [store] read attempt 2: { "totalLines": 527, "isComplete": false, "lastLineType": "system/stop_hook_summary" } [2026-02-06T08:47:36.311Z] [store] turn not complete, waiting 100ms before retry... [2026-02-06T08:47:36.420Z] [store] read attempt 3: { "totalLines": 528, "isComplete": true, "lastLineType": "system/turn_duration" } # Content extraction [2026-02-06T08:47:36.425Z] [store] turn range: { "turnStartIndex": 500, "turnEndIndex": 528, "totalLines": 528 } [2026-02-06T08:47:36.430Z] [store] assistantTexts count: 3 [2026-02-06T08:47:36.435Z] [store] extracted: { "userLength": 59, "assistantLength": 847, ... } # API upload results [2026-02-06T08:47:36.970Z] [store] results: [ { "type": "USER", "len": 59, "status": 202, "ok": true, "response": { "message": "Message accepted and queued for processing", "status": "queued" } }, { "type": "ASSISTANT", "len": 127, "status": 202, "ok": true, "response": { ... } } ] [2026-02-06T08:47:36.975Z] [store] skipped: [] ``` **Using debug.js in your own hooks:** ```javascript import { debug, setDebugPrefix, isDebugEnabled } from './utils/debug.js'; // Set prefix to identify your script in logs setDebugPrefix('my-hook'); // Log objects (auto JSON stringified) or strings debug('processing:', { key: 'value' }); // Check if debug is enabled if (isDebugEnabled()) { // expensive debug operations } ``` ## Project Structure ``` evermem-plugin/ ├── plugin.json # Plugin manifest ├── commands/ │ ├── help.md # /evermem:help command │ ├── search.md # /evermem:search command │ ├── hub.md # /evermem:hub command │ ├── debug.md # /evermem:debug command │ └── projects.md # /evermem:projects command ├── data/ │ ├── groups.jsonl # Local storage for tracked projects (JSONL format) │ └── sessions.jsonl # Local storage for session summaries (JSONL format) ├── hooks/ │ ├── hooks.json # Hook configuration │ └── scripts/ │ ├── inject-memories.js # Memory recall (UserPromptSubmit) │ ├── store-memories.js # Memory save (Stop) │ ├── session-context.js # Session context (SessionStart) │ ├── session-summary.js # Session summary (SessionEnd) │ └── utils/ │ ├── evermem-api.js # EverMem Cloud API client │ ├── config.js # Configuration utilities │ ├── debug.js # Shared debug logging utility │ └── groups-store.js # Local groups persistence ├── dashboard/ │ └── dashboard.html # Memory Hub dashboard ├── server/ │ └── proxy.js # Local proxy server for dashboard └── README.md ``` ## API Reference The plugin uses the EverMem Cloud API at `https://api.evermind.ai`: - `POST /api/v1/memories/group` - Store a new memory (group); `POST /api/v1/memories` for personal memories - `POST /api/v1/memories/search` - Search memories (hybrid retrieval, with JSON body) - `POST /api/v1/memories/get` - Get memories (list with JSON body) ## Development ### Local Development ```bash # Clone the repository git clone https://github.com/EverMind-AI/evermem-claude-code.git cd evermem-claude-code # Install dependencies npm install # Run Claude Code with local plugin claude --plugin-dir . ``` ### Testing Hooks ```bash # Test memory recall echo '{"prompt":"How do I handle authentication?"}' | node hooks/scripts/inject-memories.js # Test memory save (requires transcript file) echo '{"transcript_path":"/path/to/transcript.json"}' | node hooks/scripts/store-memories.js ```