Files
EverOS/use-cases/claude-code-plugin/README.md
Elliot Chen 8f175d3f8f docs: document EverOS 1.0.0 issue migration (#258)
* docs: document EverOS 1.0 issue migration

* docs: standardize EverOS 1.0.0 wording
2026-06-06 14:33:00 +08:00

1109 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 <query>` | Search your memories for specific topics |
| `/evermem:ask <question>` | 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 <script.js> │
│ 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: <session-context>...</session-context> │
│ │
│ The systemPrompt is injected into Claude's context window │
└─────────────────────────────────────────────────────────────────┘
```
### Hook Input (stdin)
```json
{
"session_id": "<session-uuid>",
"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": "<session-context>\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</session-context>"
}
```
### 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": "<session-uuid>",
"transcript_path": "~/.claude/projects/<hash>/<session-uuid>.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": "<session-uuid>",
"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": "<session-uuid>",
"transcript_path": "~/.claude/projects/<project-hash>/<session-uuid>.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": "<request-uuid>",
"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
```