{"id":51492159,"url":"https://github.com/evermind-ai/evermem-claude-code","last_synced_at":"2026-07-07T12:02:12.110Z","repository":{"id":335288998,"uuid":"1144953129","full_name":"EverMind-AI/evermem-claude-code","owner":"EverMind-AI","description":null,"archived":false,"fork":false,"pushed_at":"2026-02-09T10:04:08.000Z","size":468,"stargazers_count":9,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-09T12:13:35.482Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/EverMind-AI.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-29T08:39:09.000Z","updated_at":"2026-02-09T08:43:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/EverMind-AI/evermem-claude-code","commit_stats":null,"previous_names":["evermind-ai/evermem-claude-code"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/EverMind-AI/evermem-claude-code","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EverMind-AI%2Fevermem-claude-code","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EverMind-AI%2Fevermem-claude-code/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EverMind-AI%2Fevermem-claude-code/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EverMind-AI%2Fevermem-claude-code/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/EverMind-AI","download_url":"https://codeload.github.com/EverMind-AI/evermem-claude-code/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EverMind-AI%2Fevermem-claude-code/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35226918,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-07T02:00:07.222Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-07T12:02:09.380Z","updated_at":"2026-07-07T12:02:12.097Z","avatar_url":"https://github.com/EverMind-AI.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EverMem Plugin for Claude Code\n\nPersistent memory for Claude Code. Automatically saves and recalls context from past coding sessions.\n\n![Memory Hub Screenshot](assets/hub-screenshot.png)\n\n## Features\n\n- **Automatic Memory Save** - Conversations are saved when Claude finishes responding\n- **Automatic Memory Retrieval** - Relevant memories are retrieved when you submit a prompt\n- **Session Context** - Recent work summary loaded on session start\n- **Memory Search** - Manually search your memory history\n- **Memory Hub** - Visual dashboard to explore and manage memories\n\n## Quick Install\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/EverMind-AI/evermem-claude-code/main/install.sh | bash\n```\n\nThis will:\n1. Prompt for your EverMem API key\n2. Save it to your shell profile\n3. Install the plugin via Claude Code's plugin system\n\n**Get your API key:** [console.evermind.ai](https://console.evermind.ai/)\n\n## Manual Installation\n\n### 1. Get Your API Key\n\nVisit [console.evermind.ai](https://console.evermind.ai/) to create an account and get your API key.\n\n### 2. Configure Environment Variable\n\nAdd to your shell profile (`~/.zshrc` or `~/.bashrc`):\n\n```bash\nexport EVERMEM_API_KEY=\"your-api-key-here\"\n```\n\nReload your shell:\n\n```bash\nsource ~/.zshrc  # or source ~/.bashrc\n```\n\n### 3. Install the Plugin\n\n```bash\n# Add marketplace from GitHub (tracks updates automatically)\nclaude plugin marketplace add https://github.com/EverMind-AI/evermem-claude-code\n\n# Install the plugin\nclaude plugin install evermem@evermem --scope user\n```\n\nTo update the plugin later:\n\n```bash\nclaude plugin marketplace update evermem\nclaude plugin update evermem@evermem\n```\n\n### 4. Verify Installation\n\nRun `/evermem:help` to check if the plugin is configured correctly.\n\n## Usage\n\n### Commands\n\n| Command | Description |\n|---------|-------------|\n| `/evermem:help` | Show setup status and available commands |\n| `/evermem:search \u003cquery\u003e` | Search your memories for specific topics |\n| `/evermem:ask \u003cquestion\u003e` | Ask about past work (combines memory + context) |\n| `/evermem:hub` | Open the Memory Hub dashboard |\n| `/evermem:debug` | View debug logs for troubleshooting |\n| `/evermem:projects` | View your Claude Code projects table |\n\n### Automatic Behavior\n\nThe plugin works automatically in the background:\n\n**On Session Start:**\n```\n💡 EverMem: Last session (2h ago): \"Implementing JWT authentication...\" | 3 memories\n```\nRecent memories and last session summary are loaded to provide context.\n\n**On Prompt Submit:**\n```\nYou: \"How should I handle authentication?\"\n         ↓\n📝 Memory Retrieved (2):\n  • [0.85] (2 days ago) Discussion about JWT token implementation\n  • [0.72] (1 week ago) Auth middleware setup decisions\n         ↓\nClaude receives the relevant context and responds accordingly\n```\n\n**On Response Complete:**\n```\n💾 EverMem: Memory saved (4 messages)\n```\n\n### Memory Hub\n\nThe Memory Hub provides a visual interface to explore your memories:\n\n- Activity heatmap (GitHub-style, 6 months)\n- Memory statistics (Total, Projects, Active Days, Avg/Day, Avg/Project)\n- Last 7 Days growth chart\n- Project-based memory grouping with expandable cards\n- Timeline view within each project (grouped by date)\n- Load more pagination for large projects\n\nTo use the hub, run `/evermem:hub` and follow the instructions.\n\n## Configuration\n\n### Environment Variables\n\n| Variable | Description | Required |\n|----------|-------------|----------|\n| `EVERMEM_API_KEY` | Your EverMem API key | Yes |\n\n### Project-Specific Settings\n\nCreate `.claude/evermem.local.md` in your project root for per-project configuration:\n\n```markdown\n---\ngroup_id: \"my-project\"\n---\n\nProject-specific notes here.\n```\n\n## Troubleshooting\n\n### API Key Not Configured\n\n```bash\n# Check if the key is set\necho $EVERMEM_API_KEY\n\n# If empty, add to your shell profile and reload\nexport EVERMEM_API_KEY=\"your-key-here\"\nsource ~/.zshrc\n```\n\n### No Memories Found\n\n1. Memories are only recalled after you've had previous conversations\n2. Short prompts (less than 3 words) are skipped\n3. Check that your API key is valid at [console.evermind.ai](https://console.evermind.ai/)\n\n### API Errors\n\n- **403 Forbidden**: Invalid or expired API key\n- **502 Bad Gateway**: Server temporarily unavailable, try again\n\n### Debug Mode\n\nEnable debug logging to troubleshoot issues:\n\n```bash\n# Set environment variable\nexport EVERMEM_DEBUG=1\n\n# View logs in real-time\ntail -f /tmp/evermem-debug.log\n\n# Clear logs\n\u003e /tmp/evermem-debug.log\n```\n\nRun `/evermem:debug` to view recent debug logs directly.\n\n## Links\n\n- **Console**: [console.evermind.ai](https://console.evermind.ai/)\n- **API Documentation**: [docs.evermind.ai](https://docs.evermind.ai)\n- **Issues**: [GitHub Issues](https://github.com/EverMind-AI/evermem-claude-code/issues)\n\n## License\n\nMIT\n\n---\n\n# Technical Details\n\nThe following sections explain how EverMem works internally. This is useful for developers who want to understand the implementation or contribute to the project.\n\n## How It Works\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│                     Session Start                            │\n└─────────────────────────────────────────────────────────────┘\n                            │\n                            ▼\n┌─────────────────────────────────────────────────────────────┐\n│  SessionStart Hook                                          │\n│  • Fetches recent memories from EverMem Cloud               │\n│  • Loads last session summary from local storage            │\n│  • Injects session context into Claude's prompt             │\n└─────────────────────────────────────────────────────────────┘\n                            │\n                            ▼\n┌─────────────────────────────────────────────────────────────┐\n│                     Your Prompt                              │\n└─────────────────────────────────────────────────────────────┘\n                            │\n                            ▼\n┌─────────────────────────────────────────────────────────────┐\n│  UserPromptSubmit Hook                                      │\n│  • Searches EverMem Cloud for relevant memories             │\n│  • Displays memory summary to user                          │\n│  • Injects context into Claude's prompt                     │\n└─────────────────────────────────────────────────────────────┘\n                            │\n                            ▼\n┌─────────────────────────────────────────────────────────────┐\n│                   Claude Response                            │\n└─────────────────────────────────────────────────────────────┘\n                            │\n                            ▼\n┌─────────────────────────────────────────────────────────────┐\n│  Stop Hook                                                  │\n│  • Extracts conversation from transcript                    │\n│  • Sends to EverMem Cloud for storage                       │\n│  • Server generates summary and stores memory               │\n└─────────────────────────────────────────────────────────────┘\n                            │\n                            ▼\n┌─────────────────────────────────────────────────────────────┐\n│                     Session End                              │\n└─────────────────────────────────────────────────────────────┘\n                            │\n                            ▼\n┌─────────────────────────────────────────────────────────────┐\n│  SessionEnd Hook                                            │\n│  • Parses transcript to extract first user prompt           │\n│  • Saves session summary to local storage                   │\n│  • No AI calls - pure local data extraction                 │\n└─────────────────────────────────────────────────────────────┘\n```\n\n## Claude Code Hooks Mechanism\n\n\u003e Reference: [Claude Code Hooks Documentation](https://docs.anthropic.com/en/docs/claude-code/hooks)\n\nClaude 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.\n\n### How Hooks Work\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│                    Claude Code (Main Process)                   │\n│                                                                 │\n│  1. Event occurs (e.g., user sends message, Claude responds)    │\n│  2. Claude Code reads hooks.json                                │\n│  3. Finds matching hooks for the event                          │\n│  4. Spawns child process: node \u003cscript.js\u003e                      │\n│  5. Sends JSON data via stdin pipe ─────────────┐               │\n│  6. Reads response from stdout                  │               │\n└─────────────────────────────────────────────────│───────────────┘\n                                                  │\n                                                  ▼\n┌─────────────────────────────────────────────────────────────────┐\n│                    Hook Script (Child Process)                  │\n│                                                                 │\n│  // Read JSON from stdin (sent by Claude Code)                  │\n│  let input = '';                                                │\n│  for await (const chunk of process.stdin) {                     │\n│    input += chunk;                                              │\n│  }                                                              │\n│  const hookInput = JSON.parse(input);                           │\n│                                                                 │\n│  // Process and return result via stdout                        │\n│  console.log(JSON.stringify({ ... }));                          │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n### Hook Events\n\n| Event | Trigger | Use Case |\n|-------|---------|----------|\n| `SessionStart` | Claude Code starts | Load context, setup environment |\n| `UserPromptSubmit` | User sends a message | Validate prompt, inject context |\n| `PreToolUse` | Before tool execution | Approve/deny/modify tool calls |\n| `PostToolUse` | After tool execution | Validate results, run linters |\n| `Stop` | Claude finishes responding | Save conversation, cleanup |\n| `Notification` | System notification | Custom alerts |\n\n### Plugin hooks.json Configuration\n\n```json\n{\n  \"hooks\": {\n    \"EventName\": [\n      {\n        \"matcher\": \"*\",           // Pattern to match (for tool events)\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"${CLAUDE_PLUGIN_ROOT}/scripts/my-hook.js\",\n            \"timeout\": 30         // Timeout in seconds\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\n**Environment Variables:**\n- `${CLAUDE_PLUGIN_ROOT}` - Plugin directory path (for plugins)\n- `${CLAUDE_PROJECT_DIR}` - Project root directory\n\n### EverMem Plugin Hooks\n\n```json\n{\n  \"hooks\": {\n    \"SessionStart\": [...],        // Load session context + track groups locally\n    \"UserPromptSubmit\": [...],    // Search \u0026 inject memories\n    \"Stop\": [...],                // Save conversation to cloud\n    \"SessionEnd\": [...]           // Save session summary locally\n  }\n}\n```\n\n## SessionStart Hook\n\nThe SessionStart hook runs when Claude Code starts a new session. It loads recent memories from the cloud and last session summary from local storage.\n\n### Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│                    Claude Code Session Start                     │\n│                                                                  │\n│  1. Claude Code spawns: session-context-wrapper.sh              │\n│  2. Wrapper checks npm dependencies                              │\n│  3. Wrapper executes: node session-context.js                   │\n└─────────────────────────────────────────────────────────────────┘\n                               │\n                               ▼\n┌─────────────────────────────────────────────────────────────────┐\n│                    session-context.js                            │\n│                                                                  │\n│  1. Read hook input from stdin (contains cwd)                   │\n│  2. Save group to local storage (groups.jsonl)                  │\n│  3. Fetch recent memories from EverMem API (limit: 100)         │\n│  4. Take the 5 most recent memories                             │\n│  5. Get last session summary from sessions.jsonl                │\n│  6. Output systemMessage + systemPrompt via stdout              │\n└─────────────────────────────────────────────────────────────────┘\n                               │\n                               ▼\n┌─────────────────────────────────────────────────────────────────┐\n│                    Claude Code Receives                          │\n│                                                                  │\n│  • systemMessage: \"💡 EverMem: Last session (2h ago): \\\"...\\\" | 5 memories\"│\n│  • systemPrompt: \u003csession-context\u003e...\u003c/session-context\u003e         │\n│                                                                  │\n│  The systemPrompt is injected into Claude's context window      │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n### Hook Input (stdin)\n\n```json\n{\n  \"session_id\": \"\u003csession-uuid\u003e\",\n  \"cwd\": \"/path/to/your/project\",\n  \"permission_mode\": \"default\",\n  \"hook_event_name\": \"SessionStart\"\n}\n```\n\n### Hook Output (stdout)\n\n```json\n{\n  \"continue\": true,\n  \"systemMessage\": \"💡 EverMem: Last session (2h ago): \\\"Implementing JWT authentication...\\\" | 5 memories\",\n  \"systemPrompt\": \"\u003csession-context\u003e\\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\u003c/session-context\u003e\"\n}\n```\n\n### Output Fields\n\n| Field | Description |\n|-------|-------------|\n| `continue` | Always `true` - never block session start |\n| `systemMessage` | Displayed to user in terminal |\n| `systemPrompt` | Injected into Claude's context (invisible to user) |\n\n### Data Sources\n\nThe hook combines two data sources:\n\n1. **Cloud Memories** - Recent memories from EverMem API (5 most recent)\n2. **Local Session Summary** - Last session from `data/sessions.jsonl` (saved by SessionEnd hook)\n\nNo AI summarization is used - pure local data extraction for zero latency and no additional API costs.\n\n### Error Handling\n\n| Error Type | User Message |\n|------------|--------------|\n| Network error | \"Cannot reach EverMem server. Check your internet connection.\" |\n| Timeout | \"EverMem server is slow or unreachable.\" |\n| 401/Unauthorized | \"Authentication failed. Check your EVERMEM_API_KEY.\" |\n| 404 | \"API endpoint not found. Check EVERMEM_BASE_URL.\" |\n| Module not found | \"Missing dependency. Run: npm install\" |\n\nAll errors return `continue: true` to ensure session starts normally.\n\n### Node.js Version Check\n\nThe hook requires Node.js 18+ for ES modules support. If an older version is detected:\n\n```json\n{\n  \"continue\": true,\n  \"systemMessage\": \"⚠️ EverMem: Node.js 16.x is too old. Please upgrade to Node.js 18+.\"\n}\n```\n\n## SessionEnd Hook\n\nThe SessionEnd hook runs when a Claude Code session ends. It saves a session summary to local storage for use by the SessionStart hook.\n\n### Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│                    Claude Code Session End                       │\n│                                                                  │\n│  Triggers: /exit, closing terminal, idle timeout                 │\n│  Claude Code spawns: node session-summary.js                    │\n└─────────────────────────────────────────────────────────────────┘\n                               │\n                               ▼\n┌─────────────────────────────────────────────────────────────────┐\n│                    session-summary.js                            │\n│                                                                  │\n│  1. Read hook input from stdin (contains transcript_path)       │\n│  2. Check if session already summarized (skip if yes)           │\n│  3. Parse transcript JSONL file                                 │\n│  4. Extract: first user prompt, turn count, timestamps          │\n│  5. Save to data/sessions.jsonl                                 │\n└─────────────────────────────────────────────────────────────────┘\n                               │\n                               ▼\n┌─────────────────────────────────────────────────────────────────┐\n│                    Local Storage                                 │\n│                                                                  │\n│  data/sessions.jsonl:                                           │\n│  {\"sessionId\":\"abc\",\"groupId\":\"...\",\"summary\":\"First user       │\n│   prompt truncated to 200 chars\",\"turnCount\":5,...}             │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n### Hook Input (stdin)\n\n```json\n{\n  \"session_id\": \"\u003csession-uuid\u003e\",\n  \"transcript_path\": \"~/.claude/projects/\u003chash\u003e/\u003csession-uuid\u003e.jsonl\",\n  \"cwd\": \"/path/to/your/project\",\n  \"reason\": \"user_exit\",\n  \"hook_event_name\": \"SessionEnd\"\n}\n```\n\n### Hook Output (stdout)\n\n```json\n{\n  \"systemMessage\": \"📝 Session saved (5 turns): Implementing JWT authentication for the...\"\n}\n```\n\n### Session Summary Format\n\nEach session is saved as a single line in `data/sessions.jsonl`:\n\n```json\n{\n  \"sessionId\": \"\u003csession-uuid\u003e\",\n  \"groupId\": \"claude-code:/path/to/project\",\n  \"summary\": \"First user prompt truncated to 200 characters\",\n  \"turnCount\": 5,\n  \"reason\": \"user_exit\",\n  \"startTime\": \"2026-02-09T10:00:00.000Z\",\n  \"endTime\": \"2026-02-09T10:30:00.000Z\",\n  \"timestamp\": \"2026-02-09T10:30:05.000Z\"\n}\n```\n\n### Fields\n\n| Field | Description |\n|-------|-------------|\n| `sessionId` | Unique session identifier (from Claude Code) |\n| `groupId` | Project identifier (based on working directory) |\n| `summary` | First user prompt (truncated to 200 chars) |\n| `turnCount` | Number of conversation turns |\n| `reason` | Why session ended (user_exit, idle_timeout, etc.) |\n| `startTime` | First message timestamp |\n| `endTime` | Last message timestamp |\n| `timestamp` | When summary was saved |\n\n### Deduplication\n\nEach session is only saved once. Before saving, the hook checks if the sessionId already exists in `sessions.jsonl`.\n\n### No AI Summarization\n\nThe SessionEnd hook uses a simple approach: the first user prompt becomes the session summary. This provides:\n\n- **Zero latency** - No API calls needed\n- **Zero cost** - No Haiku or other model usage\n- **Reliability** - Works offline, no external dependencies\n\nThe first user prompt typically describes what the user wanted to accomplish, making it a natural summary of the session's purpose.\n\n### Design Philosophy: Deferred Display Pattern\n\nThe SessionEnd and SessionStart hooks work together using a **\"save now, display later\"** pattern:\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│  Session A (ending)                                             │\n│                                                                 │\n│  SessionEnd Hook:                                               │\n│  • Extracts first user prompt, turn count, duration             │\n│  • Saves to sessions.jsonl                                      │\n│  • Output NOT displayed (session already closed)                │\n└─────────────────────────────────────────────────────────────────┘\n                               │\n                               │  sessions.jsonl (local storage)\n                               │\n                               ▼\n┌─────────────────────────────────────────────────────────────────┐\n│  Session B (starting)                                           │\n│                                                                 │\n│  SessionStart Hook:                                             │\n│  • Reads last session from sessions.jsonl                       │\n│  • Displays: \"Last (2h ago, 5 turns): Your question...\"         │\n│  • Provides continuity across sessions                          │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n**Why this design?**\n\n1. **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.\n\n2. **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.\n\n3. **Local-first architecture** - Session summaries are stored locally in `sessions.jsonl`, not in the cloud. This ensures:\n   - Instant access (no API latency)\n   - Works offline\n   - No additional API costs\n   - Privacy (session data stays on your machine)\n\n4. **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.\n\n**Data Flow Summary:**\n\n| Event | Action | Storage | Display |\n|-------|--------|---------|---------|\n| SessionEnd | Save summary | Local (sessions.jsonl) | None |\n| SessionStart | Read summary | Local + Cloud | Yes |\n\n## Local Groups Tracking\n\nThe SessionStart hook automatically records project groups to `data/groups.jsonl` (JSONL format):\n\n```jsonl\n{\"keyId\":\"9a823d2f8ea5\",\"groupId\":\"claude-code:/path/to/project-a\",\"name\":\"project-a\",\"path\":\"/path/to/project-a\",\"timestamp\":\"2026-02-09T06:00:00Z\"}\n{\"keyId\":\"9a823d2f8ea5\",\"groupId\":\"claude-code:/path/to/api-server\",\"name\":\"api-server\",\"path\":\"/path/to/api-server\",\"timestamp\":\"2026-02-09T08:00:00Z\"}\n```\n\n**Fields:**\n- `keyId`: SHA-256 hash (first 12 chars) of the API key - associates groups with accounts\n- `groupId`: Unique identifier based on working directory, format: `claude-code:{path}`\n- `name`: Project folder name\n- `path`: Full path to the project\n- `timestamp`: When the group was first recorded\n\n**Deduplication:** Each `keyId + groupId` combination is stored only once (no duplicates).\n\nView tracked projects with `/evermem:projects` command.\n\n## Stop Hook: Conversation Flow\n\nClaude Code stores all conversations locally in JSONL (JSON Lines) format. The EverMem plugin reads this transcript and uploads the latest Q\u0026A pair to the cloud.\n\n### Hook Input\n\nWhen Claude finishes responding, the Stop hook receives input like this:\n\n```json\n{\n  \"session_id\": \"\u003csession-uuid\u003e\",\n  \"transcript_path\": \"~/.claude/projects/\u003cproject-hash\u003e/\u003csession-uuid\u003e.jsonl\",\n  \"cwd\": \"/path/to/your/project\",\n  \"permission_mode\": \"default\",\n  \"hook_event_name\": \"Stop\",\n  \"stop_hook_active\": false\n}\n```\n\n### Transcript File Format\n\nThe 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.\n\n**Common Fields:**\n\n| Field | Description |\n|-------|-------------|\n| `type` | Line type: `user`, `assistant`, `progress`, `system`, `file-history-snapshot` |\n| `uuid` | Unique message ID |\n| `parentUuid` | Parent message ID (for threading) |\n| `timestamp` | ISO 8601 timestamp |\n| `sessionId` | Session UUID |\n| `message.role` | `user` or `assistant` |\n| `message.content` | String or array of content blocks |\n\n**Content Block Types (in `message.content` array):**\n\n| Type | Description |\n|------|-------------|\n| `text` | Final text response to user |\n| `thinking` | Claude's internal reasoning (extended thinking) |\n| `tool_use` | Tool invocation (Read, Write, Bash, etc.) |\n| `tool_result` | Result returned from tool execution |\n\n**Complete Conversation Example:**\n\nA single Q\u0026A turn generates multiple JSONL lines:\n\n```jsonl\n// 1. User message\n{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"debug.js 如何使用\"},\"uuid\":\"696034a3-...\",\"timestamp\":\"2026-02-09T02:20:16.540Z\"}\n\n// 2. Assistant thinking (extended thinking mode)\n{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"用户希望了解 debug.js 的使用方法...\",\"signature\":\"EuAC...\"}]},\"uuid\":\"b375ff09-...\",\"timestamp\":\"2026-02-09T02:20:26.866Z\"}\n\n// 3. Assistant tool use (e.g., Read file)\n{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01Qur8BnkKD9t53JSSorDLbm\",\"name\":\"Read\",\"input\":{\"file_path\":\"/path/to/README.md\"}}]},\"uuid\":\"f01ec15c-...\"}\n\n// 4. Progress event (hook execution)\n{\"type\":\"progress\",\"data\":{\"type\":\"hook_progress\",\"hookEvent\":\"PostToolUse\",\"hookName\":\"PostToolUse:Read\"},\"uuid\":\"f4219b83-...\"}\n\n// 5. Tool result (returned as user message)\n{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"tool_use_id\":\"toolu_01Qur8BnkKD9t53JSSorDLbm\",\"type\":\"tool_result\",\"content\":\"file contents here...\"}]},\"uuid\":\"f5c5f7c6-...\"}\n\n// 6. Assistant final text response\n{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"完成！README 已更新...\"}]},\"uuid\":\"cae1b79c-...\"}\n\n// 7. System events (stop hook, timing)\n{\"type\":\"system\",\"subtype\":\"stop_hook_summary\",\"hookCount\":1,\"hasOutput\":true,\"uuid\":\"25a25edf-...\"}\n{\"type\":\"system\",\"subtype\":\"turn_duration\",\"durationMs\":81371,\"uuid\":\"55418b2c-...\"}\n```\n\n**Simplified View:**\n\n```\nUser Input\n    ↓\n[thinking] → [tool_use] → [tool_result] → [tool_use] → ... → [text]\n    ↓\nSystem Events (hooks, timing)\n```\n\n### Turn Boundary \u0026 Segmentation\n\n**Session Level:** One JSONL file = One Session (filename is session ID)\n\n**Turn Level:** A \"Turn\" = User sends message → Claude fully responds\n\n**Turn boundary marker (ONLY this one):**\n```json\n{\"type\":\"system\",\"subtype\":\"turn_duration\",\"durationMs\":30692}\n```\n\n\u003e **Note:** `file-history-snapshot` is NOT a turn boundary. It's a session-level marker that can appear anywhere in the file.\n\n**JSONL Structure:**\n```\nLine 1:      file-history-snapshot  ← Session marker (NOT turn boundary)\nLine 2-21:   Turn 1\nLine 22:     turn_duration          ← Turn 1 end ✓\nLine 23:     file-history-snapshot  ← Can appear mid-session (NOT turn boundary)\nLine 24-43:  Turn 2\nLine 44:     turn_duration          ← Turn 2 end ✓\n...\n```\n\n**Message Chain (parentUuid):**\n```\nuser (uuid: aaa, parent: None)     ← Turn start\n  ↓\nassistant/thinking (parent: aaa)\n  ↓\nassistant/tool_use (parent: ...)\n  ↓\nuser/tool_result (parent: ...)     ← NOT user input, skip!\n  ↓\nassistant/text (parent: ...)       ← Final response\n  ↓\nsystem/turn_duration (parent: ...) ← Turn end\n```\n\n### Memory Extraction\n\nThe `store-memories.js` hook extracts the **last complete Turn**:\n\n1. **Wait for completion** - Retry reading file until `turn_duration` marker appears (indicates turn is complete)\n2. **Find turn boundaries** - Start after last `turn_duration`, end at current `turn_duration`\n   - **ONLY** `turn_duration` is used as boundary (NOT `file-history-snapshot`)\n3. **Collect user text** - Original input only (skip `tool_result`)\n4. **Collect assistant text** - All `text` blocks (skip `thinking`, `tool_use`)\n5. **Merge content** - Join scattered text blocks with `\\n\\n` separator\n6. **Upload to cloud** - Send both user and assistant content to EverMem API\n\n**Race Condition Handling:**\n\nThe Stop hook runs before `turn_duration` is written. To ensure complete content extraction:\n\n```javascript\n// Retry until turn_duration appears (max 5 attempts, 100ms delay)\nasync function readTranscriptWithRetry(path) {\n  for (let attempt = 1; attempt \u003c= 5; attempt++) {\n    const lines = readFile(path);\n    const lastLine = JSON.parse(lines[lines.length - 1]);\n\n    // turn_duration = turn complete\n    if (lastLine.type === 'system' \u0026\u0026 lastLine.subtype === 'turn_duration') {\n      return lines;\n    }\n\n    await sleep(100);  // Wait and retry\n  }\n}\n```\n\n**Why merge?** A single Claude response spans multiple JSONL lines:\n- `thinking` → `tool_use` → `tool_result` → ... → `text` (final response)\n\nThe hook merges all `text` blocks to capture the complete response.\n\n### API Upload\n\nEach message is sent to `POST /api/v1/memories/group` (or `POST /api/v1/memories` for personal memories):\n\n```json\n{\n  \"group_id\": \"claude-code:/path/to/project\",\n  \"messages\": [\n    {\n      \"sender_id\": \"claude-code-user\",\n      \"role\": \"user\",\n      \"timestamp\": 1770367656189,\n      \"content\": \"How do I add authentication?\"\n    }\n  ],\n  \"async_mode\": true\n}\n```\n\nResponse on success:\n```json\n{\n  \"message\": \"Message accepted and queued for processing\",\n  \"request_id\": \"\u003crequest-uuid\u003e\",\n  \"status\": \"queued\"\n}\n```\n\n### Hook Output (stdout)\n\nThe hook returns JSON via stdout to communicate with Claude Code:\n\n```json\n{\n  \"systemMessage\": \"💾 Memory saved (2) [user: 59, assistant: 127]\"\n}\n```\n\nThis message is displayed to the user after Claude finishes responding.\n\n## Memory Hub Implementation\n\nThe `/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.\n\n### Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                           /evermem:hub Command                               │\n│  1. Start proxy server: node server/proxy.js \u0026                              │\n│  2. Generate URL: http://localhost:3456/?key=${EVERMEM_API_KEY}             │\n│  3. User opens URL in browser                                                │\n└─────────────────────────────────────────────────────────────────────────────┘\n                                    │\n                                    ▼\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                         Browser (dashboard.html)                             │\n│                                                                              │\n│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │\n│  │   Stats     │  │  Heatmap    │  │  7-Day      │  │  Project    │        │\n│  │   Cards     │  │  (6 months) │  │  Chart      │  │   Cards     │        │\n│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘        │\n│                                                                              │\n│  Data Flow:                                                                  │\n│  1. GET /api/groups → Local groups.jsonl (filtered by keyId)                │\n│  2. For each group: POST /api/v1/memories/get → Fetch memories               │\n│  3. Render dashboard with aggregated data                                    │\n└─────────────────────────────────────────────────────────────────────────────┘\n                                    │\n                                    ▼\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                      Proxy Server (localhost:3456)                           │\n│                                                                              │\n│  Routes:                                                                     │\n│  ┌─────────────────────────────────────────────────────────────────────┐   │\n│  │  GET  /              → Serve dashboard.html                          │   │\n│  │  GET  /api/groups    → Read groups.jsonl, filter by keyId           │   │\n│  │  POST /api/v1/memories/search → Forward to EverMind API             │   │\n│  │  POST /api/v1/memories/get    → Forward to EverMind API             │   │\n│  │  GET  /health        → Health check                                  │   │\n│  └─────────────────────────────────────────────────────────────────────┘   │\n│                                                                              │\n│  Why Proxy?                                                                  │\n│  - The proxy forwards browser calls to the EverMind API and serves the      │\n│    dashboard.                                                                │\n└─────────────────────────────────────────────────────────────────────────────┘\n                                    │\n                                    ▼\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                         EverMem Cloud API                                    │\n│                      https://api.evermind.ai                                 │\n│                                                                              │\n│  POST /api/v1/memories/search  { query, filters, method, memory_types, top_k } │\n│  POST /api/v1/memories/get     { memory_type, filters, page, page_size, ... }  │\n│  Response: { data: { episodes[], total_count, count } }                     │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n### Proxy Server (`server/proxy.js`)\n\n```javascript\n// Key function: Convert API key to keyId (for groups filtering)\nfunction computeKeyId(apiKey) {\n  const hash = createHash('sha256').update(apiKey).digest('hex');\n  return hash.substring(0, 12);  // First 12 chars of SHA-256\n}\n\n// Key function: Read groups.jsonl and filter by keyId\nfunction getGroupsForKey(keyId) {\n  const content = readFileSync(GROUPS_FILE, 'utf8');\n  const lines = content.trim().split('\\n');\n\n  const groupMap = new Map();\n  for (const line of lines) {\n    const entry = JSON.parse(line);\n    if (entry.keyId !== keyId) continue;  // Filter by current API key\n\n    // Aggregate: count sessions, track first/last seen\n    // ...\n  }\n  return Array.from(groupMap.values());\n}\n\n// Key route: Forward POST requests to EverMind API\n// Browser sends:  POST /api/v1/memories/search  { query, filters, ... }\n// Browser sends:  POST /api/v1/memories/get     { memory_type, filters, ... }\n// Proxy forwards to upstream API with Authorization header\n```\n\n### Dashboard (`assets/dashboard.html`)\n\n**Data Loading Flow:**\n\n```javascript\nasync function loadGroups() {\n  // 1. Fetch groups from local storage (via proxy)\n  const groupsData = await fetch('/api/groups', {\n    headers: { 'Authorization': `Bearer ${apiKey}` }\n  });\n\n  // 2. For each group, fetch memories with pagination\n  for (const group of groups) {\n    const data = await fetch('/api/v1/memories/get', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        memory_type: 'episodic_memory',\n        filters: { group_id: group.id },\n        page: 1,\n        page_size: 100,\n        rank_by: 'timestamp',\n        rank_order: 'desc'\n      })\n    });\n\n    // Store: memories[], totalCount, hasMore, offset\n    groupMemories[group.id] = { ... };\n  }\n\n  // 3. Render dashboard\n  renderDashboard(totalMemories);\n}\n```\n\n**UI Components:**\n\n| Component | Description |\n|-----------|-------------|\n| Stats Grid | 5 cards: Total Memories, Projects, Active Days, Avg/Day, Avg/Project |\n| Heatmap | GitHub-style 6-month activity grid with tooltips |\n| Growth Chart | Last 7 days bar chart |\n| Project Cards | Expandable cards showing memories per project |\n| Timeline | Within each project, memories grouped by date |\n| Load More | Pagination button when `has_more: true` |\n\n**Timeline within Project:**\n\n```\n📁 evermem-claude-code (25 memories)\n├── ● Sun, Feb 9 [Today]               3 memories\n│   ├── 💭 Discussion about JWT...     10:30 AM\n│   ├── 🔧 Fixed authentication...     09:15 AM\n│   └── ✨ Created new API endpoint    08:00 AM\n│\n├── ● Sat, Feb 8                       5 memories\n│   ├── 📝 Updated README...           16:20 PM\n│   └── ...\n│\n└── [Load more (17 remaining)]\n```\n\n## Debug Logging\n\nBoth `inject-memories.js` and `store-memories.js` use a shared debug utility:\n\n```javascript\nimport { debug, setDebugPrefix } from './utils/debug.js';\n\nsetDebugPrefix('inject');  // Log lines will show [inject] prefix\ndebug('hookInput:', data); // Only writes when EVERMEM_DEBUG=1\n```\n\n**Debug output by script:**\n\n| Script | Prefix | Debug Points |\n|--------|--------|--------------|\n| `inject-memories.js` | `[inject]` | hookInput, search query, search results, filtered/selected memories, output |\n| `store-memories.js` | `[store]` | hookInput, read attempts, turn range, line types, extracted content, results |\n\n**Example debug log:**\n\n```log\n# Memory injection (UserPromptSubmit hook)\n[2026-02-06T08:47:30.100Z] [inject] hookInput: { \"prompt\": \"How do I add auth?\", ... }\n[2026-02-06T08:47:30.150Z] [inject] searching memories for prompt: How do I add auth?\n[2026-02-06T08:47:30.500Z] [inject] search results: {\"total\": 5, \"memories\": [...]}\n[2026-02-06T08:47:30.520Z] [inject] selected memories: [{\"score\": 0.85, \"subject\": \"JWT implementation\"}]\n\n# Memory storage (Stop hook)\n[2026-02-06T08:47:36.184Z] [store] hookInput: { \"transcript_path\": \"...jsonl\", ... }\n\n# Retry logic - waiting for turn_duration\n[2026-02-06T08:47:36.200Z] [store] read attempt 1: { \"totalLines\": 525, \"isComplete\": false, \"lastLineType\": \"progress\" }\n[2026-02-06T08:47:36.201Z] [store] turn not complete, waiting 100ms before retry...\n[2026-02-06T08:47:36.310Z] [store] read attempt 2: { \"totalLines\": 527, \"isComplete\": false, \"lastLineType\": \"system/stop_hook_summary\" }\n[2026-02-06T08:47:36.311Z] [store] turn not complete, waiting 100ms before retry...\n[2026-02-06T08:47:36.420Z] [store] read attempt 3: { \"totalLines\": 528, \"isComplete\": true, \"lastLineType\": \"system/turn_duration\" }\n\n# Content extraction\n[2026-02-06T08:47:36.425Z] [store] turn range: { \"turnStartIndex\": 500, \"turnEndIndex\": 528, \"totalLines\": 528 }\n[2026-02-06T08:47:36.430Z] [store] assistantTexts count: 3\n[2026-02-06T08:47:36.435Z] [store] extracted: { \"userLength\": 59, \"assistantLength\": 847, ... }\n\n# API upload results\n[2026-02-06T08:47:36.970Z] [store] results: [\n  {\n    \"type\": \"USER\",\n    \"len\": 59,\n    \"status\": 202,\n    \"ok\": true,\n    \"response\": {\n      \"message\": \"Message accepted and queued for processing\",\n      \"status\": \"queued\"\n    }\n  },\n  {\n    \"type\": \"ASSISTANT\",\n    \"len\": 127,\n    \"status\": 202,\n    \"ok\": true,\n    \"response\": { ... }\n  }\n]\n[2026-02-06T08:47:36.975Z] [store] skipped: []\n```\n\n**Using debug.js in your own hooks:**\n\n```javascript\nimport { debug, setDebugPrefix, isDebugEnabled } from './utils/debug.js';\n\n// Set prefix to identify your script in logs\nsetDebugPrefix('my-hook');\n\n// Log objects (auto JSON stringified) or strings\ndebug('processing:', { key: 'value' });\n\n// Check if debug is enabled\nif (isDebugEnabled()) {\n  // expensive debug operations\n}\n```\n\n## Project Structure\n\n```\nevermem-plugin/\n├── plugin.json               # Plugin manifest\n├── commands/\n│   ├── help.md               # /evermem:help command\n│   ├── search.md             # /evermem:search command\n│   ├── hub.md                # /evermem:hub command\n│   ├── debug.md              # /evermem:debug command\n│   └── projects.md           # /evermem:projects command\n├── data/\n│   ├── groups.jsonl          # Local storage for tracked projects (JSONL format)\n│   └── sessions.jsonl        # Local storage for session summaries (JSONL format)\n├── hooks/\n│   ├── hooks.json            # Hook configuration\n│   └── scripts/\n│       ├── inject-memories.js    # Memory recall (UserPromptSubmit)\n│       ├── store-memories.js     # Memory save (Stop)\n│       ├── session-context.js    # Session context (SessionStart)\n│       ├── session-summary.js    # Session summary (SessionEnd)\n│       └── utils/\n│           ├── evermem-api.js    # EverMem Cloud API client\n│           ├── config.js         # Configuration utilities\n│           ├── debug.js          # Shared debug logging utility\n│           └── groups-store.js   # Local groups persistence\n├── assets/\n│   └── dashboard.html        # Memory Hub dashboard\n├── server/\n│   └── proxy.js              # Local proxy server for dashboard\n└── README.md\n```\n\n## API Reference\n\nThe plugin uses the EverMem Cloud API at `https://api.evermind.ai`:\n\n- `POST /api/v1/memories/group` - Store a new memory (group); `POST /api/v1/memories` for personal memories\n- `POST /api/v1/memories/search` - Search memories (hybrid retrieval, with JSON body)\n- `POST /api/v1/memories/get` - Get memories (list with JSON body)\n\n## Development\n\n### Local Development\n\n```bash\n# Clone the repository\ngit clone https://github.com/EverMind-AI/evermem-claude-code.git\ncd evermem-claude-code\n\n# Install dependencies\nnpm install\n\n# Run Claude Code with local plugin\nclaude --plugin-dir .\n```\n\n### Testing Hooks\n\n```bash\n# Test memory recall\necho '{\"prompt\":\"How do I handle authentication?\"}' | node hooks/scripts/inject-memories.js\n\n# Test memory save (requires transcript file)\necho '{\"transcript_path\":\"/path/to/transcript.json\"}' | node hooks/scripts/store-memories.js\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fevermind-ai%2Fevermem-claude-code","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fevermind-ai%2Fevermem-claude-code","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fevermind-ai%2Fevermem-claude-code/lists"}