Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63f930a433 | |||
| 3cb003f4e4 | |||
| 8b5e5efeb5 | |||
| a57aba82a4 | |||
| 7b7a53ee19 | |||
| e57bfc4187 | |||
| 3cfdd5fd65 | |||
| 1a226b08f0 | |||
| 15b5176fe6 | |||
| 418dc963b2 | |||
| 2314362028 | |||
| f373b682dd | |||
| 1c25c3a7e7 | |||
| 3ca5e33b4a | |||
| 3042de1093 | |||
| f7f62ef3f3 | |||
| 726f167ebf | |||
| a62887a6e0 | |||
| 635cc87462 | |||
| c27f07023c |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.1.1",
|
||||
"version": "4.2.1",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Claude-mem is a persistent memory compression system that preserves context across Claude Code sessions. It automatically captures tool usage observations, processes them through the Claude Agent SDK, and makes summaries available to future sessions.
|
||||
|
||||
**Current Version**: 4.1.0
|
||||
**Current Version**: 4.2.0
|
||||
**License**: AGPL-3.0
|
||||
**Author**: Alex Newman (@thedotmack)
|
||||
|
||||
@@ -28,12 +28,13 @@ Claude-mem integrates with Claude Code through 5 lifecycle hooks:
|
||||
1. **SessionStart Hook** (`context-hook`)
|
||||
- Injects context from previous sessions
|
||||
- Auto-starts PM2 worker service
|
||||
- Retrieves last 3 session summaries
|
||||
- Retrieves last 10 session summaries with three-tier verbosity (v4.2.0)
|
||||
- Fixed in v4.1.0 to use proper JSON hookSpecificOutput format
|
||||
|
||||
2. **UserPromptSubmit Hook** (`new-hook`)
|
||||
- Creates new session records
|
||||
- Initializes session tracking
|
||||
- Saves raw user prompts for full-text search (as of v4.2.0)
|
||||
|
||||
3. **PostToolUse Hook** (`save-hook`)
|
||||
- Captures tool execution observations
|
||||
@@ -73,6 +74,7 @@ The worker service runs as a PM2-managed background process that handles AI proc
|
||||
- `sdk_sessions` - Session tracking with prompt counters
|
||||
- `session_summaries` - AI-generated session summaries (multiple per session)
|
||||
- `observations` - Captured tool usage with structured fields
|
||||
- `user_prompts` - Raw user prompts with FTS5 search (as of v4.2.0)
|
||||
|
||||
**Schema Features**:
|
||||
- FTS5 (Full-Text Search) virtual tables for fast searching
|
||||
@@ -82,23 +84,24 @@ The worker service runs as a PM2-managed background process that handles AI proc
|
||||
- Observation types: decision, bugfix, feature, refactor, discovery, change
|
||||
|
||||
**Database Classes**:
|
||||
- `SessionStore` - CRUD operations for sessions, observations, summaries
|
||||
- `SessionSearch` - FTS5 full-text search with 7 search methods
|
||||
- `SessionStore` - CRUD operations for sessions, observations, summaries, user prompts
|
||||
- `SessionSearch` - FTS5 full-text search with 8 search methods
|
||||
|
||||
### MCP Search Server
|
||||
|
||||
**Location**: `src/servers/search-server.ts`
|
||||
**Configuration**: `plugin/.mcp.json`
|
||||
|
||||
Exposes 7 specialized search tools to Claude:
|
||||
Exposes 8 specialized search tools to Claude:
|
||||
|
||||
1. **search_observations** - Full-text search across observations
|
||||
2. **search_sessions** - Full-text search across session summaries
|
||||
3. **find_by_concept** - Find observations tagged with specific concepts
|
||||
4. **find_by_file** - Find observations referencing specific file paths
|
||||
5. **find_by_type** - Find observations by type (decision/bugfix/feature/etc.)
|
||||
6. **get_recent_context** - Get recent session context including summaries and observations for a project
|
||||
7. **advanced_search** - Combine multiple filters with full-text search
|
||||
3. **search_user_prompts** - Full-text search across raw user prompts (as of v4.2.0)
|
||||
4. **find_by_concept** - Find observations tagged with specific concepts
|
||||
5. **find_by_file** - Find observations referencing specific file paths
|
||||
6. **find_by_type** - Find observations by type (decision/bugfix/feature/etc.)
|
||||
7. **get_recent_context** - Get recent session context including summaries and observations for a project
|
||||
8. **advanced_search** - Combine multiple filters with full-text search
|
||||
|
||||
**Search Pipeline**:
|
||||
```
|
||||
@@ -201,7 +204,27 @@ npm run build && git commit -a -m "Build and update" && git push && cd ~/.claude
|
||||
|
||||
## Version History
|
||||
|
||||
### v4.1.0 (Current)
|
||||
### v4.2.0 (Current)
|
||||
**Breaking Changes**: None (minor version)
|
||||
|
||||
**Features**:
|
||||
- User prompt storage with FTS5 full-text search
|
||||
- New `user_prompts` table stores raw user input for every prompt
|
||||
- New `search_user_prompts` MCP tool enables searching actual user requests
|
||||
- Automatic FTS5 indexing of all user prompts for fast retrieval
|
||||
|
||||
**Benefits**:
|
||||
- Full context reconstruction from user intent → Claude actions → outcomes
|
||||
- Pattern detection for repeated requests (identify when Claude isn't listening)
|
||||
- Improved debugging by tracing from original user words to final implementation
|
||||
- Historical search: "How many times did user ask for X feature?"
|
||||
|
||||
**Implementation**:
|
||||
- Migration 10: Creates user_prompts table with FTS5 virtual table and sync triggers
|
||||
- UserPromptSubmit hook now saves prompts using claude_session_id (available immediately)
|
||||
- Citations use `claude-mem://user-prompt/{id}` URI scheme
|
||||
|
||||
### v4.1.0
|
||||
**Breaking Changes**: None (minor version)
|
||||
|
||||
**Features**:
|
||||
@@ -220,8 +243,8 @@ npm run build && git commit -a -m "Build and update" && git push && cd ~/.claude
|
||||
- Worker auto-starts in SessionStart hook
|
||||
|
||||
**Features**:
|
||||
- MCP Search Server with 7 specialized search tools
|
||||
- FTS5 full-text search across observations and sessions
|
||||
- MCP Search Server with 8 specialized search tools
|
||||
- FTS5 full-text search across observations, sessions, and user prompts
|
||||
- Citation support with `claude-mem://` URIs
|
||||
- HTTP REST API architecture with PM2 management
|
||||
- Plugin data directory integration
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+297
@@ -0,0 +1,297 @@
|
||||
# Claude Never Forgets
|
||||
|
||||
**Give Claude a memory that spans your entire project.**
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
```
|
||||
You: "Remember that bug we fixed last Tuesday with the auth flow?"
|
||||
Claude: "I don't have access to previous conversations..."
|
||||
```
|
||||
|
||||
Every `/clear` wipes Claude's memory. Every new session starts from zero. You repeat yourself constantly.
|
||||
|
||||
**Until now.**
|
||||
|
||||
---
|
||||
|
||||
## What Changes
|
||||
|
||||
### Before claude-mem
|
||||
```typescript
|
||||
// Monday: You debug an issue
|
||||
You: "Why is the database connection failing?"
|
||||
Claude: [Helps you fix it]
|
||||
|
||||
// Wednesday: Similar issue appears
|
||||
You: "The database is timing out again"
|
||||
Claude: "Let me investigate..." [Starts from scratch]
|
||||
```
|
||||
|
||||
### After claude-mem
|
||||
```typescript
|
||||
// Monday: You debug an issue
|
||||
You: "Why is the database connection failing?"
|
||||
Claude: [Helps you fix it]
|
||||
✓ Remembers: connection pool exhaustion pattern
|
||||
|
||||
// Wednesday: Similar issue appears
|
||||
You: "The database is timing out again"
|
||||
Claude: "Based on Monday's session, this looks like the same
|
||||
connection pool issue. Let me check the pool size config..."
|
||||
```
|
||||
|
||||
Claude **remembers**. Claude **learns**. Claude gets **better** over time.
|
||||
|
||||
---
|
||||
|
||||
## Real Examples
|
||||
|
||||
### 1. **Context Across Sessions**
|
||||
|
||||
**Without claude-mem:**
|
||||
```
|
||||
Session 1: "We use Redux for state management"
|
||||
Session 2: "What state management do you use?" ❌
|
||||
```
|
||||
|
||||
**With claude-mem:**
|
||||
```
|
||||
Session 1: "We use Redux for state management"
|
||||
Session 2: Claude already knows you use Redux ✓
|
||||
Suggests Redux patterns automatically ✓
|
||||
References your store structure ✓
|
||||
```
|
||||
|
||||
### 2. **Architectural Memory**
|
||||
|
||||
**Your third session of the day:**
|
||||
```
|
||||
You: "Add a new API endpoint for user preferences"
|
||||
|
||||
Claude: "I see from previous sessions that:
|
||||
- Your API follows REST conventions in src/api/
|
||||
- You use Zod for validation
|
||||
- Auth middleware is required for user routes
|
||||
- You prefer async/await over promises
|
||||
|
||||
I'll create the endpoint following these patterns..."
|
||||
```
|
||||
|
||||
**No explaining. No repeating. Just building.**
|
||||
|
||||
### 3. **Bug Pattern Recognition**
|
||||
|
||||
```
|
||||
Week 1: Fixed race condition in webhook handler
|
||||
Week 2: Different race condition in event processor
|
||||
|
||||
Claude: "This looks similar to the webhook race condition
|
||||
we fixed last week. The same solution should work..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ You code with │
|
||||
│ Claude today │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ claude-mem captures & │
|
||||
│ compresses everything │
|
||||
│ into structured memories │
|
||||
└────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ Tomorrow, Claude starts │
|
||||
│ with full context of │
|
||||
│ your project history │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
**Automatic. Zero effort. Always on.**
|
||||
|
||||
---
|
||||
|
||||
## What Gets Remembered
|
||||
|
||||
✓ **Decisions**: "Why did we choose this architecture?"
|
||||
✓ **Bugs Fixed**: "How did we solve this before?"
|
||||
✓ **Code Patterns**: "What's our convention for this?"
|
||||
✓ **File Changes**: "What did we modify last session?"
|
||||
✓ **Refactorings**: "What was the old implementation?"
|
||||
✓ **Dependencies**: "Which libraries are we using?"
|
||||
|
||||
Everything Claude does with you gets compressed into **searchable, reusable memory**.
|
||||
|
||||
---
|
||||
|
||||
## Powerful Search
|
||||
|
||||
Ask Claude to search your project history:
|
||||
|
||||
```
|
||||
You: "Find all the database migrations we did"
|
||||
Claude: [Searches across all sessions]
|
||||
"I found 7 database-related changes:
|
||||
- March 15: Added user_preferences table
|
||||
- March 12: Migration for OAuth tokens
|
||||
- March 8: Index optimization on sessions
|
||||
..."
|
||||
|
||||
You: "What decisions did we make about authentication?"
|
||||
Claude: [Retrieves decision observations]
|
||||
"We decided to use JWT tokens because..."
|
||||
```
|
||||
|
||||
7 specialized search tools. Instant recall. Full project history.
|
||||
|
||||
---
|
||||
|
||||
## The Numbers
|
||||
|
||||
| Metric | Before | After |
|
||||
|--------|--------|-------|
|
||||
| Context repetition | Every session | Never |
|
||||
| Onboarding time | 5-10 min per session | 0 seconds |
|
||||
| Bug re-investigation | Common | Rare |
|
||||
| Architectural questions | "What did we decide?" | Claude already knows |
|
||||
| Code pattern consistency | Manual enforcement | Automatic |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Start (2 minutes)
|
||||
|
||||
```bash
|
||||
# 1. Clone and install
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
|
||||
# 2. Add to Claude Code
|
||||
/plugin marketplace add .claude-plugin/marketplace.json
|
||||
|
||||
# 3. Install
|
||||
/plugin install claude-mem
|
||||
```
|
||||
|
||||
**Done.** Claude now has memory.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Choose your AI model (controls cost vs. quality of memory compression):
|
||||
|
||||
```bash
|
||||
./claude-mem-settings.sh
|
||||
```
|
||||
|
||||
**Models:**
|
||||
- `claude-haiku-4-5` - Fast & cheap
|
||||
- `claude-sonnet-4-5` - Balanced (default) ✓
|
||||
- `claude-opus-4` - Maximum quality
|
||||
|
||||
---
|
||||
|
||||
## Under The Hood
|
||||
|
||||
**Simple architecture, powerful results:**
|
||||
|
||||
1. **Hooks** capture every tool Claude uses
|
||||
2. **Worker service** compresses observations with AI
|
||||
3. **SQLite database** stores structured memories
|
||||
4. **MCP server** makes everything searchable
|
||||
5. **Context injection** gives Claude the right memories at the right time
|
||||
|
||||
**Zero maintenance. Runs in the background. Just works.**
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Solo Developers
|
||||
- Never lose context between coding sessions
|
||||
- Build on past decisions automatically
|
||||
- Remember why you made each choice
|
||||
|
||||
### Team Projects
|
||||
- Share architectural knowledge across sessions
|
||||
- Maintain consistency in code patterns
|
||||
- Document decisions as they happen
|
||||
|
||||
### Learning & Experiments
|
||||
- Track what you tried and what worked
|
||||
- Build a personal knowledge base
|
||||
- Learn from past mistakes
|
||||
|
||||
### Large Refactors
|
||||
- Remember what you changed across multiple sessions
|
||||
- Track progress on multi-day tasks
|
||||
- Maintain context through interruptions
|
||||
|
||||
---
|
||||
|
||||
## What Developers Say
|
||||
|
||||
> *"I used to spend 10 minutes every morning explaining my project to Claude. Now it just knows."*
|
||||
|
||||
> *"It's like having a teammate who was actually there for every line of code."*
|
||||
|
||||
> *"The search is incredible. I can ask about decisions we made weeks ago."*
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Does this slow down Claude?**
|
||||
No. Memory processing happens in the background. Claude responds instantly.
|
||||
|
||||
**How much does it cost?**
|
||||
Minimal. Memory compression uses your chosen model (default: Sonnet 4.5). Typical cost: $0.01-0.05 per coding session.
|
||||
|
||||
**Where is data stored?**
|
||||
Locally in `~/.claude-mem/claude-mem.db`. Fully private. Never leaves your machine.
|
||||
|
||||
**Can I search my memories?**
|
||||
Yes. 7 specialized search tools available through Claude.
|
||||
|
||||
**Does it work with existing projects?**
|
||||
Yes. Starts learning immediately when installed.
|
||||
|
||||
**What if I want to forget something?**
|
||||
Delete observations directly from the SQLite database, or start fresh by removing the DB file.
|
||||
|
||||
---
|
||||
|
||||
## Get Started
|
||||
|
||||
```bash
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
/plugin marketplace add .claude-plugin/marketplace.json
|
||||
/plugin install claude-mem
|
||||
```
|
||||
|
||||
**Give Claude a memory. Transform how you code.**
|
||||
|
||||
---
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Technical Documentation](./CLAUDE.md)
|
||||
- [GitHub Repository](https://github.com/thedotmack/claude-mem)
|
||||
- [Report Issues](https://github.com/thedotmack/claude-mem/issues)
|
||||
|
||||
**License**: AGPL-3.0
|
||||
**Version**: 4.1.0
|
||||
**Author**: Alex Newman ([@thedotmack](https://github.com/thedotmack))
|
||||
@@ -0,0 +1,246 @@
|
||||
# Plan: Store Raw User Prompts for Full Context Search
|
||||
|
||||
## Problem
|
||||
|
||||
Claude-mem currently only captures tool executions (via PostToolUse hook), not the user's actual instructions and requests. This creates a gap:
|
||||
|
||||
- We can see WHAT Claude did (observations)
|
||||
- We can see SUMMARIES of what happened (session_summaries)
|
||||
- We CANNOT see what the user actually said/requested
|
||||
|
||||
**Real Example:**
|
||||
User repeatedly asked to "remove session validation" with increasing frustration over multiple prompts. The memory system captured the final implementation but not the conversation where the user had to convince Claude 3-4 times.
|
||||
|
||||
## Solution
|
||||
|
||||
Store ALL raw user prompts in a dedicated table with full-text search capability.
|
||||
|
||||
## Database Schema Changes
|
||||
|
||||
### 1. Create `user_prompts` table
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_sdk_session ON user_prompts(sdk_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
```
|
||||
|
||||
### 2. Create FTS5 virtual table for full-text search
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Create triggers to sync FTS5
|
||||
|
||||
```sql
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
```
|
||||
|
||||
## Code Changes
|
||||
|
||||
### 1. Update schema migration
|
||||
|
||||
**File:** `src/services/sqlite/SessionStore.ts`
|
||||
|
||||
Add the user_prompts table creation to the schema initialization (look for the `CREATE TABLE` statements in the constructor).
|
||||
|
||||
### 2. Add method to save user prompts
|
||||
|
||||
**File:** `src/services/sqlite/SessionStore.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Save a user prompt
|
||||
*/
|
||||
saveUserPrompt(sdkSessionId: string, promptNumber: number, promptText: string): number {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(sdk_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(sdkSessionId, promptNumber, promptText, now.toISOString(), nowEpoch);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Update new-hook to save user prompts
|
||||
|
||||
**File:** `src/hooks/new.ts`
|
||||
|
||||
After creating/getting the session and incrementing prompt counter, save the raw prompt:
|
||||
|
||||
```typescript
|
||||
// Around line 37, after incrementPromptCounter
|
||||
const sessionDbId = db.createSDKSession(session_id, project, prompt);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
|
||||
// Get sdk_session_id for foreign key
|
||||
const session = db.findAnySDKSession(session_id);
|
||||
if (session?.sdk_session_id) {
|
||||
db.saveUserPrompt(session.sdk_session_id, promptNumber, prompt);
|
||||
}
|
||||
|
||||
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
```
|
||||
|
||||
**Note:** We need the sdk_session_id (not just sessionDbId) for the foreign key. May need to adjust the logic to handle prompts before SDK session is fully initialized.
|
||||
|
||||
### 4. Add SessionSearch methods
|
||||
|
||||
**File:** `src/services/sqlite/SessionSearch.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Search user prompts with full-text search
|
||||
*/
|
||||
searchUserPrompts(query: string, options: SearchOptions = {}): UserPrompt[] {
|
||||
const { limit = 20, offset = 0 } = options;
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
up.id,
|
||||
up.sdk_session_id,
|
||||
up.prompt_number,
|
||||
up.prompt_text,
|
||||
up.created_at,
|
||||
up.created_at_epoch
|
||||
FROM user_prompts_fts fts
|
||||
JOIN user_prompts up ON fts.rowid = up.id
|
||||
WHERE user_prompts_fts MATCH ?
|
||||
ORDER BY rank
|
||||
LIMIT ? OFFSET ?
|
||||
`);
|
||||
|
||||
return stmt.all(query, limit, offset) as UserPrompt[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all prompts for a session
|
||||
*/
|
||||
getUserPromptsBySession(sdkSessionId: string): UserPrompt[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
sdk_session_id,
|
||||
prompt_number,
|
||||
prompt_text,
|
||||
created_at,
|
||||
created_at_epoch
|
||||
FROM user_prompts
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY prompt_number ASC
|
||||
`);
|
||||
|
||||
return stmt.all(sdkSessionId) as UserPrompt[];
|
||||
}
|
||||
```
|
||||
|
||||
Add the `UserPrompt` type:
|
||||
|
||||
```typescript
|
||||
interface UserPrompt {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add MCP search tool
|
||||
|
||||
**File:** `src/servers/search-server.ts`
|
||||
|
||||
Add a new tool `search_user_prompts`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
name: 'search_user_prompts',
|
||||
description: 'Search raw user prompts with full-text search. Use this to find what the user actually said/requested across all sessions.',
|
||||
inputSchema: zodToJsonSchema(z.object({
|
||||
query: z.string().describe('Search query for FTS5 full-text search'),
|
||||
limit: z.number().optional().default(20).describe('Maximum results'),
|
||||
offset: z.number().optional().default(0).describe('Results to skip'),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
And implement the handler.
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Full context reconstruction:** See exact user language, frustration level, repeated requests
|
||||
2. **Pattern detection:** Identify when Claude isn't listening (user repeats same request)
|
||||
3. **Improved summaries:** AI can reference actual user words, not just observations
|
||||
4. **Debugging:** Trace from user request → Claude actions → outcomes
|
||||
5. **Search across time:** "How many times did user ask for X feature?"
|
||||
|
||||
## Testing
|
||||
|
||||
1. Create a new session and submit several prompts
|
||||
2. Query `user_prompts` table to verify they're saved
|
||||
3. Test FTS5 search: `SELECT * FROM user_prompts_fts WHERE user_prompts_fts MATCH 'validation'`
|
||||
4. Test MCP tool: `/claude-mem search_user_prompts "remove validation"`
|
||||
5. Verify prompt_number increments correctly
|
||||
6. Test cascade delete: delete a session, verify prompts are deleted
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- This is a NEW table, no data migration needed
|
||||
- Existing sessions won't have historical prompts (that's fine)
|
||||
- FTS5 triggers will auto-populate as new prompts arrive
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **sdk_session_id timing:** New-hook runs BEFORE worker initializes SDK session. Need to either:
|
||||
- Save prompts with sessionDbId initially, update with sdk_session_id later
|
||||
- OR use sessionDbId as the foreign key instead
|
||||
|
||||
2. **Storage size:** User prompts can be large. Consider max length or compression?
|
||||
|
||||
3. **Privacy:** User prompts may contain sensitive info. Document this clearly.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Add schema to SessionStore constructor
|
||||
2. Add saveUserPrompt method to SessionStore
|
||||
3. Add search methods to SessionSearch
|
||||
4. Update new-hook to save prompts
|
||||
5. Add MCP tool to search-server
|
||||
6. Test end-to-end
|
||||
7. Update CLAUDE.md documentation
|
||||
@@ -0,0 +1,283 @@
|
||||
# Magic UI Components Catalog
|
||||
|
||||
Complete catalog of Magic UI components with descriptions and use cases.
|
||||
|
||||
## Animation Components
|
||||
|
||||
### 1. **Animated List**
|
||||
- **Purpose**: Animates list items sequentially with delay
|
||||
- **Use Case**: Showcasing events, notifications, feature lists
|
||||
- **Key Props**: `delay` (ms between items)
|
||||
- **Effect**: Staggered reveal animation
|
||||
|
||||
### 2. **Text Animate**
|
||||
- **Purpose**: Sophisticated text animation effects
|
||||
- **Animation Types**:
|
||||
- `blurIn` - Characters fade from blur
|
||||
- `slideUp` - Words slide up
|
||||
- `scaleUp` - Text scales up
|
||||
- `fadeIn` - Lines fade in
|
||||
- `slideLeft` - Characters slide from left
|
||||
- **Animate By**: `character`, `word`, `text`, `line`
|
||||
- **Use Case**: Hero headlines, feature announcements, storytelling
|
||||
- **Customization**: Delay, duration, custom motion variants
|
||||
|
||||
### 3. **Flip Text**
|
||||
- **Purpose**: Vertical flip animation for text
|
||||
- **Key Props**: `duration`, `delayMultiple`, custom variants
|
||||
- **Use Case**: Eye-catching headlines, call-to-actions
|
||||
|
||||
### 4. **Morphing Text**
|
||||
- **Purpose**: Dynamic text transitions between multiple strings
|
||||
- **Key Props**: `texts` array (strings to morph between)
|
||||
- **Use Case**: Dynamic value propositions, rotating benefits
|
||||
|
||||
### 5. **Word Rotate**
|
||||
- **Purpose**: Vertical rotation of words
|
||||
- **Key Props**: `words` (string array), `duration` (2500ms default)
|
||||
- **Use Case**: Rotating feature names, dynamic headlines
|
||||
|
||||
### 6. **Aurora Text**
|
||||
- **Purpose**: Beautiful aurora text effect
|
||||
- **Key Props**: `colors` array, `speed` multiplier
|
||||
- **Default Colors**: `["#FF0080", "#7928CA", "#0070F3", "#38bdf8"]`
|
||||
- **Use Case**: Premium headlines, brand emphasis
|
||||
|
||||
## Visual Effect Components
|
||||
|
||||
### 7. **Orbiting Circles**
|
||||
- **Purpose**: Circles moving in orbit along circular paths
|
||||
- **Key Props**:
|
||||
- `radius` (orbit size)
|
||||
- `duration` (animation speed)
|
||||
- `reverse` (direction)
|
||||
- `delay`, `path` (show orbit path)
|
||||
- `iconSize`, `speed`
|
||||
- **Use Case**: Technology visualization, ecosystem diagrams, feature satellites
|
||||
|
||||
### 8. **Particles**
|
||||
- **Purpose**: Animated particle background with depth and interactivity
|
||||
- **Use Case**: Hero sections, immersive backgrounds
|
||||
|
||||
### 9. **Confetti**
|
||||
- **Purpose**: Celebration confetti effect
|
||||
- **Key Props**:
|
||||
- `particleCount`, `angle`, `spread`
|
||||
- `startVelocity`, `decay`, `gravity`
|
||||
- `colors`, `shapes` (square, circle, star)
|
||||
- `origin` point
|
||||
- **Includes**: `ConfettiButton` wrapper component
|
||||
- **Use Case**: Success states, milestones, achievements
|
||||
|
||||
### 10. **Border Beam**
|
||||
- **Purpose**: Animated beam effect along borders
|
||||
- **Key Props**: `reverse`, spring animations
|
||||
- **Use Case**: Card highlights, section emphasis
|
||||
|
||||
### 11. **Shine Border**
|
||||
- **Purpose**: Animated shining border effect
|
||||
- **Key Props**: `color` array, `borderWidth`, `duration`
|
||||
- **Use Case**: Premium cards, CTAs, feature boxes
|
||||
|
||||
### 12. **Magic Card**
|
||||
- **Purpose**: Spotlight effect following mouse cursor with border highlights
|
||||
- **Key Props**:
|
||||
- `gradientSize` (200 default)
|
||||
- `gradientColor` (#262626 default)
|
||||
- `gradientOpacity` (0.8)
|
||||
- `gradientFrom`/`gradientTo` (border colors)
|
||||
- **Use Case**: Interactive feature cards, pricing tables
|
||||
|
||||
## Background Components
|
||||
|
||||
### 13. **Grid Beams**
|
||||
- **Purpose**: Dynamic grid background with animated light beams
|
||||
- **Key Props**:
|
||||
- `gridSize` (40px default)
|
||||
- `gridColor` (rgba)
|
||||
- `rayCount` (15 default)
|
||||
- `rayOpacity` (0.35)
|
||||
- `raySpeed`, `rayLength` (45vh)
|
||||
- `gridFadeStart`/`gridFadeEnd` (%)
|
||||
- `backgroundColor`
|
||||
- **Use Case**: Hero sections, feature backgrounds, immersive layouts
|
||||
|
||||
### 14. **Warp Background**
|
||||
- **Purpose**: Warped perspective grid effect
|
||||
- **Key Props**:
|
||||
- `perspective` (depth)
|
||||
- `beamsPerSide` (4 default)
|
||||
- `beamSize` (thickness)
|
||||
- `beamDuration` (speed)
|
||||
- `gridColor`
|
||||
- **Use Case**: Futuristic hero sections, tech-focused pages
|
||||
|
||||
### 15. **Dot Pattern**
|
||||
- **Purpose**: Customizable dotted background pattern (SVG)
|
||||
- **Key Props**: `width`, `height`, `cx`, `cy`, `cr` (dot radius)
|
||||
- **Effects**: Supports glow effects
|
||||
- **Use Case**: Subtle backgrounds, section dividers
|
||||
|
||||
## Interactive Components
|
||||
|
||||
### 16. **Dock**
|
||||
- **Purpose**: macOS-style dock with magnification effect
|
||||
- **Key Props**:
|
||||
- `iconMagnification` (zoom amount)
|
||||
- `iconDistance` (hover range)
|
||||
- `direction` (middle, start, end)
|
||||
- **Child Component**: `DockIcon`
|
||||
- **Use Case**: Navigation, tool showcases, social links
|
||||
|
||||
### 17. **Scratch To Reveal**
|
||||
- **Purpose**: Interactive scratch-off effect revealing hidden content
|
||||
- **Key Props**:
|
||||
- `width`, `height`
|
||||
- `minScratchPercentage` (50 default, completion threshold)
|
||||
- `onComplete` callback
|
||||
- `gradientColors`
|
||||
- **Use Case**: Interactive reveals, gamification, teasers
|
||||
|
||||
### 18. **Highlighter**
|
||||
- **Purpose**: Animated text highlighting and underlining
|
||||
- **Key Props**:
|
||||
- `color`
|
||||
- `strokeWidth`
|
||||
- `action` (underline, highlight)
|
||||
- `animationDuration`
|
||||
- `iterations`, `padding`
|
||||
- **Use Case**: Emphasis on key phrases, call-outs
|
||||
|
||||
## Layout/Display Components
|
||||
|
||||
### 19. **Marquee**
|
||||
- **Purpose**: Scrolling content (horizontal or vertical)
|
||||
- **Key Props**:
|
||||
- `reverse` (direction)
|
||||
- `pauseOnHover`
|
||||
- `vertical` (orientation)
|
||||
- `repeat` (count)
|
||||
- **Effects**: 3D perspective option
|
||||
- **Use Case**: Testimonials, logo clouds, infinite scrollers
|
||||
|
||||
### 20. **Safari**
|
||||
- **Purpose**: Safari browser mockup for showcasing
|
||||
- **Key Props**:
|
||||
- `url` (address bar)
|
||||
- `imageSrc` or `videoSrc`
|
||||
- `width` (1203 default), `height` (753)
|
||||
- `mode` (default, simple)
|
||||
- **Use Case**: Product demos, website previews
|
||||
|
||||
### 21. **Bento Grid**
|
||||
- **Purpose**: Grid layout for organizing content
|
||||
- **Use Case**: Feature showcases, portfolios
|
||||
|
||||
### 22. **Avatar Circles**
|
||||
- **Purpose**: Overlapping circles of avatars
|
||||
- **Key Props**: `numPeople` (99 default, shown in last circle)
|
||||
- **Use Case**: Social proof, team displays, user counts
|
||||
|
||||
## Timeline Components
|
||||
|
||||
### 23. **Arc Timeline**
|
||||
- **Purpose**: Curved timeline visualizing milestones
|
||||
- **Key Props**:
|
||||
- `data` (array of {time, title})
|
||||
- `arcConfig`:
|
||||
- `circleWidth` (5000 default)
|
||||
- `angleBetweenMinorSteps` (0.35)
|
||||
- `lineCountFillBetweenSteps` (10)
|
||||
- `boundaryPlaceholderLinesCount` (50)
|
||||
- `defaultActiveStep` ({time, stepIndex})
|
||||
- **Use Case**: Project roadmaps, product evolution, version history
|
||||
|
||||
## Button Components
|
||||
|
||||
### 24. **Shiny Button**
|
||||
- **Purpose**: Button with shiny effect
|
||||
- **Features**: Dark/light mode support
|
||||
- **Use Case**: Primary CTAs, important actions
|
||||
|
||||
### 25. **Pulsating Button**
|
||||
- **Purpose**: Button with pulsing wave animation
|
||||
- **Key Props**: `pulseColor` (RGB), `duration`
|
||||
- **Use Case**: Attention-grabbing CTAs, urgent actions
|
||||
|
||||
### 26. **Rainbow Button**
|
||||
- **Purpose**: Rainbow gradient button effect
|
||||
- **Variants**: Default, Outline
|
||||
- **Use Case**: Premium CTAs, playful actions
|
||||
|
||||
## Theme Components
|
||||
|
||||
### 27. **Animated Theme Toggler**
|
||||
- **Purpose**: Smooth animated light/dark mode toggle
|
||||
- **Built With**: Tailwind CSS
|
||||
- **Use Case**: Theme switching UI
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Via CLI (recommended)
|
||||
npx shadcn-ui@latest add [component-name]
|
||||
|
||||
# Manual installation
|
||||
npm install magicui
|
||||
# or
|
||||
yarn add magicui
|
||||
```
|
||||
|
||||
## Component Categories Summary
|
||||
|
||||
| Category | Components | Best For |
|
||||
|----------|-----------|----------|
|
||||
| **Text Animation** | Animated List, Text Animate, Flip Text, Morphing Text, Word Rotate, Aurora Text | Headlines, feature lists, dynamic content |
|
||||
| **Visual Effects** | Orbiting Circles, Particles, Confetti, Border Beam, Shine Border, Magic Card | Visual interest, interactivity, emphasis |
|
||||
| **Backgrounds** | Grid Beams, Warp Background, Dot Pattern | Hero sections, immersive layouts |
|
||||
| **Interactive** | Dock, Scratch To Reveal, Highlighter | User engagement, gamification |
|
||||
| **Layout** | Marquee, Safari, Bento Grid, Avatar Circles | Content organization, showcases |
|
||||
| **Timeline** | Arc Timeline | Roadmaps, history, progression |
|
||||
| **Buttons** | Shiny Button, Pulsating Button, Rainbow Button | CTAs, actions |
|
||||
| **Utility** | Animated Theme Toggler | UI controls |
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Magic UI components focus on:
|
||||
- **Delight**: Unexpected animations that create joy
|
||||
- **Fluidity**: Smooth, natural motion
|
||||
- **Performance**: Optimized for web performance
|
||||
- **Flexibility**: Highly customizable via props
|
||||
- **Modern**: Built with React, Tailwind CSS, Framer Motion
|
||||
|
||||
## Use Case Recommendations by Landing Page Section
|
||||
|
||||
### Hero Sections
|
||||
- Grid Beams or Warp Background (background)
|
||||
- Aurora Text or Morphing Text (headline)
|
||||
- Pulsating Button or Rainbow Button (CTA)
|
||||
- Orbiting Circles (tech visualization)
|
||||
|
||||
### Feature Showcases
|
||||
- Bento Grid (layout)
|
||||
- Magic Card (individual features)
|
||||
- Animated List (feature details)
|
||||
- Highlighter (emphasis)
|
||||
|
||||
### Social Proof
|
||||
- Marquee (testimonials/logos)
|
||||
- Avatar Circles (user counts)
|
||||
|
||||
### Product Demos
|
||||
- Safari (browser mockups)
|
||||
- Border Beam or Shine Border (emphasis)
|
||||
|
||||
### Roadmaps/Progress
|
||||
- Arc Timeline (milestones)
|
||||
|
||||
### Interactive Elements
|
||||
- Scratch To Reveal (teasers)
|
||||
- Dock (navigation/tools)
|
||||
- Confetti (celebrations)
|
||||
@@ -0,0 +1,471 @@
|
||||
# Magic UI Landing Page Creative Ideas
|
||||
|
||||
Creative applications of Magic UI components for each section of the claude-mem landing page.
|
||||
|
||||
## Section 1: HERO - "Claude Never Forgets"
|
||||
|
||||
### Idea 1: "Fading Memory" Effect ⭐ WINNER
|
||||
**Components**: Morphing Text, Grid Beams, Orbiting Circles, Scratch To Reveal
|
||||
|
||||
**Vision**:
|
||||
- **Headline**: Morphing Text rotating between:
|
||||
- "Claude Never Forgets"
|
||||
- "Claude Always Remembers"
|
||||
- "Claude Learns Forever"
|
||||
- **Background**: Grid Beams in blue/purple gradient representing the "memory grid"
|
||||
- **Central Visual**: Orbiting Circles around a central "brain/database" icon
|
||||
- Inner orbit: File icons (representing code files)
|
||||
- Middle orbit: Lightbulb icons (decisions)
|
||||
- Outer orbit: Bug icons (fixes)
|
||||
- **Problem Statement**: Scratch To Reveal overlay
|
||||
- User must scratch to reveal: "Every /clear wipes Claude's memory"
|
||||
- Makes the pain point visceral and interactive
|
||||
- **CTA**: Pulsating Button "Give Claude a Memory"
|
||||
|
||||
### Idea 2: "Memory Timeline" Hero
|
||||
**Components**: Warp Background, Arc Timeline, Aurora Text, Word Rotate
|
||||
|
||||
**Vision**:
|
||||
- Warp Background creating perspective depth
|
||||
- Arc Timeline showing "Session 1 → Session 2 → Session 3" with memories persisting across
|
||||
- Aurora Text for main headline with flowing gradient
|
||||
- Word Rotate cycling pain points: "Repeating" → "Explaining" → "Re-discovering" → "Forgetting"
|
||||
|
||||
### Idea 3: "Brain Storage" Visualization
|
||||
**Components**: Particles, Magic Card, Highlighter, Pulsating Button
|
||||
|
||||
**Vision**:
|
||||
- Particles background (subtle, neuron-like firing)
|
||||
- Central Magic Card with spotlight effect containing headline
|
||||
- Highlighter emphasizing "Never" and "Forgets" with animated underlines
|
||||
- Pulsating Button for CTA with urgency
|
||||
|
||||
---
|
||||
|
||||
## Section 2: BEFORE/AFTER Comparison
|
||||
|
||||
### Idea 1: "Split Screen Wipe" ⭐ WINNER
|
||||
**Components**: Safari, Text Animate, Border Beam, Scratch To Reveal
|
||||
|
||||
**Vision**:
|
||||
- Two Safari browser mockups side by side
|
||||
- **Left (Before)**: Conversation fades/blurs out showing memory loss
|
||||
- **Right (After)**: Sharp, persistent content with Border Beam highlighting
|
||||
- Text Animate with slideUp for conversations appearing sequentially
|
||||
- Optional: Scratch To Reveal over "Before" side to uncover the painful reality
|
||||
|
||||
### Idea 2: "Memory Decay Visualization"
|
||||
**Components**: Text Animate, Shine Border, Magic Card, Animated Theme Toggler
|
||||
|
||||
**Vision**:
|
||||
- Before side: Text with progressive fade-out animation (opacity decreasing)
|
||||
- After side: Text with Shine Border, glowing persistently
|
||||
- Magic Card on After side with spotlight following mouse
|
||||
- Animated Theme Toggler metaphorically switching "forgetting" → "remembering"
|
||||
|
||||
### Idea 3: "Conversation Replay"
|
||||
**Components**: Animated List, Border Beam, Highlighter
|
||||
|
||||
**Vision**:
|
||||
- Animated List showing conversation history
|
||||
- Before: List items fade out and disappear sequentially
|
||||
- After: List items persist, Border Beam appears on referenced items
|
||||
- Highlighter underlining key persistent information on After side
|
||||
|
||||
---
|
||||
|
||||
## Section 3: REAL EXAMPLES (3 Scenarios)
|
||||
|
||||
### Idea 1: "Tabbed Experience"
|
||||
**Components**: Bento Grid, Magic Card, Text Animate, Flip Text, Confetti
|
||||
|
||||
**Vision**:
|
||||
- Bento Grid layout with 3 Magic Cards (one per scenario)
|
||||
- Each card has spotlight effect on hover
|
||||
- Text Animate (blurIn) for code examples appearing
|
||||
- Flip Text for headings revealing scenario names
|
||||
- Confetti burst when clicking through to third example (pattern recognized!)
|
||||
|
||||
### Idea 2: "Timeline Story" ⭐ WINNER
|
||||
**Components**: Arc Timeline, Orbiting Circles, Highlighter, Animated List
|
||||
|
||||
**Vision**:
|
||||
- Arc Timeline showing progression: "Monday Session" → "Wednesday Session"
|
||||
- Each timeline node expands to show the scenario details
|
||||
- Orbiting Circles around active node showing:
|
||||
- Files involved (icons in orbit)
|
||||
- Decisions made (decision icons)
|
||||
- Bugs fixed (bug icons)
|
||||
- Highlighter emphasizing key remembered details in the expanded content
|
||||
- Animated List revealing the bulleted "Claude remembers" items
|
||||
|
||||
### Idea 3: "Memory Card Flip"
|
||||
**Components**: Scratch To Reveal, Magic Card, Pulsating Button, Word Rotate
|
||||
|
||||
**Vision**:
|
||||
- Three cards with Scratch To Reveal overlay
|
||||
- Scratch to reveal what Claude remembers from previous sessions
|
||||
- Magic Card underneath with gradient borders
|
||||
- Pulsating Button to cycle through scenarios
|
||||
- Word Rotate showing memory types: "Patterns" → "Decisions" → "Context" → "Architecture"
|
||||
|
||||
---
|
||||
|
||||
## Section 4: HOW IT WORKS (Pipeline)
|
||||
|
||||
### Idea 1: "Animated Data Flow"
|
||||
**Components**: Magic Card, Orbiting Circles, Border Beam, Text Animate, Particles
|
||||
|
||||
**Vision**:
|
||||
- Three connected Magic Cards showing pipeline stages:
|
||||
1. "You code with Claude today"
|
||||
2. "claude-mem captures & compresses"
|
||||
3. "Tomorrow, Claude starts with context"
|
||||
- Orbiting Circles representing data flowing between stages
|
||||
- Border Beam animating along connections between cards
|
||||
- Text Animate (slideUp) for each stage description on scroll
|
||||
- Particles flowing from one stage to the next
|
||||
|
||||
### Idea 2: "Arc Timeline as Process"
|
||||
**Components**: Arc Timeline, Orbiting Circles, Warp Background, Animated List, Shine Border
|
||||
|
||||
**Vision**:
|
||||
- Arc Timeline showing 5 steps of memory system
|
||||
- Each node has Orbiting Circles showing components (hooks, worker, DB, MCP, context)
|
||||
- Warp Background creating depth
|
||||
- Animated List revealing sub-bullets under each stage
|
||||
- Shine Border on active/hovered stage
|
||||
|
||||
### Idea 3: "Layered Depth Model" ⭐ WINNER
|
||||
**Components**: Grid Beams, Magic Card, Border Beam, Morphing Text, Highlighter
|
||||
|
||||
**Vision**:
|
||||
- Grid Beams background representing the "storage layer"
|
||||
- Three floating Magic Cards in z-space (perspective/depth):
|
||||
- Top layer: "Hooks capture"
|
||||
- Middle layer: "AI compresses"
|
||||
- Bottom layer: "Database stores"
|
||||
- Arrows with Border Beam animation showing data flow downward
|
||||
- Morphing Text showing state transformation: "Capturing" → "Compressing" → "Storing" → "Retrieving"
|
||||
- Highlighter emphasizing "Automatic. Zero effort. Always on."
|
||||
|
||||
---
|
||||
|
||||
## Section 5: WHAT GETS REMEMBERED (Feature List)
|
||||
|
||||
### Idea 1: "Checkbox Delight"
|
||||
**Components**: Animated List, Text Animate, Highlighter, Confetti, Magic Card
|
||||
|
||||
**Vision**:
|
||||
- Animated List of 6 checkmark items
|
||||
- Each item appears with Text Animate (blurIn by character)
|
||||
- Highlighter underlining key words: "Decisions", "Bugs Fixed", "Code Patterns", etc.
|
||||
- Confetti burst when all items are visible (celebration of completeness)
|
||||
- Magic Card container with subtle spotlight effect
|
||||
|
||||
### Idea 2: "Memory Bank Slots" ⭐ WINNER
|
||||
**Components**: Bento Grid, Scratch To Reveal, Shine Border, Orbiting Circles, Pulsating Button
|
||||
|
||||
**Vision**:
|
||||
- Bento Grid of 6 cards (one per memory type)
|
||||
- Each card has Scratch To Reveal to discover what gets saved (gamification!)
|
||||
- Shine Border appears around revealed cards
|
||||
- Orbiting Circles showing example icons around each card type
|
||||
- Pulsating indicator on most important memory types
|
||||
|
||||
### Idea 3: "Collector Animation"
|
||||
**Components**: Magic Card, Border Beam, Flip Text, Aurora Text, Dot Pattern
|
||||
|
||||
**Vision**:
|
||||
- Six Magic Cards arranged in grid
|
||||
- Border Beam cascading through cards in sequence
|
||||
- Each card flips (Flip Text) to reveal icon + description
|
||||
- Aurora Text for section heading
|
||||
- Dot Pattern background with subtle glow on active items
|
||||
|
||||
---
|
||||
|
||||
## Section 6: POWERFUL SEARCH
|
||||
|
||||
### Idea 1: "Live Search Demo" ⭐ WINNER
|
||||
**Components**: Safari, Text Animate, Animated List, Highlighter, Border Beam, Morphing Text
|
||||
|
||||
**Vision**:
|
||||
- Safari browser mockup showing search interface
|
||||
- Text Animate typing out search queries with realistic timing:
|
||||
- "Find all database migrations..."
|
||||
- "What decisions about authentication..."
|
||||
- Animated List revealing search results sequentially
|
||||
- Highlighter emphasizing matched keywords in results
|
||||
- Border Beam around result cards
|
||||
- Morphing Text cycling search types: "Migrations" → "Decisions" → "Patterns" → "Bugs"
|
||||
|
||||
### Idea 2: "Search Radar"
|
||||
**Components**: Orbiting Circles, Magic Card, Particles, Text Animate, Grid Beams
|
||||
|
||||
**Vision**:
|
||||
- Orbiting Circles representing different search dimensions:
|
||||
- Inner orbit: File search
|
||||
- Middle orbit: Concept search
|
||||
- Outer orbit: Type/date filters
|
||||
- Central Magic Card with search query
|
||||
- Particles radiating outward to show search happening
|
||||
- Results appear with Text Animate (slideUp)
|
||||
- Grid Beams background representing indexed database
|
||||
|
||||
### Idea 3: "Memory Retrieval Visualization"
|
||||
**Components**: Warp Background, Aurora Text, Magic Card, Shine Border, Dock, Confetti
|
||||
|
||||
**Vision**:
|
||||
- Warp Background creating depth into "memory storage"
|
||||
- Search query in Aurora Text
|
||||
- Seven Magic Cards (one per search tool) with Shine Border
|
||||
- Dock component at bottom showing 7 search tool icons
|
||||
- Confetti when "perfect match" is found
|
||||
|
||||
---
|
||||
|
||||
## Section 7: THE NUMBERS (Metrics Table)
|
||||
|
||||
### Idea 1: "Counting Animation"
|
||||
**Components**: Safari, Text Animate, Aurora Text, Confetti, Border Beam
|
||||
|
||||
**Vision**:
|
||||
- Safari mockup showing comparison table
|
||||
- Text Animate for each metric value counting up
|
||||
- "Before" values in faded text
|
||||
- "After" values with Aurora Text effect (glowing)
|
||||
- Confetti when hovering over dramatic improvements
|
||||
- Border Beam highlighting entire "After" column
|
||||
|
||||
### Idea 2: "Progress Bar Transformation" ⭐ WINNER
|
||||
**Components**: Magic Card, Morphing Text, Shine Border, Pulsating Button
|
||||
|
||||
**Vision**:
|
||||
- Five Magic Cards, one per metric
|
||||
- Animated progress bars showing transformation:
|
||||
- Before (low/red) → After (high/green)
|
||||
- Visual representation of improvement
|
||||
- Morphing Text cycling through metrics
|
||||
- Shine Border on cards with biggest improvements
|
||||
- Pulsating Button: "See the Difference"
|
||||
|
||||
### Idea 3: "Flip Cards Reveal"
|
||||
**Components**: Scratch To Reveal, Magic Card, Highlighter, Animated List
|
||||
|
||||
**Vision**:
|
||||
- Five cards with Scratch To Reveal
|
||||
- Scratch "Before" to reveal "After" metrics
|
||||
- Each card has Magic Card spotlight effect
|
||||
- Highlighter emphasizing: "Never", "0 seconds", "Rare"
|
||||
- Animated List showing additional benefits below table
|
||||
|
||||
---
|
||||
|
||||
## Section 8: INSTALLATION (Quick Start)
|
||||
|
||||
### Idea 1: "Copy-Paste Delight" ⭐ WINNER
|
||||
**Components**: Safari, Text Animate, Shiny Button, Confetti, Animated List, Highlighter
|
||||
|
||||
**Vision**:
|
||||
- Safari mockup showing terminal window
|
||||
- Text Animate typing out commands with realistic timing (typewriter effect)
|
||||
- Shiny Button next to each command for copy
|
||||
- Confetti celebration when installation completes
|
||||
- Animated List showing 3 steps with checkmarks appearing
|
||||
- Highlighter emphasizing "2 minutes"
|
||||
|
||||
### Idea 2: "Progress Stepper"
|
||||
**Components**: Arc Timeline, Border Beam, Magic Card, Pulsating Button, Word Rotate
|
||||
|
||||
**Vision**:
|
||||
- Arc Timeline with 3 nodes: Clone → Add → Install
|
||||
- Each step expands to show code when active
|
||||
- Border Beam connecting steps as they complete
|
||||
- Magic Card for code blocks with spotlight
|
||||
- Pulsating Button for "Get Started"
|
||||
- Word Rotate: "Simple" → "Fast" → "Easy" → "Done"
|
||||
|
||||
### Idea 3: "Interactive Terminal"
|
||||
**Components**: Grid Beams, Safari, Text Animate, Orbiting Circles, Rainbow Button, Shine Border
|
||||
|
||||
**Vision**:
|
||||
- Grid Beams background (tech aesthetic)
|
||||
- Safari terminal mockup
|
||||
- Text Animate simulating command execution
|
||||
- Orbiting Circles showing installed components appearing
|
||||
- Rainbow Button for final "Installation Complete" CTA
|
||||
- Shine Border around success message
|
||||
|
||||
---
|
||||
|
||||
## Section 9: UNDER THE HOOD (Architecture)
|
||||
|
||||
### Idea 1: "System Diagram"
|
||||
**Components**: Bento Grid, Magic Card, Orbiting Circles, Border Beam, Dot Pattern, Text Animate, Highlighter
|
||||
|
||||
**Vision**:
|
||||
- Bento Grid showing 5 architecture components:
|
||||
- Hooks, Worker, Database, MCP, Context Injection
|
||||
- Each grid cell is a Magic Card with spotlight
|
||||
- Orbiting Circles showing data flow between components
|
||||
- Border Beam animating along connections
|
||||
- Dot Pattern background
|
||||
- Text Animate for each component description
|
||||
- Highlighter on "Zero maintenance. Just works."
|
||||
|
||||
### Idea 2: "Layered Stack" ⭐ WINNER
|
||||
**Components**: Magic Card, Warp Background, Morphing Text, Particles, Shine Border
|
||||
|
||||
**Vision**:
|
||||
- Five Magic Cards stacked with perspective (z-depth)
|
||||
- Each layer slides out on scroll to reveal architecture:
|
||||
1. Hooks (top)
|
||||
2. Worker Service
|
||||
3. SQLite Database (center)
|
||||
4. MCP Server
|
||||
5. Context Injection (bottom)
|
||||
- Warp Background creating depth
|
||||
- Morphing Text showing active layer: "Hooks" → "Worker" → "SQLite" → "MCP" → "Context"
|
||||
- Particles flowing between layers
|
||||
- Shine Border on active layer
|
||||
|
||||
### Idea 3: "Technical Orbits"
|
||||
**Components**: Orbiting Circles, Grid Beams, Magic Card, Text Animate
|
||||
|
||||
**Vision**:
|
||||
- Central "SQLite DB" icon
|
||||
- Orbiting Circles representing different systems:
|
||||
- Inner orbit: 5 hooks (SessionStart, UserPrompt, PostTool, Summary, SessionEnd)
|
||||
- Middle orbit: Worker service (PM2 process)
|
||||
- Outer orbit: MCP server (7 search tools)
|
||||
- Farthest orbit: Context injection
|
||||
- Grid Beams background
|
||||
- Magic Card for each orbit explanation
|
||||
- Text Animate (slideUp) for technical details
|
||||
|
||||
---
|
||||
|
||||
## Section 10: USE CASES (User Types)
|
||||
|
||||
### Idea 1: "User Journey Cards"
|
||||
**Components**: Bento Grid, Magic Card, Avatar Circles, Animated List, Flip Text, Border Beam
|
||||
|
||||
**Vision**:
|
||||
- Four Magic Cards in Bento Grid layout
|
||||
- Each card has different gradient colors (gradientFrom/To)
|
||||
- Avatar Circles showing user type icon
|
||||
- Animated List of benefits per user type
|
||||
- Flip Text for headings revealing user types
|
||||
- Border Beam highlighting active/hovered card
|
||||
|
||||
### Idea 2: "Role Selector" ⭐ WINNER
|
||||
**Components**: Dock, Magic Card, Text Animate, Highlighter, Confetti
|
||||
|
||||
**Vision**:
|
||||
- Dock component with 4 icons representing user types:
|
||||
- Solo Developer icon
|
||||
- Team icon
|
||||
- Learning/Student icon
|
||||
- Large Refactor icon
|
||||
- Click icon to expand that use case
|
||||
- Magic Card expands with spotlight effect
|
||||
- Text Animate revealing use case details
|
||||
- Highlighter emphasizing key benefits
|
||||
- Confetti celebration when selecting "your" use case (engagement!)
|
||||
|
||||
### Idea 3: "Story Scenarios"
|
||||
**Components**: Arc Timeline, Safari, Orbiting Circles, Morphing Text, Shine Border, Animated List
|
||||
|
||||
**Vision**:
|
||||
- Arc Timeline showing progression for each user type
|
||||
- Safari mockup showing actual usage scenario
|
||||
- Orbiting Circles around timeline nodes showing features being used
|
||||
- Morphing Text cycling through user types
|
||||
- Shine Border on selected use case
|
||||
- Animated List showing specific benefits
|
||||
|
||||
---
|
||||
|
||||
## Section 11: FAQ
|
||||
|
||||
### Idea 1: "Expandable Cards"
|
||||
**Components**: Magic Card, Text Animate, Border Beam, Highlighter, Dot Pattern, Animated List
|
||||
|
||||
**Vision**:
|
||||
- Six Magic Cards with questions visible
|
||||
- Click to expand with Text Animate (blurIn)
|
||||
- Border Beam appears on expanded card
|
||||
- Highlighter emphasizing key answer points
|
||||
- Dot Pattern background
|
||||
- Animated List for multi-point answers
|
||||
|
||||
### Idea 2: "Scratch to Answer" ⭐ WINNER
|
||||
**Components**: Scratch To Reveal, Confetti, Magic Card, Morphing Text
|
||||
|
||||
**Vision**:
|
||||
- Questions visible, answers hidden under scratch surface
|
||||
- Scratch To Reveal to see answers (highly engaging!)
|
||||
- Confetti on revealing particularly important answers (e.g., "Fully private")
|
||||
- Magic Card container with spotlight
|
||||
- Morphing Text cycling through common concerns: "Cost?" → "Speed?" → "Privacy?" → "Storage?"
|
||||
|
||||
### Idea 3: "Interactive Q&A"
|
||||
**Components**: Bento Grid, Border Beam, Shine Border, Text Animate, Pulsating Button, Avatar Circles
|
||||
|
||||
**Vision**:
|
||||
- Bento Grid of question cards
|
||||
- Hover triggers Border Beam
|
||||
- Click expands with Shine Border
|
||||
- Text Animate typing out answers
|
||||
- Pulsating Button for "More Questions?"
|
||||
- Avatar Circles showing "5,000+ developers trust claude-mem"
|
||||
|
||||
---
|
||||
|
||||
## BONUS: Testimonials Section (Not in Original)
|
||||
|
||||
### Idea 1: "Social Proof Marquee"
|
||||
**Components**: Marquee (3D), Magic Card, Avatar Circles, Highlighter, Shine Border
|
||||
|
||||
**Vision**:
|
||||
- Marquee component in 3D mode scrolling developer testimonials
|
||||
- Each testimonial in a Magic Card
|
||||
- Avatar Circles showing total developer count
|
||||
- Highlighter on impactful quote fragments
|
||||
- Shine Border around featured testimonial
|
||||
|
||||
---
|
||||
|
||||
## Component Usage Summary
|
||||
|
||||
| Component | Times Used (Winners) | Primary Purpose |
|
||||
|-----------|---------------------|-----------------|
|
||||
| **Magic Card** | 9 | Spotlight effects, containers |
|
||||
| **Text Animate** | 7 | Typing effects, reveals |
|
||||
| **Border Beam** | 6 | Connections, highlights |
|
||||
| **Highlighter** | 6 | Emphasis on key text |
|
||||
| **Animated List** | 5 | Sequential reveals |
|
||||
| **Confetti** | 5 | Celebrations, milestones |
|
||||
| **Shine Border** | 5 | Premium emphasis |
|
||||
| **Morphing Text** | 4 | Dynamic headlines |
|
||||
| **Orbiting Circles** | 4 | Data flow, ecosystems |
|
||||
| **Safari** | 4 | Browser mockups |
|
||||
| **Scratch To Reveal** | 4 | Interactive discovery |
|
||||
| **Pulsating Button** | 4 | CTAs |
|
||||
| **Grid Beams** | 3 | Tech backgrounds |
|
||||
| **Warp Background** | 3 | Depth, perspective |
|
||||
| **Arc Timeline** | 2 | Progress, history |
|
||||
| **Dock** | 1 | Navigation selector |
|
||||
| **Aurora Text** | 1 | Premium headlines |
|
||||
| **Bento Grid** | 1 | Layout organization |
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Show, Don't Tell**: Use animations to demonstrate concepts (memory persistence, data flow)
|
||||
2. **Interactive Discovery**: Scratch-to-reveal and interactive elements engage users
|
||||
3. **Visual Metaphors**: Orbiting circles for data flow, layers for architecture
|
||||
4. **Celebration**: Confetti at key moments creates joy
|
||||
5. **Progressive Disclosure**: Animated lists and timelines reveal information naturally
|
||||
6. **Spatial Depth**: Warp backgrounds and z-space create dimensional understanding
|
||||
7. **Consistent Magic**: Reuse components (Magic Card, Border Beam) for cohesion
|
||||
@@ -0,0 +1,389 @@
|
||||
# Landing Page Ideas - Ranked Analysis
|
||||
|
||||
Ranking criteria (1-10 scale):
|
||||
1. **Creativity**: How novel and unexpected is the approach?
|
||||
2. **Storytelling**: How effectively does it communicate the value?
|
||||
3. **Intuitive**: How easily will users understand it?
|
||||
4. **Feasibility**: How practical is implementation?
|
||||
5. **Delight**: How much joy does it create?
|
||||
|
||||
---
|
||||
|
||||
## HERO SECTION Rankings
|
||||
|
||||
### Idea 1: "Fading Memory" Effect ⭐ WINNER (42/50)
|
||||
- **Creativity**: 9/10 - Scratch-to-reveal pain point is unexpected
|
||||
- **Storytelling**: 9/10 - Morphing text and orbiting circles tell complete story
|
||||
- **Intuitive**: 8/10 - Orbiting content makes memory concept clear
|
||||
- **Feasibility**: 7/10 - Multiple complex components need coordination
|
||||
- **Delight**: 9/10 - Interactive scratch creates engagement
|
||||
|
||||
**Why it wins**: The scratch-to-reveal pain point makes the problem visceral. Orbiting circles create immediate visual understanding of what gets remembered.
|
||||
|
||||
### Idea 2: "Memory Timeline" Hero (39/50)
|
||||
- **Creativity**: 7/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 8/10
|
||||
- **Delight**: 7/10
|
||||
|
||||
### Idea 3: "Brain Storage" Visualization (36/50)
|
||||
- **Creativity**: 6/10
|
||||
- **Storytelling**: 7/10
|
||||
- **Intuitive**: 7/10
|
||||
- **Feasibility**: 9/10
|
||||
- **Delight**: 7/10
|
||||
|
||||
---
|
||||
|
||||
## BEFORE/AFTER COMPARISON Rankings
|
||||
|
||||
### Idea 1: "Split Screen Wipe" ⭐ WINNER (44/50)
|
||||
- **Creativity**: 8/10 - Clean comparison approach
|
||||
- **Storytelling**: 10/10 - Side-by-side is the clearest possible comparison
|
||||
- **Intuitive**: 10/10 - Immediately obvious what's different
|
||||
- **Feasibility**: 8/10 - Safari mockups are well-supported
|
||||
- **Delight**: 8/10 - Fade vs persist is satisfying
|
||||
|
||||
**Why it wins**: Safari browser mockups provide immediate familiarity. The contrast between fading (Before) and persistent with Border Beam (After) is crystal clear storytelling.
|
||||
|
||||
### Idea 2: "Memory Decay Visualization" (40/50)
|
||||
- **Creativity**: 9/10
|
||||
- **Storytelling**: 9/10
|
||||
- **Intuitive**: 8/10
|
||||
- **Feasibility**: 7/10
|
||||
- **Delight**: 7/10
|
||||
|
||||
### Idea 3: "Conversation Replay" (40/50)
|
||||
- **Creativity**: 7/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 9/10
|
||||
- **Delight**: 7/10
|
||||
|
||||
---
|
||||
|
||||
## REAL EXAMPLES Rankings
|
||||
|
||||
### Idea 2: "Timeline Story" ⭐ WINNER (44/50)
|
||||
- **Creativity**: 9/10 - Arc timeline for session progression is novel
|
||||
- **Storytelling**: 10/10 - Timeline naturally shows progression over time
|
||||
- **Intuitive**: 9/10 - Timeline metaphor is universally understood
|
||||
- **Feasibility**: 7/10 - Complex interaction between timeline and orbiting elements
|
||||
- **Delight**: 9/10 - Orbiting context around nodes is magical
|
||||
|
||||
**Why it wins**: Arc Timeline naturally communicates the "across sessions" aspect. Orbiting circles showing related context (files, decisions, bugs) is brilliant visual storytelling.
|
||||
|
||||
### Idea 1: "Tabbed Experience" (41/50)
|
||||
- **Creativity**: 7/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 9/10
|
||||
- **Delight**: 8/10
|
||||
|
||||
### Idea 3: "Memory Card Flip" (35/50)
|
||||
- **Creativity**: 8/10
|
||||
- **Storytelling**: 7/10
|
||||
- **Intuitive**: 6/10
|
||||
- **Feasibility**: 6/10
|
||||
- **Delight**: 8/10
|
||||
|
||||
---
|
||||
|
||||
## HOW IT WORKS Rankings
|
||||
|
||||
### Idea 3: "Layered Depth Model" ⭐ WINNER (43/50)
|
||||
- **Creativity**: 9/10 - Spatial depth for understanding layers is clever
|
||||
- **Storytelling**: 9/10 - Visual depth communicates layered architecture
|
||||
- **Intuitive**: 10/10 - Layers immediately convey hierarchy and flow
|
||||
- **Feasibility**: 6/10 - Z-space perspective requires careful implementation
|
||||
- **Delight**: 9/10 - Morphing text showing transformation is satisfying
|
||||
|
||||
**Why it wins**: Depth/perspective creates intuitive understanding of the layered architecture. Morphing text showing state transformation ("Capturing" → "Storing") tells the story perfectly.
|
||||
|
||||
### Idea 1: "Animated Data Flow" (42/50)
|
||||
- **Creativity**: 8/10
|
||||
- **Storytelling**: 9/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 8/10
|
||||
- **Delight**: 8/10
|
||||
|
||||
### Idea 2: "Arc Timeline as Process" (37/50)
|
||||
- **Creativity**: 7/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 8/10
|
||||
- **Feasibility**: 7/10
|
||||
- **Delight**: 7/10
|
||||
|
||||
---
|
||||
|
||||
## WHAT GETS REMEMBERED Rankings
|
||||
|
||||
### Idea 2: "Memory Bank Slots" ⭐ WINNER (41/50)
|
||||
- **Creativity**: 9/10 - Memory bank metaphor with scratch-to-reveal
|
||||
- **Storytelling**: 8/10 - Discovery process tells the story
|
||||
- **Intuitive**: 8/10 - Bank slot metaphor is clear
|
||||
- **Feasibility**: 7/10 - Six scratch-to-reveal elements need optimization
|
||||
- **Delight**: 9/10 - Gamification through scratching is highly engaging
|
||||
|
||||
**Why it wins**: Scratch-to-reveal gamification makes exploring features fun. Discovering what gets saved creates memorable engagement.
|
||||
|
||||
### Idea 3: "Collector Animation" (41/50 - tied)
|
||||
- **Creativity**: 8/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 8/10
|
||||
- **Delight**: 8/10
|
||||
|
||||
### Idea 1: "Checkbox Delight" (40/50)
|
||||
- **Creativity**: 6/10
|
||||
- **Storytelling**: 7/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 10/10
|
||||
- **Delight**: 8/10
|
||||
|
||||
---
|
||||
|
||||
## POWERFUL SEARCH Rankings
|
||||
|
||||
### Idea 1: "Live Search Demo" ⭐ WINNER (44/50)
|
||||
- **Creativity**: 7/10 - Straightforward but effective
|
||||
- **Storytelling**: 10/10 - Actually showing search in action is perfect
|
||||
- **Intuitive**: 10/10 - Real search demo is immediately clear
|
||||
- **Feasibility**: 9/10 - Safari + Text Animate + Animated List is achievable
|
||||
- **Delight**: 8/10 - Watching search happen is satisfying
|
||||
|
||||
**Why it wins**: Showing actual search with real queries and results is the most effective storytelling. Users immediately understand the capability.
|
||||
|
||||
### Idea 2: "Search Radar" (39/50)
|
||||
- **Creativity**: 9/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 7/10
|
||||
- **Feasibility**: 6/10
|
||||
- **Delight**: 9/10
|
||||
|
||||
### Idea 3: "Memory Retrieval Visualization" (38/50)
|
||||
- **Creativity**: 8/10
|
||||
- **Storytelling**: 7/10
|
||||
- **Intuitive**: 8/10
|
||||
- **Feasibility**: 8/10
|
||||
- **Delight**: 7/10
|
||||
|
||||
---
|
||||
|
||||
## THE NUMBERS Rankings
|
||||
|
||||
### Idea 2: "Progress Bar Transformation" ⭐ WINNER (45/50)
|
||||
- **Creativity**: 8/10 - Progress bars are familiar but effective
|
||||
- **Storytelling**: 10/10 - Visual transformation from bad to good is powerful
|
||||
- **Intuitive**: 10/10 - Everyone understands progress bars
|
||||
- **Feasibility**: 8/10 - Animated progress bars are well-supported
|
||||
- **Delight**: 9/10 - Watching bars transform is satisfying
|
||||
|
||||
**Why it wins**: Visual transformation of progress bars (red/low → green/high) is incredibly effective storytelling. Everyone intuitively understands the improvement.
|
||||
|
||||
### Idea 1: "Counting Animation" (42/50)
|
||||
- **Creativity**: 7/10
|
||||
- **Storytelling**: 9/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 9/10
|
||||
- **Delight**: 8/10
|
||||
|
||||
### Idea 3: "Flip Cards Reveal" (39/50)
|
||||
- **Creativity**: 8/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 7/10
|
||||
- **Feasibility**: 7/10
|
||||
- **Delight**: 9/10
|
||||
|
||||
---
|
||||
|
||||
## INSTALLATION Rankings
|
||||
|
||||
### Idea 1: "Copy-Paste Delight" ⭐ WINNER (44/50)
|
||||
- **Creativity**: 7/10 - Simple but right
|
||||
- **Storytelling**: 8/10 - Shows exactly what to do
|
||||
- **Intuitive**: 10/10 - Terminal + copy buttons is universal pattern
|
||||
- **Feasibility**: 10/10 - Very achievable
|
||||
- **Delight**: 9/10 - Confetti celebration seals the deal
|
||||
|
||||
**Why it wins**: Simplicity wins for installation instructions. Confetti celebration when done creates satisfying completion.
|
||||
|
||||
### Idea 2: "Progress Stepper" (42/50)
|
||||
- **Creativity**: 8/10
|
||||
- **Storytelling**: 9/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 8/10
|
||||
- **Delight**: 8/10
|
||||
|
||||
### Idea 3: "Interactive Terminal" (41/50)
|
||||
- **Creativity**: 9/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 8/10
|
||||
- **Feasibility**: 7/10
|
||||
- **Delight**: 9/10
|
||||
|
||||
---
|
||||
|
||||
## UNDER THE HOOD Rankings
|
||||
|
||||
### Idea 2: "Layered Stack" ⭐ WINNER (44/50)
|
||||
- **Creativity**: 9/10 - Layers with z-depth is clever
|
||||
- **Storytelling**: 9/10 - Stacking shows dependency hierarchy
|
||||
- **Intuitive**: 10/10 - Stack metaphor is perfect for architecture
|
||||
- **Feasibility**: 7/10 - Perspective effects require work
|
||||
- **Delight**: 9/10 - Slides revealing layers is satisfying
|
||||
|
||||
**Why it wins**: Layered stack with perspective visually explains the architecture hierarchy perfectly. Each layer sliding out to reveal itself tells the dependency story.
|
||||
|
||||
### Idea 3: "Technical Orbits" (43/50)
|
||||
- **Creativity**: 10/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 8/10
|
||||
- **Delight**: 8/10
|
||||
|
||||
### Idea 1: "System Diagram" (40/50)
|
||||
- **Creativity**: 7/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 9/10
|
||||
- **Delight**: 7/10
|
||||
|
||||
---
|
||||
|
||||
## USE CASES Rankings
|
||||
|
||||
### Idea 2: "Role Selector" ⭐ WINNER (46/50)
|
||||
- **Creativity**: 9/10 - Dock as role selector is novel
|
||||
- **Storytelling**: 9/10 - Interactive selection tells personalized stories
|
||||
- **Intuitive**: 10/10 - Dock interface is familiar and clear
|
||||
- **Feasibility**: 8/10 - Dock + expanding cards is achievable
|
||||
- **Delight**: 10/10 - Confetti for "your" use case is pure joy
|
||||
|
||||
**Why it wins**: Interactive Dock selector with confetti when you find "your" use case creates personal connection and delight. Highest delight score overall!
|
||||
|
||||
### Idea 3: "Story Scenarios" (41/50)
|
||||
- **Creativity**: 8/10
|
||||
- **Storytelling**: 10/10
|
||||
- **Intuitive**: 8/10
|
||||
- **Feasibility**: 7/10
|
||||
- **Delight**: 8/10
|
||||
|
||||
### Idea 1: "User Journey Cards" (40/50)
|
||||
- **Creativity**: 7/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 9/10
|
||||
- **Delight**: 7/10
|
||||
|
||||
---
|
||||
|
||||
## FAQ Rankings
|
||||
|
||||
### Idea 2: "Scratch to Answer" ⭐ WINNER (42/50)
|
||||
- **Creativity**: 9/10 - Making FAQ interactive is fresh
|
||||
- **Storytelling**: 8/10 - Discovery process engages
|
||||
- **Intuitive**: 8/10 - Scratch metaphor is understood
|
||||
- **Feasibility**: 7/10 - Multiple scratch elements need optimization
|
||||
- **Delight**: 10/10 - Scratching makes FAQs fun!
|
||||
|
||||
**Why it wins**: Scratch-to-reveal makes FAQs engaging instead of boring. Confetti on important answers (like "Fully private") creates moments of delight.
|
||||
|
||||
### Idea 3: "Interactive Q&A" (41/50)
|
||||
- **Creativity**: 7/10
|
||||
- **Storytelling**: 8/10
|
||||
- **Intuitive**: 9/10
|
||||
- **Feasibility**: 9/10
|
||||
- **Delight**: 8/10
|
||||
|
||||
### Idea 1: "Expandable Cards" (40/50)
|
||||
- **Creativity**: 6/10
|
||||
- **Storytelling**: 7/10
|
||||
- **Intuitive**: 10/10
|
||||
- **Feasibility**: 10/10
|
||||
- **Delight**: 7/10
|
||||
|
||||
---
|
||||
|
||||
## Overall Component Winners
|
||||
|
||||
### Most Effective Components
|
||||
1. **Magic Card** (9 winning sections) - Versatile spotlight effects
|
||||
2. **Text Animate** (7 sections) - Essential for reveals and typing effects
|
||||
3. **Border Beam** (6 sections) - Perfect for connections and highlights
|
||||
4. **Scratch To Reveal** (4 sections) - Highest delight factor
|
||||
5. **Confetti** (5 sections) - Celebration moments
|
||||
|
||||
### Highest Delight Components
|
||||
1. Scratch To Reveal - 10/10 in FAQ, 9/10 in Features
|
||||
2. Confetti - Creates joy at key moments
|
||||
3. Dock - 10/10 for use case selection
|
||||
4. Orbiting Circles - 9/10 for data visualization
|
||||
|
||||
### Best Storytelling Components
|
||||
1. Arc Timeline - Perfect for progression narratives
|
||||
2. Safari - Immediately familiar, great for demos
|
||||
3. Progress Bars - Universal understanding of improvement
|
||||
4. Layered Stack - Architecture hierarchy storytelling
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
### High Priority (Core Experience)
|
||||
1. **Hero** - First impression is critical
|
||||
2. **Before/After** - Core value proposition
|
||||
3. **Installation** - Conversion point
|
||||
4. **The Numbers** - Proof of value
|
||||
|
||||
### Medium Priority (Supporting Narrative)
|
||||
1. **Real Examples** - Concrete use cases
|
||||
2. **How It Works** - Understanding the system
|
||||
3. **Powerful Search** - Feature highlight
|
||||
|
||||
### Lower Priority (Deep Dive)
|
||||
1. **What Gets Remembered** - Feature details
|
||||
2. **Under The Hood** - Technical deep dive
|
||||
3. **Use Cases** - Audience segmentation
|
||||
4. **FAQ** - Support content
|
||||
|
||||
---
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
### Performance Concerns
|
||||
- **Scratch To Reveal**: 4 instances across page - needs optimization
|
||||
- **Orbiting Circles**: Multiple orbits can be CPU intensive
|
||||
- **Particles**: Use sparingly, can impact performance
|
||||
- **Z-space/Perspective**: May have cross-browser issues
|
||||
|
||||
### Accessibility Considerations
|
||||
- **Scratch To Reveal**: Needs keyboard alternative
|
||||
- **Confetti**: Should respect `prefers-reduced-motion`
|
||||
- **Animations**: All should be pausable/stoppable
|
||||
- **Color Contrast**: Aurora/gradient text needs testing
|
||||
|
||||
### Browser Compatibility
|
||||
- **Safari Component**: Meta - using Safari to show Safari
|
||||
- **Grid Beams/Warp**: Check WebGL support
|
||||
- **Perspective transforms**: Test in Firefox, Safari
|
||||
|
||||
---
|
||||
|
||||
## Final Winning Lineup
|
||||
|
||||
1. **Hero**: Fading Memory (42/50)
|
||||
2. **Before/After**: Split Screen Wipe (44/50)
|
||||
3. **Real Examples**: Timeline Story (44/50)
|
||||
4. **How It Works**: Layered Depth Model (43/50)
|
||||
5. **What Gets Remembered**: Memory Bank Slots (41/50)
|
||||
6. **Powerful Search**: Live Search Demo (44/50)
|
||||
7. **The Numbers**: Progress Bar Transformation (45/50) ⭐ HIGHEST SCORE
|
||||
8. **Installation**: Copy-Paste Delight (44/50)
|
||||
9. **Under The Hood**: Layered Stack (44/50)
|
||||
10. **Use Cases**: Role Selector (46/50) ⭐ HIGHEST DELIGHT
|
||||
11. **FAQ**: Scratch to Answer (42/50)
|
||||
|
||||
**Average Score**: 43.5/50 (87%)
|
||||
**Total Delight**: 94/110 (85%)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.1.1",
|
||||
"version": "4.2.1",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import P from"better-sqlite3";import{join as a,dirname as U,basename as q}from"path";import{homedir as R}from"os";import{existsSync as z,mkdirSync as w}from"fs";import{fileURLToPath as X}from"url";function M(){return typeof __dirname<"u"?__dirname:U(X(import.meta.url))}var F=M(),p=process.env.CLAUDE_MEM_DATA_DIR||a(R(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||a(R(),".claude"),ee=a(p,"archives"),se=a(p,"logs"),te=a(p,"trash"),re=a(p,"backups"),ne=a(p,"settings.json"),I=a(p,"claude-mem.db"),oe=a(u,"settings.json"),ie=a(u,"commands"),ae=a(u,"CLAUDE.md");function O(o){w(o,{recursive:!0})}function L(){return a(F,"..","..")}var _=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(_||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=_[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),d=_[e].padEnd(5),c=s.padEnd(6),m="";r?.correlationId?m=`[${r.correlationId}] `:r?.sessionId&&(m=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
|
||||
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let b="";if(r){let{sessionId:B,sdkSessionId:G,correlationId:$,...N}=r;Object.keys(N).length>0&&(b=` {${Object.entries(N).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let h=`[${i}] [${d}] [${c}] ${m}${t}${b}${E}`;e===3?console.error(h):console.log(h)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},v=new T;var l=class{db;constructor(){O(p),this.db=new P(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
|
||||
import P from"better-sqlite3";import{join as c,dirname as U,basename as V}from"path";import{homedir as f}from"os";import{existsSync as z,mkdirSync as w}from"fs";import{fileURLToPath as X}from"url";function M(){return typeof __dirname<"u"?__dirname:U(X(import.meta.url))}var F=M(),p=process.env.CLAUDE_MEM_DATA_DIR||c(f(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||c(f(),".claude"),ee=c(p,"archives"),se=c(p,"logs"),te=c(p,"trash"),re=c(p,"backups"),ne=c(p,"settings.json"),I=c(p,"claude-mem.db"),oe=c(u,"settings.json"),ie=c(u,"commands"),ae=c(u,"CLAUDE.md");function O(o){w(o,{recursive:!0})}function L(){return c(F,"..","..")}var l=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(l||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=l[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=l[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let _="";n!=null&&(this.level===0&&typeof n=="object"?_=`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let b="";if(r){let{sessionId:B,sdkSessionId:j,correlationId:$,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(R):console.log(R)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new T;var m=class{db;constructor(){O(p),this.db=new P(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(c=>c.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -129,7 +129,44 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`),this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
@@ -185,7 +222,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let d=JSON.parse(i.files_read);Array.isArray(d)&&d.forEach(c=>r.add(c))}catch{}if(i.files_modified)try{let d=JSON.parse(i.files_modified);Array.isArray(d)&&d.forEach(c=>n.add(c))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -212,15 +249,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),a=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),n);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(v.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
`).run(s,e).changes===0?(A.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -229,12 +268,28 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),i)}storeSummary(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
@@ -251,5 +306,5 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};import g from"path";import{existsSync as S}from"fs";import{spawn as H}from"child_process";var W=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),j=`http://127.0.0.1:${W}/health`;async function A(){try{return(await fetch(j,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await A())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=g.join(o,"plugin","scripts","worker-service.cjs");if(!S(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(o,"ecosystem.config.cjs"),t=g.join(o,"node_modules",".bin","pm2");if(!S(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!S(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=H(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await A())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function C(o){try{console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s}),await k()||console.error("[claude-mem cleanup] Worker not available - skipping HTTP cleanup");let r=new l,n=r.findActiveSDKSession(e);n||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),r.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:n.id,sdk_session_id:n.sdk_session_id,project:n.project,worker_port:n.worker_port});try{r.markSessionCompleted(n.id),console.error("[claude-mem cleanup] Session marked as completed in database")}catch(i){console.error("[claude-mem cleanup] Failed to mark session as completed:",i)}r.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(e){console.error("[claude-mem cleanup] Unexpected error in hook",{error:e.message,stack:e.stack,name:e.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}import{stdin as D}from"process";var f="";D.on("data",o=>f+=o);D.on("end",async()=>{try{let o=f.trim()?JSON.parse(f):void 0;await C(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};import S from"path";import{existsSync as g}from"fs";import{spawn as G}from"child_process";var H=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),W=`http://127.0.0.1:${H}/health`;async function v(){try{return(await fetch(W,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await v())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=S.join(o,"plugin","scripts","worker-service.cjs");if(!g(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=S.join(o,"ecosystem.config.cjs"),t=S.join(o,"node_modules",".bin","pm2");if(!g(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!g(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=G(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await v())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function k(o){try{console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s}),await C()||console.error("[claude-mem cleanup] Worker not available - skipping HTTP cleanup");let r=new m,n=r.findActiveSDKSession(e);n||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),r.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:n.id,sdk_session_id:n.sdk_session_id,project:n.project,worker_port:n.worker_port});try{r.markSessionCompleted(n.id),console.error("[claude-mem cleanup] Session marked as completed in database")}catch(i){console.error("[claude-mem cleanup] Failed to mark session as completed:",i)}r.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(e){console.error("[claude-mem cleanup] Unexpected error in hook",{error:e.message,stack:e.stack,name:e.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}import{stdin as D}from"process";var N="";D.on("data",o=>N+=o);D.on("end",async()=>{try{let o=N.trim()?JSON.parse(N):void 0;await k(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
+103
-36
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
import q from"path";import W from"better-sqlite3";import{join as l,dirname as M,basename as V}from"path";import{homedir as v}from"os";import{existsSync as Z,mkdirSync as X}from"fs";import{fileURLToPath as F}from"url";function P(){return typeof __dirname<"u"?__dirname:M(F(import.meta.url))}var j=P(),E=process.env.CLAUDE_MEM_DATA_DIR||l(v(),".claude-mem"),b=process.env.CLAUDE_CONFIG_DIR||l(v(),".claude"),se=l(E,"archives"),te=l(E,"logs"),re=l(E,"trash"),ne=l(E,"backups"),ie=l(E,"settings.json"),A=l(E,"claude-mem.db"),oe=l(b,"settings.json"),ae=l(b,"commands"),de=l(b,"CLAUDE.md");function y(p){X(p,{recursive:!0})}function D(){return l(j,"..","..")}var S=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(S||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=S[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let r=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&r.command){let n=r.command.length>50?r.command.substring(0,50)+"...":r.command;return`${e}(${n})`}if(e==="Read"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${e}(${n})`}if(e==="Edit"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${e}(${n})`}if(e==="Write"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${e}(${n})`}return e}catch{return e}}log(e,t,r,n,o){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),s=S[e].padEnd(5),c=t.padEnd(6),T="";n?.correlationId?T=`[${n.correlationId}] `:n?.sessionId&&(T=`[session-${n.sessionId}] `);let a="";o!=null&&(this.level===0&&typeof o=="object"?a=`
|
||||
`+JSON.stringify(o,null,2):a=" "+this.formatData(o));let m="";if(n){let{sessionId:_,sdkSessionId:f,correlationId:C,...h}=n;Object.keys(h).length>0&&(m=` {${Object.entries(h).map(([U,w])=>`${U}=${w}`).join(", ")}}`)}let u=`[${d}] [${s}] [${c}] ${T}${r}${m}${a}`;e===3?console.error(u):console.log(u)}debug(e,t,r,n){this.log(0,e,t,r,n)}info(e,t,r,n){this.log(1,e,t,r,n)}warn(e,t,r,n){this.log(2,e,t,r,n)}error(e,t,r,n){this.log(3,e,t,r,n)}dataIn(e,t,r,n){this.info(e,`\u2192 ${t}`,r,n)}dataOut(e,t,r,n){this.info(e,`\u2190 ${t}`,r,n)}success(e,t,r,n){this.info(e,`\u2713 ${t}`,r,n)}failure(e,t,r,n){this.error(e,`\u2717 ${t}`,r,n)}timing(e,t,r,n){this.info(e,`\u23F1 ${t}`,n,{duration:`${r}ms`})}},k=new N;var g=class{db;constructor(){y(E),this.db=new W(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
|
||||
import V from"path";import B from"better-sqlite3";import{join as E,dirname as P,basename as Q}from"path";import{homedir as x}from"os";import{existsSync as se,mkdirSync as G}from"fs";import{fileURLToPath as W}from"url";function j(){return typeof __dirname<"u"?__dirname:P(W(import.meta.url))}var H=j(),m=process.env.CLAUDE_MEM_DATA_DIR||E(x(),".claude-mem"),I=process.env.CLAUDE_CONFIG_DIR||E(x(),".claude"),re=E(m,"archives"),ne=E(m,"logs"),ie=E(m,"trash"),oe=E(m,"backups"),ae=E(m,"settings.json"),k=E(m,"claude-mem.db"),de=E(I,"settings.json"),pe=E(I,"commands"),ce=E(I,"CLAUDE.md");function U(p){G(p,{recursive:!0})}function w(){return E(H,"..","..")}var O=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(O||{}),L=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,i){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),n=O[e].padEnd(5),c=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let a="";i!=null&&(this.level===0&&typeof i=="object"?a=`
|
||||
`+JSON.stringify(i,null,2):a=" "+this.formatData(i));let u="";if(r){let{sessionId:F,sdkSessionId:D,correlationId:N,...l}=r;Object.keys(l).length>0&&(u=` {${Object.entries(l).map(([f,g])=>`${f}=${g}`).join(", ")}}`)}let T=`[${d}] [${n}] [${c}] ${_}${t}${u}${a}`;e===3?console.error(T):console.log(T)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},$=new L;var b=class{db;constructor(){U(m),this.db=new B(k),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
applied_at TEXT NOT NULL
|
||||
)
|
||||
`);let e=this.db.prepare("SELECT version FROM schema_versions ORDER BY version").all();(e.length>0?Math.max(...e.map(r=>r.version)):0)===0&&(console.error("[SessionStore] Initializing fresh database with migration004..."),this.db.exec(`
|
||||
`);let e=this.db.prepare("SELECT version FROM schema_versions ORDER BY version").all();(e.length>0?Math.max(...e.map(t=>t.version)):0)===0&&(console.error("[SessionStore] Initializing fresh database with migration004..."),this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(n=>n.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(c=>c.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(n=>n.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(c=>c.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -91,7 +91,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString()),console.error("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(n){throw this.db.exec("ROLLBACK"),n}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(8))return;if(this.db.pragma("table_info(observations)").some(n=>n.name==="title")){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString());return}console.error("[SessionStore] Adding hierarchical fields to observations table..."),this.db.exec(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString()),console.error("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(8))return;if(this.db.pragma("table_info(observations)").some(r=>r.name==="title")){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString());return}console.error("[SessionStore] Adding hierarchical fields to observations table..."),this.db.exec(`
|
||||
ALTER TABLE observations ADD COLUMN title TEXT;
|
||||
ALTER TABLE observations ADD COLUMN subtitle TEXT;
|
||||
ALTER TABLE observations ADD COLUMN facts TEXT;
|
||||
@@ -99,7 +99,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
ALTER TABLE observations ADD COLUMN concepts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_read TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_modified TEXT;
|
||||
`),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString()),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(9))return;let r=this.db.pragma("table_info(observations)").find(n=>n.name==="text");if(!r||r.notnull===0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString());return}console.error("[SessionStore] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
`),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString()),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(9))return;let t=this.db.pragma("table_info(observations)").find(r=>r.name==="text");if(!t||t.notnull===0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString());return}console.error("[SessionStore] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE observations_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -129,7 +129,44 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(n){throw this.db.exec("ROLLBACK"),n}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`),this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
@@ -137,7 +174,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentSummariesWithSessionInfo(e,t=3){return this.db.prepare(`
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
@@ -145,13 +182,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentSessionsWithStatus(e,t=3){return this.db.prepare(`
|
||||
`).all(e,s)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
@@ -168,7 +205,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
LIMIT ?
|
||||
)
|
||||
ORDER BY started_at_epoch ASC
|
||||
`).all(e,t)}getObservationsForSession(e){return this.db.prepare(`
|
||||
`).all(e,s)}getObservationsForSession(e){return this.db.prepare(`
|
||||
SELECT title, subtitle, type, prompt_number
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
@@ -181,11 +218,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getFilesForSession(e){let r=this.db.prepare(`
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),n=new Set,o=new Set;for(let d of r){if(d.files_read)try{let s=JSON.parse(d.files_read);Array.isArray(s)&&s.forEach(c=>n.add(c))}catch{}if(d.files_modified)try{let s=JSON.parse(d.files_modified);Array.isArray(s)&&s.forEach(c=>o.add(c))}catch{}}return{filesRead:Array.from(n),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,i=new Set;for(let d of t){if(d.files_read)try{let n=JSON.parse(d.files_read);Array.isArray(n)&&n.forEach(c=>r.add(c))}catch{}if(d.files_modified)try{let n=JSON.parse(d.files_modified);Array.isArray(n)&&n.forEach(c=>i.add(c))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(i)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -200,11 +237,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,t){this.db.prepare(`
|
||||
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(t,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
@@ -212,52 +249,82 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,t,r){let n=new Date,o=n.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,t,r,n.toISOString(),o).lastInsertRowid}updateSDKSessionId(e,t){return this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,i=r.getTime(),n=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),i);return n.lastInsertRowid===0||n.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:n.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(t,e).changes===0?(k.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
|
||||
`).run(s,e).changes===0?($.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,r,n){let o=new Date,d=o.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,i=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),i).lastInsertRowid}storeObservation(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,r.type,r.title,r.subtitle,JSON.stringify(r.facts),r.narrative,JSON.stringify(r.concepts),JSON.stringify(r.files_read),JSON.stringify(r.files_modified),n||null,o.toISOString(),d)}storeSummary(e,t,r,n){let o=new Date,d=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,i.toISOString(),d)}storeSummary(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,r.request,r.investigated,r.learned,r.completed,r.next_steps,r.notes,n||null,o.toISOString(),d)}markSessionCompleted(e){let t=new Date,r=t.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,i.toISOString(),d)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(t.toISOString(),r,e)}markSessionFailed(e){let t=new Date,r=t.getTime();this.db.prepare(`
|
||||
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(t.toISOString(),r,e)}cleanupOrphanedSessions(){let e=new Date,t=e.getTime();return this.db.prepare(`
|
||||
`).run(s.toISOString(),t,e)}cleanupOrphanedSessions(){let e=new Date,s=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};import R from"path";import{existsSync as I}from"fs";import{spawn as H}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),G=`http://127.0.0.1:${B}/health`;async function $(){try{return(await fetch(G,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function x(){try{if(await $())return!0;console.error("[claude-mem] Worker not responding, starting...");let p=D(),e=R.join(p,"plugin","scripts","worker-service.cjs");if(!I(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let t=R.join(p,"ecosystem.config.cjs"),r=R.join(p,"node_modules",".bin","pm2");if(!I(r))throw new Error(`PM2 binary not found at ${r}. This is a bundled dependency - try running: npm install`);if(!I(t))throw new Error(`PM2 ecosystem config not found at ${t}. Plugin installation may be corrupted.`);let n=H(r,["start",t],{detached:!0,stdio:"ignore",cwd:p});n.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),n.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(d=>setTimeout(d,500)),await $())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(p){return console.error(`[claude-mem] Failed to start worker: ${p.message}`),!1}}var i={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m"};function O(p,e=!1,t=!1){x();let r=p?.cwd??process.cwd(),n=r?q.basename(r):"unknown-project",o=new g;try{let d=o.getRecentSummariesWithSessionInfo(n,3);if(d.length===0)return e?`
|
||||
${i.bright}${i.cyan}\u{1F4DD} [${n}] recent context${i.reset}
|
||||
${i.gray}${"\u2500".repeat(60)}${i.reset}
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};import A from"path";import{existsSync as v}from"fs";import{spawn as Y}from"child_process";var q=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),K=`http://127.0.0.1:${q}/health`;async function M(){try{return(await fetch(K,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function X(){try{if(await M())return!0;console.error("[claude-mem] Worker not responding, starting...");let p=w(),e=A.join(p,"plugin","scripts","worker-service.cjs");if(!v(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=A.join(p,"ecosystem.config.cjs"),t=A.join(p,"node_modules",".bin","pm2");if(!v(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!v(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=Y(t,["start",s],{detached:!0,stdio:"ignore",cwd:p});r.on("error",i=>{throw new Error(`Failed to spawn PM2: ${i.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let i=0;i<3;i++)if(await new Promise(d=>setTimeout(d,500)),await M())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(p){return console.error(`[claude-mem] Failed to start worker: ${p.message}`),!1}}var o={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m"};function y(p,e=!1,s=!1){X();let t=p?.cwd??process.cwd(),r=t?V.basename(t):"unknown-project",i=new b;try{let d=i.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT sdk_session_id, request, learned, completed, next_steps, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 10
|
||||
)
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(r);if(d.length===0)return e?`
|
||||
${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset}
|
||||
${o.gray}${"\u2500".repeat(60)}${o.reset}
|
||||
|
||||
${i.dim}No previous summaries found for this project yet.${i.reset}
|
||||
`:`# [${n}] recent context
|
||||
${o.dim}No previous summaries found for this project yet.${o.reset}
|
||||
`:`# [${r}] recent context
|
||||
|
||||
No previous summaries found for this project yet.`;let s=[];if(t){if(e?(s.push(""),s.push(`${i.bright}${i.cyan}\u{1F4DD} [${n}] recent context${i.reset}`),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),s.push("")):(s.push(`# [${n}] recent context`),s.push("")),d.length>1){e?(s.push(`${i.bright}${i.dim}Previous Requests:${i.reset}`),s.push("")):(s.push("**Previous Requests:**"),s.push(""));for(let _=d.length-1;_>=1;_--){let f=d[_],h=new Date(f.created_at).toLocaleString();e?s.push(`${i.dim}\u2022 ${h}:${i.reset} ${f.request||"(no request)"}`):s.push(`- ${h}: ${f.request||"(no request)"}`)}e?(s.push(""),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),s.push("")):(s.push(""),s.push("---"),s.push(""))}let a=d[0];a.request&&(e?(s.push(`${i.bright}${i.yellow}Request:${i.reset} ${a.request}`),s.push("")):(s.push(`**Request:** ${a.request}`),s.push(""))),a.learned&&(e?(s.push(`${i.bright}${i.blue}Learned:${i.reset} ${a.learned}`),s.push("")):(s.push(`**Learned:** ${a.learned}`),s.push(""))),a.completed&&(e?(s.push(`${i.bright}${i.green}Completed:${i.reset} ${a.completed}`),s.push("")):(s.push(`**Completed:** ${a.completed}`),s.push(""))),a.next_steps&&(e?(s.push(`${i.bright}${i.magenta}Next Steps:${i.reset} ${a.next_steps}`),s.push("")):(s.push(`**Next Steps:** ${a.next_steps}`),s.push("")));let m=o.getFilesForSession(a.sdk_session_id);m.filesRead.length>0&&(e?s.push(`${i.dim}Files Read: ${m.filesRead.join(", ")}${i.reset}`):s.push(`**Files Read:** ${m.filesRead.join(", ")}`)),m.filesModified.length>0&&(e?s.push(`${i.dim}Files Modified: ${m.filesModified.join(", ")}${i.reset}`):s.push(`**Files Modified:** ${m.filesModified.join(", ")}`));let u=new Date(a.created_at).toLocaleString();return e?s.push(`${i.dim}Date: ${u}${i.reset}`):s.push(`**Date:** ${u}`),e&&(s.push(""),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`)),s.join(`
|
||||
`)}e?(s.push(""),s.push(`${i.bright}${i.cyan}\u{1F4DD} [${n}] recent context${i.reset}`),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`)):(s.push(`# [${n}] recent context`),s.push(""));let c=null,T=!0;for(let a of d){c!==null&&a.sdk_session_id!==c?e?(s.push(""),s.push(`${i.dim}${"\u2500".repeat(23)} New Session ${"\u2500".repeat(24)}${i.reset}`),s.push("")):(s.push(""),s.push("--- New Session ---"),s.push("")):T?e&&s.push(""):e?(s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),s.push("")):(s.push("---"),s.push("")),T=!1,a.request&&(e?(s.push(`${i.bright}${i.yellow}Request:${i.reset} ${a.request}`),s.push("")):(s.push(`**Request:** ${a.request}`),s.push(""))),a.learned&&(e?(s.push(`${i.bright}${i.blue}Learned:${i.reset} ${a.learned}`),s.push("")):(s.push(`**Learned:** ${a.learned}`),s.push(""))),a.completed&&(e?(s.push(`${i.bright}${i.green}Completed:${i.reset} ${a.completed}`),s.push("")):(s.push(`**Completed:** ${a.completed}`),s.push(""))),a.next_steps&&(e?(s.push(`${i.bright}${i.magenta}Next Steps:${i.reset} ${a.next_steps}`),s.push("")):(s.push(`**Next Steps:** ${a.next_steps}`),s.push("")));let u=o.getFilesForSession(a.sdk_session_id);u.filesRead.length>0&&(e?s.push(`${i.dim}Files Read: ${u.filesRead.join(", ")}${i.reset}`):s.push(`**Files Read:** ${u.filesRead.join(", ")}`)),u.filesModified.length>0&&(e?s.push(`${i.dim}Files Modified: ${u.filesModified.join(", ")}${i.reset}`):s.push(`**Files Modified:** ${u.filesModified.join(", ")}`));let _=new Date(a.created_at).toLocaleString();e?s.push(`${i.dim}Date: ${_}${i.reset}`):s.push(`**Date:** ${_}`),e||s.push(""),c=a.sdk_session_id}return e&&(s.push(""),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`)),s.join(`
|
||||
`)}finally{o.close()}}import{stdin as L}from"process";try{let p=process.argv.includes("--index");if(L.isTTY){let e=O(void 0,!0,p);console.log(e),process.exit(0)}else{let e="";L.on("data",t=>e+=t),L.on("end",()=>{let t=e.trim()?JSON.parse(e):void 0,n={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:O(t,!1,p)}};console.log(JSON.stringify(n)),process.exit(0)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)}
|
||||
No previous summaries found for this project yet.`;let n=[];e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`)):(n.push(`# [${r}] recent context`),n.push(""));let c=!0;for(let _=0;_<d.length;_++){let a=d[_],u=d.length-1-_,T=u===0,F=u>=1&&u<=3,D=u>3;if(c?e&&n.push(""):e?(n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push("---"),n.push("")),c=!1,D){a.request&&(e?(n.push(`${o.bright}${o.yellow}Request:${o.reset} ${a.request}`),n.push("")):(n.push(`**Request:** ${a.request}`),n.push("")));let l=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${l}${o.reset}`):(n.push(`**Date:** ${l}`),n.push(""));continue}if(a.request&&(e?(n.push(`${o.bright}${o.yellow}Request:${o.reset} ${a.request}`),n.push("")):(n.push(`**Request:** ${a.request}`),n.push(""))),T&&a.learned&&(e?(n.push(`${o.bright}${o.blue}Learned:${o.reset} ${a.learned}`),n.push("")):(n.push(`**Learned:** ${a.learned}`),n.push(""))),a.completed&&(e?(n.push(`${o.bright}${o.green}Completed:${o.reset} ${a.completed}`),n.push("")):(n.push(`**Completed:** ${a.completed}`),n.push(""))),T&&a.next_steps&&(e?(n.push(`${o.bright}${o.magenta}Next Steps:${o.reset} ${a.next_steps}`),n.push("")):(n.push(`**Next Steps:** ${a.next_steps}`),n.push(""))),T){let l=i.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(a.sdk_session_id),h=new Set,f=new Set;for(let g of l){if(g.files_read)try{let S=JSON.parse(g.files_read);Array.isArray(S)&&S.forEach(R=>h.add(R))}catch{}if(g.files_modified)try{let S=JSON.parse(g.files_modified);Array.isArray(S)&&S.forEach(R=>f.add(R))}catch{}}h.size>0&&(e?n.push(`${o.dim}Files Read: ${Array.from(h).join(", ")}${o.reset}`):n.push(`**Files Read:** ${Array.from(h).join(", ")}`)),f.size>0&&(e?n.push(`${o.dim}Files Modified: ${Array.from(f).join(", ")}${o.reset}`):n.push(`**Files Modified:** ${Array.from(f).join(", ")}`))}let N=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${N}${o.reset}`):n.push(`**Date:** ${N}`),e||n.push("")}return e&&(n.push(""),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`)),n.join(`
|
||||
`)}finally{i.close()}}import{stdin as C}from"process";try{let p=process.argv.includes("--index");if(C.isTTY){let e=y(void 0,!0,p);console.log(e),process.exit(0)}else{let e="";C.on("data",s=>e+=s),C.on("end",()=>{let s=e.trim()?JSON.parse(e):void 0,r={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:y(s,!1,p)}};console.log(JSON.stringify(r)),process.exit(0)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)}
|
||||
|
||||
+70
-15
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import K from"path";import $ from"better-sqlite3";import{join as p,dirname as M,basename as z}from"path";import{homedir as h}from"os";import{existsSync as te,mkdirSync as P}from"fs";import{fileURLToPath as F}from"url";function H(){return typeof __dirname<"u"?__dirname:M(F(import.meta.url))}var W=H(),m=process.env.CLAUDE_MEM_DATA_DIR||p(h(),".claude-mem"),T=process.env.CLAUDE_CONFIG_DIR||p(h(),".claude"),ne=p(m,"archives"),oe=p(m,"logs"),ie=p(m,"trash"),ae=p(m,"backups"),de=p(m,"settings.json"),O=p(m,"claude-mem.db"),pe=p(T,"settings.json"),ce=p(T,"commands"),ue=p(T,"CLAUDE.md");function I(o){P(o,{recursive:!0})}function L(){return p(W,"..","..")}var g=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(g||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=g[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),d=g[e].padEnd(5),a=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let l="";n!=null&&(this.level===0&&typeof n=="object"?l=`
|
||||
`+JSON.stringify(n,null,2):l=" "+this.formatData(n));let c="";if(r){let{sessionId:Y,sdkSessionId:q,correlationId:V,...R}=r;Object.keys(R).length>0&&(c=` {${Object.entries(R).map(([w,X])=>`${w}=${X}`).join(", ")}}`)}let u=`[${i}] [${d}] [${a}] ${E}${t}${c}${l}`;e===3?console.error(u):console.log(u)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new S;var _=class{db;constructor(){I(m),this.db=new $(O),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
|
||||
import Y from"path";import W from"better-sqlite3";import{join as p,dirname as M,basename as z}from"path";import{homedir as h}from"os";import{existsSync as te,mkdirSync as P}from"fs";import{fileURLToPath as F}from"url";function H(){return typeof __dirname<"u"?__dirname:M(F(import.meta.url))}var G=H(),u=process.env.CLAUDE_MEM_DATA_DIR||p(h(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(h(),".claude"),oe=p(u,"archives"),ne=p(u,"logs"),ie=p(u,"trash"),ae=p(u,"backups"),de=p(u,"settings.json"),O=p(u,"claude-mem.db"),pe=p(l,"settings.json"),ce=p(l,"commands"),Ee=p(l,"CLAUDE.md");function I(n){P(n,{recursive:!0})}function L(){return p(G,"..","..")}var T=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let c="";o!=null&&(this.level===0&&typeof o=="object"?c=`
|
||||
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let _="";if(r){let{sessionId:K,sdkSessionId:V,correlationId:q,...f}=r;Object.keys(f).length>0&&(_=` {${Object.entries(f).map(([w,X])=>`${w}=${X}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${_}${c}`;e===3?console.error(R):console.log(R)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new S;var m=class{db;constructor(){I(u),this.db=new W(O),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(a=>a.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(a=>a.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(a=>a.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -129,7 +129,44 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`),this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
@@ -185,7 +222,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let d=JSON.parse(i.files_read);Array.isArray(d)&&d.forEach(a=>r.add(a))}catch{}if(i.files_modified)try{let d=JSON.parse(i.files_modified);Array.isArray(d)&&d.forEach(a=>n.add(a))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,o=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -212,11 +249,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),a=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
@@ -229,17 +268,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),i)}storeSummary(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -251,4 +306,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function j(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(o,e,s={}){let t=j(o,e,s);return JSON.stringify(t)}import f from"path";import{existsSync as b}from"fs";import{spawn as B}from"child_process";var C=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),G=`http://127.0.0.1:${C}/health`;async function k(){try{return(await fetch(G,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=f.join(o,"plugin","scripts","worker-service.cjs");if(!b(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=f.join(o,"ecosystem.config.cjs"),t=f.join(o,"node_modules",".bin","pm2");if(!b(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!b(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=B(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await k())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}function y(){return C}async function x(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=K.basename(s);if(!await D())throw new Error("Worker service failed to start or become healthy");let i=new _;try{let d=i.findActiveSDKSession(e),a,E=!1;if(d){a=d.id;let c=i.incrementPromptCounter(a);console.error(`[new-hook] Continuing session ${a}, prompt #${c}`)}else{let c=i.findAnySDKSession(e);if(c){a=c.id,i.reactivateSession(a,t);let u=i.incrementPromptCounter(a);E=!0,console.error(`[new-hook] Reactivated session ${a}, prompt #${u}`)}else{a=i.createSDKSession(e,r,t);let u=i.incrementPromptCounter(a);E=!0,console.error(`[new-hook] Created new session ${a}, prompt #${u}`)}}let l=y();if(E){let c=await fetch(`http://127.0.0.1:${l}/sessions/${a}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let u=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${u}`)}}console.log(v("UserPromptSubmit",!0))}finally{i.close()}}import{stdin as U}from"process";var N="";U.on("data",o=>N+=o);U.on("end",async()=>{let o=N.trim()?JSON.parse(N):void 0;await x(o),process.exit(0)});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function B(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(n,e,s={}){let t=B(n,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as b}from"fs";import{spawn as $}from"child_process";var k=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),j=`http://127.0.0.1:${k}/health`;async function C(){try{return(await fetch(j,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=L(),e=g.join(n,"plugin","scripts","worker-service.cjs");if(!b(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(n,"ecosystem.config.cjs"),t=g.join(n,"node_modules",".bin","pm2");if(!b(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!b(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=$(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}function y(){return k}async function x(n){if(!n)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=n,r=Y.basename(s);if(!await D())throw new Error("Worker service failed to start or become healthy");let i=new m;try{let a=i.createSDKSession(e,r,t),d=i.incrementPromptCounter(a);i.saveUserPrompt(e,d,t),console.error(`[new-hook] Session ${a}, prompt #${d}`);let E=y(),c=await fetch(`http://127.0.0.1:${E}/sessions/${a}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let _=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${_}`)}console.log(v("UserPromptSubmit",!0))}finally{i.close()}}import{stdin as U}from"process";var N="";U.on("data",n=>N+=n);U.on("end",async()=>{let n=N.trim()?JSON.parse(N):void 0;await x(n),process.exit(0)});
|
||||
|
||||
+70
-15
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import H from"better-sqlite3";import{join as p,dirname as w,basename as Q}from"path";import{homedir as I}from"os";import{existsSync as se,mkdirSync as X}from"fs";import{fileURLToPath as M}from"url";function P(){return typeof __dirname<"u"?__dirname:w(M(import.meta.url))}var F=P(),u=process.env.CLAUDE_MEM_DATA_DIR||p(I(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||p(I(),".claude"),re=p(u,"archives"),oe=p(u,"logs"),ne=p(u,"trash"),ie=p(u,"backups"),ae=p(u,"settings.json"),L=p(u,"claude-mem.db"),de=p(g,"settings.json"),pe=p(g,"commands"),ce=p(g,"CLAUDE.md");function v(n){X(n,{recursive:!0})}function A(){return p(F,"..","..")}var f=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(f||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=f[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),i=f[e].padEnd(5),d=s.padEnd(6),l="";r?.correlationId?l=`[${r.correlationId}] `:r?.sessionId&&(l=`[session-${r.sessionId}] `);let c="";o!=null&&(this.level===0&&typeof o=="object"?c=`
|
||||
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let m="";if(r){let{sessionId:K,sdkSessionId:Y,correlationId:q,...O}=r;Object.keys(O).length>0&&(m=` {${Object.entries(O).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let h=`[${a}] [${i}] [${d}] ${l}${t}${m}${c}`;e===3?console.error(h):console.log(h)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},E=new S;var _=class{db;constructor(){v(u),this.db=new H(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
|
||||
import H from"better-sqlite3";import{join as p,dirname as w,basename as Q}from"path";import{homedir as I}from"os";import{existsSync as se,mkdirSync as M}from"fs";import{fileURLToPath as X}from"url";function P(){return typeof __dirname<"u"?__dirname:w(X(import.meta.url))}var F=P(),u=process.env.CLAUDE_MEM_DATA_DIR||p(I(),".claude-mem"),S=process.env.CLAUDE_CONFIG_DIR||p(I(),".claude"),re=p(u,"archives"),oe=p(u,"logs"),ne=p(u,"trash"),ie=p(u,"backups"),ae=p(u,"settings.json"),L=p(u,"claude-mem.db"),de=p(S,"settings.json"),pe=p(S,"commands"),ce=p(S,"CLAUDE.md");function A(n){M(n,{recursive:!0})}function v(){return p(F,"..","..")}var g=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(g||{}),b=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=g[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=g[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let E="";o!=null&&(this.level===0&&typeof o=="object"?E=`
|
||||
`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let _="";if(r){let{sessionId:K,sdkSessionId:Y,correlationId:V,...h}=r;Object.keys(h).length>0&&(_=` {${Object.entries(h).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let l=`[${i}] [${a}] [${d}] ${c}${t}${_}${E}`;e===3?console.error(l):console.log(l)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},m=new b;var T=class{db;constructor(){A(u),this.db=new H(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -129,7 +129,44 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`),this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
@@ -185,7 +222,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,o=new Set;for(let a of t){if(a.files_read)try{let i=JSON.parse(a.files_read);Array.isArray(i)&&i.forEach(d=>r.add(d))}catch{}if(a.files_modified)try{let i=JSON.parse(a.files_modified);Array.isArray(i)&&i.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,o=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -212,15 +249,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),a=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(E.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
`).run(s,e).changes===0?(m.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -229,17 +268,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let o=new Date,a=o.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),a)}storeSummary(e,s,t,r){let o=new Date,a=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),a)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -251,4 +306,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function W(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function T(n,e,s={}){let t=W(n,e,s);return JSON.stringify(t)}import b from"path";import{existsSync as N}from"fs";import{spawn as $}from"child_process";var j=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${j}/health`;async function k(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=A(),e=b.join(n,"plugin","scripts","worker-service.cjs");if(!N(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=b.join(n,"ecosystem.config.cjs"),t=b.join(n,"node_modules",".bin","pm2");if(!N(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!N(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=$(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(a=>setTimeout(a,500)),await k())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}var G=new Set(["ListMcpResourcesTool"]);async function y(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(G.has(s)){console.log(T("PostToolUse",!0));return}if(!await C())throw new Error("Worker service failed to start or become healthy");let a=new _,i=a.findActiveSDKSession(e);if(!i){a.close(),console.log(T("PostToolUse",!0));return}if(!i.worker_port)throw a.close(),E.error("HOOK","No worker port for session",{sessionId:i.id}),new Error("No worker port for session - session may not be properly initialized");let d=a.getPromptCounter(i.id);a.close();let l=E.formatTool(s,t);E.dataIn("HOOK",`PostToolUse: ${l}`,{sessionId:i.id,workerPort:i.worker_port});let c=await fetch(`http://127.0.0.1:${i.worker_port}/sessions/${i.id}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_output:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let m=await c.text();throw E.failure("HOOK","Failed to send observation",{sessionId:i.id,status:c.status},m),new Error(`Failed to send observation to worker: ${c.status} ${m}`)}E.debug("HOOK","Observation sent successfully",{sessionId:i.id,toolName:s}),console.log(T("PostToolUse",!0))}import{stdin as D}from"process";var R="";D.on("data",n=>R+=n);D.on("end",async()=>{let n=R.trim()?JSON.parse(R):void 0;await y(n),process.exit(0)});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function G(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function N(n,e,s={}){let t=G(n,e,s);return JSON.stringify(t)}import R from"path";import{existsSync as f}from"fs";import{spawn as W}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),$=`http://127.0.0.1:${B}/health`;async function C(){try{return(await fetch($,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=v(),e=R.join(n,"plugin","scripts","worker-service.cjs");if(!f(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=R.join(n,"ecosystem.config.cjs"),t=R.join(n,"node_modules",".bin","pm2");if(!f(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!f(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=W(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}var j=new Set(["ListMcpResourcesTool"]);async function D(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(j.has(s)){console.log(N("PostToolUse",!0));return}if(!await k())throw new Error("Worker service failed to start or become healthy");let i=new T,a=i.createSDKSession(e,"",""),d=i.getPromptCounter(a);i.close();let c=m.formatTool(s,t),E=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);m.dataIn("HOOK",`PostToolUse: ${c}`,{sessionId:a,workerPort:E});let _=await fetch(`http://127.0.0.1:${E}/sessions/${a}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_output:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(!_.ok){let l=await _.text();throw m.failure("HOOK","Failed to send observation",{sessionId:a,status:_.status},l),new Error(`Failed to send observation to worker: ${_.status} ${l}`)}m.debug("HOOK","Observation sent successfully",{sessionId:a,toolName:s}),console.log(N("PostToolUse",!0))}import{stdin as y}from"process";var O="";y.on("data",n=>O+=n);y.on("end",async()=>{let n=O.trim()?JSON.parse(O):void 0;await D(n),process.exit(0)});
|
||||
|
||||
+131
-43
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import H from"better-sqlite3";import{join as d,dirname as w,basename as J}from"path";import{homedir as I}from"os";import{existsSync as ee,mkdirSync as X}from"fs";import{fileURLToPath as M}from"url";function P(){return typeof __dirname<"u"?__dirname:w(M(import.meta.url))}var F=P(),c=process.env.CLAUDE_MEM_DATA_DIR||d(I(),".claude-mem"),_=process.env.CLAUDE_CONFIG_DIR||d(I(),".claude"),te=d(c,"archives"),re=d(c,"logs"),ne=d(c,"trash"),oe=d(c,"backups"),ie=d(c,"settings.json"),L=d(c,"claude-mem.db"),ae=d(_,"settings.json"),de=d(_,"commands"),pe=d(_,"CLAUDE.md");function A(o){X(o,{recursive:!0})}function v(){return d(F,"..","..")}var T=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(T||{}),g=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),p=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let l="";n!=null&&(this.level===0&&typeof n=="object"?l=`
|
||||
`+JSON.stringify(n,null,2):l=" "+this.formatData(n));let N="";if(r){let{sessionId:G,sdkSessionId:K,correlationId:Y,...O}=r;Object.keys(O).length>0&&(N=` {${Object.entries(O).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let h=`[${i}] [${a}] [${p}] ${E}${t}${N}${l}`;e===3?console.error(h):console.log(h)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},u=new g;var m=class{db;constructor(){A(c),this.db=new H(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
|
||||
import H from"better-sqlite3";import{join as p,dirname as w,basename as J}from"path";import{homedir as O}from"os";import{existsSync as ee,mkdirSync as X}from"fs";import{fileURLToPath as M}from"url";function P(){return typeof __dirname<"u"?__dirname:w(M(import.meta.url))}var F=P(),c=process.env.CLAUDE_MEM_DATA_DIR||p(O(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(O(),".claude"),te=p(c,"archives"),re=p(c,"logs"),oe=p(c,"trash"),ne=p(c,"backups"),ie=p(c,"settings.json"),I=p(c,"claude-mem.db"),ae=p(l,"settings.json"),de=p(l,"commands"),pe=p(l,"CLAUDE.md");function L(n){X(n,{recursive:!0})}function A(){return p(F,"..","..")}var T=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let _="";o!=null&&(this.level===0&&typeof o=="object"?_=`
|
||||
`+JSON.stringify(o,null,2):_=" "+this.formatData(o));let b="";if(r){let{sessionId:j,sdkSessionId:K,correlationId:Y,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let f=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(f):console.log(f)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},u=new S;var m=class{db;constructor(){L(c),this.db=new H(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(p=>p.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -129,7 +129,44 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`),this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
@@ -185,7 +222,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(p=>r.add(p))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(p=>n.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,o=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -212,11 +249,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),a=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
@@ -229,17 +268,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),i)}storeSummary(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -251,4 +306,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function W(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function S(o,e,s={}){let t=W(o,e,s);return JSON.stringify(t)}import f from"path";import{existsSync as b}from"fs";import{spawn as j}from"child_process";var $=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${$}/health`;async function k(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=v(),e=f.join(o,"plugin","scripts","worker-service.cjs");if(!b(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=f.join(o,"ecosystem.config.cjs"),t=f.join(o,"node_modules",".bin","pm2");if(!b(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!b(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=j(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await k())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function y(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o;if(!await C())throw new Error("Worker service failed to start or become healthy");let t=new m,r=t.findActiveSDKSession(e);if(!r){t.close(),console.log(S("Stop",!0));return}if(!r.worker_port)throw t.close(),u.error("HOOK","No worker port for session",{sessionId:r.id}),new Error("No worker port for session - session may not be properly initialized");let n=t.getPromptCounter(r.id);t.close(),u.dataIn("HOOK","Stop: Requesting summary",{sessionId:r.id,workerPort:r.worker_port,promptNumber:n});let i=await fetch(`http://127.0.0.1:${r.worker_port}/sessions/${r.id}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:n}),signal:AbortSignal.timeout(2e3)});if(!i.ok){let a=await i.text();throw u.failure("HOOK","Failed to generate summary",{sessionId:r.id,status:i.status},a),new Error(`Failed to request summary from worker: ${i.status} ${a}`)}u.debug("HOOK","Summary request sent successfully",{sessionId:r.id}),console.log(S("Stop",!0))}import{stdin as D}from"process";var R="";D.on("data",o=>R+=o);D.on("end",async()=>{let o=R.trim()?JSON.parse(R):void 0;await y(o),process.exit(0)});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function G(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(n,e,s={}){let t=G(n,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as R}from"fs";import{spawn as W}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),$=`http://127.0.0.1:${B}/health`;async function C(){try{return(await fetch($,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=A(),e=g.join(n,"plugin","scripts","worker-service.cjs");if(!R(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(n,"ecosystem.config.cjs"),t=g.join(n,"node_modules",".bin","pm2");if(!R(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!R(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=W(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}async function D(n){if(!n)throw new Error("summaryHook requires input");let{session_id:e}=n;if(!await k())throw new Error("Worker service failed to start or become healthy");let t=new m,r=t.createSDKSession(e,"",""),o=t.getPromptCounter(r);t.close();let i=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);u.dataIn("HOOK","Stop: Requesting summary",{sessionId:r,workerPort:i,promptNumber:o});let a=await fetch(`http://127.0.0.1:${i}/sessions/${r}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:o}),signal:AbortSignal.timeout(2e3)});if(!a.ok){let d=await a.text();throw u.failure("HOOK","Failed to generate summary",{sessionId:r,status:a.status},d),new Error(`Failed to request summary from worker: ${a.status} ${d}`)}u.debug("HOOK","Summary request sent successfully",{sessionId:r}),console.log(v("Stop",!0))}import{stdin as y}from"process";var N="";y.on("data",n=>N+=n);y.on("end",async()=>{let n=N.trim()?JSON.parse(N):void 0;await D(n),process.exit(0)});
|
||||
|
||||
File diff suppressed because one or more lines are too long
+109
-164
@@ -27,11 +27,8 @@ const colors = {
|
||||
/**
|
||||
* Context Hook - SessionStart
|
||||
* Shows user what happened in recent sessions
|
||||
*
|
||||
* Output: Returns formatted context string to be wrapped in hookSpecificOutput
|
||||
*/
|
||||
export function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): string {
|
||||
// v4.0.0: Ensure worker is running before loading context
|
||||
ensureWorkerRunning();
|
||||
const cwd = input?.cwd ?? process.cwd();
|
||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
||||
@@ -39,7 +36,24 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
const db = new SessionStore();
|
||||
|
||||
try {
|
||||
const summaries = db.getRecentSummariesWithSessionInfo(project, 3);
|
||||
// Get the most recent summaries, then display them chronologically (oldest to newest, like a chat)
|
||||
const summaries = db.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT sdk_session_id, request, learned, completed, next_steps, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 10
|
||||
)
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(project) as Array<{
|
||||
sdk_session_id: string;
|
||||
request: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
next_steps: string | null;
|
||||
created_at: string;
|
||||
}>;
|
||||
|
||||
if (summaries.length === 0) {
|
||||
if (useColors) {
|
||||
@@ -50,129 +64,6 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
|
||||
const output: string[] = [];
|
||||
|
||||
// Index view: Show previous as index, latest in full at bottom (chat-style)
|
||||
if (useIndexView) {
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`);
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`# [${project}] recent context`);
|
||||
output.push('');
|
||||
}
|
||||
|
||||
// Show index of previous summaries (oldest to newest)
|
||||
if (summaries.length > 1) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.dim}Previous Requests:${colors.reset}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push('**Previous Requests:**');
|
||||
output.push('');
|
||||
}
|
||||
|
||||
// Iterate backwards through array (skip first which is most recent)
|
||||
for (let i = summaries.length - 1; i >= 1; i--) {
|
||||
const prev = summaries[i];
|
||||
const prevDate = new Date(prev.created_at);
|
||||
const dateTimeStr = prevDate.toLocaleString();
|
||||
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}• ${dateTimeStr}:${colors.reset} ${prev.request || '(no request)'}`);
|
||||
} else {
|
||||
output.push(`- ${dateTimeStr}: ${prev.request || '(no request)'}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push('');
|
||||
output.push('---');
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// Show most recent summary in full at the bottom
|
||||
const latest = summaries[0];
|
||||
|
||||
if (latest.request) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${latest.request}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Request:** ${latest.request}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
if (latest.learned) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.blue}Learned:${colors.reset} ${latest.learned}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Learned:** ${latest.learned}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
if (latest.completed) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.green}Completed:${colors.reset} ${latest.completed}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Completed:** ${latest.completed}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
if (latest.next_steps) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.magenta}Next Steps:${colors.reset} ${latest.next_steps}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Next Steps:** ${latest.next_steps}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// Get files for latest summary
|
||||
const latestFiles = db.getFilesForSession(latest.sdk_session_id);
|
||||
|
||||
if (latestFiles.filesRead.length > 0) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Files Read: ${latestFiles.filesRead.join(', ')}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Files Read:** ${latestFiles.filesRead.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (latestFiles.filesModified.length > 0) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Files Modified: ${latestFiles.filesModified.join(', ')}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Files Modified:** ${latestFiles.filesModified.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
const latestDate = new Date(latest.created_at).toLocaleString();
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Date: ${latestDate}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Date:** ${latestDate}`);
|
||||
}
|
||||
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
}
|
||||
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`);
|
||||
@@ -182,25 +73,20 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
output.push('');
|
||||
}
|
||||
|
||||
let previousSessionId: string | null = null;
|
||||
let isFirstSummary = true;
|
||||
|
||||
for (const summary of summaries) {
|
||||
// Add session break indicator if this is a different session
|
||||
const isNewSession = previousSessionId !== null && summary.sdk_session_id !== previousSessionId;
|
||||
for (let i = 0; i < summaries.length; i++) {
|
||||
const summary = summaries[i];
|
||||
|
||||
if (isNewSession) {
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.dim}${'─'.repeat(23)} New Session ${'─'.repeat(24)}${colors.reset}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push('');
|
||||
output.push('--- New Session ---');
|
||||
output.push('');
|
||||
}
|
||||
} else if (!isFirstSummary) {
|
||||
// Only show regular separator if not first summary and not showing "New Session"
|
||||
// Determine verbosity tier based on position
|
||||
// Most recent summary is at the end (highest index) since we display chronologically
|
||||
const positionFromEnd = summaries.length - 1 - i;
|
||||
const isTier1 = positionFromEnd === 0; // Most recent (full verbosity)
|
||||
const isTier2 = positionFromEnd >= 1 && positionFromEnd <= 3; // Middle 3 (request + what was done)
|
||||
const isTier3 = positionFromEnd > 3; // Oldest 6 (request only)
|
||||
|
||||
// Add separator between summaries (but not before the first one)
|
||||
if (!isFirstSummary) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
output.push('');
|
||||
@@ -209,7 +95,6 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
output.push('');
|
||||
}
|
||||
} else {
|
||||
// First summary - just add a blank line after header
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
}
|
||||
@@ -217,6 +102,28 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
|
||||
isFirstSummary = false;
|
||||
|
||||
// TIER 3: Minimal (just Request + Date)
|
||||
if (isTier3) {
|
||||
if (summary.request) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Request:** ${summary.request}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
const dateTime = new Date(summary.created_at).toLocaleString();
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Date:** ${dateTime}`);
|
||||
output.push('');
|
||||
}
|
||||
continue; // Skip the rest for Tier 3
|
||||
}
|
||||
|
||||
// TIER 1 & 2: Show Request
|
||||
if (summary.request) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`);
|
||||
@@ -227,7 +134,8 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
}
|
||||
}
|
||||
|
||||
if (summary.learned) {
|
||||
// TIER 1 ONLY: Show Learned
|
||||
if (isTier1 && summary.learned) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.blue}Learned:${colors.reset} ${summary.learned}`);
|
||||
output.push('');
|
||||
@@ -237,6 +145,7 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 & 2: Show Completed
|
||||
if (summary.completed) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.green}Completed:${colors.reset} ${summary.completed}`);
|
||||
@@ -247,7 +156,8 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
}
|
||||
}
|
||||
|
||||
if (summary.next_steps) {
|
||||
// TIER 1 ONLY: Show Next Steps
|
||||
if (isTier1 && summary.next_steps) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.magenta}Next Steps:${colors.reset} ${summary.next_steps}`);
|
||||
output.push('');
|
||||
@@ -257,25 +167,62 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
}
|
||||
}
|
||||
|
||||
// Get files from observations (not from summary which is never populated)
|
||||
const sessionFiles = db.getFilesForSession(summary.sdk_session_id);
|
||||
// TIER 1 ONLY: Get and show files
|
||||
if (isTier1) {
|
||||
const observations = db.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(summary.sdk_session_id) as Array<{
|
||||
files_read: string | null;
|
||||
files_modified: string | null;
|
||||
}>;
|
||||
|
||||
if (sessionFiles.filesRead.length > 0) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Files Read: ${sessionFiles.filesRead.join(', ')}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Files Read:** ${sessionFiles.filesRead.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionFiles.filesModified.length > 0) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Files Modified: ${sessionFiles.filesModified.join(', ')}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Files Modified:** ${sessionFiles.filesModified.join(', ')}`);
|
||||
const filesReadSet = new Set<string>();
|
||||
const filesModifiedSet = new Set<string>();
|
||||
|
||||
for (const obs of observations) {
|
||||
if (obs.files_read) {
|
||||
try {
|
||||
const files = JSON.parse(obs.files_read);
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach(f => filesReadSet.add(f));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
if (obs.files_modified) {
|
||||
try {
|
||||
const files = JSON.parse(obs.files_modified);
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach(f => filesModifiedSet.add(f));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filesReadSet.size > 0) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Files Read: ${Array.from(filesReadSet).join(', ')}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Files Read:** ${Array.from(filesReadSet).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (filesModifiedSet.size > 0) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Files Modified: ${Array.from(filesModifiedSet).join(', ')}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Files Modified:** ${Array.from(filesModifiedSet).join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 & 2: Show Date
|
||||
const dateTime = new Date(summary.created_at).toLocaleString();
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`);
|
||||
@@ -286,8 +233,6 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
if (!useColors) {
|
||||
output.push('');
|
||||
}
|
||||
|
||||
previousSessionId = summary.sdk_session_id;
|
||||
}
|
||||
|
||||
if (useColors) {
|
||||
@@ -299,4 +244,4 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-41
@@ -31,54 +31,29 @@ export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
const db = new SessionStore();
|
||||
|
||||
try {
|
||||
// Just save session_id for indexing - no validation, no state management
|
||||
const sessionDbId = db.createSDKSession(session_id, project, prompt);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
|
||||
// Check for any existing session (active, failed, or completed)
|
||||
let existing = db.findActiveSDKSession(session_id);
|
||||
let sessionDbId: number;
|
||||
let isNewSession = false;
|
||||
// Save raw user prompt for full-text search
|
||||
db.saveUserPrompt(session_id, promptNumber, prompt);
|
||||
|
||||
if (existing) {
|
||||
// Session already active, increment prompt counter
|
||||
sessionDbId = existing.id;
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
console.error(`[new-hook] Continuing session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
} else {
|
||||
// Check for inactive sessions we can reuse
|
||||
const inactive = db.findAnySDKSession(session_id);
|
||||
|
||||
if (inactive) {
|
||||
// Reactivate the existing session
|
||||
sessionDbId = inactive.id;
|
||||
db.reactivateSession(sessionDbId, prompt);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
isNewSession = true;
|
||||
console.error(`[new-hook] Reactivated session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
} else {
|
||||
// Create new session
|
||||
sessionDbId = db.createSDKSession(session_id, project, prompt);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
isNewSession = true;
|
||||
console.error(`[new-hook] Created new session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
}
|
||||
}
|
||||
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
|
||||
// Get fixed port
|
||||
const port = getWorkerPort();
|
||||
|
||||
// Only initialize worker on new sessions
|
||||
if (isNewSession) {
|
||||
// Initialize session via HTTP
|
||||
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ project, userPrompt: prompt }),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
// Initialize session via HTTP
|
||||
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ project, userPrompt: prompt }),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to initialize session: ${response.status} ${errorText}`);
|
||||
}
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to initialize session: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
console.log(createHookResponse('UserPromptSubmit', true));
|
||||
|
||||
+11
-20
@@ -40,32 +40,23 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
}
|
||||
|
||||
const db = new SessionStore();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
|
||||
if (!session) {
|
||||
db.close();
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.worker_port) {
|
||||
db.close();
|
||||
logger.error('HOOK', 'No worker port for session', { sessionId: session.id });
|
||||
throw new Error('No worker port for session - session may not be properly initialized');
|
||||
}
|
||||
|
||||
// Get current prompt number for this session
|
||||
const promptNumber = db.getPromptCounter(session.id);
|
||||
// Get or create session - no validation, just use the session_id from hook
|
||||
const sessionDbId = db.createSDKSession(session_id, '', ''); // project and prompt not needed for observations
|
||||
const promptNumber = db.getPromptCounter(sessionDbId);
|
||||
db.close();
|
||||
|
||||
const toolStr = logger.formatTool(tool_name, tool_input);
|
||||
|
||||
// Use fixed worker port - no session.worker_port validation needed
|
||||
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
|
||||
|
||||
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
|
||||
sessionId: session.id,
|
||||
workerPort: session.worker_port
|
||||
sessionId: sessionDbId,
|
||||
workerPort: FIXED_PORT
|
||||
});
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/observations`, {
|
||||
const response = await fetch(`http://127.0.0.1:${FIXED_PORT}/sessions/${sessionDbId}/observations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -80,12 +71,12 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.failure('HOOK', 'Failed to send observation', {
|
||||
sessionId: session.id,
|
||||
sessionId: sessionDbId,
|
||||
status: response.status
|
||||
}, errorText);
|
||||
throw new Error(`Failed to send observation to worker: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
logger.debug('HOOK', 'Observation sent successfully', { sessionId: session.id, toolName: tool_name });
|
||||
logger.debug('HOOK', 'Observation sent successfully', { sessionId: sessionDbId, toolName: tool_name });
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
}
|
||||
|
||||
+11
-20
@@ -27,31 +27,22 @@ export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
}
|
||||
|
||||
const db = new SessionStore();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
|
||||
if (!session) {
|
||||
db.close();
|
||||
console.log(createHookResponse('Stop', true));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.worker_port) {
|
||||
db.close();
|
||||
logger.error('HOOK', 'No worker port for session', { sessionId: session.id });
|
||||
throw new Error('No worker port for session - session may not be properly initialized');
|
||||
}
|
||||
|
||||
// Get current prompt number
|
||||
const promptNumber = db.getPromptCounter(session.id);
|
||||
// Get or create session - no validation, just use the session_id from hook
|
||||
const sessionDbId = db.createSDKSession(session_id, '', '');
|
||||
const promptNumber = db.getPromptCounter(sessionDbId);
|
||||
db.close();
|
||||
|
||||
// Use fixed worker port - no session.worker_port validation needed
|
||||
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
|
||||
|
||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||
sessionId: session.id,
|
||||
workerPort: session.worker_port,
|
||||
sessionId: sessionDbId,
|
||||
workerPort: FIXED_PORT,
|
||||
promptNumber
|
||||
});
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/summarize`, {
|
||||
const response = await fetch(`http://127.0.0.1:${FIXED_PORT}/sessions/${sessionDbId}/summarize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt_number: promptNumber }),
|
||||
@@ -61,12 +52,12 @@ export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.failure('HOOK', 'Failed to generate summary', {
|
||||
sessionId: session.id,
|
||||
sessionId: sessionDbId,
|
||||
status: response.status
|
||||
}, errorText);
|
||||
throw new Error(`Failed to request summary from worker: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
logger.debug('HOOK', 'Summary request sent successfully', { sessionId: session.id });
|
||||
logger.debug('HOOK', 'Summary request sent successfully', { sessionId: sessionDbId });
|
||||
console.log(createHookResponse('Stop', true));
|
||||
}
|
||||
|
||||
+47
-23
@@ -22,21 +22,31 @@ export interface SDKSession {
|
||||
* Build initial prompt to initialize the SDK agent
|
||||
*/
|
||||
export function buildInitPrompt(project: string, sessionId: string, userPrompt: string): string {
|
||||
return `You are a memory processor for a Claude Code session. Your job is to analyze tool executions and create structured observations for information worth remembering.
|
||||
return `You are observing a development session to create searchable memory FOR FUTURE SESSIONS.
|
||||
|
||||
You are processing tool executions from a Claude Code session with the following context:
|
||||
CRITICAL: Record what was BUILT/FIXED/DEPLOYED/CONFIGURED, not what you (the observer) are doing.
|
||||
|
||||
User's Goal: ${userPrompt}
|
||||
Date: ${new Date().toISOString().split('T')[0]}
|
||||
|
||||
WHEN TO STORE
|
||||
-------------
|
||||
Store observations when the tool output contains information worth remembering about:
|
||||
- How things work
|
||||
- Why things exist or were chosen
|
||||
- What changed
|
||||
- Problems and their solutions
|
||||
- Important patterns or gotchas
|
||||
WHAT TO RECORD
|
||||
--------------
|
||||
Focus on deliverables and capabilities:
|
||||
- What the system NOW DOES differently (new capabilities)
|
||||
- What shipped to users/production (features, fixes, configs, docs)
|
||||
- Changes in technical domains (auth, data, UI, infra, DevOps, docs)
|
||||
|
||||
Use verbs like: implemented, fixed, deployed, configured, migrated, optimized, added, refactored
|
||||
|
||||
✅ GOOD EXAMPLES (describes what was built):
|
||||
- "Authentication now supports OAuth2 with PKCE flow"
|
||||
- "Deployment pipeline runs canary releases with auto-rollback"
|
||||
- "Database indexes optimized for common query patterns"
|
||||
|
||||
❌ BAD EXAMPLES (describes observation process - DO NOT DO THIS):
|
||||
- "Analyzed authentication implementation and stored findings"
|
||||
- "Tracked deployment steps and logged outcomes"
|
||||
- "Monitored database performance and recorded metrics"
|
||||
|
||||
WHEN TO SKIP
|
||||
------------
|
||||
@@ -53,12 +63,15 @@ Output observations using this XML structure:
|
||||
|
||||
\`\`\`xml
|
||||
<observation>
|
||||
<type>[ change | discovery | decision ]</type>
|
||||
<type>[ bugfix | feature | refactor | change | discovery | decision ]</type>
|
||||
<!--
|
||||
**type**: MUST be EXACTLY one of these 3 options (no other values allowed):
|
||||
- change: modifications to code, config, or documentation
|
||||
**type**: MUST be EXACTLY one of these 6 options (no other values allowed):
|
||||
- bugfix: something was broken, now fixed
|
||||
- feature: new capability or functionality added
|
||||
- refactor: code restructured, behavior unchanged
|
||||
- change: generic modification (docs, config, misc)
|
||||
- discovery: learning about existing system
|
||||
- decision: choosing an approach and why it was chosen
|
||||
- decision: architectural/design choice with rationale
|
||||
-->
|
||||
<title>[**title**: Short title capturing the core action or topic]</title>
|
||||
<subtitle>[**subtitle**: One sentence explanation (max 24 words)]</subtitle>
|
||||
@@ -145,21 +158,32 @@ export function buildObservationPrompt(obs: Observation): string {
|
||||
export function buildSummaryPrompt(session: SDKSession): string {
|
||||
return `REQUEST SUMMARY
|
||||
===============
|
||||
Review the observations you generated for THIS REQUEST and create a summary.
|
||||
Review the observations and create a summary of what was BUILT/SHIPPED.
|
||||
|
||||
IMPORTANT: Summarize only THIS REQUEST, not the entire session.
|
||||
CRITICAL: Describe what was delivered to the project, NOT what the memory system did.
|
||||
|
||||
User's Original Request: ${session.user_prompt}
|
||||
|
||||
✅ GOOD - Describes deliverables:
|
||||
<request>Fix authentication timeout bug</request>
|
||||
<request>Add three-tier verbosity system to session summaries</request>
|
||||
<request>Deploy Kubernetes cluster with auto-scaling</request>
|
||||
|
||||
❌ BAD - Describes meta-operations (DO NOT DO THIS):
|
||||
<request>Process tool executions and store observations</request>
|
||||
<request>Analyze session data and generate summaries</request>
|
||||
<request>Track file modifications across sessions</request>
|
||||
|
||||
Output this XML:
|
||||
<summary>
|
||||
<request>[What did the user request?]</request>
|
||||
<investigated>[What code and systems did you explore?]</investigated>
|
||||
<learned>[What did you learn about the codebase?]</learned>
|
||||
<completed>[What was accomplished in this request?]</completed>
|
||||
<next_steps>[What should be done next?]</next_steps>
|
||||
<notes>[Additional insights or context]</notes>
|
||||
<request>[What did the user want to build/fix/deploy? Use their original words from: ${session.user_prompt}]</request>
|
||||
<investigated>[What code/systems were explored?]</investigated>
|
||||
<learned>[What was discovered about how things work?]</learned>
|
||||
<completed>[What shipped? What does the system now do?]</completed>
|
||||
<next_steps>[What remains to build/fix/deploy?]</next_steps>
|
||||
<notes>[Additional insights]</notes>
|
||||
</summary>
|
||||
|
||||
**Required fields**: request, investigated, learned, completed, next_steps
|
||||
|
||||
**Optional fields**: notes`;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import { basename } from 'path';
|
||||
import { SessionSearch } from '../services/sqlite/SessionSearch.js';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { ObservationSearchResult, SessionSummarySearchResult } from '../services/sqlite/types.js';
|
||||
import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../services/sqlite/types.js';
|
||||
|
||||
// Initialize search instance
|
||||
let search: SessionSearch;
|
||||
@@ -221,6 +221,38 @@ function formatSessionResult(session: SessionSummarySearchResult, index: number)
|
||||
return contentParts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format user prompt as index entry (truncated text, date, ID only)
|
||||
*/
|
||||
function formatUserPromptIndex(prompt: UserPromptSearchResult, index: number): string {
|
||||
const truncated = prompt.prompt_text.length > 100
|
||||
? prompt.prompt_text.substring(0, 100) + '...'
|
||||
: prompt.prompt_text;
|
||||
const date = new Date(prompt.created_at_epoch).toLocaleString();
|
||||
|
||||
return `${index + 1}. "${truncated}"
|
||||
Date: ${date} | Prompt #${prompt.prompt_number}
|
||||
Source: claude-mem://user-prompt/${prompt.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format user prompt as text content with metadata
|
||||
*/
|
||||
function formatUserPromptResult(prompt: UserPromptSearchResult, index: number): string {
|
||||
const contentParts: string[] = [];
|
||||
contentParts.push(`## User Prompt #${prompt.prompt_number}`);
|
||||
contentParts.push(`*Source: claude-mem://user-prompt/${prompt.id}*`);
|
||||
contentParts.push('');
|
||||
contentParts.push(prompt.prompt_text);
|
||||
contentParts.push('');
|
||||
contentParts.push('---');
|
||||
|
||||
const date = new Date(prompt.created_at_epoch).toLocaleString();
|
||||
contentParts.push(`Date: ${date}`);
|
||||
|
||||
return contentParts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Common filter schema
|
||||
*/
|
||||
@@ -686,6 +718,63 @@ const tools = [
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'search_user_prompts',
|
||||
description: 'Search raw user prompts with full-text search. Use this to find what the user actually said/requested across all sessions. IMPORTANT: Always use index format first (default) to get an overview with minimal token usage, then use format: "full" only for specific items of interest.',
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe('Search query for FTS5 full-text search'),
|
||||
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for truncated prompts/dates (default, RECOMMENDED for initial search), "full" for complete prompt text (use only after reviewing index results)'),
|
||||
project: z.string().optional().describe('Filter by project name'),
|
||||
dateRange: z.object({
|
||||
start: z.union([z.string(), z.number()]).optional(),
|
||||
end: z.union([z.string(), z.number()]).optional()
|
||||
}).optional().describe('Filter by date range'),
|
||||
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
|
||||
offset: z.number().min(0).default(0).describe('Number of results to skip'),
|
||||
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('relevance').describe('Sort order')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const { query, format = 'index', ...options } = args;
|
||||
const results = search.searchUserPrompts(query, options);
|
||||
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `No user prompts found matching "${query}"`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// Format based on requested format
|
||||
let combinedText: string;
|
||||
if (format === 'index') {
|
||||
const header = `Found ${results.length} user prompt(s) matching "${query}":\n\n`;
|
||||
const formattedResults = results.map((prompt, i) => formatUserPromptIndex(prompt, i));
|
||||
combinedText = header + formattedResults.join('\n\n') + formatSearchTips();
|
||||
} else {
|
||||
const formattedResults = results.map((prompt, i) => formatUserPromptResult(prompt, i));
|
||||
combinedText = formattedResults.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: combinedText
|
||||
}]
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Search failed: ${error.message}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
|
||||
import {
|
||||
ObservationSearchResult,
|
||||
SessionSummarySearchResult,
|
||||
UserPromptSearchResult,
|
||||
SearchOptions,
|
||||
SearchFilters,
|
||||
DateRange,
|
||||
ObservationRow
|
||||
ObservationRow,
|
||||
UserPromptRow
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
@@ -473,6 +475,101 @@ export class SessionSearch {
|
||||
return this.db.prepare(sql).all(...params) as ObservationSearchResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search user prompts with full-text search
|
||||
*/
|
||||
searchUserPrompts(query: string, options: SearchOptions = {}): UserPromptSearchResult[] {
|
||||
const params: any[] = [];
|
||||
const { limit = 20, offset = 0, orderBy = 'relevance', ...filters } = options;
|
||||
|
||||
// Build FTS5 match query
|
||||
const ftsQuery = this.escapeFTS5(query);
|
||||
params.push(ftsQuery);
|
||||
|
||||
// Build filter conditions (join with sdk_sessions for project filtering)
|
||||
const baseConditions: string[] = [];
|
||||
if (filters.project) {
|
||||
baseConditions.push('s.project = ?');
|
||||
params.push(filters.project);
|
||||
}
|
||||
|
||||
if (filters.dateRange) {
|
||||
const { start, end } = filters.dateRange;
|
||||
if (start) {
|
||||
const startEpoch = typeof start === 'number' ? start : new Date(start).getTime();
|
||||
baseConditions.push('up.created_at_epoch >= ?');
|
||||
params.push(startEpoch);
|
||||
}
|
||||
if (end) {
|
||||
const endEpoch = typeof end === 'number' ? end : new Date(end).getTime();
|
||||
baseConditions.push('up.created_at_epoch <= ?');
|
||||
params.push(endEpoch);
|
||||
}
|
||||
}
|
||||
|
||||
const whereClause = baseConditions.length > 0 ? `AND ${baseConditions.join(' AND ')}` : '';
|
||||
|
||||
// Build ORDER BY
|
||||
const orderClause = orderBy === 'relevance'
|
||||
? 'ORDER BY user_prompts_fts.rank ASC'
|
||||
: orderBy === 'date_asc'
|
||||
? 'ORDER BY up.created_at_epoch ASC'
|
||||
: 'ORDER BY up.created_at_epoch DESC';
|
||||
|
||||
// Main query with FTS5 (join sdk_sessions for project filtering)
|
||||
const sql = `
|
||||
SELECT
|
||||
up.*,
|
||||
user_prompts_fts.rank as rank
|
||||
FROM user_prompts up
|
||||
JOIN user_prompts_fts ON up.id = user_prompts_fts.rowid
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE user_prompts_fts MATCH ?
|
||||
${whereClause}
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(limit, offset);
|
||||
|
||||
const results = this.db.prepare(sql).all(...params) as UserPromptSearchResult[];
|
||||
|
||||
// Normalize rank to score
|
||||
if (results.length > 0) {
|
||||
const minRank = Math.min(...results.map(r => r.rank || 0));
|
||||
const maxRank = Math.max(...results.map(r => r.rank || 0));
|
||||
const range = maxRank - minRank || 1;
|
||||
|
||||
results.forEach(r => {
|
||||
if (r.rank !== undefined) {
|
||||
r.score = 1 - ((r.rank - minRank) / range);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all prompts for a session by claude_session_id
|
||||
*/
|
||||
getUserPromptsBySession(claudeSessionId: string): UserPromptRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
claude_session_id,
|
||||
prompt_number,
|
||||
prompt_text,
|
||||
created_at,
|
||||
created_at_epoch
|
||||
FROM user_prompts
|
||||
WHERE claude_session_id = ?
|
||||
ORDER BY prompt_number ASC
|
||||
`);
|
||||
|
||||
return stmt.all(claudeSessionId) as UserPromptRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ import { logger } from '../../utils/logger.js';
|
||||
* Provides simple, synchronous CRUD operations for session-based memory
|
||||
*/
|
||||
export class SessionStore {
|
||||
private db: Database.Database;
|
||||
public db: Database.Database;
|
||||
|
||||
constructor() {
|
||||
ensureDir(DATA_DIR);
|
||||
@@ -27,6 +27,7 @@ export class SessionStore {
|
||||
this.removeSessionSummariesUniqueConstraint();
|
||||
this.addObservationHierarchicalFields();
|
||||
this.makeObservationsTextNullable();
|
||||
this.createUserPromptsTable();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -405,6 +406,92 @@ export class SessionStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user_prompts table with FTS5 support (migration 10)
|
||||
*/
|
||||
private createUserPromptsTable(): void {
|
||||
try {
|
||||
// Check if migration already applied
|
||||
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(10) as {version: number} | undefined;
|
||||
if (applied) return;
|
||||
|
||||
// Check if table already exists
|
||||
const tableInfo = this.db.pragma('table_info(user_prompts)');
|
||||
if ((tableInfo as any[]).length > 0) {
|
||||
// Already migrated
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(10, new Date().toISOString());
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('[SessionStore] Creating user_prompts table with FTS5 support...');
|
||||
|
||||
// Begin transaction
|
||||
this.db.exec('BEGIN TRANSACTION');
|
||||
|
||||
try {
|
||||
// Create main table (using claude_session_id since sdk_session_id is set asynchronously by worker)
|
||||
this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`);
|
||||
|
||||
// Create FTS5 virtual table
|
||||
this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`);
|
||||
|
||||
// Create triggers to sync FTS5
|
||||
this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`);
|
||||
|
||||
// Commit transaction
|
||||
this.db.exec('COMMIT');
|
||||
|
||||
// Record migration
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(10, new Date().toISOString());
|
||||
|
||||
console.error('[SessionStore] Successfully created user_prompts table with FTS5 support');
|
||||
} catch (error: any) {
|
||||
// Rollback on error
|
||||
this.db.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[SessionStore] Migration error (create user_prompts table):', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent session summaries for a project
|
||||
*/
|
||||
@@ -706,19 +793,32 @@ export class SessionStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SDK session
|
||||
* Create a new SDK session (idempotent - returns existing session ID if already exists)
|
||||
* Sets both claude_session_id and sdk_session_id to the same value
|
||||
*/
|
||||
createSDKSession(claudeSessionId: string, project: string, userPrompt: string): number {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
// Try to insert - will be ignored if session already exists
|
||||
// claude_session_id and sdk_session_id are the same value
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`);
|
||||
|
||||
const result = stmt.run(claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch);
|
||||
const result = stmt.run(claudeSessionId, claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch);
|
||||
|
||||
// If lastInsertRowid is 0, insert was ignored (session exists), so fetch existing ID
|
||||
if (result.lastInsertRowid === 0 || result.changes === 0) {
|
||||
const selectStmt = this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`);
|
||||
const existing = selectStmt.get(claudeSessionId) as { id: number } | undefined;
|
||||
return existing!.id;
|
||||
}
|
||||
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
@@ -777,8 +877,26 @@ export class SessionStore {
|
||||
return result?.worker_port || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a user prompt
|
||||
*/
|
||||
saveUserPrompt(claudeSessionId: string, promptNumber: number, promptText: string): number {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(claudeSessionId, promptNumber, promptText, now.toISOString(), nowEpoch);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an observation (from SDK parsing)
|
||||
* Auto-creates session record if it doesn't exist in the index
|
||||
*/
|
||||
storeObservation(
|
||||
sdkSessionId: string,
|
||||
@@ -798,6 +916,29 @@ export class SessionStore {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
// Ensure session record exists in the index (auto-create if missing)
|
||||
const checkStmt = this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`);
|
||||
const existingSession = checkStmt.get(sdkSessionId) as { id: number } | undefined;
|
||||
|
||||
if (!existingSession) {
|
||||
// Auto-create session record if it doesn't exist
|
||||
const insertSession = this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`);
|
||||
insertSession.run(
|
||||
sdkSessionId, // claude_session_id and sdk_session_id are the same
|
||||
sdkSessionId,
|
||||
project,
|
||||
now.toISOString(),
|
||||
nowEpoch
|
||||
);
|
||||
console.error(`[SessionStore] Auto-created session record for session_id: ${sdkSessionId}`);
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
@@ -824,6 +965,7 @@ export class SessionStore {
|
||||
|
||||
/**
|
||||
* Store a session summary (from SDK parsing)
|
||||
* Auto-creates session record if it doesn't exist in the index
|
||||
*/
|
||||
storeSummary(
|
||||
sdkSessionId: string,
|
||||
@@ -841,6 +983,29 @@ export class SessionStore {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
// Ensure session record exists in the index (auto-create if missing)
|
||||
const checkStmt = this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`);
|
||||
const existingSession = checkStmt.get(sdkSessionId) as { id: number } | undefined;
|
||||
|
||||
if (!existingSession) {
|
||||
// Auto-create session record if it doesn't exist
|
||||
const insertSession = this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`);
|
||||
insertSession.run(
|
||||
sdkSessionId, // claude_session_id and sdk_session_id are the same
|
||||
sdkSessionId,
|
||||
project,
|
||||
now.toISOString(),
|
||||
nowEpoch
|
||||
);
|
||||
console.error(`[SessionStore] Auto-created session record for session_id: ${sdkSessionId}`);
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
|
||||
@@ -236,6 +236,15 @@ export interface SessionSummaryRow {
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
export interface UserPromptRow {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search and Filter Types
|
||||
*/
|
||||
@@ -267,3 +276,8 @@ export interface SessionSummarySearchResult extends SessionSummaryRow {
|
||||
rank?: number; // FTS5 relevance score (lower is better)
|
||||
score?: number; // Normalized score (higher is better, 0-1)
|
||||
}
|
||||
|
||||
export interface UserPromptSearchResult extends UserPromptRow {
|
||||
rank?: number; // FTS5 relevance score (lower is better)
|
||||
score?: number; // Normalized score (higher is better, 0-1)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ type WorkerMessage = ObservationMessage | SummarizeMessage;
|
||||
*/
|
||||
interface ActiveSession {
|
||||
sessionDbId: number;
|
||||
claudeSessionId: string; // Real Claude Code session ID
|
||||
sdkSessionId: string | null;
|
||||
project: string;
|
||||
userPrompt: string;
|
||||
@@ -118,15 +119,23 @@ class WorkerService {
|
||||
const correlationId = logger.sessionId(sessionDbId);
|
||||
logger.info('WORKER', 'Session init', { correlationId, project });
|
||||
|
||||
if (this.sessions.has(sessionDbId)) {
|
||||
res.status(409).json({ error: 'Session already exists' });
|
||||
// Fetch real Claude Code session ID from database
|
||||
const db = new SessionStore();
|
||||
const dbSession = db.getSessionById(sessionDbId);
|
||||
if (!dbSession) {
|
||||
db.close();
|
||||
res.status(404).json({ error: 'Session not found in database' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the real claude_session_id (which is the same as sdk_session_id now)
|
||||
const claudeSessionId = dbSession.sdk_session_id || `session-${sessionDbId}`;
|
||||
|
||||
// Create session state
|
||||
const session: ActiveSession = {
|
||||
sessionDbId,
|
||||
sdkSessionId: null,
|
||||
claudeSessionId,
|
||||
sdkSessionId: dbSession.sdk_session_id || null, // Set from database since we set both fields now
|
||||
project,
|
||||
userPrompt,
|
||||
pendingMessages: [],
|
||||
@@ -140,7 +149,6 @@ class WorkerService {
|
||||
this.sessions.set(sessionDbId, session);
|
||||
|
||||
// Update port in database
|
||||
const db = new SessionStore();
|
||||
db.setWorkerPort(sessionDbId, this.port!);
|
||||
db.close();
|
||||
|
||||
@@ -169,10 +177,39 @@ class WorkerService {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
const { tool_name, tool_input, tool_output, prompt_number } = req.body;
|
||||
|
||||
const session = this.sessions.get(sessionDbId);
|
||||
let session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
res.status(404).json({ error: 'Session not found' });
|
||||
return;
|
||||
// Auto-create session if it doesn't exist (e.g., worker restarted)
|
||||
// Fetch real session ID from database
|
||||
const db = new SessionStore();
|
||||
const dbSession = db.getSessionById(sessionDbId);
|
||||
db.close();
|
||||
|
||||
const claudeSessionId = dbSession?.sdk_session_id || `session-${sessionDbId}`;
|
||||
|
||||
session = {
|
||||
sessionDbId,
|
||||
claudeSessionId,
|
||||
sdkSessionId: null,
|
||||
project: dbSession?.project || '',
|
||||
userPrompt: dbSession?.user_prompt || '',
|
||||
pendingMessages: [],
|
||||
abortController: new AbortController(),
|
||||
generatorPromise: null,
|
||||
lastPromptNumber: 0,
|
||||
observationCounter: 0,
|
||||
startTime: Date.now()
|
||||
};
|
||||
this.sessions.set(sessionDbId, session);
|
||||
|
||||
// Start SDK agent in background
|
||||
session.generatorPromise = this.runSDKAgent(session).catch(err => {
|
||||
logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
const db = new SessionStore();
|
||||
db.markSessionFailed(sessionDbId);
|
||||
db.close();
|
||||
this.sessions.delete(sessionDbId);
|
||||
});
|
||||
}
|
||||
|
||||
// Create correlation ID for tracking this observation
|
||||
@@ -204,10 +241,39 @@ class WorkerService {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
const { prompt_number } = req.body;
|
||||
|
||||
const session = this.sessions.get(sessionDbId);
|
||||
let session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
res.status(404).json({ error: 'Session not found' });
|
||||
return;
|
||||
// Auto-create session if it doesn't exist (e.g., worker restarted)
|
||||
// Fetch real session ID from database
|
||||
const db = new SessionStore();
|
||||
const dbSession = db.getSessionById(sessionDbId);
|
||||
db.close();
|
||||
|
||||
const claudeSessionId = dbSession?.sdk_session_id || `session-${sessionDbId}`;
|
||||
|
||||
session = {
|
||||
sessionDbId,
|
||||
claudeSessionId,
|
||||
sdkSessionId: null,
|
||||
project: dbSession?.project || '',
|
||||
userPrompt: dbSession?.user_prompt || '',
|
||||
pendingMessages: [],
|
||||
abortController: new AbortController(),
|
||||
generatorPromise: null,
|
||||
lastPromptNumber: 0,
|
||||
observationCounter: 0,
|
||||
startTime: Date.now()
|
||||
};
|
||||
this.sessions.set(sessionDbId, session);
|
||||
|
||||
// Start SDK agent in background
|
||||
session.generatorPromise = this.runSDKAgent(session).catch(err => {
|
||||
logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
const db = new SessionStore();
|
||||
db.markSessionFailed(sessionDbId);
|
||||
db.close();
|
||||
this.sessions.delete(sessionDbId);
|
||||
});
|
||||
}
|
||||
|
||||
logger.dataIn('WORKER', 'Summary requested', {
|
||||
@@ -367,18 +433,19 @@ class WorkerService {
|
||||
* Keeps running continuously - no finalize, agent stays alive for entire Claude Code session
|
||||
*/
|
||||
private async* createMessageGenerator(session: ActiveSession): AsyncIterable<SDKUserMessage> {
|
||||
const claudeSessionId = `session-${session.sessionDbId}`;
|
||||
const initPrompt = buildInitPrompt(session.project, claudeSessionId, session.userPrompt);
|
||||
// Use real Claude Code session ID instead of fake session-{dbId}
|
||||
const initPrompt = buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt);
|
||||
|
||||
logger.dataIn('SDK', `Init prompt sent (${initPrompt.length} chars)`, {
|
||||
sessionId: session.sessionDbId,
|
||||
claudeSessionId: session.claudeSessionId,
|
||||
project: session.project
|
||||
});
|
||||
logger.debug('SDK', 'Full init prompt', { sessionId: session.sessionDbId }, initPrompt);
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: session.sdkSessionId || claudeSessionId,
|
||||
session_id: session.claudeSessionId, // Use real session ID from the start
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
@@ -418,7 +485,7 @@ class WorkerService {
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: session.sdkSessionId || claudeSessionId,
|
||||
session_id: session.claudeSessionId, // Use real session ID
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
@@ -449,7 +516,7 @@ class WorkerService {
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: session.sdkSessionId || claudeSessionId,
|
||||
session_id: session.claudeSessionId, // Use real session ID
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
|
||||
Reference in New Issue
Block a user