Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| adc5853c73 | |||
| 81fdf28347 | |||
| b3a565c448 | |||
| 5b28c23b20 | |||
| f4217cb2b9 | |||
| e7252c8999 | |||
| 74637705d7 | |||
| 322cb94c43 | |||
| 21c7ab2929 | |||
| 3846d66ccc | |||
| 817a069323 | |||
| d7b9f68d80 | |||
| c48290a156 | |||
| 226a52f8b8 | |||
| 93b5f80168 | |||
| f6cf895847 | |||
| dba29de2a7 | |||
| c16f453017 | |||
| 8b4c962e62 | |||
| 12149470a2 | |||
| fd4cd0444c | |||
| 0adbf38c39 | |||
| 15362d1aed | |||
| c04e5c571c | |||
| 66a69f3044 | |||
| e50c6142bb | |||
| 8d5bfec90e | |||
| ab2c44069b | |||
| babb375955 | |||
| 334fbdd913 | |||
| 4a269183a3 | |||
| 37cd8b8328 | |||
| 33d2422faa | |||
| dad3a104b4 | |||
| bcad4c484d | |||
| 4284b31f42 | |||
| f556546994 | |||
| 6441d9103c | |||
| 2952b7fb8d | |||
| 63f930a433 | |||
| 3cb003f4e4 | |||
| 8b5e5efeb5 | |||
| a57aba82a4 | |||
| 7b7a53ee19 | |||
| e57bfc4187 | |||
| 3cfdd5fd65 | |||
| 1a226b08f0 | |||
| 15b5176fe6 | |||
| 418dc963b2 | |||
| 2314362028 | |||
| f373b682dd | |||
| 1c25c3a7e7 | |||
| 3ca5e33b4a | |||
| 3042de1093 | |||
| f7f62ef3f3 | |||
| 726f167ebf | |||
| a62887a6e0 | |||
| 635cc87462 | |||
| c27f07023c | |||
| e073c0b75f | |||
| 562194612e | |||
| 02bce8a6e6 | |||
| e9bcb7e9db | |||
| 86214b93a9 | |||
| ef572ec032 | |||
| 3cb99ab7f4 | |||
| 20136121d8 | |||
| df0f3fd96c | |||
| fe861a85bd | |||
| 32f45a1100 | |||
| 1ea692e0b0 | |||
| 861cd20adf | |||
| d275715974 | |||
| fd4684dcb3 | |||
| c54e50ec0c | |||
| c42444e06c | |||
| cb2f2a0432 | |||
| 692bb07dfe | |||
| 58e9dcc0fc | |||
| f60ee3b4f5 | |||
| 7ed166323c | |||
| 651989c423 | |||
| 9dd8b180ac | |||
| 9b1d801a46 | |||
| 6fb9570291 | |||
| ae90b26995 | |||
| 3b0cd0b3f6 | |||
| f849a69506 | |||
| daf368e343 | |||
| cbd43240c7 |
@@ -4,18 +4,15 @@
|
||||
"name": "Alex Newman"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Alex Newman's Claude Code plugins",
|
||||
"version": "1.0.0"
|
||||
"description": "Plugins by Alex Newman (thedotmack)",
|
||||
"homepage": "https://github.com/thedotmack/claude-mem"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.8",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"version": "4.0.2",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
}
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"env": {}
|
||||
}
|
||||
+1
-1
@@ -5,6 +5,6 @@ node_modules/
|
||||
.env.local
|
||||
*.tmp
|
||||
*.temp
|
||||
.claude/
|
||||
.claude/settings.local.json
|
||||
plugin/data/
|
||||
plugin/data.backup/
|
||||
@@ -5,6 +5,76 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [4.2.3] - 2025-10-23
|
||||
|
||||
### Security
|
||||
- **FTS5 injection vulnerability fix**: Added proper escaping to prevent SQL injection attacks in search functions
|
||||
- Implemented double-quote escaping for FTS5 full-text search queries
|
||||
- Added comprehensive test suite with 332 new tests covering injection scenarios
|
||||
- Affects: `search_observations`, `search_sessions`, `search_user_prompts` MCP tools
|
||||
|
||||
### Fixed
|
||||
- **ESM/CJS compatibility**: Fixed getDirname function to work in both ESM (hooks) and CJS (worker) contexts
|
||||
- Detects context using `typeof __dirname !== 'undefined'`
|
||||
- Falls back to `fileURLToPath(import.meta.url)` for ESM modules
|
||||
- Resolves path resolution issues across different module systems
|
||||
- **Windows PowerShell compatibility**: Fixed SessionStart hook error on Windows systems
|
||||
- Replaced bash-specific test command `[` with standard cross-platform npm install
|
||||
- Simplified hook command to use idempotent npm install (fast when dependencies exist)
|
||||
- Dependencies install from root package.json in marketplace folder
|
||||
|
||||
### Changed
|
||||
- **SessionStart hook command**: Now uses `cd ... && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node context-hook.js`
|
||||
- Removed bash-specific conditional check
|
||||
- npm install is fast (~500ms) and idempotent when dependencies already exist
|
||||
- Works cross-platform on Windows, macOS, and Linux
|
||||
|
||||
|
||||
## [4.2.1] - 2025-10-22
|
||||
|
||||
### Added
|
||||
- **Summary skip logic**: Summaries now skip when work is already covered, banter/trivial requests, or no meaningful observations
|
||||
- New "WHEN NOT TO SUMMARIZE" section in buildSummaryPrompt guides SDK to avoid duplicate/trivial summaries
|
||||
- Parser detects `<skip_summary reason="..."/>` format and logs reason
|
||||
- Prevents duplicate summaries like the three "restore 6 types" summaries observed in session d9137878
|
||||
|
||||
### Fixed
|
||||
- **Observation type validation**: Parser now validates all 6 observation types (bugfix, feature, refactor, change, discovery, decision) instead of only 3
|
||||
|
||||
### Changed
|
||||
- **Chronological summary guidance**: Summaries now explicitly instructed to capture "what happened in THIS prompt" rather than re-summarizing previous work
|
||||
|
||||
|
||||
## [4.1.1] - 2025-10-21
|
||||
|
||||
### Removed
|
||||
- **advanced_search tool**: Removed redundant MCP tool that provided no functionality beyond calling search_observations + search_sessions
|
||||
|
||||
### Fixed
|
||||
- **MCP search limit bug**: Fixed findByConcept, findByType, and findByFile methods to properly respect limit/offset parameters
|
||||
- **Type contamination in concepts**: Added parser validation to prevent observation types from being added to concepts array
|
||||
- **Token limit warnings**: Added guidance in tool descriptions to start with 3-5 results to avoid MCP token limits
|
||||
|
||||
### Changed
|
||||
- **Simplified MCP API**: Reduced from 7 to 6 search tools by removing the redundant advanced_search
|
||||
- **Improved search prompts**: Enhanced type and concept constraint language in SDK prompts to prevent AI contamination
|
||||
|
||||
|
||||
## [4.1.0] - 2025-10-21
|
||||
|
||||
### Changed
|
||||
- **Graceful session cleanup**: Cleanup hook now marks sessions as completed instead of sending DELETE requests to worker
|
||||
- **Natural worker shutdown**: Workers now finish pending operations (like summary generation) before terminating
|
||||
- **Restored MCP search server**: Re-enabled full-text search capabilities from backup
|
||||
|
||||
### Fixed
|
||||
- Session summaries no longer interrupted by aggressive cleanup during session end
|
||||
- Workers can now complete final operations before shutdown
|
||||
|
||||
|
||||
## [4.0.2] - 2025-10-19
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
# Claude-Mem: Persistent Memory for Claude Code
|
||||
|
||||
## Overview
|
||||
|
||||
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.2.8
|
||||
**License**: AGPL-3.0
|
||||
**Author**: Alex Newman (@thedotmack)
|
||||
|
||||
## What It Does
|
||||
|
||||
Claude-mem operates as a Claude Code plugin that:
|
||||
- Captures every tool execution during your coding sessions
|
||||
- Processes observations using AI-powered compression
|
||||
- Generates session summaries when sessions end
|
||||
- Injects relevant context into future sessions
|
||||
- Provides full-text search across your entire project history
|
||||
|
||||
This creates a continuous memory system where Claude can learn from past sessions and maintain context across your entire project lifecycle.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Hook-Based Lifecycle System
|
||||
|
||||
Claude-mem integrates with Claude Code through 5 lifecycle hooks:
|
||||
|
||||
1. **SessionStart Hook** (`context-hook`)
|
||||
- Ensures dependencies are installed (runs fast idempotent npm install)
|
||||
- Injects context from previous sessions
|
||||
- Auto-starts PM2 worker service
|
||||
- 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
|
||||
- Sends observations to worker service for processing
|
||||
|
||||
4. **Summary Hook**
|
||||
- Generates AI-powered session summaries
|
||||
- Processes accumulated observations
|
||||
|
||||
5. **SessionEnd Hook** (`cleanup-hook`)
|
||||
- Marks sessions as completed (graceful cleanup as of v4.1.0)
|
||||
- Skips cleanup on `/clear` commands to preserve ongoing sessions
|
||||
- Previously sent DELETE requests; now allows workers to finish naturally
|
||||
|
||||
### Worker Service Architecture
|
||||
|
||||
- **Technology**: HTTP REST API built with Express.js, managed by PM2
|
||||
- **Port**: Fixed port 37777 (configurable via CLAUDE_MEM_WORKER_PORT)
|
||||
- **Location**: `src/services/worker-service.ts`
|
||||
- **Configurable Model**: Uses `CLAUDE_MEM_MODEL` environment variable (default: claude-sonnet-4-5)
|
||||
|
||||
**REST API Endpoints** (6 total):
|
||||
- Session management endpoints
|
||||
- Observation processing endpoints
|
||||
- Worker port tracking
|
||||
|
||||
The worker service runs as a PM2-managed background process that handles AI processing separately from the hook execution, preventing hook timeout issues.
|
||||
|
||||
### Database Layer
|
||||
|
||||
**Technology**: SQLite 3 with better-sqlite3 native module
|
||||
**Location**: `~/.claude-mem/claude-mem.db`
|
||||
|
||||
**Note**: SessionStore and SessionSearch use better-sqlite3 as the primary database implementation. Database.ts (which uses bun:sqlite) is legacy code.
|
||||
|
||||
**Core Tables**:
|
||||
- `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
|
||||
- Automatic sync triggers between main tables and FTS5 tables
|
||||
- Support for multi-prompt sessions (prompt_counter, prompt_number)
|
||||
- Hierarchical observations (title, subtitle, facts, narrative, concepts, files_read, files_modified)
|
||||
- Observation types: decision, bugfix, feature, refactor, discovery, change
|
||||
|
||||
**Database Classes**:
|
||||
- `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 8 specialized search tools to Claude:
|
||||
|
||||
1. **search_observations** - Full-text search across observations
|
||||
2. **search_sessions** - Full-text search across session summaries
|
||||
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**:
|
||||
```
|
||||
Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Results → Claude
|
||||
```
|
||||
|
||||
**Citations**: All search results use the `claude-mem://` URI scheme for referencing specific observations and sessions.
|
||||
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
- Node.js 18+
|
||||
- Claude Code plugin system
|
||||
|
||||
### Installation Method
|
||||
|
||||
**Local Marketplace Installation** (recommended as of v4.0.4+):
|
||||
|
||||
```bash
|
||||
# 1. Clone the repository
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
|
||||
# 2. Add to Claude Code marketplace
|
||||
/plugin marketplace add .claude-plugin/marketplace.json
|
||||
|
||||
# 3. Install the plugin
|
||||
/plugin install claude-mem
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Model Selection
|
||||
|
||||
Configure which AI model processes your observations:
|
||||
|
||||
**Using the interactive script**:
|
||||
```bash
|
||||
./claude-mem-settings.sh
|
||||
```
|
||||
|
||||
**Available models**:
|
||||
- `claude-haiku-4-5` - Fast, cost-efficient
|
||||
- `claude-sonnet-4-5` - Balanced (default)
|
||||
- `claude-opus-4` - Most capable
|
||||
- `claude-3-7-sonnet` - Alternative version
|
||||
|
||||
The script manages `CLAUDE_MEM_MODEL` in `~/.claude/settings.json`.
|
||||
TODO: also have script create and manage `CLAUDE_MEM_MODEL` in `~/.claude/plugins/marketplaces/thedotmack/.env` so our worker script has access to the value (we may not even need it in our settings but only in our plugin folder since hooks shouldn't be calling queries, not sure).
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Memory Pipeline
|
||||
```
|
||||
Tool Execution → Hook Capture → Worker Processing → AI Compression → Database Storage → Future Context Injection
|
||||
```
|
||||
|
||||
### Search Pipeline
|
||||
```
|
||||
Search Query → MCP Server → SessionSearch → FTS5 Query → Results with Citations
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
claude-mem/
|
||||
├── src/
|
||||
│ ├── bin/hooks/ # Hook entry points
|
||||
│ ├── hooks/ # Hook implementations
|
||||
│ ├── services/ # Worker service
|
||||
│ ├── services/sqlite/ # Database layer
|
||||
│ ├── servers/ # MCP search server
|
||||
│ ├── sdk/ # Claude Agent SDK integration
|
||||
│ ├── shared/ # Shared utilities
|
||||
│ └── utils/ # General utilities
|
||||
├── plugin/ # Built plugin files
|
||||
│ ├── scripts/ # Built hook executables
|
||||
│ └── .mcp.json # MCP server configuration
|
||||
└── .claude-plugin/ # Plugin metadata
|
||||
└── marketplace.json # Marketplace definition
|
||||
```
|
||||
|
||||
### Technology Stack
|
||||
- **Language**: TypeScript
|
||||
- **Database**: SQLite 3 with better-sqlite3
|
||||
- **HTTP**: Express.js
|
||||
- **Process Management**: PM2
|
||||
- **AI SDK**: @anthropic-ai/claude-agent-sdk (v0.1.23)
|
||||
- **MCP SDK**: @modelcontextprotocol/sdk (v1.20.1)
|
||||
- **Schema Validation**: zod-to-json-schema (v3.24.6)
|
||||
|
||||
### Build Process
|
||||
```bash
|
||||
npm run build && git commit -a -m "Build and update" && git push && cd ~/.claude/plugins/marketplaces/thedotmack/ && git pull && pm2 flush claude-mem-worker && pm2 restart claude-mem-worker && pm2 logs claude-mem-worker --nostream
|
||||
```
|
||||
|
||||
1) Compiles TypeScript and outputs hook executables to `plugin/scripts/`
|
||||
2) Does all the things needed to update and test since plugin-based installs are out of the .claude/ folder
|
||||
|
||||
**Build Outputs**:
|
||||
- Hook executables: `*-hook.js` (ESM format)
|
||||
- Worker service: `worker-service.cjs` (CJS format)
|
||||
- Search server: `search-server.js` (ESM format)
|
||||
|
||||
## Version History
|
||||
|
||||
### v4.2.8 (Current)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Critical Bugfix**:
|
||||
- Fixed NOT NULL constraint violation that prevented observations and summaries from being stored
|
||||
- Root cause: `SessionStore.getSessionById()` was not selecting `claude_session_id` from database
|
||||
- Worker service received `undefined` for `claude_session_id` when initializing sessions
|
||||
- Result: Database inserts failed with "NOT NULL constraint failed: sdk_sessions.claude_session_id"
|
||||
- Fix: Added `claude_session_id` to SELECT query and return type in `getSessionById()`
|
||||
- Impact: Session ID from hooks now flows correctly: hook → database → worker → SDK agent
|
||||
- Affects: All observation and summary storage operations
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/services/sqlite/SessionStore.ts:711` to include `claude_session_id` in SELECT
|
||||
- Updated return type signature to include `claude_session_id: string` field
|
||||
- Worker service now correctly receives and uses `claude_session_id` from database
|
||||
- System maintains consistency throughout entire session lifecycle
|
||||
|
||||
**Files Changed**:
|
||||
- `src/services/sqlite/SessionStore.ts` (getSessionById method)
|
||||
|
||||
### v4.2.7
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Improvements**:
|
||||
- Enhanced data quality with consistent null handling
|
||||
- `extractField()` now returns null for empty/whitespace-only strings
|
||||
- Ensures database stores clean null values instead of empty strings
|
||||
- Improves query efficiency and data consistency
|
||||
|
||||
**Testing**:
|
||||
- Added comprehensive regression test suite (49 tests)
|
||||
- Tests v4.2.5 summary validation fixes (partial summaries preserved)
|
||||
- Tests v4.2.6 observation validation fixes (partial observations preserved)
|
||||
- Tests edge cases: missing fields, empty fields, whitespace, invalid types
|
||||
- Tests data integrity: concept filtering, type validation, field preservation
|
||||
- New test script: `npm run test:parser`
|
||||
- All 49 tests passing with 100% coverage of critical parser edge cases
|
||||
|
||||
**Code Quality**:
|
||||
- Removed unused `extractFileArray()` function (replaced by `extractArrayElements()`)
|
||||
- Improved function documentation with clearer descriptions
|
||||
- TypeScript diagnostics clean
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/sdk/parser.ts:163-169` extractField function
|
||||
- Created `src/sdk/parser.test.ts` with comprehensive regression tests
|
||||
- Added `test:parser` script to package.json
|
||||
- All changes backward compatible with existing database schema
|
||||
|
||||
### v4.2.6
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Critical Bugfix**:
|
||||
- Fixed overly defensive observation validation that was blocking observations from being saved
|
||||
- Removed validation requiring title, subtitle, and narrative fields
|
||||
- Parser now NEVER skips observations - always saves them
|
||||
- Invalid or missing type defaults to "change" (generic catch-all type)
|
||||
- Prevents critical data loss - partial observations are better than no observations
|
||||
|
||||
**Impact**:
|
||||
- Before: Missing title, subtitle, OR narrative caused entire observation to be discarded
|
||||
- After: ALL observations preserved regardless of field completeness
|
||||
- Even partial observations contain valuable data: concepts, files_read, files_modified, facts
|
||||
- LLMs make mistakes - system must be resilient and save everything
|
||||
- Consistent with v4.2.5 summary fix - partial data is always better than no data
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/sdk/parser.ts:52-67` to never skip observations
|
||||
- Uses "change" as fallback type for invalid/missing types (no schema change needed)
|
||||
- Updated ParsedObservation interface to allow null for title, subtitle, narrative
|
||||
- Database schema already supports nullable fields
|
||||
- Parser now matches database schema constraints exactly
|
||||
- Affects `parseObservations()` function used by worker service
|
||||
|
||||
### v4.2.5
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Critical Bugfix**:
|
||||
- Fixed overly defensive summary validation that was blocking summaries from being saved
|
||||
- Removed validation check that returned null when any required fields were missing
|
||||
- Summaries are now always saved when `<summary>` tags are present, even if fields are incomplete
|
||||
- Prevents critical data loss - partial summaries are better than no summaries
|
||||
- Database schema already supports null/empty values for all fields
|
||||
|
||||
**Impact**:
|
||||
- Before: Missing a single field (e.g., `next_steps`) would cause entire summary to be discarded
|
||||
- After: All summaries are preserved, maintaining session context even when incomplete
|
||||
- This fix ensures continuity of the memory compression system
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/sdk/parser.ts:137-147` to remove blocking validation
|
||||
- Parser now returns ParsedSummary with whatever fields are available
|
||||
- Affects `parseSummary()` function used by worker service
|
||||
|
||||
### v4.2.4
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Improvements**:
|
||||
- Enhanced summary prompt clarity and reliability
|
||||
- Removed optional skip_summary functionality (summaries now always generated)
|
||||
- Clarified that summaries are mid-session checkpoints, not session endings
|
||||
- Improved request field instructions to better form descriptive titles
|
||||
- Changed wording from "discovered" to "learned" for consistency
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/sdk/prompts.ts` to remove `WHEN NOT TO SUMMARIZE` section
|
||||
- Added footer text clarifying summaries track progress within ongoing sessions
|
||||
- Changed request field prompt from "Use their original sentiment" to "Form a title that reflects the actual request"
|
||||
- Affects both observation and summary prompt generation
|
||||
|
||||
### v4.2.3
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Security**:
|
||||
- Fixed FTS5 injection vulnerability in search functions
|
||||
- Implemented proper double-quote escaping for FTS5 queries
|
||||
- Added comprehensive test suite with 332 injection attack tests
|
||||
- Affects: `search_observations`, `search_sessions`, `search_user_prompts` MCP tools
|
||||
|
||||
**Fixes**:
|
||||
- Fixed ESM/CJS compatibility for getDirname function in src/shared/paths.ts
|
||||
- Detects context using `typeof __dirname !== 'undefined'`
|
||||
- Falls back to `fileURLToPath(import.meta.url)` for ESM modules
|
||||
- Resolves path resolution issues across hook (ESM) and worker (CJS) contexts
|
||||
- Fixed Windows PowerShell compatibility issue with SessionStart hook
|
||||
- Replaced bash-specific test command `[` with cross-platform npm install command
|
||||
- Hook now runs `npm install` with quiet flags (fast and idempotent when dependencies exist)
|
||||
|
||||
**Technical Details**:
|
||||
- SessionSearch.ts now escapes double quotes in FTS5 queries: `query.replace(/"/g, '""')`
|
||||
- Updated `plugin/hooks/hooks.json` SessionStart command to use standard shell syntax
|
||||
- Changed from: `[ ! -d ... ] && cd ... && npm install && node ... || node ...`
|
||||
- Changed to: `cd ... && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ...`
|
||||
- Dependencies are installed in marketplace folder (parent of CLAUDE_PLUGIN_ROOT) where root package.json exists
|
||||
- getDirname function now properly handles both CommonJS (__dirname) and ES modules (import.meta.url)
|
||||
|
||||
### v4.2.0
|
||||
**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**:
|
||||
- Graceful session cleanup (marks complete instead of DELETE)
|
||||
- Restored MCP search server from backup
|
||||
- Updated dependencies (claude-agent-sdk 0.1.23, MCP SDK 1.20.1)
|
||||
|
||||
**Fixes**:
|
||||
- `/clear` command now skips cleanup to prevent session interruption
|
||||
- Session workers can finish pending operations naturally
|
||||
|
||||
### v4.0.0
|
||||
**Breaking Changes**:
|
||||
- Data directory relocated to `${CLAUDE_PLUGIN_ROOT}/data/`
|
||||
- Fresh start required (no migration from v3.x)
|
||||
- Worker auto-starts in SessionStart hook
|
||||
|
||||
**Features**:
|
||||
- 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
|
||||
|
||||
**Changes**:
|
||||
- Improved session continuity
|
||||
- Enhanced error handling
|
||||
- Better process cleanup
|
||||
|
||||
### Earlier Versions (v3.x)
|
||||
- v3.9.17: MCP integration, hookSpecificOutput JSON format
|
||||
- v3.7.1: SQLite storage backend
|
||||
- Earlier: Mintlify documentation, statusline support
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Graceful Cleanup (v4.1.0)
|
||||
Changed from aggressive session deletion (HTTP DELETE to workers) to graceful completion (marking sessions complete and allowing workers to finish). This prevents interruption of important operations like summary generation.
|
||||
|
||||
### FTS5 for Search Performance
|
||||
Implements SQLite FTS5 (Full-Text Search) virtual tables with automatic synchronization triggers, enabling fast full-text search across thousands of observations without performance degradation.
|
||||
|
||||
### Multi-Prompt Session Support
|
||||
Tracks `prompt_counter` and `prompt_number` across sessions and observations, enabling context preservation across conversation restarts within the same coding session.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Worker Service Issues
|
||||
- Check PM2 status: `pm2 list`
|
||||
- View logs: `npm run worker:logs`
|
||||
- Restart worker: `npm run worker:restart`
|
||||
|
||||
### Database Issues
|
||||
- Database location: `~/.claude-mem/claude-mem.db`
|
||||
- Check schema: `sqlite3 <db-path> ".schema"`
|
||||
- FTS5 tables are automatically synchronized via triggers
|
||||
|
||||
### Hook Issues
|
||||
- Hooks output to Claude Code's hook execution log
|
||||
- Check `plugin/scripts/` for built executables
|
||||
|
||||
### Model Configuration Issues
|
||||
- Use `./claude-mem-settings.sh` to manage model settings
|
||||
- Settings stored in `~/.claude/settings.json`
|
||||
- Default fallback: `claude-sonnet-4-5`
|
||||
|
||||
## Citations & References
|
||||
|
||||
This project uses the `claude-mem://` URI scheme for citations:
|
||||
- `claude-mem://observation/{id}` - References specific observations
|
||||
- `claude-mem://session/{id}` - References specific sessions
|
||||
|
||||
All MCP search results include citations, enabling Claude to reference specific historical context.
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0
|
||||
|
||||
## Repository
|
||||
|
||||
https://github.com/thedotmack/claude-mem
|
||||
@@ -1,709 +1,249 @@
|
||||
# Claude-Mem
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://github.com/thedotmack/claude-mem">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/claude-mem-logo-for-dark-mode.webp">
|
||||
<source media="(prefers-color-scheme: light)" srcset="docs/claude-mem-logo-for-light-mode.webp">
|
||||
<img src="docs/claude-mem-logo-for-light-mode.webp" alt="Claude-Mem" width="400">
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
**Persistent memory compression system for Claude Code**
|
||||
<h4 align="center">Persistent memory compression system built for <a href="https://claude.com/claude-code" target="_blank">Claude Code</a>.</h4>
|
||||
|
||||
Claude-Mem seamlessly preserves context across sessions by automatically capturing tool usage observations, generating semantic summaries, and making them available to future sessions. This enables Claude to maintain continuity of knowledge about projects even after sessions end or reconnect.
|
||||
<p align="center">
|
||||
<a href="LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
|
||||
</a>
|
||||
<a href="package.json">
|
||||
<img src="https://img.shields.io/badge/version-4.2.3-green.svg" alt="Version">
|
||||
</a>
|
||||
<a href="package.json">
|
||||
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg" alt="Node">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[](LICENSE)
|
||||
[](package.json)
|
||||
[](package.json)
|
||||
<p align="center">
|
||||
<a href="#quick-start">Quick Start</a> •
|
||||
<a href="#how-it-works">How It Works</a> •
|
||||
<a href="#mcp-search-tools">Search Tools</a> •
|
||||
<a href="#documentation">Documentation</a> •
|
||||
<a href="#configuration">Configuration</a> •
|
||||
<a href="#troubleshooting">Troubleshooting</a> •
|
||||
<a href="#license">License</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Claude-Mem seamlessly preserves context across sessions by automatically capturing tool usage observations, generating semantic summaries, and making them available to future sessions. This enables Claude to maintain continuity of knowledge about projects even after sessions end or reconnect.
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## What's New in v4.0.0
|
||||
## Quick Start
|
||||
|
||||
**BREAKING CHANGES - Please Read:**
|
||||
Start a new Claude Code session in the terminal and enter the following commands:
|
||||
|
||||
- **Data Location Changed**: Database moved from `~/.claude-mem/` to `${CLAUDE_PLUGIN_ROOT}/data/` (inside plugin directory)
|
||||
- **Fresh Start Required**: No automatic migration from v3.x. Users must start with a clean database
|
||||
- **Worker Auto-Starts**: Worker service now starts automatically - no manual `npm run worker:start` needed
|
||||
- **MCP Search Server**: 6 new search tools with full-text search and citations
|
||||
- **Enhanced Architecture**: Improved plugin integration and data organization
|
||||
```
|
||||
> /plugin marketplace add thedotmack/claude-mem
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for complete details.
|
||||
> /plugin install claude-mem
|
||||
```
|
||||
|
||||
Restart Claude Code. Context from previous sessions will automatically appear in new sessions.
|
||||
|
||||
**Key Features:**
|
||||
- 🧠 **Persistent Memory** - Context survives across sessions
|
||||
- 🔍 **7 Search Tools** - Query your project history via MCP
|
||||
- 🤖 **Automatic Operation** - No manual intervention required
|
||||
- 📊 **FTS5 Search** - Fast full-text search across observations
|
||||
- 🔗 **Citations** - Reference past decisions with `claude-mem://` URIs
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
## Documentation
|
||||
|
||||
- [Overview](#overview)
|
||||
- [How It Works](#how-it-works)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [MCP Search Tools](#mcp-search-tools)
|
||||
- [Architecture](#architecture)
|
||||
- [Configuration](#configuration)
|
||||
- [Development](#development)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [License](#license)
|
||||
📚 **[View Full Documentation](docs/)** - Browse markdown docs on GitHub
|
||||
|
||||
---
|
||||
💻 **Local Preview**: Run Mintlify docs locally:
|
||||
```bash
|
||||
cd docs
|
||||
npx mintlify dev
|
||||
```
|
||||
|
||||
## Overview
|
||||
### Getting Started
|
||||
- **[Installation Guide](docs/installation.mdx)** - Quick start & advanced installation
|
||||
- **[Usage Guide](docs/usage/getting-started.mdx)** - How Claude-Mem works automatically
|
||||
- **[MCP Search Tools](docs/usage/search-tools.mdx)** - Query your project history
|
||||
|
||||
### What is Claude-Mem?
|
||||
### Architecture
|
||||
- **[Overview](docs/architecture/overview.mdx)** - System components & data flow
|
||||
- **[Hooks](docs/architecture/hooks.mdx)** - 5 lifecycle hooks explained
|
||||
- **[Worker Service](docs/architecture/worker-service.mdx)** - HTTP API & PM2 management
|
||||
- **[Database](docs/architecture/database.mdx)** - SQLite schema & FTS5 search
|
||||
- **[MCP Search](docs/architecture/mcp-search.mdx)** - 7 search tools & examples
|
||||
|
||||
Claude-Mem is a **Claude Code plugin** that provides persistent memory across sessions. When you work with Claude Code on a project, claude-mem:
|
||||
|
||||
1. **Captures** every tool execution (Read, Write, Bash, Edit, etc.)
|
||||
2. **Processes** observations through Claude Agent SDK to extract learnings
|
||||
3. **Summarizes** what was accomplished, learned, and what's next
|
||||
4. **Restores** context automatically when you start new sessions
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Session Continuity**: Knowledge persists across Claude Code sessions
|
||||
- **Automatic Context Injection**: Recent session summaries appear when Claude starts
|
||||
- **Auto-Starting Worker**: Worker service starts automatically on first session (v4.0+)
|
||||
- **MCP Search Server**: Search and retrieve observations and sessions via 6 specialized tools
|
||||
- **Structured Observations**: XML-formatted extraction of learnings
|
||||
- **Smart Filtering**: Skips low-value tool observations
|
||||
- **Multi-Prompt Sessions**: Tracks multiple prompts within a single session
|
||||
- **HTTP API**: Modern REST interface for hook communication
|
||||
- **Process Management**: PM2-managed long-running service
|
||||
- **Graceful Degradation**: Doesn't block Claude even if worker is down
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **Node.js**: 18.0.0 or higher
|
||||
- **Claude Code**: Latest version with plugin support
|
||||
- **PM2**: Process manager (bundled with plugin - no global install required)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
### Configuration & Development
|
||||
- **[Configuration](docs/configuration.mdx)** - Environment variables & settings
|
||||
- **[Development](docs/development.mdx)** - Building, testing, contributing
|
||||
- **[Troubleshooting](docs/troubleshooting.mdx)** - Common issues & solutions
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### The Full Lifecycle
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. Session Starts → Context Hook Fires │
|
||||
│ Injects summaries from last 3 sessions into Claude's context │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. User Types Prompt → UserPromptSubmit Hook Fires │
|
||||
│ Creates SDK session in database, notifies worker service │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. Claude Uses Tools → PostToolUse Hook Fires (100+ times) │
|
||||
│ Sends observations to worker service for processing │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. Worker Processes → Claude Agent SDK Analyzes │
|
||||
│ Extracts structured learnings via iterative AI processing │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 5. Claude Stops → Stop Hook Fires │
|
||||
│ Generates final summary with request, status, next steps │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 6. Session Ends → Cleanup Hook Fires │
|
||||
│ Marks session complete, ready for next session context │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Session Start → Inject context from last 10 sessions │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ User Prompts → Create session, save user prompts │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Tool Executions → Capture observations (Read, Write, etc.) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Worker Processes → Extract learnings via Claude Agent SDK │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Session Ends → Generate summary, ready for next session │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Core Components
|
||||
**Core Components:**
|
||||
1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd
|
||||
2. **Worker Service** - HTTP API on port 37777 managed by PM2
|
||||
3. **SQLite Database** - Stores sessions, observations, summaries with FTS5 search
|
||||
4. **7 MCP Search Tools** - Query historical context with citations
|
||||
|
||||
#### 1. Plugin Hooks (5 Lifecycle Hooks)
|
||||
|
||||
- **SessionStart Hook** (`context-hook.js`): Queries database for last 3 sessions and injects context
|
||||
- **UserPromptSubmit Hook** (`new-hook.js`): Creates/reuses SDK session, sends init signal
|
||||
- **PostToolUse Hook** (`save-hook.js`): Sends tool observations to worker service
|
||||
- **Stop Hook** (`summary-hook.js`): Triggers final summary generation
|
||||
- **SessionEnd Hook** (`cleanup-hook.js`): Marks session as completed/failed
|
||||
|
||||
#### 2. Worker Service
|
||||
|
||||
Long-running HTTP service (managed by PM2) that:
|
||||
|
||||
- Listens on dynamic port 37000-37999
|
||||
- Provides REST API for hook communication
|
||||
- Maintains active session state in memory
|
||||
- Routes observations to Claude Agent SDK
|
||||
- Writes processed summaries back to database
|
||||
|
||||
**Endpoints:**
|
||||
- `POST /sessions/:id/init` - Initialize session
|
||||
- `POST /sessions/:id/observations` - Queue tool observations
|
||||
- `POST /sessions/:id/summarize` - Generate summary
|
||||
- `GET /sessions/:id/status` - Check status
|
||||
- `DELETE /sessions/:id` - Clean up session
|
||||
- `GET /health` - Health check
|
||||
|
||||
#### 3. SDK Memory Processor
|
||||
|
||||
Uses Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) to:
|
||||
|
||||
- Build specialized XML-structured prompts
|
||||
- Feed observations through iterative cycles
|
||||
- Parse Claude's XML responses for structured data
|
||||
- Accumulate learnings about modifications, discoveries, decisions
|
||||
- Generate final summaries with lessons learned and next steps
|
||||
|
||||
#### 4. MCP Search Server
|
||||
|
||||
Claude-Mem includes a Model Context Protocol (MCP) server that exposes 6 specialized search tools for querying stored observations and sessions:
|
||||
|
||||
**Search Tools:**
|
||||
- `search_observations` - Full-text search across observation titles, narratives, facts, and concepts
|
||||
- `search_sessions` - Full-text search across session summaries, requests, and learnings
|
||||
- `find_by_concept` - Find observations tagged with specific concepts
|
||||
- `find_by_file` - Find observations and sessions that reference specific file paths
|
||||
- `find_by_type` - Find observations by type (decision, bugfix, feature, refactor, discovery, change)
|
||||
- `advanced_search` - Combined search with filters across observations and sessions
|
||||
|
||||
All search results are returned in `search_result` format with **citations enabled**, allowing Claude to reference specific observations and sessions from your project history using the `claude-mem://` URI scheme.
|
||||
|
||||
**Configuration:** The MCP server is automatically registered via `plugin/.mcp.json` and runs when Claude Code starts.
|
||||
|
||||
#### 5. Database Layer
|
||||
|
||||
SQLite database (`${CLAUDE_PLUGIN_ROOT}/data/claude-mem.db`) with tables:
|
||||
|
||||
- **sdk_sessions**: Active/completed session tracking
|
||||
- **observations**: Individual tool executions with FTS5 full-text search
|
||||
- **session_summaries**: Processed semantic summaries with FTS5 full-text search
|
||||
- **sessions**, **memories**, **overviews**: Legacy tables
|
||||
|
||||
**Full-Text Search:** The database includes FTS5 (Full-Text Search) virtual tables for fast searching across observations and summaries, powering the MCP search tools.
|
||||
See [Architecture Overview](docs/architecture/overview.mdx) for details.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
## MCP Search Tools
|
||||
|
||||
### Prerequisites
|
||||
Claude-Mem provides 7 specialized search tools:
|
||||
|
||||
```bash
|
||||
# Ensure Node.js 18+ is installed
|
||||
node --version # Should be >= 18.0.0
|
||||
```
|
||||
|
||||
### Method 1: Clone and Build (Recommended for Development)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build hooks and worker service
|
||||
npm run build
|
||||
|
||||
# Worker service will auto-start on first Claude Code session
|
||||
# Or manually start with:
|
||||
npm run worker:start
|
||||
|
||||
# Verify worker is running
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
### Method 2: NPM Package (Coming Soon)
|
||||
|
||||
```bash
|
||||
# Install from NPM (when published)
|
||||
npm install -g claude-mem
|
||||
|
||||
# Worker service auto-starts on first hook execution
|
||||
```
|
||||
|
||||
### Post-Installation
|
||||
|
||||
1. **Verify Plugin Installation**
|
||||
|
||||
Check that hooks are configured in Claude Code:
|
||||
```bash
|
||||
cat plugin/hooks/hooks.json
|
||||
```
|
||||
|
||||
2. **Data Directory Location**
|
||||
|
||||
v4.0.0+ stores data in `${CLAUDE_PLUGIN_ROOT}/data/`:
|
||||
- Database: `${CLAUDE_PLUGIN_ROOT}/data/claude-mem.db`
|
||||
- Worker port file: `${CLAUDE_PLUGIN_ROOT}/data/worker.port`
|
||||
- Logs: `${CLAUDE_PLUGIN_ROOT}/data/logs/`
|
||||
|
||||
For development/testing, you can override:
|
||||
```bash
|
||||
export CLAUDE_MEM_DATA_DIR=/custom/path
|
||||
```
|
||||
|
||||
3. **Check Worker Logs**
|
||||
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
4. **Test Context Retrieval**
|
||||
|
||||
```bash
|
||||
npm run test:context
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Automatic Operation
|
||||
|
||||
Claude-Mem works automatically once installed. No manual intervention required!
|
||||
|
||||
1. **Start Claude Code** - Context from last 3 sessions appears automatically
|
||||
2. **Work normally** - Every tool execution is captured
|
||||
3. **Stop Claude** - Summary is generated and saved
|
||||
4. **Next session** - Previous work appears in context
|
||||
|
||||
### MCP Search Tools
|
||||
|
||||
Once claude-mem is installed as a plugin, six search tools become available in your Claude Code sessions:
|
||||
1. **search_observations** - Full-text search across observations
|
||||
2. **search_sessions** - Full-text search across session summaries
|
||||
3. **search_user_prompts** - Search raw user requests
|
||||
4. **find_by_concept** - Find by concept tags
|
||||
5. **find_by_file** - Find by file references
|
||||
6. **find_by_type** - Find by type (decision, bugfix, feature, etc.)
|
||||
7. **get_recent_context** - Get recent session context
|
||||
|
||||
**Example Queries:**
|
||||
|
||||
```
|
||||
"Use search_observations to find all decisions about the build system"
|
||||
→ Searches for observations with type="decision" and content matching "build system"
|
||||
|
||||
"Use find_by_file to show me everything related to worker-service.ts"
|
||||
→ Returns all observations and sessions that read/modified worker-service.ts
|
||||
|
||||
"Use search_sessions to find what we learned about hooks"
|
||||
→ Searches session summaries for mentions of "hooks" in learnings
|
||||
|
||||
"Use find_by_concept to show observations tagged with 'architecture'"
|
||||
→ Returns observations tagged with the concept "architecture"
|
||||
search_observations with query="authentication" and type="decision"
|
||||
find_by_file with filePath="worker-service.ts"
|
||||
search_user_prompts with query="add dark mode"
|
||||
get_recent_context with limit=5
|
||||
```
|
||||
|
||||
All results include:
|
||||
- **Citations**: Results use `claude-mem://observation/{id}` or `claude-mem://session/{id}` URIs
|
||||
- **Metadata**: Type, concepts, files, facts, and dates
|
||||
- **Rich Context**: Full observation narratives and session summaries
|
||||
- **Filtering**: Project, date ranges, types, concepts, files
|
||||
|
||||
### Manual Commands
|
||||
|
||||
#### Worker Management
|
||||
|
||||
**Note**: v4.0+ auto-starts the worker on first session. Manual commands below are optional.
|
||||
|
||||
```bash
|
||||
# Start worker service (optional - auto-starts automatically)
|
||||
npm run worker:start
|
||||
|
||||
# Stop worker service
|
||||
npm run worker:stop
|
||||
|
||||
# Restart worker service
|
||||
npm run worker:restart
|
||||
|
||||
# View worker logs
|
||||
npm run worker:logs
|
||||
|
||||
# Check worker status
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
#### Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Test context injection
|
||||
npm run test:context
|
||||
|
||||
# Verbose context test
|
||||
npm run test:context:verbose
|
||||
```
|
||||
|
||||
#### Development
|
||||
|
||||
```bash
|
||||
# Build hooks and worker
|
||||
npm run build
|
||||
|
||||
# Build only hooks
|
||||
npm run build:hooks
|
||||
|
||||
# Publish to NPM (maintainers only)
|
||||
npm run publish:npm
|
||||
```
|
||||
|
||||
### Viewing Stored Context
|
||||
|
||||
Context is stored in SQLite database. Location varies by version:
|
||||
- v4.0+: `${CLAUDE_PLUGIN_ROOT}/data/claude-mem.db` (inside plugin)
|
||||
- v3.x: `~/.claude-mem/claude-mem.db` (legacy)
|
||||
|
||||
Query the database directly:
|
||||
|
||||
```bash
|
||||
# v4.0+ uses ~/.claude-mem directory
|
||||
sqlite3 ~/.claude-mem/claude-mem.db
|
||||
|
||||
# View recent sessions
|
||||
SELECT session_id, project, created_at, status FROM sdk_sessions ORDER BY created_at DESC LIMIT 10;
|
||||
|
||||
# View session summaries
|
||||
SELECT session_id, request, completed, learned FROM session_summaries ORDER BY created_at DESC LIMIT 5;
|
||||
|
||||
# View observations for a session
|
||||
SELECT tool_name, created_at FROM observations WHERE session_id = 'YOUR_SESSION_ID';
|
||||
```
|
||||
See [MCP Search Tools Guide](docs/usage/search-tools.mdx) for detailed examples.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
## What's New in v4.2.3
|
||||
|
||||
### Technology Stack
|
||||
**Security:**
|
||||
- Fixed FTS5 injection vulnerability in search functions
|
||||
- Added comprehensive test suite with 332 injection attack tests
|
||||
|
||||
| Layer | Technology |
|
||||
|------------------------|-------------------------------------------|
|
||||
| **Language** | TypeScript (ES2022, ESNext modules) |
|
||||
| **Runtime** | Node.js 18+ |
|
||||
| **Database** | SQLite 3 with better-sqlite3 driver |
|
||||
| **HTTP Server** | Express.js 4.18 |
|
||||
| **AI SDK** | @anthropic-ai/claude-agent-sdk |
|
||||
| **Build Tool** | esbuild (bundles TypeScript) |
|
||||
| **Process Manager** | PM2 |
|
||||
| **Testing** | Node.js built-in test runner |
|
||||
**Fixes:**
|
||||
- Fixed ESM/CJS compatibility for getDirname function
|
||||
- Fixed Windows PowerShell compatibility in SessionStart hook
|
||||
- Cross-platform dependency installation now works on Windows, macOS, and Linux
|
||||
|
||||
### Directory Structure
|
||||
See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
|
||||
```
|
||||
claude-mem/
|
||||
├── src/
|
||||
│ ├── bin/hooks/ # Entry point scripts for 5 hooks
|
||||
│ │ ├── context-hook.ts # SessionStart
|
||||
│ │ ├── new-hook.ts # UserPromptSubmit
|
||||
│ │ ├── save-hook.ts # PostToolUse
|
||||
│ │ ├── summary-hook.ts # Stop
|
||||
│ │ └── cleanup-hook.ts # SessionEnd
|
||||
│ │
|
||||
│ ├── hooks/ # Hook implementation logic
|
||||
│ │ ├── context.ts
|
||||
│ │ ├── new.ts
|
||||
│ │ ├── save.ts
|
||||
│ │ ├── summary.ts
|
||||
│ │ └── cleanup.ts
|
||||
│ │
|
||||
│ ├── servers/ # MCP servers
|
||||
│ │ └── search-server.ts # MCP search tools server
|
||||
│ │
|
||||
│ ├── sdk/ # Claude Agent SDK integration
|
||||
│ │ ├── prompts.ts # XML prompt builders
|
||||
│ │ ├── parser.ts # XML response parser
|
||||
│ │ └── worker.ts # Main SDK agent loop
|
||||
│ │
|
||||
│ ├── services/
|
||||
│ │ ├── worker-service.ts # Express HTTP service
|
||||
│ │ └── sqlite/ # Database layer
|
||||
│ │ ├── Database.ts
|
||||
│ │ ├── HooksDatabase.ts
|
||||
│ │ ├── SessionSearch.ts # FTS5 search service
|
||||
│ │ ├── migrations.ts
|
||||
│ │ └── types.ts
|
||||
│ │
|
||||
│ ├── shared/ # Shared utilities
|
||||
│ │ ├── config.ts
|
||||
│ │ ├── paths.ts
|
||||
│ │ └── storage.ts
|
||||
│ │
|
||||
│ └── utils/
|
||||
│ ├── logger.ts
|
||||
│ ├── platform.ts
|
||||
│ └── port-allocator.ts
|
||||
│
|
||||
├── plugin/ # Plugin distribution
|
||||
│ ├── .claude-plugin/
|
||||
│ │ └── plugin.json
|
||||
│ ├── .mcp.json # MCP server configuration
|
||||
│ ├── hooks/
|
||||
│ │ └── hooks.json
|
||||
│ └── scripts/ # Built executables
|
||||
│ ├── context-hook.js
|
||||
│ ├── new-hook.js
|
||||
│ ├── save-hook.js
|
||||
│ ├── summary-hook.js
|
||||
│ ├── cleanup-hook.js
|
||||
│ └── search-server.js # MCP search server
|
||||
│
|
||||
├── dist/ # Built output
|
||||
│ └── worker-service.cjs
|
||||
│
|
||||
├── tests/ # Test suite
|
||||
├── context/ # Architecture docs
|
||||
└── ecosystem.config.cjs # PM2 configuration
|
||||
```
|
||||
---
|
||||
|
||||
### Data Flow
|
||||
## System Requirements
|
||||
|
||||
#### Memory Pipeline
|
||||
```
|
||||
Hook (stdin) → Database → Worker Service → SDK Processor → Database → Next Session Hook
|
||||
```
|
||||
- **Node.js**: 18.0.0 or higher
|
||||
- **Claude Code**: Latest version with plugin support
|
||||
- **PM2**: Process manager (bundled - no global install required)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
|
||||
1. **Input**: Claude Code sends tool execution data via stdin to hooks
|
||||
2. **Storage**: Hooks write observations to SQLite database
|
||||
3. **Processing**: Worker service reads observations, processes via SDK
|
||||
4. **Output**: Processed summaries written back to database
|
||||
5. **Retrieval**: Next session's context hook reads summaries from database
|
||||
---
|
||||
|
||||
#### Search Pipeline
|
||||
```
|
||||
Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Search Results → Claude
|
||||
```
|
||||
## Key Benefits
|
||||
|
||||
1. **Query**: Claude uses MCP search tools (e.g., `search_observations`)
|
||||
2. **Search**: MCP server calls SessionSearch service with query parameters
|
||||
3. **FTS5**: Full-text search executes against FTS5 virtual tables
|
||||
4. **Format**: Results formatted as `search_result` blocks with citations
|
||||
5. **Return**: Claude receives citable search results for analysis
|
||||
### Automatic Memory
|
||||
- Context automatically injected when Claude starts
|
||||
- No manual commands or configuration needed
|
||||
- Works transparently in the background
|
||||
|
||||
### Full History Search
|
||||
- Search across all sessions and observations
|
||||
- FTS5 full-text search for fast queries
|
||||
- Citations link back to specific observations
|
||||
|
||||
### Structured Observations
|
||||
- AI-powered extraction of learnings
|
||||
- Categorized by type (decision, bugfix, feature, etc.)
|
||||
- Tagged with concepts and file references
|
||||
|
||||
### Multi-Prompt Sessions
|
||||
- Sessions span multiple user prompts
|
||||
- Context preserved across `/clear` commands
|
||||
- Track entire conversation threads
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|-------------------------|---------------------------------|---------------------------------------|
|
||||
| `CLAUDE_PLUGIN_ROOT` | Set by Claude Code | Plugin installation directory |
|
||||
| `CLAUDE_MEM_DATA_DIR` | `${CLAUDE_PLUGIN_ROOT}/data/` | Data directory override (dev only) |
|
||||
| `CLAUDE_MEM_WORKER_PORT`| `0` (dynamic) | Worker service port (37000-37999) |
|
||||
| `NODE_ENV` | `production` | Environment mode |
|
||||
| `FORCE_COLOR` | `1` | Enable colored logs |
|
||||
|
||||
### Files and Directories
|
||||
|
||||
**v4.0.0+ Structure:**
|
||||
|
||||
```
|
||||
${CLAUDE_PLUGIN_ROOT}/data/
|
||||
├── claude-mem.db # SQLite database
|
||||
├── worker.port # Current worker port file
|
||||
└── logs/
|
||||
├── worker-out.log # Worker stdout logs
|
||||
└── worker-error.log # Worker stderr logs
|
||||
**Model Selection:**
|
||||
```bash
|
||||
./claude-mem-settings.sh
|
||||
```
|
||||
|
||||
**Legacy (v3.x):**
|
||||
**Environment Variables:**
|
||||
- `CLAUDE_MEM_MODEL` - AI model for processing (default: claude-sonnet-4-5)
|
||||
- `CLAUDE_MEM_WORKER_PORT` - Worker port (default: 37777)
|
||||
- `CLAUDE_MEM_DATA_DIR` - Data directory override (dev only)
|
||||
|
||||
```
|
||||
~/.claude-mem/ # Old location (no longer used)
|
||||
```
|
||||
|
||||
### Plugin Configuration
|
||||
|
||||
#### Hooks Configuration
|
||||
|
||||
Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"SessionStart": {
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 180000
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
|
||||
"timeout": 60000
|
||||
},
|
||||
"PostToolUse": {
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
|
||||
"timeout": 180000
|
||||
},
|
||||
"Stop": {
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
|
||||
"timeout": 60000
|
||||
},
|
||||
"SessionEnd": {
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
|
||||
"timeout": 60000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### MCP Server Configuration
|
||||
|
||||
The MCP search server is configured in `plugin/.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-mem-search": {
|
||||
"type": "stdio",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This registers the `claude-mem-search` server with Claude Code, making the 6 search tools available in all sessions. The server is automatically started when Claude Code launches and communicates via stdio transport.
|
||||
See [Configuration Guide](docs/configuration.mdx) for details.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Building from Source
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
# Clone and build
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build all components
|
||||
npm run build
|
||||
```
|
||||
|
||||
The build process:
|
||||
1. Compiles TypeScript to JavaScript using esbuild
|
||||
2. Creates standalone executables for each hook in `plugin/scripts/`
|
||||
3. Bundles MCP search server to `plugin/scripts/search-server.js`
|
||||
4. Bundles worker service to `dist/worker-service.cjs`
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Run specific test file
|
||||
node --test tests/session-lifecycle.test.ts
|
||||
# Start worker
|
||||
npm run worker:start
|
||||
|
||||
# View logs
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Make changes to TypeScript source files
|
||||
2. Run `npm run build` to compile
|
||||
3. Test with `npm run test:context` or start Claude Code
|
||||
4. Check worker logs with `npm run worker:logs`
|
||||
|
||||
### Adding New Features
|
||||
|
||||
#### Adding a New Hook
|
||||
|
||||
1. Create hook implementation in `src/hooks/your-hook.ts`
|
||||
2. Create entry point in `src/bin/hooks/your-hook.ts`
|
||||
3. Add to `plugin/hooks/hooks.json`
|
||||
4. Rebuild with `npm run build`
|
||||
|
||||
#### Modifying Database Schema
|
||||
|
||||
1. Add migration to `src/services/sqlite/migrations.ts`
|
||||
2. Update types in `src/services/sqlite/types.ts`
|
||||
3. Update database methods in `src/services/sqlite/HooksDatabase.ts`
|
||||
|
||||
#### Extending SDK Prompts
|
||||
|
||||
1. Modify prompts in `src/sdk/prompts.ts`
|
||||
2. Update parser in `src/sdk/parser.ts` to handle new XML structure
|
||||
3. Test with `npm test`
|
||||
See [Development Guide](docs/development.mdx) for detailed instructions.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Worker Service Issues
|
||||
**Common Issues:**
|
||||
- Worker not starting → `npm run worker:restart`
|
||||
- No context appearing → `npm run test:context`
|
||||
- Database issues → `sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"`
|
||||
- Search not working → Check FTS5 tables exist
|
||||
|
||||
**Problem**: Worker service not starting
|
||||
|
||||
```bash
|
||||
# Check if PM2 is running
|
||||
pm2 status
|
||||
|
||||
# Check worker logs
|
||||
npm run worker:logs
|
||||
|
||||
# Restart worker
|
||||
npm run worker:restart
|
||||
|
||||
# Full reset
|
||||
pm2 delete claude-mem-worker
|
||||
npm run worker:start
|
||||
```
|
||||
|
||||
**Problem**: Port allocation failed
|
||||
|
||||
```bash
|
||||
# Check if port file exists (v4.0+)
|
||||
cat ${CLAUDE_PLUGIN_ROOT}/data/worker.port
|
||||
|
||||
# Manually specify port
|
||||
CLAUDE_MEM_WORKER_PORT=37500 npm run worker:start
|
||||
```
|
||||
|
||||
### Hook Issues
|
||||
|
||||
**Problem**: Hooks not firing
|
||||
|
||||
```bash
|
||||
# Test context hook manually
|
||||
echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plugin/scripts/context-hook.js
|
||||
|
||||
# Check hook permissions
|
||||
ls -la plugin/scripts/*.js
|
||||
|
||||
# Verify hooks.json is valid
|
||||
cat plugin/hooks/hooks.json | jq .
|
||||
```
|
||||
|
||||
**Problem**: Context not appearing
|
||||
|
||||
```bash
|
||||
# Check if summaries exist
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM session_summaries;"
|
||||
|
||||
# View recent sessions
|
||||
npm run test:context:verbose
|
||||
|
||||
# Check database integrity
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
|
||||
```
|
||||
|
||||
### Database Issues
|
||||
|
||||
**Problem**: Database locked
|
||||
|
||||
```bash
|
||||
# Close all connections
|
||||
pm2 stop claude-mem-worker
|
||||
|
||||
# Check for stale locks
|
||||
lsof ~/.claude-mem/claude-mem.db
|
||||
|
||||
# Backup and recreate (nuclear option)
|
||||
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup
|
||||
rm ~/.claude-mem/claude-mem.db
|
||||
npm run worker:start # Will recreate schema
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable verbose logging:
|
||||
|
||||
```bash
|
||||
# Set debug mode
|
||||
export DEBUG=claude-mem:*
|
||||
|
||||
# View all logs
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
Check correlation IDs to trace observations through the pipeline:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT correlation_id, tool_name, created_at FROM observations WHERE session_id = 'YOUR_SESSION_ID' ORDER BY created_at;"
|
||||
```
|
||||
See [Troubleshooting Guide](docs/troubleshooting.mdx) for complete solutions.
|
||||
|
||||
---
|
||||
|
||||
@@ -712,17 +252,12 @@ sqlite3 ~/.claude-mem/claude-mem.db "SELECT correlation_id, tool_name, created_a
|
||||
Contributions are welcome! Please:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
2. Create a feature branch
|
||||
3. Make your changes with tests
|
||||
4. Update documentation
|
||||
5. Submit a Pull Request
|
||||
|
||||
### Code Style
|
||||
|
||||
- Use TypeScript strict mode
|
||||
- Follow existing code formatting
|
||||
- Add tests for new features
|
||||
- Update documentation as needed
|
||||
See [Development Guide](docs/development.mdx) for contribution workflow.
|
||||
|
||||
---
|
||||
|
||||
@@ -734,49 +269,21 @@ Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
|
||||
|
||||
See the [LICENSE](LICENSE) file for full details.
|
||||
|
||||
### What This Means
|
||||
|
||||
**What This Means:**
|
||||
- You can use, modify, and distribute this software freely
|
||||
- If you modify and deploy this software on a network server, you must make your source code available
|
||||
- Any derivative works must also be licensed under AGPL-3.0
|
||||
- If you modify and deploy on a network server, you must make your source code available
|
||||
- Derivative works must also be licensed under AGPL-3.0
|
||||
- There is NO WARRANTY for this software
|
||||
|
||||
For more information about AGPL-3.0, see: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: [docs/](docs/)
|
||||
- **Issues**: [GitHub Issues](https://github.com/thedotmack/claude-mem/issues)
|
||||
- **Repository**: [github.com/thedotmack/claude-mem](https://github.com/thedotmack/claude-mem)
|
||||
- **Author**: Alex Newman ([@thedotmack](https://github.com/thedotmack))
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### v4.0.0 (Current)
|
||||
|
||||
- **NEW**: MCP Search Server with 6 specialized search tools
|
||||
- **NEW**: FTS5 full-text search across observations and session summaries
|
||||
- **BREAKING**: Data directory moved to `${CLAUDE_PLUGIN_ROOT}/data/`
|
||||
- **NEW**: Auto-starting worker service
|
||||
- **NEW**: Session continuity with automatic context injection
|
||||
- Refactored summary and context handling in hooks
|
||||
- Implemented structured logging across the application
|
||||
- Improved error handling and graceful degradation
|
||||
|
||||
### v3.9.17
|
||||
|
||||
- **FIX**: Context hook now uses proper `hookSpecificOutput` JSON format
|
||||
- MCP Search Server with 6 specialized search tools
|
||||
- FTS5 full-text search across observations and session summaries
|
||||
- Refactored summary and context handling in hooks
|
||||
- Implemented structured logging across the application
|
||||
- Fixed race condition in summary generation
|
||||
- Added missing process.exit(0) calls in hook entry points
|
||||
- Improved error handling and graceful degradation
|
||||
|
||||
---
|
||||
|
||||
**Built with Claude Agent SDK** | **Powered by Claude Code** | **Made with TypeScript**
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Claude-Mem Architecture Refactor Plan
|
||||
# Claude-Mem Architecture v3 to v4 Plan (✅ Completed)
|
||||
|
||||
This file exists as a reference to explain the path forward from v3 to v4.
|
||||
|
||||
## Core Purpose
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
# Phase 1 Implementation - Complete ✅
|
||||
|
||||
Phase 1 of the REFACTOR-PLAN.md has been successfully implemented and tested.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Database Schema (Migration 004)
|
||||
Created four new tables to support the SDK agent architecture:
|
||||
|
||||
- **`sdk_sessions`** - Tracks SDK streaming sessions
|
||||
- **`observation_queue`** - Message queue for pending observations
|
||||
- **`observations`** - Stores extracted observations from SDK
|
||||
- **`session_summaries`** - Stores structured session summaries
|
||||
|
||||
All tables include proper indexes for performance and foreign key constraints for data integrity.
|
||||
|
||||
### 2. Shared Database Layer
|
||||
Created `HooksDatabase` class ([src/services/sqlite/HooksDatabase.ts](src/services/sqlite/HooksDatabase.ts)) that provides:
|
||||
|
||||
- Simple, synchronous database operations for hooks
|
||||
- No complex logic - just basic CRUD operations
|
||||
- Optimized SQLite settings (WAL mode, foreign keys enabled)
|
||||
- Methods for all hook operations:
|
||||
- `getRecentSummaries()` - Retrieve session context
|
||||
- `createSDKSession()` - Initialize new session
|
||||
- `queueObservation()` - Add observation to queue
|
||||
- `storeObservation()` - Save SDK observations
|
||||
- `storeSummary()` - Save session summaries
|
||||
- And more...
|
||||
|
||||
### 3. Hook Functions
|
||||
Implemented all four hook functions in [src/hooks/](src/hooks/):
|
||||
|
||||
#### **context.ts** - SessionStart Hook
|
||||
- Shows user recent session context on startup
|
||||
- Formats summaries in markdown for Claude
|
||||
- Exits silently if no context or errors occur
|
||||
|
||||
#### **save.ts** - PostToolUse Hook
|
||||
- Queues tool observations for SDK processing
|
||||
- Skips low-value tools (TodoWrite, ListMcpResourcesTool)
|
||||
- Non-blocking - returns immediately
|
||||
|
||||
#### **new.ts** - UserPromptSubmit Hook
|
||||
- Initializes SDK session in database
|
||||
- Prepares for SDK worker spawn (TODO in Phase 2)
|
||||
- Non-blocking - returns immediately
|
||||
|
||||
#### **summary.ts** - Stop Hook
|
||||
- Queues FINALIZE message for SDK
|
||||
- Signals SDK to generate session summary
|
||||
- Non-blocking - returns immediately
|
||||
|
||||
### 4. CLI Integration
|
||||
Added four new commands to [src/bin/cli.ts](src/bin/cli.ts:227-274):
|
||||
|
||||
```bash
|
||||
claude-mem context # SessionStart hook
|
||||
claude-mem new # UserPromptSubmit hook
|
||||
claude-mem save # PostToolUse hook
|
||||
claude-mem summary # Stop hook
|
||||
```
|
||||
|
||||
All commands read JSON input from stdin and execute the corresponding hook function.
|
||||
|
||||
### 5. Testing
|
||||
Created comprehensive test suite ([test-phase1.ts](test-phase1.ts)) that validates:
|
||||
|
||||
- ✅ Database schema migration 004 applied correctly
|
||||
- ✅ All four tables exist
|
||||
- ✅ SDK session creation and retrieval
|
||||
- ✅ Observation queue operations
|
||||
- ✅ Observation and summary storage
|
||||
- ✅ Session status transitions
|
||||
|
||||
**All tests pass! 🎉**
|
||||
|
||||
## What's Left for Phase 2
|
||||
|
||||
The foundation is complete. Next steps:
|
||||
|
||||
1. **SDK Worker Process** - Implement the background agent that:
|
||||
- Polls observation queue
|
||||
- Sends observations to Claude SDK
|
||||
- Parses XML responses (`<observation>` and `<summary>` blocks)
|
||||
- Stores results in database
|
||||
|
||||
2. **SDK Prompts** - Implement the three prompt builders:
|
||||
- `buildInitPrompt()` - Initialize SDK agent
|
||||
- `buildObservationPrompt()` - Send tool observation
|
||||
- `buildFinalizePrompt()` - Request session summary
|
||||
|
||||
3. **Process Management** - Update [src/hooks/new.ts](src/hooks/new.ts:35-42) to spawn SDK worker as detached process
|
||||
|
||||
4. **End-to-End Testing** - Test with real Claude Code session
|
||||
|
||||
## File Changes
|
||||
|
||||
### New Files
|
||||
- [src/services/sqlite/HooksDatabase.ts](src/services/sqlite/HooksDatabase.ts) - Shared database layer
|
||||
- [src/hooks/context.ts](src/hooks/context.ts) - SessionStart hook
|
||||
- [src/hooks/save.ts](src/hooks/save.ts) - PostToolUse hook
|
||||
- [src/hooks/new.ts](src/hooks/new.ts) - UserPromptSubmit hook
|
||||
- [src/hooks/summary.ts](src/hooks/summary.ts) - Stop hook
|
||||
- [src/hooks/index.ts](src/hooks/index.ts) - Exports
|
||||
- [test-phase1.ts](test-phase1.ts) - Test suite
|
||||
|
||||
### Modified Files
|
||||
- [src/services/sqlite/migrations.ts](src/services/sqlite/migrations.ts:205-315) - Added migration 004
|
||||
- [src/services/sqlite/index.ts](src/services/sqlite/index.ts:13) - Exported HooksDatabase
|
||||
- [src/bin/cli.ts](src/bin/cli.ts:227-274) - Added hook commands
|
||||
|
||||
## Verification
|
||||
|
||||
To verify Phase 1 implementation:
|
||||
|
||||
```bash
|
||||
# Build
|
||||
bun run build
|
||||
|
||||
# Run tests
|
||||
bun test-phase1.ts
|
||||
|
||||
# Check hook commands exist
|
||||
./dist/claude-mem.min.js --help | grep -A 1 'context\|new\|save\|summary'
|
||||
```
|
||||
|
||||
All should pass without errors.
|
||||
|
||||
## Next Steps
|
||||
|
||||
Ready to proceed to Phase 2: **SDK Worker Implementation**
|
||||
|
||||
The architecture is sound, the database layer is working, and all hook functions are ready to integrate with the SDK worker process.
|
||||
@@ -1,175 +0,0 @@
|
||||
# Phase 2 Implementation Complete
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 2 of the SDK Worker Process has been successfully implemented. This phase adds the background agent architecture that processes tool observations and generates session summaries.
|
||||
|
||||
## Implementation Date
|
||||
|
||||
October 15, 2025
|
||||
|
||||
## Files Created
|
||||
|
||||
### 1. SDK Prompts Module
|
||||
- **File**: [src/sdk/prompts.ts](src/sdk/prompts.ts)
|
||||
- **Purpose**: Generates prompts for the Claude Agent SDK
|
||||
- **Functions**:
|
||||
- `buildInitPrompt()` - Initialize the memory agent
|
||||
- `buildObservationPrompt()` - Send tool observations to agent
|
||||
- `buildFinalizePrompt()` - Request session summary
|
||||
|
||||
### 2. XML Parser Module
|
||||
- **File**: [src/sdk/parser.ts](src/sdk/parser.ts)
|
||||
- **Purpose**: Parse XML responses from SDK agent
|
||||
- **Functions**:
|
||||
- `parseObservations()` - Extract observation blocks
|
||||
- `parseSummary()` - Extract session summary
|
||||
- **Features**:
|
||||
- Validates observation types (decision, bugfix, feature, refactor, discovery)
|
||||
- Validates all required summary fields
|
||||
- Handles file arrays in summaries
|
||||
- No external dependencies (uses regex)
|
||||
|
||||
### 3. SDK Worker Process
|
||||
- **File**: [src/sdk/worker.ts](src/sdk/worker.ts)
|
||||
- **Purpose**: Background agent that processes observations
|
||||
- **Features**:
|
||||
- Runs as detached background process
|
||||
- Uses Claude Agent SDK streaming input mode
|
||||
- Polls observation queue every 1 second
|
||||
- Parses and stores observations and summaries
|
||||
- Handles graceful shutdown via FINALIZE message
|
||||
- Automatic error handling and session status updates
|
||||
|
||||
### 4. SDK Index Module
|
||||
- **File**: [src/sdk/index.ts](src/sdk/index.ts)
|
||||
- **Purpose**: Export all SDK module functionality
|
||||
|
||||
### 5. Test Suite
|
||||
- **File**: [test-phase2.ts](test-phase2.ts)
|
||||
- **Coverage**:
|
||||
- SDK prompt generation (3 tests)
|
||||
- XML observation parsing (4 tests)
|
||||
- XML summary parsing (4 tests)
|
||||
- Database integration (3 tests)
|
||||
- **Result**: ✅ All 14 tests passing
|
||||
|
||||
## Files Modified
|
||||
|
||||
### 1. newHook Implementation
|
||||
- **File**: [src/hooks/new.ts](src/hooks/new.ts:38-61)
|
||||
- **Changes**:
|
||||
- Uncommented SDK worker spawn code
|
||||
- Added worker path resolution (dev vs production)
|
||||
- Spawns worker as detached process with stdio: 'ignore'
|
||||
- Worker receives session DB ID as argument
|
||||
|
||||
## Architecture Validation
|
||||
|
||||
### SDK Worker Flow
|
||||
1. ✅ newHook spawns worker as detached process
|
||||
2. ✅ Worker loads session from database
|
||||
3. ✅ Worker initializes SDK agent with streaming input
|
||||
4. ✅ Worker polls observation queue continuously
|
||||
5. ✅ Worker sends observations to SDK agent
|
||||
6. ✅ Worker parses XML responses
|
||||
7. ✅ Worker stores observations and summaries
|
||||
8. ✅ Worker handles FINALIZE message
|
||||
9. ✅ Worker updates session status
|
||||
|
||||
### Data Flow
|
||||
```
|
||||
User Prompt → newHook → Create SDK Session → Spawn Worker
|
||||
↓
|
||||
Initialize SDK Agent
|
||||
↓
|
||||
← Poll Observation Queue
|
||||
↓
|
||||
Send Observations to SDK
|
||||
↓
|
||||
← Parse XML Response
|
||||
↓
|
||||
Store in Database
|
||||
↓
|
||||
Wait for FINALIZE
|
||||
↓
|
||||
Generate Summary → Exit
|
||||
```
|
||||
|
||||
## Test Results
|
||||
|
||||
```bash
|
||||
$ bun test ./test-phase2.ts
|
||||
|
||||
✅ SDK Prompts (3 tests)
|
||||
✅ should build init prompt with all required sections
|
||||
✅ should build observation prompt with tool details
|
||||
✅ should build finalize prompt with session context
|
||||
|
||||
✅ XML Parser (8 tests)
|
||||
✅ parseObservations
|
||||
✅ should parse single observation
|
||||
✅ should parse multiple observations
|
||||
✅ should skip observations with invalid types
|
||||
✅ should handle observations with surrounding text
|
||||
✅ parseSummary
|
||||
✅ should parse complete summary with all fields
|
||||
✅ should handle empty file arrays
|
||||
✅ should return null if required fields are missing
|
||||
✅ should return null if no summary block found
|
||||
|
||||
✅ HooksDatabase Integration (3 tests)
|
||||
✅ should store and retrieve observations
|
||||
✅ should store and retrieve summaries
|
||||
✅ should queue and process observations
|
||||
|
||||
14 pass, 0 fail, 53 expect() calls
|
||||
Ran 14 tests across 1 file. [60.00ms]
|
||||
```
|
||||
|
||||
## Build Verification
|
||||
|
||||
```bash
|
||||
$ npm run build
|
||||
|
||||
📌 Version: 3.9.16
|
||||
✓ Bun detected
|
||||
✓ Cleaned dist directory
|
||||
✓ Bundle created
|
||||
✓ Shebang added
|
||||
✓ Made executable
|
||||
✅ Build complete! (344.57 KB)
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
|
||||
All Phase 2 success criteria have been met:
|
||||
|
||||
- [x] SDK worker runs as detached process
|
||||
- [x] Worker polls observation queue continuously
|
||||
- [x] Worker sends observations to Claude SDK
|
||||
- [x] Worker parses `<observation>` and `<summary>` XML correctly
|
||||
- [x] Worker stores results in database using HooksDatabase
|
||||
- [x] Worker handles FINALIZE message and exits gracefully
|
||||
- [x] All tests pass
|
||||
- [x] No blocking of main Claude Code session
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Bundled CLI**: The worker process is currently bundled into the main CLI. For production use, we may want to extract it as a separate executable.
|
||||
2. **No logging**: Worker runs with `stdio: 'ignore'` for non-blocking behavior. Consider adding file-based logging for debugging.
|
||||
|
||||
## Next Steps
|
||||
|
||||
Phase 2 is complete and ready for integration testing with a real Claude Code session. The next phase would involve:
|
||||
|
||||
1. Testing the full end-to-end flow with actual tool observations
|
||||
2. Implementing the `saveHook` to queue observations
|
||||
3. Implementing the `summaryHook` to send FINALIZE message
|
||||
4. Verifying the context hook retrieves summaries correctly
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [REFACTOR-PLAN.md](REFACTOR-PLAN.md) - Original refactor plan
|
||||
- [PHASE1-COMPLETE.md](PHASE1-COMPLETE.md) - Phase 1 completion
|
||||
- [PHASE2-PROMPT.md](PHASE2-PROMPT.md) - Phase 2 implementation requirements
|
||||
@@ -1,118 +0,0 @@
|
||||
# Phase 2 Implementation Prompt
|
||||
|
||||
Use this prompt to start a new chat for Phase 2 implementation:
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
I'm implementing a refactor of the claude-mem memory system based on [REFACTOR-PLAN.md](REFACTOR-PLAN.md).
|
||||
|
||||
**Phase 1 is complete** (see [PHASE1-COMPLETE.md](PHASE1-COMPLETE.md)):
|
||||
- ✅ Database schema with migration 004
|
||||
- ✅ HooksDatabase shared layer
|
||||
- ✅ All four hook functions (context, new, save, summary)
|
||||
- ✅ CLI integration and tests passing
|
||||
|
||||
## Task
|
||||
|
||||
Implement **Phase 2: SDK Worker Process**
|
||||
|
||||
According to [REFACTOR-PLAN.md](REFACTOR-PLAN.md#2-userpromptsubmit-hook) (lines 296-423), I need to:
|
||||
|
||||
1. **Create SDK Worker Process** (`src/sdk/worker.ts`)
|
||||
- Uses Agent SDK streaming input mode
|
||||
- AsyncIterable message generator that:
|
||||
- Yields initial prompt
|
||||
- Polls observation_queue table
|
||||
- Yields observation prompts
|
||||
- Handles FINALIZE message
|
||||
- Parses SDK responses for `<observation>` and `<summary>` XML blocks
|
||||
- Stores results using HooksDatabase methods
|
||||
|
||||
2. **Create SDK Prompts** (`src/sdk/prompts.ts`)
|
||||
- `buildInitPrompt()` - Initialize agent (see REFACTOR-PLAN.md:537-595)
|
||||
- `buildObservationPrompt()` - Send tool observation (see REFACTOR-PLAN.md:601-634)
|
||||
- `buildFinalizePrompt()` - Request summary (see REFACTOR-PLAN.md:640-692)
|
||||
|
||||
3. **Create XML Parser** (`src/sdk/parser.ts`)
|
||||
- Parse `<observation>` blocks with `<type>` and `<text>`
|
||||
- Parse `<summary>` blocks with 8 required fields
|
||||
- Extract file arrays from `<file>` child elements
|
||||
|
||||
4. **Update newHook** ([src/hooks/new.ts](src/hooks/new.ts:35-42))
|
||||
- Uncomment SDK worker spawn code
|
||||
- Pass session ID to worker
|
||||
- Detached process with stdio: 'ignore'
|
||||
|
||||
5. **Test End-to-End**
|
||||
- Create test that simulates full lifecycle
|
||||
- Verify observations are queued, processed, and stored
|
||||
- Verify summary generation works
|
||||
|
||||
## Key Requirements
|
||||
|
||||
From [REFACTOR-PLAN.md](REFACTOR-PLAN.md):
|
||||
|
||||
- Use `@anthropic-ai/claude-agent-sdk` query function with streaming input mode
|
||||
- Model: `claude-sonnet-4-5`
|
||||
- Use `disallowedTools: ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch']`
|
||||
- Message generator yields `{ role: "user", content: string }` objects
|
||||
- Capture SDK session ID from system init message
|
||||
- Poll observation queue every 1 second
|
||||
- Use AbortController for graceful cancellation
|
||||
- Parse XML with a library (not regex) - suggest fast-xml-parser
|
||||
- Store observations and summaries using HooksDatabase methods
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
The SDK worker is a **synthesis engine** that:
|
||||
- Receives tool observations (not raw data)
|
||||
- Extracts meaningful insights
|
||||
- Stores atomic observations in SQLite
|
||||
- Generates structured summaries at session end
|
||||
|
||||
See [REFACTOR-PLAN.md](REFACTOR-PLAN.md#visual-overview) (lines 69-119) for the full architecture diagram.
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. `src/sdk/worker.ts` - Main SDK worker process
|
||||
2. `src/sdk/prompts.ts` - Prompt builders
|
||||
3. `src/sdk/parser.ts` - XML response parser
|
||||
4. `src/sdk/index.ts` - Exports
|
||||
5. `test-phase2.ts` - End-to-end tests
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. [src/hooks/new.ts](src/hooks/new.ts:35-42) - Spawn worker process
|
||||
2. [package.json](package.json) - May need to add fast-xml-parser dependency
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. Unit tests for prompts (verify prompt structure)
|
||||
2. Unit tests for parser (verify XML parsing)
|
||||
3. Integration test for worker (mock SDK responses)
|
||||
4. End-to-end test (simulate full observation → summary flow)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] SDK worker runs as detached process
|
||||
- [ ] Worker polls observation queue continuously
|
||||
- [ ] Worker sends observations to Claude SDK
|
||||
- [ ] Worker parses `<observation>` and `<summary>` XML correctly
|
||||
- [ ] Worker stores results in database using HooksDatabase
|
||||
- [ ] Worker handles FINALIZE message and exits gracefully
|
||||
- [ ] All tests pass
|
||||
- [ ] No blocking of main Claude Code session
|
||||
|
||||
## Notes
|
||||
|
||||
- Keep hooks fast and non-blocking (they already are)
|
||||
- SDK worker is fire-and-forget background process
|
||||
- Use HooksDatabase methods (already implemented in Phase 1)
|
||||
- Follow the exact prompt formats from REFACTOR-PLAN.md
|
||||
- Use proper TypeScript types from Agent SDK
|
||||
|
||||
---
|
||||
|
||||
**Start with:** Create the SDK prompts module first, then the parser, then the worker. Test each piece before integrating.
|
||||
@@ -1,271 +0,0 @@
|
||||
# Phase 3 Implementation Complete ✅
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 3 of the claude-mem architecture refactor has been successfully completed. This phase integrated all hook functions with the database layer and validated the complete end-to-end lifecycle through comprehensive testing.
|
||||
|
||||
## Implementation Date
|
||||
|
||||
October 15, 2025
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Hook Integration Verification
|
||||
|
||||
All four hook functions were verified to be working correctly with the database layer:
|
||||
|
||||
#### **[contextHook](src/hooks/context.ts)** - SessionStart Hook
|
||||
- ✅ Retrieves recent session summaries from database
|
||||
- ✅ Formats summaries in markdown for Claude consumption
|
||||
- ✅ Handles missing summaries gracefully
|
||||
- ✅ Only runs on startup (skips resume)
|
||||
- ✅ Fast, non-blocking operation (< 50ms)
|
||||
|
||||
#### **[newHook](src/hooks/new.ts)** - UserPromptSubmit Hook
|
||||
- ✅ Creates SDK session record in database
|
||||
- ✅ Spawns SDK worker as detached background process
|
||||
- ✅ Handles duplicate sessions gracefully
|
||||
- ✅ Fast, non-blocking operation (< 50ms)
|
||||
- ✅ Returns immediately with suppressed output
|
||||
|
||||
#### **[saveHook](src/hooks/save.ts)** - PostToolUse Hook
|
||||
- ✅ Queues tool observations to database
|
||||
- ✅ Filters out low-value tools (TodoWrite, ListMcpResourcesTool)
|
||||
- ✅ Handles missing sessions gracefully
|
||||
- ✅ Fast, non-blocking operation (< 50ms)
|
||||
- ✅ Stores JSON-stringified tool input/output
|
||||
|
||||
#### **[summaryHook](src/hooks/summary.ts)** - Stop Hook
|
||||
- ✅ Sends FINALIZE message to observation queue
|
||||
- ✅ Triggers SDK worker to generate session summary
|
||||
- ✅ Handles missing sessions gracefully
|
||||
- ✅ Fast, non-blocking operation (< 50ms)
|
||||
|
||||
### 2. Comprehensive Test Suite
|
||||
|
||||
Created two new comprehensive test files:
|
||||
|
||||
#### **[test-phase3-integration.ts](test-phase3-integration.ts)**
|
||||
Tests individual hook database integration:
|
||||
- ✅ Session management (create, find, update, complete)
|
||||
- ✅ Observation queue (queue, retrieve, process, FINALIZE)
|
||||
- ✅ Observations storage (store and retrieve)
|
||||
- ✅ Summaries (store and retrieve, project isolation)
|
||||
- **9 tests, all passing**
|
||||
|
||||
#### **[test-phase3-e2e.ts](test-phase3-e2e.ts)**
|
||||
Tests complete session lifecycle:
|
||||
- ✅ Full lifecycle: new → save → summary → context
|
||||
- ✅ Performance requirements (< 50ms per operation)
|
||||
- ✅ Interrupted sessions (observations remain in queue)
|
||||
- ✅ Multiple concurrent projects (project isolation)
|
||||
- **4 tests, all passing**
|
||||
|
||||
### 3. Database Integration
|
||||
|
||||
All hooks correctly use the [HooksDatabase](src/services/sqlite/HooksDatabase.ts) layer:
|
||||
- ✅ Simple, synchronous database operations
|
||||
- ✅ Foreign key constraints enforced
|
||||
- ✅ Proper session lifecycle management
|
||||
- ✅ Atomic operations with WAL mode
|
||||
- ✅ No complex logic in hooks (delegated to SDK worker)
|
||||
|
||||
### 4. CLI Commands
|
||||
|
||||
All four CLI commands verified working:
|
||||
- ✅ `claude-mem context` - [src/bin/cli.ts:228-234](src/bin/cli.ts#L228-L234)
|
||||
- ✅ `claude-mem new` - [src/bin/cli.ts:237-243](src/bin/cli.ts#L237-L243)
|
||||
- ✅ `claude-mem save` - [src/bin/cli.ts:246-252](src/bin/cli.ts#L246-L252)
|
||||
- ✅ `claude-mem summary` - [src/bin/cli.ts:255-261](src/bin/cli.ts#L255-L261)
|
||||
|
||||
All commands:
|
||||
- Read JSON from stdin
|
||||
- Execute corresponding hook function
|
||||
- Return proper JSON response
|
||||
- Exit with code 0
|
||||
|
||||
## Test Results
|
||||
|
||||
### All Tests Passing
|
||||
```bash
|
||||
Phase 1: ✅ Database schema and HooksDatabase tests
|
||||
Phase 2: ✅ 14 tests (SDK prompts, parser, database integration)
|
||||
Phase 3: ✅ 13 tests (9 integration + 4 e2e)
|
||||
Total: ✅ 27+ tests passing
|
||||
```
|
||||
|
||||
### Performance Validation
|
||||
```
|
||||
Average operation time: 0.04ms (well under 50ms requirement)
|
||||
Maximum operation time: 1.60ms (well under 100ms threshold)
|
||||
```
|
||||
|
||||
### Build Verification
|
||||
```bash
|
||||
✅ Build complete! (344.57 KB)
|
||||
Output: dist/claude-mem.min.js
|
||||
```
|
||||
|
||||
## Architecture Validation
|
||||
|
||||
### ✅ Complete Hook Lifecycle
|
||||
|
||||
```
|
||||
1. SessionStart (contextHook)
|
||||
↓ Retrieves recent summaries from database
|
||||
↓ Formats for Claude consumption
|
||||
↓
|
||||
2. UserPromptSubmit (newHook)
|
||||
↓ Creates SDK session
|
||||
↓ Spawns background SDK worker
|
||||
↓
|
||||
3. PostToolUse (saveHook)
|
||||
↓ Queues observations
|
||||
↓ SDK worker polls queue
|
||||
↓ SDK processes observations
|
||||
↓ SDK stores meaningful insights
|
||||
↓
|
||||
4. Stop (summaryHook)
|
||||
↓ Sends FINALIZE message
|
||||
↓ SDK generates structured summary
|
||||
↓ SDK stores summary in database
|
||||
↓
|
||||
5. Next SessionStart
|
||||
↓ New context retrieved
|
||||
⟲ Cycle repeats
|
||||
```
|
||||
|
||||
### ✅ Non-Blocking Requirements
|
||||
|
||||
All hooks meet the < 50ms performance requirement:
|
||||
- **contextHook**: Retrieves summaries (simple SELECT query)
|
||||
- **newHook**: Creates session + spawns detached process
|
||||
- **saveHook**: Inserts into queue (simple INSERT)
|
||||
- **summaryHook**: Inserts FINALIZE message (simple INSERT)
|
||||
|
||||
SDK worker runs in background independently of main session.
|
||||
|
||||
### ✅ Error Handling
|
||||
|
||||
All hooks handle errors gracefully:
|
||||
- Database errors → log + continue
|
||||
- Missing sessions → silently continue
|
||||
- Process spawn failures → log + continue
|
||||
- Never block Claude Code session
|
||||
|
||||
### ✅ Data Integrity
|
||||
|
||||
Foreign key constraints enforce referential integrity:
|
||||
- Observations reference SDK sessions
|
||||
- Summaries reference SDK sessions
|
||||
- Queue items reference SDK sessions
|
||||
- Sessions reference Claude sessions
|
||||
|
||||
## Success Criteria Met
|
||||
|
||||
All Phase 3 success criteria have been achieved:
|
||||
|
||||
- [x] saveHook queues observations to database
|
||||
- [x] summaryHook sends FINALIZE message
|
||||
- [x] contextHook retrieves and formats summaries
|
||||
- [x] End-to-end test passes (full lifecycle)
|
||||
- [x] All hooks respond in < 50ms
|
||||
- [x] Worker processes observations and generates summary
|
||||
- [x] CLI commands work correctly
|
||||
- [x] All tests pass (27+ tests)
|
||||
- [x] Build succeeds (344.57 KB)
|
||||
- [x] Database foreign key constraints enforced
|
||||
- [x] Multiple concurrent projects supported
|
||||
- [x] Interrupted sessions handled gracefully
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Hook Implementations (Already Complete)
|
||||
- [src/hooks/context.ts](src/hooks/context.ts) - SessionStart hook
|
||||
- [src/hooks/save.ts](src/hooks/save.ts) - PostToolUse hook
|
||||
- [src/hooks/new.ts](src/hooks/new.ts) - UserPromptSubmit hook
|
||||
- [src/hooks/summary.ts](src/hooks/summary.ts) - Stop hook
|
||||
|
||||
### Test Files Created
|
||||
- [test-phase3-integration.ts](test-phase3-integration.ts) - Hook database integration tests
|
||||
- [test-phase3-e2e.ts](test-phase3-e2e.ts) - End-to-end lifecycle tests
|
||||
|
||||
### CLI Integration (Already Complete)
|
||||
- [src/bin/cli.ts](src/bin/cli.ts) - CLI commands for all hooks
|
||||
|
||||
## Install Flow Updates
|
||||
|
||||
### ✅ CLI-Based Hook Architecture
|
||||
|
||||
Updated the install flow to use the new CLI-based architecture:
|
||||
|
||||
**Before (Old Architecture):**
|
||||
- Installed hook template files (`session-start.js`, etc.)
|
||||
- Copied shared helper modules
|
||||
- Configured settings.json to point to hook files
|
||||
|
||||
**After (New Architecture):**
|
||||
- Hooks are CLI commands: `claude-mem context`, `claude-mem new`, `claude-mem save`, `claude-mem summary`
|
||||
- Settings.json configured directly with CLI commands
|
||||
- No separate hook files needed
|
||||
- Simpler installation and maintenance
|
||||
|
||||
**Updated Install Steps:**
|
||||
```javascript
|
||||
settings.hooks.SessionStart = [{ type: "command", command: "claude-mem context", timeout: 180 }]
|
||||
settings.hooks.Stop = [{ type: "command", command: "claude-mem summary", timeout: 60 }]
|
||||
settings.hooks.UserPromptSubmit = [{ type: "command", command: "claude-mem new", timeout: 60 }]
|
||||
settings.hooks.PostToolUse = [{ type: "command", command: "claude-mem save", timeout: 180, matcher: "*" }]
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Single source of truth (CLI implementation)
|
||||
- ✅ No hook file synchronization issues
|
||||
- ✅ Easier debugging (just test CLI commands)
|
||||
- ✅ Simpler installation process
|
||||
- ✅ Better maintainability
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [REFACTOR-PLAN.md](REFACTOR-PLAN.md) - Complete architecture plan
|
||||
- [PHASE1-COMPLETE.md](PHASE1-COMPLETE.md) - Database & HooksDatabase layer
|
||||
- [PHASE2-COMPLETE.md](PHASE2-COMPLETE.md) - SDK worker process
|
||||
- **PHASE3-COMPLETE.md** (this document) - Hook integration & testing
|
||||
|
||||
## Next Steps
|
||||
|
||||
Phase 3 is complete! The claude-mem system is now ready for real-world testing with actual Claude Code sessions.
|
||||
|
||||
### Recommended Next Actions
|
||||
|
||||
1. **Manual Testing**
|
||||
- Configure hooks in `~/.config/claude-code/settings.json`
|
||||
- Run a real Claude Code session
|
||||
- Verify observations are queued
|
||||
- Verify summaries are generated
|
||||
- Verify context is injected on next session
|
||||
|
||||
2. **Monitoring & Debugging**
|
||||
- Add file-based logging to SDK worker
|
||||
- Monitor `~/.claude-mem/claude-mem.db` for data
|
||||
- Check observation queue processing
|
||||
- Verify summary generation
|
||||
|
||||
3. **Future Enhancements**
|
||||
- Extract SDK worker as separate executable (not bundled)
|
||||
- Add resumption support for interrupted SDK sessions
|
||||
- Implement retry logic for failed observations
|
||||
- Add telemetry and error reporting
|
||||
- Optimize database queries with additional indexes
|
||||
|
||||
## Conclusion
|
||||
|
||||
Phase 3 successfully completes the claude-mem architecture refactor. All three phases are now complete:
|
||||
|
||||
- ✅ **Phase 1**: Database schema and shared layer
|
||||
- ✅ **Phase 2**: SDK worker process and prompts
|
||||
- ✅ **Phase 3**: Hook integration and end-to-end testing
|
||||
|
||||
The system is architecturally sound, fully tested, and ready for production use!
|
||||
|
||||
🎉 **Refactor Complete!** 🎉
|
||||
@@ -0,0 +1,309 @@
|
||||
---
|
||||
title: "Database Architecture"
|
||||
description: "SQLite schema, FTS5 search, and data storage"
|
||||
---
|
||||
|
||||
# Database Architecture
|
||||
|
||||
Claude-Mem uses SQLite 3 with the better-sqlite3 native module for persistent storage and FTS5 for full-text search.
|
||||
|
||||
## Database Location
|
||||
|
||||
- **Current**: `~/.claude-mem/claude-mem.db`
|
||||
|
||||
**Note**: Despite the README claiming v4.0.0+ moved the database to `${CLAUDE_PLUGIN_ROOT}/data/`, the actual implementation still uses `~/.claude-mem/`.
|
||||
|
||||
## Database Implementation
|
||||
|
||||
**Primary Implementation**: better-sqlite3 (native SQLite module)
|
||||
- Used by: SessionStore and SessionSearch
|
||||
- Format: Synchronous API with better performance
|
||||
- **Note**: Database.ts (using bun:sqlite) is legacy code
|
||||
|
||||
## Core Tables
|
||||
|
||||
### 1. sdk_sessions
|
||||
|
||||
Tracks active and completed sessions.
|
||||
|
||||
```sql
|
||||
CREATE TABLE sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
claude_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
prompt_counter INTEGER DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
last_activity_at TEXT,
|
||||
last_activity_epoch INTEGER
|
||||
);
|
||||
```
|
||||
|
||||
**Indexes**:
|
||||
- `idx_sdk_sessions_claude_session` on `claude_session_id`
|
||||
- `idx_sdk_sessions_project` on `project`
|
||||
- `idx_sdk_sessions_status` on `status`
|
||||
- `idx_sdk_sessions_created_at` on `created_at_epoch DESC`
|
||||
|
||||
### 2. observations
|
||||
|
||||
Individual tool executions with hierarchical structure.
|
||||
|
||||
```sql
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
claude_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
prompt_number INTEGER,
|
||||
tool_name TEXT NOT NULL,
|
||||
correlation_id TEXT,
|
||||
|
||||
-- Hierarchical fields
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
narrative TEXT,
|
||||
text TEXT,
|
||||
facts TEXT,
|
||||
concepts TEXT,
|
||||
type TEXT,
|
||||
files_read TEXT,
|
||||
files_modified TEXT,
|
||||
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY (sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
|
||||
);
|
||||
```
|
||||
|
||||
**Observation Types**:
|
||||
- `decision` - Architectural or design decisions
|
||||
- `bugfix` - Bug fixes and corrections
|
||||
- `feature` - New features or capabilities
|
||||
- `refactor` - Code refactoring and cleanup
|
||||
- `discovery` - Learnings about the codebase
|
||||
- `change` - General changes and modifications
|
||||
|
||||
**Indexes**:
|
||||
- `idx_observations_session` on `session_id`
|
||||
- `idx_observations_sdk_session` on `sdk_session_id`
|
||||
- `idx_observations_project` on `project`
|
||||
- `idx_observations_tool_name` on `tool_name`
|
||||
- `idx_observations_created_at` on `created_at_epoch DESC`
|
||||
- `idx_observations_type` on `type`
|
||||
|
||||
### 3. session_summaries
|
||||
|
||||
AI-generated session summaries (multiple per session).
|
||||
|
||||
```sql
|
||||
CREATE TABLE session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
claude_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
prompt_number INTEGER,
|
||||
|
||||
-- Summary fields
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
notes TEXT,
|
||||
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY (sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
|
||||
);
|
||||
```
|
||||
|
||||
**Indexes**:
|
||||
- `idx_session_summaries_sdk_session` on `sdk_session_id`
|
||||
- `idx_session_summaries_project` on `project`
|
||||
- `idx_session_summaries_created_at` on `created_at_epoch DESC`
|
||||
|
||||
### 4. user_prompts
|
||||
|
||||
Raw user prompts with FTS5 search (as of v4.2.0).
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
claude_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
prompt_number INTEGER,
|
||||
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)
|
||||
);
|
||||
```
|
||||
|
||||
**Indexes**:
|
||||
- `idx_user_prompts_sdk_session` on `sdk_session_id`
|
||||
- `idx_user_prompts_project` on `project`
|
||||
- `idx_user_prompts_created_at` on `created_at_epoch DESC`
|
||||
|
||||
### Legacy Tables
|
||||
|
||||
- **sessions**: Legacy session tracking (v3.x)
|
||||
- **memories**: Legacy compressed memory chunks (v3.x)
|
||||
- **overviews**: Legacy session summaries (v3.x)
|
||||
|
||||
## FTS5 Full-Text Search
|
||||
|
||||
SQLite FTS5 (Full-Text Search) virtual tables enable fast full-text search across observations, summaries, and user prompts.
|
||||
|
||||
### FTS5 Virtual Tables
|
||||
|
||||
#### observations_fts
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
||||
title,
|
||||
subtitle,
|
||||
narrative,
|
||||
text,
|
||||
facts,
|
||||
concepts,
|
||||
content='observations',
|
||||
content_rowid='id'
|
||||
);
|
||||
```
|
||||
|
||||
#### session_summaries_fts
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE session_summaries_fts USING fts5(
|
||||
request,
|
||||
investigated,
|
||||
learned,
|
||||
completed,
|
||||
next_steps,
|
||||
notes,
|
||||
content='session_summaries',
|
||||
content_rowid='id'
|
||||
);
|
||||
```
|
||||
|
||||
#### user_prompts_fts
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
```
|
||||
|
||||
### Automatic Synchronization
|
||||
|
||||
FTS5 tables stay in sync via triggers:
|
||||
|
||||
```sql
|
||||
-- Insert trigger example
|
||||
CREATE TRIGGER observations_ai AFTER INSERT ON observations BEGIN
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
|
||||
-- Update trigger example
|
||||
CREATE TRIGGER observations_au AFTER UPDATE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
|
||||
-- Delete trigger example
|
||||
CREATE TRIGGER observations_ad AFTER DELETE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
END;
|
||||
```
|
||||
|
||||
### FTS5 Query Syntax
|
||||
|
||||
FTS5 supports rich query syntax:
|
||||
|
||||
- **Simple**: `"error handling"`
|
||||
- **AND**: `"error" AND "handling"`
|
||||
- **OR**: `"bug" OR "fix"`
|
||||
- **NOT**: `"bug" NOT "feature"`
|
||||
- **Phrase**: `"'exact phrase'"`
|
||||
- **Column**: `title:"authentication"`
|
||||
|
||||
### Security
|
||||
|
||||
As of v4.2.3, all FTS5 queries are properly escaped to prevent SQL injection:
|
||||
- Double quotes are escaped: `query.replace(/"/g, '""')`
|
||||
- Comprehensive test suite with 332 injection attack tests
|
||||
|
||||
## Database Classes
|
||||
|
||||
### SessionStore
|
||||
|
||||
CRUD operations for sessions, observations, summaries, and user prompts.
|
||||
|
||||
**Location**: `src/services/sqlite/SessionStore.ts`
|
||||
|
||||
**Methods**:
|
||||
- `createSession()`
|
||||
- `getSession()`
|
||||
- `updateSession()`
|
||||
- `createObservation()`
|
||||
- `getObservations()`
|
||||
- `createSummary()`
|
||||
- `getSummaries()`
|
||||
- `createUserPrompt()`
|
||||
|
||||
### SessionSearch
|
||||
|
||||
FTS5 full-text search with 8 specialized search methods.
|
||||
|
||||
**Location**: `src/services/sqlite/SessionSearch.ts`
|
||||
|
||||
**Methods**:
|
||||
- `searchObservations()` - Full-text search across observations
|
||||
- `searchSessions()` - Full-text search across summaries
|
||||
- `searchUserPrompts()` - Full-text search across user prompts
|
||||
- `findByConcept()` - Find by concept tags
|
||||
- `findByFile()` - Find by file references
|
||||
- `findByType()` - Find by observation type
|
||||
- `getRecentContext()` - Get recent session context
|
||||
- `advancedSearch()` - Combined filters
|
||||
|
||||
## Migrations
|
||||
|
||||
Database schema is managed via migrations in `src/services/sqlite/migrations.ts`.
|
||||
|
||||
**Migration History**:
|
||||
- Migration 001: Initial schema (sessions, memories, overviews, diagnostics, transcript_events)
|
||||
- Migration 002: Hierarchical memory fields (title, subtitle, facts, concepts, files_touched)
|
||||
- Migration 003: SDK sessions and observations
|
||||
- Migration 004: Session summaries
|
||||
- Migration 005: Multi-prompt sessions (prompt_counter, prompt_number)
|
||||
- Migration 006: FTS5 virtual tables and triggers
|
||||
- Migration 007-010: Various improvements and user prompts table
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Indexes**: All foreign keys and frequently queried columns are indexed
|
||||
- **FTS5**: Full-text search is significantly faster than LIKE queries
|
||||
- **Triggers**: Automatic synchronization has minimal overhead
|
||||
- **Connection Pooling**: better-sqlite3 reuses connections efficiently
|
||||
- **Synchronous API**: better-sqlite3 uses synchronous API for better performance
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See [Troubleshooting - Database Issues](../troubleshooting.md#database-issues) for common problems and solutions.
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
title: "Plugin Hooks"
|
||||
description: "5 lifecycle hooks that power Claude-Mem"
|
||||
---
|
||||
|
||||
# Plugin Hooks
|
||||
|
||||
Claude-Mem integrates with Claude Code through 5 lifecycle hooks that capture events and inject context.
|
||||
|
||||
## Hook Overview
|
||||
|
||||
| Hook Name | Purpose | Timeout | Script |
|
||||
|---------------------|--------------------------------------|---------|-------------------------|
|
||||
| SessionStart | Inject context from previous sessions| 120s | context-hook.js |
|
||||
| UserPromptSubmit | Create/track new sessions | 120s | new-hook.js |
|
||||
| PostToolUse | Capture tool execution observations | 120s | save-hook.js |
|
||||
| Stop | Generate session summaries | 120s | summary-hook.js |
|
||||
| SessionEnd | Mark sessions complete | 120s | cleanup-hook.js |
|
||||
|
||||
## Hook Configuration
|
||||
|
||||
Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Claude-mem memory system hooks",
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"UserPromptSubmit": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"Stop": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"SessionEnd": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 1. SessionStart Hook (`context-hook.js`)
|
||||
|
||||
**Purpose**: Inject context from previous sessions into Claude's initial context.
|
||||
|
||||
**Behavior**:
|
||||
- Ensures dependencies are installed (runs fast idempotent npm install)
|
||||
- Auto-starts PM2 worker service if not running
|
||||
- Retrieves last 10 session summaries with three-tier verbosity (v4.2.0)
|
||||
- Returns context via `hookSpecificOutput` in JSON format (fixed in v4.1.0)
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"source": "startup"
|
||||
}
|
||||
```
|
||||
|
||||
**Output** (via stdout):
|
||||
```json
|
||||
{
|
||||
"hookSpecificOutput": "# Recent Sessions\n\n## Session 1...\n"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/context.ts` and `src/bin/hooks/context-hook.ts`
|
||||
|
||||
## 2. UserPromptSubmit Hook (`new-hook.js`)
|
||||
|
||||
**Purpose**: Create new session records and initialize session tracking.
|
||||
|
||||
**Behavior**:
|
||||
- Creates new session in database
|
||||
- Initializes session tracking
|
||||
- Saves raw user prompts for full-text search (as of v4.2.0)
|
||||
- Sends init signal to worker service
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"prompt": "User's actual prompt text"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/new.ts` and `src/bin/hooks/new-hook.ts`
|
||||
|
||||
## 3. PostToolUse Hook (`save-hook.js`)
|
||||
|
||||
**Purpose**: Capture tool execution observations.
|
||||
|
||||
**Behavior**:
|
||||
- Fires after EVERY tool execution (Read, Write, Edit, Bash, etc.)
|
||||
- Sends observations to worker service for processing
|
||||
- Includes correlation IDs for tracing
|
||||
- Filters low-value observations
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"tool_name": "Read",
|
||||
"tool_input": {...},
|
||||
"tool_result": "...",
|
||||
"correlation_id": "abc-123"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/save.ts` and `src/bin/hooks/save-hook.ts`
|
||||
|
||||
## 4. Stop Hook (`summary-hook.js`)
|
||||
|
||||
**Purpose**: Generate session summaries when Claude stops.
|
||||
|
||||
**Behavior**:
|
||||
- Triggers final summary generation
|
||||
- Sends summarize request to worker service
|
||||
- Summary includes: request, completed, learned, next_steps
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"source": "user_stop"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/summary.ts` and `src/bin/hooks/summary-hook.ts`
|
||||
|
||||
## 5. SessionEnd Hook (`cleanup-hook.js`)
|
||||
|
||||
**Purpose**: Mark sessions as completed (graceful cleanup as of v4.1.0).
|
||||
|
||||
**Behavior**:
|
||||
- Marks sessions as completed
|
||||
- Skips cleanup on `/clear` commands to preserve ongoing sessions
|
||||
- Allows workers to finish pending operations naturally
|
||||
- Previously sent DELETE requests; now uses graceful completion
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"source": "normal"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/cleanup.ts` and `src/bin/hooks/cleanup-hook.ts`
|
||||
|
||||
## Hook Development
|
||||
|
||||
### Adding a New Hook
|
||||
|
||||
1. Create hook implementation in `src/hooks/your-hook.ts`
|
||||
2. Create entry point in `src/bin/hooks/your-hook.ts`
|
||||
3. Add to `plugin/hooks/hooks.json`
|
||||
4. Rebuild with `npm run build`
|
||||
|
||||
### Hook Best Practices
|
||||
|
||||
- **Fast execution**: Hooks should complete quickly (< 1s ideal)
|
||||
- **Graceful degradation**: Don't block Claude if worker is down
|
||||
- **Structured logging**: Use logger for debugging
|
||||
- **Error handling**: Catch and log errors, don't crash
|
||||
- **JSON output**: Use `hookSpecificOutput` for context injection
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See [Troubleshooting - Hook Issues](../troubleshooting.md#hook-issues) for common problems and solutions.
|
||||
@@ -0,0 +1,313 @@
|
||||
---
|
||||
title: "MCP Search Server"
|
||||
description: "7 search tools with examples and usage patterns"
|
||||
---
|
||||
|
||||
# MCP Search Server
|
||||
|
||||
Claude-Mem includes a Model Context Protocol (MCP) server that exposes 7 specialized search tools for querying stored observations and sessions.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Location**: `src/servers/search-server.ts`
|
||||
- **Configuration**: `plugin/.mcp.json`
|
||||
- **Transport**: stdio
|
||||
- **Tools**: 7 specialized search functions
|
||||
- **Citations**: All results use `claude-mem://` URI scheme
|
||||
|
||||
## Configuration
|
||||
|
||||
The MCP server is automatically registered via `plugin/.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-mem-search": {
|
||||
"type": "stdio",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This registers the `claude-mem-search` server with Claude Code, making the 7 search tools available in all sessions. The server is automatically started when Claude Code launches and communicates via stdio transport.
|
||||
|
||||
## Search Tools
|
||||
|
||||
### 1. search_observations
|
||||
|
||||
Full-text search across observation titles, narratives, facts, and concepts.
|
||||
|
||||
**Parameters**:
|
||||
- `query` (required): Search query for FTS5 full-text search
|
||||
- `type`: Filter by observation type(s) (decision, bugfix, feature, refactor, discovery, change)
|
||||
- `concepts`: Filter by concept tags
|
||||
- `files`: Filter by file paths (partial match)
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range (`{start, end}`)
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" for titles/dates only, "full" for complete details)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
search_observations with query="build system" and type="decision"
|
||||
```
|
||||
|
||||
### 2. search_sessions
|
||||
|
||||
Full-text search across session summaries, requests, and learnings.
|
||||
|
||||
**Parameters**:
|
||||
- `query` (required): Search query for FTS5 full-text search
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" or "full")
|
||||
|
||||
**Example**:
|
||||
```
|
||||
search_sessions with query="hooks implementation"
|
||||
```
|
||||
|
||||
### 3. search_user_prompts
|
||||
|
||||
Search raw user prompts with full-text search. Use this to find what the user actually said/requested across all sessions.
|
||||
|
||||
**Parameters**:
|
||||
- `query` (required): Search query for FTS5 full-text search
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" for truncated prompts/dates, "full" for complete prompt text)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
search_user_prompts with query="authentication feature"
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Full context reconstruction from user intent → Claude actions → outcomes
|
||||
- Pattern detection for repeated requests
|
||||
- Improved debugging by tracing from original user words to final implementation
|
||||
|
||||
### 4. find_by_concept
|
||||
|
||||
Find observations tagged with specific concepts.
|
||||
|
||||
**Parameters**:
|
||||
- `concept` (required): Concept tag to search for
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" or "full")
|
||||
|
||||
**Example**:
|
||||
```
|
||||
find_by_concept with concept="architecture"
|
||||
```
|
||||
|
||||
### 5. find_by_file
|
||||
|
||||
Find observations and sessions that reference specific file paths.
|
||||
|
||||
**Parameters**:
|
||||
- `filePath` (required): File path to search for (supports partial matching)
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" or "full")
|
||||
|
||||
**Example**:
|
||||
```
|
||||
find_by_file with filePath="worker-service.ts"
|
||||
```
|
||||
|
||||
### 6. find_by_type
|
||||
|
||||
Find observations by type (decision, bugfix, feature, refactor, discovery, change).
|
||||
|
||||
**Parameters**:
|
||||
- `type` (required): Observation type(s) to filter by (single type or array)
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" or "full")
|
||||
|
||||
**Example**:
|
||||
```
|
||||
find_by_type with type=["decision", "feature"]
|
||||
```
|
||||
|
||||
### 7. get_recent_context
|
||||
|
||||
Get recent session context including summaries and observations for a project.
|
||||
|
||||
**Parameters**:
|
||||
- `project`: Project name (defaults to current working directory basename)
|
||||
- `limit`: Number of recent sessions to retrieve (default: 3, max: 10)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
get_recent_context with limit=5
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
All search tools support two output formats:
|
||||
|
||||
### Index Format (Default)
|
||||
|
||||
Returns titles, dates, and source URIs only. Uses ~10x fewer tokens than full format.
|
||||
|
||||
**Always use index format first** to get an overview and identify relevant results.
|
||||
|
||||
**Example Output**:
|
||||
```
|
||||
1. [decision] Implement graceful session cleanup
|
||||
Date: 2025-10-21 14:23:45
|
||||
Source: claude-mem://observation/123
|
||||
|
||||
2. [feature] Add FTS5 full-text search
|
||||
Date: 2025-10-21 13:15:22
|
||||
Source: claude-mem://observation/124
|
||||
```
|
||||
|
||||
### Full Format
|
||||
|
||||
Returns complete observation/summary details including narrative, facts, concepts, files, etc.
|
||||
|
||||
**Only use after reviewing index results** to dive deep into specific items of interest.
|
||||
|
||||
## Search Strategy
|
||||
|
||||
**Recommended Workflow**:
|
||||
|
||||
1. **Initial search**: Use default (index) format to see titles, dates, and sources
|
||||
2. **Review results**: Identify which items are most relevant to your needs
|
||||
3. **Deep dive**: Only then use `format: "full"` on specific items of interest
|
||||
4. **Narrow down**: Use filters (type, dateRange, concepts, files) to refine results
|
||||
|
||||
**Token Efficiency**:
|
||||
- Index format: ~50-100 tokens per result
|
||||
- Full format: ~500-1000 tokens per result
|
||||
- Start with 3-5 results to avoid MCP token limits
|
||||
|
||||
## Citations
|
||||
|
||||
All search results use the `claude-mem://` URI scheme for citations:
|
||||
|
||||
- `claude-mem://observation/{id}` - References specific observations
|
||||
- `claude-mem://session/{id}` - References specific sessions
|
||||
- `claude-mem://user-prompt/{id}` - References specific user prompts
|
||||
|
||||
These citations allow Claude to reference specific historical context in responses.
|
||||
|
||||
## FTS5 Query Syntax
|
||||
|
||||
The `query` parameter supports SQLite FTS5 full-text search syntax:
|
||||
|
||||
- **Simple**: `"error handling"`
|
||||
- **AND**: `"error" AND "handling"`
|
||||
- **OR**: `"bug" OR "fix"`
|
||||
- **NOT**: `"bug" NOT "feature"`
|
||||
- **Phrase**: `"'exact phrase'"`
|
||||
- **Column**: `title:"authentication"`
|
||||
|
||||
## Security
|
||||
|
||||
As of v4.2.3, all FTS5 queries are properly escaped to prevent SQL injection attacks:
|
||||
- Double quotes are escaped: `query.replace(/"/g, '""')`
|
||||
- Comprehensive test suite with 332 injection attack tests
|
||||
- Affects: `search_observations`, `search_sessions`, `search_user_prompts`
|
||||
|
||||
## Example Queries
|
||||
|
||||
```
|
||||
# Find all decisions about build system
|
||||
search_observations with query="build system" and type="decision"
|
||||
|
||||
# Show everything related to worker-service.ts
|
||||
find_by_file with filePath="worker-service.ts"
|
||||
|
||||
# Search what we learned about hooks
|
||||
search_sessions with query="hooks"
|
||||
|
||||
# Show observations tagged with 'architecture'
|
||||
find_by_concept with concept="architecture"
|
||||
|
||||
# Find what user asked about authentication
|
||||
search_user_prompts with query="authentication"
|
||||
|
||||
# Get recent context for debugging
|
||||
get_recent_context with limit=5
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
The MCP search server is implemented using:
|
||||
- `@modelcontextprotocol/sdk` (v1.20.1)
|
||||
- `SessionSearch` service for FTS5 queries
|
||||
- `SessionStore` for database access
|
||||
- `zod-to-json-schema` for parameter validation
|
||||
|
||||
**Source Code**: `src/servers/search-server.ts`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tool Not Available
|
||||
|
||||
If search tools are not available in Claude Code sessions:
|
||||
|
||||
1. Check MCP configuration:
|
||||
```bash
|
||||
cat plugin/.mcp.json
|
||||
```
|
||||
|
||||
2. Verify search server is built:
|
||||
```bash
|
||||
ls -l plugin/scripts/search-server.js
|
||||
```
|
||||
|
||||
3. Rebuild if needed:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Search Returns No Results
|
||||
|
||||
1. Check database has data:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
|
||||
```
|
||||
|
||||
2. Verify FTS5 tables exist:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts';"
|
||||
```
|
||||
|
||||
3. Test query syntax:
|
||||
```bash
|
||||
# Simple query should work
|
||||
search_observations with query="test"
|
||||
```
|
||||
|
||||
### Token Limit Errors
|
||||
|
||||
If you hit MCP token limits:
|
||||
|
||||
1. Use `format: "index"` instead of `format: "full"`
|
||||
2. Reduce `limit` parameter (try 3-5 instead of 20)
|
||||
3. Use more specific filters to narrow results
|
||||
4. Use `offset` to paginate through results
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "Architecture Overview"
|
||||
description: "System components and data flow in Claude-Mem"
|
||||
---
|
||||
|
||||
# Architecture Overview
|
||||
|
||||
## System Components
|
||||
|
||||
Claude-Mem operates as a Claude Code plugin with four core components:
|
||||
|
||||
1. **Plugin Hooks** - Capture lifecycle events
|
||||
2. **Worker Service** - Process observations via Claude Agent SDK
|
||||
3. **Database Layer** - Store sessions and observations (SQLite + FTS5)
|
||||
4. **MCP Search Server** - Query historical context
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|------------------------|-------------------------------------------|
|
||||
| **Language** | TypeScript (ES2022, ESNext modules) |
|
||||
| **Runtime** | Node.js 18+ |
|
||||
| **Database** | SQLite 3 with better-sqlite3 driver |
|
||||
| **HTTP Server** | Express.js 4.18 |
|
||||
| **AI SDK** | @anthropic-ai/claude-agent-sdk |
|
||||
| **Build Tool** | esbuild (bundles TypeScript) |
|
||||
| **Process Manager** | PM2 |
|
||||
| **Testing** | Node.js built-in test runner |
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Memory Pipeline
|
||||
```
|
||||
Hook (stdin) → Database → Worker Service → SDK Processor → Database → Next Session Hook
|
||||
```
|
||||
|
||||
1. **Input**: Claude Code sends tool execution data via stdin to hooks
|
||||
2. **Storage**: Hooks write observations to SQLite database
|
||||
3. **Processing**: Worker service reads observations, processes via SDK
|
||||
4. **Output**: Processed summaries written back to database
|
||||
5. **Retrieval**: Next session's context hook reads summaries from database
|
||||
|
||||
### Search Pipeline
|
||||
```
|
||||
Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Search Results → Claude
|
||||
```
|
||||
|
||||
1. **Query**: Claude uses MCP search tools (e.g., `search_observations`)
|
||||
2. **Search**: MCP server calls SessionSearch service with query parameters
|
||||
3. **FTS5**: Full-text search executes against FTS5 virtual tables
|
||||
4. **Format**: Results formatted as `search_result` blocks with citations
|
||||
5. **Return**: Claude receives citable search results for analysis
|
||||
|
||||
## Session Lifecycle
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. Session Starts → Context Hook Fires │
|
||||
│ Injects summaries from last 3 sessions into Claude's context │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. User Types Prompt → UserPromptSubmit Hook Fires │
|
||||
│ Creates SDK session in database, notifies worker service │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. Claude Uses Tools → PostToolUse Hook Fires (100+ times) │
|
||||
│ Sends observations to worker service for processing │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. Worker Processes → Claude Agent SDK Analyzes │
|
||||
│ Extracts structured learnings via iterative AI processing │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 5. Claude Stops → Stop Hook Fires │
|
||||
│ Generates final summary with request, status, next steps │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 6. Session Ends → Cleanup Hook Fires │
|
||||
│ Marks session complete, ready for next session context │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
claude-mem/
|
||||
├── src/
|
||||
│ ├── bin/hooks/ # Entry point scripts for 5 hooks
|
||||
│ │ ├── context-hook.ts # SessionStart
|
||||
│ │ ├── new-hook.ts # UserPromptSubmit
|
||||
│ │ ├── save-hook.ts # PostToolUse
|
||||
│ │ ├── summary-hook.ts # Stop
|
||||
│ │ └── cleanup-hook.ts # SessionEnd
|
||||
│ │
|
||||
│ ├── hooks/ # Hook implementation logic
|
||||
│ │ ├── context.ts
|
||||
│ │ ├── new.ts
|
||||
│ │ ├── save.ts
|
||||
│ │ ├── summary.ts
|
||||
│ │ └── cleanup.ts
|
||||
│ │
|
||||
│ ├── servers/ # MCP servers
|
||||
│ │ └── search-server.ts # MCP search tools server
|
||||
│ │
|
||||
│ ├── sdk/ # Claude Agent SDK integration
|
||||
│ │ ├── prompts.ts # XML prompt builders
|
||||
│ │ ├── parser.ts # XML response parser
|
||||
│ │ └── worker.ts # Main SDK agent loop
|
||||
│ │
|
||||
│ ├── services/
|
||||
│ │ ├── worker-service.ts # Express HTTP service
|
||||
│ │ └── sqlite/ # Database layer
|
||||
│ │ ├── SessionStore.ts # CRUD operations
|
||||
│ │ ├── SessionSearch.ts # FTS5 search service
|
||||
│ │ ├── migrations.ts
|
||||
│ │ └── types.ts
|
||||
│ │
|
||||
│ ├── shared/ # Shared utilities
|
||||
│ │ ├── config.ts
|
||||
│ │ ├── paths.ts
|
||||
│ │ └── storage.ts
|
||||
│ │
|
||||
│ └── utils/
|
||||
│ ├── logger.ts
|
||||
│ ├── platform.ts
|
||||
│ └── port-allocator.ts
|
||||
│
|
||||
├── plugin/ # Plugin distribution
|
||||
│ ├── .claude-plugin/
|
||||
│ │ └── plugin.json
|
||||
│ ├── .mcp.json # MCP server configuration
|
||||
│ ├── hooks/
|
||||
│ │ └── hooks.json
|
||||
│ └── scripts/ # Built executables
|
||||
│ ├── context-hook.js
|
||||
│ ├── new-hook.js
|
||||
│ ├── save-hook.js
|
||||
│ ├── summary-hook.js
|
||||
│ ├── cleanup-hook.js
|
||||
│ ├── worker-service.cjs # Background worker
|
||||
│ └── search-server.js # MCP search server
|
||||
│
|
||||
├── tests/ # Test suite
|
||||
├── docs/ # Documentation
|
||||
└── ecosystem.config.cjs # PM2 configuration
|
||||
```
|
||||
|
||||
## Component Details
|
||||
|
||||
### 1. Plugin Hooks
|
||||
See [Plugin Hooks](/architecture/hooks) for detailed hook documentation.
|
||||
|
||||
### 2. Worker Service
|
||||
See [Worker Service](/architecture/worker-service) for HTTP API and endpoints.
|
||||
|
||||
### 3. Database Layer
|
||||
See [Database Architecture](/architecture/database) for schema and FTS5 search.
|
||||
|
||||
### 4. MCP Search Server
|
||||
See [MCP Search Server](/architecture/mcp-search) for search tools and examples.
|
||||
@@ -0,0 +1,248 @@
|
||||
---
|
||||
title: "Worker Service"
|
||||
description: "HTTP API and PM2 process management"
|
||||
---
|
||||
|
||||
# Worker Service
|
||||
|
||||
The worker service is a long-running HTTP API built with Express.js and managed by PM2. It processes observations through the Claude Agent SDK separately from hook execution to prevent timeout issues.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Technology**: Express.js HTTP server
|
||||
- **Process Manager**: PM2
|
||||
- **Port**: Fixed port 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`)
|
||||
- **Location**: `src/services/worker-service.ts`
|
||||
- **Built Output**: `plugin/scripts/worker-service.cjs`
|
||||
- **Model**: Configurable via `CLAUDE_MEM_MODEL` environment variable (default: claude-sonnet-4-5)
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
The worker service exposes 6 HTTP endpoints:
|
||||
|
||||
### 1. Health Check
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"uptime": 12345,
|
||||
"port": 37777
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Initialize Session
|
||||
```
|
||||
POST /sessions/:sessionDbId/init
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"sdk_session_id": "abc-123",
|
||||
"project": "my-project"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"session_id": "abc-123"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Add Observation
|
||||
```
|
||||
POST /sessions/:sessionDbId/observations
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"tool_name": "Read",
|
||||
"tool_input": {...},
|
||||
"tool_result": "...",
|
||||
"correlation_id": "xyz-789"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"observation_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Generate Summary
|
||||
```
|
||||
POST /sessions/:sessionDbId/summarize
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"trigger": "stop"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"summary_id": 456
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Session Status
|
||||
```
|
||||
GET /sessions/:sessionDbId/status
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"session_id": "abc-123",
|
||||
"status": "active",
|
||||
"observation_count": 42,
|
||||
"summary_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Delete Session
|
||||
```
|
||||
DELETE /sessions/:sessionDbId
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: As of v4.1.0, the cleanup hook no longer calls this endpoint. Sessions are marked complete instead of deleted to allow graceful worker shutdown.
|
||||
|
||||
## PM2 Management
|
||||
|
||||
### Configuration
|
||||
|
||||
The worker is configured via `ecosystem.config.cjs`:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'claude-mem-worker',
|
||||
script: './plugin/scripts/worker-service.cjs',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '1G',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
FORCE_COLOR: '1'
|
||||
}
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Start worker (auto-starts on first session)
|
||||
npm run worker:start
|
||||
|
||||
# Stop worker
|
||||
npm run worker:stop
|
||||
|
||||
# Restart worker
|
||||
npm run worker:restart
|
||||
|
||||
# View logs
|
||||
npm run worker:logs
|
||||
|
||||
# Check status
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
### Auto-Start Behavior
|
||||
|
||||
As of v4.0.0, the worker service auto-starts when the SessionStart hook fires. Manual start is optional.
|
||||
|
||||
## Claude Agent SDK Integration
|
||||
|
||||
The worker service routes observations to the Claude Agent SDK for AI-powered processing:
|
||||
|
||||
### Processing Flow
|
||||
|
||||
1. **Observation Queue**: Observations accumulate in memory
|
||||
2. **SDK Processing**: Observations sent to Claude via Agent SDK
|
||||
3. **XML Parsing**: Responses parsed for structured data
|
||||
4. **Database Storage**: Processed observations stored in SQLite
|
||||
|
||||
### SDK Components
|
||||
|
||||
- **Prompts** (`src/sdk/prompts.ts`): Builds XML-structured prompts
|
||||
- **Parser** (`src/sdk/parser.ts`): Parses Claude's XML responses
|
||||
- **Worker** (`src/sdk/worker.ts`): Main SDK agent loop
|
||||
|
||||
### Model Configuration
|
||||
|
||||
Set the AI model used for processing via environment variable:
|
||||
|
||||
```bash
|
||||
export CLAUDE_MEM_MODEL=claude-sonnet-4-5
|
||||
```
|
||||
|
||||
Available models:
|
||||
- `claude-haiku-4-5` - Fast, cost-efficient
|
||||
- `claude-sonnet-4-5` - Balanced (default)
|
||||
- `claude-opus-4` - Most capable
|
||||
- `claude-3-7-sonnet` - Alternative version
|
||||
|
||||
## Port Allocation
|
||||
|
||||
The worker uses a fixed port (37777 by default) for consistent communication:
|
||||
|
||||
- **Default**: Port 37777
|
||||
- **Override**: Set `CLAUDE_MEM_WORKER_PORT` environment variable
|
||||
- **Port File**: `${CLAUDE_PLUGIN_ROOT}/data/worker.port` tracks current port
|
||||
|
||||
If port 37777 is in use, the worker will fail to start. Set a custom port via environment variable.
|
||||
|
||||
## Data Storage
|
||||
|
||||
The worker service stores data in the plugin data directory:
|
||||
|
||||
```
|
||||
${CLAUDE_PLUGIN_ROOT}/data/
|
||||
├── claude-mem.db # SQLite database
|
||||
├── worker.port # Current worker port file
|
||||
└── logs/
|
||||
├── worker-out.log # Worker stdout logs
|
||||
└── worker-error.log # Worker stderr logs
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The worker implements graceful degradation:
|
||||
|
||||
- **Database Errors**: Logged but don't crash the service
|
||||
- **SDK Errors**: Retried with exponential backoff
|
||||
- **Network Errors**: Logged and skipped
|
||||
- **Invalid Input**: Validated and rejected with error response
|
||||
|
||||
## Performance
|
||||
|
||||
- **Async Processing**: Observations processed asynchronously
|
||||
- **In-Memory Queue**: Fast observation accumulation
|
||||
- **Batch Processing**: Multiple observations processed together
|
||||
- **Connection Pooling**: SQLite connections reused
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See [Troubleshooting - Worker Issues](../troubleshooting.md#worker-service-issues) for common problems and solutions.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,303 @@
|
||||
---
|
||||
title: "Configuration"
|
||||
description: "Environment variables and settings for Claude-Mem"
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|-------------------------|---------------------------------|---------------------------------------|
|
||||
| `CLAUDE_PLUGIN_ROOT` | Set by Claude Code | Plugin installation directory |
|
||||
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem/` | Data directory (dev override) |
|
||||
| `CLAUDE_MEM_WORKER_PORT`| `37777` | Worker service port |
|
||||
| `CLAUDE_MEM_MODEL` | `claude-sonnet-4-5` | AI model for processing observations |
|
||||
| `NODE_ENV` | `production` | Environment mode |
|
||||
| `FORCE_COLOR` | `1` | Enable colored logs |
|
||||
|
||||
## Model Configuration
|
||||
|
||||
Configure which AI model processes your observations.
|
||||
|
||||
### Available Models
|
||||
|
||||
- `claude-haiku-4-5` - Fast, cost-efficient
|
||||
- `claude-sonnet-4-5` - Balanced (default)
|
||||
- `claude-opus-4` - Most capable
|
||||
- `claude-3-7-sonnet` - Alternative version
|
||||
|
||||
### Using the Interactive Script
|
||||
|
||||
```bash
|
||||
./claude-mem-settings.sh
|
||||
```
|
||||
|
||||
This script manages `CLAUDE_MEM_MODEL` in `~/.claude/settings.json`.
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Edit `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"CLAUDE_MEM_MODEL": "claude-sonnet-4-5"
|
||||
}
|
||||
```
|
||||
|
||||
## Files and Directories
|
||||
|
||||
### Data Directory Structure
|
||||
|
||||
```
|
||||
~/.claude-mem/
|
||||
├── claude-mem.db # SQLite database
|
||||
├── worker.port # Current worker port file
|
||||
└── logs/
|
||||
├── worker-out.log # Worker stdout logs
|
||||
└── worker-error.log # Worker stderr logs
|
||||
```
|
||||
|
||||
### Plugin Directory Structure
|
||||
|
||||
```
|
||||
${CLAUDE_PLUGIN_ROOT}/
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json # Plugin metadata
|
||||
├── .mcp.json # MCP server configuration
|
||||
├── hooks/
|
||||
│ └── hooks.json # Hook configuration
|
||||
└── scripts/ # Built executables
|
||||
├── context-hook.js
|
||||
├── new-hook.js
|
||||
├── save-hook.js
|
||||
├── summary-hook.js
|
||||
├── cleanup-hook.js
|
||||
├── worker-service.cjs
|
||||
└── search-server.js
|
||||
```
|
||||
|
||||
## Plugin Configuration
|
||||
|
||||
### Hooks Configuration
|
||||
|
||||
Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Claude-mem memory system hooks",
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"UserPromptSubmit": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"Stop": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"SessionEnd": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MCP Server Configuration
|
||||
|
||||
The MCP search server is configured in `plugin/.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-mem-search": {
|
||||
"type": "stdio",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This registers the `claude-mem-search` server with Claude Code, making the 7 search tools available in all sessions.
|
||||
|
||||
## PM2 Configuration
|
||||
|
||||
Worker service is managed by PM2 via `ecosystem.config.cjs`:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'claude-mem-worker',
|
||||
script: './plugin/scripts/worker-service.cjs',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '1G',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
FORCE_COLOR: '1'
|
||||
}
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
### PM2 Settings
|
||||
|
||||
- **instances**: 1 (single instance)
|
||||
- **autorestart**: true (auto-restart on crash)
|
||||
- **watch**: false (no file watching)
|
||||
- **max_memory_restart**: 1G (restart if memory exceeds 1GB)
|
||||
|
||||
## Customization
|
||||
|
||||
### Custom Data Directory
|
||||
|
||||
For development or testing, override the data directory:
|
||||
|
||||
```bash
|
||||
export CLAUDE_MEM_DATA_DIR=/custom/path
|
||||
```
|
||||
|
||||
### Custom Worker Port
|
||||
|
||||
If port 37777 is in use:
|
||||
|
||||
```bash
|
||||
export CLAUDE_MEM_WORKER_PORT=38000
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
### Custom Model
|
||||
|
||||
Use a different AI model:
|
||||
|
||||
```bash
|
||||
export CLAUDE_MEM_MODEL=claude-opus-4
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Hook Timeouts
|
||||
|
||||
Modify timeouts in `plugin/hooks/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"timeout": 120 // Default: 120 seconds
|
||||
}
|
||||
```
|
||||
|
||||
Recommended values:
|
||||
- SessionStart: 120s (needs time for npm install and context retrieval)
|
||||
- UserPromptSubmit: 60s
|
||||
- PostToolUse: 120s (can process many observations)
|
||||
- Stop: 60s
|
||||
- SessionEnd: 60s
|
||||
|
||||
### Worker Memory Limit
|
||||
|
||||
Modify PM2 memory limit in `ecosystem.config.cjs`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
max_memory_restart: '2G' // Increase if needed
|
||||
}
|
||||
```
|
||||
|
||||
### Logging Verbosity
|
||||
|
||||
Enable debug logging:
|
||||
|
||||
```bash
|
||||
export DEBUG=claude-mem:*
|
||||
npm run worker:restart
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
## Configuration Best Practices
|
||||
|
||||
1. **Use defaults**: Default configuration works for most use cases
|
||||
2. **Override selectively**: Only change what you need
|
||||
3. **Document changes**: Keep track of custom configurations
|
||||
4. **Test after changes**: Verify worker restarts successfully
|
||||
5. **Monitor logs**: Check worker logs after configuration changes
|
||||
|
||||
## Troubleshooting Configuration
|
||||
|
||||
### Configuration Not Applied
|
||||
|
||||
1. Restart worker after changes:
|
||||
```bash
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
2. Verify environment variables:
|
||||
```bash
|
||||
echo $CLAUDE_MEM_MODEL
|
||||
echo $CLAUDE_MEM_WORKER_PORT
|
||||
```
|
||||
|
||||
3. Check worker logs:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
### Invalid Model Name
|
||||
|
||||
If you specify an invalid model name, the worker will fall back to `claude-sonnet-4-5` and log a warning.
|
||||
|
||||
Valid models:
|
||||
- claude-haiku-4-5
|
||||
- claude-sonnet-4-5
|
||||
- claude-opus-4
|
||||
- claude-3-7-sonnet
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If port 37777 is already in use:
|
||||
|
||||
1. Set custom port:
|
||||
```bash
|
||||
export CLAUDE_MEM_WORKER_PORT=38000
|
||||
```
|
||||
|
||||
2. Restart worker:
|
||||
```bash
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
3. Verify new port:
|
||||
```bash
|
||||
cat ~/.claude-mem/worker.port
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Architecture Overview](architecture/overview) - Understand the system
|
||||
- [Troubleshooting](troubleshooting) - Common issues
|
||||
- [Development](development) - Building from source
|
||||
@@ -0,0 +1,562 @@
|
||||
---
|
||||
title: "Development"
|
||||
description: "Build from source, run tests, and contribute to Claude-Mem"
|
||||
---
|
||||
|
||||
# Development Guide
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18.0.0 or higher
|
||||
- npm (comes with Node.js)
|
||||
- Git
|
||||
|
||||
### Clone and Build
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build all components
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Build Process
|
||||
|
||||
The build process uses esbuild to compile TypeScript:
|
||||
|
||||
1. Compiles TypeScript to JavaScript
|
||||
2. Creates standalone executables for each hook in `plugin/scripts/`
|
||||
3. Bundles MCP search server to `plugin/scripts/search-server.js`
|
||||
4. Bundles worker service to `plugin/scripts/worker-service.cjs`
|
||||
|
||||
**Build Output**:
|
||||
- Hook executables: `*-hook.js` (ESM format)
|
||||
- Worker service: `worker-service.cjs` (CJS format)
|
||||
- Search server: `search-server.js` (ESM format)
|
||||
|
||||
### Build Scripts
|
||||
|
||||
```bash
|
||||
# Build everything
|
||||
npm run build
|
||||
|
||||
# Build only hooks
|
||||
npm run build:hooks
|
||||
|
||||
# The build script is defined in scripts/build-hooks.js
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Make Changes
|
||||
|
||||
Edit TypeScript source files in `src/`:
|
||||
|
||||
```
|
||||
src/
|
||||
├── bin/hooks/ # Hook entry points
|
||||
├── hooks/ # Hook implementations
|
||||
├── services/ # Worker service and database
|
||||
├── servers/ # MCP search server
|
||||
├── sdk/ # Claude Agent SDK integration
|
||||
├── shared/ # Shared utilities
|
||||
└── utils/ # General utilities
|
||||
```
|
||||
|
||||
### 2. Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 3. Test
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Test specific file
|
||||
node --test tests/session-lifecycle.test.ts
|
||||
|
||||
# Test context injection
|
||||
npm run test:context
|
||||
|
||||
# Verbose context test
|
||||
npm run test:context:verbose
|
||||
```
|
||||
|
||||
### 4. Manual Testing
|
||||
|
||||
```bash
|
||||
# Start worker manually
|
||||
npm run worker:start
|
||||
|
||||
# Check worker status
|
||||
npm run worker:status
|
||||
|
||||
# View logs
|
||||
npm run worker:logs
|
||||
|
||||
# Test hooks manually
|
||||
echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plugin/scripts/context-hook.js
|
||||
```
|
||||
|
||||
### 5. Iterate
|
||||
|
||||
Repeat steps 1-4 until your changes work as expected.
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### Adding a New Hook
|
||||
|
||||
1. Create hook implementation in `src/hooks/your-hook.ts`:
|
||||
|
||||
```typescript
|
||||
import { HookInput } from './types';
|
||||
|
||||
export async function yourHook(input: HookInput) {
|
||||
// Hook implementation
|
||||
return {
|
||||
hookSpecificOutput: 'Optional output'
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
2. Create entry point in `src/bin/hooks/your-hook.ts`:
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env node
|
||||
import { readStdin } from '../../shared/stdin';
|
||||
import { yourHook } from '../../hooks/your-hook';
|
||||
|
||||
async function main() {
|
||||
const input = await readStdin();
|
||||
const result = await yourHook(input);
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
```
|
||||
|
||||
3. Add to `plugin/hooks/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"YourHook": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/your-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
4. Rebuild:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Modifying Database Schema
|
||||
|
||||
1. Add migration to `src/services/sqlite/migrations.ts`:
|
||||
|
||||
```typescript
|
||||
export const migration011: Migration = {
|
||||
version: 11,
|
||||
up: (db: Database) => {
|
||||
db.run(`
|
||||
ALTER TABLE observations ADD COLUMN new_field TEXT;
|
||||
`);
|
||||
},
|
||||
down: (db: Database) => {
|
||||
// Optional: define rollback
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. Update types in `src/services/sqlite/types.ts`:
|
||||
|
||||
```typescript
|
||||
export interface Observation {
|
||||
// ... existing fields
|
||||
new_field?: string;
|
||||
}
|
||||
```
|
||||
|
||||
3. Update database methods in `src/services/sqlite/SessionStore.ts`:
|
||||
|
||||
```typescript
|
||||
createObservation(obs: Observation) {
|
||||
// Include new_field in INSERT
|
||||
}
|
||||
```
|
||||
|
||||
4. Test migration:
|
||||
|
||||
```bash
|
||||
# Backup database first!
|
||||
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
```
|
||||
|
||||
### Extending SDK Prompts
|
||||
|
||||
1. Modify prompts in `src/sdk/prompts.ts`:
|
||||
|
||||
```typescript
|
||||
export function buildObservationPrompt(observation: Observation): string {
|
||||
return `
|
||||
<observation>
|
||||
<!-- Add new XML structure -->
|
||||
</observation>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
2. Update parser in `src/sdk/parser.ts`:
|
||||
|
||||
```typescript
|
||||
export function parseObservation(xml: string): ParsedObservation {
|
||||
// Parse new XML fields
|
||||
}
|
||||
```
|
||||
|
||||
3. Test:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### Adding MCP Search Tools
|
||||
|
||||
1. Add tool definition in `src/servers/search-server.ts`:
|
||||
|
||||
```typescript
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === 'your_new_tool') {
|
||||
// Implement tool logic
|
||||
const results = await search.yourNewSearch(params);
|
||||
return formatResults(results);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
2. Add search method in `src/services/sqlite/SessionSearch.ts`:
|
||||
|
||||
```typescript
|
||||
yourNewSearch(params: YourParams): SearchResult[] {
|
||||
// Implement FTS5 search
|
||||
}
|
||||
```
|
||||
|
||||
3. Rebuild and test:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# All tests
|
||||
npm test
|
||||
|
||||
# Specific test file
|
||||
node --test tests/your-test.test.ts
|
||||
|
||||
# With coverage (if configured)
|
||||
npm test -- --coverage
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
Create test files in `tests/`:
|
||||
|
||||
```typescript
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
describe('YourFeature', () => {
|
||||
it('should do something', () => {
|
||||
// Test implementation
|
||||
assert.strictEqual(result, expected);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Test Database
|
||||
|
||||
Use a separate test database:
|
||||
|
||||
```typescript
|
||||
import { SessionStore } from '../src/services/sqlite/SessionStore';
|
||||
|
||||
const store = new SessionStore(':memory:'); // In-memory database
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### TypeScript Guidelines
|
||||
|
||||
- Use TypeScript strict mode
|
||||
- Define interfaces for all data structures
|
||||
- Use async/await for asynchronous code
|
||||
- Handle errors explicitly
|
||||
- Add JSDoc comments for public APIs
|
||||
|
||||
### Formatting
|
||||
|
||||
- Follow existing code formatting
|
||||
- Use 2-space indentation
|
||||
- Use single quotes for strings
|
||||
- Add trailing commas in objects/arrays
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Create a new observation in the database
|
||||
*/
|
||||
export async function createObservation(
|
||||
obs: Observation
|
||||
): Promise<number> {
|
||||
try {
|
||||
const result = await db.insert('observations', {
|
||||
session_id: obs.session_id,
|
||||
tool_name: obs.tool_name,
|
||||
// ...
|
||||
});
|
||||
return result.id;
|
||||
} catch (error) {
|
||||
logger.error('Failed to create observation', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
```bash
|
||||
export DEBUG=claude-mem:*
|
||||
npm run worker:restart
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
### Inspect Database
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db
|
||||
|
||||
# View schema
|
||||
.schema observations
|
||||
|
||||
# Query data
|
||||
SELECT * FROM observations LIMIT 10;
|
||||
```
|
||||
|
||||
### Trace Observations
|
||||
|
||||
Use correlation IDs to trace observations through the pipeline:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db
|
||||
SELECT correlation_id, tool_name, created_at
|
||||
FROM observations
|
||||
WHERE session_id = 'YOUR_SESSION_ID'
|
||||
ORDER BY created_at;
|
||||
```
|
||||
|
||||
### Debug Hooks
|
||||
|
||||
Run hooks manually with test input:
|
||||
|
||||
```bash
|
||||
# Test context hook
|
||||
echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plugin/scripts/context-hook.js
|
||||
|
||||
# Test new hook
|
||||
echo '{"session_id":"test-123","cwd":"'$(pwd)'","prompt":"test"}' | node plugin/scripts/new-hook.js
|
||||
```
|
||||
|
||||
## Publishing
|
||||
|
||||
### NPM Publishing
|
||||
|
||||
```bash
|
||||
# Update version in package.json
|
||||
npm version patch # or minor, or major
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Publish to NPM
|
||||
npm run release
|
||||
```
|
||||
|
||||
The `release` script:
|
||||
1. Runs tests
|
||||
2. Builds all components
|
||||
3. Publishes to NPM registry
|
||||
|
||||
### Creating a Release
|
||||
|
||||
1. Update version in `package.json`
|
||||
2. Update `CHANGELOG.md`
|
||||
3. Commit changes
|
||||
4. Create git tag
|
||||
5. Push to GitHub
|
||||
6. Publish to NPM
|
||||
|
||||
```bash
|
||||
# Update version
|
||||
npm version 4.2.4
|
||||
|
||||
# Update changelog
|
||||
# Edit CHANGELOG.md manually
|
||||
|
||||
# Commit
|
||||
git add .
|
||||
git commit -m "chore: Release v4.2.4"
|
||||
|
||||
# Tag
|
||||
git tag v4.2.4
|
||||
|
||||
# Push
|
||||
git push origin main --tags
|
||||
|
||||
# Publish to NPM
|
||||
npm run release
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
### Contribution Workflow
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Make your changes
|
||||
4. Write tests
|
||||
5. Update documentation
|
||||
6. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
7. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
8. Open a Pull Request
|
||||
|
||||
### Pull Request Guidelines
|
||||
|
||||
- **Clear title**: Describe what the PR does
|
||||
- **Description**: Explain why the change is needed
|
||||
- **Tests**: Include tests for new features
|
||||
- **Documentation**: Update docs as needed
|
||||
- **Changelog**: Add entry to CHANGELOG.md
|
||||
- **Commits**: Use clear, descriptive commit messages
|
||||
|
||||
### Code Review Process
|
||||
|
||||
1. Automated tests must pass
|
||||
2. Code review by maintainer
|
||||
3. Address feedback
|
||||
4. Final approval
|
||||
5. Merge to main
|
||||
|
||||
## Development Tools
|
||||
|
||||
### Recommended VSCode Extensions
|
||||
|
||||
- TypeScript
|
||||
- ESLint
|
||||
- Prettier
|
||||
- SQLite Viewer
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# Check TypeScript types
|
||||
npx tsc --noEmit
|
||||
|
||||
# Lint code (if configured)
|
||||
npm run lint
|
||||
|
||||
# Format code (if configured)
|
||||
npm run format
|
||||
|
||||
# Clean build artifacts
|
||||
rm -rf plugin/scripts/*.js plugin/scripts/*.cjs
|
||||
```
|
||||
|
||||
## Troubleshooting Development
|
||||
|
||||
### Build Fails
|
||||
|
||||
1. Clean node_modules:
|
||||
```bash
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Check Node.js version:
|
||||
```bash
|
||||
node --version # Should be >= 18.0.0
|
||||
```
|
||||
|
||||
3. Check for syntax errors:
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
### Tests Fail
|
||||
|
||||
1. Check database:
|
||||
```bash
|
||||
rm ~/.claude-mem/claude-mem.db
|
||||
npm test
|
||||
```
|
||||
|
||||
2. Check worker status:
|
||||
```bash
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
3. View logs:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
### Worker Won't Start
|
||||
|
||||
1. Kill existing process:
|
||||
```bash
|
||||
pm2 delete claude-mem-worker
|
||||
```
|
||||
|
||||
2. Check port:
|
||||
```bash
|
||||
lsof -i :37777
|
||||
```
|
||||
|
||||
3. Try custom port:
|
||||
```bash
|
||||
export CLAUDE_MEM_WORKER_PORT=38000
|
||||
npm run worker:start
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Architecture Overview](architecture/overview) - Understand the system
|
||||
- [Configuration](configuration) - Customize Claude-Mem
|
||||
- [Troubleshooting](troubleshooting) - Common issues
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/schema.json",
|
||||
"name": "Claude-Mem",
|
||||
"description": "Persistent memory compression system that preserves context across Claude Code sessions",
|
||||
"theme": "mint",
|
||||
"favicon": "/claude-mem-logomark.webp",
|
||||
"logo": {
|
||||
"light": "/claude-mem-logo-for-light-mode.webp",
|
||||
"dark": "/claude-mem-logo-for-dark-mode.webp",
|
||||
"href": "https://github.com/thedotmack/claude-mem"
|
||||
},
|
||||
"colors": {
|
||||
"primary": "#3B82F6",
|
||||
"light": "#EFF6FF",
|
||||
"dark": "#1E40AF"
|
||||
},
|
||||
"navbar": {
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/thedotmack/claude-mem"
|
||||
}
|
||||
],
|
||||
"primary": {
|
||||
"type": "button",
|
||||
"label": "Install",
|
||||
"href": "https://github.com/thedotmack/claude-mem#quick-start"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"groups": [
|
||||
{
|
||||
"group": "Get Started",
|
||||
"icon": "rocket",
|
||||
"pages": [
|
||||
"introduction",
|
||||
"installation",
|
||||
"usage/getting-started",
|
||||
"usage/search-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Configuration & Development",
|
||||
"icon": "gear",
|
||||
"pages": [
|
||||
"configuration",
|
||||
"development",
|
||||
"troubleshooting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Architecture",
|
||||
"icon": "diagram-project",
|
||||
"pages": [
|
||||
"architecture/overview",
|
||||
"architecture/hooks",
|
||||
"architecture/worker-service",
|
||||
"architecture/database",
|
||||
"architecture/mcp-search"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"footer": {
|
||||
"socials": {
|
||||
"github": "https://github.com/thedotmack/claude-mem"
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"header": "Resources",
|
||||
"items": [
|
||||
{
|
||||
"label": "Documentation",
|
||||
"href": "https://github.com/thedotmack/claude-mem"
|
||||
},
|
||||
{
|
||||
"label": "Issues",
|
||||
"href": "https://github.com/thedotmack/claude-mem/issues"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"header": "Legal",
|
||||
"items": [
|
||||
{
|
||||
"label": "License (AGPL-3.0)",
|
||||
"href": "https://github.com/thedotmack/claude-mem/blob/main/LICENSE"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"seo": {
|
||||
"indexing": "all",
|
||||
"metatags": {
|
||||
"og:type": "website",
|
||||
"og:site_name": "Claude-Mem Documentation",
|
||||
"og:description": "Persistent memory compression system that preserves context across Claude Code sessions"
|
||||
}
|
||||
},
|
||||
"contextual": {
|
||||
"options": [
|
||||
"copy",
|
||||
"view",
|
||||
"chatgpt",
|
||||
"claude",
|
||||
"cursor"
|
||||
]
|
||||
},
|
||||
"integrations": {
|
||||
"telemetry": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: "Installation"
|
||||
description: "Install Claude-Mem plugin for persistent memory across sessions"
|
||||
---
|
||||
|
||||
# Installation Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
Install Claude-Mem directly from the plugin marketplace:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add thedotmack/claude-mem
|
||||
/plugin install claude-mem
|
||||
```
|
||||
|
||||
That's it! The plugin will automatically:
|
||||
- Download prebuilt binaries (no compilation needed)
|
||||
- Install all dependencies (including PM2 and SQLite binaries)
|
||||
- Configure hooks for session lifecycle management
|
||||
- Set up the MCP search server
|
||||
- Auto-start the worker service on first session
|
||||
|
||||
Start a new Claude Code session and you'll see context from previous sessions automatically loaded.
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Node.js**: 18.0.0 or higher
|
||||
- **Claude Code**: Latest version with plugin support
|
||||
- **PM2**: Process manager (bundled with plugin - no global install required)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
|
||||
## Advanced Installation
|
||||
|
||||
For development or testing, you can clone and build from source:
|
||||
|
||||
### Clone and Build
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build hooks and worker service
|
||||
npm run build
|
||||
|
||||
# Worker service will auto-start on first Claude Code session
|
||||
# Or manually start with:
|
||||
npm run worker:start
|
||||
|
||||
# Verify worker is running
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
### Post-Installation Verification
|
||||
|
||||
#### 1. Automatic Dependency Installation
|
||||
|
||||
Dependencies are installed automatically during plugin installation. The SessionStart hook also ensures dependencies are up-to-date on each session start (this is fast and idempotent). Works cross-platform on Windows, macOS, and Linux.
|
||||
|
||||
#### 2. Verify Plugin Installation
|
||||
|
||||
Check that hooks are configured in Claude Code:
|
||||
```bash
|
||||
cat plugin/hooks/hooks.json
|
||||
```
|
||||
|
||||
#### 3. Data Directory Location
|
||||
|
||||
v4.0.0+ stores data in `${CLAUDE_PLUGIN_ROOT}/data/`:
|
||||
- Database: `${CLAUDE_PLUGIN_ROOT}/data/claude-mem.db`
|
||||
- Worker port file: `${CLAUDE_PLUGIN_ROOT}/data/worker.port`
|
||||
- Logs: `${CLAUDE_PLUGIN_ROOT}/data/logs/`
|
||||
|
||||
For development/testing, you can override:
|
||||
```bash
|
||||
export CLAUDE_MEM_DATA_DIR=/custom/path
|
||||
```
|
||||
|
||||
#### 4. Check Worker Logs
|
||||
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
#### 5. Test Context Retrieval
|
||||
|
||||
```bash
|
||||
npm run test:context
|
||||
```
|
||||
|
||||
## Upgrading from v3.x
|
||||
|
||||
**BREAKING CHANGES - Please Read:**
|
||||
|
||||
v4.0.0 introduces breaking changes:
|
||||
|
||||
- **Data Location Changed**: Database moved from `~/.claude-mem/` to `${CLAUDE_PLUGIN_ROOT}/data/` (inside plugin directory)
|
||||
- **Fresh Start Required**: No automatic migration from v3.x. You must start with a clean database
|
||||
- **Worker Auto-Starts**: Worker service now starts automatically - no manual `npm run worker:start` needed
|
||||
- **MCP Search Server**: 7 new search tools with full-text search and citations
|
||||
- **Enhanced Architecture**: Improved plugin integration and data organization
|
||||
|
||||
See [CHANGELOG](https://github.com/thedotmack/claude-mem/blob/main/CHANGELOG.md) for complete details.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Getting Started Guide](usage/getting-started) - Learn how Claude-Mem works automatically
|
||||
- [MCP Search Tools](usage/search-tools) - Query your project history
|
||||
- [Configuration](configuration) - Customize Claude-Mem behavior
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: "Introduction"
|
||||
description: "Persistent memory compression system for Claude Code"
|
||||
---
|
||||
|
||||
# Claude-Mem
|
||||
|
||||
**Persistent memory compression system for Claude Code**
|
||||
|
||||
Claude-Mem seamlessly preserves context across sessions by automatically capturing tool usage observations, generating semantic summaries, and making them available to future sessions. This enables Claude to maintain continuity of knowledge about projects even after sessions end or reconnect.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Start a new Claude Code session in the terminal and enter the following commands:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add thedotmack/claude-mem
|
||||
/plugin install claude-mem
|
||||
```
|
||||
|
||||
Restart Claude Code. Context from previous sessions will automatically appear in new sessions.
|
||||
|
||||
## Key Features
|
||||
|
||||
- 🧠 **Persistent Memory** - Context survives across sessions
|
||||
- 🔍 **7 Search Tools** - Query your project history via MCP
|
||||
- 🤖 **Automatic Operation** - No manual intervention required
|
||||
- 📊 **FTS5 Search** - Fast full-text search across observations
|
||||
- 🔗 **Citations** - Reference past decisions with `claude-mem://` URIs
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Session Start → Inject context from last 10 sessions │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ User Prompts → Create session, save user prompts │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Tool Executions → Capture observations (Read, Write, etc.) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Worker Processes → Extract learnings via Claude Agent SDK │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Session Ends → Generate summary, ready for next session │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Core Components:**
|
||||
1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd
|
||||
2. **Worker Service** - HTTP API on port 37777 managed by PM2
|
||||
3. **SQLite Database** - Stores sessions, observations, summaries with FTS5 search
|
||||
4. **7 MCP Search Tools** - Query historical context with citations
|
||||
|
||||
See [Architecture Overview](architecture/overview) for details.
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Node.js**: 18.0.0 or higher
|
||||
- **Claude Code**: Latest version with plugin support
|
||||
- **PM2**: Process manager (bundled - no global install required)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
|
||||
## What's New in v4.2.3
|
||||
|
||||
**Security:**
|
||||
- Fixed FTS5 injection vulnerability in search functions
|
||||
- Added comprehensive test suite with 332 injection attack tests
|
||||
|
||||
**Fixes:**
|
||||
- Fixed ESM/CJS compatibility for getDirname function
|
||||
- Fixed Windows PowerShell compatibility in SessionStart hook
|
||||
- Cross-platform dependency installation now works on Windows, macOS, and Linux
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Installation" icon="download" href="/installation">
|
||||
Quick start & advanced installation
|
||||
</Card>
|
||||
<Card title="Getting Started" icon="rocket" href="/usage/getting-started">
|
||||
Learn how Claude-Mem works automatically
|
||||
</Card>
|
||||
<Card title="Architecture" icon="sitemap" href="/architecture/overview">
|
||||
System components & data flow
|
||||
</Card>
|
||||
<Card title="Search Tools" icon="magnifying-glass" href="/usage/search-tools">
|
||||
Query your project history
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,444 +0,0 @@
|
||||
# Prompt Flow Analysis & Rankings
|
||||
|
||||
## Rating System
|
||||
- ✅ **Smart**: Well-designed, clear purpose, effective
|
||||
- ⚠️ **Problematic**: Has issues but salvageable
|
||||
- ❌ **Stupid**: Poorly designed, confusing, or counterproductive
|
||||
- 🧠 **Context Poison**: Will confuse the AI or create inconsistent behavior
|
||||
- 🔍 **No Clear Purpose**: Exists but unclear why
|
||||
- 🎯 **Clarity Score**: 1-10 (10 = crystal clear, 1 = incomprehensible)
|
||||
|
||||
---
|
||||
|
||||
## Element-by-Element Comparison
|
||||
|
||||
### INIT PROMPTS (Session Start)
|
||||
|
||||
#### CURRENT: "You are a memory processor"
|
||||
```
|
||||
You will PROCESS tool executions during this Claude Code session. Your job is to:
|
||||
1. ANALYZE each tool response for meaningful content
|
||||
2. DECIDE whether it contains something worth storing
|
||||
3. EXTRACT the key insight
|
||||
4. STORE it as an observation in the XML format below
|
||||
|
||||
For MOST meaningful tool outputs, you should generate an observation. Only skip truly routine operations.
|
||||
```
|
||||
|
||||
**Rating**: ❌ **Stupid** + 🧠 **Context Poison**
|
||||
**Clarity**: 3/10
|
||||
|
||||
**Issues**:
|
||||
1. "For MOST" is ambiguous - does that mean 51%? 80%? 95%?
|
||||
2. Creates bias toward over-storage (fear of missing things)
|
||||
3. Contradicts "Only skip truly routine operations" later in prompt
|
||||
4. No clear guidance on what "meaningful" actually means
|
||||
5. "Only skip truly routine" implies almost everything should be stored
|
||||
|
||||
**Why Context Poison**:
|
||||
- Agent will second-guess every decision
|
||||
- Creates inconsistent thresholds across sessions
|
||||
- User gets frustrated with noise
|
||||
|
||||
---
|
||||
|
||||
#### OLD: "You are a semantic memory compressor"
|
||||
```
|
||||
## FIRST: Generate Session Title
|
||||
IMMEDIATELY generate a title and subtitle for this session based on the user request.
|
||||
|
||||
## THEN: Process Tool Responses
|
||||
You will receive a stream of tool responses. For each one:
|
||||
1. ANALYZE: Does this contain information worth remembering?
|
||||
2. DECIDE: Should I store this or skip it?
|
||||
3. EXTRACT: What are the key semantic concepts?
|
||||
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
|
||||
5. STORE: Use bash to save the hierarchical memory
|
||||
6. TRACK: Keep count of stored memories (001, 002, 003...)
|
||||
|
||||
# IMPORTANT REMINDERS
|
||||
- Be selective - quality over quantity
|
||||
```
|
||||
|
||||
**Rating**: ⚠️ **Problematic** but contains ✅ **Smart** elements
|
||||
**Clarity**: 6/10
|
||||
|
||||
**Issues**:
|
||||
1. "IMMEDIATELY" vs "THEN" creates ordering confusion
|
||||
2. Session title generation is unclear when it should happen
|
||||
3. Bash tool dependency is fragile
|
||||
4. Manual counter tracking is error-prone
|
||||
|
||||
**Smart Elements**:
|
||||
1. "Quality over quantity" is clear directive
|
||||
2. Hierarchical decomposition gives structure
|
||||
3. Explicit state tracking (counter)
|
||||
4. "Be selective" is unambiguous
|
||||
|
||||
**Verdict**: The philosophy is better (selective, quality-focused), execution is messier (bash commands, ordering confusion)
|
||||
|
||||
---
|
||||
|
||||
### OBSERVATION PROMPTS
|
||||
|
||||
#### CURRENT: buildObservationPrompt
|
||||
```
|
||||
ANALYSIS TASK
|
||||
-------------
|
||||
ANALYZE this tool response and DECIDE: Does it contain something worth storing?
|
||||
|
||||
Most Read, Edit, Grep, Bash, and Write operations contain meaningful content.
|
||||
|
||||
If this contains something worth remembering, output the observation...
|
||||
```
|
||||
|
||||
**Rating**: ❌ **Stupid** + 🧠 **Context Poison**
|
||||
**Clarity**: 4/10
|
||||
|
||||
**Issues**:
|
||||
1. "Most Read, Edit, Grep, Bash, and Write operations contain meaningful content" - Why repeat this per observation?
|
||||
2. Contradicts init prompt's "be selective" guidance
|
||||
3. Creates bias: agent thinks "well, this is a Read, so I should probably store it"
|
||||
4. No guidance on WHAT to extract from the content
|
||||
5. One-sentence observations lose context
|
||||
|
||||
**Why Context Poison**:
|
||||
- Tool-type bias ("It's a Read, so store it") instead of content-based evaluation
|
||||
- Encourages lazy pattern matching instead of semantic analysis
|
||||
- Results in garbage like "Read package.json file" with no insight
|
||||
|
||||
---
|
||||
|
||||
#### OLD: Tool Message
|
||||
```
|
||||
# Tool Response {timeFormatted}
|
||||
|
||||
Tool: {toolName}
|
||||
User Context: "{userPrompt}"
|
||||
|
||||
```
|
||||
{toolResponse}
|
||||
```
|
||||
|
||||
Analyze and store if meaningful.
|
||||
```
|
||||
|
||||
**Rating**: ✅ **Smart** (minimal, non-leading)
|
||||
**Clarity**: 8/10
|
||||
|
||||
**Smart Elements**:
|
||||
1. Doesn't tell agent what to think about tool types
|
||||
2. Trusts agent's judgment from system prompt
|
||||
3. Short and clear
|
||||
4. Includes user context for relevance filtering
|
||||
|
||||
**Issue**:
|
||||
1. Maybe TOO minimal - no reminder of format requirements
|
||||
|
||||
---
|
||||
|
||||
### STORAGE FORMATS
|
||||
|
||||
#### CURRENT: XML Observations
|
||||
```xml
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<text>Implemented JWT token refresh flow with 7-day expiry</text>
|
||||
</observation>
|
||||
```
|
||||
|
||||
**Rating**: ⚠️ **Problematic**
|
||||
**Clarity**: 7/10
|
||||
|
||||
**Issues**:
|
||||
1. One sentence only - loses narrative context
|
||||
2. Five types (decision, bugfix, feature, refactor, discovery) - are these actually useful categories?
|
||||
3. No file associations
|
||||
4. No concept tagging
|
||||
5. Flat structure - all observations equal weight
|
||||
|
||||
**Smart Elements**:
|
||||
1. Simple to parse
|
||||
2. Structured typing
|
||||
3. Regex-parseable
|
||||
|
||||
---
|
||||
|
||||
#### OLD: Hierarchical Memory (4 levels)
|
||||
```bash
|
||||
--title "SDK Transcript Auto-Cleanup"
|
||||
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history"
|
||||
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"]'
|
||||
--concepts '["cleanup", "SDK-lifecycle", "UX"]'
|
||||
--files '["hooks/stop-streaming.js"]'
|
||||
```
|
||||
|
||||
**Rating**: ✅ **Smart** (structure) but ❌ **Stupid** (execution via bash)
|
||||
**Clarity**: 8/10 (concept), 3/10 (implementation)
|
||||
|
||||
**Smart Elements**:
|
||||
1. Multiple levels of granularity (title → subtitle → facts → narrative)
|
||||
2. Atomic facts enable precise retrieval
|
||||
3. File associations explicit
|
||||
4. Concept tags for categorization
|
||||
5. Subtitle gives the "why it matters"
|
||||
|
||||
**Stupid Elements**:
|
||||
1. Bash command execution is fragile
|
||||
2. Quote escaping nightmare
|
||||
3. Manual counter tracking
|
||||
4. JSON in bash arguments is error-prone
|
||||
|
||||
**Verdict**: Great data model, terrible implementation
|
||||
|
||||
---
|
||||
|
||||
### SUMMARY/FINALIZE PROMPTS
|
||||
|
||||
#### CURRENT: buildFinalizePrompt (per prompt)
|
||||
```xml
|
||||
<summary>
|
||||
<request>Implement JWT authentication system</request>
|
||||
<investigated>Existing auth middleware, session management</investigated>
|
||||
<learned>Current system uses session cookies; no JWT support</learned>
|
||||
<completed>Implemented JWT token + refresh flow</completed>
|
||||
<next_steps>Add token revocation API endpoint</next_steps>
|
||||
<files_read><file>src/auth.ts</file></files_read>
|
||||
<files_edited><file>src/auth.ts</file></files_edited>
|
||||
<notes>Token secret stored in .env</notes>
|
||||
</summary>
|
||||
```
|
||||
|
||||
**Rating**: ✅ **Smart** (structure) but 🔍 **No Clear Purpose** (frequency)
|
||||
**Clarity**: 9/10
|
||||
|
||||
**Smart Elements**:
|
||||
1. Structured format with clear fields
|
||||
2. Tracks what was learned (semantic value)
|
||||
3. Files read/edited tracked explicitly
|
||||
4. Next steps captured
|
||||
|
||||
**Issues**:
|
||||
1. Generated PER PROMPT - is this too granular?
|
||||
2. Will create many summaries per session
|
||||
3. Unclear how these summaries are used
|
||||
4. No aggregation across prompts
|
||||
|
||||
**Question**: Should this be per-session instead of per-prompt?
|
||||
|
||||
---
|
||||
|
||||
#### OLD: Session Overview (per session)
|
||||
```bash
|
||||
claude-mem store-overview --project "{project}" --session "{sessionId}" --content "2-3 sentence overview"
|
||||
```
|
||||
|
||||
**Rating**: ⚠️ **Problematic**
|
||||
**Clarity**: 5/10
|
||||
|
||||
**Issues**:
|
||||
1. Only 2-3 sentences - very lossy
|
||||
2. No structured fields
|
||||
3. Happens once at end - loses per-prompt context
|
||||
4. Relies on agent's memory of entire session
|
||||
|
||||
**Smart Element**:
|
||||
1. One overview per session (not noisy)
|
||||
|
||||
---
|
||||
|
||||
### DECISION GUIDANCE
|
||||
|
||||
#### CURRENT: What to Store/Skip
|
||||
```
|
||||
Store these:
|
||||
✓ File contents with logic, algorithms, or patterns
|
||||
✓ Search results revealing project structure
|
||||
✓ Build errors or test failures with context
|
||||
...
|
||||
|
||||
Skip these:
|
||||
✗ Simple status checks (git status with no changes)
|
||||
✗ Trivial edits (one-line config changes)
|
||||
...
|
||||
```
|
||||
|
||||
**Rating**: ✅ **Smart**
|
||||
**Clarity**: 8/10
|
||||
|
||||
**Smart Elements**:
|
||||
1. Concrete examples
|
||||
2. Both positive and negative cases
|
||||
3. Action-oriented
|
||||
|
||||
**Issue**:
|
||||
1. Contradicted by "For MOST" and "Most Read, Edit..." statements elsewhere
|
||||
|
||||
---
|
||||
|
||||
#### OLD: What to Store/Skip
|
||||
```
|
||||
Store these:
|
||||
- File contents with logic, algorithms, or patterns
|
||||
- Search results revealing project structure
|
||||
...
|
||||
|
||||
Skip these:
|
||||
- Simple status checks (git status with no changes)
|
||||
- Trivial edits (one-line config changes)
|
||||
- Binary data or noise
|
||||
- Anything without semantic value
|
||||
```
|
||||
|
||||
**Rating**: ✅ **Smart**
|
||||
**Clarity**: 8/10
|
||||
|
||||
**Same as current**, which is good.
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL ISSUES RANKED
|
||||
|
||||
### 1. "For MOST meaningful tool outputs" - 🧠 **CONTEXT POISON #1**
|
||||
**Severity**: CRITICAL
|
||||
**Impact**: Destroys selectivity, fills DB with noise
|
||||
**Fix**: Remove entirely. Replace with: "Be selective. Only store if it reveals important information about the codebase."
|
||||
|
||||
---
|
||||
|
||||
### 2. "Most Read, Edit, Grep, Bash, and Write operations contain meaningful content" - 🧠 **CONTEXT POISON #2**
|
||||
**Severity**: CRITICAL
|
||||
**Impact**: Creates tool-type bias instead of content-based evaluation
|
||||
**Fix**: Remove entirely. It's redundant and harmful.
|
||||
|
||||
---
|
||||
|
||||
### 3. One-sentence observations lose context - ❌ **STUPID**
|
||||
**Severity**: HIGH
|
||||
**Impact**: Can't understand observation without narrative
|
||||
**Fix**: Add narrative field to observations (like old system)
|
||||
|
||||
---
|
||||
|
||||
### 4. No hierarchical structure in current system - ❌ **STUPID**
|
||||
**Severity**: HIGH
|
||||
**Impact**: Can't do granular retrieval (fact-level vs narrative-level)
|
||||
**Fix**: Adopt 4-level hierarchy from old system
|
||||
|
||||
---
|
||||
|
||||
### 5. Bash command execution in old system - ❌ **STUPID**
|
||||
**Severity**: HIGH
|
||||
**Impact**: Fragile, error-prone, quote-escaping nightmare
|
||||
**Fix**: Keep current approach (XML parsing + direct DB writes)
|
||||
|
||||
---
|
||||
|
||||
### 6. Manual memory counter in old system - ⚠️ **PROBLEMATIC**
|
||||
**Severity**: MEDIUM
|
||||
**Impact**: Agent forgets, skips numbers, duplicates
|
||||
**Fix**: Auto-increment in database (current approach)
|
||||
|
||||
---
|
||||
|
||||
### 7. Per-prompt summaries unclear purpose - 🔍 **NO CLEAR PURPOSE**
|
||||
**Severity**: MEDIUM
|
||||
**Impact**: Creates many summaries, unclear how they're used
|
||||
**Fix**: Decide: per-session summary only, or per-prompt with aggregation?
|
||||
|
||||
---
|
||||
|
||||
### 8. Five observation types unclear value - 🔍 **NO CLEAR PURPOSE**
|
||||
**Severity**: LOW
|
||||
**Impact**: Are these categories actually useful for retrieval?
|
||||
**Fix**: Evaluate if types should be: (1) kept as-is, (2) expanded, (3) removed
|
||||
|
||||
---
|
||||
|
||||
## BEST ELEMENTS FROM EACH SYSTEM
|
||||
|
||||
### From OLD System (Keep These)
|
||||
1. ✅ 4-level hierarchy (title → subtitle → facts → narrative)
|
||||
2. ✅ "Be selective - quality over quantity"
|
||||
3. ✅ Atomic facts (50-150 char, self-contained, no pronouns)
|
||||
4. ✅ Concept tagging
|
||||
5. ✅ File associations
|
||||
6. ✅ Minimal observation prompts (don't bias agent)
|
||||
|
||||
### From CURRENT System (Keep These)
|
||||
1. ✅ XML parsing (not bash commands)
|
||||
2. ✅ Auto-increment IDs (not manual counters)
|
||||
3. ✅ Structured summary format (8 fields)
|
||||
4. ✅ Per-prompt tracking
|
||||
5. ✅ Foreign key integrity
|
||||
6. ✅ Typed observations (decision/bugfix/feature/refactor/discovery)
|
||||
|
||||
### From NEITHER System (Add These)
|
||||
1. Clear threshold guidance: "Only store if it reveals important information about the codebase"
|
||||
2. Explicit narrative field in observations
|
||||
3. Vector embeddings for semantic search (current stores in SQLite only)
|
||||
|
||||
---
|
||||
|
||||
## RECOMMENDED HYBRID SYSTEM
|
||||
|
||||
### Storage Format: Hierarchical Observations (XML)
|
||||
```xml
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<title>JWT Token Refresh Implementation</title>
|
||||
<subtitle>Added 7-day refresh token rotation with Redis storage</subtitle>
|
||||
<facts>
|
||||
<fact>src/auth.ts: refreshToken() generates new JWT with 7-day expiry</fact>
|
||||
<fact>Redis key format: refresh:{userId}:{tokenId} with TTL 604800s</fact>
|
||||
<fact>Old token invalidated on refresh to prevent replay attacks</fact>
|
||||
</facts>
|
||||
<narrative>Implemented JWT refresh token functionality in src/auth.ts. The refreshToken() function validates the old refresh token from Redis, generates a new JWT access token (7-day expiry) and new refresh token, stores the new refresh token in Redis with key format refresh:{userId}:{tokenId} and TTL of 604800 seconds (7 days), and invalidates the old refresh token to prevent replay attacks. This enables long-lived authenticated sessions without requiring users to re-login while maintaining security through token rotation.</narrative>
|
||||
<concepts>
|
||||
<concept>authentication</concept>
|
||||
<concept>security</concept>
|
||||
<concept>session-management</concept>
|
||||
</concepts>
|
||||
<files>
|
||||
<file>src/auth.ts</file>
|
||||
<file>src/middleware/auth.ts</file>
|
||||
</files>
|
||||
</observation>
|
||||
```
|
||||
|
||||
### Guidance: Clear and Unambiguous
|
||||
```
|
||||
Be selective. Only store observations when the tool output reveals important information about:
|
||||
- Architecture or design patterns
|
||||
- Implementation details of features or bug fixes
|
||||
- System state or configuration
|
||||
- Business logic or algorithms
|
||||
|
||||
Skip routine operations like empty git status, simple npm installs, or trivial config changes.
|
||||
|
||||
Each observation should be self-contained and searchable.
|
||||
```
|
||||
|
||||
### Summary: Per-Session (Not Per-Prompt)
|
||||
- Generate ONE summary when session ends
|
||||
- Aggregate all observations from session
|
||||
- Use current structured format (request, investigated, learned, completed, next_steps, files_read, files_edited, notes)
|
||||
|
||||
---
|
||||
|
||||
## FINAL VERDICT
|
||||
|
||||
| Element | Current | Old | Winner |
|
||||
|---------|---------|-----|--------|
|
||||
| **Storage Structure** | Flat one-sentence | 4-level hierarchy | **OLD** |
|
||||
| **Storage Implementation** | XML parsing | Bash commands | **CURRENT** |
|
||||
| **Decision Guidance** | Contradictory | Clear | **OLD** |
|
||||
| **Session Metadata** | None | Title + subtitle | **OLD** |
|
||||
| **Per-Prompt Tracking** | Yes (summaries) | No | **CURRENT** |
|
||||
| **Semantic Search** | No | Yes (ChromaDB) | **OLD** |
|
||||
| **Observation Prompts** | Biased, repetitive | Minimal, clear | **OLD** |
|
||||
| **Auto-Increment IDs** | Yes | No (manual) | **CURRENT** |
|
||||
| **File Associations** | No | Yes | **OLD** |
|
||||
| **Concept Tagging** | No | Yes | **OLD** |
|
||||
|
||||
**Optimal System**: Hybrid - Old system's data model + Current system's implementation approach
|
||||
@@ -1,41 +0,0 @@
|
||||
# Draft Finalize Prompt
|
||||
|
||||
```
|
||||
SESSION ENDING
|
||||
==============
|
||||
This Claude Code session is completing.
|
||||
|
||||
TASK
|
||||
----
|
||||
Review the observations you generated and create a session summary.
|
||||
|
||||
Output this XML:
|
||||
|
||||
```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 session?</completed>
|
||||
<next_steps>What should be done next?</next_steps>
|
||||
<files_read>
|
||||
<file>src/auth.ts</file>
|
||||
<file>src/middleware/session.ts</file>
|
||||
</files_read>
|
||||
<files_edited>
|
||||
<file>src/auth.ts</file>
|
||||
</files_edited>
|
||||
<notes>Additional insights or context</notes>
|
||||
</summary>
|
||||
```
|
||||
|
||||
REQUIREMENTS
|
||||
------------
|
||||
All 8 fields are required: request, investigated, learned, completed, next_steps, files_read, files_edited, notes
|
||||
|
||||
Files must be wrapped in <file> tags
|
||||
|
||||
If no files were read/edited, use empty tags: <files_read></files_read>
|
||||
|
||||
Focus on semantic insights, not mechanical details.
|
||||
```
|
||||
@@ -1,92 +0,0 @@
|
||||
# Draft Init Prompt
|
||||
|
||||
```
|
||||
You are a memory processor for the "{project}" project.
|
||||
|
||||
SESSION CONTEXT
|
||||
---------------
|
||||
Session ID: {sessionId}
|
||||
User's Goal: {userPrompt}
|
||||
Date: {date}
|
||||
|
||||
YOUR ROLE
|
||||
---------
|
||||
Process tool executions from this Claude Code session and store important observations.
|
||||
|
||||
WHEN TO STORE
|
||||
-------------
|
||||
Store an observation when the tool output reveals significant information about:
|
||||
- Implementation of features or bug fixes
|
||||
- Architecture, design patterns, or system structure
|
||||
- Configuration, environment, or deployment details
|
||||
- Algorithms, business logic, or data flows
|
||||
- Errors, failures, or debugging insights
|
||||
|
||||
WHEN TO SKIP
|
||||
------------
|
||||
Skip routine operations:
|
||||
- Empty status checks (git status with no changes)
|
||||
- Package installations with no errors
|
||||
- Simple file listings
|
||||
- Repetitive operations you've already documented
|
||||
|
||||
OBSERVATION FORMAT
|
||||
------------------
|
||||
Output observations using this XML structure:
|
||||
|
||||
```xml
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<title>JWT Refresh Token Implementation</title>
|
||||
<subtitle>Added token rotation with Redis storage for secure sessions without re-login</subtitle>
|
||||
<facts>
|
||||
<fact>src/auth.ts: refreshToken() generates new JWT with 7-day expiry</fact>
|
||||
<fact>Redis stores tokens as refresh:{userId}:{tokenId} with 604800s TTL</fact>
|
||||
<fact>Old token invalidated on refresh to prevent replay attacks</fact>
|
||||
</facts>
|
||||
<narrative>Implemented JWT refresh token functionality in src/auth.ts. The refreshToken() function validates the old refresh token from Redis, generates a new JWT access token with 7-day expiry and new refresh token, stores the new refresh token in Redis using the key format refresh:{userId}:{tokenId} with TTL of 604800 seconds, and invalidates the old refresh token to prevent replay attacks. This enables long-lived authenticated sessions without requiring users to re-login while maintaining security through token rotation.</narrative>
|
||||
<concepts>
|
||||
<concept>authentication</concept>
|
||||
<concept>security</concept>
|
||||
<concept>session-management</concept>
|
||||
</concepts>
|
||||
<files>
|
||||
<file>src/auth.ts</file>
|
||||
<file>src/middleware/auth.ts</file>
|
||||
</files>
|
||||
</observation>
|
||||
```
|
||||
|
||||
FIELD REQUIREMENTS
|
||||
------------------
|
||||
|
||||
**type**: One of: decision, bugfix, feature, refactor, discovery
|
||||
|
||||
**title**: 3-8 words capturing the core action
|
||||
Examples: "JWT Refresh Token Implementation", "Database Connection Pool Fix"
|
||||
|
||||
**subtitle**: One sentence (max 24 words) explaining the significance
|
||||
Focus on outcome or benefit
|
||||
Examples: "Added token rotation with Redis storage for secure sessions without re-login"
|
||||
|
||||
**facts**: 3-7 specific, searchable statements (each 50-150 chars)
|
||||
Each fact is ONE piece of information
|
||||
Include filename or component name
|
||||
No pronouns - each fact must stand alone
|
||||
Examples:
|
||||
- "src/auth.ts: refreshToken() generates new JWT with 7-day expiry"
|
||||
- "Redis stores tokens as refresh:{userId}:{tokenId} with 604800s TTL"
|
||||
|
||||
**narrative**: Full explanation (200-400 words)
|
||||
What was done, how it works, why it matters
|
||||
Technical details: files, functions, data structures
|
||||
|
||||
**concepts**: 2-5 broad categories
|
||||
Examples: "authentication", "caching", "error-handling", "performance"
|
||||
|
||||
**files**: All files touched
|
||||
Full paths from project root
|
||||
Examples: "src/auth.ts", "tests/auth.test.ts"
|
||||
|
||||
Ready to process tool executions.
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
# Draft Observation Prompt
|
||||
|
||||
```
|
||||
TOOL OBSERVATION
|
||||
================
|
||||
Tool: {tool_name}
|
||||
Time: {timestamp}
|
||||
Prompt: {prompt_number}
|
||||
|
||||
Input:
|
||||
{tool_input JSON}
|
||||
|
||||
Output:
|
||||
{tool_output JSON}
|
||||
|
||||
TASK
|
||||
----
|
||||
Analyze this tool output. If it contains significant information about the codebase, generate an observation using the XML format from the init prompt.
|
||||
|
||||
If this is routine or repetitive, you can skip it.
|
||||
```
|
||||
@@ -1,286 +0,0 @@
|
||||
# Current Prompt Flow (SDK System)
|
||||
|
||||
## Architecture Overview
|
||||
- **System**: SDK Agent (persistent HTTP service via PM2)
|
||||
- **Storage**: SQLite (observations + summaries per prompt)
|
||||
- **Hooks**: Context (START), Summary (STOP)
|
||||
|
||||
---
|
||||
|
||||
## Flow Timeline
|
||||
|
||||
### 1. SESSION START (context-hook.js)
|
||||
|
||||
**Trigger**: Claude Code session starts
|
||||
**Hook**: `user-prompt-submit`
|
||||
|
||||
**Actions**:
|
||||
1. Create SDK session in database
|
||||
2. Initialize HTTP worker (if not running)
|
||||
3. Send init request to worker
|
||||
4. Worker starts SDK agent subprocess
|
||||
|
||||
**Init Prompt Sent to SDK**:
|
||||
```
|
||||
You are a memory processor for the "{project}" project.
|
||||
|
||||
SESSION CONTEXT
|
||||
---------------
|
||||
Session ID: {sessionId}
|
||||
User's Goal: {userPrompt}
|
||||
Date: {date}
|
||||
|
||||
YOUR ROLE
|
||||
---------
|
||||
You will PROCESS tool executions during this Claude Code session. Your job is to:
|
||||
|
||||
1. ANALYZE each tool response for meaningful content
|
||||
2. DECIDE whether it contains something worth storing
|
||||
3. EXTRACT the key insight
|
||||
4. STORE it as an observation in the XML format below
|
||||
|
||||
For MOST meaningful tool outputs, you should generate an observation. Only skip truly routine operations.
|
||||
|
||||
WHAT TO STORE
|
||||
--------------
|
||||
Store these:
|
||||
✓ File contents with logic, algorithms, or patterns
|
||||
✓ Search results revealing project structure
|
||||
✓ Build errors or test failures with context
|
||||
✓ Code revealing architecture or design decisions
|
||||
✓ Git diffs with significant changes
|
||||
✓ Command outputs showing system state
|
||||
✓ Bug fixes (e.g., "fixed race condition in auth middleware by adding mutex")
|
||||
✓ New features (e.g., "implemented JWT refresh token flow")
|
||||
✓ Refactorings (e.g., "extracted validation logic into separate service")
|
||||
✓ Discoveries (e.g., "found that API rate limit is 100 req/min")
|
||||
|
||||
WHAT TO SKIP
|
||||
------------
|
||||
Skip these:
|
||||
✗ Simple status checks (git status with no changes)
|
||||
✗ Trivial edits (one-line config changes)
|
||||
✗ Repeated operations
|
||||
✗ Anything without semantic value
|
||||
|
||||
HOW TO STORE OBSERVATIONS
|
||||
--------------------------
|
||||
When you identify something worth remembering, output your observation in this EXACT XML format:
|
||||
|
||||
```xml
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<text>Implemented JWT token refresh flow with 7-day expiry</text>
|
||||
</observation>
|
||||
```
|
||||
|
||||
Valid types: decision, bugfix, feature, refactor, discovery
|
||||
|
||||
Structure requirements:
|
||||
- <observation> is the root element
|
||||
- <type> must be one of the 5 valid types (single word)
|
||||
- <text> contains your concise observation (one sentence preferred)
|
||||
- No additional fields or nesting
|
||||
|
||||
The SDK worker will parse all <observation> blocks from your response using regex and store them in SQLite.
|
||||
|
||||
You can include your reasoning before or after the observation block, or just output the observation by itself.
|
||||
|
||||
Ready to process tool responses.
|
||||
```
|
||||
|
||||
**SDK Agent State**: Running, waiting for observations
|
||||
|
||||
---
|
||||
|
||||
### 2. TOOL EXECUTION (context-hook.js, continuous)
|
||||
|
||||
**Trigger**: Each tool execution in Claude Code
|
||||
**Hook**: `user-prompt-submit` (captures tool results)
|
||||
|
||||
**Actions**:
|
||||
1. Hook captures tool_name, tool_input, tool_output
|
||||
2. Sends observation to worker HTTP endpoint
|
||||
3. Worker queues observation for SDK agent
|
||||
|
||||
**Observation Prompt Sent to SDK**:
|
||||
```
|
||||
TOOL OBSERVATION
|
||||
================
|
||||
Tool: {tool_name}
|
||||
Time: {timestamp}
|
||||
|
||||
Input:
|
||||
{tool_input JSON}
|
||||
|
||||
Output:
|
||||
{tool_output JSON}
|
||||
|
||||
ANALYSIS TASK
|
||||
-------------
|
||||
ANALYZE this tool response and DECIDE: Does it contain something worth storing?
|
||||
|
||||
Most Read, Edit, Grep, Bash, and Write operations contain meaningful content.
|
||||
|
||||
If this contains something worth remembering, output the observation in this EXACT XML format:
|
||||
|
||||
```xml
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<text>Your concise observation here</text>
|
||||
</observation>
|
||||
```
|
||||
|
||||
Requirements:
|
||||
- Use one of these types: decision, bugfix, feature, refactor, discovery
|
||||
- Keep text concise (one sentence preferred)
|
||||
- No markdown formatting inside <text>
|
||||
- No additional XML fields
|
||||
|
||||
If this is truly routine (e.g., empty git status), you can skip it. Otherwise, PROCESS and STORE it.
|
||||
```
|
||||
|
||||
**SDK Response**:
|
||||
- Generates 0-N `<observation>` XML blocks
|
||||
- Worker parses and stores in `observations` table
|
||||
- Links to `prompt_number` (increments per user prompt)
|
||||
|
||||
---
|
||||
|
||||
### 3. PROMPT END (summary-hook.js)
|
||||
|
||||
**Trigger**: User prompt completes (stop-streaming event)
|
||||
**Hook**: `stop-streaming`
|
||||
|
||||
**Actions**:
|
||||
1. Send summarize request to worker
|
||||
2. Worker sends finalize prompt to SDK agent
|
||||
|
||||
**Finalize Prompt Sent to SDK**:
|
||||
```
|
||||
SESSION ENDING
|
||||
==============
|
||||
The Claude Code session is finishing.
|
||||
|
||||
FINAL TASK
|
||||
----------
|
||||
1. Review the observations you've stored this session
|
||||
2. Generate a structured summary that answers these questions:
|
||||
- What did user request?
|
||||
- What did you investigate?
|
||||
- What did you learn?
|
||||
- What did you do?
|
||||
- What's next?
|
||||
- Files read
|
||||
- Files edited
|
||||
- Notes
|
||||
|
||||
3. Generate the structured summary and output it in this EXACT XML format:
|
||||
|
||||
```xml
|
||||
<summary>
|
||||
<request>Implement JWT authentication system</request>
|
||||
<investigated>Existing auth middleware, session management, token storage patterns</investigated>
|
||||
<learned>Current system uses session cookies; no JWT support; race condition in middleware</learned>
|
||||
<completed>Implemented JWT token + refresh flow with 7-day expiry; fixed race condition with mutex; added token validation middleware</completed>
|
||||
<next_steps>Add token revocation API endpoint; write integration tests</next_steps>
|
||||
<files_read>
|
||||
<file>src/auth.ts</file>
|
||||
<file>src/middleware/session.ts</file>
|
||||
<file>src/types/user.ts</file>
|
||||
</files_read>
|
||||
<files_edited>
|
||||
<file>src/auth.ts</file>
|
||||
<file>src/middleware/auth.ts</file>
|
||||
<file>src/routes/auth.ts</file>
|
||||
</files_edited>
|
||||
<notes>Token secret stored in .env; refresh tokens use rotation strategy</notes>
|
||||
</summary>
|
||||
```
|
||||
|
||||
Structure requirements:
|
||||
- <summary> is the root element
|
||||
- All 8 child elements are REQUIRED: request, investigated, learned, completed, next_steps, files_read, files_edited, notes
|
||||
- <files_read> and <files_edited> must contain <file> child elements (one per file)
|
||||
- If no files were read/edited, use empty tags: <files_read></files_read>
|
||||
- Text fields can be multiple sentences but avoid markdown formatting
|
||||
- Use underscores in element names: next_steps, files_read, files_edited
|
||||
|
||||
The SDK worker will parse the <summary> block and extract all fields to store in SQLite.
|
||||
|
||||
Generate the summary now in the required XML format.
|
||||
```
|
||||
|
||||
**SDK Response**:
|
||||
- Generates `<summary>` XML block
|
||||
- Worker parses and stores in `session_summaries` table
|
||||
- Links to specific `prompt_number`
|
||||
|
||||
---
|
||||
|
||||
### 4. SESSION END (cleanup-hook.js)
|
||||
|
||||
**Trigger**: Claude Code session ends
|
||||
**Hook**: `session-end`
|
||||
|
||||
**Actions**:
|
||||
1. Mark session as completed
|
||||
2. SDK agent continues running (doesn't terminate)
|
||||
3. Worker stays alive for next session
|
||||
|
||||
---
|
||||
|
||||
## Data Storage
|
||||
|
||||
### Observations Table
|
||||
```sql
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
|
||||
)
|
||||
```
|
||||
|
||||
### Session Summaries Table
|
||||
```sql
|
||||
CREATE TABLE session_summaries (
|
||||
id INTEGER PRIMARY KEY,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT NOT NULL,
|
||||
investigated TEXT NOT NULL,
|
||||
learned TEXT NOT NULL,
|
||||
completed TEXT NOT NULL,
|
||||
next_steps TEXT NOT NULL,
|
||||
files_read TEXT NOT NULL, -- JSON array
|
||||
files_edited TEXT NOT NULL, -- JSON array
|
||||
notes TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Characteristics
|
||||
|
||||
### Strengths
|
||||
1. **Persistent SDK agent**: No restart overhead per prompt
|
||||
2. **Structured data**: Typed observations, structured summaries
|
||||
3. **Per-prompt tracking**: `prompt_number` links observations to specific requests
|
||||
4. **Foreign key integrity**: Observations link to sessions via SDK session ID
|
||||
|
||||
### Weaknesses
|
||||
1. **"MOST" ambiguity**: Init prompt says "For MOST meaningful tool outputs" - confusing
|
||||
2. **Observation prompt repetition**: "Most Read, Edit, Grep, Bash, and Write operations contain meaningful content" - contradicts selectivity
|
||||
3. **XML parsing brittleness**: Regex-based XML parsing fragile
|
||||
4. **No narrative context**: Observations are one-sentence only
|
||||
5. **Summary per prompt**: Creates many summaries, unclear if useful
|
||||
6. **No hierarchical organization**: Flat observation list
|
||||
7. **Limited searchability**: Simple text fields, no embedding/vector search
|
||||
@@ -1,38 +0,0 @@
|
||||
# Final Finalize Prompt
|
||||
|
||||
```
|
||||
SESSION ENDING
|
||||
==============
|
||||
This Claude Code session is completing.
|
||||
|
||||
TASK
|
||||
----
|
||||
Review the observations you generated and create a session summary.
|
||||
|
||||
Output this XML:
|
||||
|
||||
```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 session?]</completed>
|
||||
<next_steps>[What should be done next?]</next_steps>
|
||||
<files_read>
|
||||
<file>[path/to/file]</file>
|
||||
</files_read>
|
||||
<files_edited>
|
||||
<file>[path/to/file]</file>
|
||||
</files_edited>
|
||||
<notes>[Additional insights or context]</notes>
|
||||
</summary>
|
||||
```
|
||||
|
||||
REQUIREMENTS
|
||||
------------
|
||||
All 8 fields are required: request, investigated, learned, completed, next_steps, files_read, files_edited, notes
|
||||
|
||||
Files must be wrapped in <file> tags
|
||||
|
||||
If no files were read/edited, use empty tags: <files_read></files_read>
|
||||
```
|
||||
@@ -1,389 +0,0 @@
|
||||
# Recommended Prompt Flow (Hybrid System)
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Be Selective**: Quality over quantity - only store meaningful insights
|
||||
2. **Hierarchical Storage**: Multiple levels for granular retrieval
|
||||
3. **Clear Guidance**: No ambiguous language like "MOST"
|
||||
4. **Structured Data**: XML format with clear schema
|
||||
5. **Session Tracking**: Title + subtitle per session
|
||||
6. **Per-Prompt Context**: Track which observations came from which user request
|
||||
|
||||
---
|
||||
|
||||
## Flow Timeline
|
||||
|
||||
### 1. SESSION START
|
||||
|
||||
**Trigger**: Claude Code session starts
|
||||
**Hook**: `user-prompt-submit` (context-hook.js)
|
||||
|
||||
**Init Prompt Sent to SDK**:
|
||||
```
|
||||
You are a memory processor for the "{project}" project.
|
||||
|
||||
SESSION CONTEXT
|
||||
---------------
|
||||
Session ID: {sessionId}
|
||||
User's Goal: {userPrompt}
|
||||
Date: {date}
|
||||
|
||||
YOUR ROLE
|
||||
---------
|
||||
Process tool executions from this Claude Code session and store important observations.
|
||||
|
||||
Be selective. Only store observations when the tool output reveals important information about:
|
||||
- Architecture or design patterns
|
||||
- Implementation details of features or bug fixes
|
||||
- System state or configuration
|
||||
- Business logic or algorithms
|
||||
|
||||
Skip routine operations like:
|
||||
- Empty git status checks
|
||||
- Simple npm install output
|
||||
- Trivial config changes
|
||||
- Repetitive operations
|
||||
|
||||
OBSERVATION FORMAT
|
||||
------------------
|
||||
When you identify something worth remembering, output this XML structure:
|
||||
|
||||
```xml
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<title>Short Title (3-8 words)</title>
|
||||
<subtitle>Concise summary explaining the significance (max 24 words)</subtitle>
|
||||
<facts>
|
||||
<fact>Specific detail 1 (50-150 chars, self-contained)</fact>
|
||||
<fact>Specific detail 2 (50-150 chars, self-contained)</fact>
|
||||
<fact>Specific detail 3 (50-150 chars, self-contained)</fact>
|
||||
</facts>
|
||||
<narrative>Full context: what was done, why it matters, how it works. (200-400 words)</narrative>
|
||||
<concepts>
|
||||
<concept>broad-category-1</concept>
|
||||
<concept>broad-category-2</concept>
|
||||
</concepts>
|
||||
<files>
|
||||
<file>path/to/file1.ts</file>
|
||||
<file>path/to/file2.ts</file>
|
||||
</files>
|
||||
</observation>
|
||||
```
|
||||
|
||||
FIELD REQUIREMENTS
|
||||
------------------
|
||||
|
||||
**Type**: One of: decision, bugfix, feature, refactor, discovery
|
||||
|
||||
**Title**: 3-8 words capturing the core action
|
||||
- Examples: "JWT Refresh Token Implementation", "Race Condition Fix in Auth Middleware"
|
||||
|
||||
**Subtitle**: Max 24 words explaining the significance
|
||||
- Focus on outcome or benefit
|
||||
- Examples: "Added 7-day refresh token rotation with Redis storage for secure long-lived sessions"
|
||||
|
||||
**Facts**: 3-7 atomic facts (50-150 chars each)
|
||||
- Each fact is ONE specific piece of information
|
||||
- Include filename/component when relevant
|
||||
- No pronouns - each fact stands alone
|
||||
- Examples:
|
||||
- "src/auth.ts: refreshToken() generates new JWT with 7-day expiry"
|
||||
- "Redis key format: refresh:{userId}:{tokenId} with TTL 604800s"
|
||||
- "Old token invalidated on refresh to prevent replay attacks"
|
||||
|
||||
**Narrative**: Full story (200-400 words)
|
||||
- What was done
|
||||
- Technical details (files, functions, implementation)
|
||||
- Why it matters
|
||||
|
||||
**Concepts**: 2-5 broad categories for filtering
|
||||
- Examples: "authentication", "caching", "error-handling"
|
||||
|
||||
**Files**: All files touched
|
||||
- Full relative paths from project root
|
||||
|
||||
Ready to process tool executions.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. TOOL EXECUTION
|
||||
|
||||
**Trigger**: Each tool execution
|
||||
**Hook**: `user-prompt-submit` (context-hook.js)
|
||||
|
||||
**Observation Prompt Sent to SDK**:
|
||||
```
|
||||
TOOL OBSERVATION
|
||||
================
|
||||
Tool: {tool_name}
|
||||
Time: {timestamp}
|
||||
Prompt: {prompt_number}
|
||||
|
||||
Input:
|
||||
{tool_input JSON}
|
||||
|
||||
Output:
|
||||
{tool_output JSON}
|
||||
|
||||
Analyze this tool output. If it reveals important information about the codebase, generate an observation using the XML format from the init prompt.
|
||||
```
|
||||
|
||||
**SDK Response Processing**:
|
||||
1. SDK agent analyzes output
|
||||
2. If meaningful, generates `<observation>` XML block
|
||||
3. Worker parses XML and stores in SQLite
|
||||
4. Links to `prompt_number` for per-request tracking
|
||||
|
||||
**Database Schema**:
|
||||
```sql
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
|
||||
-- Core fields
|
||||
type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
subtitle TEXT NOT NULL,
|
||||
narrative TEXT NOT NULL,
|
||||
|
||||
-- Arrays (stored as JSON)
|
||||
facts TEXT NOT NULL, -- JSON array of strings
|
||||
concepts TEXT NOT NULL, -- JSON array of strings
|
||||
files TEXT NOT NULL, -- JSON array of strings
|
||||
|
||||
created_at INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Indexes for fast retrieval
|
||||
CREATE INDEX idx_observations_session ON observations(sdk_session_id);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_prompt ON observations(prompt_number);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. SESSION END
|
||||
|
||||
**Trigger**: Claude Code session ends
|
||||
**Hook**: `session-end` (cleanup-hook.js)
|
||||
|
||||
**Finalize Prompt Sent to SDK**:
|
||||
```
|
||||
SESSION ENDING
|
||||
==============
|
||||
The Claude Code session is completing.
|
||||
|
||||
FINAL TASK
|
||||
----------
|
||||
Review all observations you've generated and create a session summary.
|
||||
|
||||
Output this XML structure:
|
||||
|
||||
```xml
|
||||
<summary>
|
||||
<request>What did the user request?</request>
|
||||
<investigated>What code/systems did you explore?</investigated>
|
||||
<learned>What did you learn about the codebase?</learned>
|
||||
<completed>What was accomplished?</completed>
|
||||
<next_steps>What should happen next?</next_steps>
|
||||
<files_read>
|
||||
<file>path/to/file1.ts</file>
|
||||
<file>path/to/file2.ts</file>
|
||||
</files_read>
|
||||
<files_edited>
|
||||
<file>path/to/file3.ts</file>
|
||||
</files_edited>
|
||||
<notes>Additional context or insights</notes>
|
||||
</summary>
|
||||
```
|
||||
|
||||
Be concise but comprehensive. Focus on semantic insights, not mechanical details.
|
||||
```
|
||||
|
||||
**Database Schema**:
|
||||
```sql
|
||||
CREATE TABLE session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
|
||||
request TEXT NOT NULL,
|
||||
investigated TEXT NOT NULL,
|
||||
learned TEXT NOT NULL,
|
||||
completed TEXT NOT NULL,
|
||||
next_steps TEXT NOT NULL,
|
||||
files_read TEXT NOT NULL, -- JSON array
|
||||
files_edited TEXT NOT NULL, -- JSON array
|
||||
notes TEXT NOT NULL,
|
||||
|
||||
created_at INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Retrieval Patterns
|
||||
|
||||
### Level 1: Session Titles (High-Level Browsing)
|
||||
```sql
|
||||
SELECT
|
||||
sdk_session_id,
|
||||
user_prompt as title,
|
||||
created_at
|
||||
FROM sdk_sessions
|
||||
WHERE project = ?
|
||||
ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
### Level 2: Session Summaries (Session Overview)
|
||||
```sql
|
||||
SELECT
|
||||
request,
|
||||
completed,
|
||||
next_steps
|
||||
FROM session_summaries
|
||||
WHERE sdk_session_id = ?;
|
||||
```
|
||||
|
||||
### Level 3: Observation Titles (Scannable List)
|
||||
```sql
|
||||
SELECT
|
||||
type,
|
||||
title,
|
||||
subtitle
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY id;
|
||||
```
|
||||
|
||||
### Level 4: Atomic Facts (Precise Search)
|
||||
```sql
|
||||
SELECT
|
||||
title,
|
||||
facts
|
||||
FROM observations
|
||||
WHERE
|
||||
sdk_session_id = ?
|
||||
AND facts LIKE '%keyword%';
|
||||
```
|
||||
|
||||
### Level 5: Full Narrative (Deep Dive)
|
||||
```sql
|
||||
SELECT
|
||||
title,
|
||||
subtitle,
|
||||
facts,
|
||||
narrative,
|
||||
files
|
||||
FROM observations
|
||||
WHERE id = ?;
|
||||
```
|
||||
|
||||
### By Concept (Category Filter)
|
||||
```sql
|
||||
SELECT
|
||||
title,
|
||||
subtitle,
|
||||
concepts
|
||||
FROM observations
|
||||
WHERE concepts LIKE '%"authentication"%';
|
||||
```
|
||||
|
||||
### By File (File-Based Search)
|
||||
```sql
|
||||
SELECT
|
||||
title,
|
||||
subtitle,
|
||||
files
|
||||
FROM observations
|
||||
WHERE files LIKE '%src/auth.ts%';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Phase 2: Semantic Search
|
||||
- Add vector embeddings for facts and narratives
|
||||
- Store in ChromaDB or similar
|
||||
- Enable similarity search: "Find observations about authentication patterns"
|
||||
|
||||
### Phase 3: Cross-Session Memory
|
||||
- Link related observations across sessions
|
||||
- "Show all JWT-related observations from past 30 days"
|
||||
|
||||
### Phase 4: Session Metadata
|
||||
- Add title/subtitle to sdk_sessions table
|
||||
- Auto-generate from user_prompt or first summary
|
||||
|
||||
---
|
||||
|
||||
## Migration from Current System
|
||||
|
||||
### Step 1: Update Database Schema
|
||||
```sql
|
||||
-- Add new columns to observations table
|
||||
ALTER TABLE observations ADD COLUMN title TEXT;
|
||||
ALTER TABLE observations ADD COLUMN subtitle TEXT;
|
||||
ALTER TABLE observations ADD COLUMN narrative TEXT;
|
||||
ALTER TABLE observations ADD COLUMN facts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN concepts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files TEXT;
|
||||
|
||||
-- Migrate existing observations (best-effort)
|
||||
UPDATE observations
|
||||
SET
|
||||
title = type || ' - ' || substr(text, 1, 50),
|
||||
subtitle = text,
|
||||
narrative = text,
|
||||
facts = '[]',
|
||||
concepts = '[]',
|
||||
files = '[]'
|
||||
WHERE title IS NULL;
|
||||
```
|
||||
|
||||
### Step 2: Update Prompts
|
||||
- Replace `buildInitPrompt()` with new version (no "MOST")
|
||||
- Replace `buildObservationPrompt()` with new version (no tool-type bias)
|
||||
- Keep `buildFinalizePrompt()` mostly as-is
|
||||
|
||||
### Step 3: Update Parser
|
||||
- Extend `parseObservations()` to extract all new fields
|
||||
- Add `extractFactArray()`, `extractConceptArray()`, `extractFileArray()` helpers
|
||||
- Keep backward compatibility with old one-sentence format
|
||||
|
||||
### Step 4: Update Storage
|
||||
- Modify `HooksDatabase.storeObservation()` to accept all fields
|
||||
- Store arrays as JSON strings
|
||||
|
||||
---
|
||||
|
||||
## Key Improvements Over Current System
|
||||
|
||||
1. ✅ **No "MOST" ambiguity** - Clear "be selective" guidance
|
||||
2. ✅ **No tool-type bias** - Observation prompt doesn't mention tool names
|
||||
3. ✅ **Hierarchical storage** - Title → Subtitle → Facts → Narrative
|
||||
4. ✅ **Atomic facts** - Precise, searchable details
|
||||
5. ✅ **File associations** - Track which files each observation relates to
|
||||
6. ✅ **Concept tagging** - Categorical organization
|
||||
7. ✅ **Rich narratives** - Full context for deep dives
|
||||
8. ✅ **Multiple retrieval levels** - Can search at any granularity
|
||||
|
||||
---
|
||||
|
||||
## Key Improvements Over Old System
|
||||
|
||||
1. ✅ **No bash commands** - XML parsing instead of shell execution
|
||||
2. ✅ **Auto-increment IDs** - No manual counter tracking
|
||||
3. ✅ **Per-prompt tracking** - `prompt_number` links observations to requests
|
||||
4. ✅ **Foreign key integrity** - Automatic cascade deletes
|
||||
5. ✅ **No quote escaping hell** - JSON arrays instead of bash arguments
|
||||
6. ✅ **Structured typing** - Typed observations (decision/bugfix/feature/refactor/discovery)
|
||||
7. ✅ **Session summary at end** - Not just 2-3 sentences, but full structured summary
|
||||
@@ -1,91 +0,0 @@
|
||||
# Final Init Prompt
|
||||
|
||||
```
|
||||
You are a memory processor for the "{project}" project.
|
||||
|
||||
SESSION CONTEXT
|
||||
---------------
|
||||
Session ID: {sessionId}
|
||||
User's Goal: {userPrompt}
|
||||
Date: {date}
|
||||
|
||||
YOUR ROLE
|
||||
---------
|
||||
Process tool executions from this Claude Code session and store observations that contain information worth remembering.
|
||||
|
||||
WHEN TO STORE
|
||||
-------------
|
||||
Store an observation 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
|
||||
|
||||
WHEN TO SKIP
|
||||
------------
|
||||
Skip routine operations:
|
||||
- Empty status checks
|
||||
- Package installations with no errors
|
||||
- Simple file listings
|
||||
- Repetitive operations you've already documented
|
||||
|
||||
OBSERVATION FORMAT
|
||||
------------------
|
||||
Output observations using this XML structure:
|
||||
|
||||
```xml
|
||||
<observation>
|
||||
<type>change</type>
|
||||
<title>[Short title]</title>
|
||||
<subtitle>[One sentence explanation (max 24 words)]</subtitle>
|
||||
<facts>
|
||||
<fact>[Concise, self-contained statement]</fact>
|
||||
<fact>[Concise, self-contained statement]</fact>
|
||||
<fact>[Concise, self-contained statement]</fact>
|
||||
</facts>
|
||||
<narrative>[Full context: what, how, and why]</narrative>
|
||||
<concepts>
|
||||
<concept>[knowledge-type-category]</concept>
|
||||
<concept>[knowledge-type-category]</concept>
|
||||
</concepts>
|
||||
<files>
|
||||
<file>[path/to/file]</file>
|
||||
<file>[path/to/file]</file>
|
||||
</files>
|
||||
</observation>
|
||||
```
|
||||
|
||||
FIELD REQUIREMENTS
|
||||
------------------
|
||||
|
||||
**type**: One of:
|
||||
- change: modifications to code, config, or documentation
|
||||
- discovery: learning about existing system
|
||||
- decision: choosing an approach and why it was chosen
|
||||
|
||||
**title**: Short title capturing the core action or topic
|
||||
|
||||
**subtitle**: One sentence explanation (max 24 words)
|
||||
|
||||
**facts**: Concise, self-contained statements
|
||||
Each fact is ONE piece of information
|
||||
No pronouns - each fact must stand alone
|
||||
Include specific details: filenames, functions, values
|
||||
|
||||
**narrative**: Full context: what, how, and why
|
||||
What was done, how it works, why it matters
|
||||
|
||||
**concepts**: 2-5 knowledge-type categories:
|
||||
- how-it-works: understanding mechanisms
|
||||
- why-it-exists: purpose or rationale
|
||||
- what-changed: modifications made
|
||||
- problem-solution: issues and their fixes
|
||||
- gotcha: traps or edge cases
|
||||
- pattern: reusable approach
|
||||
- trade-off: pros/cons of a decision
|
||||
|
||||
**files**: All files touched (full paths from project root)
|
||||
|
||||
Ready to process tool executions.
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
# Final Observation Prompt
|
||||
|
||||
```
|
||||
TOOL OBSERVATION
|
||||
================
|
||||
Tool: {tool_name}
|
||||
Time: {timestamp}
|
||||
Prompt: {prompt_number}
|
||||
|
||||
Input:
|
||||
{tool_input JSON}
|
||||
|
||||
Output:
|
||||
{tool_output JSON}
|
||||
|
||||
Analyze this tool output. If it contains information worth remembering, generate an observation using the XML format.
|
||||
```
|
||||
@@ -1,293 +0,0 @@
|
||||
# Old Prompt Flow (Bash Command System)
|
||||
|
||||
## Architecture Overview
|
||||
- **System**: SDK Agent (per-session subprocess)
|
||||
- **Storage**: ChromaDB (hierarchical memories via bash commands)
|
||||
- **Hooks**: Session lifecycle tracking
|
||||
|
||||
---
|
||||
|
||||
## Flow Timeline
|
||||
|
||||
### 1. SESSION START (system prompt)
|
||||
|
||||
**Trigger**: Session initialization
|
||||
**Hook**: Implicit (session start)
|
||||
|
||||
**System Prompt Sent to SDK**:
|
||||
```
|
||||
You are a semantic memory compressor for claude-mem. You process tool responses from an active Claude Code session and store the important ones as searchable, hierarchical memories.
|
||||
|
||||
# SESSION CONTEXT
|
||||
- Project: {project}
|
||||
- Session: {sessionId}
|
||||
- Date: {date}
|
||||
- User Request: "{userPrompt}"
|
||||
|
||||
# YOUR JOB
|
||||
|
||||
## FIRST: Generate Session Title
|
||||
|
||||
IMMEDIATELY generate a title and subtitle for this session based on the user request.
|
||||
|
||||
Use this bash command:
|
||||
```bash
|
||||
claude-mem update-session-metadata \
|
||||
--project "{project}" \
|
||||
--session "{sessionId}" \
|
||||
--title "Short title (3-6 words)" \
|
||||
--subtitle "One sentence description (max 20 words)"
|
||||
```
|
||||
|
||||
Example for "Help me add dark mode to my app":
|
||||
- Title: "Dark Mode Implementation"
|
||||
- Subtitle: "Adding theme toggle and dark color scheme support to the application"
|
||||
|
||||
## THEN: Process Tool Responses
|
||||
|
||||
You will receive a stream of tool responses. For each one:
|
||||
|
||||
1. ANALYZE: Does this contain information worth remembering?
|
||||
2. DECIDE: Should I store this or skip it?
|
||||
3. EXTRACT: What are the key semantic concepts?
|
||||
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
|
||||
5. STORE: Use bash to save the hierarchical memory
|
||||
6. TRACK: Keep count of stored memories (001, 002, 003...)
|
||||
|
||||
# WHAT TO STORE
|
||||
|
||||
Store these:
|
||||
- File contents with logic, algorithms, or patterns
|
||||
- Search results revealing project structure
|
||||
- Build errors or test failures with context
|
||||
- Code revealing architecture or design decisions
|
||||
- Git diffs with significant changes
|
||||
- Command outputs showing system state
|
||||
|
||||
Skip these:
|
||||
- Simple status checks (git status with no changes)
|
||||
- Trivial edits (one-line config changes)
|
||||
- Repeated operations
|
||||
- Binary data or noise
|
||||
- Anything without semantic value
|
||||
|
||||
# HIERARCHICAL MEMORY FORMAT
|
||||
|
||||
Each memory has FOUR components:
|
||||
|
||||
## 1. TITLE (3-8 words)
|
||||
A scannable headline that captures the core action or topic.
|
||||
Examples:
|
||||
- "SDK Transcript Cleanup Implementation"
|
||||
- "Hook System Architecture Analysis"
|
||||
- "ChromaDB Migration Planning"
|
||||
|
||||
## 2. SUBTITLE (max 24 words)
|
||||
A concise, memorable summary that captures the essence of the change.
|
||||
Examples:
|
||||
- "Automatic transcript cleanup after SDK session completion prevents memory conversations from appearing in UI history"
|
||||
- "Four lifecycle hooks coordinate session events: start, prompt submission, tool processing, and completion"
|
||||
- "Data migration from SQLite to ChromaDB enables semantic search across compressed conversation memories"
|
||||
|
||||
Guidelines:
|
||||
- Clear and descriptive
|
||||
- Focus on the outcome or benefit
|
||||
- Use active voice when possible
|
||||
- Keep it professional and informative
|
||||
|
||||
## 3. ATOMIC FACTS (3-7 facts, 50-150 chars each)
|
||||
Individual, searchable statements that can be vector-embedded separately.
|
||||
Each fact is ONE specific piece of information.
|
||||
|
||||
Examples:
|
||||
- "stop-streaming.js: Auto-deletes SDK transcripts after completion"
|
||||
- "Path format: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"
|
||||
- "Uses fs.unlink with graceful error handling for missing files"
|
||||
- "Checks two transcript path formats for backward compatibility"
|
||||
|
||||
Guidelines:
|
||||
- Start with filename or component when relevant
|
||||
- Be specific: include paths, function names, actual values
|
||||
- Each fact stands alone (no pronouns like "it" or "this")
|
||||
- 50-150 characters target
|
||||
- Focus on searchable technical details
|
||||
|
||||
## 4. NARRATIVE (512-1024 tokens, same as current format)
|
||||
The full contextual story for deep dives:
|
||||
|
||||
"In the {project} project, [action taken]. [Technical details: files, functions, concepts]. [Why this matters]."
|
||||
|
||||
This is the detailed explanation for when someone needs full context.
|
||||
|
||||
# STORAGE COMMAND FORMAT
|
||||
|
||||
Store using this EXACT bash command structure:
|
||||
```bash
|
||||
claude-mem store-memory \
|
||||
--id "{project}_{sessionId}_{date}_001" \
|
||||
--title "Your Title Here" \
|
||||
--subtitle "Your concise subtitle here" \
|
||||
--facts '["Fact 1 here", "Fact 2 here", "Fact 3 here"]' \
|
||||
--concepts '["concept1", "concept2", "concept3"]' \
|
||||
--files '["path/to/file1.js", "path/to/file2.ts"]' \
|
||||
--project "{project}" \
|
||||
--session "{sessionId}" \
|
||||
--date "{date}"
|
||||
```
|
||||
|
||||
CRITICAL FORMATTING RULES:
|
||||
- Use single quotes around JSON arrays: --facts '["item1", "item2"]'
|
||||
- Use double quotes inside the JSON arrays: "item"
|
||||
- Use double quotes around simple string values: --title "Title"
|
||||
- Escape any quotes in the content properly
|
||||
- Sequential numbering: 001, 002, 003, etc.
|
||||
|
||||
Concepts: 2-5 broad categories (e.g., "hooks", "storage", "async-processing")
|
||||
Files: Actual file paths touched (e.g., "hooks/stop-streaming.js")
|
||||
|
||||
# EXAMPLE MEMORY
|
||||
|
||||
Tool response shows: [Read file hooks/stop-streaming.js with 167 lines of code implementing SDK cleanup]
|
||||
|
||||
Your storage command:
|
||||
```bash
|
||||
claude-mem store-memory \
|
||||
--id "claude-mem_abc123_2025-10-01_001" \
|
||||
--title "SDK Transcript Auto-Cleanup" \
|
||||
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history" \
|
||||
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl", "Uses fs.unlink with error handling for missing files", "Prevents memory conversations from polluting Claude Code UI"]' \
|
||||
--concepts '["cleanup", "SDK-lifecycle", "UX", "file-management"]' \
|
||||
--files '["hooks/stop-streaming.js"]' \
|
||||
--project "claude-mem" \
|
||||
--session "abc123" \
|
||||
--date "2025-10-01"
|
||||
```
|
||||
|
||||
# STATE TRACKING
|
||||
|
||||
CRITICAL: Keep track of your memory counter across all tool messages.
|
||||
- Start at 001
|
||||
- Increment for each stored memory
|
||||
- Never repeat numbers
|
||||
- Each session has separate numbering
|
||||
|
||||
# SESSION END
|
||||
|
||||
At the end (when I send "SESSION ENDING"), generate an overview using:
|
||||
```bash
|
||||
claude-mem store-overview --project "{project}" --session "{sessionId}" --content "2-3 sentence overview"
|
||||
```
|
||||
|
||||
# IMPORTANT REMINDERS
|
||||
|
||||
- You're processing a DIFFERENT Claude Code session (not your own)
|
||||
- Use Bash tool to call claude-mem commands
|
||||
- Keep subtitles clear and informative (max 24 words)
|
||||
- Each fact is ONE specific thing (not multiple ideas)
|
||||
- Be selective - quality over quantity
|
||||
- Always increment memory numbers
|
||||
- Facts should be searchable (specific file names, paths, functions)
|
||||
|
||||
Ready for tool responses.
|
||||
```
|
||||
|
||||
**SDK Agent State**: Running, waiting for first tool response, expected to generate session title
|
||||
|
||||
---
|
||||
|
||||
### 2. TOOL EXECUTION (tool message, continuous)
|
||||
|
||||
**Trigger**: Each tool execution
|
||||
**Hook**: Per-tool message
|
||||
|
||||
**Tool Message Sent to SDK**:
|
||||
```
|
||||
# Tool Response {timeFormatted}
|
||||
|
||||
Tool: {toolName}
|
||||
User Context: "{userPrompt}"
|
||||
|
||||
```
|
||||
{toolResponse}
|
||||
```
|
||||
|
||||
Analyze and store if meaningful.
|
||||
```
|
||||
|
||||
**Expected SDK Behavior**:
|
||||
1. Analyze tool response
|
||||
2. If meaningful, decompose into 4-part hierarchical memory
|
||||
3. Generate bash command: `claude-mem store-memory ...`
|
||||
4. Execute bash command via Bash tool
|
||||
5. Increment memory counter (001, 002, 003...)
|
||||
|
||||
**Memory Storage**:
|
||||
- Bash command writes to ChromaDB
|
||||
- Each memory has: title, subtitle, atomic facts (array), narrative, concepts, files
|
||||
- Vector embeddings generated for semantic search
|
||||
- Hierarchical structure enables multiple levels of retrieval
|
||||
|
||||
---
|
||||
|
||||
### 3. SESSION END (end message)
|
||||
|
||||
**Trigger**: Session completion
|
||||
**Hook**: Explicit end signal
|
||||
|
||||
**End Message Sent to SDK**:
|
||||
```
|
||||
# SESSION ENDING
|
||||
|
||||
Review our entire conversation. Generate a concise 2-3 sentence overview of what was accomplished.
|
||||
|
||||
Store it using Bash:
|
||||
```bash
|
||||
claude-mem store-overview --project "{project}" --session "{sessionId}" --content "YOUR_OVERVIEW_HERE"
|
||||
```
|
||||
|
||||
Focus on: what was done, current state, key decisions, outcomes.
|
||||
```
|
||||
|
||||
**Expected SDK Behavior**:
|
||||
1. Review all stored memories from session
|
||||
2. Generate 2-3 sentence overview
|
||||
3. Execute: `claude-mem store-overview ...`
|
||||
4. Overview stored in ChromaDB
|
||||
|
||||
---
|
||||
|
||||
## Data Storage
|
||||
|
||||
### ChromaDB Collections
|
||||
- **Memories**: title, subtitle, facts[], narrative, concepts[], files[]
|
||||
- **Overviews**: session summaries
|
||||
- **Metadata**: project, session, date
|
||||
- **Embeddings**: Vector representations for semantic search
|
||||
|
||||
---
|
||||
|
||||
## Key Characteristics
|
||||
|
||||
### Strengths
|
||||
1. **Hierarchical memory**: 4 levels (title → subtitle → facts → narrative)
|
||||
2. **Semantic search**: Vector embeddings via ChromaDB
|
||||
3. **Granular retrieval**: Can search at fact level or narrative level
|
||||
4. **Concept tagging**: Broad categories for filtering
|
||||
5. **File tracking**: Explicit file associations
|
||||
6. **Session metadata**: Title + subtitle per session
|
||||
7. **Clear examples**: Concrete bash command examples
|
||||
8. **State tracking**: Explicit memory counter (001, 002, 003...)
|
||||
9. **Quality over quantity**: Emphasis on being selective
|
||||
10. **Standalone facts**: No pronouns, each fact self-contained
|
||||
|
||||
### Weaknesses
|
||||
1. **Bash tool dependency**: Requires SDK agent to execute bash commands
|
||||
2. **Complex prompt**: Very long system prompt (185 lines)
|
||||
3. **Manual counter**: Agent must track memory numbers manually
|
||||
4. **Quote escaping**: Complex bash quoting rules prone to errors
|
||||
5. **No structured types**: Observations not categorized (decision/bugfix/feature/refactor/discovery)
|
||||
6. **Single overview**: Only one overview per session (not per prompt)
|
||||
7. **ChromaDB dependency**: Requires external vector database
|
||||
8. **Token-heavy**: 512-1024 token narratives + long prompts = high token usage
|
||||
9. **Session title ambiguity**: "IMMEDIATELY generate" but also "THEN process tools" - unclear ordering
|
||||
10. **No per-prompt summaries**: Can't track what was accomplished per user request
|
||||
@@ -1,217 +0,0 @@
|
||||
// src/prompts/hook-prompts.config.ts
|
||||
var HOOK_CONFIG = {
|
||||
maxUserPromptLength: 200,
|
||||
maxToolResponseLength: 20000,
|
||||
sdk: {
|
||||
model: "claude-sonnet-4-5",
|
||||
allowedTools: ["Bash"],
|
||||
maxTokensSystem: 8192,
|
||||
maxTokensTool: 8192,
|
||||
maxTokensEnd: 2048
|
||||
}
|
||||
};
|
||||
var SYSTEM_PROMPT = `You are a semantic memory compressor for claude-mem. You process tool responses from an active Claude Code session and store the important ones as searchable, hierarchical memories.
|
||||
|
||||
# SESSION CONTEXT
|
||||
- Project: {{project}}
|
||||
- Session: {{sessionId}}
|
||||
- Date: {{date}}
|
||||
- User Request: "{{userPrompt}}"
|
||||
|
||||
# YOUR JOB
|
||||
|
||||
## FIRST: Generate Session Title
|
||||
|
||||
IMMEDIATELY generate a title and subtitle for this session based on the user request.
|
||||
|
||||
Use this bash command:
|
||||
\`\`\`bash
|
||||
claude-mem update-session-metadata \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--title "Short title (3-6 words)" \\
|
||||
--subtitle "One sentence description (max 20 words)"
|
||||
\`\`\`
|
||||
|
||||
Example for "Help me add dark mode to my app":
|
||||
- Title: "Dark Mode Implementation"
|
||||
- Subtitle: "Adding theme toggle and dark color scheme support to the application"
|
||||
|
||||
## THEN: Process Tool Responses
|
||||
|
||||
You will receive a stream of tool responses. For each one:
|
||||
|
||||
1. ANALYZE: Does this contain information worth remembering?
|
||||
2. DECIDE: Should I store this or skip it?
|
||||
3. EXTRACT: What are the key semantic concepts?
|
||||
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
|
||||
5. STORE: Use bash to save the hierarchical memory
|
||||
6. TRACK: Keep count of stored memories (001, 002, 003...)
|
||||
|
||||
# WHAT TO STORE
|
||||
|
||||
Store these:
|
||||
- File contents with logic, algorithms, or patterns
|
||||
- Search results revealing project structure
|
||||
- Build errors or test failures with context
|
||||
- Code revealing architecture or design decisions
|
||||
- Git diffs with significant changes
|
||||
- Command outputs showing system state
|
||||
|
||||
Skip these:
|
||||
- Simple status checks (git status with no changes)
|
||||
- Trivial edits (one-line config changes)
|
||||
- Repeated operations
|
||||
- Binary data or noise
|
||||
- Anything without semantic value
|
||||
|
||||
# HIERARCHICAL MEMORY FORMAT
|
||||
|
||||
Each memory has FOUR components:
|
||||
|
||||
## 1. TITLE (3-8 words)
|
||||
A scannable headline that captures the core action or topic.
|
||||
Examples:
|
||||
- "SDK Transcript Cleanup Implementation"
|
||||
- "Hook System Architecture Analysis"
|
||||
- "ChromaDB Migration Planning"
|
||||
|
||||
## 2. SUBTITLE (max 24 words)
|
||||
A concise, memorable summary that captures the essence of the change.
|
||||
Examples:
|
||||
- "Automatic transcript cleanup after SDK session completion prevents memory conversations from appearing in UI history"
|
||||
- "Four lifecycle hooks coordinate session events: start, prompt submission, tool processing, and completion"
|
||||
- "Data migration from SQLite to ChromaDB enables semantic search across compressed conversation memories"
|
||||
|
||||
Guidelines:
|
||||
- Clear and descriptive
|
||||
- Focus on the outcome or benefit
|
||||
- Use active voice when possible
|
||||
- Keep it professional and informative
|
||||
|
||||
## 3. ATOMIC FACTS (3-7 facts, 50-150 chars each)
|
||||
Individual, searchable statements that can be vector-embedded separately.
|
||||
Each fact is ONE specific piece of information.
|
||||
|
||||
Examples:
|
||||
- "stop-streaming.js: Auto-deletes SDK transcripts after completion"
|
||||
- "Path format: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"
|
||||
- "Uses fs.unlink with graceful error handling for missing files"
|
||||
- "Checks two transcript path formats for backward compatibility"
|
||||
|
||||
Guidelines:
|
||||
- Start with filename or component when relevant
|
||||
- Be specific: include paths, function names, actual values
|
||||
- Each fact stands alone (no pronouns like "it" or "this")
|
||||
- 50-150 characters target
|
||||
- Focus on searchable technical details
|
||||
|
||||
## 4. NARRATIVE (512-1024 tokens, same as current format)
|
||||
The full contextual story for deep dives:
|
||||
|
||||
"In the {{project}} project, [action taken]. [Technical details: files, functions, concepts]. [Why this matters]."
|
||||
|
||||
This is the detailed explanation for when someone needs full context.
|
||||
|
||||
# STORAGE COMMAND FORMAT
|
||||
|
||||
Store using this EXACT bash command structure:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "{{project}}_{{sessionId}}_{{date}}_001" \\
|
||||
--title "Your Title Here" \\
|
||||
--subtitle "Your concise subtitle here" \\
|
||||
--facts '["Fact 1 here", "Fact 2 here", "Fact 3 here"]' \\
|
||||
--concepts '["concept1", "concept2", "concept3"]' \\
|
||||
--files '["path/to/file1.js", "path/to/file2.ts"]' \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--date "{{date}}"
|
||||
\`\`\`
|
||||
|
||||
CRITICAL FORMATTING RULES:
|
||||
- Use single quotes around JSON arrays: --facts '["item1", "item2"]'
|
||||
- Use double quotes inside the JSON arrays: "item"
|
||||
- Use double quotes around simple string values: --title "Title"
|
||||
- Escape any quotes in the content properly
|
||||
- Sequential numbering: 001, 002, 003, etc.
|
||||
|
||||
Concepts: 2-5 broad categories (e.g., "hooks", "storage", "async-processing")
|
||||
Files: Actual file paths touched (e.g., "hooks/stop-streaming.js")
|
||||
|
||||
# EXAMPLE MEMORY
|
||||
|
||||
Tool response shows: [Read file hooks/stop-streaming.js with 167 lines of code implementing SDK cleanup]
|
||||
|
||||
Your storage command:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "claude-mem_abc123_2025-10-01_001" \\
|
||||
--title "SDK Transcript Auto-Cleanup" \\
|
||||
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history" \\
|
||||
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl", "Uses fs.unlink with error handling for missing files", "Prevents memory conversations from polluting Claude Code UI"]' \\
|
||||
--concepts '["cleanup", "SDK-lifecycle", "UX", "file-management"]' \\
|
||||
--files '["hooks/stop-streaming.js"]' \\
|
||||
--project "claude-mem" \\
|
||||
--session "abc123" \\
|
||||
--date "2025-10-01"
|
||||
\`\`\`
|
||||
|
||||
# STATE TRACKING
|
||||
|
||||
CRITICAL: Keep track of your memory counter across all tool messages.
|
||||
- Start at 001
|
||||
- Increment for each stored memory
|
||||
- Never repeat numbers
|
||||
- Each session has separate numbering
|
||||
|
||||
# SESSION END
|
||||
|
||||
At the end (when I send "SESSION ENDING"), generate an overview using:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "2-3 sentence overview"
|
||||
\`\`\`
|
||||
|
||||
# IMPORTANT REMINDERS
|
||||
|
||||
- You're processing a DIFFERENT Claude Code session (not your own)
|
||||
- Use Bash tool to call claude-mem commands
|
||||
- Keep subtitles clear and informative (max 24 words)
|
||||
- Each fact is ONE specific thing (not multiple ideas)
|
||||
- Be selective - quality over quantity
|
||||
- Always increment memory numbers
|
||||
- Facts should be searchable (specific file names, paths, functions)
|
||||
|
||||
Ready for tool responses.`;
|
||||
var TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
|
||||
|
||||
Tool: {{toolName}}
|
||||
User Context: "{{userPrompt}}"
|
||||
|
||||
\`\`\`
|
||||
{{toolResponse}}
|
||||
\`\`\`
|
||||
|
||||
Analyze and store if meaningful.`;
|
||||
var END_MESSAGE = `# SESSION ENDING
|
||||
|
||||
Review our entire conversation. Generate a concise 2-3 sentence overview of what was accomplished.
|
||||
|
||||
Store it using Bash:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "YOUR_OVERVIEW_HERE"
|
||||
\`\`\`
|
||||
|
||||
Focus on: what was done, current state, key decisions, outcomes.`;
|
||||
var PROMPTS = {
|
||||
system: SYSTEM_PROMPT,
|
||||
tool: TOOL_MESSAGE,
|
||||
end: END_MESSAGE
|
||||
};
|
||||
export {
|
||||
TOOL_MESSAGE,
|
||||
SYSTEM_PROMPT,
|
||||
PROMPTS,
|
||||
HOOK_CONFIG,
|
||||
END_MESSAGE
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
MEMORY PROCESSING SESSION COMPLETED
|
||||
===================================
|
||||
This session has completed. Review the observations you generated and create a session summary.
|
||||
|
||||
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 session?]</completed>
|
||||
<next_steps>[What should be done next?]</next_steps>
|
||||
<notes>[Additional insights or context]</notes>
|
||||
</summary>
|
||||
|
||||
**Required fields**: request, investigated, learned, completed, next_steps
|
||||
|
||||
**Optional fields**: notes
|
||||
@@ -1,83 +0,0 @@
|
||||
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.
|
||||
|
||||
You are processing tool executions from a Claude Code session with the following context:
|
||||
|
||||
User's Goal: {userPrompt}
|
||||
Date: {date}
|
||||
|
||||
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
|
||||
|
||||
WHEN TO SKIP
|
||||
------------
|
||||
Skip routine operations:
|
||||
- Empty status checks
|
||||
- Package installations with no errors
|
||||
- Simple file listings
|
||||
- Repetitive operations you've already documented
|
||||
|
||||
OUTPUT FORMAT
|
||||
-------------
|
||||
Output observations using this XML structure:
|
||||
|
||||
```xml
|
||||
<observation>
|
||||
<type>[ change | discovery | decision ]</type>
|
||||
<!--
|
||||
**type**: One of:
|
||||
- change: modifications to code, config, or documentation
|
||||
- discovery: learning about existing system
|
||||
- decision: choosing an approach and why it was chosen
|
||||
-->
|
||||
<title>[**title**: Short title capturing the core action or topic]</title>
|
||||
<subtitle>[**subtitle**: One sentence explanation (max 24 words)]</subtitle>
|
||||
<facts>
|
||||
<fact>[Concise, self-contained statement]</fact>
|
||||
<fact>[Concise, self-contained statement]</fact>
|
||||
<fact>[Concise, self-contained statement]</fact>
|
||||
</facts>
|
||||
<!--
|
||||
**facts**: Concise, self-contained statements
|
||||
Each fact is ONE piece of information
|
||||
No pronouns - each fact must stand alone
|
||||
Include specific details: filenames, functions, values
|
||||
-->
|
||||
<narrative>[**narrative**: Full context: What was done, how it works, why it matters]</narrative>
|
||||
<concepts>
|
||||
<concept>[knowledge-type-category]</concept>
|
||||
<concept>[knowledge-type-category]</concept>
|
||||
</concepts>
|
||||
<!--
|
||||
**concepts**: 2-5 knowledge-type categories:
|
||||
- how-it-works: understanding mechanisms
|
||||
- why-it-exists: purpose or rationale
|
||||
- what-changed: modifications made
|
||||
- problem-solution: issues and their fixes
|
||||
- gotcha: traps or edge cases
|
||||
- pattern: reusable approach
|
||||
- trade-off: pros/cons of a decision
|
||||
-->
|
||||
<files_read>
|
||||
<file>[path/to/file]</file>
|
||||
<file>[path/to/file]</file>
|
||||
</files_read>
|
||||
<files_modified>
|
||||
<file>[path/to/file]</file>
|
||||
<file>[path/to/file]</file>
|
||||
</files_modified>
|
||||
<!--
|
||||
**files**: All files touched (full paths from project root)
|
||||
-->
|
||||
</observation>
|
||||
```
|
||||
|
||||
Process the following tool executions.
|
||||
|
||||
MEMORY PROCESSING SESSION START
|
||||
===============================
|
||||
@@ -1,6 +0,0 @@
|
||||
<tool_used>
|
||||
<tool_name>[tool_name]</tool_name>
|
||||
<tool_time>[time_formatted]</tool_time>
|
||||
<tool_input>[tool_input JSON]</tool_input>
|
||||
<tool_output>[tool_output JSON]</tool_output>
|
||||
</tool_used>
|
||||
@@ -0,0 +1,674 @@
|
||||
---
|
||||
title: "Troubleshooting"
|
||||
description: "Common issues and solutions for Claude-Mem"
|
||||
---
|
||||
|
||||
# Troubleshooting Guide
|
||||
|
||||
## Worker Service Issues
|
||||
|
||||
### Worker Service Not Starting
|
||||
|
||||
**Symptoms**: Worker doesn't start, or `pm2 status` shows no processes.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check if PM2 is running:
|
||||
```bash
|
||||
pm2 status
|
||||
```
|
||||
|
||||
2. Try starting manually:
|
||||
```bash
|
||||
npm run worker:start
|
||||
```
|
||||
|
||||
3. Check worker logs for errors:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
4. Full reset:
|
||||
```bash
|
||||
pm2 delete claude-mem-worker
|
||||
npm run worker:start
|
||||
```
|
||||
|
||||
5. Verify PM2 is installed:
|
||||
```bash
|
||||
which pm2
|
||||
npm list pm2
|
||||
```
|
||||
|
||||
### Port Allocation Failed
|
||||
|
||||
**Symptoms**: Worker fails to start with "port already in use" error.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check if port 37777 is in use:
|
||||
```bash
|
||||
lsof -i :37777
|
||||
```
|
||||
|
||||
2. Kill process using the port:
|
||||
```bash
|
||||
kill -9 $(lsof -t -i:37777)
|
||||
```
|
||||
|
||||
3. Or use a different port:
|
||||
```bash
|
||||
export CLAUDE_MEM_WORKER_PORT=38000
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
4. Verify new port:
|
||||
```bash
|
||||
cat ~/.claude-mem/worker.port
|
||||
```
|
||||
|
||||
### Worker Keeps Crashing
|
||||
|
||||
**Symptoms**: Worker restarts repeatedly, PM2 shows high restart count.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check error logs:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
2. Check memory usage:
|
||||
```bash
|
||||
pm2 status
|
||||
```
|
||||
|
||||
3. Increase memory limit in `ecosystem.config.cjs`:
|
||||
```javascript
|
||||
{
|
||||
max_memory_restart: '2G' // Increase if needed
|
||||
}
|
||||
```
|
||||
|
||||
4. Check database for corruption:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
|
||||
```
|
||||
|
||||
### Worker Not Processing Observations
|
||||
|
||||
**Symptoms**: Observations saved but not processed, no summaries generated.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check worker is running:
|
||||
```bash
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
2. Check worker logs:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
3. Verify database has observations:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
|
||||
```
|
||||
|
||||
4. Restart worker:
|
||||
```bash
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
## Hook Issues
|
||||
|
||||
### Hooks Not Firing
|
||||
|
||||
**Symptoms**: No context appears, observations not saved.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Verify hooks are configured:
|
||||
```bash
|
||||
cat plugin/hooks/hooks.json
|
||||
```
|
||||
|
||||
2. Test hooks manually:
|
||||
```bash
|
||||
# Test context hook
|
||||
echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plugin/scripts/context-hook.js
|
||||
```
|
||||
|
||||
3. Check hook permissions:
|
||||
```bash
|
||||
ls -la plugin/scripts/*.js
|
||||
```
|
||||
|
||||
4. Verify hooks.json is valid JSON:
|
||||
```bash
|
||||
cat plugin/hooks/hooks.json | jq .
|
||||
```
|
||||
|
||||
### Context Not Appearing
|
||||
|
||||
**Symptoms**: No session context when Claude starts.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check if summaries exist:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM session_summaries;"
|
||||
```
|
||||
|
||||
2. View recent sessions:
|
||||
```bash
|
||||
npm run test:context:verbose
|
||||
```
|
||||
|
||||
3. Check database integrity:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
|
||||
```
|
||||
|
||||
4. Manually test context hook:
|
||||
```bash
|
||||
npm run test:context
|
||||
```
|
||||
|
||||
### Hooks Timeout
|
||||
|
||||
**Symptoms**: Hook execution times out, errors in Claude Code.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Increase timeout in `plugin/hooks/hooks.json`:
|
||||
```json
|
||||
{
|
||||
"timeout": 180 // Increase from 120
|
||||
}
|
||||
```
|
||||
|
||||
2. Check worker is running (prevents timeout waiting for worker):
|
||||
```bash
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
3. Check database size (large database = slow queries):
|
||||
```bash
|
||||
ls -lh ~/.claude-mem/claude-mem.db
|
||||
```
|
||||
|
||||
4. Optimize database:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "VACUUM;"
|
||||
```
|
||||
|
||||
### Dependencies Not Installing
|
||||
|
||||
**Symptoms**: SessionStart hook fails with "module not found" errors.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Manually install dependencies:
|
||||
```bash
|
||||
cd ~/.claude/plugins/marketplaces/thedotmack
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Check npm is available:
|
||||
```bash
|
||||
which npm
|
||||
npm --version
|
||||
```
|
||||
|
||||
3. Check package.json exists:
|
||||
```bash
|
||||
ls -la ~/.claude/plugins/marketplaces/thedotmack/package.json
|
||||
```
|
||||
|
||||
## Database Issues
|
||||
|
||||
### Database Locked
|
||||
|
||||
**Symptoms**: "database is locked" errors in logs.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Close all connections:
|
||||
```bash
|
||||
pm2 stop claude-mem-worker
|
||||
```
|
||||
|
||||
2. Check for stale locks:
|
||||
```bash
|
||||
lsof ~/.claude-mem/claude-mem.db
|
||||
```
|
||||
|
||||
3. Kill processes holding locks:
|
||||
```bash
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
4. Restart worker:
|
||||
```bash
|
||||
npm run worker:start
|
||||
```
|
||||
|
||||
### Database Corruption
|
||||
|
||||
**Symptoms**: Integrity check fails, weird errors.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check database integrity:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
|
||||
```
|
||||
|
||||
2. Backup database:
|
||||
```bash
|
||||
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup
|
||||
```
|
||||
|
||||
3. Try to repair:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "VACUUM;"
|
||||
```
|
||||
|
||||
4. Nuclear option - recreate database:
|
||||
```bash
|
||||
rm ~/.claude-mem/claude-mem.db
|
||||
npm run worker:start # Will recreate schema
|
||||
```
|
||||
|
||||
### FTS5 Search Not Working
|
||||
|
||||
**Symptoms**: Search returns no results, FTS5 errors.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check FTS5 tables exist:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts';"
|
||||
```
|
||||
|
||||
2. Rebuild FTS5 tables:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts) VALUES('rebuild');
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts) VALUES('rebuild');
|
||||
"
|
||||
```
|
||||
|
||||
3. Check triggers exist:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT name FROM sqlite_master WHERE type='trigger';"
|
||||
```
|
||||
|
||||
### Database Too Large
|
||||
|
||||
**Symptoms**: Slow performance, large database file.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check database size:
|
||||
```bash
|
||||
ls -lh ~/.claude-mem/claude-mem.db
|
||||
```
|
||||
|
||||
2. Vacuum database:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "VACUUM;"
|
||||
```
|
||||
|
||||
3. Delete old sessions:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
DELETE FROM observations WHERE created_at_epoch < $(date -v-30d +%s);
|
||||
DELETE FROM session_summaries WHERE created_at_epoch < $(date -v-30d +%s);
|
||||
DELETE FROM sdk_sessions WHERE created_at_epoch < $(date -v-30d +%s);
|
||||
"
|
||||
```
|
||||
|
||||
4. Rebuild FTS5 after deletion:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts) VALUES('rebuild');
|
||||
"
|
||||
```
|
||||
|
||||
## MCP Search Issues
|
||||
|
||||
### Search Tools Not Available
|
||||
|
||||
**Symptoms**: MCP search tools not visible in Claude Code.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check MCP configuration:
|
||||
```bash
|
||||
cat plugin/.mcp.json
|
||||
```
|
||||
|
||||
2. Verify search server is built:
|
||||
```bash
|
||||
ls -l plugin/scripts/search-server.js
|
||||
```
|
||||
|
||||
3. Rebuild if needed:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. Restart Claude Code
|
||||
|
||||
### Search Returns No Results
|
||||
|
||||
**Symptoms**: Valid queries return empty results.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check database has data:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
|
||||
```
|
||||
|
||||
2. Verify FTS5 tables populated:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations_fts;"
|
||||
```
|
||||
|
||||
3. Test simple query:
|
||||
```bash
|
||||
# In Claude Code
|
||||
search_observations with query="test"
|
||||
```
|
||||
|
||||
4. Check query syntax:
|
||||
```bash
|
||||
# Bad: Special characters
|
||||
search_observations with query="[test]"
|
||||
|
||||
# Good: Simple words
|
||||
search_observations with query="test"
|
||||
```
|
||||
|
||||
### Token Limit Errors
|
||||
|
||||
**Symptoms**: "exceeded token limit" errors from MCP.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Use index format:
|
||||
```bash
|
||||
search_observations with query="..." and format="index"
|
||||
```
|
||||
|
||||
2. Reduce limit:
|
||||
```bash
|
||||
search_observations with query="..." and limit=3
|
||||
```
|
||||
|
||||
3. Use filters to narrow results:
|
||||
```bash
|
||||
search_observations with query="..." and type="decision" and limit=5
|
||||
```
|
||||
|
||||
4. Paginate results:
|
||||
```bash
|
||||
# First page
|
||||
search_observations with query="..." and limit=5 and offset=0
|
||||
|
||||
# Second page
|
||||
search_observations with query="..." and limit=5 and offset=5
|
||||
```
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Slow Context Injection
|
||||
|
||||
**Symptoms**: SessionStart hook takes too long.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Reduce context sessions:
|
||||
```typescript
|
||||
// In src/hooks/context.ts
|
||||
const CONTEXT_SESSIONS = 5; // Reduce from 10
|
||||
```
|
||||
|
||||
2. Optimize database:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
ANALYZE;
|
||||
VACUUM;
|
||||
"
|
||||
```
|
||||
|
||||
3. Add indexes (if missing):
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_project_created ON sdk_sessions(project, created_at_epoch DESC);
|
||||
"
|
||||
```
|
||||
|
||||
### Slow Search Queries
|
||||
|
||||
**Symptoms**: MCP search tools take too long.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Use more specific queries
|
||||
2. Add date range filters
|
||||
3. Add type/concept filters
|
||||
4. Reduce result limit
|
||||
5. Use index format instead of full format
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
**Symptoms**: Worker uses too much memory, frequent restarts.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check current usage:
|
||||
```bash
|
||||
pm2 status
|
||||
```
|
||||
|
||||
2. Increase memory limit:
|
||||
```javascript
|
||||
// In ecosystem.config.cjs
|
||||
{
|
||||
max_memory_restart: '2G'
|
||||
}
|
||||
```
|
||||
|
||||
3. Restart worker:
|
||||
```bash
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
4. Clean up old data (see "Database Too Large" above)
|
||||
|
||||
## Installation Issues
|
||||
|
||||
### Plugin Not Found
|
||||
|
||||
**Symptoms**: `/plugin install claude-mem` fails.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Add marketplace first:
|
||||
```bash
|
||||
/plugin marketplace add thedotmack/claude-mem
|
||||
```
|
||||
|
||||
2. Then install:
|
||||
```bash
|
||||
/plugin install claude-mem
|
||||
```
|
||||
|
||||
3. Verify installation:
|
||||
```bash
|
||||
ls -la ~/.claude/plugins/marketplaces/thedotmack/
|
||||
```
|
||||
|
||||
### Build Failures
|
||||
|
||||
**Symptoms**: `npm run build` fails.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Clean and reinstall:
|
||||
```bash
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Check Node.js version:
|
||||
```bash
|
||||
node --version # Should be >= 18.0.0
|
||||
```
|
||||
|
||||
3. Check for TypeScript errors:
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
### Missing Dependencies
|
||||
|
||||
**Symptoms**: "Cannot find module" errors.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Check package.json:
|
||||
```bash
|
||||
cat package.json
|
||||
```
|
||||
|
||||
3. Verify node_modules exists:
|
||||
```bash
|
||||
ls -la node_modules/
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Verbose Logging
|
||||
|
||||
```bash
|
||||
export DEBUG=claude-mem:*
|
||||
npm run worker:restart
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
### Check Correlation IDs
|
||||
|
||||
Trace observations through the pipeline:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
SELECT correlation_id, tool_name, created_at
|
||||
FROM observations
|
||||
WHERE session_id = 'YOUR_SESSION_ID'
|
||||
ORDER BY created_at;
|
||||
"
|
||||
```
|
||||
|
||||
### Inspect Worker State
|
||||
|
||||
```bash
|
||||
# Check if worker is running
|
||||
pm2 status
|
||||
|
||||
# View logs
|
||||
pm2 logs claude-mem-worker
|
||||
|
||||
# Check port file
|
||||
cat ~/.claude-mem/worker.port
|
||||
|
||||
# Test worker health
|
||||
curl http://localhost:37777/health
|
||||
```
|
||||
|
||||
### Database Inspection
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db
|
||||
|
||||
# View schema
|
||||
.schema
|
||||
|
||||
# Check table counts
|
||||
SELECT 'sessions', COUNT(*) FROM sdk_sessions
|
||||
UNION ALL
|
||||
SELECT 'observations', COUNT(*) FROM observations
|
||||
UNION ALL
|
||||
SELECT 'summaries', COUNT(*) FROM session_summaries
|
||||
UNION ALL
|
||||
SELECT 'prompts', COUNT(*) FROM user_prompts;
|
||||
|
||||
# View recent activity
|
||||
SELECT created_at, tool_name FROM observations ORDER BY created_at DESC LIMIT 10;
|
||||
```
|
||||
|
||||
## Common Error Messages
|
||||
|
||||
### "Worker service not responding"
|
||||
|
||||
**Cause**: Worker not running or port mismatch.
|
||||
|
||||
**Solution**: Restart worker with `npm run worker:restart`.
|
||||
|
||||
### "Database is locked"
|
||||
|
||||
**Cause**: Multiple processes accessing database.
|
||||
|
||||
**Solution**: Stop worker, kill stale processes, restart.
|
||||
|
||||
### "FTS5: syntax error"
|
||||
|
||||
**Cause**: Invalid search query syntax.
|
||||
|
||||
**Solution**: Use simpler query, avoid special characters.
|
||||
|
||||
### "SQLITE_CANTOPEN"
|
||||
|
||||
**Cause**: Database file permissions or missing directory.
|
||||
|
||||
**Solution**: Check `~/.claude-mem/` exists and is writable.
|
||||
|
||||
### "Module not found"
|
||||
|
||||
**Cause**: Missing dependencies.
|
||||
|
||||
**Solution**: Run `npm install`.
|
||||
|
||||
## Getting Help
|
||||
|
||||
If none of these solutions work:
|
||||
|
||||
1. **Check logs**:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
2. **Create issue**: [GitHub Issues](https://github.com/thedotmack/claude-mem/issues)
|
||||
- Include error messages
|
||||
- Include relevant logs
|
||||
- Include steps to reproduce
|
||||
|
||||
3. **Check existing issues**: Someone may have already solved your problem
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Configuration](configuration) - Customize Claude-Mem
|
||||
- [Development](development) - Build from source
|
||||
- [Architecture](architecture/overview) - Understand the system
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: "Getting Started"
|
||||
description: "Learn how Claude-Mem works automatically in the background"
|
||||
---
|
||||
|
||||
# Getting Started with Claude-Mem
|
||||
|
||||
## Automatic Operation
|
||||
|
||||
Claude-Mem works automatically once installed. No manual intervention required!
|
||||
|
||||
### The Full Cycle
|
||||
|
||||
1. **Start Claude Code** - Context from last 3 sessions appears automatically
|
||||
2. **Work normally** - Every tool execution is captured
|
||||
3. **Stop Claude** - Summary is generated and saved
|
||||
4. **Next session** - Previous work appears in context
|
||||
|
||||
### What Gets Captured
|
||||
|
||||
Every time Claude uses a tool, claude-mem captures it:
|
||||
|
||||
- **Read** - File reads and content access
|
||||
- **Write** - New file creation
|
||||
- **Edit** - File modifications
|
||||
- **Bash** - Command executions
|
||||
- **Glob** - File pattern searches
|
||||
- **Grep** - Content searches
|
||||
- And all other Claude Code tools
|
||||
|
||||
### What Gets Processed
|
||||
|
||||
The worker service processes tool observations and extracts:
|
||||
|
||||
- **Title** - Brief description of what happened
|
||||
- **Subtitle** - Additional context
|
||||
- **Narrative** - Detailed explanation
|
||||
- **Facts** - Key learnings as bullet points
|
||||
- **Concepts** - Relevant tags and categories
|
||||
- **Type** - Classification (decision, bugfix, feature, etc.)
|
||||
- **Files** - Which files were read or modified
|
||||
|
||||
### Session Summaries
|
||||
|
||||
When you stop Claude (or a session ends), a summary is generated with:
|
||||
|
||||
- **Request** - What you asked for
|
||||
- **Investigated** - What Claude explored
|
||||
- **Learned** - Key discoveries and insights
|
||||
- **Completed** - What was accomplished
|
||||
- **Next Steps** - What to do next
|
||||
|
||||
### Context Injection
|
||||
|
||||
When you start a new Claude Code session, the SessionStart hook:
|
||||
|
||||
1. Queries the database for recent sessions in your project
|
||||
2. Retrieves the last 10 session summaries
|
||||
3. Formats them with three-tier verbosity (most recent = most detail)
|
||||
4. Injects them into Claude's initial context
|
||||
|
||||
This means Claude "remembers" what happened in previous sessions!
|
||||
|
||||
## Manual Commands (Optional)
|
||||
|
||||
### Worker Management
|
||||
|
||||
v4.0+ auto-starts the worker on first session. Manual commands below are optional.
|
||||
|
||||
```bash
|
||||
# Start worker service (optional - auto-starts automatically)
|
||||
npm run worker:start
|
||||
|
||||
# Stop worker service
|
||||
npm run worker:stop
|
||||
|
||||
# Restart worker service
|
||||
npm run worker:restart
|
||||
|
||||
# View worker logs
|
||||
npm run worker:logs
|
||||
|
||||
# Check worker status
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Test context injection
|
||||
npm run test:context
|
||||
|
||||
# Verbose context test
|
||||
npm run test:context:verbose
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Build hooks and worker
|
||||
npm run build
|
||||
|
||||
# Build only hooks
|
||||
npm run build:hooks
|
||||
|
||||
# Publish to NPM (maintainers only)
|
||||
npm run publish:npm
|
||||
```
|
||||
|
||||
## Viewing Stored Context
|
||||
|
||||
Context is stored in SQLite database at `~/.claude-mem/claude-mem.db`.
|
||||
|
||||
Query the database directly:
|
||||
|
||||
```bash
|
||||
# Open database
|
||||
sqlite3 ~/.claude-mem/claude-mem.db
|
||||
|
||||
# View recent sessions
|
||||
SELECT session_id, project, created_at, status
|
||||
FROM sdk_sessions
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10;
|
||||
|
||||
# View session summaries
|
||||
SELECT session_id, request, completed, learned
|
||||
FROM session_summaries
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5;
|
||||
|
||||
# View observations for a session
|
||||
SELECT tool_name, created_at
|
||||
FROM observations
|
||||
WHERE session_id = 'YOUR_SESSION_ID';
|
||||
```
|
||||
|
||||
## Understanding Verbosity Levels
|
||||
|
||||
Context injection uses three-tier verbosity for efficient token usage:
|
||||
|
||||
### Tier 1 (Most Recent Session)
|
||||
- Full summary with all details
|
||||
- Request, investigated, learned, completed, next_steps, notes
|
||||
- ~500-1000 tokens
|
||||
|
||||
### Tier 2 (Sessions 2-5)
|
||||
- Medium detail
|
||||
- Request, learned, completed
|
||||
- ~200-400 tokens
|
||||
|
||||
### Tier 3 (Sessions 6-10)
|
||||
- Brief summary
|
||||
- Request and completed only
|
||||
- ~100-200 tokens
|
||||
|
||||
This ensures you get maximum detail for recent work while still having context from older sessions.
|
||||
|
||||
## Multi-Prompt Sessions
|
||||
|
||||
Claude-Mem supports sessions that span multiple user prompts:
|
||||
|
||||
- **prompt_counter**: Tracks total prompts in a session
|
||||
- **prompt_number**: Identifies specific prompt within session
|
||||
- **Session continuity**: Observations and summaries link across prompts
|
||||
|
||||
When you use `/clear`, the session doesn't end - it continues with a new prompt number. This preserves context across conversation restarts.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [MCP Search Tools](/usage/search-tools) - Learn how to search your project history
|
||||
- [Architecture Overview](/architecture/overview) - Understand how it works
|
||||
- [Troubleshooting](/troubleshooting) - Common issues and solutions
|
||||
@@ -0,0 +1,426 @@
|
||||
---
|
||||
title: "MCP Search Tools"
|
||||
description: "Query your project history with 7 specialized search tools"
|
||||
---
|
||||
|
||||
# MCP Search Tools Usage
|
||||
|
||||
Once claude-mem is installed as a plugin, 7 search tools become available in your Claude Code sessions for querying project history.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Tool | Purpose |
|
||||
|-------------------------|----------------------------------------------|
|
||||
| search_observations | Full-text search across observations |
|
||||
| search_sessions | Full-text search across session summaries |
|
||||
| search_user_prompts | Full-text search across raw user prompts |
|
||||
| find_by_concept | Find observations tagged with concepts |
|
||||
| find_by_file | Find observations referencing files |
|
||||
| find_by_type | Find observations by type |
|
||||
| get_recent_context | Get recent session context |
|
||||
|
||||
## Example Queries
|
||||
|
||||
### search_observations
|
||||
|
||||
Find all decisions about the build system:
|
||||
```
|
||||
Use search_observations to find all decisions about the build system
|
||||
```
|
||||
|
||||
Find bugs related to authentication:
|
||||
```
|
||||
search_observations with query="authentication" and type="bugfix"
|
||||
```
|
||||
|
||||
Search for refactoring work:
|
||||
```
|
||||
search_observations with query="refactor database" and type="refactor"
|
||||
```
|
||||
|
||||
### search_sessions
|
||||
|
||||
Find what we learned about hooks:
|
||||
```
|
||||
Use search_sessions to find what we learned about hooks
|
||||
```
|
||||
|
||||
Search for completed work on the API:
|
||||
```
|
||||
search_sessions with query="API implementation"
|
||||
```
|
||||
|
||||
### search_user_prompts
|
||||
|
||||
Find when user asked about authentication:
|
||||
```
|
||||
search_user_prompts with query="authentication feature"
|
||||
```
|
||||
|
||||
Trace user requests for a specific feature:
|
||||
```
|
||||
search_user_prompts with query="dark mode"
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- See exactly what the user asked for (vs what was implemented)
|
||||
- Detect patterns in repeated requests
|
||||
- Debug miscommunications between user intent and implementation
|
||||
|
||||
### find_by_file
|
||||
|
||||
Show everything related to worker-service.ts:
|
||||
```
|
||||
Use find_by_file to show me everything related to worker-service.ts
|
||||
```
|
||||
|
||||
Find all work on the database migration file:
|
||||
```
|
||||
find_by_file with filePath="migrations.ts"
|
||||
```
|
||||
|
||||
### find_by_concept
|
||||
|
||||
Show observations tagged with 'architecture':
|
||||
```
|
||||
Use find_by_concept to show observations tagged with 'architecture'
|
||||
```
|
||||
|
||||
Find all 'security' related observations:
|
||||
```
|
||||
find_by_concept with concept="security"
|
||||
```
|
||||
|
||||
### find_by_type
|
||||
|
||||
Find all feature implementations:
|
||||
```
|
||||
find_by_type with type="feature"
|
||||
```
|
||||
|
||||
Find all decisions and discoveries:
|
||||
```
|
||||
find_by_type with type=["decision", "discovery"]
|
||||
```
|
||||
|
||||
### get_recent_context
|
||||
|
||||
Get the last 5 sessions for context:
|
||||
```
|
||||
get_recent_context with limit=5
|
||||
```
|
||||
|
||||
Get recent context for debugging:
|
||||
```
|
||||
Use get_recent_context to show me what we've been working on
|
||||
```
|
||||
|
||||
## Search Strategy
|
||||
|
||||
### 1. Start with Index Format
|
||||
|
||||
**Always use index format first** to get an overview:
|
||||
|
||||
```
|
||||
search_observations with query="authentication" and format="index"
|
||||
```
|
||||
|
||||
**Why?**
|
||||
- Index format uses ~10x fewer tokens than full format
|
||||
- See titles, dates, and sources to identify relevant results
|
||||
- Avoid hitting MCP token limits
|
||||
|
||||
### 2. Review Results
|
||||
|
||||
Look at the index results to identify items of interest:
|
||||
|
||||
```
|
||||
1. [decision] Implement JWT authentication
|
||||
Date: 2025-10-21 14:23:45
|
||||
Source: claude-mem://observation/123
|
||||
|
||||
2. [feature] Add user authentication endpoints
|
||||
Date: 2025-10-21 13:15:22
|
||||
Source: claude-mem://observation/124
|
||||
|
||||
3. [bugfix] Fix authentication token expiry
|
||||
Date: 2025-10-20 16:45:30
|
||||
Source: claude-mem://observation/125
|
||||
```
|
||||
|
||||
### 3. Deep Dive with Full Format
|
||||
|
||||
Only use full format for specific items:
|
||||
|
||||
```
|
||||
search_observations with query="JWT authentication" and format="full" and limit=3
|
||||
```
|
||||
|
||||
### 4. Use Filters to Narrow Results
|
||||
|
||||
Combine filters for precise searches:
|
||||
|
||||
```
|
||||
search_observations with query="authentication" and type="decision" and dateRange={start: "2025-10-20", end: "2025-10-21"}
|
||||
```
|
||||
|
||||
## Advanced Filtering
|
||||
|
||||
### Date Ranges
|
||||
|
||||
Search within specific time periods:
|
||||
|
||||
```json
|
||||
{
|
||||
"dateRange": {
|
||||
"start": "2025-10-01",
|
||||
"end": "2025-10-31"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or use epoch timestamps:
|
||||
|
||||
```json
|
||||
{
|
||||
"dateRange": {
|
||||
"start": 1729449600,
|
||||
"end": 1732128000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Types
|
||||
|
||||
Search across multiple observation types:
|
||||
|
||||
```
|
||||
find_by_type with type=["decision", "feature", "refactor"]
|
||||
```
|
||||
|
||||
### Multiple Concepts
|
||||
|
||||
Search observations with specific concepts:
|
||||
|
||||
```
|
||||
search_observations with query="database" and concepts=["architecture", "performance"]
|
||||
```
|
||||
|
||||
### File Filtering
|
||||
|
||||
Search observations that touched specific files:
|
||||
|
||||
```
|
||||
search_observations with query="refactor" and files="worker-service.ts"
|
||||
```
|
||||
|
||||
### Project Filtering
|
||||
|
||||
Search within specific projects:
|
||||
|
||||
```
|
||||
search_observations with query="authentication" and project="my-app"
|
||||
```
|
||||
|
||||
## FTS5 Query Syntax
|
||||
|
||||
The `query` parameter supports SQLite FTS5 full-text search syntax:
|
||||
|
||||
### Simple Queries
|
||||
```
|
||||
"authentication" # Single word
|
||||
"error handling" # Multiple words (OR)
|
||||
```
|
||||
|
||||
### Boolean Operators
|
||||
```
|
||||
"error" AND "handling" # Both terms required
|
||||
"bug" OR "fix" # Either term
|
||||
"bug" NOT "feature" # First term, not second
|
||||
```
|
||||
|
||||
### Phrase Searches
|
||||
```
|
||||
"'exact phrase'" # Exact phrase match
|
||||
```
|
||||
|
||||
### Column Searches
|
||||
```
|
||||
title:"authentication" # Search specific column
|
||||
narrative:"bug fix" # Search narrative field
|
||||
```
|
||||
|
||||
## Result Metadata
|
||||
|
||||
All results include rich metadata:
|
||||
|
||||
```
|
||||
## JWT authentication decision
|
||||
|
||||
**Type**: decision
|
||||
**Date**: 2025-10-21 14:23:45
|
||||
**Concepts**: authentication, security, architecture
|
||||
**Files Read**: src/auth/middleware.ts, src/utils/jwt.ts
|
||||
**Files Modified**: src/auth/jwt-strategy.ts
|
||||
|
||||
**Narrative**:
|
||||
Decided to implement JWT-based authentication instead of session-based
|
||||
authentication for better scalability and stateless design...
|
||||
|
||||
**Facts**:
|
||||
• JWT tokens expire after 1 hour
|
||||
• Refresh tokens stored in httpOnly cookies
|
||||
• Token signing uses RS256 algorithm
|
||||
• Public keys rotated every 30 days
|
||||
```
|
||||
|
||||
## Citations
|
||||
|
||||
All search results include citations using the `claude-mem://` URI scheme:
|
||||
|
||||
- `claude-mem://observation/123` - Specific observation
|
||||
- `claude-mem://session/abc-456` - Specific session
|
||||
- `claude-mem://user-prompt/789` - Specific user prompt
|
||||
|
||||
These citations enable referencing specific historical context in your work.
|
||||
|
||||
## Token Management
|
||||
|
||||
### Token Efficiency Tips
|
||||
|
||||
1. **Start with index format**: ~50-100 tokens per result
|
||||
2. **Use small limits**: Start with 3-5 results
|
||||
3. **Apply filters**: Narrow results before searching
|
||||
4. **Paginate**: Use offset to browse results in batches
|
||||
|
||||
### Token Estimates
|
||||
|
||||
| Format | Tokens per Result |
|
||||
|--------|-------------------|
|
||||
| Index | 50-100 |
|
||||
| Full | 500-1000 |
|
||||
|
||||
**Example**:
|
||||
- 20 results in index format: ~1,000-2,000 tokens
|
||||
- 20 results in full format: ~10,000-20,000 tokens
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. Debugging Issues
|
||||
|
||||
Find what went wrong:
|
||||
```
|
||||
search_observations with query="error database connection" and type="bugfix"
|
||||
```
|
||||
|
||||
### 2. Understanding Decisions
|
||||
|
||||
Review architectural choices:
|
||||
```
|
||||
find_by_type with type="decision" and format="index"
|
||||
```
|
||||
|
||||
Then deep dive on specific decisions:
|
||||
```
|
||||
search_observations with query="[DECISION TITLE]" and format="full"
|
||||
```
|
||||
|
||||
### 3. Code Archaeology
|
||||
|
||||
Find when a file was modified:
|
||||
```
|
||||
find_by_file with filePath="worker-service.ts"
|
||||
```
|
||||
|
||||
### 4. Feature History
|
||||
|
||||
Track feature development:
|
||||
```
|
||||
search_sessions with query="authentication feature"
|
||||
search_user_prompts with query="add authentication"
|
||||
```
|
||||
|
||||
### 5. Learning from Past Work
|
||||
|
||||
Review refactoring patterns:
|
||||
```
|
||||
find_by_type with type="refactor" and limit=10
|
||||
```
|
||||
|
||||
### 6. Context Recovery
|
||||
|
||||
Restore context after time away:
|
||||
```
|
||||
get_recent_context with limit=5
|
||||
search_sessions with query="[YOUR PROJECT NAME]" and orderBy="date_desc"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Index first, full later**: Always start with index format
|
||||
2. **Small limits**: Start with 3-5 results to avoid token limits
|
||||
3. **Use filters**: Narrow results before searching
|
||||
4. **Specific queries**: More specific = better results
|
||||
5. **Review citations**: Use citations to reference past decisions
|
||||
6. **Date filtering**: Use date ranges for time-based searches
|
||||
7. **Type filtering**: Use types to categorize searches
|
||||
8. **Concept tags**: Use concepts for thematic searches
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Results Found
|
||||
|
||||
1. Check database has data:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
|
||||
```
|
||||
|
||||
2. Try broader query:
|
||||
```
|
||||
search_observations with query="authentication" # Good
|
||||
vs
|
||||
search_observations with query="'exact JWT authentication implementation'" # Too specific
|
||||
```
|
||||
|
||||
3. Remove filters:
|
||||
```
|
||||
# Start broad
|
||||
search_observations with query="auth"
|
||||
|
||||
# Then add filters
|
||||
search_observations with query="auth" and type="decision"
|
||||
```
|
||||
|
||||
### Token Limit Errors
|
||||
|
||||
1. Use index format:
|
||||
```
|
||||
search_observations with query="..." and format="index"
|
||||
```
|
||||
|
||||
2. Reduce limit:
|
||||
```
|
||||
search_observations with query="..." and limit=3
|
||||
```
|
||||
|
||||
3. Use pagination:
|
||||
```
|
||||
# First page
|
||||
search_observations with query="..." and limit=5 and offset=0
|
||||
|
||||
# Second page
|
||||
search_observations with query="..." and limit=5 and offset=5
|
||||
```
|
||||
|
||||
### Search Too Slow
|
||||
|
||||
1. Use more specific queries
|
||||
2. Add date range filters
|
||||
3. Add type/concept filters
|
||||
4. Reduce result limit
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [MCP Search Architecture](/architecture/mcp-search) - Technical details
|
||||
- [Database Schema](/architecture/database) - Understanding the data
|
||||
- [Getting Started](/usage/getting-started) - Automatic operation
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ const logDir = path.join(os.homedir(), '.claude-mem', 'logs');
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'claude-mem-worker',
|
||||
script: './dist/worker-service.cjs',
|
||||
script: './plugin/scripts/worker-service.cjs',
|
||||
interpreter: 'node',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
|
||||
Generated
+1221
-164
File diff suppressed because it is too large
Load Diff
+13
-29
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.0.2",
|
||||
"version": "4.2.8",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -16,7 +16,7 @@
|
||||
"nodejs"
|
||||
],
|
||||
"author": "Alex Newman",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thedotmack/claude-mem.git"
|
||||
@@ -25,53 +25,37 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/thedotmack/claude-mem/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/build-hooks.js",
|
||||
"build:hooks": "node scripts/build-hooks.js",
|
||||
"publish:npm": "node scripts/publish.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "node --test tests/",
|
||||
"test:parser": "npx tsx src/sdk/parser.test.ts",
|
||||
"test:context": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js 2>/dev/null",
|
||||
"test:context:verbose": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js",
|
||||
"import:xml": "tsx src/bin/import-xml-observations.ts",
|
||||
"cleanup:duplicates": "tsx src/bin/cleanup-duplicates.ts",
|
||||
"worker:start": "npx pm2 start ecosystem.config.cjs",
|
||||
"worker:stop": "npx pm2 stop claude-mem-worker",
|
||||
"worker:restart": "npx pm2 restart claude-mem-worker",
|
||||
"worker:logs": "npx pm2 logs claude-mem-worker",
|
||||
"worker:status": "npx pm2 status claude-mem-worker"
|
||||
"worker:start": "pm2 start ecosystem.config.cjs",
|
||||
"worker:stop": "pm2 stop claude-mem-worker",
|
||||
"worker:restart": "pm2 restart claude-mem-worker",
|
||||
"worker:logs": "pm2 logs claude-mem-worker"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.23",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"express": "^4.18.2",
|
||||
"glob": "^11.0.3",
|
||||
"handlebars": "^4.7.8",
|
||||
"pm2": "^5.3.0"
|
||||
"pm2": "^5.3.0",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.0.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"files": [
|
||||
"plugin",
|
||||
"dist",
|
||||
"src",
|
||||
"scripts",
|
||||
"docs",
|
||||
"ecosystem.config.cjs",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.0.2",
|
||||
"version": "4.2.8",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
},
|
||||
"repository": "https://github.com/thedotmack/claude-mem",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"license": "AGPL-3.0",
|
||||
"keywords": [
|
||||
"memory",
|
||||
"context",
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 180000
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
|
||||
"timeout": 60000
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
|
||||
"timeout": 180000
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
|
||||
"timeout": 60000
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -52,7 +52,7 @@
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
|
||||
"timeout": 60000
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
# claude-mem-settings.sh - User settings manager for claude-mem plugin
|
||||
|
||||
USER_SETTINGS_FILE="$HOME/.claude/settings.json"
|
||||
|
||||
# Function to check if jq is available
|
||||
check_jq() {
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is required for JSON manipulation"
|
||||
echo "Install with: brew install jq"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create settings file if it doesn't exist
|
||||
ensure_settings_file() {
|
||||
if [ ! -f "$USER_SETTINGS_FILE" ]; then
|
||||
mkdir -p "$(dirname "$USER_SETTINGS_FILE")"
|
||||
echo '{}' > "$USER_SETTINGS_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get current model setting
|
||||
get_model() {
|
||||
if [ -f "$USER_SETTINGS_FILE" ]; then
|
||||
jq -r '.env.CLAUDE_MEM_MODEL // "claude-sonnet-4-5"' "$USER_SETTINGS_FILE"
|
||||
else
|
||||
echo "claude-sonnet-4-5"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to set model setting
|
||||
set_model() {
|
||||
local model=$1
|
||||
|
||||
ensure_settings_file
|
||||
|
||||
# Update or create the env.CLAUDE_MEM_MODEL setting
|
||||
jq --arg model "$model" '.env.CLAUDE_MEM_MODEL = $model' "$USER_SETTINGS_FILE" > tmp.json && mv tmp.json "$USER_SETTINGS_FILE"
|
||||
echo "Set CLAUDE_MEM_MODEL to: $model"
|
||||
}
|
||||
|
||||
# Function to remove model setting
|
||||
remove_model() {
|
||||
if [ -f "$USER_SETTINGS_FILE" ]; then
|
||||
jq 'del(.env.CLAUDE_MEM_MODEL)' "$USER_SETTINGS_FILE" > tmp.json && mv tmp.json "$USER_SETTINGS_FILE"
|
||||
echo "Removed CLAUDE_MEM_MODEL (will use default: claude-sonnet-4-5)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to list available models
|
||||
list_models() {
|
||||
echo "Available models:"
|
||||
echo " claude-haiku-4-5 - Fast and efficient"
|
||||
echo " claude-sonnet-4-5 - Balanced (default)"
|
||||
echo " claude-opus-4 - Most capable"
|
||||
echo " claude-3-7-sonnet - Alternative version"
|
||||
}
|
||||
|
||||
# Interactive menu
|
||||
show_menu() {
|
||||
echo "Claude Mem Plugin - Model Configuration"
|
||||
echo "======================================"
|
||||
echo "Current model: $(get_model)"
|
||||
echo "Settings file: $USER_SETTINGS_FILE"
|
||||
echo ""
|
||||
echo "1) Set model"
|
||||
echo "2) Remove model setting (use default)"
|
||||
echo "3) List available models"
|
||||
echo "4) Exit"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main interactive loop
|
||||
main() {
|
||||
check_jq
|
||||
|
||||
while true; do
|
||||
show_menu
|
||||
read -p "Choose an option (1-4): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
list_models
|
||||
echo ""
|
||||
read -p "Enter model name: " model
|
||||
set_model "$model"
|
||||
;;
|
||||
2)
|
||||
remove_model
|
||||
;;
|
||||
3)
|
||||
list_models
|
||||
;;
|
||||
4)
|
||||
echo "Goodbye!"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Invalid option. Please choose 1-4."
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
done
|
||||
}
|
||||
|
||||
# Run main if script is executed directly
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
import x from"better-sqlite3";import{join as i,dirname as C,basename as P}from"path";import{homedir as h}from"os";import{existsSync as j,mkdirSync as D}from"fs";import{fileURLToPath as k}from"url";function y(){return typeof __dirname<"u"?__dirname:C(k(import.meta.url))}var W=y(),a=process.env.CLAUDE_MEM_DATA_DIR||i(h(),".claude-mem"),m=process.env.CLAUDE_CONFIG_DIR||i(h(),".claude"),K=i(a,"archives"),Y=i(a,"logs"),q=i(a,"trash"),V=i(a,"backups"),J=i(a,"settings.json"),I=i(a,"claude-mem.db"),Q=i(m,"settings.json"),z=i(m,"commands"),Z=i(m,"CLAUDE.md");function R(o){D(o,{recursive:!0})}var u=(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))(u||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=u[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 s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,n){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),p=u[e].padEnd(5),c=t.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let l="";n!=null&&(this.level===0&&typeof n=="object"?l=`
|
||||
`+JSON.stringify(n,null,2):l=" "+this.formatData(n));let S="";if(r){let{sessionId:U,sdkSessionId:X,correlationId:w,...b}=r;Object.keys(b).length>0&&(S=` {${Object.entries(b).map(([v,A])=>`${v}=${A}`).join(", ")}}`)}let N=`[${d}] [${p}] [${c}] ${_}${s}${S}${l}`;e===3?console.error(N):console.log(N)}debug(e,t,s,r){this.log(0,e,t,s,r)}info(e,t,s,r){this.log(1,e,t,s,r)}warn(e,t,s,r){this.log(2,e,t,s,r)}error(e,t,s,r){this.log(3,e,t,s,r)}dataIn(e,t,s,r){this.info(e,`\u2192 ${t}`,s,r)}dataOut(e,t,s,r){this.info(e,`\u2190 ${t}`,s,r)}success(e,t,s,r){this.info(e,`\u2713 ${t}`,s,r)}failure(e,t,s,r){this.error(e,`\u2717 ${t}`,s,r)}timing(e,t,s,r){this.info(e,`\u23F1 ${t}`,r,{duration:`${s}ms`})}},O=new T;var E=class{db;constructor(){R(a),this.db=new x(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,
|
||||
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(s=>s.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(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,
|
||||
@@ -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 s=this.db.pragma("table_info(observations)").find(r=>r.name==="text");if(!s||s.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(r){throw this.db.exec("ROLLBACK"),r}}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,13 +174,21 @@ ${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)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).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,
|
||||
@@ -160,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 = ?
|
||||
@@ -173,8 +218,12 @@ ${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}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
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(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, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
@@ -188,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 = ?
|
||||
@@ -200,44 +249,62 @@ ${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,s){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,t,s,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,t){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(t,e).changes===0?(O.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?(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 = ?
|
||||
`).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,s,r){let n=new Date,d=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,t,s.type,s.title,s.subtitle,JSON.stringify(s.facts),s.narrative,JSON.stringify(s.concepts),JSON.stringify(s.files_read),JSON.stringify(s.files_modified),r||null,n.toISOString(),d)}storeSummary(e,t,s,r){let n=new Date,d=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,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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request,s.investigated,s.learned,s.completed,s.next_steps,s.notes,r||null,n.toISOString(),d)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
`).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(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(t.toISOString(),s,e)}markSessionFailed(e){let t=new Date,s=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(),s,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()}};async function f(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:t}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:t});let s=new E,r=s.findActiveSDKSession(e);if(r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),s.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),r.worker_port)try{let n=await fetch(`http://127.0.0.1:${r.worker_port}/sessions/${r.id}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});n.ok?console.error("[claude-mem cleanup] Session deleted successfully via HTTP"):console.error("[claude-mem cleanup] Failed to delete session:",await n.text())}catch(n){console.error("[claude-mem cleanup] HTTP DELETE error:",n.message)}else console.error("[claude-mem cleanup] No worker port, cannot send DELETE request");try{s.markSessionFailed(r.id),console.error("[claude-mem cleanup] Session marked as failed in database")}catch(n){console.error("[claude-mem cleanup] Failed to mark session as failed:",n)}s.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 L}from"process";var g="";L.on("data",o=>g+=o);L.on("end",async()=>{try{let o=g.trim()?JSON.parse(g):void 0;await f(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)}});
|
||||
|
||||
+115
-30
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
import W from"path";import F from"better-sqlite3";import{join as p,dirname as x,basename as q}from"path";import{homedir as I}from"os";import{existsSync as z,mkdirSync as U}from"fs";import{fileURLToPath as X}from"url";function w(){return typeof __dirname<"u"?__dirname:x(X(import.meta.url))}var M=w(),u=process.env.CLAUDE_MEM_DATA_DIR||p(I(),".claude-mem"),_=process.env.CLAUDE_CONFIG_DIR||p(I(),".claude"),ee=p(u,"archives"),se=p(u,"logs"),te=p(u,"trash"),re=p(u,"backups"),ne=p(u,"settings.json"),O=p(u,"claude-mem.db"),oe=p(_,"settings.json"),ie=p(_,"commands"),ae=p(_,"CLAUDE.md");function L(a){U(a,{recursive:!0})}function v(){return p(M,"..","..")}var l=(r=>(r[r.DEBUG=0]="DEBUG",r[r.INFO=1]="INFO",r[r.WARN=2]="WARN",r[r.ERROR=3]="ERROR",r[r.SILENT=4]="SILENT",r))(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,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 s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let n=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${n})`}if(e==="Read"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}if(e==="Edit"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}if(e==="Write"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}return e}catch{return e}}log(e,t,s,n,r){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),o=l[e].padEnd(5),d=t.padEnd(6),m="";n?.correlationId?m=`[${n.correlationId}] `:n?.sessionId&&(m=`[session-${n.sessionId}] `);let c="";r!=null&&(this.level===0&&typeof r=="object"?c=`
|
||||
`+JSON.stringify(r,null,2):c=" "+this.formatData(r));let b="";if(n){let{sessionId:H,sdkSessionId:B,correlationId:G,...R}=n;Object.keys(R).length>0&&(b=` {${Object.entries(R).map(([D,y])=>`${D}=${y}`).join(", ")}}`)}let N=`[${i}] [${o}] [${d}] ${m}${s}${b}${c}`;e===3?console.error(N):console.log(N)}debug(e,t,s,n){this.log(0,e,t,s,n)}info(e,t,s,n){this.log(1,e,t,s,n)}warn(e,t,s,n){this.log(2,e,t,s,n)}error(e,t,s,n){this.log(3,e,t,s,n)}dataIn(e,t,s,n){this.info(e,`\u2192 ${t}`,s,n)}dataOut(e,t,s,n){this.info(e,`\u2190 ${t}`,s,n)}success(e,t,s,n){this.info(e,`\u2713 ${t}`,s,n)}failure(e,t,s,n){this.error(e,`\u2717 ${t}`,s,n)}timing(e,t,s,n){this.info(e,`\u23F1 ${t}`,n,{duration:`${s}ms`})}},A=new T;var E=class{db;constructor(){L(u),this.db=new F(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 C from"path";import q from"better-sqlite3";import{join as _,dirname as W,basename as z}from"path";import{homedir as U}from"os";import{existsSync as te,mkdirSync as j}from"fs";import{fileURLToPath as H}from"url";function B(){return typeof __dirname<"u"?__dirname:W(H(import.meta.url))}var Y=B(),m=process.env.CLAUDE_MEM_DATA_DIR||_(U(),".claude-mem"),O=process.env.CLAUDE_CONFIG_DIR||_(U(),".claude"),ne=_(m,"archives"),ie=_(m,"logs"),oe=_(m,"trash"),ae=_(m,"backups"),de=_(m,"settings.json"),w=_(m,"claude-mem.db"),pe=_(O,"settings.json"),ce=_(O,"commands"),_e=_(O,"CLAUDE.md");function $(p){j(p,{recursive:!0})}function M(){return _(Y,"..","..")}var L=(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))(L||{}),A=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,i){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),n=L[e].padEnd(5),c=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let a="";i!=null&&(this.level===0&&typeof i=="object"?a=`
|
||||
`+JSON.stringify(i,null,2):a=" "+this.formatData(i));let l="";if(r){let{sessionId:G,sdkSessionId:k,correlationId:R,...T}=r;Object.keys(T).length>0&&(l=` {${Object.entries(T).map(([g,b])=>`${g}=${b}`).join(", ")}}`)}let f=`[${d}] [${n}] [${c}] ${E}${t}${l}${a}`;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`})}},X=new A;var N=class{db;constructor(){$(m),this.db=new q(w),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(s=>s.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(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(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 s=this.db.pragma("table_info(observations)").find(n=>n.name==="text");if(!s||s.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,13 +174,21 @@ ${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)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).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,
|
||||
@@ -160,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 = ?
|
||||
@@ -173,8 +218,12 @@ ${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}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).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, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
@@ -188,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 = ?
|
||||
@@ -200,46 +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,s){let n=new Date,r=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,s,n.toISOString(),r).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?(A.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?(X.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,s,n){let r=new Date,i=r.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,s.type,s.title,s.subtitle,JSON.stringify(s.facts),s.narrative,JSON.stringify(s.concepts),JSON.stringify(s.files_read),JSON.stringify(s.files_modified),n||null,r.toISOString(),i)}storeSummary(e,t,s,n){let r=new Date,i=r.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,s.request,s.investigated,s.learned,s.completed,s.next_steps,s.notes,n||null,r.toISOString(),i)}markSessionCompleted(e){let t=new Date,s=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(),s,e)}markSessionFailed(e){let t=new Date,s=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(),s,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 g from"path";import{existsSync as h}from"fs";import{spawn as P}from"child_process";var $=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),j=`http://127.0.0.1:${$}/health`;async function C(){try{return(await fetch(j,{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 a=v(),e=g.join(a,"dist","worker-service.cjs");if(!h(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let t=g.join(a,"ecosystem.config.cjs"),s=g.join(a,"node_modules",".bin","pm2");if(!h(s))throw new Error(`PM2 binary not found at ${s}. This is a bundled dependency - try running: npm install`);if(!h(t))throw new Error(`PM2 ecosystem config not found at ${t}. Plugin installation may be corrupted.`);let n=P(s,["start",t],{detached:!0,stdio:"ignore",cwd:a});n.on("error",r=>{throw new Error(`Failed to spawn PM2: ${r.message}`)}),n.unref(),console.error("[claude-mem] Worker started with PM2");for(let r=0;r<3;r++)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(a){return console.error(`[claude-mem] Failed to start worker: ${a.message}`),!1}}function S(a){k();let e=a?.cwd??process.cwd(),t=e?W.basename(e):"unknown-project",s=new E;try{let n=s.getRecentSessionsWithStatus(t,3);if(n.length===0)return`# Recent Session Context
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};import v from"path";import{existsSync as y}from"fs";import{spawn as K}from"child_process";var V=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),J=`http://127.0.0.1:${V}/health`;async function F(){try{return(await fetch(J,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function P(){try{if(await F())return!0;console.error("[claude-mem] Worker not responding, starting...");let p=M(),e=v.join(p,"plugin","scripts","worker-service.cjs");if(!y(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=v.join(p,"ecosystem.config.cjs"),t=v.join(p,"node_modules",".bin","pm2");if(!y(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!y(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=K(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 F())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 D(p,e=!1,s=!1){P();let t=p?.cwd??process.cwd(),r=t?C.basename(t):"unknown-project",i=new N;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}
|
||||
|
||||
No previous sessions found for this project yet.`;let r=[];r.push("# Recent Session Context"),r.push(""),r.push(`Showing last ${n.length} session(s) for **${t}**:`),r.push("");for(let i of n)if(i.sdk_session_id){if(r.push("---"),r.push(""),i.has_summary){let o=s.getSummaryForSession(i.sdk_session_id);if(o){let d=o.prompt_number?` (Prompt #${o.prompt_number})`:"";if(r.push(`**Summary${d}**`),r.push(""),o.request&&r.push(`**Request:** ${o.request}`),o.completed&&r.push(`**Completed:** ${o.completed}`),o.learned&&r.push(`**Learned:** ${o.learned}`),o.next_steps&&r.push(`**Next Steps:** ${o.next_steps}`),o.files_read)try{let c=JSON.parse(o.files_read);Array.isArray(c)&&c.length>0&&r.push(`**Files Read:** ${c.join(", ")}`)}catch{o.files_read.trim()&&r.push(`**Files Read:** ${o.files_read}`)}if(o.files_edited)try{let c=JSON.parse(o.files_edited);Array.isArray(c)&&c.length>0&&r.push(`**Files Edited:** ${c.join(", ")}`)}catch{o.files_edited.trim()&&r.push(`**Files Edited:** ${o.files_edited}`)}let m=new Date(o.created_at).toLocaleString();r.push(`**Date:** ${m}`)}}else if(i.status==="active"){r.push("**In Progress**"),r.push(""),i.user_prompt&&r.push(`**Request:** ${i.user_prompt}`);let o=s.getObservationsForSession(i.sdk_session_id);if(o.length>0){r.push(""),r.push(`**Observations (${o.length}):**`);for(let m of o)r.push(`- ${m.title}`)}else r.push(""),r.push("*No observations yet*");r.push(""),r.push("**Status:** Active - summary pending");let d=new Date(i.started_at).toLocaleString();r.push(`**Date:** ${d}`)}else{let o=i.status==="failed"?"stopped":i.status;r.push(`**${o.charAt(0).toUpperCase()+o.slice(1)}**`),r.push(""),i.user_prompt&&r.push(`**Request:** ${i.user_prompt}`),r.push(""),r.push(`**Status:** ${o} - no summary available`);let d=new Date(i.started_at).toLocaleString();r.push(`**Date:** ${d}`)}r.push("")}return r.join(`
|
||||
`)}finally{s.close()}}import{stdin as f}from"process";try{if(f.isTTY){let e={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:S()}};console.log(JSON.stringify(e)),process.exit(0)}else{let a="";f.on("data",e=>a+=e),f.on("end",()=>{let e=a.trim()?JSON.parse(a):void 0,s={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:S(e)}};console.log(JSON.stringify(s)),process.exit(0)})}}catch(a){console.error(`[claude-mem context-hook error: ${a.message}]`),process.exit(0)}
|
||||
${o.dim}No previous summaries found for this project yet.${o.reset}
|
||||
`:`# [${r}] recent context
|
||||
|
||||
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 E=0;E<d.length;E++){let a=d[E],l=d.length-1-E,f=l===0,G=l>=1&&l<=3,k=l>3;if(c?e&&n.push(""):e?(n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push("---"),n.push("")),c=!1,k){a.request&&(e?(n.push(`${o.bright}${o.yellow}Request:${o.reset} ${a.request}`),n.push("")):(n.push(`**Request:** ${a.request}`),n.push("")));let T=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${T}${o.reset}`):(n.push(`**Date:** ${T}`),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(""))),f&&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(""))),f&&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(""))),f){let T=i.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(a.sdk_session_id),h=new Set,g=new Set,b=u=>{try{return C.isAbsolute(u)?C.relative(t,u):u}catch{return u}};for(let u of T){if(u.files_read)try{let S=JSON.parse(u.files_read);Array.isArray(S)&&S.forEach(I=>h.add(b(I)))}catch{}if(u.files_modified)try{let S=JSON.parse(u.files_modified);Array.isArray(S)&&S.forEach(I=>g.add(b(I)))}catch{}}g.forEach(u=>h.delete(u)),h.size>0&&(e?n.push(`${o.dim}Files Read: ${Array.from(h).join(", ")}${o.reset}`):n.push(`**Files Read:** ${Array.from(h).join(", ")}`)),g.size>0&&(e?n.push(`${o.dim}Files Modified: ${Array.from(g).join(", ")}${o.reset}`):n.push(`**Files Modified:** ${Array.from(g).join(", ")}`))}let R=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${R}${o.reset}`):n.push(`**Date:** ${R}`),e||n.push("")}return e&&(n.push(""),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`)),n.join(`
|
||||
`)}finally{i.close()}}import{stdin as x}from"process";try{let p=process.argv.includes("--index");if(x.isTTY){let e=D(void 0,!0,p);console.log(e),process.exit(0)}else{let e="";x.on("data",s=>e+=s),x.on("end",()=>{let s=e.trim()?JSON.parse(e):void 0,r={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:D(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)}
|
||||
|
||||
+84
-17
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import K from"path";import W from"better-sqlite3";import{join as d,dirname as P,basename as z}from"path";import{homedir as R}from"os";import{existsSync as te,mkdirSync as M}from"fs";import{fileURLToPath as F}from"url";function H(){return typeof __dirname<"u"?__dirname:P(F(import.meta.url))}var $=H(),m=process.env.CLAUDE_MEM_DATA_DIR||d(R(),".claude-mem"),T=process.env.CLAUDE_CONFIG_DIR||d(R(),".claude"),ne=d(m,"archives"),oe=d(m,"logs"),ie=d(m,"trash"),ae=d(m,"backups"),de=d(m,"settings.json"),O=d(m,"claude-mem.db"),pe=d(T,"settings.json"),ce=d(T,"commands"),ue=d(T,"CLAUDE.md");function I(o){M(o,{recursive:!0})}function L(){return d($,"..","..")}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 a=new Date().toISOString().replace("T"," ").substring(0,23),i=g[e].padEnd(5),p=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
|
||||
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let c="";if(r){let{sessionId:Y,sdkSessionId:q,correlationId:V,...h}=r;Object.keys(h).length>0&&(c=` {${Object.entries(h).map(([w,X])=>`${w}=${X}`).join(", ")}}`)}let u=`[${a}] [${i}] [${p}] ${_}${t}${c}${E}`;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`})}},v=new S;var l=class{db;constructor(){I(m),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()}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(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
|
||||
@@ -137,6 +174,14 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
@@ -173,8 +218,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).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, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
@@ -200,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,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?(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 = ?
|
||||
@@ -217,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,a=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(),a)}storeSummary(e,s,t,r){let n=new Date,a=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(),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 = ?
|
||||
@@ -239,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 A(o,e,s={}){let t=j(o,e,s);return JSON.stringify(t)}import b from"path";import{existsSync as N}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=b.join(o,"dist","worker-service.cjs");if(!N(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=b.join(o,"ecosystem.config.cjs"),t=b.join(o,"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=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(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(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),n=new l;try{let a=n.findActiveSDKSession(e),i,p=!1;if(a){i=a.id;let c=n.incrementPromptCounter(i);console.error(`[new-hook] Continuing session ${i}, prompt #${c}`)}else{let c=n.findAnySDKSession(e);if(c){i=c.id,n.reactivateSession(i,t);let u=n.incrementPromptCounter(i);p=!0,console.error(`[new-hook] Reactivated session ${i}, prompt #${u}`)}else{i=n.createSDKSession(e,r,t);let u=n.incrementPromptCounter(i);p=!0,console.error(`[new-hook] Created new session ${i}, prompt #${u}`)}}if(!await D())throw new Error("Worker service failed to start or become healthy");let E=y();if(p){let c=await fetch(`http://127.0.0.1:${E}/sessions/${i}/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(A("UserPromptSubmit",!0))}finally{n.close()}}import{stdin as U}from"process";var f="";U.on("data",o=>f+=o);U.on("end",async()=>{let o=f.trim()?JSON.parse(f):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)});
|
||||
|
||||
+81
-14
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import U from"better-sqlite3";import{join as a,dirname as k,basename as G}from"path";import{homedir as I}from"os";import{existsSync as K,mkdirSync as D}from"fs";import{fileURLToPath as y}from"url";function x(){return typeof __dirname<"u"?__dirname:k(y(import.meta.url))}var q=x(),c=process.env.CLAUDE_MEM_DATA_DIR||a(I(),".claude-mem"),T=process.env.CLAUDE_CONFIG_DIR||a(I(),".claude"),V=a(c,"archives"),J=a(c,"logs"),Q=a(c,"trash"),z=a(c,"backups"),Z=a(c,"settings.json"),f=a(c,"claude-mem.db"),ee=a(T,"settings.json"),se=a(T,"commands"),te=a(T,"CLAUDE.md");function h(n){D(n,{recursive:!0})}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||{}),g=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,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),u=S[e].padEnd(5),d=s.padEnd(6),p="";r?.correlationId?p=`[${r.correlationId}] `:r?.sessionId&&(p=`[session-${r.sessionId}] `);let _="";o!=null&&(this.level===0&&typeof o=="object"?_=`
|
||||
`+JSON.stringify(o,null,2):_=" "+this.formatData(o));let N="";if(r){let{sessionId:M,sdkSessionId:P,correlationId:F,...R}=r;Object.keys(R).length>0&&(N=` {${Object.entries(R).map(([A,C])=>`${A}=${C}`).join(", ")}}`)}let O=`[${i}] [${u}] [${d}] ${p}${t}${N}${_}`;e===3?console.error(O):console.log(O)}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 g;var m=class{db;constructor(){h(c),this.db=new U(f),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
|
||||
@@ -137,6 +174,14 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
@@ -173,8 +218,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).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, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
@@ -200,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 = ?
|
||||
@@ -217,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 o=new Date,i=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(),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)
|
||||
@@ -239,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 X(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 l(n,e,s={}){let t=X(n,e,s);return JSON.stringify(t)}var w=new Set(["ListMcpResourcesTool"]);async function L(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(w.has(s)){console.log(l("PostToolUse",!0));return}let o=new m,i=o.findActiveSDKSession(e);if(!i){o.close(),console.log(l("PostToolUse",!0));return}if(!i.worker_port)throw o.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 u=o.getPromptCounter(i.id);o.close();let d=E.formatTool(s,t);E.dataIn("HOOK",`PostToolUse: ${d}`,{sessionId:i.id,workerPort:i.worker_port});let p=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:u}),signal:AbortSignal.timeout(2e3)});if(!p.ok){let _=await p.text();throw E.failure("HOOK","Failed to send observation",{sessionId:i.id,status:p.status},_),new Error(`Failed to send observation to worker: ${p.status} ${_}`)}E.debug("HOOK","Observation sent successfully",{sessionId:i.id,toolName:s}),console.log(l("PostToolUse",!0))}import{stdin as v}from"process";var b="";v.on("data",n=>b+=n);v.on("end",async()=>{let n=b.trim()?JSON.parse(b):void 0;await L(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)});
|
||||
|
||||
+440
-46
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import U from"better-sqlite3";import{join as i,dirname as k,basename as B}from"path";import{homedir as I}from"os";import{existsSync as W,mkdirSync as D}from"fs";import{fileURLToPath as y}from"url";function x(){return typeof __dirname<"u"?__dirname:k(y(import.meta.url))}var Y=x(),d=process.env.CLAUDE_MEM_DATA_DIR||i(I(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||i(I(),".claude"),q=i(d,"archives"),V=i(d,"logs"),J=i(d,"trash"),Q=i(d,"backups"),z=i(d,"settings.json"),f=i(d,"claude-mem.db"),Z=i(l,"settings.json"),ee=i(l,"commands"),se=i(l,"CLAUDE.md");function h(o){D(o,{recursive:!0})}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||{}),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,n){if(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),E=T[e].padEnd(5),p=s.padEnd(6),m="";r?.correlationId?m=`[${r.correlationId}] `:r?.sessionId&&(m=`[session-${r.sessionId}] `);let _="";n!=null&&(this.level===0&&typeof n=="object"?_=`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let N="";if(r){let{sessionId:w,sdkSessionId:M,correlationId:F,...R}=r;Object.keys(R).length>0&&(N=` {${Object.entries(R).map(([A,C])=>`${A}=${C}`).join(", ")}}`)}let O=`[${a}] [${E}] [${p}] ${m}${t}${N}${_}`;e===3?console.error(O):console.log(O)}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`})}},c=new S;var u=class{db;constructor(){h(d),this.db=new U(f),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
|
||||
@@ -137,6 +174,14 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
@@ -173,8 +218,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).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, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
@@ -200,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,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?(c.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?(u.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 = ?
|
||||
@@ -217,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,a=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(),a)}storeSummary(e,s,t,r){let n=new Date,a=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(),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 = ?
|
||||
@@ -239,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 X(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 g(o,e,s={}){let t=X(o,e,s);return JSON.stringify(t)}async function L(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o,s=new u,t=s.findActiveSDKSession(e);if(!t){s.close(),console.log(g("Stop",!0));return}if(!t.worker_port)throw s.close(),c.error("HOOK","No worker port for session",{sessionId:t.id}),new Error("No worker port for session - session may not be properly initialized");let r=s.getPromptCounter(t.id);s.close(),c.dataIn("HOOK","Stop: Requesting summary",{sessionId:t.id,workerPort:t.worker_port,promptNumber:r});let n=await fetch(`http://127.0.0.1:${t.worker_port}/sessions/${t.id}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(!n.ok){let a=await n.text();throw c.failure("HOOK","Failed to generate summary",{sessionId:t.id,status:n.status},a),new Error(`Failed to request summary from worker: ${n.status} ${a}`)}c.debug("HOOK","Summary request sent successfully",{sessionId:t.id}),console.log(g("Stop",!0))}import{stdin as v}from"process";var b="";v.on("data",o=>b+=o);v.on("end",async()=>{let o=b.trim()?JSON.parse(b):void 0;await L(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
+11
-13
@@ -39,18 +39,14 @@ async function buildHooks() {
|
||||
const version = packageJson.version;
|
||||
console.log(`📌 Version: ${version}`);
|
||||
|
||||
// Create output directories
|
||||
console.log('\n📦 Preparing output directories...');
|
||||
// Create output directory
|
||||
console.log('\n📦 Preparing output directory...');
|
||||
const hooksDir = 'plugin/scripts';
|
||||
const distDir = 'dist';
|
||||
|
||||
if (!fs.existsSync(hooksDir)) {
|
||||
fs.mkdirSync(hooksDir, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(distDir)) {
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
}
|
||||
console.log('✓ Output directories ready');
|
||||
console.log('✓ Output directory ready');
|
||||
|
||||
// Build worker service
|
||||
console.log(`\n🔧 Building worker service...`);
|
||||
@@ -60,7 +56,7 @@ async function buildHooks() {
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
format: 'cjs',
|
||||
outfile: `${distDir}/${WORKER_SERVICE.name}.cjs`,
|
||||
outfile: `${hooksDir}/${WORKER_SERVICE.name}.cjs`,
|
||||
minify: true,
|
||||
external: ['better-sqlite3'],
|
||||
define: {
|
||||
@@ -72,8 +68,8 @@ async function buildHooks() {
|
||||
});
|
||||
|
||||
// Make worker service executable
|
||||
fs.chmodSync(`${distDir}/${WORKER_SERVICE.name}.cjs`, 0o755);
|
||||
const workerStats = fs.statSync(`${distDir}/${WORKER_SERVICE.name}.cjs`);
|
||||
fs.chmodSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`, 0o755);
|
||||
const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
|
||||
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// Build each hook
|
||||
@@ -133,9 +129,11 @@ async function buildHooks() {
|
||||
console.log(`✓ search-server built (${(searchStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
console.log('\n✅ All hooks, worker service, and search server built successfully!');
|
||||
console.log(` Hooks: ${hooksDir}/`);
|
||||
console.log(` Worker: ${distDir}/worker-service.cjs`);
|
||||
console.log(` Search: ${hooksDir}/search-server.js`);
|
||||
console.log(` Output: ${hooksDir}/`);
|
||||
console.log(` - Hooks: *-hook.js`);
|
||||
console.log(` - Worker: worker-service.cjs`);
|
||||
console.log(` - Search: search-server.js`);
|
||||
console.log('\n💡 Note: Dependencies will be auto-installed on first hook execution');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Build failed:', error.message);
|
||||
|
||||
+20
-21
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Publish script for claude-mem
|
||||
* Handles version bumping, building, and publishing to npm
|
||||
* Release script for claude-mem
|
||||
* Handles version bumping, building, and creating marketplace releases
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
@@ -21,7 +21,7 @@ const question = (query) => new Promise((resolve) => rl.question(query, resolve)
|
||||
|
||||
async function publish() {
|
||||
try {
|
||||
console.log('📦 Claude-mem Publishing Tool\n');
|
||||
console.log('📦 Claude-mem Marketplace Release Tool\n');
|
||||
|
||||
// Check git status
|
||||
console.log('🔍 Checking git status...');
|
||||
@@ -82,15 +82,19 @@ async function publish() {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Update package.json version
|
||||
console.log('\n📝 Updating package.json...');
|
||||
// Update package.json and marketplace.json versions
|
||||
console.log('\n📝 Updating package.json and marketplace.json...');
|
||||
packageJson.version = newVersion;
|
||||
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
|
||||
console.log('✓ Version updated');
|
||||
|
||||
const marketplaceJson = JSON.parse(fs.readFileSync('.claude-plugin/marketplace.json', 'utf-8'));
|
||||
marketplaceJson.plugins[0].version = newVersion;
|
||||
fs.writeFileSync('.claude-plugin/marketplace.json', JSON.stringify(marketplaceJson, null, 2) + '\n');
|
||||
console.log('✓ Versions updated in both files');
|
||||
|
||||
// Run build
|
||||
console.log('\n🔨 Building...');
|
||||
await execAsync('node build.js');
|
||||
console.log('\n🔨 Building hooks...');
|
||||
await execAsync('npm run build');
|
||||
console.log('✓ Build complete');
|
||||
|
||||
// Run tests if they exist
|
||||
@@ -112,31 +116,26 @@ async function publish() {
|
||||
|
||||
// Git commit and tag
|
||||
console.log('\n📌 Creating git commit and tag...');
|
||||
await execAsync('git add package.json dist/');
|
||||
await execAsync(`git commit -m "Release v${newVersion}
|
||||
await execAsync('git add package.json .claude-plugin/marketplace.json plugin/');
|
||||
await execAsync(`git commit -m "chore: Release v${newVersion}
|
||||
|
||||
Published from npm package build
|
||||
Source: https://github.com/thedotmack/claude-mem"`);
|
||||
Marketplace release for Claude Code plugin
|
||||
https://github.com/thedotmack/claude-mem"`);
|
||||
await execAsync(`git tag v${newVersion}`);
|
||||
console.log(`✓ Created commit and tag v${newVersion}`);
|
||||
|
||||
// Publish to npm
|
||||
console.log('\n🚀 Publishing to npm...');
|
||||
await execAsync('npm publish');
|
||||
console.log('✓ Published to npm');
|
||||
|
||||
// Push to git
|
||||
console.log('\n⬆️ Pushing to git...');
|
||||
await execAsync('git push');
|
||||
await execAsync('git push --tags');
|
||||
console.log('✓ Pushed to git');
|
||||
|
||||
console.log(`\n✅ Successfully published v${newVersion}! 🎉`);
|
||||
console.log(`\n📦 Package: https://www.npmjs.com/package/claude-mem`);
|
||||
console.log(`🏷️ Tag: https://github.com/thedotmack/claude-mem/releases/tag/v${newVersion}`);
|
||||
console.log(`\n✅ Successfully released v${newVersion}! 🎉`);
|
||||
console.log(`\n🏷️ Tag: https://github.com/thedotmack/claude-mem/releases/tag/v${newVersion}`);
|
||||
console.log(`📦 Marketplace will sync from this tag automatically`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Publish failed:', error.message);
|
||||
console.error('\n❌ Release failed:', error.message);
|
||||
if (error.stderr) {
|
||||
console.error('\nError details:', error.stderr);
|
||||
}
|
||||
|
||||
@@ -8,22 +8,21 @@ import { contextHook } from '../../hooks/context.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
try {
|
||||
// Check for --index flag
|
||||
const useIndexView = process.argv.includes('--index');
|
||||
|
||||
if (stdin.isTTY) {
|
||||
const contextOutput = contextHook();
|
||||
const result = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "SessionStart",
|
||||
additionalContext: contextOutput
|
||||
}
|
||||
};
|
||||
console.log(JSON.stringify(result));
|
||||
// Running manually from terminal - print formatted output with colors
|
||||
const contextOutput = contextHook(undefined, true, useIndexView);
|
||||
console.log(contextOutput);
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Running from hook - wrap in JSON format without colors
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
const contextOutput = contextHook(parsed);
|
||||
const contextOutput = contextHook(parsed, false, useIndexView);
|
||||
const result = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "SessionStart",
|
||||
|
||||
+14
-27
@@ -1,4 +1,5 @@
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface SessionEndInput {
|
||||
session_id: string;
|
||||
@@ -10,12 +11,12 @@ export interface SessionEndInput {
|
||||
|
||||
/**
|
||||
* Cleanup Hook - SessionEnd
|
||||
* Cleans up worker session via HTTP DELETE
|
||||
* Marks session as completed when Claude Code session ends
|
||||
*
|
||||
* This hook runs when a Claude Code session ends. It:
|
||||
* 1. Finds active SDK session for this Claude session
|
||||
* 2. Sends DELETE request to worker service
|
||||
* 3. Marks session as failed if not already completed
|
||||
* 2. Marks session as completed in database
|
||||
* 3. Allows worker to finish pending operations naturally
|
||||
*/
|
||||
export async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
||||
try {
|
||||
@@ -45,6 +46,12 @@ export async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
||||
const { session_id, reason } = input;
|
||||
console.error('[claude-mem cleanup] Searching for active SDK session', { session_id, reason });
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
console.error('[claude-mem cleanup] Worker not available - skipping HTTP cleanup');
|
||||
}
|
||||
|
||||
// Find active SDK session
|
||||
const db = new SessionStore();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
@@ -64,32 +71,12 @@ export async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
||||
worker_port: session.worker_port
|
||||
});
|
||||
|
||||
// 1. Delete session via HTTP
|
||||
if (session.worker_port) {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}`, {
|
||||
method: 'DELETE',
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.error('[claude-mem cleanup] Session deleted successfully via HTTP');
|
||||
} else {
|
||||
console.error('[claude-mem cleanup] Failed to delete session:', await response.text());
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[claude-mem cleanup] HTTP DELETE error:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.error('[claude-mem cleanup] No worker port, cannot send DELETE request');
|
||||
}
|
||||
|
||||
// 2. Mark session as failed in DB (if not already completed)
|
||||
// 1. Mark session as completed in DB (if not already completed)
|
||||
try {
|
||||
db.markSessionFailed(session.id);
|
||||
console.error('[claude-mem cleanup] Session marked as failed in database');
|
||||
db.markSessionCompleted(session.id);
|
||||
console.error('[claude-mem cleanup] Session marked as completed in database');
|
||||
} catch (markErr: any) {
|
||||
console.error('[claude-mem cleanup] Failed to mark session as failed:', markErr);
|
||||
console.error('[claude-mem cleanup] Failed to mark session as completed:', markErr);
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
+216
-103
@@ -11,14 +11,24 @@ export interface SessionStartInput {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// ANSI color codes for terminal output
|
||||
const colors = {
|
||||
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',
|
||||
};
|
||||
|
||||
/**
|
||||
* 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): string {
|
||||
// v4.0.0: Ensure worker is running before loading context
|
||||
export function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): string {
|
||||
ensureWorkerRunning();
|
||||
const cwd = input?.cwd ?? process.cwd();
|
||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
||||
@@ -26,125 +36,228 @@ export function contextHook(input?: SessionStartInput): string {
|
||||
const db = new SessionStore();
|
||||
|
||||
try {
|
||||
const sessions = db.getRecentSessionsWithStatus(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 (sessions.length === 0) {
|
||||
return '# Recent Session Context\n\nNo previous sessions found for this project yet.';
|
||||
if (summaries.length === 0) {
|
||||
if (useColors) {
|
||||
return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous summaries found for this project yet.${colors.reset}\n`;
|
||||
}
|
||||
return `# [${project}] recent context\n\nNo previous summaries found for this project yet.`;
|
||||
}
|
||||
|
||||
const output: string[] = [];
|
||||
output.push('# Recent Session Context');
|
||||
output.push('');
|
||||
output.push(`Showing last ${sessions.length} session(s) for **${project}**:`);
|
||||
output.push('');
|
||||
|
||||
for (const session of sessions) {
|
||||
if (!session.sdk_session_id) continue;
|
||||
|
||||
output.push('---');
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`);
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`# [${project}] recent context`);
|
||||
output.push('');
|
||||
}
|
||||
|
||||
// Check if session has a summary
|
||||
if (session.has_summary) {
|
||||
const summary = db.getSummaryForSession(session.sdk_session_id);
|
||||
let isFirstSummary = true;
|
||||
|
||||
if (summary) {
|
||||
const promptLabel = summary.prompt_number ? ` (Prompt #${summary.prompt_number})` : '';
|
||||
output.push(`**Summary${promptLabel}**`);
|
||||
for (let i = 0; i < summaries.length; i++) {
|
||||
const summary = summaries[i];
|
||||
|
||||
// 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('');
|
||||
|
||||
if (summary.request) {
|
||||
output.push(`**Request:** ${summary.request}`);
|
||||
}
|
||||
|
||||
if (summary.completed) {
|
||||
output.push(`**Completed:** ${summary.completed}`);
|
||||
}
|
||||
|
||||
if (summary.learned) {
|
||||
output.push(`**Learned:** ${summary.learned}`);
|
||||
}
|
||||
|
||||
if (summary.next_steps) {
|
||||
output.push(`**Next Steps:** ${summary.next_steps}`);
|
||||
}
|
||||
|
||||
if (summary.files_read) {
|
||||
try {
|
||||
const files = JSON.parse(summary.files_read);
|
||||
if (Array.isArray(files) && files.length > 0) {
|
||||
output.push(`**Files Read:** ${files.join(', ')}`);
|
||||
}
|
||||
} catch {
|
||||
if (summary.files_read.trim()) {
|
||||
output.push(`**Files Read:** ${summary.files_read}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (summary.files_edited) {
|
||||
try {
|
||||
const files = JSON.parse(summary.files_edited);
|
||||
if (Array.isArray(files) && files.length > 0) {
|
||||
output.push(`**Files Edited:** ${files.join(', ')}`);
|
||||
}
|
||||
} catch {
|
||||
if (summary.files_edited.trim()) {
|
||||
output.push(`**Files Edited:** ${summary.files_edited}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dateTime = new Date(summary.created_at).toLocaleString();
|
||||
output.push(`**Date:** ${dateTime}`);
|
||||
}
|
||||
} else if (session.status === 'active') {
|
||||
// Active session without summary - show observation titles
|
||||
output.push(`**In Progress**`);
|
||||
output.push('');
|
||||
|
||||
if (session.user_prompt) {
|
||||
output.push(`**Request:** ${session.user_prompt}`);
|
||||
}
|
||||
|
||||
const observations = db.getObservationsForSession(session.sdk_session_id);
|
||||
|
||||
if (observations.length > 0) {
|
||||
output.push('');
|
||||
output.push(`**Observations (${observations.length}):**`);
|
||||
for (const obs of observations) {
|
||||
output.push(`- ${obs.title}`);
|
||||
}
|
||||
} else {
|
||||
output.push('---');
|
||||
output.push('');
|
||||
output.push('*No observations yet*');
|
||||
}
|
||||
|
||||
output.push('');
|
||||
output.push(`**Status:** Active - summary pending`);
|
||||
const activeDateTime = new Date(session.started_at).toLocaleString();
|
||||
output.push(`**Date:** ${activeDateTime}`);
|
||||
} else {
|
||||
// Failed or completed session without summary
|
||||
const displayStatus = session.status === 'failed' ? 'stopped' : session.status;
|
||||
output.push(`**${displayStatus.charAt(0).toUpperCase() + displayStatus.slice(1)}**`);
|
||||
output.push('');
|
||||
|
||||
if (session.user_prompt) {
|
||||
output.push(`**Request:** ${session.user_prompt}`);
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
}
|
||||
|
||||
output.push('');
|
||||
output.push(`**Status:** ${displayStatus} - no summary available`);
|
||||
const failedDateTime = new Date(session.started_at).toLocaleString();
|
||||
output.push(`**Date:** ${failedDateTime}`);
|
||||
}
|
||||
|
||||
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}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Request:** ${summary.request}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 ONLY: Show Learned
|
||||
if (isTier1 && summary.learned) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.blue}Learned:${colors.reset} ${summary.learned}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Learned:** ${summary.learned}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 & 2: Show Completed
|
||||
if (summary.completed) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.green}Completed:${colors.reset} ${summary.completed}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Completed:** ${summary.completed}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// 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('');
|
||||
} else {
|
||||
output.push(`**Next Steps:** ${summary.next_steps}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}>;
|
||||
|
||||
const filesReadSet = new Set<string>();
|
||||
const filesModifiedSet = new Set<string>();
|
||||
|
||||
// Helper function to convert absolute paths to relative paths
|
||||
const toRelativePath = (filePath: string): string => {
|
||||
try {
|
||||
// Only convert if it's an absolute path
|
||||
if (path.isAbsolute(filePath)) {
|
||||
return path.relative(cwd, filePath);
|
||||
}
|
||||
return filePath;
|
||||
} catch {
|
||||
return filePath;
|
||||
}
|
||||
};
|
||||
|
||||
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(toRelativePath(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(toRelativePath(f)));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove files from filesReadSet if they're already in filesModifiedSet (avoid redundancy)
|
||||
filesModifiedSet.forEach(file => filesReadSet.delete(file));
|
||||
|
||||
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}`);
|
||||
} else {
|
||||
output.push(`**Date:** ${dateTime}`);
|
||||
}
|
||||
|
||||
if (!useColors) {
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
}
|
||||
|
||||
return output.join('\n');
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-46
@@ -21,62 +21,39 @@ export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
|
||||
const { session_id, cwd, prompt } = input;
|
||||
const project = path.basename(cwd);
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
}
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
try {
|
||||
// Check for any existing session (active, failed, or completed)
|
||||
let existing = db.findActiveSDKSession(session_id);
|
||||
let sessionDbId: number;
|
||||
let isNewSession = false;
|
||||
// Just save session_id for indexing - no validation, no state management
|
||||
const sessionDbId = db.createSDKSession(session_id, project, prompt);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
|
||||
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);
|
||||
// Save raw user prompt for full-text search
|
||||
db.saveUserPrompt(session_id, promptNumber, prompt);
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure worker service is running (v4.0.0 auto-start)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
}
|
||||
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));
|
||||
|
||||
+18
-20
@@ -1,6 +1,7 @@
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface PostToolUseInput {
|
||||
session_id: string;
|
||||
@@ -32,33 +33,30 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
}
|
||||
|
||||
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({
|
||||
@@ -73,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));
|
||||
}
|
||||
|
||||
+19
-20
@@ -1,6 +1,7 @@
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface StopInput {
|
||||
session_id: string;
|
||||
@@ -18,32 +19,30 @@ export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
}
|
||||
|
||||
const { session_id } = input;
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
}
|
||||
|
||||
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 }),
|
||||
@@ -53,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));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* Parser Regression Tests
|
||||
* Ensures v4.2.5 and v4.2.6 bugfixes remain stable
|
||||
*/
|
||||
|
||||
import { parseObservations, parseSummary } from './parser.js';
|
||||
|
||||
// ANSI color codes for output
|
||||
const GREEN = '\x1b[32m';
|
||||
const RED = '\x1b[31m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
let testsRun = 0;
|
||||
let testsPassed = 0;
|
||||
let testsFailed = 0;
|
||||
|
||||
function assert(condition: boolean, testName: string, errorMsg?: string): void {
|
||||
testsRun++;
|
||||
if (condition) {
|
||||
testsPassed++;
|
||||
console.log(`${GREEN}✓${RESET} ${testName}`);
|
||||
} else {
|
||||
testsFailed++;
|
||||
console.log(`${RED}✗${RESET} ${testName}`);
|
||||
if (errorMsg) {
|
||||
console.log(` ${RED}${errorMsg}${RESET}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertEqual<T>(actual: T, expected: T, testName: string): void {
|
||||
const isEqual = JSON.stringify(actual) === JSON.stringify(expected);
|
||||
if (!isEqual) {
|
||||
assert(false, testName, `Expected: ${JSON.stringify(expected)}, Got: ${JSON.stringify(actual)}`);
|
||||
} else {
|
||||
assert(true, testName);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + YELLOW + '='.repeat(60) + RESET);
|
||||
console.log(YELLOW + 'Parser Regression Tests (v4.2.5 & v4.2.6)' + RESET);
|
||||
console.log(YELLOW + '='.repeat(60) + RESET + '\n');
|
||||
|
||||
// ============================================================================
|
||||
// v4.2.6: Observation Parsing - NEVER Skip Observations
|
||||
// ============================================================================
|
||||
|
||||
console.log(YELLOW + '\nv4.2.6: Observation Validation Fixes' + RESET);
|
||||
console.log('─'.repeat(60) + '\n');
|
||||
|
||||
// Test 1: Observation with missing title should be saved
|
||||
const missingTitleXml = `
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<subtitle>Added new feature</subtitle>
|
||||
<narrative>Implemented the feature successfully</narrative>
|
||||
<facts>
|
||||
<fact>Created new file</fact>
|
||||
</facts>
|
||||
<concepts>
|
||||
<concept>authentication</concept>
|
||||
</concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified>
|
||||
<file>src/app.ts</file>
|
||||
</files_modified>
|
||||
</observation>
|
||||
`;
|
||||
|
||||
const missingTitleResult = parseObservations(missingTitleXml);
|
||||
assert(missingTitleResult.length === 1, 'Should parse observation with missing title');
|
||||
assert(missingTitleResult[0].title === null, 'Missing title should be null');
|
||||
assertEqual(missingTitleResult[0].type, 'feature', 'Should preserve type when title missing');
|
||||
|
||||
// Test 2: Observation with missing subtitle should be saved
|
||||
const missingSubtitleXml = `
|
||||
<observation>
|
||||
<type>bugfix</type>
|
||||
<title>Fixed critical bug</title>
|
||||
<narrative>Resolved the issue</narrative>
|
||||
<facts></facts>
|
||||
<concepts></concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
`;
|
||||
|
||||
const missingSubtitleResult = parseObservations(missingSubtitleXml);
|
||||
assert(missingSubtitleResult.length === 1, 'Should parse observation with missing subtitle');
|
||||
assert(missingSubtitleResult[0].subtitle === null, 'Missing subtitle should be null');
|
||||
assertEqual(missingSubtitleResult[0].title, 'Fixed critical bug', 'Should preserve title when subtitle missing');
|
||||
|
||||
// Test 3: Observation with missing narrative should be saved
|
||||
const missingNarrativeXml = `
|
||||
<observation>
|
||||
<type>refactor</type>
|
||||
<title>Code cleanup</title>
|
||||
<subtitle>Improved structure</subtitle>
|
||||
<facts>
|
||||
<fact>Removed dead code</fact>
|
||||
</facts>
|
||||
<concepts></concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
`;
|
||||
|
||||
const missingNarrativeResult = parseObservations(missingNarrativeXml);
|
||||
assert(missingNarrativeResult.length === 1, 'Should parse observation with missing narrative');
|
||||
assert(missingNarrativeResult[0].narrative === null, 'Missing narrative should be null');
|
||||
assertEqual(missingNarrativeResult[0].facts, ['Removed dead code'], 'Should preserve facts when narrative missing');
|
||||
|
||||
// Test 4: Observation with ALL fields missing (except type) should be saved
|
||||
const minimalObservationXml = `
|
||||
<observation>
|
||||
<type>change</type>
|
||||
<title></title>
|
||||
<subtitle></subtitle>
|
||||
<narrative></narrative>
|
||||
<facts></facts>
|
||||
<concepts></concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
`;
|
||||
|
||||
const minimalResult = parseObservations(minimalObservationXml);
|
||||
assert(minimalResult.length === 1, 'Should parse minimal observation with only type');
|
||||
assertEqual(minimalResult[0].type, 'change', 'Should preserve type for minimal observation');
|
||||
assert(minimalResult[0].title === null, 'Empty title should be null');
|
||||
assert(minimalResult[0].subtitle === null, 'Empty subtitle should be null');
|
||||
assert(minimalResult[0].narrative === null, 'Empty narrative should be null');
|
||||
|
||||
// Test 5: Observation with missing type should use "change" as fallback
|
||||
const missingTypeXml = `
|
||||
<observation>
|
||||
<title>Something happened</title>
|
||||
<subtitle>Details here</subtitle>
|
||||
<narrative>More info</narrative>
|
||||
<facts></facts>
|
||||
<concepts></concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
`;
|
||||
|
||||
const missingTypeResult = parseObservations(missingTypeXml);
|
||||
assert(missingTypeResult.length === 1, 'Should parse observation with missing type');
|
||||
assertEqual(missingTypeResult[0].type, 'change', 'Missing type should default to "change"');
|
||||
|
||||
// Test 6: Observation with invalid type should use "change" as fallback
|
||||
const invalidTypeXml = `
|
||||
<observation>
|
||||
<type>invalid_type_here</type>
|
||||
<title>Something happened</title>
|
||||
<subtitle>Details here</subtitle>
|
||||
<narrative>More info</narrative>
|
||||
<facts></facts>
|
||||
<concepts></concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
`;
|
||||
|
||||
const invalidTypeResult = parseObservations(invalidTypeXml);
|
||||
assert(invalidTypeResult.length === 1, 'Should parse observation with invalid type');
|
||||
assertEqual(invalidTypeResult[0].type, 'change', 'Invalid type should default to "change"');
|
||||
|
||||
// Test 7: Multiple observations with mixed completeness should all be saved
|
||||
const mixedObservationsXml = `
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<title>Full observation</title>
|
||||
<subtitle>Complete</subtitle>
|
||||
<narrative>All fields present</narrative>
|
||||
<facts><fact>Fact 1</fact></facts>
|
||||
<concepts><concept>concept1</concept></concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
<observation>
|
||||
<type>bugfix</type>
|
||||
<subtitle>Only subtitle and type</subtitle>
|
||||
<facts></facts>
|
||||
<concepts></concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
<observation>
|
||||
<title>Only title, no type</title>
|
||||
<facts></facts>
|
||||
<concepts></concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
`;
|
||||
|
||||
const mixedResult = parseObservations(mixedObservationsXml);
|
||||
assertEqual(mixedResult.length, 3, 'Should parse all three observations regardless of completeness');
|
||||
assertEqual(mixedResult[0].type, 'feature', 'First observation should have correct type');
|
||||
assertEqual(mixedResult[1].type, 'bugfix', 'Second observation should have correct type');
|
||||
assertEqual(mixedResult[2].type, 'change', 'Third observation should default to "change"');
|
||||
|
||||
// ============================================================================
|
||||
// v4.2.5: Summary Parsing - NEVER Skip Summaries
|
||||
// ============================================================================
|
||||
|
||||
console.log(YELLOW + '\nv4.2.5: Summary Validation Fixes' + RESET);
|
||||
console.log('─'.repeat(60) + '\n');
|
||||
|
||||
// Test 8: Summary with missing request field should be saved
|
||||
const missingRequestXml = `
|
||||
<summary>
|
||||
<investigated>Looked into the codebase</investigated>
|
||||
<learned>Found the issue</learned>
|
||||
<completed>Fixed the bug</completed>
|
||||
<next_steps>Deploy to production</next_steps>
|
||||
</summary>
|
||||
`;
|
||||
|
||||
const missingRequestResult = parseSummary(missingRequestXml);
|
||||
assert(missingRequestResult !== null, 'Should parse summary with missing request');
|
||||
assert(missingRequestResult!.request === null, 'Missing request should be null');
|
||||
assertEqual(missingRequestResult!.investigated, 'Looked into the codebase', 'Should preserve other fields');
|
||||
|
||||
// Test 9: Summary with missing investigated field should be saved
|
||||
const missingInvestigatedXml = `
|
||||
<summary>
|
||||
<request>Fix the bug</request>
|
||||
<learned>Root cause identified</learned>
|
||||
<completed>Applied the fix</completed>
|
||||
<next_steps>Monitor production</next_steps>
|
||||
</summary>
|
||||
`;
|
||||
|
||||
const missingInvestigatedResult = parseSummary(missingInvestigatedXml);
|
||||
assert(missingInvestigatedResult !== null, 'Should parse summary with missing investigated');
|
||||
assert(missingInvestigatedResult!.investigated === null, 'Missing investigated should be null');
|
||||
|
||||
// Test 10: Summary with missing learned field should be saved
|
||||
const missingLearnedXml = `
|
||||
<summary>
|
||||
<request>Add new feature</request>
|
||||
<investigated>Reviewed the requirements</investigated>
|
||||
<completed>Implemented the feature</completed>
|
||||
<next_steps>Write tests</next_steps>
|
||||
</summary>
|
||||
`;
|
||||
|
||||
const missingLearnedResult = parseSummary(missingLearnedXml);
|
||||
assert(missingLearnedResult !== null, 'Should parse summary with missing learned');
|
||||
assert(missingLearnedResult!.learned === null, 'Missing learned should be null');
|
||||
|
||||
// Test 11: Summary with missing completed field should be saved
|
||||
const missingCompletedXml = `
|
||||
<summary>
|
||||
<request>Refactor code</request>
|
||||
<investigated>Analyzed the structure</investigated>
|
||||
<learned>Found improvement opportunities</learned>
|
||||
<next_steps>Continue refactoring</next_steps>
|
||||
</summary>
|
||||
`;
|
||||
|
||||
const missingCompletedResult = parseSummary(missingCompletedXml);
|
||||
assert(missingCompletedResult !== null, 'Should parse summary with missing completed');
|
||||
assert(missingCompletedResult!.completed === null, 'Missing completed should be null');
|
||||
|
||||
// Test 12: Summary with missing next_steps field should be saved
|
||||
const missingNextStepsXml = `
|
||||
<summary>
|
||||
<request>Review code</request>
|
||||
<investigated>Examined all files</investigated>
|
||||
<learned>Code quality is good</learned>
|
||||
<completed>Review complete</completed>
|
||||
</summary>
|
||||
`;
|
||||
|
||||
const missingNextStepsResult = parseSummary(missingNextStepsXml);
|
||||
assert(missingNextStepsResult !== null, 'Should parse summary with missing next_steps');
|
||||
assert(missingNextStepsResult!.next_steps === null, 'Missing next_steps should be null');
|
||||
|
||||
// Test 13: Summary with only notes field should be saved
|
||||
const onlyNotesXml = `
|
||||
<summary>
|
||||
<notes>Some random notes</notes>
|
||||
</summary>
|
||||
`;
|
||||
|
||||
const onlyNotesResult = parseSummary(onlyNotesXml);
|
||||
assert(onlyNotesResult !== null, 'Should parse summary with only notes field');
|
||||
assertEqual(onlyNotesResult!.notes, 'Some random notes', 'Should preserve notes field');
|
||||
|
||||
// Test 14: Completely empty summary should be saved
|
||||
const emptySummaryXml = `
|
||||
<summary>
|
||||
<request></request>
|
||||
<investigated></investigated>
|
||||
<learned></learned>
|
||||
<completed></completed>
|
||||
<next_steps></next_steps>
|
||||
</summary>
|
||||
`;
|
||||
|
||||
const emptySummaryResult = parseSummary(emptySummaryXml);
|
||||
assert(emptySummaryResult !== null, 'Should parse completely empty summary');
|
||||
assert(emptySummaryResult!.request === null, 'Empty request should be null');
|
||||
assert(emptySummaryResult!.investigated === null, 'Empty investigated should be null');
|
||||
|
||||
// Test 15: Summary with skip_summary should return null (valid use case)
|
||||
const skipSummaryXml = `
|
||||
<skip_summary reason="Not enough context yet" />
|
||||
`;
|
||||
|
||||
const skipSummaryResult = parseSummary(skipSummaryXml);
|
||||
assert(skipSummaryResult === null, 'Should return null for skip_summary directive');
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases & Data Integrity
|
||||
// ============================================================================
|
||||
|
||||
console.log(YELLOW + '\nEdge Cases & Data Integrity' + RESET);
|
||||
console.log('─'.repeat(60) + '\n');
|
||||
|
||||
// Test 16: Observation with whitespace-only fields should be null
|
||||
const whitespaceObservationXml = `
|
||||
<observation>
|
||||
<type>change</type>
|
||||
<title> </title>
|
||||
<subtitle>
|
||||
|
||||
</subtitle>
|
||||
<narrative></narrative>
|
||||
<facts></facts>
|
||||
<concepts></concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
`;
|
||||
|
||||
const whitespaceResult = parseObservations(whitespaceObservationXml);
|
||||
assert(whitespaceResult.length === 1, 'Should parse observation with whitespace fields');
|
||||
assert(whitespaceResult[0].title === null || whitespaceResult[0].title!.trim() === '', 'Whitespace title should be null or empty');
|
||||
|
||||
// Test 17: Observation with concepts including type should filter out type
|
||||
const conceptsWithTypeXml = `
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<title>New feature</title>
|
||||
<subtitle>Details</subtitle>
|
||||
<narrative>Description</narrative>
|
||||
<facts></facts>
|
||||
<concepts>
|
||||
<concept>feature</concept>
|
||||
<concept>authentication</concept>
|
||||
</concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
`;
|
||||
|
||||
const conceptsWithTypeResult = parseObservations(conceptsWithTypeXml);
|
||||
assert(conceptsWithTypeResult.length === 1, 'Should parse observation with type in concepts');
|
||||
assertEqual(conceptsWithTypeResult[0].concepts, ['authentication'], 'Should filter out type from concepts');
|
||||
|
||||
// Test 18: Observation with all valid types
|
||||
const validTypes = ['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change'];
|
||||
validTypes.forEach(type => {
|
||||
const typeXml = `
|
||||
<observation>
|
||||
<type>${type}</type>
|
||||
<title>Test</title>
|
||||
<subtitle>Test</subtitle>
|
||||
<narrative>Test</narrative>
|
||||
<facts></facts>
|
||||
<concepts></concepts>
|
||||
<files_read></files_read>
|
||||
<files_modified></files_modified>
|
||||
</observation>
|
||||
`;
|
||||
const result = parseObservations(typeXml);
|
||||
assertEqual(result[0].type, type, `Should accept valid type: ${type}`);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Results Summary
|
||||
// ============================================================================
|
||||
|
||||
console.log('\n' + YELLOW + '='.repeat(60) + RESET);
|
||||
console.log(YELLOW + 'Test Results Summary' + RESET);
|
||||
console.log(YELLOW + '='.repeat(60) + RESET + '\n');
|
||||
|
||||
console.log(`Total Tests: ${testsRun}`);
|
||||
console.log(`${GREEN}Passed: ${testsPassed}${RESET}`);
|
||||
console.log(`${RED}Failed: ${testsFailed}${RESET}`);
|
||||
|
||||
if (testsFailed > 0) {
|
||||
console.log(`\n${RED}❌ TESTS FAILED${RESET}\n`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`\n${GREEN}✅ ALL TESTS PASSED${RESET}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
+63
-60
@@ -7,10 +7,10 @@ import { logger } from '../utils/logger.js';
|
||||
|
||||
export interface ParsedObservation {
|
||||
type: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
title: string | null;
|
||||
subtitle: string | null;
|
||||
facts: string[];
|
||||
narrative: string;
|
||||
narrative: string | null;
|
||||
concepts: string[];
|
||||
files_read: string[];
|
||||
files_modified: string[];
|
||||
@@ -49,32 +49,44 @@ export function parseObservations(text: string, correlationId?: string): ParsedO
|
||||
const files_read = extractArrayElements(obsContent, 'files_read', 'file');
|
||||
const files_modified = extractArrayElements(obsContent, 'files_modified', 'file');
|
||||
|
||||
// Validate required fields
|
||||
if (!type || !title || !subtitle || !narrative) {
|
||||
logger.warn('PARSER', 'Observation missing required fields, skipping', {
|
||||
correlationId,
|
||||
hasType: !!type,
|
||||
hasTitle: !!title,
|
||||
hasSubtitle: !!subtitle,
|
||||
hasNarrative: !!narrative
|
||||
});
|
||||
continue;
|
||||
// NOTE FROM THEDOTMACK: ALWAYS save observations - never skip. 10/24/2025
|
||||
// All fields except type are nullable in schema
|
||||
// If type is missing or invalid, use "change" as catch-all fallback
|
||||
|
||||
// Determine final type
|
||||
let finalType = 'change'; // Default catch-all
|
||||
if (type) {
|
||||
const validTypes = ['bugfix', 'feature', 'refactor', 'change', 'discovery', 'decision'];
|
||||
if (validTypes.includes(type.trim())) {
|
||||
finalType = type.trim();
|
||||
} else {
|
||||
logger.warn('PARSER', `Invalid observation type: ${type}, using "change"`, { correlationId });
|
||||
}
|
||||
} else {
|
||||
logger.warn('PARSER', 'Observation missing type field, using "change"', { correlationId });
|
||||
}
|
||||
|
||||
// Validate type
|
||||
const validTypes = ['change', 'discovery', 'decision'];
|
||||
if (!validTypes.includes(type.trim())) {
|
||||
logger.warn('PARSER', `Invalid observation type: ${type}, skipping`, { correlationId });
|
||||
continue;
|
||||
// All other fields are optional - save whatever we have
|
||||
|
||||
// Filter out type from concepts array (types and concepts are separate dimensions)
|
||||
const cleanedConcepts = concepts.filter(c => c !== finalType);
|
||||
|
||||
if (cleanedConcepts.length !== concepts.length) {
|
||||
logger.warn('PARSER', 'Removed observation type from concepts array', {
|
||||
correlationId,
|
||||
type: finalType,
|
||||
originalConcepts: concepts,
|
||||
cleanedConcepts
|
||||
});
|
||||
}
|
||||
|
||||
observations.push({
|
||||
type: type.trim(),
|
||||
type: finalType,
|
||||
title,
|
||||
subtitle,
|
||||
facts,
|
||||
narrative,
|
||||
concepts,
|
||||
concepts: cleanedConcepts,
|
||||
files_read,
|
||||
files_modified
|
||||
});
|
||||
@@ -85,9 +97,21 @@ export function parseObservations(text: string, correlationId?: string): ParsedO
|
||||
|
||||
/**
|
||||
* Parse summary XML block from SDK response
|
||||
* Returns null if no valid summary found
|
||||
* Returns null if no valid summary found or if summary was skipped
|
||||
*/
|
||||
export function parseSummary(text: string, sessionId?: number): ParsedSummary | null {
|
||||
// Check for skip_summary first
|
||||
const skipRegex = /<skip_summary\s+reason="([^"]+)"\s*\/>/;
|
||||
const skipMatch = skipRegex.exec(text);
|
||||
|
||||
if (skipMatch) {
|
||||
logger.info('PARSER', 'Summary skipped', {
|
||||
sessionId,
|
||||
reason: skipMatch[1]
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Match <summary>...</summary> block (non-greedy)
|
||||
const summaryRegex = /<summary>([\s\S]*?)<\/summary>/;
|
||||
const summaryMatch = summaryRegex.exec(text);
|
||||
@@ -106,18 +130,21 @@ export function parseSummary(text: string, sessionId?: number): ParsedSummary |
|
||||
const next_steps = extractField(summaryContent, 'next_steps');
|
||||
const notes = extractField(summaryContent, 'notes'); // Optional
|
||||
|
||||
// NOTE FROM THEDOTMACK: 100% of the time we must SAVE the summary, even if fields are missing. 10/24/2025
|
||||
// NEVER DO THIS NONSENSE AGAIN.
|
||||
|
||||
// Validate required fields are present (notes is optional)
|
||||
if (!request || !investigated || !learned || !completed || !next_steps) {
|
||||
logger.warn('PARSER', 'Summary missing required fields', {
|
||||
sessionId,
|
||||
hasRequest: !!request,
|
||||
hasInvestigated: !!investigated,
|
||||
hasLearned: !!learned,
|
||||
hasCompleted: !!completed,
|
||||
hasNextSteps: !!next_steps
|
||||
});
|
||||
return null;
|
||||
}
|
||||
// if (!request || !investigated || !learned || !completed || !next_steps) {
|
||||
// logger.warn('PARSER', 'Summary missing required fields', {
|
||||
// sessionId,
|
||||
// hasRequest: !!request,
|
||||
// hasInvestigated: !!investigated,
|
||||
// hasLearned: !!learned,
|
||||
// hasCompleted: !!completed,
|
||||
// hasNextSteps: !!next_steps
|
||||
// });
|
||||
// return null;
|
||||
// }
|
||||
|
||||
return {
|
||||
request,
|
||||
@@ -131,43 +158,19 @@ export function parseSummary(text: string, sessionId?: number): ParsedSummary |
|
||||
|
||||
/**
|
||||
* Extract a simple field value from XML content
|
||||
* Returns null for missing or empty/whitespace-only fields
|
||||
*/
|
||||
function extractField(content: string, fieldName: string): string | null {
|
||||
const regex = new RegExp(`<${fieldName}>([^<]*)</${fieldName}>`);
|
||||
const match = regex.exec(content);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
if (!match) return null;
|
||||
|
||||
/**
|
||||
* Extract file array from XML content
|
||||
* Handles both <file> children and empty tags
|
||||
*/
|
||||
function extractFileArray(content: string, arrayName: string): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
// Match the array block
|
||||
const arrayRegex = new RegExp(`<${arrayName}>(.*?)</${arrayName}>`, 's');
|
||||
const arrayMatch = arrayRegex.exec(content);
|
||||
|
||||
if (!arrayMatch) {
|
||||
return files;
|
||||
}
|
||||
|
||||
const arrayContent = arrayMatch[1];
|
||||
|
||||
// Extract individual <file> elements
|
||||
const fileRegex = /<file>([^<]+)<\/file>/g;
|
||||
let fileMatch;
|
||||
while ((fileMatch = fileRegex.exec(arrayContent)) !== null) {
|
||||
files.push(fileMatch[1].trim());
|
||||
}
|
||||
|
||||
return files;
|
||||
const trimmed = match[1].trim();
|
||||
return trimmed === '' ? null : trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract array of elements from XML content
|
||||
* Generic version of extractFileArray that works with any element name
|
||||
*/
|
||||
function extractArrayElements(content: string, arrayName: string, elementName: string): string[] {
|
||||
const elements: string[] = [];
|
||||
|
||||
+49
-31
@@ -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**: One of:
|
||||
- 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>
|
||||
@@ -79,7 +92,7 @@ Output observations using this XML structure:
|
||||
<concept>[knowledge-type-category]</concept>
|
||||
</concepts>
|
||||
<!--
|
||||
**concepts**: 2-5 knowledge-type categories:
|
||||
**concepts**: 2-5 knowledge-type categories. MUST use ONLY these exact keywords:
|
||||
- how-it-works: understanding mechanisms
|
||||
- why-it-exists: purpose or rationale
|
||||
- what-changed: modifications made
|
||||
@@ -87,6 +100,9 @@ Output observations using this XML structure:
|
||||
- gotcha: traps or edge cases
|
||||
- pattern: reusable approach
|
||||
- trade-off: pros/cons of a decision
|
||||
|
||||
IMPORTANT: Do NOT include the observation type (change/discovery/decision) as a concept.
|
||||
Types and concepts are separate dimensions.
|
||||
-->
|
||||
<files_read>
|
||||
<file>[path/to/file]</file>
|
||||
@@ -137,24 +153,26 @@ export function buildObservationPrompt(obs: Observation): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build finalization prompt to generate session summary
|
||||
* Build prompt to generate request summary
|
||||
*/
|
||||
export function buildFinalizePrompt(session: SDKSession): string {
|
||||
return `MEMORY PROCESSING SESSION COMPLETED
|
||||
===================================
|
||||
This session has completed. Review the observations you generated and create a session summary.
|
||||
export function buildSummaryPrompt(session: SDKSession): string {
|
||||
return `THIS REQUEST'S SUMMARY
|
||||
===============
|
||||
Think about the last request, and write a summary of what was done, what was learned, and what's next.
|
||||
|
||||
Output this XML:
|
||||
IMPORTANT! DO NOT summarize the observation process itself - you are summarizing a DIFFERENT claude code session, not this one.
|
||||
|
||||
User's Original Request: ${session.user_prompt}
|
||||
|
||||
Respond in this XML format:
|
||||
<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 session?]</completed>
|
||||
<next_steps>[What should be done next?]</next_steps>
|
||||
<notes>[Additional insights or context]</notes>
|
||||
<request>[What did the user request? Form a title that reflects the actual request: ${session.user_prompt}]</request>
|
||||
<investigated>[Was anything explored? What was it?]</investigated>
|
||||
<learned>[Did you learn anything? What was learned about how things work?]</learned>
|
||||
<completed>[Did you do any work? What shipped? What does the system now do?]</completed>
|
||||
<next_steps>[What are the next steps?]</next_steps>
|
||||
<notes>[Additional insights]</notes>
|
||||
</summary>
|
||||
|
||||
**Required fields**: request, investigated, learned, completed, next_steps
|
||||
|
||||
**Optional fields**: notes`;
|
||||
}
|
||||
IMPORTANT: This is not the end of the session. You will receive more requests to process, and more tool usages to observe and record. The summary helps keep track of progress. Always write at least a minimal summary explaining where we are at currently, even if you didn't learn anything new or complete any work.`;
|
||||
}
|
||||
+2
-2
@@ -17,7 +17,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { SDKUserMessage, SDKSystemMessage } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { getWorkerSocketPath } from '../shared/paths.js';
|
||||
import { buildInitPrompt, buildObservationPrompt, buildFinalizePrompt } from './prompts.js';
|
||||
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt } from './prompts.js';
|
||||
import { parseObservations, parseSummary } from './parser.js';
|
||||
import type { SDKSession } from './prompts.js';
|
||||
|
||||
@@ -374,7 +374,7 @@ class SDKWorker {
|
||||
this.isFinalized = true;
|
||||
const session = await this.loadSession();
|
||||
if (session) {
|
||||
const finalizePrompt = buildFinalizePrompt(session);
|
||||
const finalizePrompt = buildSummaryPrompt(session);
|
||||
console.error('[claude-mem worker] Yielding finalize prompt to SDK agent', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId,
|
||||
|
||||
+671
-298
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
|
||||
/**
|
||||
@@ -134,12 +136,19 @@ export class SessionSearch {
|
||||
|
||||
/**
|
||||
* Escape FTS5 special characters in user input
|
||||
*
|
||||
* FTS5 uses double quotes for phrase searches and treats certain characters
|
||||
* as operators (*, AND, OR, NOT, parentheses, etc.). To prevent injection,
|
||||
* we wrap user input in double quotes and escape internal quotes by doubling them.
|
||||
* This converts any user input into a safe phrase search.
|
||||
*
|
||||
* @param text - User input to escape for FTS5 MATCH queries
|
||||
* @returns Safely escaped FTS5 query string
|
||||
*/
|
||||
private escapeFTS5(text: string): string {
|
||||
// FTS5 special characters: " * ( ) AND OR NOT
|
||||
// For safety, we'll wrap the entire query in quotes for phrase search
|
||||
// or let advanced users pass boolean operators directly
|
||||
return text;
|
||||
// Escape internal double quotes by doubling them (FTS5 standard)
|
||||
// Then wrap the entire string in double quotes for phrase search
|
||||
return `"${text.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -349,43 +358,53 @@ export class SessionSearch {
|
||||
/**
|
||||
* Find observations by concept tag
|
||||
*/
|
||||
findByConcept(concept: string, filters: SearchFilters = {}): ObservationSearchResult[] {
|
||||
findByConcept(concept: string, options: SearchOptions = {}): ObservationSearchResult[] {
|
||||
const params: any[] = [];
|
||||
const { limit = 50, offset = 0, orderBy = 'date_desc', ...filters } = options;
|
||||
|
||||
// Add concept to filters
|
||||
const conceptFilters = { ...filters, concepts: concept };
|
||||
const filterClause = this.buildFilterClause(conceptFilters, params, 'o');
|
||||
const orderClause = this.buildOrderClause(orderBy, false);
|
||||
|
||||
const sql = `
|
||||
SELECT o.*
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
ORDER BY o.created_at_epoch DESC
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(limit, offset);
|
||||
|
||||
return this.db.prepare(sql).all(...params) as ObservationSearchResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find observations and summaries by file path
|
||||
*/
|
||||
findByFile(filePath: string, filters: SearchFilters = {}): {
|
||||
findByFile(filePath: string, options: SearchOptions = {}): {
|
||||
observations: ObservationSearchResult[];
|
||||
sessions: SessionSummarySearchResult[];
|
||||
} {
|
||||
const params: any[] = [];
|
||||
const { limit = 50, offset = 0, orderBy = 'date_desc', ...filters } = options;
|
||||
|
||||
// Add file to filters
|
||||
const fileFilters = { ...filters, files: filePath };
|
||||
const filterClause = this.buildFilterClause(fileFilters, params, 'o');
|
||||
const orderClause = this.buildOrderClause(orderBy, false);
|
||||
|
||||
const observationsSql = `
|
||||
SELECT o.*
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
ORDER BY o.created_at_epoch DESC
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(limit, offset);
|
||||
|
||||
const observations = this.db.prepare(observationsSql).all(...params) as ObservationSearchResult[];
|
||||
|
||||
// For session summaries, search files_read and files_edited
|
||||
@@ -425,8 +444,11 @@ export class SessionSearch {
|
||||
FROM session_summaries s
|
||||
WHERE ${baseConditions.join(' AND ')}
|
||||
ORDER BY s.created_at_epoch DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
sessionParams.push(limit, offset);
|
||||
|
||||
const sessions = this.db.prepare(sessionsSql).all(...sessionParams) as SessionSummarySearchResult[];
|
||||
|
||||
return { observations, sessions };
|
||||
@@ -437,83 +459,122 @@ export class SessionSearch {
|
||||
*/
|
||||
findByType(
|
||||
type: ObservationRow['type'] | ObservationRow['type'][],
|
||||
filters: SearchFilters = {}
|
||||
options: SearchOptions = {}
|
||||
): ObservationSearchResult[] {
|
||||
const params: any[] = [];
|
||||
const { limit = 50, offset = 0, orderBy = 'date_desc', ...filters } = options;
|
||||
|
||||
// Add type to filters
|
||||
const typeFilters = { ...filters, type };
|
||||
const filterClause = this.buildFilterClause(typeFilters, params, 'o');
|
||||
const orderClause = this.buildOrderClause(orderBy, false);
|
||||
|
||||
const sql = `
|
||||
SELECT o.*
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
ORDER BY o.created_at_epoch DESC
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(limit, offset);
|
||||
|
||||
return this.db.prepare(sql).all(...params) as ObservationSearchResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced search combining FTS5 and structured filters
|
||||
* Search user prompts with full-text search
|
||||
*/
|
||||
advancedSearch(options: {
|
||||
textQuery?: string;
|
||||
searchSessions?: boolean;
|
||||
} & SearchOptions): {
|
||||
observations: ObservationSearchResult[];
|
||||
sessions: SessionSummarySearchResult[];
|
||||
} {
|
||||
const { textQuery, searchSessions = true, ...searchOptions } = options;
|
||||
searchUserPrompts(query: string, options: SearchOptions = {}): UserPromptSearchResult[] {
|
||||
const params: any[] = [];
|
||||
const { limit = 20, offset = 0, orderBy = 'relevance', ...filters } = options;
|
||||
|
||||
let observations: ObservationSearchResult[] = [];
|
||||
let sessions: SessionSummarySearchResult[] = [];
|
||||
// Build FTS5 match query
|
||||
const ftsQuery = this.escapeFTS5(query);
|
||||
params.push(ftsQuery);
|
||||
|
||||
if (textQuery) {
|
||||
// Use FTS5 search
|
||||
observations = this.searchObservations(textQuery, searchOptions);
|
||||
if (searchSessions) {
|
||||
sessions = this.searchSessions(textQuery, searchOptions);
|
||||
// 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);
|
||||
}
|
||||
} else {
|
||||
// Pure structured query (no FTS)
|
||||
const params: any[] = [];
|
||||
const filterClause = this.buildFilterClause(searchOptions, params, 'o');
|
||||
|
||||
if (filterClause) {
|
||||
const obsSql = `
|
||||
SELECT o.*
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
${this.buildOrderClause(searchOptions.orderBy, false)}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
params.push(searchOptions.limit || 50, searchOptions.offset || 0);
|
||||
observations = this.db.prepare(obsSql).all(...params) as ObservationSearchResult[];
|
||||
}
|
||||
|
||||
if (searchSessions) {
|
||||
const sessionParams: any[] = [];
|
||||
const sessionFilters = { ...searchOptions };
|
||||
delete sessionFilters.type;
|
||||
const sessionFilterClause = this.buildFilterClause(sessionFilters, sessionParams, 's');
|
||||
|
||||
if (sessionFilterClause) {
|
||||
const sessSql = `
|
||||
SELECT s.*
|
||||
FROM session_summaries s
|
||||
WHERE ${sessionFilterClause}
|
||||
ORDER BY s.created_at_epoch DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
sessionParams.push(searchOptions.limit || 50, searchOptions.offset || 0);
|
||||
sessions = this.db.prepare(sessSql).all(...sessionParams) as SessionSummarySearchResult[];
|
||||
}
|
||||
if (end) {
|
||||
const endEpoch = typeof end === 'number' ? end : new Date(end).getTime();
|
||||
baseConditions.push('up.created_at_epoch <= ?');
|
||||
params.push(endEpoch);
|
||||
}
|
||||
}
|
||||
|
||||
return { observations, sessions };
|
||||
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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
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
|
||||
*/
|
||||
@@ -433,6 +520,31 @@ export class SessionStore {
|
||||
return stmt.all(project, limit) as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent summaries with session info for context display
|
||||
*/
|
||||
getRecentSummariesWithSessionInfo(project: string, limit: number = 3): Array<{
|
||||
sdk_session_id: string;
|
||||
request: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
next_steps: string | null;
|
||||
prompt_number: number | null;
|
||||
created_at: string;
|
||||
}> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
return stmt.all(project, limit) as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent observations for a project
|
||||
*/
|
||||
@@ -532,17 +644,71 @@ export class SessionStore {
|
||||
return stmt.get(sdkSessionId) as any || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated files from all observations for a session
|
||||
*/
|
||||
getFilesForSession(sdkSessionId: string): {
|
||||
filesRead: string[];
|
||||
filesModified: string[];
|
||||
} {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`);
|
||||
|
||||
const rows = stmt.all(sdkSessionId) as Array<{
|
||||
files_read: string | null;
|
||||
files_modified: string | null;
|
||||
}>;
|
||||
|
||||
const filesReadSet = new Set<string>();
|
||||
const filesModifiedSet = new Set<string>();
|
||||
|
||||
for (const row of rows) {
|
||||
// Parse files_read
|
||||
if (row.files_read) {
|
||||
try {
|
||||
const files = JSON.parse(row.files_read);
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach(f => filesReadSet.add(f));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
// Parse files_modified
|
||||
if (row.files_modified) {
|
||||
try {
|
||||
const files = JSON.parse(row.files_modified);
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach(f => filesModifiedSet.add(f));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filesRead: Array.from(filesReadSet),
|
||||
filesModified: Array.from(filesModifiedSet)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session by ID
|
||||
*/
|
||||
getSessionById(id: number): {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
sdk_session_id: string | null;
|
||||
project: string;
|
||||
user_prompt: string;
|
||||
} | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
@@ -628,19 +794,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;
|
||||
}
|
||||
|
||||
@@ -699,18 +878,36 @@ 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,
|
||||
project: string,
|
||||
observation: {
|
||||
type: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
title: string | null;
|
||||
subtitle: string | null;
|
||||
facts: string[];
|
||||
narrative: string;
|
||||
narrative: string | null;
|
||||
concepts: string[];
|
||||
files_read: string[];
|
||||
files_modified: string[];
|
||||
@@ -720,6 +917,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,
|
||||
@@ -746,6 +966,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,
|
||||
@@ -763,6 +984,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)
|
||||
}
|
||||
|
||||
+123
-67
@@ -7,13 +7,13 @@ import express, { Request, Response } from 'express';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { SDKUserMessage, SDKSystemMessage } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { SessionStore } from './sqlite/SessionStore.js';
|
||||
import { buildInitPrompt, buildObservationPrompt, buildFinalizePrompt } from '../sdk/prompts.js';
|
||||
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt } from '../sdk/prompts.js';
|
||||
import { parseObservations, parseSummary } from '../sdk/parser.js';
|
||||
import type { SDKSession } from '../sdk/prompts.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { ensureAllDataDirs } from '../shared/paths.js';
|
||||
|
||||
const MODEL = 'claude-sonnet-4-5';
|
||||
const MODEL = process.env.CLAUDE_MEM_MODEL || 'claude-sonnet-4-5';
|
||||
const DISALLOWED_TOOLS = ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch'];
|
||||
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
|
||||
|
||||
@@ -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,14 +119,21 @@ 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;
|
||||
}
|
||||
|
||||
const claudeSessionId = dbSession.claude_session_id;
|
||||
|
||||
// Create session state
|
||||
const session: ActiveSession = {
|
||||
sessionDbId,
|
||||
claudeSessionId,
|
||||
sdkSessionId: null,
|
||||
project,
|
||||
userPrompt,
|
||||
@@ -140,7 +148,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 +176,36 @@ 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)
|
||||
const db = new SessionStore();
|
||||
const dbSession = db.getSessionById(sessionDbId);
|
||||
db.close();
|
||||
|
||||
session = {
|
||||
sessionDbId,
|
||||
claudeSessionId: dbSession!.claude_session_id,
|
||||
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 +237,36 @@ 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)
|
||||
const db = new SessionStore();
|
||||
const dbSession = db.getSessionById(sessionDbId);
|
||||
db.close();
|
||||
|
||||
session = {
|
||||
sessionDbId,
|
||||
claudeSessionId: dbSession!.claude_session_id,
|
||||
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', {
|
||||
@@ -300,26 +359,8 @@ class WorkerService {
|
||||
});
|
||||
|
||||
for await (const message of queryResult) {
|
||||
// Handle system init message
|
||||
if (message.type === 'system' && message.subtype === 'init') {
|
||||
const systemMsg = message as SDKSystemMessage;
|
||||
if (systemMsg.session_id) {
|
||||
// Update in database first, check if it succeeded
|
||||
const db = new SessionStore();
|
||||
const updated = db.updateSDKSessionId(session.sessionDbId, systemMsg.session_id);
|
||||
db.close();
|
||||
|
||||
if (updated) {
|
||||
logger.success('SDK', 'Session initialized', {
|
||||
sessionId: session.sessionDbId,
|
||||
sdkSessionId: systemMsg.session_id
|
||||
});
|
||||
session.sdkSessionId = systemMsg.session_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle assistant messages
|
||||
else if (message.type === 'assistant') {
|
||||
if (message.type === 'assistant') {
|
||||
const content = message.message.content;
|
||||
const textContent = Array.isArray(content)
|
||||
? content.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('\n')
|
||||
@@ -367,18 +408,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',
|
||||
@@ -404,28 +446,26 @@ class WorkerService {
|
||||
session.lastPromptNumber = message.prompt_number;
|
||||
|
||||
const db = new SessionStore();
|
||||
const dbSession = db.getSessionById(session.sessionDbId) as SDKSession | undefined;
|
||||
const dbSession = db.getSessionById(session.sessionDbId) as SDKSession;
|
||||
db.close();
|
||||
|
||||
if (dbSession) {
|
||||
const summarizePrompt = buildFinalizePrompt(dbSession);
|
||||
const summarizePrompt = buildSummaryPrompt(dbSession);
|
||||
|
||||
logger.dataIn('SDK', `Summary prompt sent (${summarizePrompt.length} chars)`, {
|
||||
sessionId: session.sessionDbId,
|
||||
promptNumber: message.prompt_number
|
||||
});
|
||||
logger.debug('SDK', 'Full summary prompt', { sessionId: session.sessionDbId }, summarizePrompt);
|
||||
logger.dataIn('SDK', `Summary prompt sent (${summarizePrompt.length} chars)`, {
|
||||
sessionId: session.sessionDbId,
|
||||
promptNumber: message.prompt_number
|
||||
});
|
||||
logger.debug('SDK', 'Full summary prompt', { sessionId: session.sessionDbId }, summarizePrompt);
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: session.sdkSessionId || claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: summarizePrompt
|
||||
}
|
||||
};
|
||||
}
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: session.claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: summarizePrompt
|
||||
}
|
||||
};
|
||||
} else if (message.type === 'observation') {
|
||||
session.lastPromptNumber = message.prompt_number;
|
||||
|
||||
@@ -449,7 +489,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',
|
||||
@@ -468,6 +508,13 @@ class WorkerService {
|
||||
private handleAgentMessage(session: ActiveSession, content: string, promptNumber: number): void {
|
||||
const correlationId = logger.correlationId(session.sessionDbId, session.observationCounter);
|
||||
|
||||
// Always log what we received for debugging
|
||||
logger.info('PARSER', `Processing response (${content.length} chars)`, {
|
||||
sessionId: session.sessionDbId,
|
||||
promptNumber,
|
||||
preview: content.substring(0, 200)
|
||||
});
|
||||
|
||||
// Parse observations
|
||||
const observations = parseObservations(content, correlationId);
|
||||
|
||||
@@ -481,26 +528,35 @@ class WorkerService {
|
||||
|
||||
const db = new SessionStore();
|
||||
for (const obs of observations) {
|
||||
if (session.sdkSessionId) {
|
||||
db.storeObservation(session.sdkSessionId, session.project, obs, promptNumber);
|
||||
logger.success('DB', 'Observation stored', {
|
||||
correlationId,
|
||||
type: obs.type,
|
||||
title: obs.title
|
||||
});
|
||||
}
|
||||
db.storeObservation(session.claudeSessionId, session.project, obs, promptNumber);
|
||||
logger.success('DB', 'Observation stored', {
|
||||
correlationId,
|
||||
type: obs.type,
|
||||
title: obs.title
|
||||
});
|
||||
}
|
||||
|
||||
// Parse summary
|
||||
// Parse summary and ALWAYS store it
|
||||
logger.info('PARSER', 'Looking for summary tags...', { sessionId: session.sessionDbId });
|
||||
const summary = parseSummary(content, session.sessionDbId);
|
||||
if (summary && session.sdkSessionId) {
|
||||
logger.info('PARSER', 'Summary parsed', {
|
||||
if (summary) {
|
||||
logger.success('PARSER', 'Summary parsed successfully!', {
|
||||
sessionId: session.sessionDbId,
|
||||
promptNumber
|
||||
promptNumber,
|
||||
hasRequest: !!summary.request,
|
||||
hasInvestigated: !!summary.investigated,
|
||||
hasLearned: !!summary.learned,
|
||||
hasCompleted: !!summary.completed,
|
||||
hasNextSteps: !!summary.next_steps
|
||||
});
|
||||
db.storeSummary(session.claudeSessionId, session.project, summary, promptNumber);
|
||||
logger.success('DB', '📝 SUMMARY STORED IN DATABASE', { sessionId: session.sessionDbId, promptNumber });
|
||||
} else {
|
||||
logger.warn('PARSER', 'NO SUMMARY TAGS FOUND in response', {
|
||||
sessionId: session.sessionDbId,
|
||||
promptNumber,
|
||||
contentSample: content.substring(0, 500)
|
||||
});
|
||||
|
||||
db.storeSummary(session.sdkSessionId, session.project, summary, promptNumber);
|
||||
logger.success('DB', 'Summary stored', { sessionId: session.sessionDbId });
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function ensureWorkerRunning(): Promise<boolean> {
|
||||
|
||||
// Find worker service path
|
||||
const packageRoot = getPackageRoot();
|
||||
const workerPath = path.join(packageRoot, 'dist', 'worker-service.cjs');
|
||||
const workerPath = path.join(packageRoot, 'plugin', 'scripts', 'worker-service.cjs');
|
||||
|
||||
if (!existsSync(workerPath)) {
|
||||
console.error(`[claude-mem] Worker service not found at ${workerPath}`);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,332 @@
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import Database from 'better-sqlite3';
|
||||
import { SessionSearch } from '../src/services/sqlite/SessionSearch';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const TEST_DB_DIR = '/tmp/claude-mem-test';
|
||||
const TEST_DB_PATH = path.join(TEST_DB_DIR, 'test.db');
|
||||
|
||||
describe('SessionSearch FTS5 Injection Tests', () => {
|
||||
let search: SessionSearch;
|
||||
let db: Database.Database;
|
||||
|
||||
// Setup test database before each test
|
||||
function setupTestDB() {
|
||||
// Clean up any existing test database
|
||||
if (fs.existsSync(TEST_DB_DIR)) {
|
||||
fs.rmSync(TEST_DB_DIR, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(TEST_DB_DIR, { recursive: true });
|
||||
|
||||
// Create database with required schema
|
||||
db = new Database(TEST_DB_PATH);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// Create minimal schema needed for search tests
|
||||
// Note: Using claude_session_id to match SessionSearch expectations
|
||||
db.exec(`
|
||||
CREATE TABLE sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
started_at_epoch INTEGER DEFAULT ((unixepoch() * 1000))
|
||||
);
|
||||
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER DEFAULT 1,
|
||||
type TEXT NOT NULL,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
narrative TEXT,
|
||||
text TEXT,
|
||||
facts TEXT,
|
||||
concepts TEXT,
|
||||
files_read TEXT,
|
||||
files_modified TEXT,
|
||||
project TEXT,
|
||||
created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)),
|
||||
FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id)
|
||||
);
|
||||
|
||||
CREATE TABLE session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER DEFAULT 1,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
notes TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
project TEXT,
|
||||
created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)),
|
||||
FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id)
|
||||
);
|
||||
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER DEFAULT 1,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)),
|
||||
FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id)
|
||||
);
|
||||
|
||||
-- Create FTS5 tables manually
|
||||
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
||||
title,
|
||||
subtitle,
|
||||
narrative,
|
||||
text,
|
||||
facts,
|
||||
concepts,
|
||||
content='observations',
|
||||
content_rowid='id'
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE session_summaries_fts USING fts5(
|
||||
request,
|
||||
investigated,
|
||||
learned,
|
||||
completed,
|
||||
next_steps,
|
||||
notes,
|
||||
content='session_summaries',
|
||||
content_rowid='id'
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
|
||||
-- Create triggers for observations
|
||||
CREATE TRIGGER observations_ai AFTER INSERT ON observations BEGIN
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER observations_ad AFTER DELETE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER observations_au AFTER UPDATE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
|
||||
-- Create triggers for session_summaries
|
||||
CREATE TRIGGER session_summaries_ai AFTER INSERT ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER session_summaries_ad AFTER DELETE ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER session_summaries_au AFTER UPDATE ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
|
||||
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
|
||||
END;
|
||||
|
||||
-- Create triggers for user_prompts
|
||||
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;
|
||||
`);
|
||||
|
||||
db.close();
|
||||
|
||||
// Create SessionSearch instance
|
||||
return new SessionSearch(TEST_DB_PATH);
|
||||
}
|
||||
|
||||
function teardownTestDB() {
|
||||
if (search) {
|
||||
search.close();
|
||||
search = null;
|
||||
}
|
||||
if (fs.existsSync(TEST_DB_DIR)) {
|
||||
fs.rmSync(TEST_DB_DIR, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('should escape double quotes in search queries', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-1', 'test-project');
|
||||
INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
|
||||
VALUES ('test-session-1', 1, 'feature', 'Test observation', 'A test "quoted" narrative', 'Some text', '[]', '[]', '[]', '[]', 'test-project');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Test query with double quotes - should not cause injection
|
||||
const maliciousQuery = 'test" OR 1=1 --';
|
||||
|
||||
// This should not throw an error and should search safely
|
||||
const results = search.searchObservations(maliciousQuery);
|
||||
|
||||
// With proper escaping, this should return 0 results (no match for the literal string)
|
||||
// Without escaping, it could match everything due to OR 1=1
|
||||
assert.strictEqual(Array.isArray(results), true, 'Should return an array');
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
|
||||
test('should handle FTS5 special operators safely', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-2', 'test-project');
|
||||
INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
|
||||
VALUES ('test-session-2', 1, 'feature', 'Security feature', 'Implements security', 'Authentication system', '[]', '[]', '[]', '[]', 'test-project');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Test queries with FTS5 operators that should be escaped
|
||||
const testQueries = [
|
||||
'AND OR NOT', // Boolean operators
|
||||
'(parentheses)', // Grouping
|
||||
'asterisk*', // Wildcard
|
||||
'column:value', // Column filter attempt
|
||||
];
|
||||
|
||||
testQueries.forEach(query => {
|
||||
// Should not throw an error
|
||||
const results = search.searchObservations(query);
|
||||
assert.strictEqual(Array.isArray(results), true, `Should return array for query: ${query}`);
|
||||
});
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
|
||||
test('should find exact phrase matches when properly escaped', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-3', 'test-project');
|
||||
INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
|
||||
VALUES ('test-session-3', 1, 'feature', 'Hello world', 'This is a hello world example', 'Hello world program', '[]', '[]', '[]', '[]', 'test-project');
|
||||
INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
|
||||
VALUES ('test-session-3', 2, 'feature', 'Goodbye moon', 'This is something else', 'Different content', '[]', '[]', '[]', '[]', 'test-project');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Search for exact phrase
|
||||
const results = search.searchObservations('hello world');
|
||||
|
||||
assert.strictEqual(Array.isArray(results), true, 'Should return an array');
|
||||
assert.ok(results.length > 0, 'Should find at least one result');
|
||||
assert.ok(
|
||||
results.some(r => r.title?.toLowerCase().includes('hello') || r.narrative?.toLowerCase().includes('hello')),
|
||||
'Should find observation with "hello"'
|
||||
);
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
|
||||
test('should handle empty and special character queries safely', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-4', 'test-project');
|
||||
INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
|
||||
VALUES ('test-session-4', 1, 'feature', 'Test', 'Test observation', 'Test content', '[]', '[]', '[]', '[]', 'test-project');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Test edge cases
|
||||
const edgeCases = [
|
||||
'""', // Empty quoted string
|
||||
' ', // Whitespace only
|
||||
'!!!', // Special characters
|
||||
'@#$%', // More special characters
|
||||
];
|
||||
|
||||
edgeCases.forEach(query => {
|
||||
// Should not throw an error
|
||||
const results = search.searchObservations(query);
|
||||
assert.strictEqual(Array.isArray(results), true, `Should return array for edge case: "${query}"`);
|
||||
});
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
|
||||
test('should search session summaries safely', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-5', 'test-project');
|
||||
INSERT INTO session_summaries (claude_session_id, prompt_number, request, investigated, learned, completed, next_steps, notes, files_read, files_edited, project)
|
||||
VALUES ('test-session-5', 1, 'Implement feature', 'Looked into options', 'Learned new approach', 'Completed task', 'Next: testing', 'Notes here', '[]', '[]', 'test-project');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Test with potential injection
|
||||
const maliciousQuery = 'feature" OR type:*';
|
||||
const results = search.searchSessions(maliciousQuery);
|
||||
|
||||
assert.strictEqual(Array.isArray(results), true, 'Should return an array');
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
|
||||
test('should search user prompts safely', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-6', 'test-project');
|
||||
INSERT INTO user_prompts (claude_session_id, prompt_number, prompt_text)
|
||||
VALUES ('test-session-6', 1, 'Please implement authentication');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Test with potential injection
|
||||
const maliciousQuery = 'authentication" AND request:*';
|
||||
const results = search.searchUserPrompts(maliciousQuery);
|
||||
|
||||
assert.strictEqual(Array.isArray(results), true, 'Should return an array');
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user