Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fdfdd5d5e | |||
| e872c2da38 | |||
| 0b476e971a | |||
| f7b51a963e | |||
| b68ea38bcc | |||
| 12459eab3b | |||
| 5e6ef4aeb1 | |||
| f46b5b452f | |||
| 2af8db6b82 | |||
| 6a4fa85c10 | |||
| 22f4655a8c | |||
| 79ff1849f0 | |||
| ff28db9d76 | |||
| 268b78083e | |||
| a1f76af902 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.0.2",
|
||||
"version": "5.1.4",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: version-bump
|
||||
description: Manage semantic version updates for claude-mem project. Handles patch, minor, and major version increments following semantic versioning. Updates package.json, marketplace.json, plugin.json, and CLAUDE.md consistently. Creates git tags.
|
||||
description: Manage semantic version updates for claude-mem project. Handles patch, minor, and major version increments following semantic versioning. Updates package.json, marketplace.json, plugin.json, and CLAUDE.md version number (NOT version history). Creates git tags.
|
||||
---
|
||||
|
||||
# Version Bump Skill
|
||||
@@ -13,7 +13,7 @@ IMPORTANT: This skill manages semantic versioning across the claude-mem project.
|
||||
1. `package.json` (line 3)
|
||||
2. `.claude-plugin/marketplace.json` (line 13)
|
||||
3. `plugin/.claude-plugin/plugin.json` (line 3)
|
||||
4. `CLAUDE.md` (version history section)
|
||||
4. `CLAUDE.md` (line 9 ONLY - version number, NOT version history)
|
||||
|
||||
**Semantic versioning:**
|
||||
- PATCH (x.y.Z): Bugfixes only
|
||||
@@ -61,7 +61,7 @@ Files to update:
|
||||
- package.json: "version": "4.2.9"
|
||||
- marketplace.json: "version": "4.2.9"
|
||||
- plugin.json: "version": "4.2.9"
|
||||
- CLAUDE.md: Add v4.2.9 entry
|
||||
- CLAUDE.md line 9: "**Current Version**: 4.2.9" (version number ONLY)
|
||||
- Git tag: v4.2.9
|
||||
|
||||
Proceed? (yes/no)
|
||||
@@ -97,7 +97,12 @@ Proceed? (yes/no)
|
||||
```
|
||||
|
||||
**Update CLAUDE.md:**
|
||||
Add entry at top of Version History section following the template below.
|
||||
ONLY update line 9 with the version number:
|
||||
```markdown
|
||||
**Current Version**: 4.2.9
|
||||
```
|
||||
|
||||
**CRITICAL**: DO NOT add version history entries to CLAUDE.md. Version history is managed separately outside this skill.
|
||||
|
||||
### 6. Verify Consistency
|
||||
```bash
|
||||
@@ -110,7 +115,6 @@ grep -n '"version"' package.json .claude-plugin/marketplace.json plugin/.claude-
|
||||
```bash
|
||||
# Verify the plugin loads correctly
|
||||
npm run build
|
||||
# Or whatever build command is appropriate
|
||||
```
|
||||
|
||||
### 8. Commit and Tag
|
||||
@@ -131,8 +135,7 @@ git push && git push --tags
|
||||
### 9. Create GitHub Release
|
||||
```bash
|
||||
# Create GitHub release from the tag
|
||||
# Extract release notes from CLAUDE.md for the current version
|
||||
gh release create vX.Y.Z --title "vX.Y.Z" --notes "[Paste relevant section from CLAUDE.md]"
|
||||
gh release create vX.Y.Z --title "vX.Y.Z" --notes "[Brief release notes]"
|
||||
|
||||
# Or generate notes automatically from commits
|
||||
gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes
|
||||
@@ -140,61 +143,6 @@ gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes
|
||||
|
||||
**IMPORTANT**: Always create the GitHub release immediately after pushing the tag. This makes the release discoverable to users and triggers any automated workflows.
|
||||
|
||||
## CLAUDE.md Templates
|
||||
|
||||
### PATCH Version Template
|
||||
```markdown
|
||||
### v4.2.9
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Fixes**:
|
||||
- [Specific bug fixed with file reference: src/db/query.ts:45]
|
||||
- [Impact: what this fixes for users]
|
||||
|
||||
**Technical Details**:
|
||||
- Modified: [file paths with line numbers]
|
||||
- Root cause: [brief explanation]
|
||||
```
|
||||
|
||||
### MINOR Version Template
|
||||
```markdown
|
||||
### v4.3.0
|
||||
**Breaking Changes**: None (minor version)
|
||||
|
||||
**Features**:
|
||||
- [Feature name and user benefit]
|
||||
- [How to use: command or API example]
|
||||
|
||||
**Improvements**:
|
||||
- [Enhancement description]
|
||||
|
||||
**Technical Details**:
|
||||
- New files: [paths]
|
||||
- Modified: [paths with line numbers]
|
||||
- Dependencies: [any new dependencies added]
|
||||
```
|
||||
|
||||
### MAJOR Version Template
|
||||
```markdown
|
||||
### v5.0.0
|
||||
**Breaking Changes**:
|
||||
⚠️ [Change 1: what breaks and why]
|
||||
⚠️ [Change 2: what breaks and why]
|
||||
|
||||
**Migration Guide**:
|
||||
1. [Step-by-step instructions]
|
||||
2. [Code examples showing old vs new]
|
||||
3. [Data migration commands if needed]
|
||||
|
||||
**Features**:
|
||||
- [New capabilities enabled by breaking changes]
|
||||
|
||||
**Technical Details**:
|
||||
- Architectural changes: [high-level overview]
|
||||
- Modified: [key files with line numbers]
|
||||
- Removed: [deprecated APIs or features]
|
||||
```
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
**Scenario 1: Bug fix after testing**
|
||||
@@ -202,12 +150,11 @@ gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes
|
||||
User: "Fixed the memory leak in the search function"
|
||||
You: Determine → PATCH
|
||||
Calculate → 4.2.8 → 4.2.9
|
||||
Update all four files
|
||||
Update all four files (version numbers only)
|
||||
Build and commit
|
||||
Create git tag v4.2.9
|
||||
Push commit and tags
|
||||
Create GitHub release v4.2.9
|
||||
CLAUDE.md: Focus on the fix and impact
|
||||
```
|
||||
|
||||
**Scenario 2: New MCP tool added**
|
||||
@@ -215,12 +162,11 @@ You: Determine → PATCH
|
||||
User: "Added web search MCP integration"
|
||||
You: Determine → MINOR (new feature)
|
||||
Calculate → 4.2.8 → 4.3.0
|
||||
Update all four files
|
||||
Update all four files (version numbers only)
|
||||
Build and commit
|
||||
Create git tag v4.3.0
|
||||
Push commit and tags
|
||||
Create GitHub release v4.3.0
|
||||
CLAUDE.md: Describe feature and usage
|
||||
```
|
||||
|
||||
**Scenario 3: Database schema redesign**
|
||||
@@ -228,12 +174,11 @@ You: Determine → MINOR (new feature)
|
||||
User: "Rewrote storage layer, old data needs migration"
|
||||
You: Determine → MAJOR (breaking change)
|
||||
Calculate → 4.2.8 → 5.0.0
|
||||
Update all four files
|
||||
Update all four files (version numbers only)
|
||||
Build and commit
|
||||
Create git tag v5.0.0
|
||||
Push commit and tags
|
||||
Create GitHub release v5.0.0
|
||||
CLAUDE.md: Include migration steps
|
||||
```
|
||||
|
||||
## Error Prevention
|
||||
@@ -242,10 +187,7 @@ You: Determine → MAJOR (breaking change)
|
||||
- [ ] All FOUR files have matching version numbers (package.json, marketplace.json, plugin.json, CLAUDE.md)
|
||||
- [ ] Git tag created with format vX.Y.Z
|
||||
- [ ] GitHub release created from the tag
|
||||
- [ ] CLAUDE.md entry matches version type (patch/minor/major)
|
||||
- [ ] Breaking changes are clearly marked with ⚠️
|
||||
- [ ] File references use format: `path/to/file.ts:line_number`
|
||||
- [ ] CLAUDE.md entry is added at TOP of version history
|
||||
- [ ] CLAUDE.md: ONLY updated line 9 (version number), did NOT touch version history
|
||||
- [ ] Commit and tags pushed to remote
|
||||
|
||||
**NEVER:**
|
||||
@@ -254,15 +196,7 @@ You: Determine → MAJOR (breaking change)
|
||||
- Forget to create git tag
|
||||
- Forget to create GitHub release
|
||||
- Forget to ask user if version type is unclear
|
||||
- Use vague descriptions in CLAUDE.md
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Be explicit about breaking changes** - Users need clear migration paths[(2)](https://docs.claude.com/en/docs/claude-code/plugins-reference#plugin-manifest-schema)
|
||||
2. **Include file references** - Makes debugging easier later[(1)](https://www.anthropic.com/engineering/claude-code-best-practices)
|
||||
3. **Test after bumping** - Ensure version displays correctly[(3)](https://www.anthropic.com/engineering/claude-code-best-practices)
|
||||
4. **Keep CLAUDE.md concise** - Focus on user impact, not implementation minutiae[(1)](https://www.anthropic.com/engineering/claude-code-best-practices)
|
||||
5. **Use consistent formatting** - Follow existing CLAUDE.md style[(1)](https://www.anthropic.com/engineering/claude-code-best-practices)
|
||||
- Add version history entries to CLAUDE.md (that's managed separately)
|
||||
|
||||
## Reference Commands
|
||||
|
||||
@@ -270,12 +204,9 @@ You: Determine → MAJOR (breaking change)
|
||||
# View current version
|
||||
cat package.json | grep version
|
||||
|
||||
# Check version history
|
||||
head -50 CLAUDE.md | grep "^###"
|
||||
|
||||
# Verify consistency across all version files
|
||||
grep '"version"' package.json .claude-plugin/marketplace.json plugin/.claude-plugin/plugin.json
|
||||
|
||||
# View git tags
|
||||
git tag -l -n1
|
||||
```
|
||||
```
|
||||
|
||||
+5
-1
@@ -5,8 +5,12 @@ node_modules/
|
||||
.env.local
|
||||
*.tmp
|
||||
*.temp
|
||||
.install-version
|
||||
.claude/settings.local.json
|
||||
plugin/data/
|
||||
plugin/data.backup/
|
||||
package-lock.json
|
||||
private/
|
||||
private/
|
||||
|
||||
# Generated UI files (built from viewer-template.html)
|
||||
src/ui/viewer.html
|
||||
+188
@@ -8,6 +8,194 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [5.1.2] - 2025-11-06
|
||||
|
||||
### Added
|
||||
- **Theme Toggle**: Light/dark mode support in viewer UI
|
||||
- User-selectable theme with persistent settings
|
||||
- Automatic system preference detection
|
||||
- Smooth transitions between themes
|
||||
- Updated viewer UI with theme toggle controls in header
|
||||
|
||||
### Changed
|
||||
- Version bumped from 5.1.1 to 5.1.2 across all metadata files
|
||||
- Rebuilt all plugin scripts with theme functionality
|
||||
|
||||
|
||||
## [5.1.1] - 2025-11-06
|
||||
|
||||
### Fixed
|
||||
- **PM2 ENOENT error on Windows**: Fixed PM2 process spawning by using full path to PM2 binary
|
||||
- Improved cross-platform compatibility for PM2 process management
|
||||
- Updated scripts/smart-install.js to use full PM2 binary path
|
||||
|
||||
|
||||
## [5.1.0] - 2025-11-05
|
||||
|
||||
### Added
|
||||
- **Web-Based Viewer UI**: Production-ready viewer accessible at http://localhost:37777
|
||||
- Real-time visualization via Server-Sent Events (SSE)
|
||||
- Infinite scroll pagination with automatic deduplication
|
||||
- Project filtering to focus on specific codebases
|
||||
- Settings persistence (sidebar state, selected project)
|
||||
- Auto-reconnection with exponential backoff
|
||||
- GPU-accelerated animations for smooth interactions
|
||||
- **New Worker Endpoints** (8 HTTP/SSE routes, +500 lines):
|
||||
- `/api/prompts` - Paginated user prompts with project filtering
|
||||
- `/api/observations` - Paginated observations with project filtering
|
||||
- `/api/summaries` - Paginated session summaries with project filtering
|
||||
- `/api/stats` - Database statistics (total counts by project)
|
||||
- `/api/projects` - List of unique project names
|
||||
- `/stream` - Server-Sent Events for real-time updates
|
||||
- `/` - Serves viewer HTML
|
||||
- **Database Enhancements** (+98 lines in SessionStore):
|
||||
- `getRecentPrompts()` - Paginated prompts with OFFSET/LIMIT
|
||||
- `getRecentObservations()` - Paginated observations with OFFSET/LIMIT
|
||||
- `getRecentSummaries()` - Paginated summaries with OFFSET/LIMIT
|
||||
- `getStats()` - Aggregated statistics by project
|
||||
- `getUniqueProjects()` - Distinct project names
|
||||
- **Complete React UI** (17 new files, 1,500+ lines):
|
||||
- Components: Header, Sidebar, Feed, Cards (Observation, Prompt, Summary, Skeleton)
|
||||
- Hooks: useSSE, usePagination, useSettings, useStats
|
||||
- Utils: Data merging, formatters, constants
|
||||
- Assets: Monaspace Radon font, logos (dark mode + logomark)
|
||||
- Build: esbuild pipeline for self-contained HTML bundle
|
||||
|
||||
|
||||
## [5.0.3] - 2025-11-05
|
||||
|
||||
### Added
|
||||
- **Smart Install Caching**: Eliminated redundant npm install on every SessionStart (2-5s → 10ms)
|
||||
- Caches version state in `.install-version` file
|
||||
- Only runs npm install when actually needed (first time, version change, missing deps)
|
||||
- 200x faster SessionStart for cached installations
|
||||
- Dynamic Python version detection in Windows error messages
|
||||
- Comprehensive Windows troubleshooting guidance
|
||||
|
||||
### Fixed
|
||||
- Fixed Windows installation issues with smart caching installer
|
||||
|
||||
### Changed
|
||||
- Enhanced rsync to respect gitignore rules
|
||||
- Better PM2 worker startup verification
|
||||
- Cross-platform compatible (pure Node.js)
|
||||
|
||||
### Technical Details
|
||||
- New: scripts/smart-install.js (smart caching installer)
|
||||
- Modified: plugin/hooks/hooks.json (use smart-install.js instead of inline npm install)
|
||||
- Modified: package.json (enhanced sync-marketplace script)
|
||||
|
||||
|
||||
## [5.0.2] - 2025-11-05
|
||||
|
||||
### Fixed
|
||||
- **Worker startup reliability**: Fixed async health checks with proper error handling
|
||||
- Added isWorkerHealthy() and waitForWorkerHealth() functions to src/shared/worker-utils.ts
|
||||
- Worker now verifies health before proceeding with hook operations
|
||||
- Improved handling of PM2 failures when not yet installed
|
||||
|
||||
### Changed
|
||||
- Changed ensureWorkerRunning() from synchronous to async with proper await
|
||||
- All hooks now await ensureWorkerRunning for reliable worker communication
|
||||
- Rebuilt all plugin executables with version 5.0.2
|
||||
|
||||
|
||||
## [5.0.1] - 2025-11-05
|
||||
|
||||
### Fixed
|
||||
- Fixed worker service stability issues
|
||||
- Enhanced worker process management and restart reliability
|
||||
- Improved session management and logging across all hooks
|
||||
- Better error handling throughout hook lifecycle
|
||||
|
||||
### Added
|
||||
- GitHub Actions workflows for automated code review
|
||||
|
||||
### Technical Details
|
||||
- Modified: src/services/worker-service.ts (stability improvements)
|
||||
- Modified: src/shared/worker-utils.ts (consistent formatting)
|
||||
- Modified: ecosystem.config.cjs (removed error/output redirection)
|
||||
- Modified: src/hooks/*-hook.ts (ensure worker running)
|
||||
- New: .github/workflows/claude-code-review.yml
|
||||
- New: .github/workflows/claude.yml
|
||||
|
||||
|
||||
## [5.0.0] - 2025-10-27
|
||||
|
||||
### BREAKING CHANGES
|
||||
- **Python dependency for optimal performance**: Semantic search requires Python for ChromaDB
|
||||
- **Search behavior prioritizes semantic relevance**: Chroma semantic search combined with SQLite temporal filtering
|
||||
- **Worker service now initializes ChromaSync on startup**: Automatic vector database synchronization
|
||||
|
||||
### Added
|
||||
- **Hybrid Search Architecture**: Combining ChromaDB semantic search with SQLite FTS5 keyword search
|
||||
- ChromaSync Service for automatic vector database synchronization (738 lines)
|
||||
- Vector embeddings for semantic similarity search
|
||||
- 90-day recency filtering for relevant results
|
||||
- Performance: Semantic search <200ms
|
||||
- **get_context_timeline** MCP tool: Get unified timeline of context around a specific point in time
|
||||
- Anchor by observation ID, session ID, or ISO timestamp
|
||||
- Configurable depth before/after anchor
|
||||
- **get_timeline_by_query** MCP tool: Search for observations and get timeline context around best match
|
||||
- Auto mode: Automatically use top search result as timeline anchor
|
||||
- Interactive mode: Show top N search results for manual anchor selection
|
||||
- **Enhanced MCP tools**: All 9 search tools now support hybrid semantic + keyword search
|
||||
|
||||
### Technical Details
|
||||
- New: src/services/sync/ChromaSync.ts (vector database sync)
|
||||
- Modified: src/servers/search-server.ts (+995 lines for hybrid search)
|
||||
- Modified: src/services/worker-service.ts (+136 lines for ChromaSync integration)
|
||||
- Modified: src/services/sqlite/SessionStore.ts (+276 lines for timeline queries)
|
||||
- Validation: 1,390 observations → 8,279 vector documents
|
||||
- Total MCP tools: 7 → 9 (added timeline tools)
|
||||
|
||||
|
||||
## [4.3.4] - 2025-10-26
|
||||
|
||||
### Fixed
|
||||
- **SessionStart hooks running on session resume**: Added matcher configuration to only run hooks on startup, clear, or compact events
|
||||
- Prevents unnecessary hook execution and improves performance
|
||||
|
||||
### Technical Details
|
||||
- Modified: plugin/hooks/hooks.json (added matcher configuration)
|
||||
|
||||
|
||||
## [4.3.3] - 2025-10-26
|
||||
|
||||
### Added
|
||||
- Made session display count configurable (DISPLAY_SESSION_COUNT = 8)
|
||||
- First-time setup detection with helpful user messaging
|
||||
- Improved UX: First install message clarifies Plugin Hook Error display
|
||||
|
||||
### Technical Details
|
||||
- Updated: src/hooks/context-hook.ts (configurable session count)
|
||||
- Updated: src/hooks/user-message-hook.ts (first-time setup detection)
|
||||
|
||||
|
||||
## [4.3.2] - 2025-10-26
|
||||
|
||||
### Added
|
||||
- **User-facing context display**: Added user-message-hook for displaying context to users via stderr
|
||||
- Hook fires simultaneously with context injection
|
||||
- Error messages don't get added to context, enabling user visibility
|
||||
- Temporary workaround until Claude Code adds ability to share messages with both user and context
|
||||
- **Comprehensive documentation** (4 new files, 2500+ lines total):
|
||||
- docs/architecture-evolution.mdx (801 lines)
|
||||
- docs/context-engineering.mdx (222 lines)
|
||||
- docs/hooks-architecture.mdx (784 lines)
|
||||
- docs/progressive-disclosure.mdx (655 lines)
|
||||
|
||||
### Fixed
|
||||
- Improved cross-platform path handling in context-hook
|
||||
|
||||
### Technical Details
|
||||
- New: src/hooks/user-message-hook.ts (stderr-based display mechanism)
|
||||
- New: plugin/scripts/user-message-hook.js (built executable)
|
||||
- Modified: plugin/hooks/hooks.json (hook configuration)
|
||||
- Modified: src/hooks/context-hook.ts (path handling)
|
||||
- Modified: scripts/build-hooks.js (build support)
|
||||
|
||||
|
||||
## [4.3.1] - 2025-10-26
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,287 +1,282 @@
|
||||
# Claude-Mem: Persistent Memory for Claude Code
|
||||
# Claude-Mem: AI Development Instructions
|
||||
|
||||
## Overview
|
||||
## What This Project Is
|
||||
|
||||
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.
|
||||
Claude-mem is a Claude Code plugin providing persistent memory across sessions. It captures tool usage, compresses observations using the Claude Agent SDK, and injects relevant context into future sessions.
|
||||
|
||||
**Current Version**: 5.0.2
|
||||
**License**: AGPL-3.0
|
||||
**Author**: Alex Newman (@thedotmack)
|
||||
**Your Role**: You are working on the plugin itself. When users interact with Claude Code with this plugin installed, your observations get captured and become their persistent memory.
|
||||
|
||||
## What It Does
|
||||
**Current Version**: 5.1.4
|
||||
|
||||
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
|
||||
## Critical Architecture Knowledge
|
||||
|
||||
This creates a continuous memory system where Claude can learn from past sessions and maintain context across your entire project lifecycle.
|
||||
### The Lifecycle Flow
|
||||
|
||||
## Architecture
|
||||
1. **SessionStart** → `context-hook.ts` runs
|
||||
- Smart installer checks dependencies (cached, only runs on version changes)
|
||||
- Starts PM2 worker if not healthy
|
||||
- Injects context from previous sessions (configurable observation count)
|
||||
|
||||
### Hook-Based Lifecycle System
|
||||
2. **UserPromptSubmit** → `new-hook.ts` runs
|
||||
- Creates session record in SQLite
|
||||
- Saves raw user prompt for FTS5 search
|
||||
|
||||
Claude-mem integrates with Claude Code through 5 lifecycle hooks:
|
||||
3. **PostToolUse** → `save-hook.ts` runs
|
||||
- Captures your tool executions
|
||||
- Sends to worker service for AI compression
|
||||
|
||||
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
|
||||
4. **Summary** → Summary hook generates session summaries
|
||||
|
||||
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)
|
||||
5. **SessionEnd** → `cleanup-hook.ts` runs
|
||||
- Marks session complete (graceful, not DELETE)
|
||||
- Skips on `/clear` to preserve ongoing sessions
|
||||
|
||||
3. **PostToolUse Hook** (`save-hook`)
|
||||
- Captures tool execution observations
|
||||
- Sends observations to worker service for processing
|
||||
### Key Components
|
||||
|
||||
4. **Summary Hook**
|
||||
- Generates AI-powered session summaries
|
||||
- Processes accumulated observations
|
||||
**Hooks** (`src/hooks/*.ts`)
|
||||
- Built to `plugin/scripts/*-hook.js` (ESM format)
|
||||
- Must output valid JSON to `hookSpecificOutput` field
|
||||
- Called by Claude Code lifecycle events
|
||||
|
||||
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** (`src/services/worker-service.ts`)
|
||||
- Express.js API on port 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`)
|
||||
- Managed by PM2 (auto-started by hooks)
|
||||
- Built to `plugin/worker-service.cjs` (CJS format)
|
||||
- Handles AI processing asynchronously to avoid hook timeouts
|
||||
|
||||
### Worker Service Architecture
|
||||
**Database** (`src/services/sqlite/`)
|
||||
- SQLite3 with better-sqlite3 (NOT bun:sqlite - that's legacy)
|
||||
- Location: `~/.claude-mem/claude-mem.db`
|
||||
- FTS5 virtual tables for full-text search
|
||||
- `SessionStore` = CRUD, `SessionSearch` = FTS5 queries
|
||||
|
||||
- **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)
|
||||
**MCP Search Server** (`src/servers/search-server.ts`)
|
||||
- Exposes 9 search tools to Claude Code
|
||||
- Configured in `plugin/.mcp.json`
|
||||
- Built to `plugin/search-server.mjs` (ESM format)
|
||||
|
||||
**REST API Endpoints** (6 total):
|
||||
- Session management endpoints
|
||||
- Observation processing endpoints
|
||||
- Worker port tracking
|
||||
**Chroma Vector Database** (`src/services/sync/ChromaSync.ts`)
|
||||
- Hybrid semantic + keyword search architecture
|
||||
- Automatic vector embedding synchronization
|
||||
- 90-day recency filtering for relevant results
|
||||
- Combined with SQLite FTS5 for optimal search performance
|
||||
|
||||
The worker service runs as a PM2-managed background process that handles AI processing separately from the hook execution, preventing hook timeout issues.
|
||||
**Viewer UI** (`src/ui/viewer/`)
|
||||
- React + TypeScript web interface accessible at http://localhost:37777
|
||||
- Real-time memory stream visualization via Server-Sent Events (SSE)
|
||||
- Infinite scroll pagination for observations, sessions, and user prompts
|
||||
- Project filtering and settings persistence
|
||||
- Built to `plugin/ui/viewer.html` (self-contained bundle via esbuild)
|
||||
- Auto-reconnection and error recovery
|
||||
|
||||
### Database Layer
|
||||
## How to Make Changes
|
||||
|
||||
**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).
|
||||
|
||||
### Context Display Settings
|
||||
|
||||
Configure how much historical context is displayed at session start via `~/.claude/settings.json`:
|
||||
|
||||
**Environment variable** (in the `env` section):
|
||||
- `CLAUDE_MEM_CONTEXT_OBSERVATIONS` - Number of recent observations to display (default: 50, ~1.2K tokens typical)
|
||||
|
||||
**Example settings.json**:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_MODEL": "claude-haiku-4-5",
|
||||
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "100"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Notes**:
|
||||
- Higher observation counts = more context but more tokens consumed at startup
|
||||
- 50 observations ≈ 4-8 hours of work ≈ 1.2K tokens
|
||||
- 100 observations ≈ 1-2 days of work ≈ 2.4K tokens
|
||||
- 200 observations ≈ 2-3 days of work ≈ 4.8K tokens
|
||||
- Session summaries are shown when available but are not the primary timeline
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
### Usage Tracking
|
||||
|
||||
Claude-mem automatically tracks SDK usage metrics to JSONL files for cost analysis:
|
||||
|
||||
**Location**: `~/.claude-mem/usage-logs/usage-YYYY-MM-DD.jsonl`
|
||||
|
||||
**Captured Metrics**:
|
||||
- Token counts (input, output, cache creation, cache read)
|
||||
- Total cost in USD per API call
|
||||
- Duration metrics (total time and API time)
|
||||
- Number of turns per session
|
||||
- Session and project attribution
|
||||
- Model information
|
||||
|
||||
**Analysis Tools**:
|
||||
```bash
|
||||
# Analyze today's usage
|
||||
npm run usage:today
|
||||
|
||||
# Analyze specific date
|
||||
npm run usage:analyze 2025-11-03
|
||||
```
|
||||
|
||||
The analysis script provides:
|
||||
- Total cost and token usage
|
||||
- Cache hit rates and savings
|
||||
- Cost breakdowns by project
|
||||
- Cost breakdowns by model
|
||||
- Average cost per API call
|
||||
|
||||
## 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
|
||||
|
||||
**Build and sync to marketplace plugin**:
|
||||
### When You Modify Hooks
|
||||
```bash
|
||||
npm run build
|
||||
npm run sync-marketplace
|
||||
```
|
||||
Changes take effect on next Claude Code session. No worker restart needed.
|
||||
|
||||
**If you changed the worker service** (`src/services/worker-service.ts`):
|
||||
### When You Modify Worker Service
|
||||
```bash
|
||||
npm run build
|
||||
npm run sync-marketplace
|
||||
npm run worker:restart
|
||||
```
|
||||
Must restart PM2 worker for changes to take effect.
|
||||
|
||||
**What happens**:
|
||||
1. `npm run build` - Compiles TypeScript and outputs hook executables to `plugin/scripts/`
|
||||
2. `npm run sync-marketplace` - Syncs built files to `~/.claude/plugins/marketplaces/thedotmack/`
|
||||
3. `npm run worker:restart` - (Optional) Only needed if you modified the worker service code
|
||||
### When You Modify MCP Server
|
||||
```bash
|
||||
npm run build
|
||||
npm run sync-marketplace
|
||||
# Restart Claude Code for MCP changes
|
||||
```
|
||||
|
||||
**Build Outputs**:
|
||||
- Hook executables: `*-hook.js` (ESM format)
|
||||
- Worker service: `worker-service.cjs` (CJS format)
|
||||
- Search server: `search-server.js` (ESM format)
|
||||
### When You Modify Viewer UI
|
||||
```bash
|
||||
npm run build
|
||||
npm run sync-marketplace
|
||||
npm run worker:restart
|
||||
```
|
||||
Changes to React components, styles, or viewer logic require rebuilding and restarting the worker. Refresh browser to see changes.
|
||||
|
||||
**Note**: Hook changes take effect immediately on next session. Worker changes require restart.
|
||||
### Build Pipeline
|
||||
1. `npm run build` → Compiles TypeScript, outputs to `plugin/`
|
||||
2. `npm run sync-marketplace` → Syncs to `~/.claude/plugins/marketplaces/thedotmack/`
|
||||
3. Changes are live for next session (hooks/MCP) or after restart (worker)
|
||||
|
||||
### Investigation Best Practices
|
||||
## Coding Standards: DRY, YAGNI, and Anti-Patterns
|
||||
|
||||
**Philosophy**: Write the dumb, obvious thing first. Add complexity only when you actually hit the problem.
|
||||
|
||||
### Common Anti-Patterns to Avoid
|
||||
|
||||
**1. Wrapper Functions for Constants**
|
||||
```typescript
|
||||
// ❌ DON'T: Ceremonial wrapper that adds zero value
|
||||
export function getWorkerPort(): number {
|
||||
return FIXED_PORT;
|
||||
}
|
||||
|
||||
// ✅ DO: Export the constant directly
|
||||
export const WORKER_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || "37777", 10);
|
||||
```
|
||||
|
||||
**2. Unused Default Parameters**
|
||||
```typescript
|
||||
// ❌ DON'T: Defaults that are never actually used
|
||||
async function isHealthy(timeout: number = 3000) { ... }
|
||||
// Every call: isHealthy(1000) - the default is dead code
|
||||
|
||||
// ✅ DO: Remove the default if no one uses it
|
||||
async function isHealthy(timeout: number) { ... }
|
||||
```
|
||||
|
||||
**3. Magic Numbers Everywhere**
|
||||
```typescript
|
||||
// ❌ DON'T: Unexplained magic numbers scattered throughout
|
||||
if (await isWorkerHealthy(1000)) { ... }
|
||||
await waitForHealth(10000);
|
||||
setTimeout(resolve, 100);
|
||||
|
||||
// ✅ DO: Named constants with context
|
||||
const HEALTH_CHECK_TIMEOUT_MS = 1000;
|
||||
const HEALTH_CHECK_MAX_WAIT_MS = 10000;
|
||||
const HEALTH_CHECK_POLL_INTERVAL_MS = 100;
|
||||
```
|
||||
|
||||
**4. Overengineered Error Handling**
|
||||
```typescript
|
||||
// ❌ DON'T: Silent failures and defensive programming for ghosts
|
||||
checkProcess.on("close", (code) => {
|
||||
// PM2 list can fail, but we should still continue - just assume worker isn't running
|
||||
resolve(); // <- Silent failure!
|
||||
});
|
||||
|
||||
// ✅ DO: Fail fast with clear errors
|
||||
checkProcess.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`PM2 not found - install dependencies first`));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
```
|
||||
|
||||
**5. Fragile String Parsing**
|
||||
```typescript
|
||||
// ❌ DON'T: Parse human-readable output with string matching
|
||||
const isRunning = output.includes("claude-mem-worker") && output.includes("online");
|
||||
|
||||
// ✅ DO: Use structured output (JSON)
|
||||
const processes = JSON.parse(execSync('pm2 jlist'));
|
||||
const isRunning = processes.some(p => p.name === 'claude-mem-worker' && p.pm2_env.status === 'online');
|
||||
```
|
||||
|
||||
**6. Duplicated Promise Wrappers**
|
||||
```typescript
|
||||
// ❌ DON'T: Copy-paste the same promise pattern multiple times
|
||||
await new Promise((resolve, reject) => {
|
||||
process1.on("error", reject);
|
||||
process1.on("close", (code) => { /* ... */ });
|
||||
});
|
||||
// ... later ...
|
||||
await new Promise((resolve, reject) => {
|
||||
process2.on("error", reject);
|
||||
process2.on("close", (code) => { /* ... same pattern */ });
|
||||
});
|
||||
|
||||
// ✅ DO: Extract a helper function
|
||||
async function waitForProcess(process: ChildProcess, validateExitCode = false): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
process.on("error", reject);
|
||||
process.on("close", (code) => {
|
||||
if (validateExitCode && code !== 0 && code !== null) {
|
||||
reject(new Error(`Process failed with exit code ${code}`));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**7. YAGNI Violations - Solving Problems You Don't Have**
|
||||
```typescript
|
||||
// ❌ DON'T: 50+ lines checking PM2 status before starting
|
||||
const checkProcess = spawn(pm2Path, ["list", "--no-color"]);
|
||||
// ... parse output ...
|
||||
// ... check if running ...
|
||||
// ... then maybe start it ...
|
||||
|
||||
// ✅ DO: Just start it (PM2 start is idempotent)
|
||||
if (!await isWorkerHealthy()) {
|
||||
await startWorker(); // PM2 handles "already running" gracefully
|
||||
if (!await waitForWorkerHealth()) {
|
||||
throw new Error("Worker failed to become healthy");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Why These Patterns Appear
|
||||
|
||||
These anti-patterns often emerge from:
|
||||
- **Training bias**: Code that looks "professional" is often overengineered
|
||||
- **Risk aversion**: Optimizing for "what could go wrong" instead of "what do you actually need"
|
||||
- **Pattern matching**: Seeing a problem and immediately scaffolding a framework
|
||||
- **No real-world pain**: Not debugging at 2am means not feeling the cost of complexity
|
||||
|
||||
### The Actual Standard
|
||||
|
||||
1. **YAGNI (You Aren't Gonna Need It)**: Don't build it until you need it
|
||||
2. **DRY (Don't Repeat Yourself)**: Extract patterns after the second duplication, not before
|
||||
3. **Fail Fast**: Explicit errors beat silent failures
|
||||
4. **Simple First**: Write the obvious solution, then optimize only if needed
|
||||
5. **Delete Aggressively**: Less code = fewer bugs
|
||||
|
||||
**Reference**: See worker-utils.ts critique (conversation 2025-11-05) for detailed examples.
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Hook
|
||||
1. Create `src/hooks/new-hook.ts`
|
||||
2. Add to `scripts/build-hooks.js` build list
|
||||
3. Add configuration to `plugin/hooks/hooks.json`
|
||||
4. Build and sync: `npm run build && npm run sync-marketplace`
|
||||
|
||||
### Modifying Database Schema
|
||||
1. Update schema in `src/services/sqlite/schema.ts`
|
||||
2. Update SessionStore/SessionSearch classes
|
||||
3. Migration strategy: The plugin currently recreates on schema changes (acceptable for alpha)
|
||||
4. TODO: Add proper migrations for production
|
||||
|
||||
### Debugging Worker Issues
|
||||
```bash
|
||||
pm2 list # Check worker status
|
||||
npm run worker:logs # View logs
|
||||
npm run worker:restart # Restart if needed
|
||||
pm2 delete claude-mem-worker # Force clean start
|
||||
```
|
||||
|
||||
### Testing Changes Locally
|
||||
1. Make changes in `src/`
|
||||
2. `npm run build && npm run sync-marketplace`
|
||||
3. Start new Claude Code session (hooks) or restart worker (worker changes)
|
||||
4. Check `~/.claude-mem/claude-mem.db` for database state
|
||||
5. Use MCP search tools to verify behavior
|
||||
|
||||
### Version Bumps
|
||||
Use the version-bump skill:
|
||||
```bash
|
||||
/skill version-bump
|
||||
```
|
||||
Choose patch/minor/major. Updates package.json, marketplace.json, plugin.json, and CLAUDE.md.
|
||||
|
||||
## Investigation Best Practices
|
||||
|
||||
**When investigations are failing persistently**, use Task agents for comprehensive file analysis instead of grep/search:
|
||||
|
||||
**❌ Don't:** Repeatedly grep and search for patterns when failing to find the issue
|
||||
```bash
|
||||
# Multiple failed attempts with grep, Glob, etc.
|
||||
```
|
||||
|
||||
**✅ Do:** Deploy a Task agent to read files in full and answer specific questions
|
||||
```
|
||||
@@ -292,7 +287,7 @@ npm run worker:restart
|
||||
- More efficient than multiple rounds of searching
|
||||
```
|
||||
|
||||
**Example usage:**
|
||||
**Example:**
|
||||
```
|
||||
Deploy a general-purpose Task agent to:
|
||||
1. Read src/hooks/context-hook.ts in full
|
||||
@@ -301,198 +296,148 @@ Deploy a general-purpose Task agent to:
|
||||
4. Find any bugs or inconsistencies between them
|
||||
```
|
||||
|
||||
This approach is especially valuable when:
|
||||
- You're investigating how multiple files interact
|
||||
Use this when:
|
||||
- Investigating how multiple files interact
|
||||
- Search queries aren't finding what you expect
|
||||
- You need to understand complete implementation context
|
||||
- The issue might be a subtle inconsistency between files
|
||||
- Need complete implementation context
|
||||
- Issue might be a subtle inconsistency between files
|
||||
|
||||
## Version History
|
||||
## Recent Changes
|
||||
|
||||
For detailed version history and changelog, see [CHANGELOG.md](CHANGELOG.md).
|
||||
### v5.1.2 - Theme Toggle
|
||||
**Theme Support**: Light/dark mode for viewer UI
|
||||
- User-selectable theme with persistent settings
|
||||
- Automatic system preference detection
|
||||
- Smooth transitions between themes
|
||||
- Settings stored in browser localStorage
|
||||
|
||||
**Current Version**: 5.0.2
|
||||
### v5.1.0 - Web-Based Viewer UI
|
||||
**Major Feature**: Web-Based Viewer UI for Real-Time Memory Stream
|
||||
- Production-ready viewer accessible at http://localhost:37777
|
||||
- Real-time visualization via Server-Sent Events (SSE) - see observations, sessions, and prompts as they happen
|
||||
- Infinite scroll pagination with automatic deduplication
|
||||
- Project filtering to focus on specific codebases
|
||||
- Settings persistence (sidebar state, selected project)
|
||||
- Auto-reconnection with exponential backoff
|
||||
- GPU-accelerated animations for smooth interactions
|
||||
|
||||
### Recent Highlights
|
||||
**Worker Service API Endpoints** (14 HTTP/SSE endpoints total):
|
||||
|
||||
#### v5.0.2 (2025-11-04)
|
||||
**Breaking Changes**: None (patch version)
|
||||
*Viewer & Health:*
|
||||
- `GET /` - Serves viewer HTML (self-contained React app)
|
||||
- `GET /health` - Health check endpoint
|
||||
- `GET /stream` - Server-Sent Events for real-time updates
|
||||
|
||||
**Fixes**:
|
||||
- Fixed worker startup reliability with async health checks (PR #51: src/shared/worker-utils.ts)
|
||||
- Added proper error handling to PM2 process spawning (src/shared/worker-utils.ts)
|
||||
- Worker now verifies health before proceeding with hook operations
|
||||
- Improved handling of PM2 failures when not yet installed
|
||||
*Data Retrieval:*
|
||||
- `GET /api/prompts` - Paginated user prompts with project filtering
|
||||
- `GET /api/observations` - Paginated observations with project filtering
|
||||
- `GET /api/summaries` - Paginated session summaries with project filtering
|
||||
- `GET /api/stats` - Database statistics (total counts by project)
|
||||
|
||||
**Technical Details**:
|
||||
- Modified: src/shared/worker-utils.ts (added isWorkerHealthy, waitForWorkerHealth functions)
|
||||
- Modified: src/hooks/*.ts (all hooks now await ensureWorkerRunning)
|
||||
- Modified: plugin/scripts/*.js (rebuilt hook executables)
|
||||
- Root cause: ensureWorkerRunning was synchronous and didn't verify worker was actually responsive before proceeding
|
||||
- Impact: More reliable worker startup with proper health verification
|
||||
*Settings:*
|
||||
- `GET /api/settings` - Get current viewer settings
|
||||
- `POST /api/settings` - Update viewer settings
|
||||
|
||||
#### v5.0.1 (2025-11-04)
|
||||
**Breaking Changes**: None (patch version)
|
||||
*Session Management:*
|
||||
- `POST /sessions/:sessionDbId/init` - Initialize new session
|
||||
- `POST /sessions/:sessionDbId/observations` - Add observations to session
|
||||
- `POST /sessions/:sessionDbId/summarize` - Generate session summary
|
||||
- `GET /sessions/:sessionDbId/status` - Get session status
|
||||
- `DELETE /sessions/:sessionDbId` - Delete session (graceful cleanup)
|
||||
|
||||
**Fixes**:
|
||||
- Fixed worker service stability issues (PR #47: src/services/worker-service.ts, src/shared/worker-utils.ts)
|
||||
- Improved worker process management and restart reliability (src/hooks/*-hook.ts)
|
||||
- Enhanced session management and logging across all hooks
|
||||
- Removed error/output file redirection from PM2 ecosystem config for better debugging (ecosystem.config.cjs)
|
||||
**Database Enhancements** (+98 lines in SessionStore):
|
||||
- `getRecentPrompts()` - Paginated prompts with OFFSET/LIMIT
|
||||
- `getRecentObservations()` - Paginated observations with OFFSET/LIMIT
|
||||
- `getRecentSummaries()` - Paginated summaries with OFFSET/LIMIT
|
||||
- `getStats()` - Aggregated statistics by project
|
||||
- `getUniqueProjects()` - Distinct project names
|
||||
|
||||
**Improvements**:
|
||||
- Added GitHub Actions workflows for automated code review (PR #48)
|
||||
- Claude Code Review workflow (.github/workflows/claude-code-review.yml)
|
||||
- Claude PR Assistant workflow (.github/workflows/claude.yml)
|
||||
- Better worker health checks and startup sequence
|
||||
- Improved error handling and logging throughout hook lifecycle
|
||||
- Cleaned up documentation files and consolidated project context
|
||||
**Complete React UI** (17 new files, 1,500+ lines):
|
||||
- Components: Header, Sidebar, Feed, Cards (Observation, Prompt, Summary, Skeleton)
|
||||
- Hooks: useSSE, usePagination, useSettings, useStats
|
||||
- Utils: Data merging, formatters, constants
|
||||
- Assets: Monaspace Radon font, logos (dark mode + logomark)
|
||||
- Build: esbuild pipeline for self-contained HTML bundle
|
||||
|
||||
**Technical Details**:
|
||||
- Modified: src/services/worker-service.ts (stability improvements)
|
||||
- Modified: src/shared/worker-utils.ts (consistent formatting and readability)
|
||||
- Modified: ecosystem.config.cjs (removed error/output redirection)
|
||||
- Modified: src/hooks/*-hook.ts (ensure worker running before processing)
|
||||
- New: .github/workflows/claude-code-review.yml
|
||||
- New: .github/workflows/claude.yml
|
||||
- Rebuilt: plugin/scripts/*.js (all hook executables)
|
||||
- Impact: More reliable worker service with better error visibility and automated PR assistance
|
||||
**Why This Matters**: Users can now visualize their memory stream in real-time. See exactly what claude-mem is capturing as you work, filter by project, and understand the context being injected into sessions.
|
||||
|
||||
#### v4.3.4 (2025-11-01)
|
||||
**Breaking Changes**: None (patch version)
|
||||
### v5.0.3 - Smart Install Caching
|
||||
**Smart Caching Installer for Windows Compatibility**:
|
||||
- Eliminated redundant npm install on every SessionStart (2-5s → 10ms)
|
||||
- Caches version in `.install-version` file
|
||||
- Only runs npm install when actually needed (first time, version change, missing deps)
|
||||
- 200x performance improvement for cached installations
|
||||
|
||||
**Fixes**:
|
||||
- Fixed SessionStart hooks running on session resume (plugin/hooks/hooks.json:4)
|
||||
- Added matcher configuration to only run SessionStart hooks on startup, clear, or compact events
|
||||
- Prevents unnecessary hook execution and improves performance on session resume
|
||||
### v5.0.0 - Hybrid Search Architecture
|
||||
**Major Feature**: Chroma Vector Database Integration
|
||||
- Hybrid semantic + keyword search combining ChromaDB with SQLite FTS5
|
||||
- ChromaSync service for automatic vector embedding synchronization (738 lines)
|
||||
- 90-day recency filtering for contextually relevant results
|
||||
- New MCP tools: `get_context_timeline` and `get_timeline_by_query`
|
||||
- Performance: Semantic search <200ms with 8,000+ vector documents
|
||||
- Enhanced all 9 MCP search tools with hybrid search capabilities
|
||||
|
||||
**Technical Details**:
|
||||
- Modified: plugin/hooks/hooks.json:4 (added `"matcher": "startup|clear|compact"`)
|
||||
- Impact: Hooks now skip execution when resuming existing sessions
|
||||
## Configuration Users Can Set
|
||||
|
||||
#### v4.3.3 (2025-10-27)
|
||||
**Breaking Changes**: None (patch version)
|
||||
**Model Selection** (`~/.claude/settings.json`):
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_MODEL": "claude-haiku-4-5" // or sonnet-4-5, opus-4, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Improvements**:
|
||||
- Made session display count configurable via constant (DISPLAY_SESSION_COUNT = 8) in src/hooks/context-hook.ts:11
|
||||
- Added first-time setup detection with helpful user messaging in src/hooks/user-message-hook.ts:12-39
|
||||
- Improved user experience: First install message clarifies why it appears under "Plugin Hook Error"
|
||||
**Context Observation Count** (`~/.claude/settings.json`):
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "50" // default, adjust based on needs
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fixes**:
|
||||
- Cleaned up profanity in code comments (src/hooks/context-hook.ts:3)
|
||||
- Fixed first-time setup UX by detecting missing node_modules and showing informative message
|
||||
|
||||
**Technical Details**:
|
||||
- Modified: src/hooks/context-hook.ts:11 (configurable DISPLAY_SESSION_COUNT constant)
|
||||
- Modified: src/hooks/user-message-hook.ts:12-39 (first-time setup detection and messaging)
|
||||
- Modified: plugin/scripts/context-hook.js (rebuilt)
|
||||
- Modified: plugin/scripts/user-message-hook.js (rebuilt)
|
||||
|
||||
#### v4.3.2 (2025-10-27)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Improvements**:
|
||||
- Added user-message-hook for displaying context to users via stderr mechanism (src/hooks/user-message-hook.ts)
|
||||
- Enhanced context visibility: Hook fires simultaneously with context injection, sending duplicate message as "error" so Claude Code displays it to users
|
||||
- Added comprehensive documentation (4 new MDX files covering architecture evolution, context engineering, hooks architecture, and progressive disclosure)
|
||||
- Improved cross-platform path handling in context-hook (src/hooks/context-hook.ts:14)
|
||||
|
||||
**Technical Details**:
|
||||
- New files:
|
||||
- src/hooks/user-message-hook.ts (stderr-based user-facing context display)
|
||||
- plugin/scripts/user-message-hook.js (built hook executable)
|
||||
- docs/architecture-evolution.mdx (801 lines)
|
||||
- docs/context-engineering.mdx (222 lines)
|
||||
- docs/hooks-architecture.mdx (784 lines)
|
||||
- docs/progressive-disclosure.mdx (655 lines)
|
||||
- Modified:
|
||||
- plugin/hooks/hooks.json:5 (added user-message-hook configuration)
|
||||
- src/hooks/context-hook.ts:14 (improved path handling)
|
||||
- scripts/build-hooks.js:3 (build support for new hook)
|
||||
- Design rationale: Error messages don't get added to context, so we intentionally duplicate context output via stderr for user visibility. This is a temporary workaround until Claude Code potentially adds ability to share messages with both user and context simultaneously.
|
||||
|
||||
#### v4.3.1 (2025-10-26)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Fixes**:
|
||||
- Fixed SessionStart hook context injection by silencing npm install output (plugin/hooks/hooks.json:25)
|
||||
- Changed npm loglevel from `--loglevel=error` to `--loglevel=silent` to ensure clean JSON output
|
||||
- Consolidated hooks architecture by removing bin/hooks wrapper layer (src/hooks/*-hook.ts)
|
||||
- Fixed double shebang issues in hook executables (esbuild now adds shebang during build)
|
||||
|
||||
**Technical Details**:
|
||||
- Modified: plugin/hooks/hooks.json (npm install verbosity)
|
||||
- Removed: src/bin/hooks/* (wrapper layer no longer needed)
|
||||
- Consolidated: Hook logic moved directly into src/hooks/*-hook.ts files
|
||||
- Root cause: npm install stderr/stdout was polluting hook JSON output, preventing context injection
|
||||
|
||||
#### v4.3.0 (2025-10-25)
|
||||
- Progressive Disclosure Context: Enhanced context hook with observation timeline and token cost visibility
|
||||
- Session observations now display in table format showing ID, timestamp, type indicators, title, and token counts
|
||||
- Added progressive disclosure usage instructions to guide Claude on when to fetch full observation details vs. reading code
|
||||
- Added Agent Skills documentation and version bump management skill
|
||||
- Cross-platform path improvements: Removed hardcoded paths for project and Claude Code executable (fixes #23)
|
||||
|
||||
#### v4.2.11 (2025-10-25)
|
||||
- Fixed cross-platform Claude executable path detection using `which`/`where` commands
|
||||
- Full Windows, macOS, and Linux compatibility
|
||||
|
||||
#### v4.2.8 (2025-10-25)
|
||||
- Fixed NOT NULL constraint violation for claude_session_id
|
||||
|
||||
#### v4.2.3 (2025-10-23)
|
||||
- Fixed FTS5 injection vulnerability
|
||||
- Fixed Windows PowerShell compatibility
|
||||
|
||||
#### v4.0.0 (2025-10-18)
|
||||
- MCP Search Server with FTS5 full-text search
|
||||
- Plugin data directory integration
|
||||
- HTTP REST API architecture with PM2
|
||||
**Worker Port** (`~/.claude/settings.json`):
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_WORKER_PORT": "37777" // default
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
### Why PM2 Instead of Direct Process
|
||||
Hooks have strict timeout limits. PM2 manages a persistent background worker, allowing AI processing to continue after hooks complete.
|
||||
|
||||
### 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.
|
||||
### Why SQLite FTS5
|
||||
Enables instant full-text search across thousands of observations without external dependencies. Automatic sync triggers keep FTS5 tables synchronized.
|
||||
|
||||
### 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.
|
||||
### Why Graceful Cleanup (v4.1.0)
|
||||
Changed from aggressive DELETE requests to marking sessions complete. Prevents interrupting summary generation and other async operations.
|
||||
|
||||
## Troubleshooting
|
||||
### Why Smart Install Caching (v5.0.3)
|
||||
npm install is expensive (2-5s). Caching version state and only installing on changes makes SessionStart nearly instant (10ms).
|
||||
|
||||
### Worker Service Issues
|
||||
- Check PM2 status: `pm2 list`
|
||||
- View logs: `npm run worker:logs`
|
||||
- Restart worker: `npm run worker:restart`
|
||||
### Why Web-Based Viewer UI (v5.1.0)
|
||||
Real-time visibility into memory stream helps users understand what's being captured and how context is being built. SSE provides instant updates without polling. Self-contained HTML bundle (esbuild) eliminates deployment complexity - everything served from a single file.
|
||||
|
||||
### Database Issues
|
||||
- Database location: `~/.claude-mem/claude-mem.db`
|
||||
- Check schema: `sqlite3 <db-path> ".schema"`
|
||||
- FTS5 tables are automatically synchronized via triggers
|
||||
## File Locations
|
||||
|
||||
### Hook Issues
|
||||
- Hooks output to Claude Code's hook execution log
|
||||
- Check `plugin/scripts/` for built executables
|
||||
**Source**: `<project-root>/src/` - TypeScript source files
|
||||
**Built Plugin**: `<project-root>/plugin/` - Compiled JavaScript outputs
|
||||
**Installed Plugin**: `~/.claude/plugins/marketplaces/thedotmack/` - User's installed plugin location
|
||||
**Database**: `~/.claude-mem/claude-mem.db` - SQLite database with observations, sessions, summaries
|
||||
**Chroma Database**: `~/.claude-mem/chroma/` - Vector embeddings for semantic search
|
||||
**Usage Logs**: `~/.claude-mem/usage-logs/usage-YYYY-MM-DD.jsonl` - Daily API usage tracking
|
||||
|
||||
### Model Configuration Issues
|
||||
- Use `./claude-mem-settings.sh` to manage model settings
|
||||
- Settings stored in `~/.claude/settings.json`
|
||||
- Default fallback: `claude-sonnet-4-5`
|
||||
## Quick Reference
|
||||
|
||||
## 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
|
||||
**Build**: `npm run build`
|
||||
**Sync**: `npm run sync-marketplace`
|
||||
**Worker Restart**: `npm run worker:restart`
|
||||
**Worker Logs**: `npm run worker:logs`
|
||||
**Version Bump**: `/skill version-bump`
|
||||
**Usage Analysis**: `npm run usage:today`
|
||||
**Viewer UI**: http://localhost:37777 (auto-starts with worker)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<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.3.1-green.svg" alt="Version">
|
||||
<img src="https://img.shields.io/badge/version-5.1.2-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">
|
||||
@@ -58,7 +58,8 @@ Restart Claude Code. Context from previous sessions will automatically appear in
|
||||
**Key Features:**
|
||||
- 🧠 **Persistent Memory** - Context survives across sessions
|
||||
- 📊 **Progressive Disclosure** - Layered memory retrieval with token cost visibility
|
||||
- 🔍 **7 Search Tools** - Query your project history via MCP
|
||||
- 🔍 **9 Search Tools** - Query your project history via MCP
|
||||
- 🖥️ **Web Viewer UI** - Real-time memory stream at http://localhost:37777
|
||||
- 🤖 **Automatic Operation** - No manual intervention required
|
||||
- 🔗 **Citations** - Reference past decisions with `claude-mem://` URIs
|
||||
|
||||
@@ -85,12 +86,13 @@ npx mintlify dev
|
||||
|
||||
### Architecture
|
||||
- **[Overview](docs/architecture/overview.mdx)** - System components & data flow
|
||||
- **[Architecture Evolution](docs/architecture-evolution.mdx)** - The journey from v3 to v4
|
||||
- **[Architecture Evolution](docs/architecture-evolution.mdx)** - The journey from v3 to v5
|
||||
- **[Hooks Architecture](docs/hooks-architecture.mdx)** - How Claude-Mem uses lifecycle hooks
|
||||
- **[Hooks Reference](docs/architecture/hooks.mdx)** - 5 lifecycle hooks explained
|
||||
- **[Hooks Reference](docs/architecture/hooks.mdx)** - 7 hook scripts 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
|
||||
- **[MCP Search](docs/architecture/mcp-search.mdx)** - 9 search tools & examples
|
||||
- **[Viewer UI](docs/VIEWER.md)** - Web-based memory stream visualization
|
||||
|
||||
### Configuration & Development
|
||||
- **[Configuration](docs/configuration.mdx)** - Environment variables & settings
|
||||
@@ -103,7 +105,7 @@ npx mintlify dev
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Session Start → Inject context from last 10 sessions │
|
||||
│ Session Start → Inject recent observations as context │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
@@ -124,10 +126,11 @@ npx mintlify dev
|
||||
```
|
||||
|
||||
**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. **7 Lifecycle Hook Scripts** - smart-install, context-hook, user-message-hook, new-hook, save-hook, summary-hook, cleanup-hook
|
||||
2. **Worker Service** - HTTP API on port 37777 with web viewer UI, managed by PM2
|
||||
3. **SQLite Database** - Stores sessions, observations, summaries with FTS5 full-text search
|
||||
4. **9 MCP Search Tools** - Query historical context with citations and timeline analysis
|
||||
5. **Chroma Vector Database** - Hybrid semantic + keyword search for intelligent context retrieval
|
||||
|
||||
See [Architecture Overview](docs/architecture/overview.mdx) for details.
|
||||
|
||||
@@ -135,15 +138,17 @@ See [Architecture Overview](docs/architecture/overview.mdx) for details.
|
||||
|
||||
## MCP Search Tools
|
||||
|
||||
Claude-Mem provides 7 specialized search tools:
|
||||
Claude-Mem provides 9 specialized search tools:
|
||||
|
||||
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
|
||||
4. **find_by_concept** - Find by concept tags (discovery, problem-solution, pattern, etc.)
|
||||
5. **find_by_file** - Find observations referencing specific files
|
||||
6. **find_by_type** - Find by type (decision, bugfix, feature, refactor, discovery, change)
|
||||
7. **get_recent_context** - Get recent session context for a project
|
||||
8. **get_context_timeline** - Get unified timeline of context around a specific point in time
|
||||
9. **get_timeline_by_query** - Search for observations and get timeline context around best match
|
||||
|
||||
**Example Queries:**
|
||||
```
|
||||
@@ -151,24 +156,39 @@ 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
|
||||
get_context_timeline with anchor="S890" depth_before=10 depth_after=10
|
||||
get_timeline_by_query with query="viewer UI implementation" mode="auto"
|
||||
```
|
||||
|
||||
See [MCP Search Tools Guide](docs/usage/search-tools.mdx) for detailed examples.
|
||||
|
||||
---
|
||||
|
||||
## What's New in v4.3.1
|
||||
## What's New in v5.1.2
|
||||
|
||||
**Critical Fix:**
|
||||
- **SessionStart hook context injection**: Fixed context not being injected into new sessions
|
||||
- npm install output was polluting hook JSON responses
|
||||
- Changed npm loglevel to `--loglevel=silent` for clean output
|
||||
- Context injection now works reliably across all sessions
|
||||
**🎨 Theme Toggle (v5.1.2):**
|
||||
- Light/dark mode support in viewer UI
|
||||
- System preference detection
|
||||
- Persistent theme settings across sessions
|
||||
- Smooth transitions between themes
|
||||
|
||||
**Code Quality:**
|
||||
- Consolidated hooks architecture by removing wrapper layer
|
||||
- Fixed double shebang issues in hook executables
|
||||
- Simplified codebase maintenance
|
||||
**🖥️ Web-Based Viewer UI (v5.1.0):**
|
||||
- Real-time memory stream visualization at http://localhost:37777
|
||||
- Server-Sent Events (SSE) for instant updates
|
||||
- Infinite scroll pagination with automatic deduplication
|
||||
- Project filtering to focus on specific codebases
|
||||
- Settings persistence (sidebar state, selected project)
|
||||
- Auto-reconnection with exponential backoff
|
||||
|
||||
**⚡ Smart Install Caching (v5.0.3):**
|
||||
- Eliminated redundant npm installs on every session (2-5s → 10ms)
|
||||
- Caches version in `.install-version` file
|
||||
- Only runs npm install when needed (first time, version change, missing deps)
|
||||
|
||||
**🔍 Hybrid Search Architecture (v5.0.0):**
|
||||
- Chroma vector database for semantic search
|
||||
- Combined with FTS5 keyword search
|
||||
- Intelligent context retrieval with 90-day recency filtering
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
|
||||
|
||||
+542
@@ -0,0 +1,542 @@
|
||||
# Chroma Vector Database - Hybrid Semantic Search
|
||||
|
||||
## Overview
|
||||
|
||||
Claude-Mem v5.0.0 introduced **Chroma**, a vector database that enables semantic search across your memory stream. Combined with SQLite's FTS5 keyword search, this creates a powerful **hybrid search architecture** that finds contextually relevant observations using both meaning and keywords.
|
||||
|
||||
**Key Benefits:**
|
||||
- 🧠 **Semantic Search** - Find observations by meaning, not just keywords
|
||||
- 🔍 **Hybrid Architecture** - Combines semantic similarity with keyword matching
|
||||
- ⏱️ **Recency Filtering** - Focus on recent 90 days for relevant context
|
||||
- ⚡ **Fast Performance** - Semantic search under 200ms with 8,000+ documents
|
||||
- 🔄 **Auto-Sync** - ChromaSync service keeps vectors updated automatically
|
||||
|
||||
## What is Chroma?
|
||||
|
||||
[ChromaDB](https://www.trychroma.com/) is an open-source vector database designed for AI applications. It stores text as **vector embeddings** - mathematical representations that capture semantic meaning.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Query: "authentication bug"
|
||||
Keyword Match: Must contain both "authentication" AND "bug"
|
||||
Semantic Match: Also finds "login error", "auth failure", "sign-in issue"
|
||||
```
|
||||
|
||||
Semantic search understands that "authentication bug" is conceptually similar to "login error" even though they share no keywords.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Hybrid Search Flow
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ User Query: "How does authentication work?" │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────┴─────────────────┐
|
||||
↓ ↓
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ Chroma Semantic │ │ SQLite FTS5 │
|
||||
│ Vector Similarity │ │ Keyword Search │
|
||||
│ │ │ │
|
||||
│ Finds conceptually │ │ Finds exact/fuzzy │
|
||||
│ similar observations │ │ keyword matches │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
↓ ↓
|
||||
└─────────────────┬─────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────┐
|
||||
│ Merge Results │
|
||||
│ - Deduplicate by ID │
|
||||
│ - Sort by relevance + recency │
|
||||
│ - Filter by 90-day window │
|
||||
└─────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────┐
|
||||
│ Return Top Matches │
|
||||
│ Semantic + Keyword combined │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### ChromaSync Service
|
||||
|
||||
The **ChromaSync** service (`src/services/sync/ChromaSync.ts`) automatically synchronizes observations to Chroma:
|
||||
|
||||
**When Observations Are Synced:**
|
||||
1. **Session Summary** - After each session completes, all new observations synced
|
||||
2. **Worker Startup** - On initialization, checks for unsynced observations
|
||||
3. **Manual Trigger** - Can force sync via internal API (development only)
|
||||
|
||||
**What Gets Embedded:**
|
||||
- Observation ID (unique identifier)
|
||||
- Title (compressed learning statement)
|
||||
- Narrative (detailed explanation)
|
||||
- Project path (for project-specific filtering)
|
||||
- Timestamp (for recency filtering)
|
||||
- Concepts (semantic tags)
|
||||
- File references (associated code files)
|
||||
|
||||
**Embedding Model:**
|
||||
- Currently using Chroma's default embedding function
|
||||
- Future: Configurable embedding models (e.g., OpenAI, sentence-transformers)
|
||||
|
||||
### Data Structure
|
||||
|
||||
**SQLite (Source of Truth):**
|
||||
```sql
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT,
|
||||
narrative TEXT,
|
||||
facts TEXT,
|
||||
concepts TEXT,
|
||||
files TEXT,
|
||||
type TEXT,
|
||||
projectPath TEXT,
|
||||
createdAt INTEGER
|
||||
);
|
||||
```
|
||||
|
||||
**Chroma (Vector Embeddings):**
|
||||
```typescript
|
||||
{
|
||||
ids: ["obs_12345"],
|
||||
embeddings: [[0.123, -0.456, ...]], // 384-dimensional vector
|
||||
documents: ["Title: Authentication flow\nNarrative: Implemented..."],
|
||||
metadatas: [{
|
||||
type: "feature",
|
||||
project: "claude-mem",
|
||||
timestamp: 1698765432000,
|
||||
concepts: "pattern,architecture"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## How Semantic Search Works
|
||||
|
||||
### Vector Embeddings
|
||||
|
||||
Text converted to high-dimensional vectors that capture meaning:
|
||||
|
||||
```
|
||||
"user authentication" → [0.12, -0.34, 0.56, ..., 0.78]
|
||||
"login system" → [0.15, -0.32, 0.54, ..., 0.81]
|
||||
"database schema" → [-0.45, 0.67, -0.23, ..., 0.12]
|
||||
```
|
||||
|
||||
Notice: "user authentication" and "login system" have similar vectors (close in vector space), while "database schema" is distant.
|
||||
|
||||
### Similarity Search
|
||||
|
||||
Chroma uses **cosine similarity** to find nearest neighbors:
|
||||
|
||||
```typescript
|
||||
// Query embedding
|
||||
query: "authentication bug"
|
||||
query_vector: [0.14, -0.33, 0.55, ..., 0.79]
|
||||
|
||||
// Find observations with similar vectors
|
||||
results = chroma.query(
|
||||
query_vector,
|
||||
n_results: 10,
|
||||
where: { timestamp: { $gte: now - 90_days } }
|
||||
)
|
||||
```
|
||||
|
||||
**Result Ranking:**
|
||||
- Higher cosine similarity = more semantically similar
|
||||
- Filtered by 90-day recency window
|
||||
- Combined with keyword matches from FTS5
|
||||
|
||||
## 90-Day Recency Filtering
|
||||
|
||||
Why 90 days?
|
||||
|
||||
**Rationale:**
|
||||
- Recent context more likely relevant to current work
|
||||
- Prevents very old observations from diluting results
|
||||
- Balances completeness with relevance
|
||||
- Reduces vector search space for faster queries
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
|
||||
|
||||
// Chroma metadata filter
|
||||
where: {
|
||||
timestamp: { $gte: ninetyDaysAgo }
|
||||
}
|
||||
|
||||
// SQLite WHERE clause
|
||||
WHERE createdAt >= ?
|
||||
```
|
||||
|
||||
**Configurable?**
|
||||
- Not currently user-configurable
|
||||
- Hard-coded in `src/servers/search-server.ts`
|
||||
- Future: Add `CLAUDE_MEM_RECENCY_DAYS` environment variable
|
||||
|
||||
## MCP Tool Integration
|
||||
|
||||
All 9 MCP search tools benefit from hybrid search:
|
||||
|
||||
### search_observations (Hybrid)
|
||||
|
||||
```typescript
|
||||
// Keyword-only (v4.x)
|
||||
search_observations(query: "authentication")
|
||||
// Returns: Observations containing "authentication"
|
||||
|
||||
// Hybrid semantic + keyword (v5.x)
|
||||
search_observations(query: "authentication")
|
||||
// Returns: Observations with "authentication" PLUS semantically similar:
|
||||
// - "login system"
|
||||
// - "user credentials"
|
||||
// - "session management"
|
||||
```
|
||||
|
||||
### get_timeline_by_query (Semantic-First)
|
||||
|
||||
```typescript
|
||||
// Uses Chroma to find best match, then builds timeline
|
||||
get_timeline_by_query(
|
||||
query: "when did we implement the viewer UI?",
|
||||
mode: "auto",
|
||||
depth_before: 10,
|
||||
depth_after: 10
|
||||
)
|
||||
|
||||
// Chroma finds: Observation #4057 "Web-Based Viewer UI for Real-Time Memory Stream"
|
||||
// Returns: Timeline with 10 observations before + anchor + 10 after
|
||||
```
|
||||
|
||||
### Benefits Across All Tools
|
||||
|
||||
- **find_by_concept**: Semantic similarity finds related concepts
|
||||
- **find_by_file**: Finds semantically similar code changes
|
||||
- **find_by_type**: Better relevance ranking within type
|
||||
- **get_recent_context**: Prioritizes semantically relevant recent context
|
||||
|
||||
## Performance
|
||||
|
||||
### Benchmarks (8,279 vector documents)
|
||||
|
||||
| Operation | Time | Notes |
|
||||
|-----------|------|-------|
|
||||
| **Semantic Query** | 150-200ms | 90-day window, top 10 results |
|
||||
| **Keyword Query (FTS5)** | 5-10ms | Full-text search |
|
||||
| **Hybrid Query** | 160-220ms | Combined semantic + keyword |
|
||||
| **Initial Sync** | 2-5 min | First-time embedding of all observations |
|
||||
| **Incremental Sync** | 100-500ms | 1-10 new observations per session |
|
||||
|
||||
### Memory Usage
|
||||
|
||||
- **Chroma DB Size**: ~50MB for 8,000 observations
|
||||
- **Embeddings**: 384 dimensions × 4 bytes = 1.5KB per observation
|
||||
- **Metadata**: ~500 bytes per observation (project, type, timestamp)
|
||||
- **Total**: ~2KB per observation in Chroma
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
1. **Reduce vector dimensions**: Use smaller embedding models (future)
|
||||
2. **Adjust recency window**: Narrow to 30/60 days for faster queries
|
||||
3. **Limit result count**: Request fewer results (n_results=5 vs 10)
|
||||
4. **Project filtering**: Add project filter to metadata query
|
||||
|
||||
## Installation & Dependencies
|
||||
|
||||
### Python Requirement
|
||||
|
||||
Chroma requires Python 3.7+ installed:
|
||||
|
||||
**Check Python:**
|
||||
```bash
|
||||
python3 --version
|
||||
# Should show: Python 3.7.x or higher
|
||||
```
|
||||
|
||||
**Install Python (if needed):**
|
||||
- **macOS**: `brew install python3`
|
||||
- **Windows**: Download from [python.org](https://www.python.org/downloads/)
|
||||
- **Linux**: `apt-get install python3` or `yum install python3`
|
||||
|
||||
### ChromaDB Installation
|
||||
|
||||
Chroma installed automatically as npm dependency:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# Installs: chromadb (Python package via node-gyp bindings)
|
||||
```
|
||||
|
||||
**Manual Installation (if auto-install fails):**
|
||||
```bash
|
||||
pip3 install chromadb
|
||||
```
|
||||
|
||||
### Troubleshooting Installation
|
||||
|
||||
**Error: "Python not found"**
|
||||
```bash
|
||||
# Set Python path explicitly
|
||||
export PYTHON=/usr/local/bin/python3
|
||||
npm install
|
||||
```
|
||||
|
||||
**Error: "chromadb module not found"**
|
||||
```bash
|
||||
# Reinstall chromadb
|
||||
pip3 install --upgrade chromadb
|
||||
|
||||
# Verify installation
|
||||
python3 -c "import chromadb; print(chromadb.__version__)"
|
||||
```
|
||||
|
||||
**Error: "node-gyp build failed"**
|
||||
```bash
|
||||
# Install build tools
|
||||
# macOS: xcode-select --install
|
||||
# Windows: npm install --global windows-build-tools
|
||||
# Linux: apt-get install build-essential
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Currently no user-configurable settings. Future options:
|
||||
|
||||
```json
|
||||
// Proposed for future versions
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_CHROMA_ENABLED": "true", // Enable/disable Chroma
|
||||
"CLAUDE_MEM_CHROMA_PATH": "~/.claude-mem/chroma", // DB location
|
||||
"CLAUDE_MEM_EMBEDDING_MODEL": "default", // Embedding model choice
|
||||
"CLAUDE_MEM_RECENCY_DAYS": "90", // Recency window
|
||||
"CLAUDE_MEM_VECTOR_DIM": "384" // Embedding dimensions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Disabling Chroma (Future)
|
||||
|
||||
To disable semantic search and use keyword-only:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_CHROMA_ENABLED": "false"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Falls back to SQLite FTS5 keyword search only.
|
||||
|
||||
## Database Maintenance
|
||||
|
||||
### Location
|
||||
|
||||
```
|
||||
~/.claude-mem/chroma/
|
||||
├── chroma.sqlite3 # Chroma metadata database
|
||||
└── index/ # Vector index files
|
||||
└── *.bin # Binary vector data
|
||||
```
|
||||
|
||||
### Backup
|
||||
|
||||
```bash
|
||||
# Backup entire Chroma directory
|
||||
cp -r ~/.claude-mem/chroma ~/.claude-mem/chroma.backup
|
||||
|
||||
# Restore from backup
|
||||
rm -rf ~/.claude-mem/chroma
|
||||
cp -r ~/.claude-mem/chroma.backup ~/.claude-mem/chroma
|
||||
```
|
||||
|
||||
### Reset Chroma (Force Resync)
|
||||
|
||||
```bash
|
||||
# Delete Chroma database
|
||||
rm -rf ~/.claude-mem/chroma
|
||||
|
||||
# Restart worker to trigger full resync
|
||||
npm run worker:restart
|
||||
|
||||
# Check logs for sync progress
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
**Note**: Resync can take 2-5 minutes for thousands of observations.
|
||||
|
||||
### Disk Space Management
|
||||
|
||||
**Chroma grows with observations:**
|
||||
- 1,000 observations ≈ 5MB
|
||||
- 10,000 observations ≈ 50MB
|
||||
- 100,000 observations ≈ 500MB
|
||||
|
||||
**Cleanup old observations:**
|
||||
```sql
|
||||
-- Delete observations older than 1 year
|
||||
-- This will trigger Chroma resync on next startup
|
||||
sqlite3 ~/.claude-mem/claude-mem.db \
|
||||
"DELETE FROM observations WHERE createdAt < strftime('%s', 'now', '-1 year') * 1000;"
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Direct Chroma Queries (Development)
|
||||
|
||||
For debugging or custom queries:
|
||||
|
||||
```typescript
|
||||
import { ChromaSync } from './services/sync/ChromaSync';
|
||||
|
||||
const sync = new ChromaSync();
|
||||
await sync.initialize();
|
||||
|
||||
// Query Chroma directly
|
||||
const results = await sync.query({
|
||||
queryTexts: ["authentication implementation"],
|
||||
nResults: 10,
|
||||
where: {
|
||||
type: "feature",
|
||||
timestamp: { $gte: Date.now() - 90_days }
|
||||
}
|
||||
});
|
||||
|
||||
console.log(results.ids, results.distances, results.documents);
|
||||
```
|
||||
|
||||
### Custom Embedding Models (Future)
|
||||
|
||||
Chroma supports multiple embedding models:
|
||||
|
||||
```typescript
|
||||
// Future configuration
|
||||
const sync = new ChromaSync({
|
||||
embeddingModel: "sentence-transformers/all-MiniLM-L6-v2", // Smaller, faster
|
||||
// or: "text-embedding-ada-002" (OpenAI, requires API key)
|
||||
// or: "all-mpnet-base-v2" (Higher quality, slower)
|
||||
});
|
||||
```
|
||||
|
||||
### Metadata Filtering
|
||||
|
||||
Chroma supports advanced metadata queries:
|
||||
|
||||
```typescript
|
||||
// Find observations by type and project
|
||||
results = await sync.query({
|
||||
queryTexts: ["API design"],
|
||||
where: {
|
||||
$and: [
|
||||
{ type: { $in: ["decision", "feature"] } },
|
||||
{ project: "claude-mem" }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Find recent observations
|
||||
results = await sync.query({
|
||||
queryTexts: ["database schema"],
|
||||
where: {
|
||||
timestamp: { $gte: Date.now() - 30_days }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Comparison: Semantic vs Keyword Search
|
||||
|
||||
| Aspect | Semantic (Chroma) | Keyword (FTS5) |
|
||||
|--------|-------------------|----------------|
|
||||
| **Speed** | 150-200ms | 5-10ms |
|
||||
| **Accuracy** | High (meaning-based) | Medium (exact match) |
|
||||
| **Storage** | ~2KB per observation | ~500 bytes per observation |
|
||||
| **Conceptual Matching** | ✅ Yes | ❌ No |
|
||||
| **Exact Match** | ❌ Not guaranteed | ✅ Always |
|
||||
| **Typo Tolerance** | ✅ High | ⚠️ Limited (fuzzy) |
|
||||
| **Dependencies** | Python + chromadb | None (SQLite built-in) |
|
||||
| **Recency Bias** | ✅ Built-in (90 days) | Manual filtering |
|
||||
|
||||
**Best Practice:** Use hybrid search (both) for optimal results.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Chroma not found" Error
|
||||
|
||||
**Symptom:** Worker logs show "Chroma not available, using keyword-only search"
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check Python installation
|
||||
python3 --version
|
||||
|
||||
# Reinstall chromadb
|
||||
pip3 install chromadb
|
||||
|
||||
# Restart worker
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
### Slow Query Performance
|
||||
|
||||
**Symptom:** Searches taking >1 second
|
||||
|
||||
**Solutions:**
|
||||
1. Reduce recency window (edit `src/servers/search-server.ts`)
|
||||
2. Limit result count (`nResults: 5` instead of 10)
|
||||
3. Add project filter to narrow search space
|
||||
4. Check Chroma index size (may need rebuild)
|
||||
|
||||
### Out of Memory Errors
|
||||
|
||||
**Symptom:** Worker crashes with "JavaScript heap out of memory"
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Increase Node.js heap size
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
# Restart worker
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
### Sync Taking Too Long
|
||||
|
||||
**Symptom:** Initial Chroma sync takes >10 minutes
|
||||
|
||||
**Possible Causes:**
|
||||
- Large number of observations (>10,000)
|
||||
- Slow embedding model
|
||||
- Limited CPU resources
|
||||
|
||||
**Solutions:**
|
||||
1. Let it complete (one-time cost)
|
||||
2. Delete very old observations to reduce count
|
||||
3. Close resource-intensive apps during sync
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for future versions:
|
||||
|
||||
- **Configurable Recency**: User-defined recency window (30/60/90/365 days)
|
||||
- **Custom Embeddings**: Choose embedding model (quality vs speed trade-off)
|
||||
- **Incremental Updates**: Update existing vectors instead of full resync
|
||||
- **Semantic Filters**: Search by semantic concept ("all architectural decisions")
|
||||
- **Multi-Language Support**: Embeddings optimized for non-English code/docs
|
||||
- **Clustering**: Auto-cluster related observations for discovery
|
||||
- **Visualization**: 2D/3D visualization of vector space (similar observations near each other)
|
||||
|
||||
## Resources
|
||||
|
||||
- **ChromaDB Documentation**: https://docs.trychroma.com/
|
||||
- **Source Code**: `src/services/sync/ChromaSync.ts`
|
||||
- **Search Server**: `src/servers/search-server.ts`
|
||||
- **Python Package**: https://pypi.org/project/chromadb/
|
||||
|
||||
---
|
||||
|
||||
**Powered by ChromaDB** | **Hybrid Semantic + Keyword Search** | **90-Day Recency Window**
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
# Viewer UI - Web-Based Memory Stream Visualization
|
||||
|
||||
## Overview
|
||||
|
||||
The Claude-Mem Viewer UI is a production-ready web interface that provides real-time visualization of your memory stream. Access it at **http://localhost:37777** while the claude-mem worker is running.
|
||||
|
||||
**Key Features:**
|
||||
- 🔴 **Real-time Updates** - Server-Sent Events (SSE) stream new observations, sessions, and prompts instantly
|
||||
- 📜 **Infinite Scroll** - Load historical data progressively with automatic pagination
|
||||
- 🎯 **Project Filtering** - Focus on specific codebases with smart project selection
|
||||
- 🎨 **Theme Toggle** - Light, dark, or system preference with persistent settings
|
||||
- 💾 **Settings Persistence** - Sidebar state and project filters saved automatically
|
||||
- 🔄 **Auto-Reconnection** - Exponential backoff ensures connection stability
|
||||
- ⚡ **GPU Acceleration** - Smooth animations and transitions
|
||||
|
||||
## Architecture
|
||||
|
||||
### Technology Stack
|
||||
|
||||
| Component | Technology | Purpose |
|
||||
|-----------|-----------|---------|
|
||||
| **Framework** | React + TypeScript | Component-based UI with type safety |
|
||||
| **Build System** | esbuild | Self-contained HTML bundle (no separate assets) |
|
||||
| **Real-time** | Server-Sent Events (SSE) | Push-based updates from worker service |
|
||||
| **State Management** | React hooks | Local state with custom hooks for SSE, pagination, settings |
|
||||
| **Styling** | Inline CSS | No external stylesheets, fully self-contained |
|
||||
| **Typography** | Monaspace Radon | Embedded monospace font for code aesthetics |
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
src/ui/viewer/
|
||||
├── App.tsx # Main application component
|
||||
├── types.ts # TypeScript interfaces
|
||||
├── components/
|
||||
│ ├── Header.tsx # Top navigation with logo and theme toggle
|
||||
│ ├── Sidebar.tsx # Project filter and stats sidebar
|
||||
│ ├── Feed.tsx # Main feed with infinite scroll
|
||||
│ ├── ThemeToggle.tsx # Light/dark/system theme selector
|
||||
│ └── cards/
|
||||
│ ├── ObservationCard.tsx # Displays individual observations
|
||||
│ ├── SummaryCard.tsx # Displays session summaries
|
||||
│ ├── PromptCard.tsx # Displays user prompts
|
||||
│ └── SkeletonCard.tsx # Loading placeholder
|
||||
├── hooks/
|
||||
│ ├── useSSE.ts # Server-Sent Events connection
|
||||
│ ├── usePagination.ts # Infinite scroll logic
|
||||
│ ├── useSettings.ts # Settings persistence
|
||||
│ ├── useStats.ts # Database statistics
|
||||
│ └── useTheme.ts # Theme management
|
||||
└── utils/
|
||||
├── constants.ts # Configuration constants
|
||||
├── data.ts # Data merging and deduplication
|
||||
└── formatters.ts # Date/time formatting helpers
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Worker Service (port 37777) │
|
||||
│ - Express HTTP API │
|
||||
│ - SSE endpoint: /stream │
|
||||
│ - REST endpoints: /api/* │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Viewer UI (React App) │
|
||||
│ - useSSE hook: Real-time stream │
|
||||
│ - usePagination hook: Historical data │
|
||||
│ - useSettings hook: Persistent preferences │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Feed Component │
|
||||
│ - Merges real-time + paginated data │
|
||||
│ - Deduplicates by ID │
|
||||
│ - Filters by selected project │
|
||||
│ - Infinite scroll triggers pagination │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Features In Detail
|
||||
|
||||
### Real-Time Updates (SSE)
|
||||
|
||||
The viewer uses Server-Sent Events to receive updates instantly:
|
||||
|
||||
```typescript
|
||||
// SSE message format
|
||||
{
|
||||
"type": "observation" | "summary" | "prompt" | "projects" | "processing",
|
||||
"data": { /* record data */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Event Types:**
|
||||
- `observation` - New observation created
|
||||
- `summary` - Session summary generated
|
||||
- `prompt` - User prompt captured
|
||||
- `projects` - Project list updated
|
||||
- `processing` - Session processing status changed
|
||||
|
||||
**Connection Management:**
|
||||
- Auto-reconnect on disconnect with exponential backoff
|
||||
- Visual connection status indicator in header
|
||||
- Graceful degradation if SSE unavailable
|
||||
|
||||
### Infinite Scroll Pagination
|
||||
|
||||
The feed loads historical data progressively:
|
||||
|
||||
1. **Initial Load**: First 20 records loaded on mount
|
||||
2. **Scroll Trigger**: When user scrolls to 80% of feed height
|
||||
3. **Batch Load**: Next 20 records fetched via `/api/{type}?offset=X&limit=20`
|
||||
4. **Deduplication**: Merges with real-time data, removes duplicates by ID
|
||||
5. **Loading State**: Skeleton cards show while fetching
|
||||
|
||||
**Performance:**
|
||||
- Requests debounced to prevent spam
|
||||
- Only visible when scrolled near bottom
|
||||
- Continues until no more records available
|
||||
|
||||
### Project Filtering
|
||||
|
||||
Filter memory stream by specific projects:
|
||||
|
||||
1. Projects extracted from observations, summaries, and prompts
|
||||
2. Sidebar shows all unique project names with counts
|
||||
3. Click project name to filter feed
|
||||
4. Click "All Projects" to clear filter
|
||||
5. Filter persisted to localStorage
|
||||
|
||||
**Project Detection:**
|
||||
- Extracted from `projectPath` or `project` field in records
|
||||
- Basename of path used as project name
|
||||
- Empty/null projects shown as "(No Project)"
|
||||
|
||||
### Theme Toggle (v5.1.2)
|
||||
|
||||
Three theme modes available:
|
||||
|
||||
- **Light Mode**: Clean white background, dark text
|
||||
- **Dark Mode**: Dark background, light text (default)
|
||||
- **System**: Matches OS preference automatically
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Theme preference stored in localStorage
|
||||
localStorage.setItem('theme-preference', 'light' | 'dark' | 'system');
|
||||
|
||||
// CSS variables updated dynamically
|
||||
document.documentElement.setAttribute('data-theme', resolvedTheme);
|
||||
```
|
||||
|
||||
**CSS Variables:**
|
||||
```css
|
||||
:root[data-theme="light"] {
|
||||
--bg-primary: #ffffff;
|
||||
--text-primary: #1f2937;
|
||||
/* ... */
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--bg-primary: #111827;
|
||||
--text-primary: #f9fafb;
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Persistence
|
||||
|
||||
Settings automatically saved to worker service:
|
||||
|
||||
**Saved Settings:**
|
||||
- `sidebarOpen` - Sidebar expanded/collapsed state
|
||||
- `selectedProject` - Current project filter
|
||||
- `theme` - Theme preference (light/dark/system)
|
||||
|
||||
**API Endpoints:**
|
||||
- `GET /api/settings` - Retrieve saved settings
|
||||
- `POST /api/settings` - Save settings (debounced 500ms)
|
||||
|
||||
**Local Fallback:**
|
||||
- If API unavailable, settings stored in localStorage
|
||||
- Synced back to API when connection restored
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Opening the Viewer
|
||||
|
||||
1. Ensure claude-mem worker is running (auto-starts with Claude Code)
|
||||
2. Open browser to http://localhost:37777
|
||||
3. Viewer loads automatically with recent records
|
||||
|
||||
### Navigating the Feed
|
||||
|
||||
**Cards Displayed:**
|
||||
- **Observation Cards** (blue accent) - Tool usage observations with title, narrative, concepts, files
|
||||
- **Summary Cards** (green accent) - Session summaries with request, completion, learnings
|
||||
- **Prompt Cards** (purple accent) - Raw user prompts with timestamp and project
|
||||
|
||||
**Card Features:**
|
||||
- Click to expand/collapse full details
|
||||
- Type indicators (🔴 bugfix, 🟣 feature, 🔄 refactor, etc.)
|
||||
- Concept tags (clickable for future filtering)
|
||||
- File references with paths
|
||||
- Timestamps in relative format ("2 hours ago")
|
||||
|
||||
### Using Project Filters
|
||||
|
||||
1. **Open Sidebar**: Click hamburger menu (☰) in top-left
|
||||
2. **View Stats**: See total observations, sessions, prompts
|
||||
3. **Select Project**: Click project name to filter
|
||||
4. **View Counts**: Numbers show records per project
|
||||
5. **Clear Filter**: Click "All Projects" to reset
|
||||
|
||||
### Changing Theme
|
||||
|
||||
1. **Open Theme Toggle**: Click theme icon in header
|
||||
2. **Select Mode**:
|
||||
- ☀️ Light mode
|
||||
- 🌙 Dark mode
|
||||
- 💻 System (follows OS)
|
||||
3. **Auto-Save**: Preference saved immediately
|
||||
4. **Smooth Transition**: CSS transitions between themes
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Viewer Not Loading:**
|
||||
```bash
|
||||
# Check worker status
|
||||
npm run worker:logs
|
||||
|
||||
# Restart worker
|
||||
npm run worker:restart
|
||||
|
||||
# Check if port 37777 is available
|
||||
lsof -i :37777
|
||||
```
|
||||
|
||||
**SSE Connection Issues:**
|
||||
- Check browser console for connection errors
|
||||
- Verify no proxy/firewall blocking EventSource
|
||||
- Auto-reconnect attempts every 1-5s with exponential backoff
|
||||
|
||||
**Theme Not Persisting:**
|
||||
- Check localStorage: `localStorage.getItem('theme-preference')`
|
||||
- Verify `/api/settings` endpoint responding
|
||||
- Clear browser cache if stale
|
||||
|
||||
**Infinite Scroll Not Triggering:**
|
||||
- Scroll to 80% of feed height
|
||||
- Check browser console for fetch errors
|
||||
- Verify `/api/{type}` endpoints responding with data
|
||||
|
||||
## Development
|
||||
|
||||
### Building the Viewer
|
||||
|
||||
```bash
|
||||
# Build viewer UI
|
||||
npm run build
|
||||
|
||||
# Output: plugin/ui/viewer.html (self-contained)
|
||||
```
|
||||
|
||||
### Adding New Features
|
||||
|
||||
**Example: Add a new card component**
|
||||
|
||||
1. Create component:
|
||||
```typescript
|
||||
// src/ui/viewer/components/cards/MyCard.tsx
|
||||
export function MyCard({ data }: { data: MyData }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">{data.title}</div>
|
||||
<div className="card-body">{data.content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
2. Add to Feed component:
|
||||
```typescript
|
||||
// src/ui/viewer/components/Feed.tsx
|
||||
import { MyCard } from './cards/MyCard';
|
||||
|
||||
// In render:
|
||||
{myData.map(item => <MyCard key={item.id} data={item} />)}
|
||||
```
|
||||
|
||||
3. Rebuild:
|
||||
```bash
|
||||
npm run build
|
||||
npm run sync-marketplace
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
### Testing Changes
|
||||
|
||||
1. Make changes to `src/ui/viewer/`
|
||||
2. Rebuild: `npm run build`
|
||||
3. Restart worker: `npm run worker:restart`
|
||||
4. Refresh browser (http://localhost:37777)
|
||||
5. Check browser console for errors
|
||||
|
||||
## API Integration
|
||||
|
||||
The viewer consumes these worker service endpoints:
|
||||
|
||||
### Data Retrieval
|
||||
|
||||
```typescript
|
||||
// Get paginated observations
|
||||
GET /api/observations?offset=0&limit=20&project=myproject
|
||||
Response: { observations: Observation[], hasMore: boolean }
|
||||
|
||||
// Get paginated summaries
|
||||
GET /api/summaries?offset=0&limit=20&project=myproject
|
||||
Response: { summaries: Summary[], hasMore: boolean }
|
||||
|
||||
// Get paginated prompts
|
||||
GET /api/prompts?offset=0&limit=20&project=myproject
|
||||
Response: { prompts: UserPrompt[], hasMore: boolean }
|
||||
|
||||
// Get database stats
|
||||
GET /api/stats
|
||||
Response: { totalObservations: number, totalSessions: number, ... }
|
||||
```
|
||||
|
||||
### Real-Time Stream
|
||||
|
||||
```typescript
|
||||
// Server-Sent Events stream
|
||||
GET /stream
|
||||
|
||||
// Message format:
|
||||
event: observation
|
||||
data: {"type":"observation","data":{...}}
|
||||
|
||||
event: summary
|
||||
data: {"type":"summary","data":{...}}
|
||||
```
|
||||
|
||||
### Settings
|
||||
|
||||
```typescript
|
||||
// Get settings
|
||||
GET /api/settings
|
||||
Response: { sidebarOpen: boolean, selectedProject: string, ... }
|
||||
|
||||
// Save settings
|
||||
POST /api/settings
|
||||
Body: { sidebarOpen: boolean, selectedProject: string, ... }
|
||||
Response: { success: boolean }
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Bundle Size
|
||||
- Self-contained HTML: ~150KB (gzipped)
|
||||
- No external dependencies loaded at runtime
|
||||
- Monaspace Radon font embedded (subset)
|
||||
|
||||
### Memory Management
|
||||
- Virtualization: Only renders visible cards
|
||||
- Deduplication: Prevents duplicate records in memory
|
||||
- Cleanup: Old records beyond pagination limit pruned
|
||||
|
||||
### Network Efficiency
|
||||
- SSE: Single long-lived connection for real-time updates
|
||||
- REST: Paginated requests (20 records per batch)
|
||||
- Debouncing: Settings saves debounced 500ms
|
||||
|
||||
### Rendering Performance
|
||||
- React.memo: Cards memoized to prevent unnecessary re-renders
|
||||
- useMemo: Data merging/filtering memoized
|
||||
- CSS transitions: GPU-accelerated for smooth animations
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential features for future versions:
|
||||
|
||||
- **Search**: Full-text search across observations, summaries, prompts
|
||||
- **Export**: Download data as JSON, CSV, or markdown
|
||||
- **Charts**: Visualize observation frequency, types, concepts over time
|
||||
- **Keyboard Shortcuts**: Navigate feed, toggle sidebar, switch themes
|
||||
- **Notifications**: Browser notifications for important observations
|
||||
- **Dark/Light Auto-Schedule**: Auto-switch theme based on time of day
|
||||
- **Custom Themes**: User-defined color schemes
|
||||
- **Multi-Project Views**: Compare multiple projects side-by-side
|
||||
|
||||
## Resources
|
||||
|
||||
- **Source Code**: `src/ui/viewer/`
|
||||
- **Built Output**: `plugin/ui/viewer.html`
|
||||
- **Worker Service**: `src/services/worker-service.ts`
|
||||
- **Build Script**: `scripts/build-viewer.js`
|
||||
- **Documentation**: This file
|
||||
|
||||
---
|
||||
|
||||
**Built with React + TypeScript** | **Powered by Server-Sent Events** | **Self-Contained HTML Bundle**
|
||||
+295
-27
@@ -1,4 +1,4 @@
|
||||
# Architecture Evolution: The Journey from v3 to v4
|
||||
# Architecture Evolution: The Journey from v3 to v5
|
||||
|
||||
## The Problem We Solved
|
||||
|
||||
@@ -10,6 +10,246 @@ This is the story of how claude-mem evolved from a simple idea to a production-r
|
||||
|
||||
---
|
||||
|
||||
## v5.x: Maturity and User Experience
|
||||
|
||||
After establishing the solid v4 architecture, v5.x focused on user experience, visualization, and polish.
|
||||
|
||||
### v5.1.2: Theme Toggle (November 2025)
|
||||
|
||||
**What Changed**: Added light/dark mode theme toggle to viewer UI
|
||||
|
||||
**New Features**:
|
||||
- User-selectable theme preference (light, dark, system)
|
||||
- Persistent theme settings in localStorage
|
||||
- Smooth theme transitions
|
||||
- System preference detection
|
||||
|
||||
**Implementation**:
|
||||
```typescript
|
||||
// Theme context with persistence
|
||||
const ThemeProvider = ({ children }) => {
|
||||
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>(() => {
|
||||
return localStorage.getItem('claude-mem-theme') || 'system';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('claude-mem-theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Why It Matters**: Users working in different lighting conditions can now customize the viewer for comfort.
|
||||
|
||||
### v5.1.1: PM2 Windows Fix (November 2025)
|
||||
|
||||
**The Problem**: PM2 startup failed on Windows with ENOENT error
|
||||
|
||||
**Root Cause**:
|
||||
```typescript
|
||||
// ❌ Failed on Windows - PM2 not in PATH
|
||||
execSync('pm2 start ecosystem.config.cjs');
|
||||
```
|
||||
|
||||
**The Fix**:
|
||||
```typescript
|
||||
// ✅ Use full path to PM2 binary
|
||||
const PM2_PATH = join(PLUGIN_ROOT, 'node_modules', '.bin', 'pm2');
|
||||
execSync(`"${PM2_PATH}" start "${ECOSYSTEM_CONFIG}"`);
|
||||
```
|
||||
|
||||
**Impact**: Cross-platform compatibility restored, Windows users can now use claude-mem without issues.
|
||||
|
||||
### v5.1.0: Web-Based Viewer UI (October 2025)
|
||||
|
||||
**The Breakthrough**: Real-time visualization of memory stream
|
||||
|
||||
**What We Built**:
|
||||
- React-based web UI at http://localhost:37777
|
||||
- Server-Sent Events (SSE) for real-time updates
|
||||
- Infinite scroll pagination
|
||||
- Project filtering
|
||||
- Settings persistence (sidebar state, selected project)
|
||||
- Auto-reconnection with exponential backoff
|
||||
- GPU-accelerated animations
|
||||
|
||||
**New Worker Endpoints** (8 additions):
|
||||
```
|
||||
GET / # Serves viewer HTML
|
||||
GET /stream # SSE real-time updates
|
||||
GET /api/prompts # Paginated user prompts
|
||||
GET /api/observations # Paginated observations
|
||||
GET /api/summaries # Paginated session summaries
|
||||
GET /api/stats # Database statistics
|
||||
GET /api/settings # User settings
|
||||
POST /api/settings # Save settings
|
||||
```
|
||||
|
||||
**Database Enhancements**:
|
||||
```typescript
|
||||
// New SessionStore methods for viewer
|
||||
getRecentPrompts(limit, offset, project?)
|
||||
getRecentObservations(limit, offset, project?)
|
||||
getRecentSummaries(limit, offset, project?)
|
||||
getStats()
|
||||
getUniqueProjects()
|
||||
```
|
||||
|
||||
**React Architecture**:
|
||||
```
|
||||
src/ui/viewer/
|
||||
├── components/
|
||||
│ ├── Header.tsx # Navigation + stats
|
||||
│ ├── Sidebar.tsx # Project filter
|
||||
│ ├── Feed.tsx # Infinite scroll
|
||||
│ └── cards/
|
||||
│ ├── ObservationCard.tsx
|
||||
│ ├── PromptCard.tsx
|
||||
│ ├── SummaryCard.tsx
|
||||
│ └── SkeletonCard.tsx
|
||||
├── hooks/
|
||||
│ ├── useSSE.ts # Real-time events
|
||||
│ ├── usePagination.ts # Infinite scroll
|
||||
│ ├── useSettings.ts # Persistence
|
||||
│ └── useStats.ts # Statistics
|
||||
└── utils/
|
||||
├── merge.ts # Data deduplication
|
||||
└── format.ts # Display formatting
|
||||
```
|
||||
|
||||
**Build Process**:
|
||||
```typescript
|
||||
// esbuild bundles everything into single HTML file
|
||||
esbuild.build({
|
||||
entryPoints: ['src/ui/viewer/index.tsx'],
|
||||
bundle: true,
|
||||
outfile: 'plugin/ui/viewer.html',
|
||||
loader: { '.tsx': 'tsx', '.woff2': 'dataurl' },
|
||||
define: { 'process.env.NODE_ENV': '"production"' },
|
||||
});
|
||||
```
|
||||
|
||||
**Why It Matters**: Users can now see exactly what's being captured in real-time, making the memory system transparent and debuggable.
|
||||
|
||||
### v5.0.3: Smart Install Caching (October 2025)
|
||||
|
||||
**The Problem**: `npm install` ran on every SessionStart (2-5 seconds)
|
||||
|
||||
**The Insight**: Dependencies rarely change between sessions
|
||||
|
||||
**The Solution**: Version-based caching
|
||||
```typescript
|
||||
// Check version marker before installing
|
||||
const currentVersion = getPackageVersion();
|
||||
const installedVersion = readFileSync('.install-version', 'utf-8');
|
||||
|
||||
if (currentVersion !== installedVersion) {
|
||||
// Only install if version changed
|
||||
await runNpmInstall();
|
||||
writeFileSync('.install-version', currentVersion);
|
||||
}
|
||||
```
|
||||
|
||||
**Cached Check Logic**:
|
||||
1. Does `node_modules` exist?
|
||||
2. Does `.install-version` match `package.json` version?
|
||||
3. Is `better-sqlite3` present?
|
||||
|
||||
**Impact**:
|
||||
- SessionStart hook: 2-5 seconds → 10ms (99.5% faster)
|
||||
- Only installs on: first run, version change, missing deps
|
||||
- Better Windows error messages with build tool help
|
||||
|
||||
### v5.0.2: Worker Health Checks (October 2025)
|
||||
|
||||
**What Changed**: More robust worker startup and monitoring
|
||||
|
||||
**New Features**:
|
||||
```typescript
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
uptime: process.uptime(),
|
||||
port: WORKER_PORT,
|
||||
memory: process.memoryUsage(),
|
||||
});
|
||||
});
|
||||
|
||||
// Smart worker startup
|
||||
async function ensureWorkerHealthy() {
|
||||
const healthy = await isWorkerHealthy(1000);
|
||||
if (!healthy) {
|
||||
await startWorker();
|
||||
await waitForWorkerHealth(10000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Graceful degradation when worker is down
|
||||
- Auto-recovery from crashes
|
||||
- Better error messages for debugging
|
||||
|
||||
### v5.0.1: Stability Improvements (October 2025)
|
||||
|
||||
**What Changed**: Various bug fixes and stability enhancements
|
||||
|
||||
**Key Fixes**:
|
||||
- Fixed race conditions in observation queue processing
|
||||
- Improved error handling in SDK worker
|
||||
- Better cleanup of stale PM2 processes
|
||||
- Enhanced logging for debugging
|
||||
|
||||
### v5.0.0: Hybrid Search Architecture (October 2025)
|
||||
|
||||
**The Evolution**: SQLite FTS5 + Chroma vector search
|
||||
|
||||
**What We Added**:
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ HYBRID SEARCH │
|
||||
│ │
|
||||
│ Text Query → SQLite FTS5 (keyword matching) │
|
||||
│ ↓ │
|
||||
│ Chroma Vector Search (semantic) │
|
||||
│ ↓ │
|
||||
│ Merge + Re-rank Results │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**New Dependencies**:
|
||||
- `chromadb` - Vector database for semantic search
|
||||
- Python 3.8+ - Required by chromadb
|
||||
|
||||
**MCP Tools Enhancement**:
|
||||
```typescript
|
||||
// Chroma-backed semantic search
|
||||
search_observations({
|
||||
query: "authentication bug",
|
||||
useSemanticSearch: true // Uses Chroma
|
||||
});
|
||||
|
||||
// Falls back to FTS5 if Chroma unavailable
|
||||
```
|
||||
|
||||
**Why Hybrid**:
|
||||
- FTS5: Fast keyword matching, no dependencies
|
||||
- Chroma: Semantic understanding, finds related concepts
|
||||
- Graceful degradation: Works without Chroma (FTS5 only)
|
||||
|
||||
**Trade-offs**:
|
||||
- Added Python dependency (optional)
|
||||
- Increased installation complexity
|
||||
- Better search relevance
|
||||
|
||||
---
|
||||
|
||||
## v1-v2: The Naive Approach
|
||||
|
||||
### The First Attempt: Dump Everything
|
||||
@@ -698,7 +938,7 @@ createObservation({
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide: v3 → v4
|
||||
## Migration Guide: v3 → v5
|
||||
|
||||
### Step 1: Backup Database
|
||||
|
||||
@@ -713,36 +953,45 @@ cd ~/.claude/plugins/marketplaces/thedotmack
|
||||
git pull
|
||||
```
|
||||
|
||||
### Step 3: Run Migration
|
||||
### Step 3: Update Plugin
|
||||
|
||||
```bash
|
||||
npx tsx src/services/sqlite/migrations/v3-to-v4.ts
|
||||
/plugin update claude-mem
|
||||
```
|
||||
|
||||
**What the migration does:**
|
||||
- Adds new columns to observations table
|
||||
- Creates FTS5 virtual tables
|
||||
- Sets up auto-sync triggers
|
||||
- Migrates existing observations to new schema
|
||||
**What happens automatically:**
|
||||
- Dependencies update (including new ones like chromadb for v5.0.0+)
|
||||
- Database schema migrations run automatically
|
||||
- Worker service restarts with new code
|
||||
- Smart install caching activates (v5.0.3+)
|
||||
|
||||
### Step 4: Restart Worker
|
||||
|
||||
```bash
|
||||
pm2 restart claude-mem-worker
|
||||
pm2 logs claude-mem-worker
|
||||
```
|
||||
|
||||
### Step 5: Test
|
||||
### Step 4: Test
|
||||
|
||||
```bash
|
||||
# Start Claude Code
|
||||
claude
|
||||
|
||||
# Check that context is injected
|
||||
# (Should see progressive disclosure index)
|
||||
# (Should see progressive disclosure index with v5 viewer link)
|
||||
|
||||
# Submit a prompt and check observations
|
||||
pm2 logs claude-mem-worker --nostream
|
||||
# Open viewer UI (v5.1.0+)
|
||||
open http://localhost:37777
|
||||
|
||||
# Submit a prompt and watch real-time updates in viewer
|
||||
```
|
||||
|
||||
### Step 5: Explore New Features
|
||||
|
||||
```bash
|
||||
# View memory stream in browser (v5.1.0+)
|
||||
open http://localhost:37777
|
||||
|
||||
# Toggle theme (v5.1.2+)
|
||||
# Click theme button in viewer header
|
||||
|
||||
# Check worker health
|
||||
npm run worker:status
|
||||
curl http://localhost:37777/health
|
||||
```
|
||||
|
||||
---
|
||||
@@ -767,17 +1016,34 @@ pm2 logs claude-mem-worker --nostream
|
||||
| Hook execution time | ~45ms |
|
||||
| Search latency | ~15ms (FTS5) |
|
||||
|
||||
**Improvements:**
|
||||
### v5 Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Context usage per session | ~1,100 tokens |
|
||||
| Relevant context | ~1,100 tokens (100%) |
|
||||
| Hook execution time | ~10ms (cached install) |
|
||||
| Search latency | ~12ms (FTS5) or ~25ms (hybrid) |
|
||||
| Viewer UI load time | ~50ms (bundled HTML) |
|
||||
| SSE update latency | ~5ms (real-time) |
|
||||
|
||||
**v3 → v4 Improvements:**
|
||||
- 96% reduction in context waste
|
||||
- 12x increase in relevance
|
||||
- 4x faster hooks
|
||||
- 33x faster search
|
||||
|
||||
**v4 → v5 Improvements:**
|
||||
- 78% faster hooks (smart caching)
|
||||
- Real-time visualization (viewer UI)
|
||||
- Better search relevance (hybrid)
|
||||
- Enhanced UX (theme toggle, persistence)
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The journey from v3 to v4 was about understanding these fundamental truths:
|
||||
The journey from v3 to v5 was about understanding these fundamental truths:
|
||||
|
||||
1. **Context is finite** - Progressive disclosure respects attention budget
|
||||
2. **AI is the compressor** - Semantic understanding beats keyword extraction
|
||||
@@ -787,15 +1053,17 @@ The journey from v3 to v4 was about understanding these fundamental truths:
|
||||
|
||||
The result is a memory system that's both powerful and invisible. Users never notice it working - Claude just gets smarter over time.
|
||||
|
||||
**v5 adds visibility**: Now users CAN see the memory system working if they want (via viewer UI), but it's still non-intrusive.
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Progressive Disclosure](/docs/progressive-disclosure) - The philosophy behind v4
|
||||
- [Hooks Architecture](/docs/hooks-architecture) - How hooks power the system
|
||||
- [Context Engineering](/docs/context-engineering) - Foundational principles
|
||||
- [v4.0.0 Release Notes](/CHANGELOG.md#v400) - Full changelog
|
||||
- [Progressive Disclosure](progressive-disclosure) - The philosophy behind v4
|
||||
- [Hooks Architecture](hooks-architecture) - How hooks power the system
|
||||
- [Context Engineering](context-engineering) - Foundational principles
|
||||
- [Viewer UI](VIEWER) - Real-time visualization (v5.1.0+)
|
||||
|
||||
---
|
||||
|
||||
*This architecture evolution reflects hundreds of hours of experimentation, dozens of dead ends, and the invaluable experience of real-world usage. v4 is the architecture that emerged from understanding what actually works.*
|
||||
*This architecture evolution reflects hundreds of hours of experimentation, dozens of dead ends, and the invaluable experience of real-world usage. v5 is the architecture that emerged from understanding what actually works - and making it visible to users.*
|
||||
|
||||
+80
-13
@@ -1,17 +1,19 @@
|
||||
---
|
||||
title: "Plugin Hooks"
|
||||
description: "5 lifecycle hooks that power Claude-Mem"
|
||||
description: "7 hook scripts that power Claude-Mem"
|
||||
---
|
||||
|
||||
# Plugin Hooks
|
||||
|
||||
Claude-Mem integrates with Claude Code through 5 lifecycle hooks that capture events and inject context.
|
||||
Claude-Mem integrates with Claude Code through 7 hook scripts across 5 lifecycle events that capture events and inject context.
|
||||
|
||||
## Hook Overview
|
||||
|
||||
| Hook Name | Purpose | Timeout | Script |
|
||||
|---------------------|--------------------------------------|---------|-------------------------|
|
||||
| SessionStart | Inject context from previous sessions| 120s | context-hook.js |
|
||||
| SessionStart | Smart dependency installation | 300s | smart-install.js |
|
||||
| SessionStart | Inject context from previous sessions| 300s | context-hook.js |
|
||||
| SessionStart | Display first-time setup message | 10s | user-message-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 |
|
||||
@@ -26,10 +28,15 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
"description": "Claude-mem memory system hooks",
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"matcher": "startup|clear|compact",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=silent && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/../scripts/smart-install.js\" && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 300
|
||||
}, {
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js",
|
||||
"timeout": 10
|
||||
}]
|
||||
}],
|
||||
"UserPromptSubmit": [{
|
||||
@@ -65,15 +72,48 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
}
|
||||
```
|
||||
|
||||
## 1. SessionStart Hook (`context-hook.js`)
|
||||
## 1. SessionStart Hook - Smart Install (`smart-install.js`)
|
||||
|
||||
**Purpose**: Intelligently manage dependencies and ensure worker service is running.
|
||||
|
||||
**Behavior**:
|
||||
- Checks if dependencies need installation using version marker (`.install-version`)
|
||||
- Only runs npm install when:
|
||||
- First-time installation (no node_modules)
|
||||
- Version changed in package.json
|
||||
- Critical dependency missing (e.g., better-sqlite3)
|
||||
- Provides Windows-specific error messages for build tool issues
|
||||
- Auto-starts PM2 worker service after installation
|
||||
- Fast when already installed (~10ms vs 2-5 seconds)
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"source": "startup"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `scripts/smart-install.js`
|
||||
|
||||
**Key Features**:
|
||||
- Version caching prevents redundant installs
|
||||
- Cross-platform compatible (Windows, macOS, Linux)
|
||||
- Helpful error messages with troubleshooting steps
|
||||
- Non-blocking worker startup
|
||||
|
||||
**v5.0.3 Enhancement**: Smart caching eliminates 2-5 second npm install on every SessionStart, reducing to ~10ms for already-installed dependencies.
|
||||
|
||||
## 2. SessionStart Hook - Context Injection (`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)
|
||||
- Retrieves last 50 observations (configurable via `CLAUDE_MEM_CONTEXT_OBSERVATIONS`)
|
||||
- Returns context via `hookSpecificOutput` in JSON format (fixed in v4.1.0)
|
||||
- Formats results as progressive disclosure index
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
@@ -93,9 +133,36 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
|
||||
**Implementation**: `src/hooks/context-hook.ts`
|
||||
|
||||
**v4.3.1 Fix**: Changed npm install to use `--loglevel=silent` instead of `--loglevel=error` to prevent output pollution that was breaking JSON context injection.
|
||||
## 3. SessionStart Hook - User Message (`user-message-hook.js`)
|
||||
|
||||
## 2. UserPromptSubmit Hook (`new-hook.js`)
|
||||
**Purpose**: Display helpful user messages during first-time setup or when viewing context.
|
||||
|
||||
**Behavior**:
|
||||
- Shows first-time setup message when node_modules is missing
|
||||
- Displays formatted context information with colors
|
||||
- Provides tips for using claude-mem effectively
|
||||
- Shows link to web viewer UI (http://localhost:37777)
|
||||
- Exits with code 3 (informational, not error)
|
||||
|
||||
**Output Example**:
|
||||
```
|
||||
📝 Claude-Mem Context Loaded
|
||||
ℹ️ Note: This appears as stderr but is informational only
|
||||
|
||||
[Context details...]
|
||||
|
||||
📺 Watch live in browser http://localhost:37777/ (New! v5.1)
|
||||
```
|
||||
|
||||
**Implementation**: `plugin/scripts/user-message-hook.js` (minified)
|
||||
|
||||
**Key Features**:
|
||||
- User-friendly first-time setup experience
|
||||
- Visual context display with colors
|
||||
- Links to new features (viewer UI)
|
||||
- Non-intrusive messaging
|
||||
|
||||
## 4. UserPromptSubmit Hook (`new-hook.js`)
|
||||
|
||||
**Purpose**: Create new session records and initialize session tracking.
|
||||
|
||||
@@ -116,7 +183,7 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
|
||||
**Implementation**: `src/hooks/new-hook.ts`
|
||||
|
||||
## 3. PostToolUse Hook (`save-hook.js`)
|
||||
## 5. PostToolUse Hook (`save-hook.js`)
|
||||
|
||||
**Purpose**: Capture tool execution observations.
|
||||
|
||||
@@ -140,7 +207,7 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
|
||||
**Implementation**: `src/hooks/save-hook.ts`
|
||||
|
||||
## 4. Stop Hook (`summary-hook.js`)
|
||||
## 6. Stop Hook (`summary-hook.js`)
|
||||
|
||||
**Purpose**: Generate session summaries when Claude stops.
|
||||
|
||||
@@ -160,7 +227,7 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
|
||||
**Implementation**: `src/hooks/summary-hook.ts`
|
||||
|
||||
## 5. SessionEnd Hook (`cleanup-hook.js`)
|
||||
## 7. SessionEnd Hook (`cleanup-hook.js`)
|
||||
|
||||
**Purpose**: Mark sessions as completed (graceful cleanup as of v4.1.0).
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
---
|
||||
title: "MCP Search Server"
|
||||
description: "7 search tools with examples and usage patterns"
|
||||
description: "9 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.
|
||||
Claude-Mem includes a Model Context Protocol (MCP) server that exposes 9 specialized search tools for querying stored observations and sessions.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Location**: `src/servers/search-server.ts`
|
||||
- **Built Output**: `plugin/scripts/search-server.mjs`
|
||||
- **Configuration**: `plugin/.mcp.json`
|
||||
- **Transport**: stdio
|
||||
- **Tools**: 7 specialized search functions
|
||||
- **Tools**: 9 specialized search functions
|
||||
- **Citations**: All results use `claude-mem://` URI scheme
|
||||
|
||||
## Configuration
|
||||
@@ -24,13 +25,13 @@ The MCP server is automatically registered via `plugin/.mcp.json`:
|
||||
"mcpServers": {
|
||||
"claude-mem-search": {
|
||||
"type": "stdio",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.js"
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.mjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
This registers the `claude-mem-search` server with Claude Code, making the 9 search tools available in all sessions. The server is automatically started when Claude Code launches and communicates via stdio transport.
|
||||
|
||||
## Search Tools
|
||||
|
||||
@@ -163,6 +164,133 @@ Get recent session context including summaries and observations for a project.
|
||||
get_recent_context with limit=5
|
||||
```
|
||||
|
||||
### 8. get_context_timeline
|
||||
|
||||
Get a unified timeline of context (observations, sessions, and prompts) around a specific point in time. All record types are interleaved chronologically.
|
||||
|
||||
**Parameters**:
|
||||
- `anchor` (required): Anchor point - observation ID, session ID (e.g., "S123"), or ISO timestamp
|
||||
- `depth_before` (default: 10): Number of records to retrieve before anchor (max: 50)
|
||||
- `depth_after` (default: 10): Number of records to retrieve after anchor (max: 50)
|
||||
- `project`: Filter by project name
|
||||
|
||||
**Return Format**:
|
||||
Returns `depth_before` records + anchor + `depth_after` records, all interleaved chronologically. Total records: `depth_before + 1 + depth_after`.
|
||||
|
||||
**Use Case**: Understanding "what was happening when X occurred"
|
||||
|
||||
**Example**:
|
||||
```
|
||||
# Timeline around observation #123
|
||||
get_context_timeline with anchor=123 and depth_before=5 and depth_after=5
|
||||
|
||||
# Timeline around a session
|
||||
get_context_timeline with anchor="S456" and depth_before=10 and depth_after=10
|
||||
|
||||
# Timeline around a timestamp
|
||||
get_context_timeline with anchor="2025-11-06T10:30:00Z" and depth_before=15 and depth_after=5
|
||||
```
|
||||
|
||||
**Response Structure**:
|
||||
```json
|
||||
{
|
||||
"timeline": [
|
||||
{
|
||||
"type": "observation",
|
||||
"id": 120,
|
||||
"title": "Context before",
|
||||
"created_at": "2025-11-06T10:25:00Z"
|
||||
},
|
||||
{
|
||||
"type": "user-prompt",
|
||||
"id": 45,
|
||||
"prompt": "User request",
|
||||
"created_at": "2025-11-06T10:28:00Z"
|
||||
},
|
||||
{
|
||||
"type": "observation",
|
||||
"id": 123,
|
||||
"title": "Anchor observation",
|
||||
"created_at": "2025-11-06T10:30:00Z",
|
||||
"isAnchor": true
|
||||
},
|
||||
{
|
||||
"type": "session",
|
||||
"id": "S456",
|
||||
"request": "Session summary",
|
||||
"created_at": "2025-11-06T10:32:00Z"
|
||||
}
|
||||
],
|
||||
"anchor": {
|
||||
"type": "observation",
|
||||
"id": 123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. get_timeline_by_query
|
||||
|
||||
Search for observations using natural language and get timeline context around the best match. Combines search + timeline into a single operation.
|
||||
|
||||
**Parameters**:
|
||||
- `query` (required): Natural language search query to find relevant observations
|
||||
- `mode` (default: "auto"): Operation mode
|
||||
- `"auto"`: Automatically use top search result as timeline anchor
|
||||
- `"interactive"`: Return top N search results for manual anchor selection
|
||||
- `depth_before` (default: 10): Number of timeline records before anchor (max: 50)
|
||||
- `depth_after` (default: 10): Number of timeline records after anchor (max: 50)
|
||||
- `limit` (default: 5): For interactive mode - number of top search results to display (max: 20)
|
||||
- `project`: Filter by project name
|
||||
|
||||
**Use Case**: Faster context discovery - "show me what happened around when we fixed the authentication bug"
|
||||
|
||||
**Example - Auto Mode**:
|
||||
```
|
||||
# Automatically find and show timeline for "authentication bug"
|
||||
get_timeline_by_query with query="authentication bug" and mode="auto" and depth_before=10 and depth_after=10
|
||||
```
|
||||
|
||||
**Example - Interactive Mode**:
|
||||
```
|
||||
# Show top 5 matches, let user choose anchor
|
||||
get_timeline_by_query with query="authentication bug" and mode="interactive" and limit=5
|
||||
```
|
||||
|
||||
**Auto Mode Response**:
|
||||
```json
|
||||
{
|
||||
"search_result": {
|
||||
"id": 123,
|
||||
"title": "Fix authentication bug",
|
||||
"relevance": 0.95
|
||||
},
|
||||
"timeline": [
|
||||
/* timeline records before and after observation 123 */
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Interactive Mode Response**:
|
||||
```json
|
||||
{
|
||||
"top_results": [
|
||||
{
|
||||
"id": 123,
|
||||
"title": "Fix authentication bug",
|
||||
"relevance": 0.95,
|
||||
"created_at": "2025-11-06T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"id": 98,
|
||||
"title": "Authentication refactor",
|
||||
"relevance": 0.82,
|
||||
"created_at": "2025-11-05T14:20:00Z"
|
||||
}
|
||||
],
|
||||
"message": "Select an observation ID to view its timeline context"
|
||||
}
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
All search tools support two output formats:
|
||||
@@ -252,6 +380,12 @@ search_user_prompts with query="authentication"
|
||||
|
||||
# Get recent context for debugging
|
||||
get_recent_context with limit=5
|
||||
|
||||
# Timeline around a specific observation
|
||||
get_context_timeline with anchor=123 and depth_before=10 and depth_after=10
|
||||
|
||||
# Quick timeline search for authentication work
|
||||
get_timeline_by_query with query="authentication bug" and mode="auto"
|
||||
```
|
||||
|
||||
## Implementation
|
||||
@@ -277,7 +411,7 @@ If search tools are not available in Claude Code sessions:
|
||||
|
||||
2. Verify search server is built:
|
||||
```bash
|
||||
ls -l plugin/scripts/search-server.js
|
||||
ls -l plugin/scripts/search-server.mjs
|
||||
```
|
||||
|
||||
3. Rebuild if needed:
|
||||
|
||||
@@ -7,12 +7,13 @@ description: "System components and data flow in Claude-Mem"
|
||||
|
||||
## System Components
|
||||
|
||||
Claude-Mem operates as a Claude Code plugin with four core components:
|
||||
Claude-Mem operates as a Claude Code plugin with five core components:
|
||||
|
||||
1. **Plugin Hooks** - Capture lifecycle events
|
||||
1. **Plugin Hooks** - Capture lifecycle events (7 hook files)
|
||||
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
|
||||
4. **MCP Search Server** - Query historical context (9 search tools)
|
||||
5. **Viewer UI** - Web-based real-time memory stream visualization
|
||||
|
||||
## Technology Stack
|
||||
|
||||
@@ -21,7 +22,10 @@ Claude-Mem operates as a Claude Code plugin with four core components:
|
||||
| **Language** | TypeScript (ES2022, ESNext modules) |
|
||||
| **Runtime** | Node.js 18+ |
|
||||
| **Database** | SQLite 3 with better-sqlite3 driver |
|
||||
| **Vector Store** | ChromaDB (optional, for semantic search) |
|
||||
| **HTTP Server** | Express.js 4.18 |
|
||||
| **Real-time** | Server-Sent Events (SSE) |
|
||||
| **UI Framework** | React + TypeScript |
|
||||
| **AI SDK** | @anthropic-ai/claude-agent-sdk |
|
||||
| **Build Tool** | esbuild (bundles TypeScript) |
|
||||
| **Process Manager** | PM2 |
|
||||
@@ -55,18 +59,24 @@ Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Se
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 0. Smart Install Hook Fires │
|
||||
│ Checks dependencies (cached), only runs on version changes │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. Session Starts → Context Hook Fires │
|
||||
│ Injects summaries from last 3 sessions into Claude's context │
|
||||
│ Starts PM2 worker if needed, injects context from previous │
|
||||
│ sessions (configurable observation count) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. User Types Prompt → UserPromptSubmit Hook Fires │
|
||||
│ Creates SDK session in database, notifies worker service │
|
||||
│ Creates session in database, saves raw user prompt for FTS5 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. Claude Uses Tools → PostToolUse Hook Fires (100+ times) │
|
||||
│ Sends observations to worker service for processing │
|
||||
│ Captures tool executions, sends to worker for AI compression │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
@@ -75,13 +85,14 @@ Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Se
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 5. Claude Stops → Stop Hook Fires │
|
||||
│ Generates final summary with request, status, next steps │
|
||||
│ 5. Claude Stops → Summary Hook Fires │
|
||||
│ Generates final summary with request, completions, learnings │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 6. Session Ends → Cleanup Hook Fires │
|
||||
│ Marks session complete, ready for next session context │
|
||||
│ Marks session complete (graceful, not DELETE), ready for │
|
||||
│ next session context. Skips on /clear to preserve ongoing │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -90,15 +101,17 @@ Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Se
|
||||
```
|
||||
claude-mem/
|
||||
├── src/
|
||||
│ ├── hooks/ # Hook implementations (v4.3.1+ consolidated)
|
||||
│ ├── hooks/ # Hook implementations (7 hooks)
|
||||
│ │ ├── smart-install.ts # Dependency check (cached)
|
||||
│ │ ├── context-hook.ts # SessionStart
|
||||
│ │ ├── user-message-hook.ts # UserMessage (for debugging)
|
||||
│ │ ├── new-hook.ts # UserPromptSubmit
|
||||
│ │ ├── save-hook.ts # PostToolUse
|
||||
│ │ ├── summary-hook.ts # Stop
|
||||
│ │ └── cleanup-hook.ts # SessionEnd
|
||||
│ │
|
||||
│ ├── servers/ # MCP servers
|
||||
│ │ └── search-server.ts # MCP search tools server
|
||||
│ │ └── search-server.ts # MCP search tools server (9 tools)
|
||||
│ │
|
||||
│ ├── sdk/ # Claude Agent SDK integration
|
||||
│ │ ├── prompts.ts # XML prompt builders
|
||||
@@ -106,13 +119,20 @@ claude-mem/
|
||||
│ │ └── worker.ts # Main SDK agent loop
|
||||
│ │
|
||||
│ ├── services/
|
||||
│ │ ├── worker-service.ts # Express HTTP service
|
||||
│ │ ├── worker-service.ts # Express HTTP + SSE service
|
||||
│ │ └── sqlite/ # Database layer
|
||||
│ │ ├── SessionStore.ts # CRUD operations
|
||||
│ │ ├── SessionSearch.ts # FTS5 search service
|
||||
│ │ ├── migrations.ts
|
||||
│ │ └── types.ts
|
||||
│ │
|
||||
│ ├── ui/ # Viewer UI
|
||||
│ │ └── viewer/ # React + TypeScript web interface
|
||||
│ │ ├── components/ # UI components
|
||||
│ │ ├── hooks/ # React hooks
|
||||
│ │ ├── utils/ # Utilities
|
||||
│ │ └── assets/ # Fonts, logos
|
||||
│ │
|
||||
│ ├── shared/ # Shared utilities
|
||||
│ │ ├── config.ts
|
||||
│ │ ├── paths.ts
|
||||
@@ -129,14 +149,19 @@ claude-mem/
|
||||
│ ├── .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
|
||||
│ ├── scripts/ # Built executables
|
||||
│ │ ├── smart-install.js
|
||||
│ │ ├── context-hook.js
|
||||
│ │ ├── user-message-hook.js
|
||||
│ │ ├── new-hook.js
|
||||
│ │ ├── save-hook.js
|
||||
│ │ ├── summary-hook.js
|
||||
│ │ ├── cleanup-hook.js
|
||||
│ │ ├── worker-service.cjs # Background worker
|
||||
│ │ └── search-server.mjs # MCP search server
|
||||
│ │
|
||||
│ └── ui/ # Built viewer UI
|
||||
│ └── viewer.html # Self-contained bundle
|
||||
│
|
||||
├── tests/ # Test suite
|
||||
├── docs/ # Documentation
|
||||
@@ -145,14 +170,49 @@ claude-mem/
|
||||
|
||||
## Component Details
|
||||
|
||||
### 1. Plugin Hooks
|
||||
### 1. Plugin Hooks (7 Hooks)
|
||||
- **smart-install.js** - Cached dependency checker (only runs on version changes)
|
||||
- **context-hook.js** - SessionStart: Starts PM2 worker, injects context
|
||||
- **user-message-hook.js** - UserMessage: Debugging hook
|
||||
- **new-hook.js** - UserPromptSubmit: Creates session, saves prompt
|
||||
- **save-hook.js** - PostToolUse: Captures tool executions
|
||||
- **summary-hook.js** - Stop: Generates session summary
|
||||
- **cleanup-hook.js** - SessionEnd: Marks session complete
|
||||
|
||||
See [Plugin Hooks](/architecture/hooks) for detailed hook documentation.
|
||||
|
||||
### 2. Worker Service
|
||||
Express.js HTTP server on port 37777 (configurable) with:
|
||||
- 8 HTTP/SSE endpoints for viewer UI
|
||||
- Async observation processing via Claude Agent SDK
|
||||
- Real-time updates via Server-Sent Events
|
||||
- Auto-managed by PM2 process manager
|
||||
|
||||
See [Worker Service](/architecture/worker-service) for HTTP API and endpoints.
|
||||
|
||||
### 3. Database Layer
|
||||
SQLite3 with better-sqlite3 driver featuring:
|
||||
- FTS5 virtual tables for full-text search
|
||||
- SessionStore for CRUD operations
|
||||
- SessionSearch for FTS5 queries
|
||||
- Location: `~/.claude-mem/claude-mem.db`
|
||||
|
||||
See [Database Architecture](/architecture/database) for schema and FTS5 search.
|
||||
|
||||
### 4. MCP Search Server
|
||||
### 4. MCP Search Server (9 Tools)
|
||||
Provides 9 specialized search tools:
|
||||
- search_observations, search_sessions, search_user_prompts
|
||||
- find_by_concept, find_by_file, find_by_type
|
||||
- get_recent_context, get_context_timeline, get_timeline_by_query
|
||||
|
||||
See [MCP Search Server](/architecture/mcp-search) for search tools and examples.
|
||||
|
||||
### 5. Viewer UI
|
||||
React + TypeScript web interface at http://localhost:37777 featuring:
|
||||
- Real-time memory stream via Server-Sent Events
|
||||
- Infinite scroll pagination with automatic deduplication
|
||||
- Project filtering and settings persistence
|
||||
- GPU-accelerated animations
|
||||
- Self-contained HTML bundle (viewer.html)
|
||||
|
||||
Built with esbuild into a single file deployment.
|
||||
|
||||
@@ -18,13 +18,33 @@ The worker service is a long-running HTTP API built with Express.js and managed
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
The worker service exposes 6 HTTP endpoints:
|
||||
The worker service exposes 14 HTTP endpoints organized into four categories:
|
||||
|
||||
### 1. Health Check
|
||||
### Viewer & Health Endpoints
|
||||
|
||||
#### 1. Viewer UI
|
||||
```
|
||||
GET /
|
||||
```
|
||||
|
||||
**Purpose**: Serves the web-based viewer UI (v5.1.0+)
|
||||
|
||||
**Response**: HTML page with embedded React application
|
||||
|
||||
**Features**:
|
||||
- Real-time memory stream visualization
|
||||
- Infinite scroll pagination
|
||||
- Project filtering
|
||||
- SSE-based live updates
|
||||
- Theme toggle (light/dark mode) as of v5.1.2
|
||||
|
||||
#### 2. Health Check
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
**Purpose**: Worker health status check
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
@@ -34,7 +54,182 @@ GET /health
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Initialize Session
|
||||
#### 3. Server-Sent Events Stream
|
||||
```
|
||||
GET /stream
|
||||
```
|
||||
|
||||
**Purpose**: Real-time updates for viewer UI
|
||||
|
||||
**Response**: SSE stream with events:
|
||||
- `observation-created`: New observation added
|
||||
- `session-summary-created`: New summary generated
|
||||
- `user-prompt-created`: New prompt recorded
|
||||
|
||||
**Event Format**:
|
||||
```
|
||||
event: observation-created
|
||||
data: {"id": 123, "title": "...", ...}
|
||||
```
|
||||
|
||||
### Data Retrieval Endpoints
|
||||
|
||||
#### 4. Get Prompts
|
||||
```
|
||||
GET /api/prompts?project=my-project&limit=20&offset=0
|
||||
```
|
||||
|
||||
**Purpose**: Retrieve paginated user prompts
|
||||
|
||||
**Query Parameters**:
|
||||
- `project` (optional): Filter by project name
|
||||
- `limit` (default: 20): Number of results
|
||||
- `offset` (default: 0): Pagination offset
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"prompts": [{
|
||||
"id": 1,
|
||||
"session_id": "abc123",
|
||||
"prompt": "User's prompt text",
|
||||
"prompt_number": 1,
|
||||
"created_at": "2025-11-06T10:30:00Z"
|
||||
}],
|
||||
"total": 150,
|
||||
"hasMore": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Get Observations
|
||||
```
|
||||
GET /api/observations?project=my-project&limit=20&offset=0
|
||||
```
|
||||
|
||||
**Purpose**: Retrieve paginated observations
|
||||
|
||||
**Query Parameters**:
|
||||
- `project` (optional): Filter by project name
|
||||
- `limit` (default: 20): Number of results
|
||||
- `offset` (default: 0): Pagination offset
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"observations": [{
|
||||
"id": 123,
|
||||
"title": "Fix authentication bug",
|
||||
"type": "bugfix",
|
||||
"narrative": "...",
|
||||
"created_at": "2025-11-06T10:30:00Z"
|
||||
}],
|
||||
"total": 500,
|
||||
"hasMore": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. Get Summaries
|
||||
```
|
||||
GET /api/summaries?project=my-project&limit=20&offset=0
|
||||
```
|
||||
|
||||
**Purpose**: Retrieve paginated session summaries
|
||||
|
||||
**Query Parameters**:
|
||||
- `project` (optional): Filter by project name
|
||||
- `limit` (default: 20): Number of results
|
||||
- `offset` (default: 0): Pagination offset
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"summaries": [{
|
||||
"id": 456,
|
||||
"session_id": "abc123",
|
||||
"request": "User's original request",
|
||||
"completed": "Work finished",
|
||||
"created_at": "2025-11-06T10:30:00Z"
|
||||
}],
|
||||
"total": 100,
|
||||
"hasMore": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. Get Stats
|
||||
```
|
||||
GET /api/stats
|
||||
```
|
||||
|
||||
**Purpose**: Get database statistics by project
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"byProject": {
|
||||
"my-project": {
|
||||
"observations": 245,
|
||||
"summaries": 12,
|
||||
"prompts": 48
|
||||
},
|
||||
"other-project": {
|
||||
"observations": 156,
|
||||
"summaries": 8,
|
||||
"prompts": 32
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"observations": 401,
|
||||
"summaries": 20,
|
||||
"prompts": 80,
|
||||
"sessions": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Endpoints
|
||||
|
||||
#### 8. Get Settings
|
||||
```
|
||||
GET /api/settings
|
||||
```
|
||||
|
||||
**Purpose**: Retrieve user settings
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"sidebarOpen": true,
|
||||
"selectedProject": "my-project",
|
||||
"theme": "dark"
|
||||
}
|
||||
```
|
||||
|
||||
#### 9. Save Settings
|
||||
```
|
||||
POST /api/settings
|
||||
```
|
||||
|
||||
**Purpose**: Persist user settings
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"sidebarOpen": false,
|
||||
"selectedProject": "other-project",
|
||||
"theme": "light"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### Session Management Endpoints
|
||||
|
||||
#### 10. Initialize Session
|
||||
```
|
||||
POST /sessions/:sessionDbId/init
|
||||
```
|
||||
@@ -55,7 +250,7 @@ POST /sessions/:sessionDbId/init
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Add Observation
|
||||
#### 11. Add Observation
|
||||
```
|
||||
POST /sessions/:sessionDbId/observations
|
||||
```
|
||||
@@ -78,7 +273,7 @@ POST /sessions/:sessionDbId/observations
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Generate Summary
|
||||
#### 12. Generate Summary
|
||||
```
|
||||
POST /sessions/:sessionDbId/summarize
|
||||
```
|
||||
@@ -98,7 +293,7 @@ POST /sessions/:sessionDbId/summarize
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Session Status
|
||||
#### 13. Session Status
|
||||
```
|
||||
GET /sessions/:sessionDbId/status
|
||||
```
|
||||
@@ -113,7 +308,7 @@ GET /sessions/:sessionDbId/status
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Delete Session
|
||||
#### 14. Delete Session
|
||||
```
|
||||
DELETE /sessions/:sessionDbId
|
||||
```
|
||||
|
||||
+62
-20
@@ -7,14 +7,16 @@ description: "Environment variables and settings for Claude-Mem"
|
||||
|
||||
## 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 |
|
||||
| Variable | Default | Description |
|
||||
|-------------------------------|---------------------------------|---------------------------------------|
|
||||
| `CLAUDE_PLUGIN_ROOT` | Set by Claude Code | Plugin installation directory |
|
||||
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem/` | Data directory (production default) |
|
||||
| `CLAUDE_CODE_PATH` | Auto-detected | Path to Claude Code CLI (for Windows) |
|
||||
| `CLAUDE_MEM_WORKER_PORT` | `37777` | Worker service port |
|
||||
| `CLAUDE_MEM_MODEL` | `claude-sonnet-4-5` | AI model for processing observations |
|
||||
| `CLAUDE_MEM_CONTEXT_OBSERVATIONS` | `50` | Number of observations to inject |
|
||||
| `NODE_ENV` | `production` | Environment mode |
|
||||
| `FORCE_COLOR` | `1` | Enable colored logs |
|
||||
|
||||
## Model Configuration
|
||||
|
||||
@@ -49,9 +51,14 @@ Edit `~/.claude/settings.json`:
|
||||
|
||||
### Data Directory Structure
|
||||
|
||||
The data directory location depends on the environment:
|
||||
- **Production (installed plugin)**: `~/.claude-mem/` (always, regardless of CLAUDE_PLUGIN_ROOT)
|
||||
- **Development**: Can be overridden with `CLAUDE_MEM_DATA_DIR`
|
||||
|
||||
```
|
||||
~/.claude-mem/
|
||||
├── claude-mem.db # SQLite database
|
||||
├── .install-version # Cached version for smart installer
|
||||
├── worker.port # Current worker port file
|
||||
└── logs/
|
||||
├── worker-out.log # Worker stdout logs
|
||||
@@ -67,14 +74,17 @@ ${CLAUDE_PLUGIN_ROOT}/
|
||||
├── .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
|
||||
├── scripts/ # Built executables
|
||||
│ ├── smart-install.js # Smart installer script
|
||||
│ ├── context-hook.js # Context injection hook
|
||||
│ ├── new-hook.js # Session creation hook
|
||||
│ ├── save-hook.js # Observation capture hook
|
||||
│ ├── summary-hook.js # Summary generation hook
|
||||
│ ├── cleanup-hook.js # Session cleanup hook
|
||||
│ ├── worker-service.cjs # Worker service (CJS)
|
||||
│ └── search-server.mjs # MCP search server (ESM)
|
||||
└── ui/
|
||||
└── viewer.html # Web viewer UI bundle
|
||||
```
|
||||
|
||||
## Plugin Configuration
|
||||
@@ -90,7 +100,7 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
"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",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
@@ -136,13 +146,13 @@ The MCP search server is configured in `plugin/.mcp.json`:
|
||||
"mcpServers": {
|
||||
"claude-mem-search": {
|
||||
"type": "stdio",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.js"
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.mjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This registers the `claude-mem-search` server with Claude Code, making the 7 search tools available in all sessions.
|
||||
This registers the `claude-mem-search` server with Claude Code, making the 9 search tools available in all sessions.
|
||||
|
||||
## PM2 Configuration
|
||||
|
||||
@@ -172,6 +182,36 @@ module.exports = {
|
||||
- **watch**: false (no file watching)
|
||||
- **max_memory_restart**: 1G (restart if memory exceeds 1GB)
|
||||
|
||||
## Context Injection Configuration
|
||||
|
||||
### CLAUDE_MEM_CONTEXT_OBSERVATIONS
|
||||
|
||||
Controls how many observations are injected into each new session for context continuity.
|
||||
|
||||
**Default**: 50 observations
|
||||
|
||||
**What it does**:
|
||||
- Fetches the most recent N observations from the database
|
||||
- Injects them as context at SessionStart
|
||||
- Allows Claude to maintain awareness of recent work across sessions
|
||||
|
||||
**Configuration** in `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "100"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Considerations**:
|
||||
- **Higher values** = More context but slower SessionStart and more tokens used
|
||||
- **Lower values** = Faster SessionStart but less historical awareness
|
||||
- Default of 50 balances context richness with performance
|
||||
|
||||
**Note**: This injects individual observations, not entire sessions. Each observation represents a single tool execution (Read, Write, Edit, etc.) that was compressed into a semantic learning.
|
||||
|
||||
## Customization
|
||||
|
||||
### Custom Data Directory
|
||||
@@ -213,12 +253,14 @@ Modify timeouts in `plugin/hooks/hooks.json`:
|
||||
```
|
||||
|
||||
Recommended values:
|
||||
- SessionStart: 120s (needs time for npm install and context retrieval)
|
||||
- SessionStart: 120s (needs time for smart install check and context retrieval)
|
||||
- UserPromptSubmit: 60s
|
||||
- PostToolUse: 120s (can process many observations)
|
||||
- Stop: 60s
|
||||
- SessionEnd: 60s
|
||||
|
||||
**Note**: With smart install caching (v5.0.3+), SessionStart is typically very fast (10ms) unless dependencies need installation.
|
||||
|
||||
### Worker Memory Limit
|
||||
|
||||
Modify PM2 memory limit in `ecosystem.config.cjs`:
|
||||
|
||||
+112
-2
@@ -33,13 +33,16 @@ 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`
|
||||
3. Bundles MCP search server to `plugin/scripts/search-server.mjs`
|
||||
4. Bundles worker service to `plugin/scripts/worker-service.cjs`
|
||||
5. Bundles web viewer UI to `plugin/ui/viewer.html`
|
||||
|
||||
**Build Output**:
|
||||
- Hook executables: `*-hook.js` (ESM format)
|
||||
- Smart installer: `smart-install.js` (ESM format)
|
||||
- Worker service: `worker-service.cjs` (CJS format)
|
||||
- Search server: `search-server.js` (ESM format)
|
||||
- Search server: `search-server.mjs` (ESM format)
|
||||
- Viewer UI: `viewer.html` (self-contained HTML bundle)
|
||||
|
||||
### Build Scripts
|
||||
|
||||
@@ -66,6 +69,8 @@ src/
|
||||
├── servers/ # MCP search server
|
||||
├── sdk/ # Claude Agent SDK integration
|
||||
├── shared/ # Shared utilities
|
||||
├── ui/
|
||||
│ └── viewer/ # React web viewer UI components
|
||||
└── utils/ # General utilities
|
||||
```
|
||||
|
||||
@@ -111,6 +116,111 @@ echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plug
|
||||
|
||||
Repeat steps 1-4 until your changes work as expected.
|
||||
|
||||
## Viewer UI Development
|
||||
|
||||
### Working with the React Viewer
|
||||
|
||||
The web viewer UI is a React application built into a self-contained HTML bundle.
|
||||
|
||||
**Location**: `src/ui/viewer/`
|
||||
|
||||
**Structure**:
|
||||
```
|
||||
src/ui/viewer/
|
||||
├── index.tsx # Entry point
|
||||
├── App.tsx # Main application component
|
||||
├── components/ # React components
|
||||
│ ├── Header.tsx # Header with logo and actions
|
||||
│ ├── Sidebar.tsx # Project filter sidebar
|
||||
│ ├── Feed.tsx # Main feed with infinite scroll
|
||||
│ ├── cards/ # Card components
|
||||
│ │ ├── ObservationCard.tsx
|
||||
│ │ ├── PromptCard.tsx
|
||||
│ │ ├── SummaryCard.tsx
|
||||
│ │ └── SkeletonCard.tsx
|
||||
├── hooks/ # Custom React hooks
|
||||
│ ├── useSSE.ts # Server-Sent Events connection
|
||||
│ ├── usePagination.ts # Infinite scroll pagination
|
||||
│ ├── useSettings.ts # Settings persistence
|
||||
│ └── useStats.ts # Database statistics
|
||||
├── utils/ # Utilities
|
||||
│ ├── constants.ts # Constants (API URLs, etc.)
|
||||
│ ├── formatters.ts # Date/time formatting
|
||||
│ └── merge.ts # Data merging and deduplication
|
||||
└── assets/ # Static assets (fonts, logos)
|
||||
```
|
||||
|
||||
### Building Viewer UI
|
||||
|
||||
```bash
|
||||
# Build everything including viewer
|
||||
npm run build
|
||||
|
||||
# The viewer is built to plugin/ui/viewer.html
|
||||
# It's a self-contained HTML file with inlined JS and CSS
|
||||
```
|
||||
|
||||
### Testing Viewer Changes
|
||||
|
||||
1. Make changes to React components in `src/ui/viewer/`
|
||||
2. Build: `npm run build`
|
||||
3. Sync to installed plugin: `npm run sync-marketplace`
|
||||
4. Restart worker: `npm run worker:restart`
|
||||
5. Refresh browser at http://localhost:37777
|
||||
|
||||
**Hot Reload**: Not currently supported. Full rebuild + restart required for changes.
|
||||
|
||||
### Adding New Viewer Features
|
||||
|
||||
**Example: Adding a new card type**
|
||||
|
||||
1. Create component in `src/ui/viewer/components/cards/YourCard.tsx`:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
|
||||
export interface YourCardProps {
|
||||
// Your data structure
|
||||
}
|
||||
|
||||
export const YourCard: React.FC<YourCardProps> = ({ ... }) => {
|
||||
return (
|
||||
<div className="card">
|
||||
{/* Your UI */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
2. Import and use in `Feed.tsx`:
|
||||
|
||||
```tsx
|
||||
import { YourCard } from './cards/YourCard';
|
||||
|
||||
// In render logic:
|
||||
{item.type === 'your_type' && <YourCard {...item} />}
|
||||
```
|
||||
|
||||
3. Update types if needed in `src/ui/viewer/types.ts`
|
||||
|
||||
4. Rebuild and test
|
||||
|
||||
### Viewer UI Architecture
|
||||
|
||||
**Data Flow**:
|
||||
1. Worker service exposes HTTP + SSE endpoints
|
||||
2. React app fetches initial data via HTTP (paginated)
|
||||
3. SSE connection provides real-time updates
|
||||
4. Custom hooks handle state management and data merging
|
||||
5. Components render cards based on item type
|
||||
|
||||
**Key Patterns**:
|
||||
- **Infinite Scroll**: `usePagination` hook with Intersection Observer
|
||||
- **Real-Time Updates**: `useSSE` hook with auto-reconnection
|
||||
- **Deduplication**: `merge.ts` utilities prevent duplicate items
|
||||
- **Settings Persistence**: `useSettings` hook with localStorage
|
||||
- **Theme Support**: CSS variables with light/dark/system themes
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### Adding a New Hook
|
||||
|
||||
+124
-41
@@ -15,15 +15,15 @@ Claude-Mem is fundamentally a **hook-driven system**. Every piece of functionali
|
||||
│ (Main session - user interacting with Claude) │
|
||||
│ │
|
||||
│ SessionStart → UserPromptSubmit → Tool Use → Stop │
|
||||
│ ↓ ↓ ↓ ↓ │
|
||||
│ [Hook] [Hook] [Hook] [Hook] │
|
||||
│ ↓ ↓ ↓ ↓ ↓ ↓ │
|
||||
│ [3 Hooks] [Hook] [Hook] [Hook] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ ↓ ↓ ↓
|
||||
↓ ↓ ↓ ↓ ↓ ↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ CLAUDE-MEM SYSTEM │
|
||||
│ │
|
||||
│ Context New Session Observation Summary │
|
||||
│ Injection Tracking Capture Generation │
|
||||
│ Smart Context User New Obs │
|
||||
│ Install Inject Message Session Capture │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -68,42 +68,71 @@ Claude Code's hook system provides exactly what we need:
|
||||
|
||||
---
|
||||
|
||||
## The Five Hooks
|
||||
## The Seven Hook Scripts
|
||||
|
||||
### Hook 1: SessionStart (Context Hook)
|
||||
Claude-Mem uses 7 hook scripts across 5 lifecycle events. SessionStart runs 3 hooks in sequence.
|
||||
|
||||
**Purpose:** Inject relevant context from previous sessions
|
||||
### Hook 1: SessionStart - Smart Install
|
||||
|
||||
**When:** Claude Code starts or resumes
|
||||
**Purpose:** Intelligently manage dependencies and start worker service
|
||||
|
||||
**When:** Claude Code starts (startup, clear, or compact)
|
||||
|
||||
**What it does:**
|
||||
1. Extracts project name from current working directory
|
||||
2. Queries SQLite for recent session summaries (last 10)
|
||||
3. Queries SQLite for recent observations (last 50)
|
||||
4. Formats as progressive disclosure index
|
||||
5. Outputs to stdout (automatically injected into context)
|
||||
1. Checks if dependencies need installation (version marker)
|
||||
2. Only runs `npm install` when necessary:
|
||||
- First-time installation
|
||||
- Version changed in package.json
|
||||
- Critical dependency missing (better-sqlite3)
|
||||
3. Provides Windows-specific error messages
|
||||
4. Starts PM2 worker service
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"matcher": "startup",
|
||||
"matcher": "startup|clear|compact",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/../scripts/smart-install.js\" && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 300
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- ✅ Version caching (`.install-version` file)
|
||||
- ✅ Fast when already installed (~10ms vs 2-5 seconds)
|
||||
- ✅ Cross-platform compatible
|
||||
- ✅ Helpful Windows error messages for build tools
|
||||
|
||||
**v5.0.3 Enhancement:** Smart caching eliminates redundant installs
|
||||
|
||||
**Source:** `scripts/smart-install.js`
|
||||
|
||||
---
|
||||
|
||||
### Hook 2: SessionStart - Context Injection
|
||||
|
||||
**Purpose:** Inject relevant context from previous sessions
|
||||
|
||||
**When:** Claude Code starts (runs after smart-install)
|
||||
|
||||
**What it does:**
|
||||
1. Extracts project name from current working directory
|
||||
2. Queries SQLite for recent session summaries (last 10)
|
||||
3. Queries SQLite for recent observations (configurable, default 50)
|
||||
4. Formats as progressive disclosure index
|
||||
5. Outputs to stdout (automatically injected into context)
|
||||
|
||||
**Key decisions:**
|
||||
- ✅ Only runs on "startup" (not "clear" or "compact")
|
||||
- ✅ 120-second timeout for npm install (v4.3.1 fix)
|
||||
- ✅ Uses `--loglevel=silent` for clean JSON output
|
||||
- ✅ Runs on startup, clear, and compact
|
||||
- ✅ 300-second timeout (allows for npm install if needed)
|
||||
- ✅ Progressive disclosure format (index, not full details)
|
||||
- ✅ Configurable observation count via `CLAUDE_MEM_CONTEXT_OBSERVATIONS`
|
||||
|
||||
**Output format:**
|
||||
```markdown
|
||||
@@ -125,7 +154,56 @@ Claude Code's hook system provides exactly what we need:
|
||||
|
||||
---
|
||||
|
||||
### Hook 2: UserPromptSubmit (New Session Hook)
|
||||
### Hook 3: SessionStart - User Message
|
||||
|
||||
**Purpose:** Display helpful user messages during first-time setup
|
||||
|
||||
**When:** Claude Code starts (runs after context-hook)
|
||||
|
||||
**What it does:**
|
||||
1. Checks if dependencies are installed
|
||||
2. Shows first-time setup message if needed
|
||||
3. Displays formatted context information with colors
|
||||
4. Shows link to viewer UI (http://localhost:37777)
|
||||
5. Exits with code 3 (informational, not error)
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"matcher": "startup|clear|compact",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js",
|
||||
"timeout": 10
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output Example:**
|
||||
```
|
||||
📝 Claude-Mem Context Loaded
|
||||
ℹ️ Note: This appears as stderr but is informational only
|
||||
|
||||
[Context details with colors...]
|
||||
|
||||
📺 Watch live in browser http://localhost:37777/ (New! v5.1)
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- ✅ User-friendly first-time experience
|
||||
- ✅ Visual context display
|
||||
- ✅ Links to viewer UI
|
||||
- ✅ Non-intrusive (exit code 3)
|
||||
|
||||
**Source:** `plugin/scripts/user-message-hook.js` (minified)
|
||||
|
||||
---
|
||||
|
||||
### Hook 4: UserPromptSubmit (New Session Hook)
|
||||
|
||||
**Purpose:** Initialize session tracking when user submits a prompt
|
||||
|
||||
@@ -172,7 +250,7 @@ VALUES (?, ?, ?, ...)
|
||||
|
||||
---
|
||||
|
||||
### Hook 3: PostToolUse (Save Observation Hook)
|
||||
### Hook 5: PostToolUse (Save Observation Hook)
|
||||
|
||||
**Purpose:** Capture tool execution observations for later processing
|
||||
|
||||
@@ -233,7 +311,7 @@ VALUES (?, ?, ?, ?, ...)
|
||||
|
||||
---
|
||||
|
||||
### Hook 4: Summary Hook (Mid-Session Checkpoint)
|
||||
### Hook 6: Summary Hook (Mid-Session Checkpoint)
|
||||
|
||||
**Purpose:** Generate AI-powered session summaries during the session
|
||||
|
||||
@@ -288,7 +366,7 @@ VALUES (?, ?, ?, ?, ...)
|
||||
|
||||
---
|
||||
|
||||
### Hook 5: SessionEnd (Cleanup Hook)
|
||||
### Hook 7: SessionEnd (Cleanup Hook)
|
||||
|
||||
**Purpose:** Mark sessions as completed when they end
|
||||
|
||||
@@ -395,11 +473,13 @@ sequenceDiagram
|
||||
|
||||
| Event | Timing | Blocking | Timeout | Output Handling |
|
||||
|-------|--------|----------|---------|-----------------|
|
||||
| **SessionStart** | Before session | No | 120s | stdout → context |
|
||||
| **UserPromptSubmit** | Before processing | No | 60s | stdout → context |
|
||||
| **PostToolUse** | After tool | No | 60s | Transcript only |
|
||||
| **Summary** | Worker triggered | No | 300s | Database |
|
||||
| **SessionEnd** | On exit | No | 60s | Log only |
|
||||
| **SessionStart (smart-install)** | Before session | No | 300s | stderr (info) |
|
||||
| **SessionStart (context)** | Before session | No | 300s | stdout → context |
|
||||
| **SessionStart (user-message)** | Before session | No | 10s | stderr (info) |
|
||||
| **UserPromptSubmit** | Before processing | No | 120s | stdout → context |
|
||||
| **PostToolUse** | After tool | No | 120s | Transcript only |
|
||||
| **Summary** | Worker triggered | No | 120s | Database |
|
||||
| **SessionEnd** | On exit | No | 120s | Log only |
|
||||
|
||||
---
|
||||
|
||||
@@ -668,20 +748,23 @@ claude --debug
|
||||
|
||||
| Hook | Average | p95 | p99 |
|
||||
|------|---------|-----|-----|
|
||||
| SessionStart | 45ms | 120ms | 250ms |
|
||||
| SessionStart (smart-install, cached) | 10ms | 20ms | 40ms |
|
||||
| SessionStart (smart-install, first run) | 2500ms | 5000ms | 8000ms |
|
||||
| SessionStart (context) | 45ms | 120ms | 250ms |
|
||||
| SessionStart (user-message) | 5ms | 10ms | 15ms |
|
||||
| UserPromptSubmit | 12ms | 25ms | 50ms |
|
||||
| PostToolUse | 8ms | 15ms | 30ms |
|
||||
| SessionEnd | 5ms | 10ms | 20ms |
|
||||
|
||||
**Why SessionStart is slower:**
|
||||
- npm install check (idempotent but runs every time)
|
||||
- Database query for 10 sessions + 50 observations
|
||||
- Formatting progressive disclosure index
|
||||
**Why smart-install is sometimes slow:**
|
||||
- First-time: Full npm install (2-5 seconds)
|
||||
- Cached: Version check only (~10ms)
|
||||
- Version change: Full npm install + PM2 restart
|
||||
|
||||
**Optimization (v4.3.1):**
|
||||
- Use `--loglevel=silent` for npm install
|
||||
- Cache package.json hash to skip unnecessary installs
|
||||
- Use prepared statements for database queries
|
||||
**Optimization (v5.0.3):**
|
||||
- Version caching with `.install-version` marker
|
||||
- Only install on version change or missing deps
|
||||
- Windows-specific error messages with build tool help
|
||||
|
||||
### Database Performance
|
||||
|
||||
@@ -775,9 +858,9 @@ LIMIT 20
|
||||
## Further Reading
|
||||
|
||||
- [Claude Code Hooks Reference](https://docs.claude.com/claude-code/hooks) - Official documentation
|
||||
- [Progressive Disclosure](/docs/progressive-disclosure) - Context priming philosophy
|
||||
- [Architecture Evolution](/docs/architecture-evolution) - v3 to v4 journey
|
||||
- [Worker Service Design](/docs/worker-service) - Background processing details
|
||||
- [Progressive Disclosure](progressive-disclosure) - Context priming philosophy
|
||||
- [Architecture Evolution](architecture-evolution) - v3 to v4 journey
|
||||
- [Worker Service Design](architecture/worker-service) - Background processing details
|
||||
|
||||
---
|
||||
|
||||
|
||||
+17
-13
@@ -23,7 +23,9 @@ Restart Claude Code. Context from previous sessions will automatically appear in
|
||||
## Key Features
|
||||
|
||||
- 🧠 **Persistent Memory** - Context survives across sessions
|
||||
- 🔍 **7 Search Tools** - Query your project history via MCP
|
||||
- 🔍 **9 Search Tools** - Query your project history via MCP
|
||||
- 🌐 **Web Viewer UI** - Real-time memory stream visualization at http://localhost:37777
|
||||
- 🎨 **Theme Toggle** - Light, dark, and system preference themes
|
||||
- 🤖 **Automatic Operation** - No manual intervention required
|
||||
- 📊 **FTS5 Search** - Fast full-text search across observations
|
||||
- 🔗 **Citations** - Reference past decisions with `claude-mem://` URIs
|
||||
@@ -56,7 +58,8 @@ Restart Claude Code. Context from previous sessions will automatically appear in
|
||||
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
|
||||
4. **9 MCP Search Tools** - Query historical context with citations
|
||||
5. **Web Viewer UI** - Real-time visualization with SSE and infinite scroll
|
||||
|
||||
See [Architecture Overview](architecture/overview) for details.
|
||||
|
||||
@@ -67,21 +70,22 @@ See [Architecture Overview](architecture/overview) for details.
|
||||
- **PM2**: Process manager (bundled - no global install required)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
|
||||
## What's New in v4.3.1
|
||||
## What's New in v5.1.2
|
||||
|
||||
**Critical Fix:**
|
||||
- Fixed SessionStart hook context injection (v4.3.1)
|
||||
- Context wasn't being injected due to npm output pollution
|
||||
- Changed npm loglevel to `--loglevel=silent` for clean JSON output
|
||||
**Latest Updates (v5.1.2):**
|
||||
- Theme toggle for light, dark, and system preferences in viewer UI
|
||||
- Improved visual design with theme-aware components
|
||||
|
||||
**Code Quality:**
|
||||
- Consolidated hooks architecture (removed bin/hooks wrapper layer)
|
||||
- Fixed double shebang issues in hook executables
|
||||
**Recent Updates (v5.1.0):**
|
||||
- Web-based viewer UI for real-time memory stream visualization
|
||||
- Server-Sent Events (SSE) for instant updates
|
||||
- Infinite scroll pagination with project filtering
|
||||
- 8 new HTTP/SSE endpoints in worker service
|
||||
|
||||
**Recent Updates (v4.3.0):**
|
||||
**Previous Updates (v4.3.1):**
|
||||
- Fixed SessionStart hook context injection
|
||||
- Smart install caching for Windows compatibility
|
||||
- Progressive disclosure context with observation timelines
|
||||
- Enhanced session summaries with token cost visibility
|
||||
- Cross-platform path detection improvements
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -644,8 +644,8 @@ Progressive disclosure respects the agent's intelligence and autonomy. We provid
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Context Engineering for AI Agents](/docs/context-engineering) - Foundational principles
|
||||
- [Claude-Mem Architecture](/docs/architecture) - How it all fits together
|
||||
- [Context Engineering for AI Agents](context-engineering) - Foundational principles
|
||||
- [Claude-Mem Architecture](architecture/overview) - How it all fits together
|
||||
- Cognitive Load Theory (Sweller, 1988)
|
||||
- Information Foraging Theory (Pirolli & Card, 1999)
|
||||
- Progressive Disclosure (Nielsen Norman Group)
|
||||
|
||||
@@ -5,6 +5,177 @@ description: "Common issues and solutions for Claude-Mem"
|
||||
|
||||
# Troubleshooting Guide
|
||||
|
||||
## v5.x Specific Issues
|
||||
|
||||
### Viewer UI Not Loading
|
||||
|
||||
**Symptoms**: Cannot access http://localhost:37777, page doesn't load, or shows connection error.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check if worker is running on port 37777:
|
||||
```bash
|
||||
lsof -i :37777
|
||||
# or
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
2. Verify worker is healthy:
|
||||
```bash
|
||||
curl http://localhost:37777/health
|
||||
```
|
||||
|
||||
3. Check worker logs for errors:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
4. Restart worker service:
|
||||
```bash
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
5. Check for port conflicts:
|
||||
```bash
|
||||
# If port 37777 is in use by another service
|
||||
export CLAUDE_MEM_WORKER_PORT=38000
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
### Theme Toggle Not Persisting
|
||||
|
||||
**Symptoms**: Theme preference (light/dark mode) resets after browser refresh.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check browser localStorage is enabled:
|
||||
```javascript
|
||||
// In browser console
|
||||
localStorage.getItem('claude-mem-settings')
|
||||
```
|
||||
|
||||
2. Verify settings endpoint is working:
|
||||
```bash
|
||||
curl http://localhost:37777/api/settings
|
||||
```
|
||||
|
||||
3. Clear localStorage and try again:
|
||||
```javascript
|
||||
// In browser console
|
||||
localStorage.removeItem('claude-mem-settings')
|
||||
```
|
||||
|
||||
4. Check for browser privacy mode (blocks localStorage)
|
||||
|
||||
### SSE Connection Issues
|
||||
|
||||
**Symptoms**: Viewer shows "Disconnected" status, updates not appearing in real-time.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check SSE endpoint is accessible:
|
||||
```bash
|
||||
curl -N http://localhost:37777/stream
|
||||
```
|
||||
|
||||
2. Check browser console for errors:
|
||||
- Open DevTools (F12)
|
||||
- Look for EventSource errors
|
||||
- Check Network tab for failed /stream requests
|
||||
|
||||
3. Verify worker is running:
|
||||
```bash
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
4. Check for network/proxy issues blocking SSE
|
||||
- Corporate firewalls may block SSE
|
||||
- Try disabling VPN temporarily
|
||||
|
||||
5. Restart worker and refresh browser:
|
||||
```bash
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
### Chroma/Python Dependency Issues (v5.0.0+)
|
||||
|
||||
**Symptoms**: Installation fails with chromadb or Python-related errors.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Verify Python 3.8+ is installed:
|
||||
```bash
|
||||
python --version
|
||||
# or
|
||||
python3 --version
|
||||
```
|
||||
|
||||
2. Install chromadb manually:
|
||||
```bash
|
||||
cd ~/.claude/plugins/marketplaces/thedotmack
|
||||
npm install chromadb
|
||||
```
|
||||
|
||||
3. Check chromadb health:
|
||||
```bash
|
||||
npm run chroma:health
|
||||
```
|
||||
|
||||
4. Windows-specific: Ensure Python is in PATH:
|
||||
```bash
|
||||
where python
|
||||
# Should show Python installation path
|
||||
```
|
||||
|
||||
5. If Chroma continues to fail, hybrid search will gracefully degrade to SQLite FTS5 only
|
||||
|
||||
### Smart Install Caching Issues (v5.0.3+)
|
||||
|
||||
**Symptoms**: Dependencies not updating after plugin update, stale version marker.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Clear install cache:
|
||||
```bash
|
||||
rm ~/.claude/plugins/marketplaces/thedotmack/.install-version
|
||||
```
|
||||
|
||||
2. Force reinstall:
|
||||
```bash
|
||||
cd ~/.claude/plugins/marketplaces/thedotmack
|
||||
npm install --force
|
||||
```
|
||||
|
||||
3. Check version marker:
|
||||
```bash
|
||||
cat ~/.claude/plugins/marketplaces/thedotmack/.install-version
|
||||
cat ~/.claude/plugins/marketplaces/thedotmack/package.json | grep version
|
||||
```
|
||||
|
||||
4. Restart Claude Code after manual install
|
||||
|
||||
### PM2 ENOENT Error on Windows (v5.1.1 Fix)
|
||||
|
||||
**Symptoms**: Worker fails to start with "ENOENT" error on Windows.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. This was fixed in v5.1.1 - update to latest version:
|
||||
```bash
|
||||
/plugin update claude-mem
|
||||
```
|
||||
|
||||
2. If still experiencing issues, verify PM2 path:
|
||||
```bash
|
||||
cd ~/.claude/plugins/marketplaces/thedotmack
|
||||
dir node_modules\.bin\pm2.cmd
|
||||
```
|
||||
|
||||
3. Manual PM2 install if needed:
|
||||
```bash
|
||||
npm install pm2@latest
|
||||
```
|
||||
|
||||
## Worker Service Issues
|
||||
|
||||
### Worker Service Not Starting
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: "MCP Search Tools"
|
||||
description: "Query your project history with 7 specialized search tools"
|
||||
description: "Query your project history with 9 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.
|
||||
Once claude-mem is installed as a plugin, 9 search tools become available in your Claude Code sessions for querying project history.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
@@ -18,6 +18,8 @@ Once claude-mem is installed as a plugin, 7 search tools become available in you
|
||||
| find_by_file | Find observations referencing files |
|
||||
| find_by_type | Find observations by type |
|
||||
| get_recent_context | Get recent session context |
|
||||
| get_context_timeline | Get unified timeline around a specific point |
|
||||
| get_timeline_by_query | Search and get timeline context in one step |
|
||||
|
||||
## Example Queries
|
||||
|
||||
@@ -115,6 +117,70 @@ Get recent context for debugging:
|
||||
Use get_recent_context to show me what we've been working on
|
||||
```
|
||||
|
||||
### get_context_timeline
|
||||
|
||||
Get a unified timeline of context around a specific point in time. This tool interleaves observations, sessions, and user prompts chronologically to show what was happening before and after a specific moment.
|
||||
|
||||
**Anchor by observation ID:**
|
||||
```
|
||||
get_context_timeline with anchor=12345 and depth_before=10 and depth_after=10
|
||||
```
|
||||
|
||||
**Anchor by session ID:**
|
||||
```
|
||||
get_context_timeline with anchor="S123" and depth_before=5 and depth_after=5
|
||||
```
|
||||
|
||||
**Anchor by ISO timestamp:**
|
||||
```
|
||||
get_context_timeline with anchor="2025-10-21T14:30:00Z" and depth_before=15 and depth_after=15
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Understand what was happening when a specific observation occurred
|
||||
- See the full context around a bug fix or decision
|
||||
- Trace the events leading up to and following a specific change
|
||||
- View chronological sequence of related work
|
||||
|
||||
**Benefits:**
|
||||
- All record types (observations, sessions, prompts) in one chronological view
|
||||
- Configurable depth before/after anchor point
|
||||
- Flexible anchoring by ID or timestamp
|
||||
- See the complete narrative arc around key events
|
||||
|
||||
### get_timeline_by_query
|
||||
|
||||
Search for observations using natural language and get timeline context around the best match. This combines search + timeline into a single operation for faster context discovery.
|
||||
|
||||
**Auto mode (default):**
|
||||
```
|
||||
get_timeline_by_query with query="authentication implementation"
|
||||
```
|
||||
Automatically uses the top search result as timeline anchor and returns surrounding context.
|
||||
|
||||
**Interactive mode:**
|
||||
```
|
||||
get_timeline_by_query with query="authentication" and mode="interactive" and limit=5
|
||||
```
|
||||
Shows top 5 search results for you to manually choose which to use as timeline anchor.
|
||||
|
||||
**Customize timeline depth:**
|
||||
```
|
||||
get_timeline_by_query with query="bug fix" and depth_before=20 and depth_after=10
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Quick context discovery: "What was happening when we implemented X?"
|
||||
- Investigate issues: Find a bug fix and see what led to it
|
||||
- Decision archaeology: Search for a decision and understand the context
|
||||
- Feature timeline: See the complete story of a feature implementation
|
||||
|
||||
**Benefits:**
|
||||
- Single-step operation (no need to search, then timeline separately)
|
||||
- Auto mode provides instant context
|
||||
- Interactive mode gives you control over anchor selection
|
||||
- Natural language search makes it easy to find relevant moments
|
||||
|
||||
## Search Strategy
|
||||
|
||||
### 1. Start with Index Format
|
||||
|
||||
Generated
+93
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-mem",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.3",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
|
||||
@@ -16,12 +16,16 @@
|
||||
"glob": "^11.0.3",
|
||||
"handlebars": "^4.7.8",
|
||||
"pm2": "^6.0.13",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"esbuild": "^0.25.12",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.3.0"
|
||||
@@ -1442,6 +1446,13 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
@@ -1456,6 +1467,27 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.26",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
|
||||
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz",
|
||||
@@ -1955,6 +1987,13 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/culvert": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/culvert/-/culvert-0.1.2.tgz",
|
||||
@@ -2932,6 +2971,12 @@
|
||||
"pako": "^0.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
@@ -2963,6 +3008,18 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.2.2",
|
||||
"license": "ISC",
|
||||
@@ -3776,6 +3833,31 @@
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/read": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
|
||||
@@ -3978,6 +4060,15 @@
|
||||
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"license": "ISC",
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.0.2",
|
||||
"version": "5.1.4",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -35,7 +35,7 @@
|
||||
"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",
|
||||
"sync-marketplace": "rsync -av --delete plugin/ ~/.claude/plugins/marketplaces/thedotmack/plugin/ # --delete flag removes orphaned files from destination only",
|
||||
"sync-marketplace": "rsync -av --delete --exclude=.git ./ ~/.claude/plugins/marketplaces/thedotmack/ && cd ~/.claude/plugins/marketplaces/thedotmack/ && npm install",
|
||||
"worker:start": "pm2 start ecosystem.config.cjs",
|
||||
"worker:stop": "pm2 stop claude-mem-worker",
|
||||
"worker:restart": "pm2 restart claude-mem-worker",
|
||||
@@ -51,12 +51,16 @@
|
||||
"glob": "^11.0.3",
|
||||
"handlebars": "^4.7.8",
|
||||
"pm2": "^6.0.13",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"esbuild": "^0.25.12",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.3.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.0.2",
|
||||
"version": "5.1.4",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=silent && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/../scripts/smart-install.js\" && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 300
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as I}from"process";import M from"better-sqlite3";import{join as E,dirname as y,basename as F}from"path";import{homedir as O}from"os";import{existsSync as $,mkdirSync as k}from"fs";import{fileURLToPath as x}from"url";function U(){return typeof __dirname<"u"?__dirname:y(x(import.meta.url))}var P=U(),u=process.env.CLAUDE_MEM_DATA_DIR||E(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||E(O(),".claude"),W=E(u,"archives"),Y=E(u,"logs"),K=E(u,"trash"),V=E(u,"backups"),q=E(u,"settings.json"),f=E(u,"claude-mem.db"),J=E(u,"vector-db"),Q=E(R,"settings.json"),z=E(R,"commands"),Z=E(R,"CLAUDE.md");function L(p){k(p,{recursive:!0})}var N=(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))(N||{}),h=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[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 o=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),d=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let m="";n!=null&&(this.level===0&&typeof n=="object"?m=`
|
||||
`+JSON.stringify(n,null,2):m=" "+this.formatData(n));let T="";if(r){let{sessionId:l,sdkSessionId:S,correlationId:c,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([v,D])=>`${v}=${D}`).join(", ")}}`)}let b=`[${o}] [${i}] [${d}] ${_}${t}${T}${m}`;e===3?console.error(b):console.log(b)}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 h;var g=class{db;constructor(){L(u),this.db=new M(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(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
import{stdin as I}from"process";import M from"better-sqlite3";import{join as E,dirname as y,basename as F}from"path";import{homedir as O}from"os";import{existsSync as H,mkdirSync as k}from"fs";import{fileURLToPath as x}from"url";function U(){return typeof __dirname<"u"?__dirname:y(x(import.meta.url))}var P=U(),l=process.env.CLAUDE_MEM_DATA_DIR||E(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||E(O(),".claude"),W=E(l,"archives"),Y=E(l,"logs"),K=E(l,"trash"),V=E(l,"backups"),q=E(l,"settings.json"),f=E(l,"claude-mem.db"),J=E(l,"vector-db"),Q=E(R,"settings.json"),z=E(R,"commands"),Z=E(R,"CLAUDE.md");function L(c){k(c,{recursive:!0})}var h=(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))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[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 o=new Date().toISOString().replace("T"," ").substring(0,23),i=h[e].padEnd(5),d=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
|
||||
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let T="";if(r){let{sessionId:m,sdkSessionId:b,correlationId:p,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([v,D])=>`${v}=${D}`).join(", ")}}`)}let S=`[${o}] [${i}] [${d}] ${_}${t}${T}${u}`;e===3?console.error(S):console.log(S)}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 N;var g=class{db;constructor(){L(l),this.db=new M(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(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -188,7 +188,36 @@ ${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)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
`).all(e,s)}getAllRecentObservations(e=100){return this.db.prepare(`
|
||||
SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
|
||||
FROM observations
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentSummaries(e=50){return this.db.prepare(`
|
||||
SELECT id, request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, project, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentUserPrompts(e=100){return this.db.prepare(`
|
||||
SELECT
|
||||
up.id,
|
||||
up.claude_session_id,
|
||||
s.project,
|
||||
up.prompt_number,
|
||||
up.prompt_text,
|
||||
up.created_at,
|
||||
up.created_at_epoch
|
||||
FROM user_prompts up
|
||||
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
ORDER BY up.created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
@@ -288,23 +317,23 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
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(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let m=this.db.prepare(`
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=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(),o);return{id:Number(m.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=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(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=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(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let m=this.db.prepare(`
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=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(),o);return{id:Number(m.lastInsertRowid),createdAtEpoch:o}}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,n.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}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 = ?
|
||||
@@ -331,31 +360,31 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE up.id IN (${i})
|
||||
ORDER BY up.created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],d,_;if(e!==null){let l=`
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],d,_;if(e!==null){let m=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${o}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${o}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let c=this.db.prepare(l).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary observations:",c.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
|
||||
`;try{let p=this.db.prepare(m).all(e,...i,t+1),a=this.db.prepare(b).all(e,...i,r+1);if(p.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=p.length>0?p[p.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(p){return console.error("[SessionStore] Error getting boundary observations:",p.message),{observations:[],sessions:[],prompts:[]}}}else{let m=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let c=this.db.prepare(l).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary timestamps:",c.message),{observations:[],sessions:[],prompts:[]}}}let m=`
|
||||
`;try{let p=this.db.prepare(m).all(s,...i,t),a=this.db.prepare(b).all(s,...i,r+1);if(p.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=p.length>0?p[p.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(p){return console.error("[SessionStore] Error getting boundary timestamps:",p.message),{observations:[],sessions:[],prompts:[]}}}let u=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
@@ -365,11 +394,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,b=`
|
||||
`,S=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let l=this.db.prepare(m).all(d,_,...i),S=this.db.prepare(T).all(d,_,...i),c=this.db.prepare(b).all(d,_,...i);return{observations:l,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:c.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};async function C(p){console.error("[claude-mem cleanup] Hook fired",{input:p?{session_id:p.session_id,cwd:p.cwd,reason:p.reason}:null}),p||(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}=p;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.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}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)C(void 0);else{let p="";I.on("data",e=>p+=e),I.on("end",async()=>{let e=p?JSON.parse(p):void 0;await C(e)})}
|
||||
`;try{let m=this.db.prepare(u).all(d,_,...i),b=this.db.prepare(T).all(d,_,...i),p=this.db.prepare(S).all(d,_,...i);return{observations:m,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:p.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(m){return console.error("[SessionStore] Error querying timeline records:",m.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};async function C(c){console.error("[claude-mem cleanup] Hook fired",{input:c?{session_id:c.session_id,cwd:c.cwd,reason:c.reason}:null}),c||(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}=c;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.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}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)C(void 0);else{let c="";I.on("data",e=>c+=e),I.on("end",async()=>{let e=c?JSON.parse(c):void 0;await C(e)})}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
import X from"path";import{stdin as M}from"process";import ae from"better-sqlite3";import{join as S,dirname as te,basename as be}from"path";import{homedir as B}from"os";import{existsSync as Ne,mkdirSync as re}from"fs";import{fileURLToPath as ne}from"url";function oe(){return typeof __dirname<"u"?__dirname:te(ne(import.meta.url))}var ie=oe(),I=process.env.CLAUDE_MEM_DATA_DIR||S(B(),".claude-mem"),$=process.env.CLAUDE_CONFIG_DIR||S(B(),".claude"),Ie=S(I,"archives"),Le=S(I,"logs"),ve=S(I,"trash"),ye=S(I,"backups"),Ae=S(I,"settings.json"),W=S(I,"claude-mem.db"),Ce=S(I,"vector-db"),De=S($,"settings.json"),ke=S($,"commands"),xe=S($,"CLAUDE.md");function H(d){re(d,{recursive:!0})}function G(){return S(ie,"..","..")}var U=(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))(U||{}),w=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,o){if(e<this.level)return;let c=new Date().toISOString().replace("T"," ").substring(0,23),a=U[e].padEnd(5),_=t.padEnd(6),l="";r?.correlationId?l=`[${r.correlationId}] `:r?.sessionId&&(l=`[session-${r.sessionId}] `);let E="";o!=null&&(this.level===0&&typeof o=="object"?E=`
|
||||
`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let n="";if(r){let{sessionId:f,sdkSessionId:N,correlationId:m,...p}=r;Object.keys(p).length>0&&(n=` {${Object.entries(p).map(([u,h])=>`${u}=${h}`).join(", ")}}`)}let v=`[${c}] [${a}] [${_}] ${l}${s}${n}${E}`;e===3?console.error(v):console.log(v)}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`})}},j=new w;var D=class{db;constructor(){H(I),this.db=new ae(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(`
|
||||
import F from"path";import{stdin as M}from"process";import ae from"better-sqlite3";import{join as S,dirname as te,basename as be}from"path";import{homedir as B}from"os";import{existsSync as Ne,mkdirSync as re}from"fs";import{fileURLToPath as ne}from"url";function oe(){return typeof __dirname<"u"?__dirname:te(ne(import.meta.url))}var ie=oe(),I=process.env.CLAUDE_MEM_DATA_DIR||S(B(),".claude-mem"),$=process.env.CLAUDE_CONFIG_DIR||S(B(),".claude"),Ie=S(I,"archives"),Le=S(I,"logs"),ye=S(I,"trash"),ve=S(I,"backups"),Ae=S(I,"settings.json"),j=S(I,"claude-mem.db"),Ce=S(I,"vector-db"),De=S($,"settings.json"),xe=S($,"commands"),ke=S($,"CLAUDE.md");function W(d){re(d,{recursive:!0})}function H(){return S(ie,"..","..")}var U=(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))(U||{}),w=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,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 c=new Date().toISOString().replace("T"," ").substring(0,23),a=U[e].padEnd(5),u=s.padEnd(6),m="";r?.correlationId?m=`[${r.correlationId}] `:r?.sessionId&&(m=`[session-${r.sessionId}] `);let E="";o!=null&&(this.level===0&&typeof o=="object"?E=`
|
||||
`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let n="";if(r){let{sessionId:f,sdkSessionId:N,correlationId:l,...p}=r;Object.keys(p).length>0&&(n=` {${Object.entries(p).map(([_,h])=>`${_}=${h}`).join(", ")}}`)}let y=`[${c}] [${a}] [${u}] ${m}${t}${n}${E}`;e===3?console.error(y):console.log(y)}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`})}},G=new w;var D=class{db;constructor(){W(I),this.db=new ae(j),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(_=>_.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(_=>_.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(_=>_.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(u=>u.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(u=>u.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(u=>u.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,
|
||||
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
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(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",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(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
|
||||
@@ -174,7 +174,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentSummariesWithSessionInfo(e,t=3){return this.db.prepare(`
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
@@ -182,13 +182,42 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentSessionsWithStatus(e,t=3){return this.db.prepare(`
|
||||
`).all(e,s)}getAllRecentObservations(e=100){return this.db.prepare(`
|
||||
SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
|
||||
FROM observations
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentSummaries(e=50){return this.db.prepare(`
|
||||
SELECT id, request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, project, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentUserPrompts(e=100){return this.db.prepare(`
|
||||
SELECT
|
||||
up.id,
|
||||
up.claude_session_id,
|
||||
s.project,
|
||||
up.prompt_number,
|
||||
up.prompt_text,
|
||||
up.created_at,
|
||||
up.created_at_epoch
|
||||
FROM user_prompts up
|
||||
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
ORDER BY up.created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
@@ -205,7 +234,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 = ?
|
||||
@@ -214,7 +243,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id = ?
|
||||
`).get(e)||null}getObservationsByIds(e,t={}){if(e.length===0)return[];let{orderBy:s="date_desc",limit:r}=t,o=s==="date_asc"?"ASC":"DESC",c=r?`LIMIT ${r}`:"",a=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",c=r?`LIMIT ${r}`:"",a=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id IN (${a})
|
||||
@@ -228,11 +257,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getFilesForSession(e){let s=this.db.prepare(`
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,o=new Set;for(let c of s){if(c.files_read)try{let a=JSON.parse(c.files_read);Array.isArray(a)&&a.forEach(_=>r.add(_))}catch{}if(c.files_modified)try{let a=JSON.parse(c.files_modified);Array.isArray(a)&&a.forEach(_=>o.add(_))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,o=new Set;for(let c of t){if(c.files_read)try{let a=JSON.parse(c.files_read);Array.isArray(a)&&a.forEach(u=>r.add(u))}catch{}if(c.files_modified)try{let a=JSON.parse(c.files_modified);Array.isArray(a)&&a.forEach(u=>o.add(u))}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 = ?
|
||||
@@ -247,11 +276,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 = ?
|
||||
@@ -259,69 +288,69 @@ ${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,o=r.getTime(),a=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,t,s,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
`).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,t){return this.db.prepare(`
|
||||
`).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?(j.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?(G.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}saveUserPrompt(e,t,s){let r=new Date,o=r.getTime();return 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,t,s,r.toISOString(),o).lastInsertRowid}storeObservation(e,t,s,r){let o=new Date,c=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,c=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,t,o.toISOString(),c),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
`).run(e,e,s,o.toISOString(),c),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let 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,o.toISOString(),c);return{id:Number(E.lastInsertRowid),createdAtEpoch:c}}storeSummary(e,t,s,r){let o=new Date,c=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),c);return{id:Number(E.lastInsertRowid),createdAtEpoch:c}}storeSummary(e,s,t,r){let o=new Date,c=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,t,o.toISOString(),c),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
`).run(e,e,s,o.toISOString(),c),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let 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,o.toISOString(),c);return{id:Number(E.lastInsertRowid),createdAtEpoch:c}}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,o.toISOString(),c);return{id:Number(E.lastInsertRowid),createdAtEpoch:c}}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}getSessionSummariesByIds(e,t={}){if(e.length===0)return[];let{orderBy:s="date_desc",limit:r}=t,o=s==="date_asc"?"ASC":"DESC",c=r?`LIMIT ${r}`:"",a=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).run(e.toISOString(),s).changes}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",c=r?`LIMIT ${r}`:"",a=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT * FROM session_summaries
|
||||
WHERE id IN (${a})
|
||||
ORDER BY created_at_epoch ${o}
|
||||
${c}
|
||||
`).all(...e)}getUserPromptsByIds(e,t={}){if(e.length===0)return[];let{orderBy:s="date_desc",limit:r}=t,o=s==="date_asc"?"ASC":"DESC",c=r?`LIMIT ${r}`:"",a=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",c=r?`LIMIT ${r}`:"",a=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT
|
||||
up.*,
|
||||
s.project,
|
||||
@@ -331,7 +360,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
WHERE up.id IN (${a})
|
||||
ORDER BY up.created_at_epoch ${o}
|
||||
${c}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,t=10,s=10,r){return this.getTimelineAroundObservation(null,e,t,s,r)}getTimelineAroundObservation(e,t,s=10,r=10,o){let c=o?"AND project = ?":"",a=o?[o]:[],_,l;if(e!==null){let f=`
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,o){let c=o?"AND project = ?":"",a=o?[o]:[],u,m;if(e!==null){let f=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${c}
|
||||
@@ -343,7 +372,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
WHERE id >= ? ${c}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let m=this.db.prepare(f).all(e,...a,s+1),p=this.db.prepare(N).all(e,...a,r+1);if(m.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};_=m.length>0?m[m.length-1].created_at_epoch:t,l=p.length>0?p[p.length-1].created_at_epoch:t}catch(m){return console.error("[SessionStore] Error getting boundary observations:",m.message),{observations:[],sessions:[],prompts:[]}}}else{let f=`
|
||||
`;try{let l=this.db.prepare(f).all(e,...a,t+1),p=this.db.prepare(N).all(e,...a,r+1);if(l.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};u=l.length>0?l[l.length-1].created_at_epoch:s,m=p.length>0?p[p.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary observations:",l.message),{observations:[],sessions:[],prompts:[]}}}else{let f=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${c}
|
||||
@@ -355,7 +384,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
WHERE created_at_epoch >= ? ${c}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let m=this.db.prepare(f).all(t,...a,s),p=this.db.prepare(N).all(t,...a,r+1);if(m.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};_=m.length>0?m[m.length-1].created_at_epoch:t,l=p.length>0?p[p.length-1].created_at_epoch:t}catch(m){return console.error("[SessionStore] Error getting boundary timestamps:",m.message),{observations:[],sessions:[],prompts:[]}}}let E=`
|
||||
`;try{let l=this.db.prepare(f).all(s,...a,t),p=this.db.prepare(N).all(s,...a,r+1);if(l.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};u=l.length>0?l[l.length-1].created_at_epoch:s,m=p.length>0?p[p.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary timestamps:",l.message),{observations:[],sessions:[],prompts:[]}}}let E=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${c}
|
||||
@@ -365,13 +394,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${c}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,v=`
|
||||
`,y=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${c.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let f=this.db.prepare(E).all(_,l,...a),N=this.db.prepare(n).all(_,l,...a),m=this.db.prepare(v).all(_,l,...a);return{observations:f,sessions:N.map(p=>({id:p.id,sdk_session_id:p.sdk_session_id,project:p.project,request:p.request,completed:p.completed,next_steps:p.next_steps,created_at:p.created_at,created_at_epoch:p.created_at_epoch})),prompts:m.map(p=>({id:p.id,claude_session_id:p.claude_session_id,project:p.project,prompt:p.prompt_text,created_at:p.created_at,created_at_epoch:p.created_at_epoch}))}}catch(f){return console.error("[SessionStore] Error querying timeline records:",f.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};import Y from"path";import{spawn as V}from"child_process";var de=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);async function K(d=3e3){try{return(await fetch(`http://127.0.0.1:${de}/health`,{signal:AbortSignal.timeout(d)})).ok}catch{return!1}}async function ce(d=1e4){let e=Date.now(),t=100;for(;Date.now()-e<d;){if(await K(1e3))return!0;await new Promise(s=>setTimeout(s,t))}return!1}async function q(){if(await K(1e3))return;let d=G(),e=Y.join(d,"node_modules",".bin","pm2"),t=Y.join(d,"ecosystem.config.cjs"),s=V(e,["list","--no-color"],{cwd:d,stdio:["ignore","pipe","ignore"]}),r="";if(s.stdout?.on("data",a=>{r+=a.toString()}),await new Promise((a,_)=>{s.on("error",l=>_(l)),s.on("close",l=>{a()})}),!(r.includes("claude-mem-worker")&&r.includes("online"))){let a=V(e,["start",t],{cwd:d,stdio:"ignore"});await new Promise((_,l)=>{a.on("error",E=>l(E)),a.on("close",E=>{E!==0&&E!==null?l(new Error(`PM2 start command failed with exit code ${E}`)):_()})})}if(!await ce(1e4))throw new Error("Worker failed to become healthy after starting")}var pe=parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS||"50",10),J=10,i={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m",red:"\x1B[31m"};function _e(d){if(!d)return[];let e=JSON.parse(d);return Array.isArray(e)?e:[]}function ue(d){return new Date(d).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function me(d){return new Date(d).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function le(d){return new Date(d).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function Ee(d){return d?Math.ceil(d.length/4):0}function Te(d,e){return X.isAbsolute(d)?X.relative(e,d):d}async function Q(d,e=!1,t=!1){await q();let s=d?.cwd??process.cwd(),r=s?X.basename(s):"unknown-project",o=new D,c=o.db.prepare(`
|
||||
`;try{let f=this.db.prepare(E).all(u,m,...a),N=this.db.prepare(n).all(u,m,...a),l=this.db.prepare(y).all(u,m,...a);return{observations:f,sessions:N.map(p=>({id:p.id,sdk_session_id:p.sdk_session_id,project:p.project,request:p.request,completed:p.completed,next_steps:p.next_steps,created_at:p.created_at,created_at_epoch:p.created_at_epoch})),prompts:l.map(p=>({id:p.id,claude_session_id:p.claude_session_id,project:p.project,prompt:p.prompt_text,created_at:p.created_at,created_at_epoch:p.created_at_epoch}))}}catch(f){return console.error("[SessionStore] Error querying timeline records:",f.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};import Y from"path";import{spawn as V}from"child_process";var de=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);async function q(d=100){try{return(await fetch(`http://127.0.0.1:${de}/health`,{signal:AbortSignal.timeout(d)})).ok}catch{return!1}}async function ce(d=1e4){let e=Date.now(),s=100;for(;Date.now()-e<d;){if(await q(1e3))return!0;await new Promise(t=>setTimeout(t,s))}return!1}async function K(){if(await q())return;let d=H(),e=Y.join(d,"node_modules",".bin","pm2"),s=Y.join(d,"ecosystem.config.cjs"),t=V(e,["list","--no-color"],{cwd:d,stdio:["ignore","pipe","ignore"]}),r="";if(t.stdout?.on("data",a=>{r+=a.toString()}),await new Promise((a,u)=>{t.on("error",m=>u(m)),t.on("close",m=>{a()})}),!(r.includes("claude-mem-worker")&&r.includes("online"))){let a=V(e,["start",s],{cwd:d,stdio:"ignore"});await new Promise((u,m)=>{a.on("error",E=>m(E)),a.on("close",E=>{E!==0&&E!==null?m(new Error(`PM2 start command failed with exit code ${E}`)):u()})})}if(!await ce(1e4))throw new Error("Worker failed to become healthy after starting")}var pe=parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS||"50",10),J=10,i={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m",red:"\x1B[31m"};function ue(d){if(!d)return[];let e=JSON.parse(d);return Array.isArray(e)?e:[]}function _e(d){return new Date(d).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function le(d){return new Date(d).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function me(d){return new Date(d).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function Ee(d){return d?Math.ceil(d.length/4):0}function Te(d,e){return F.isAbsolute(d)?F.relative(e,d):d}async function Q(d,e=!1,s=!1){await K();let t=d?.cwd??process.cwd(),r=t?F.basename(t):"unknown-project",o=new D,c=o.db.prepare(`
|
||||
SELECT
|
||||
id, sdk_session_id, type, title, subtitle, narrative,
|
||||
facts, concepts, files_read, files_modified,
|
||||
@@ -393,5 +422,5 @@ ${i.gray}${"\u2500".repeat(60)}${i.reset}
|
||||
${i.dim}No previous sessions found for this project yet.${i.reset}
|
||||
`:`# [${r}] recent context
|
||||
|
||||
No previous sessions found for this project yet.`;let _=c,l=a.slice(0,J),E=_,n=[];if(e?(n.push(""),n.push(`${i.bright}${i.cyan}\u{1F4DD} [${r}] recent context${i.reset}`),n.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),n.push("")):(n.push(`# [${r}] recent context`),n.push("")),E.length>0){e?(n.push(`${i.dim}Legend: \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision${i.reset}`),n.push("")):(n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),n.push("")),e?(n.push(`${i.dim}\u{1F4A1} Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${i.reset}`),n.push(`${i.dim} \u2192 Use MCP search tools to fetch full observation details on-demand (Layer 2)${i.reset}`),n.push(`${i.dim} \u2192 Prefer searching observations over re-reading code for past decisions and learnings${i.reset}`),n.push(`${i.dim} \u2192 Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately${i.reset}`),n.push("")):(n.push("\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts)."),n.push("- Use MCP search tools to fetch full observation details on-demand (Layer 2)"),n.push("- Prefer searching observations over re-reading code for past decisions and learnings"),n.push("- Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately"),n.push(""));let v=a[0]?.id,f=l.map((u,h)=>{let T=h===0?null:a[h+1];return{...u,displayEpoch:T?T.created_at_epoch:u.created_at_epoch,displayTime:T?T.created_at:u.created_at,isMostRecent:u.id===v}}),N=[...E.map(u=>({type:"observation",data:u})),...f.map(u=>({type:"summary",data:u}))];N.sort((u,h)=>{let T=u.type==="observation"?u.data.created_at_epoch:u.data.displayEpoch,L=h.type==="observation"?h.data.created_at_epoch:h.data.displayEpoch;return T-L});let m=new Map;for(let u of N){let h=u.type==="observation"?u.data.created_at:u.data.displayTime,T=le(h);m.has(T)||m.set(T,[]),m.get(T).push(u)}let p=Array.from(m.entries()).sort((u,h)=>{let T=new Date(u[0]).getTime(),L=new Date(h[0]).getTime();return T-L});for(let[u,h]of p){e?(n.push(`${i.bright}${i.cyan}${u}${i.reset}`),n.push("")):(n.push(`### ${u}`),n.push(""));let T=null,L="",y=!1;for(let k of h)if(k.type==="summary"){y&&(n.push(""),y=!1,T=null,L="");let g=k.data,A=`${g.request||"Session started"} (${ue(g.displayTime)})`,O=g.isMostRecent?"":`claude-mem://session-summary/${g.id}`;if(e){let b=O?`${i.dim}[${O}]${i.reset}`:"";n.push(`\u{1F3AF} ${i.yellow}#S${g.id}${i.reset} ${A} ${b}`)}else{let b=O?` [\u2192](${O})`:"";n.push(`**\u{1F3AF} #S${g.id}** ${A}${b}`)}n.push("")}else{let g=k.data,A=_e(g.files_modified),O=A.length>0?Te(A[0],s):"General";O!==T&&(y&&n.push(""),e?n.push(`${i.dim}${O}${i.reset}`):n.push(`**${O}**`),e||(n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|")),T=O,y=!0,L="");let b="\u2022";switch(g.type){case"bugfix":b="\u{1F534}";break;case"feature":b="\u{1F7E3}";break;case"refactor":b="\u{1F504}";break;case"change":b="\u2705";break;case"discovery":b="\u{1F535}";break;case"decision":b="\u{1F9E0}";break;default:b="\u2022"}let C=me(g.created_at),F=g.title||"Untitled",x=Ee(g.narrative),P=C!==L,Z=P?C:"";if(L=C,e){let ee=P?`${i.dim}${C}${i.reset}`:" ".repeat(C.length),se=x>0?`${i.dim}(~${x}t)${i.reset}`:"";n.push(` ${i.dim}#${g.id}${i.reset} ${ee} ${b} ${F} ${se}`)}else n.push(`| #${g.id} | ${Z||"\u2033"} | ${b} | ${F} | ~${x} |`)}y&&n.push("")}let R=a[0];R&&(R.completed||R.next_steps)&&(R.completed&&(e?n.push(`${i.green}Completed:${i.reset} ${R.completed}`):n.push(`**Completed**: ${R.completed}`),n.push("")),R.next_steps&&(e?n.push(`${i.magenta}Next Steps:${i.reset} ${R.next_steps}`):n.push(`**Next Steps**: ${R.next_steps}`),n.push(""))),e?n.push(`${i.dim}Use claude-mem MCP search to access records with the given ID${i.reset}`):n.push("*Use claude-mem MCP search to access records with the given ID*")}return o.close(),n.join(`
|
||||
`).trimEnd()}var z=process.argv.includes("--index"),he=process.argv.includes("--colors");if(M.isTTY||he)Q(void 0,!0,z).then(d=>{console.log(d),process.exit(0)});else{let d="";M.on("data",e=>d+=e),M.on("end",async()=>{let e=d.trim()?JSON.parse(d):void 0,s={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:await Q(e,!1,z)}};console.log(JSON.stringify(s)),process.exit(0)})}
|
||||
No previous sessions found for this project yet.`;let u=c,m=a.slice(0,J),E=u,n=[];if(e?(n.push(""),n.push(`${i.bright}${i.cyan}\u{1F4DD} [${r}] recent context${i.reset}`),n.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),n.push("")):(n.push(`# [${r}] recent context`),n.push("")),E.length>0){e?(n.push(`${i.dim}Legend: \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision${i.reset}`),n.push("")):(n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),n.push("")),e?(n.push(`${i.dim}\u{1F4A1} Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${i.reset}`),n.push(`${i.dim} \u2192 Use MCP search tools to fetch full observation details on-demand (Layer 2)${i.reset}`),n.push(`${i.dim} \u2192 Prefer searching observations over re-reading code for past decisions and learnings${i.reset}`),n.push(`${i.dim} \u2192 Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately${i.reset}`),n.push("")):(n.push("\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts)."),n.push("- Use MCP search tools to fetch full observation details on-demand (Layer 2)"),n.push("- Prefer searching observations over re-reading code for past decisions and learnings"),n.push("- Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately"),n.push(""));let y=a[0]?.id,f=m.map((_,h)=>{let T=h===0?null:a[h+1];return{..._,displayEpoch:T?T.created_at_epoch:_.created_at_epoch,displayTime:T?T.created_at:_.created_at,isMostRecent:_.id===y}}),N=[...E.map(_=>({type:"observation",data:_})),...f.map(_=>({type:"summary",data:_}))];N.sort((_,h)=>{let T=_.type==="observation"?_.data.created_at_epoch:_.data.displayEpoch,L=h.type==="observation"?h.data.created_at_epoch:h.data.displayEpoch;return T-L});let l=new Map;for(let _ of N){let h=_.type==="observation"?_.data.created_at:_.data.displayTime,T=me(h);l.has(T)||l.set(T,[]),l.get(T).push(_)}let p=Array.from(l.entries()).sort((_,h)=>{let T=new Date(_[0]).getTime(),L=new Date(h[0]).getTime();return T-L});for(let[_,h]of p){e?(n.push(`${i.bright}${i.cyan}${_}${i.reset}`),n.push("")):(n.push(`### ${_}`),n.push(""));let T=null,L="",v=!1;for(let x of h)if(x.type==="summary"){v&&(n.push(""),v=!1,T=null,L="");let g=x.data,A=`${g.request||"Session started"} (${_e(g.displayTime)})`,O=g.isMostRecent?"":`claude-mem://session-summary/${g.id}`;if(e){let b=O?`${i.dim}[${O}]${i.reset}`:"";n.push(`\u{1F3AF} ${i.yellow}#S${g.id}${i.reset} ${A} ${b}`)}else{let b=O?` [\u2192](${O})`:"";n.push(`**\u{1F3AF} #S${g.id}** ${A}${b}`)}n.push("")}else{let g=x.data,A=ue(g.files_modified),O=A.length>0?Te(A[0],t):"General";O!==T&&(v&&n.push(""),e?n.push(`${i.dim}${O}${i.reset}`):n.push(`**${O}**`),e||(n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|")),T=O,v=!0,L="");let b="\u2022";switch(g.type){case"bugfix":b="\u{1F534}";break;case"feature":b="\u{1F7E3}";break;case"refactor":b="\u{1F504}";break;case"change":b="\u2705";break;case"discovery":b="\u{1F535}";break;case"decision":b="\u{1F9E0}";break;default:b="\u2022"}let C=le(g.created_at),X=g.title||"Untitled",k=Ee(g.narrative),P=C!==L,Z=P?C:"";if(L=C,e){let ee=P?`${i.dim}${C}${i.reset}`:" ".repeat(C.length),se=k>0?`${i.dim}(~${k}t)${i.reset}`:"";n.push(` ${i.dim}#${g.id}${i.reset} ${ee} ${b} ${X} ${se}`)}else n.push(`| #${g.id} | ${Z||"\u2033"} | ${b} | ${X} | ~${k} |`)}v&&n.push("")}let R=a[0];R&&(R.completed||R.next_steps)&&(R.completed&&(e?n.push(`${i.green}Completed:${i.reset} ${R.completed}`):n.push(`**Completed**: ${R.completed}`),n.push("")),R.next_steps&&(e?n.push(`${i.magenta}Next Steps:${i.reset} ${R.next_steps}`):n.push(`**Next Steps**: ${R.next_steps}`),n.push(""))),e?n.push(`${i.dim}Use claude-mem MCP search to access records with the given ID${i.reset}`):n.push("*Use claude-mem MCP search to access records with the given ID*")}return o.close(),n.join(`
|
||||
`).trimEnd()}var z=process.argv.includes("--index"),he=process.argv.includes("--colors");if(M.isTTY||he)Q(void 0,!0,z).then(d=>{console.log(d),process.exit(0)});else{let d="";M.on("data",e=>d+=e),M.on("end",async()=>{let e=d.trim()?JSON.parse(d):void 0,t={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:await Q(e,!1,z)}};console.log(JSON.stringify(t)),process.exit(0)})}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import Y from"path";import{stdin as U}from"process";import $ from"better-sqlite3";import{join as E,dirname as X,basename as J}from"path";import{homedir as f}from"os";import{existsSync as ee,mkdirSync as F}from"fs";import{fileURLToPath as P}from"url";function H(){return typeof __dirname<"u"?__dirname:X(P(import.meta.url))}var B=H(),m=process.env.CLAUDE_MEM_DATA_DIR||E(f(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||E(f(),".claude"),te=E(m,"archives"),re=E(m,"logs"),ne=E(m,"trash"),oe=E(m,"backups"),ie=E(m,"settings.json"),I=E(m,"claude-mem.db"),ae=E(m,"vector-db"),de=E(g,"settings.json"),pe=E(g,"commands"),ce=E(g,"CLAUDE.md");function L(p){F(p,{recursive:!0})}function A(){return E(B,"..","..")}var h=(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))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[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}
|
||||
import Y from"path";import{stdin as U}from"process";import j from"better-sqlite3";import{join as m,dirname as X,basename as J}from"path";import{homedir as I}from"os";import{existsSync as ee,mkdirSync as F}from"fs";import{fileURLToPath as P}from"url";function B(){return typeof __dirname<"u"?__dirname:X(P(import.meta.url))}var H=B(),E=process.env.CLAUDE_MEM_DATA_DIR||m(I(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||m(I(),".claude"),te=m(E,"archives"),re=m(E,"logs"),ne=m(E,"trash"),oe=m(E,"backups"),ie=m(E,"settings.json"),f=m(E,"claude-mem.db"),ae=m(E,"vector-db"),de=m(R,"settings.json"),pe=m(R,"commands"),ce=m(R,"CLAUDE.md");function L(p){F(p,{recursive:!0})}function A(){return m(H,"..","..")}var h=(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))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[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),o=h[e].padEnd(5),a=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let _="";n!=null&&(this.level===0&&typeof n=="object"?_=`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let T="";if(r){let{sessionId:l,sdkSessionId:S,correlationId:u,...d}=r;Object.keys(d).length>0&&(T=` {${Object.entries(d).map(([w,M])=>`${w}=${M}`).join(", ")}}`)}let b=`[${i}] [${o}] [${a}] ${c}${t}${T}${_}`;e===3?console.error(b):console.log(b)}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 N;var R=class{db;constructor(){L(m),this.db=new $(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(`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let T="";if(r){let{sessionId:l,sdkSessionId:b,correlationId:u,...d}=r;Object.keys(d).length>0&&(T=` {${Object.entries(d).map(([w,M])=>`${w}=${M}`).join(", ")}}`)}let S=`[${i}] [${o}] [${a}] ${c}${t}${T}${_}`;e===3?console.error(S):console.log(S)}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 N;var g=class{db;constructor(){L(E),this.db=new j(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(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -188,7 +188,36 @@ ${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)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
`).all(e,s)}getAllRecentObservations(e=100){return this.db.prepare(`
|
||||
SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
|
||||
FROM observations
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentSummaries(e=50){return this.db.prepare(`
|
||||
SELECT id, request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, project, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentUserPrompts(e=100){return this.db.prepare(`
|
||||
SELECT
|
||||
up.id,
|
||||
up.claude_session_id,
|
||||
s.project,
|
||||
up.prompt_number,
|
||||
up.prompt_text,
|
||||
up.created_at,
|
||||
up.created_at_epoch
|
||||
FROM user_prompts up
|
||||
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
ORDER BY up.created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
@@ -337,25 +366,25 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE id <= ? ${i}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${i}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let u=this.db.prepare(l).all(e,...o,t+1),d=this.db.prepare(S).all(e,...o,r+1);if(u.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};a=u.length>0?u[u.length-1].created_at_epoch:s,c=d.length>0?d[d.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary observations:",u.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
|
||||
`;try{let u=this.db.prepare(l).all(e,...o,t+1),d=this.db.prepare(b).all(e,...o,r+1);if(u.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};a=u.length>0?u[u.length-1].created_at_epoch:s,c=d.length>0?d[d.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary observations:",u.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let u=this.db.prepare(l).all(s,...o,t),d=this.db.prepare(S).all(s,...o,r+1);if(u.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};a=u.length>0?u[u.length-1].created_at_epoch:s,c=d.length>0?d[d.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary timestamps:",u.message),{observations:[],sessions:[],prompts:[]}}}let _=`
|
||||
`;try{let u=this.db.prepare(l).all(s,...o,t),d=this.db.prepare(b).all(s,...o,r+1);if(u.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};a=u.length>0?u[u.length-1].created_at_epoch:s,c=d.length>0?d[d.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary timestamps:",u.message),{observations:[],sessions:[],prompts:[]}}}let _=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
|
||||
@@ -365,10 +394,10 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,b=`
|
||||
`,S=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${i.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let l=this.db.prepare(_).all(a,c,...o),S=this.db.prepare(T).all(a,c,...o),u=this.db.prepare(b).all(a,c,...o);return{observations:l,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:u.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function W(p,e,s){return p==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:p==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:p==="UserPromptSubmit"||p==="PostToolUse"?{continue:!0,suppressOutput:!0}:p==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(p,e,s={}){let t=W(p,e,s);return JSON.stringify(t)}import y from"path";import{spawn as D}from"child_process";var G=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);async function k(p=3e3){try{return(await fetch(`http://127.0.0.1:${G}/health`,{signal:AbortSignal.timeout(p)})).ok}catch{return!1}}async function j(p=1e4){let e=Date.now(),s=100;for(;Date.now()-e<p;){if(await k(1e3))return!0;await new Promise(t=>setTimeout(t,s))}return!1}async function x(){if(await k(1e3))return;let p=A(),e=y.join(p,"node_modules",".bin","pm2"),s=y.join(p,"ecosystem.config.cjs"),t=D(e,["list","--no-color"],{cwd:p,stdio:["ignore","pipe","ignore"]}),r="";if(t.stdout?.on("data",o=>{r+=o.toString()}),await new Promise((o,a)=>{t.on("error",c=>a(c)),t.on("close",c=>{o()})}),!(r.includes("claude-mem-worker")&&r.includes("online"))){let o=D(e,["start",s],{cwd:p,stdio:"ignore"});await new Promise((a,c)=>{o.on("error",_=>c(_)),o.on("close",_=>{_!==0&&_!==null?c(new Error(`PM2 start command failed with exit code ${_}`)):a()})})}if(!await j(1e4))throw new Error("Worker failed to become healthy after starting")}async function K(p){if(!p)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=p,r=Y.basename(s);await x();let n=new R,i=n.createSDKSession(e,r,t),o=n.incrementPromptCounter(i);n.saveUserPrompt(e,o,t),console.error(`[new-hook] Session ${i}, prompt #${o}`),n.close();let a=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);try{let c=await fetch(`http://127.0.0.1:${a}/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 _=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${_}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(v("UserPromptSubmit",!0))}var O="";U.on("data",p=>O+=p);U.on("end",async()=>{let p=O?JSON.parse(O):void 0;await K(p)});
|
||||
`;try{let l=this.db.prepare(_).all(a,c,...o),b=this.db.prepare(T).all(a,c,...o),u=this.db.prepare(S).all(a,c,...o);return{observations:l,sessions:b.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:u.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(p,e,s){return p==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:p==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:p==="UserPromptSubmit"||p==="PostToolUse"?{continue:!0,suppressOutput:!0}:p==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(p,e,s={}){let t=$(p,e,s);return JSON.stringify(t)}import y from"path";import{spawn as D}from"child_process";var W=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);async function k(p=100){try{return(await fetch(`http://127.0.0.1:${W}/health`,{signal:AbortSignal.timeout(p)})).ok}catch{return!1}}async function G(p=1e4){let e=Date.now(),s=100;for(;Date.now()-e<p;){if(await k(1e3))return!0;await new Promise(t=>setTimeout(t,s))}return!1}async function x(){if(await k())return;let p=A(),e=y.join(p,"node_modules",".bin","pm2"),s=y.join(p,"ecosystem.config.cjs"),t=D(e,["list","--no-color"],{cwd:p,stdio:["ignore","pipe","ignore"]}),r="";if(t.stdout?.on("data",o=>{r+=o.toString()}),await new Promise((o,a)=>{t.on("error",c=>a(c)),t.on("close",c=>{o()})}),!(r.includes("claude-mem-worker")&&r.includes("online"))){let o=D(e,["start",s],{cwd:p,stdio:"ignore"});await new Promise((a,c)=>{o.on("error",_=>c(_)),o.on("close",_=>{_!==0&&_!==null?c(new Error(`PM2 start command failed with exit code ${_}`)):a()})})}if(!await G(1e4))throw new Error("Worker failed to become healthy after starting")}async function K(p){if(!p)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=p,r=Y.basename(s);await x();let n=new g,i=n.createSDKSession(e,r,t),o=n.incrementPromptCounter(i);n.saveUserPrompt(e,o,t),console.error(`[new-hook] Session ${i}, prompt #${o}`),n.close();let a=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);try{let c=await fetch(`http://127.0.0.1:${a}/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 _=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${_}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(v("UserPromptSubmit",!0))}var O="";U.on("data",p=>O+=p);U.on("end",async()=>{let p=O?JSON.parse(O):void 0;await K(p)});
|
||||
|
||||
+66
-37
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as U}from"process";import $ from"better-sqlite3";import{join as E,dirname as X,basename as J}from"path";import{homedir as L}from"os";import{existsSync as ee,mkdirSync as F}from"fs";import{fileURLToPath as P}from"url";function H(){return typeof __dirname<"u"?__dirname:X(P(import.meta.url))}var B=H(),l=process.env.CLAUDE_MEM_DATA_DIR||E(L(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||E(L(),".claude"),te=E(l,"archives"),re=E(l,"logs"),oe=E(l,"trash"),ne=E(l,"backups"),ie=E(l,"settings.json"),A=E(l,"claude-mem.db"),ae=E(l,"vector-db"),de=E(h,"settings.json"),pe=E(h,"commands"),ce=E(h,"CLAUDE.md");function v(p){F(p,{recursive:!0})}function C(){return E(B,"..","..")}var N=(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))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[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 n=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),a=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let c="";o!=null&&(this.level===0&&typeof o=="object"?c=`
|
||||
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let m="";if(r){let{sessionId:T,sdkSessionId:b,correlationId:u,...d}=r;Object.keys(d).length>0&&(m=` {${Object.entries(d).map(([w,M])=>`${w}=${M}`).join(", ")}}`)}let R=`[${n}] [${i}] [${a}] ${_}${t}${m}${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`})}},S=new O;var g=class{db;constructor(){v(l),this.db=new $(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
import{stdin as U}from"process";import j from"better-sqlite3";import{join as E,dirname as F,basename as J}from"path";import{homedir as L}from"os";import{existsSync as ee,mkdirSync as X}from"fs";import{fileURLToPath as P}from"url";function B(){return typeof __dirname<"u"?__dirname:F(P(import.meta.url))}var H=B(),l=process.env.CLAUDE_MEM_DATA_DIR||E(L(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||E(L(),".claude"),te=E(l,"archives"),re=E(l,"logs"),ne=E(l,"trash"),oe=E(l,"backups"),ie=E(l,"settings.json"),A=E(l,"claude-mem.db"),ae=E(l,"vector-db"),de=E(h,"settings.json"),pe=E(h,"commands"),ce=E(h,"CLAUDE.md");function C(p){X(p,{recursive:!0})}function v(){return E(H,"..","..")}var N=(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))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[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 o=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),a=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let c="";n!=null&&(this.level===0&&typeof n=="object"?c=`
|
||||
`+JSON.stringify(n,null,2):c=" "+this.formatData(n));let m="";if(r){let{sessionId:T,sdkSessionId:g,correlationId:u,...d}=r;Object.keys(d).length>0&&(m=` {${Object.entries(d).map(([w,M])=>`${w}=${M}`).join(", ")}}`)}let S=`[${o}] [${i}] [${a}] ${_}${t}${m}${c}`;e===3?console.error(S):console.log(S)}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`})}},b=new O;var R=class{db;constructor(){C(l),this.db=new j(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -188,7 +188,36 @@ ${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)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
`).all(e,s)}getAllRecentObservations(e=100){return this.db.prepare(`
|
||||
SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
|
||||
FROM observations
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentSummaries(e=50){return this.db.prepare(`
|
||||
SELECT id, request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, project, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentUserPrompts(e=100){return this.db.prepare(`
|
||||
SELECT
|
||||
up.id,
|
||||
up.claude_session_id,
|
||||
s.project,
|
||||
up.prompt_number,
|
||||
up.prompt_text,
|
||||
up.created_at,
|
||||
up.created_at_epoch
|
||||
FROM user_prompts up
|
||||
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
ORDER BY up.created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
@@ -214,12 +243,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id = ?
|
||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id IN (${i})
|
||||
ORDER BY created_at_epoch ${o}
|
||||
${n}
|
||||
ORDER BY created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getSummaryForSession(e){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
@@ -232,7 +261,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,o=new Set;for(let n of t){if(n.files_read)try{let i=JSON.parse(n.files_read);Array.isArray(i)&&i.forEach(a=>r.add(a))}catch{}if(n.files_modified)try{let i=JSON.parse(n.files_modified);Array.isArray(i)&&i.forEach(a=>o.add(a))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,n=new Set;for(let o of t){if(o.files_read)try{let i=JSON.parse(o.files_read);Array.isArray(i)&&i.forEach(a=>r.add(a))}catch{}if(o.files_modified)try{let i=JSON.parse(o.files_modified);Array.isArray(i)&&i.forEach(a=>n.add(a))}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 = ?
|
||||
@@ -259,17 +288,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(),i=this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),i=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 i.lastInsertRowid===0||i.changes===0?this.db.prepare(`
|
||||
`).run(e,e,s,t,r.toISOString(),n);return i.lastInsertRowid===0||i.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:i.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?(S.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?(b.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 = ?
|
||||
@@ -278,33 +307,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}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return 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(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,n=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,o=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,o.toISOString(),n),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(`
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=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(),n);return{id:Number(c.lastInsertRowid),createdAtEpoch:n}}storeSummary(e,s,t,r){let o=new Date,n=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(c.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=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,o.toISOString(),n),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(`
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),n);return{id:Number(c.lastInsertRowid),createdAtEpoch:n}}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,n.toISOString(),o);return{id:Number(c.lastInsertRowid),createdAtEpoch:o}}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 = ?
|
||||
@@ -316,12 +345,12 @@ ${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}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).run(e.toISOString(),s).changes}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT * FROM session_summaries
|
||||
WHERE id IN (${i})
|
||||
ORDER BY created_at_epoch ${o}
|
||||
${n}
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
ORDER BY created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT
|
||||
up.*,
|
||||
s.project,
|
||||
@@ -329,46 +358,46 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.id IN (${i})
|
||||
ORDER BY up.created_at_epoch ${o}
|
||||
${n}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,o){let n=o?"AND project = ?":"",i=o?[o]:[],a,_;if(e!==null){let T=`
|
||||
ORDER BY up.created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],a,_;if(e!==null){let T=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${n}
|
||||
WHERE id <= ? ${o}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,b=`
|
||||
`,g=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${n}
|
||||
WHERE id >= ? ${o}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let u=this.db.prepare(T).all(e,...i,t+1),d=this.db.prepare(b).all(e,...i,r+1);if(u.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};a=u.length>0?u[u.length-1].created_at_epoch:s,_=d.length>0?d[d.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary observations:",u.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
|
||||
`;try{let u=this.db.prepare(T).all(e,...i,t+1),d=this.db.prepare(g).all(e,...i,r+1);if(u.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};a=u.length>0?u[u.length-1].created_at_epoch:s,_=d.length>0?d[d.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary observations:",u.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${n}
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,b=`
|
||||
`,g=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${n}
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let u=this.db.prepare(T).all(s,...i,t),d=this.db.prepare(b).all(s,...i,r+1);if(u.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};a=u.length>0?u[u.length-1].created_at_epoch:s,_=d.length>0?d[d.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary timestamps:",u.message),{observations:[],sessions:[],prompts:[]}}}let c=`
|
||||
`;try{let u=this.db.prepare(T).all(s,...i,t),d=this.db.prepare(g).all(s,...i,r+1);if(u.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};a=u.length>0?u[u.length-1].created_at_epoch:s,_=d.length>0?d[d.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary timestamps:",u.message),{observations:[],sessions:[],prompts:[]}}}let c=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${n}
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,m=`
|
||||
SELECT *
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${n}
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,R=`
|
||||
`,S=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${n.replace("project","s.project")}
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let T=this.db.prepare(c).all(a,_,...i),b=this.db.prepare(m).all(a,_,...i),u=this.db.prepare(R).all(a,_,...i);return{observations:T,sessions:b.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:u.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function W(p,e,s){return p==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:p==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:p==="UserPromptSubmit"||p==="PostToolUse"?{continue:!0,suppressOutput:!0}:p==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(p,e,s={}){let t=W(p,e,s);return JSON.stringify(t)}import y from"path";import{spawn as D}from"child_process";var G=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);async function k(p=3e3){try{return(await fetch(`http://127.0.0.1:${G}/health`,{signal:AbortSignal.timeout(p)})).ok}catch{return!1}}async function j(p=1e4){let e=Date.now(),s=100;for(;Date.now()-e<p;){if(await k(1e3))return!0;await new Promise(t=>setTimeout(t,s))}return!1}async function x(){if(await k(1e3))return;let p=C(),e=y.join(p,"node_modules",".bin","pm2"),s=y.join(p,"ecosystem.config.cjs"),t=D(e,["list","--no-color"],{cwd:p,stdio:["ignore","pipe","ignore"]}),r="";if(t.stdout?.on("data",i=>{r+=i.toString()}),await new Promise((i,a)=>{t.on("error",_=>a(_)),t.on("close",_=>{i()})}),!(r.includes("claude-mem-worker")&&r.includes("online"))){let i=D(e,["start",s],{cwd:p,stdio:"ignore"});await new Promise((a,_)=>{i.on("error",c=>_(c)),i.on("close",c=>{c!==0&&c!==null?_(new Error(`PM2 start command failed with exit code ${c}`)):a()})})}if(!await j(1e4))throw new Error("Worker failed to become healthy after starting")}var Y=new Set(["ListMcpResourcesTool"]);async function K(p){if(!p)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=p;if(Y.has(s)){console.log(f("PostToolUse",!0));return}await x();let o=new g,n=o.createSDKSession(e,"",""),i=o.getPromptCounter(n);o.close();let a=S.formatTool(s,t),_=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);S.dataIn("HOOK",`PostToolUse: ${a}`,{sessionId:n,workerPort:_});try{let c=await fetch(`http://127.0.0.1:${_}/sessions/${n}/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:i}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let m=await c.text();throw S.failure("HOOK","Failed to send observation",{sessionId:n,status:c.status},m),new Error(`Failed to send observation to worker: ${c.status} ${m}`)}S.debug("HOOK","Observation sent successfully",{sessionId:n,toolName:s})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var I="";U.on("data",p=>I+=p);U.on("end",async()=>{let p=I?JSON.parse(I):void 0;await K(p)});
|
||||
`;try{let T=this.db.prepare(c).all(a,_,...i),g=this.db.prepare(m).all(a,_,...i),u=this.db.prepare(S).all(a,_,...i);return{observations:T,sessions:g.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:u.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(p,e,s){return p==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:p==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:p==="UserPromptSubmit"||p==="PostToolUse"?{continue:!0,suppressOutput:!0}:p==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(p,e,s={}){let t=$(p,e,s);return JSON.stringify(t)}import y from"path";import{spawn as D}from"child_process";var W=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);async function k(p=100){try{return(await fetch(`http://127.0.0.1:${W}/health`,{signal:AbortSignal.timeout(p)})).ok}catch{return!1}}async function G(p=1e4){let e=Date.now(),s=100;for(;Date.now()-e<p;){if(await k(1e3))return!0;await new Promise(t=>setTimeout(t,s))}return!1}async function x(){if(await k())return;let p=v(),e=y.join(p,"node_modules",".bin","pm2"),s=y.join(p,"ecosystem.config.cjs"),t=D(e,["list","--no-color"],{cwd:p,stdio:["ignore","pipe","ignore"]}),r="";if(t.stdout?.on("data",i=>{r+=i.toString()}),await new Promise((i,a)=>{t.on("error",_=>a(_)),t.on("close",_=>{i()})}),!(r.includes("claude-mem-worker")&&r.includes("online"))){let i=D(e,["start",s],{cwd:p,stdio:"ignore"});await new Promise((a,_)=>{i.on("error",c=>_(c)),i.on("close",c=>{c!==0&&c!==null?_(new Error(`PM2 start command failed with exit code ${c}`)):a()})})}if(!await G(1e4))throw new Error("Worker failed to become healthy after starting")}var Y=new Set(["ListMcpResourcesTool"]);async function K(p){if(!p)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_response:r}=p;if(Y.has(s)){console.log(f("PostToolUse",!0));return}await x();let n=new R,o=n.createSDKSession(e,"",""),i=n.getPromptCounter(o);n.close();let a=b.formatTool(s,t),_=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);b.dataIn("HOOK",`PostToolUse: ${a}`,{sessionId:o,workerPort:_});try{let c=await fetch(`http://127.0.0.1:${_}/sessions/${o}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_response:r!==void 0?JSON.stringify(r):"{}",prompt_number:i}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let m=await c.text();throw b.failure("HOOK","Failed to send observation",{sessionId:o,status:c.status},m),new Error(`Failed to send observation to worker: ${c.status} ${m}`)}b.debug("HOOK","Observation sent successfully",{sessionId:o,toolName:s})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var I="";U.on("data",p=>I+=p);U.on("end",async()=>{let p=I?JSON.parse(I):void 0;await K(p)});
|
||||
|
||||
@@ -135,9 +135,9 @@ import{Server as he}from"@modelcontextprotocol/sdk/server/index.js";import{Stdio
|
||||
FROM user_prompts
|
||||
WHERE claude_session_id = ?
|
||||
ORDER BY prompt_number ASC
|
||||
`).all(e)}close(){this.db.close()}};import me from"better-sqlite3";var K=(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))(K||{}),Q=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=K[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,r){return`obs-${e}-${r}`}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}
|
||||
`).all(e)}close(){this.db.close()}};import me from"better-sqlite3";var K=(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))(K||{}),J=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=K[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,r){return`obs-${e}-${r}`}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 r=Object.keys(e);return r.length===0?"{}":r.length<=3?JSON.stringify(e):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,r){if(!r)return e;try{let s=typeof r=="string"?JSON.parse(r):r;if(e==="Bash"&&s.command){let t=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${t})`}if(e==="Read"&&s.file_path){let t=s.file_path.split("/").pop()||s.file_path;return`${e}(${t})`}if(e==="Edit"&&s.file_path){let t=s.file_path.split("/").pop()||s.file_path;return`${e}(${t})`}if(e==="Write"&&s.file_path){let t=s.file_path.split("/").pop()||s.file_path;return`${e}(${t})`}return e}catch{return e}}log(e,r,s,t,o){if(e<this.level)return;let n=new Date().toISOString().replace("T"," ").substring(0,23),a=K[e].padEnd(5),d=r.padEnd(6),l="";t?.correlationId?l=`[${t.correlationId}] `:t?.sessionId&&(l=`[session-${t.sessionId}] `);let u="";o!=null&&(this.level===0&&typeof o=="object"?u=`
|
||||
`+JSON.stringify(o,null,2):u=" "+this.formatData(o));let p="";if(t){let{sessionId:f,sdkSessionId:h,correlationId:b,..._}=t;Object.keys(_).length>0&&(p=` {${Object.entries(_).map(([x,T])=>`${x}=${T}`).join(", ")}}`)}let m=`[${n}] [${a}] [${d}] ${l}${s}${p}${u}`;e===3?console.error(m):console.log(m)}debug(e,r,s,t){this.log(0,e,r,s,t)}info(e,r,s,t){this.log(1,e,r,s,t)}warn(e,r,s,t){this.log(2,e,r,s,t)}error(e,r,s,t){this.log(3,e,r,s,t)}dataIn(e,r,s,t){this.info(e,`\u2192 ${r}`,s,t)}dataOut(e,r,s,t){this.info(e,`\u2190 ${r}`,s,t)}success(e,r,s,t){this.info(e,`\u2713 ${r}`,s,t)}failure(e,r,s,t){this.error(e,`\u2717 ${r}`,s,t)}timing(e,r,s,t){this.info(e,`\u23F1 ${r}`,t,{duration:`${s}ms`})}},se=new Q;var H=class{db;constructor(){P(w),this.db=new me(X),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(`
|
||||
`+JSON.stringify(o,null,2):u=" "+this.formatData(o));let p="";if(t){let{sessionId:f,sdkSessionId:h,correlationId:b,..._}=t;Object.keys(_).length>0&&(p=` {${Object.entries(_).map(([x,T])=>`${x}=${T}`).join(", ")}}`)}let m=`[${n}] [${a}] [${d}] ${l}${s}${p}${u}`;e===3?console.error(m):console.log(m)}debug(e,r,s,t){this.log(0,e,r,s,t)}info(e,r,s,t){this.log(1,e,r,s,t)}warn(e,r,s,t){this.log(2,e,r,s,t)}error(e,r,s,t){this.log(3,e,r,s,t)}dataIn(e,r,s,t){this.info(e,`\u2192 ${r}`,s,t)}dataOut(e,r,s,t){this.info(e,`\u2190 ${r}`,s,t)}success(e,r,s,t){this.info(e,`\u2713 ${r}`,s,t)}failure(e,r,s,t){this.error(e,`\u2717 ${r}`,s,t)}timing(e,r,s,t){this.info(e,`\u23F1 ${r}`,t,{duration:`${s}ms`})}},se=new J;var H=class{db;constructor(){P(w),this.db=new me(X),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,
|
||||
@@ -324,7 +324,36 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let r=Obje
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,r)}getRecentSessionsWithStatus(e,r=3){return this.db.prepare(`
|
||||
`).all(e,r)}getAllRecentObservations(e=100){return this.db.prepare(`
|
||||
SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
|
||||
FROM observations
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentSummaries(e=50){return this.db.prepare(`
|
||||
SELECT id, request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, project, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentUserPrompts(e=100){return this.db.prepare(`
|
||||
SELECT
|
||||
up.id,
|
||||
up.claude_session_id,
|
||||
s.project,
|
||||
up.prompt_number,
|
||||
up.prompt_text,
|
||||
up.created_at,
|
||||
up.created_at_epoch
|
||||
FROM user_prompts up
|
||||
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
ORDER BY up.created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
ORDER BY project ASC
|
||||
`).all().map(s=>s.project)}getRecentSessionsWithStatus(e,r=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
@@ -585,4 +614,4 @@ No previous sessions found for project "${e}".`}]};let t=[];t.push("# Recent Ses
|
||||
`);return{content:[{type:"text",text:o}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"get_context_timeline",description:'Get a unified timeline of context (observations, sessions, and prompts) around a specific point in time. All record types are interleaved chronologically. Useful for understanding "what was happening when X occurred". Returns depth_before records before anchor + anchor + depth_after records after (total: depth_before + 1 + depth_after mixed records).',inputSchema:i.object({anchor:i.union([i.number().describe("Observation ID to center timeline around"),i.string().describe("Session ID (format: S123) or ISO timestamp to center timeline around")]).describe('Anchor point: observation ID, session ID (e.g., "S123"), or ISO timestamp'),depth_before:i.number().min(0).max(50).default(10).describe("Number of records to retrieve before anchor, not including anchor (default: 10)"),depth_after:i.number().min(0).max(50).default(10).describe("Number of records to retrieve after anchor, not including anchor (default: 10)"),project:i.string().optional().describe("Filter by project name")}),handler:async c=>{try{let f=function(g){return new Date(g).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},h=function(g){return new Date(g).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},b=function(g){return new Date(g).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},_=function(g){return g?Math.ceil(g.length/4):0};var e=f,r=h,s=b,t=_;let{anchor:o,depth_before:n=10,depth_after:a=10,project:d}=c,l,u=o,p;if(typeof o=="number"){let g=N.getObservationById(o);if(!g)return{content:[{type:"text",text:`Observation #${o} not found`}],isError:!0};l=g.created_at_epoch,p=N.getTimelineAroundObservation(o,l,n,a,d)}else if(typeof o=="string")if(o.startsWith("S")||o.startsWith("#S")){let g=o.replace(/^#?S/,""),I=parseInt(g,10),S=N.getSessionSummariesByIds([I]);if(S.length===0)return{content:[{type:"text",text:`Session #${I} not found`}],isError:!0};l=S[0].created_at_epoch,u=`S${I}`,p=N.getTimelineAroundTimestamp(l,n,a,d)}else{let g=new Date(o);if(isNaN(g.getTime()))return{content:[{type:"text",text:`Invalid timestamp: ${o}`}],isError:!0};l=g.getTime(),p=N.getTimelineAroundTimestamp(l,n,a,d)}else return{content:[{type:"text",text:'Invalid anchor: must be observation ID (number), session ID (e.g., "S123"), or ISO timestamp'}],isError:!0};let m=[...p.observations.map(g=>({type:"observation",data:g,epoch:g.created_at_epoch})),...p.sessions.map(g=>({type:"session",data:g,epoch:g.created_at_epoch})),...p.prompts.map(g=>({type:"prompt",data:g,epoch:g.created_at_epoch}))];if(m.sort((g,I)=>g.epoch-I.epoch),m.length===0)return{content:[{type:"text",text:`No context found around ${new Date(l).toLocaleString()} (${n} records before, ${a} records after)`}]};let E=[];E.push(`# Timeline around anchor: ${u}`),E.push(`**Window:** ${n} records before \u2192 ${a} records after | **Items:** ${m.length} (${p.observations.length} obs, ${p.sessions.length} sessions, ${p.prompts.length} prompts)`),E.push(""),E.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),E.push("");let x=new Map;for(let g of m){let I=f(g.epoch);x.has(I)||x.set(I,[]),x.get(I).push(g)}let T=Array.from(x.entries()).sort((g,I)=>{let S=new Date(g[0]).getTime(),O=new Date(I[0]).getTime();return S-O});for(let[g,I]of T){E.push(`### ${g}`),E.push("");let S=null,O="",C=!1;for(let v of I){let F=typeof u=="number"&&v.type==="observation"&&v.data.id===u||typeof u=="string"&&u.startsWith("S")&&v.type==="session"&&`S${v.data.id}`===u;if(v.type==="session"){C&&(E.push(""),C=!1,S=null,O="");let y=v.data,U=y.request||"Session summary",R=`claude-mem://session-summary/${y.id}`,A=F?" \u2190 **ANCHOR**":"";E.push(`**\u{1F3AF} #S${y.id}** ${U} (${b(v.epoch)}) [\u2192](${R})${A}`),E.push("")}else if(v.type==="prompt"){C&&(E.push(""),C=!1,S=null,O="");let y=v.data,U=y.prompt.length>100?y.prompt.substring(0,100)+"...":y.prompt;E.push(`**\u{1F4AC} User Prompt #${y.prompt_number}** (${b(v.epoch)})`),E.push(`> ${U}`),E.push("")}else if(v.type==="observation"){let y=v.data,U="General";U!==S&&(C&&E.push(""),E.push(`**${U}**`),E.push("| ID | Time | T | Title | Tokens |"),E.push("|----|------|---|-------|--------|"),S=U,C=!0,O="");let R="\u2022";switch(y.type){case"bugfix":R="\u{1F534}";break;case"feature":R="\u{1F7E3}";break;case"refactor":R="\u{1F504}";break;case"change":R="\u2705";break;case"discovery":R="\u{1F535}";break;case"decision":R="\u{1F9E0}";break}let A=h(v.epoch),D=y.title||"Untitled",B=_(y.narrative),Y=A!==O?A:"\u2033";O=A;let Z=F?" \u2190 **ANCHOR**":"";E.push(`| #${y.id} | ${Y} | ${R} | ${D}${Z} | ~${B} |`)}}C&&E.push("")}return{content:[{type:"text",text:E.join(`
|
||||
`)}]}}catch(o){return{content:[{type:"text",text:`Timeline query failed: ${o.message}`}],isError:!0}}}},{name:"get_timeline_by_query",description:'Search for observations using natural language and get timeline context around the best match. Two modes: "auto" (default) automatically uses top result as timeline anchor; "interactive" returns top matches for you to choose from. This combines search + timeline into a single operation for faster context discovery.',inputSchema:i.object({query:i.string().describe("Natural language search query to find relevant observations"),mode:i.enum(["auto","interactive"]).default("auto").describe("auto: Automatically use top search result as timeline anchor. interactive: Show top N search results for manual anchor selection."),depth_before:i.number().min(0).max(50).default(10).describe("Number of timeline records before anchor (default: 10)"),depth_after:i.number().min(0).max(50).default(10).describe("Number of timeline records after anchor (default: 10)"),limit:i.number().min(1).max(20).default(5).describe("For interactive mode: number of top search results to display (default: 5)"),project:i.string().optional().describe("Filter by project name")}),handler:async c=>{try{let{query:o,mode:n="auto",depth_before:a=10,depth_after:d=10,limit:l=5,project:u}=c,p=[];if(k)try{console.error("[search-server] Using hybrid semantic search for timeline query");let m=await M(o,100);if(console.error(`[search-server] Chroma returned ${m.ids.length} semantic matches`),m.ids.length>0){let f=Date.now()-7776e6,h=m.ids.filter((b,_)=>{let E=m.metadatas[_];return E&&E.created_at_epoch>f});console.error(`[search-server] ${h.length} results within 90-day window`),h.length>0&&(p=N.getObservationsByIds(h,{orderBy:"date_desc",limit:n==="auto"?1:l}),console.error(`[search-server] Hydrated ${p.length} observations from SQLite`))}}catch(m){console.error("[search-server] Chroma query failed, falling back to FTS5:",m.message)}if(p.length===0&&(console.error("[search-server] Using FTS5 keyword search"),p=$.searchObservations(o,{orderBy:"relevance",limit:n==="auto"?1:l,project:u})),p.length===0)return{content:[{type:"text",text:`No observations found matching "${o}". Try a different search query.`}]};if(n==="interactive"){let m=[];m.push("# Timeline Anchor Search Results"),m.push(""),m.push(`Found ${p.length} observation(s) matching "${o}"`),m.push(""),m.push("To get timeline context around any of these observations, use the `get_context_timeline` tool with the observation ID as the anchor."),m.push(""),m.push(`**Top ${p.length} matches:**`),m.push("");for(let f=0;f<p.length;f++){let h=p[f],b=h.title||`Observation #${h.id}`,_=new Date(h.created_at_epoch).toLocaleString(),E=h.type?`[${h.type}]`:"";m.push(`${f+1}. **${E} ${b}**`),m.push(` - ID: ${h.id}`),m.push(` - Date: ${_}`),h.subtitle&&m.push(` - ${h.subtitle}`),m.push(` - Source: claude-mem://observation/${h.id}`),m.push("")}return{content:[{type:"text",text:m.join(`
|
||||
`)}]}}else{let b=function(S){return new Date(S).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},_=function(S){return new Date(S).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},E=function(S){return new Date(S).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},x=function(S){return S?Math.ceil(S.length/4):0};var e=b,r=_,s=E,t=x;let m=p[0];console.error(`[search-server] Auto mode: Using observation #${m.id} as timeline anchor`);let f=N.getTimelineAroundObservation(m.id,m.created_at_epoch,a,d,u),h=[...f.observations.map(S=>({type:"observation",data:S,epoch:S.created_at_epoch})),...f.sessions.map(S=>({type:"session",data:S,epoch:S.created_at_epoch})),...f.prompts.map(S=>({type:"prompt",data:S,epoch:S.created_at_epoch}))];if(h.sort((S,O)=>S.epoch-O.epoch),h.length===0)return{content:[{type:"text",text:`Found observation #${m.id} matching "${o}", but no timeline context available (${a} records before, ${d} records after).`}]};let T=[];T.push(`# Timeline for query: "${o}"`),T.push(`**Anchor:** Observation #${m.id} - ${m.title||"Untitled"}`),T.push(`**Window:** ${a} records before \u2192 ${d} records after | **Items:** ${h.length} (${f.observations.length} obs, ${f.sessions.length} sessions, ${f.prompts.length} prompts)`),T.push(""),T.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),T.push("");let g=new Map;for(let S of h){let O=b(S.epoch);g.has(O)||g.set(O,[]),g.get(O).push(S)}let I=Array.from(g.entries()).sort((S,O)=>{let C=new Date(S[0]).getTime(),v=new Date(O[0]).getTime();return C-v});for(let[S,O]of I){T.push(`### ${S}`),T.push("");let C=null,v="",F=!1;for(let y of O){let U=y.type==="observation"&&y.data.id===m.id;if(y.type==="session"){F&&(T.push(""),F=!1,C=null,v="");let R=y.data,A=R.request||"Session summary",D=`claude-mem://session-summary/${R.id}`;T.push(`**\u{1F3AF} #S${R.id}** ${A} (${E(y.epoch)}) [\u2192](${D})`),T.push("")}else if(y.type==="prompt"){F&&(T.push(""),F=!1,C=null,v="");let R=y.data,A=R.prompt.length>100?R.prompt.substring(0,100)+"...":R.prompt;T.push(`**\u{1F4AC} User Prompt #${R.prompt_number}** (${E(y.epoch)})`),T.push(`> ${A}`),T.push("")}else if(y.type==="observation"){let R=y.data,A="General";A!==C&&(F&&T.push(""),T.push(`**${A}**`),T.push("| ID | Time | T | Title | Tokens |"),T.push("|----|------|---|-------|--------|"),C=A,F=!0,v="");let D="\u2022";switch(R.type){case"bugfix":D="\u{1F534}";break;case"feature":D="\u{1F7E3}";break;case"refactor":D="\u{1F504}";break;case"change":D="\u2705";break;case"discovery":D="\u{1F535}";break;case"decision":D="\u{1F9E0}";break}let B=_(y.epoch),z=R.title||"Untitled",Y=x(R.narrative),ie=B!==v?B:"\u2033";v=B;let ae=U?" \u2190 **ANCHOR**":"";T.push(`| #${R.id} | ${ie} | ${D} | ${z}${ae} | ~${Y} |`)}}F&&T.push("")}return{content:[{type:"text",text:T.join(`
|
||||
`)}]}}}catch(o){return{content:[{type:"text",text:`Timeline query failed: ${o.message}`}],isError:!0}}}}],J=new he({name:"claude-mem-search",version:"1.0.0"},{capabilities:{tools:{}}});J.setRequestHandler(ge,async()=>({tools:oe.map(c=>({name:c.name,description:c.description,inputSchema:Te(c.inputSchema)}))}));J.setRequestHandler(be,async c=>{let e=oe.find(r=>r.name===c.params.name);if(!e)throw new Error(`Unknown tool: ${c.params.name}`);try{return await e.handler(c.params.arguments||{})}catch(r){return{content:[{type:"text",text:`Tool execution failed: ${r.message}`}],isError:!0}}});async function Ie(){let c=new _e;await J.connect(c),console.error("[search-server] Claude-mem search server started"),setTimeout(async()=>{try{console.error("[search-server] Initializing Chroma client...");let e=new Ee({command:"uvx",args:["chroma-mcp","--client-type","persistent","--data-dir",te],stderr:"ignore"}),r=new fe({name:"claude-mem-search-chroma-client",version:"1.0.0"},{capabilities:{}});await r.connect(e),k=r,console.error("[search-server] Chroma client connected successfully")}catch(e){console.error("[search-server] Failed to initialize Chroma client:",e.message),console.error("[search-server] Falling back to FTS5-only search"),k=null}},0)}Ie().catch(c=>{console.error("[search-server] Fatal error:",c),process.exit(1)});
|
||||
`)}]}}}catch(o){return{content:[{type:"text",text:`Timeline query failed: ${o.message}`}],isError:!0}}}}],Q=new he({name:"claude-mem-search",version:"1.0.0"},{capabilities:{tools:{}}});Q.setRequestHandler(ge,async()=>({tools:oe.map(c=>({name:c.name,description:c.description,inputSchema:Te(c.inputSchema)}))}));Q.setRequestHandler(be,async c=>{let e=oe.find(r=>r.name===c.params.name);if(!e)throw new Error(`Unknown tool: ${c.params.name}`);try{return await e.handler(c.params.arguments||{})}catch(r){return{content:[{type:"text",text:`Tool execution failed: ${r.message}`}],isError:!0}}});async function Ie(){let c=new _e;await Q.connect(c),console.error("[search-server] Claude-mem search server started"),setTimeout(async()=>{try{console.error("[search-server] Initializing Chroma client...");let e=new Ee({command:"uvx",args:["chroma-mcp","--client-type","persistent","--data-dir",te],stderr:"ignore"}),r=new fe({name:"claude-mem-search-chroma-client",version:"1.0.0"},{capabilities:{}});await r.connect(e),k=r,console.error("[search-server] Chroma client connected successfully")}catch(e){console.error("[search-server] Failed to initialize Chroma client:",e.message),console.error("[search-server] Falling back to FTS5-only search"),k=null}},0)}Ie().catch(c=>{console.error("[search-server] Fatal error:",c),process.exit(1)});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as U}from"process";import $ from"better-sqlite3";import{join as E,dirname as X,basename as q}from"path";import{homedir as I}from"os";import{existsSync as Z,mkdirSync as F}from"fs";import{fileURLToPath as P}from"url";function H(){return typeof __dirname<"u"?__dirname:X(P(import.meta.url))}var B=H(),m=process.env.CLAUDE_MEM_DATA_DIR||E(I(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||E(I(),".claude"),se=E(m,"archives"),te=E(m,"logs"),re=E(m,"trash"),oe=E(m,"backups"),ne=E(m,"settings.json"),L=E(m,"claude-mem.db"),ie=E(m,"vector-db"),ae=E(h,"settings.json"),de=E(h,"commands"),pe=E(h,"CLAUDE.md");function A(d){F(d,{recursive:!0})}function C(){return E(B,"..","..")}var N=(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))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[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 n=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),p=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let u="";o!=null&&(this.level===0&&typeof o=="object"?u=`
|
||||
`+JSON.stringify(o,null,2):u=" "+this.formatData(o));let T="";if(r){let{sessionId:l,sdkSessionId:S,correlationId:_,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([w,M])=>`${w}=${M}`).join(", ")}}`)}let R=`[${n}] [${i}] [${p}] ${c}${t}${T}${u}`;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`})}},b=new O;var g=class{db;constructor(){A(m),this.db=new $(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(`
|
||||
import{stdin as U}from"process";import j from"better-sqlite3";import{join as m,dirname as F,basename as V}from"path";import{homedir as f}from"os";import{existsSync as Z,mkdirSync as X}from"fs";import{fileURLToPath as P}from"url";function B(){return typeof __dirname<"u"?__dirname:F(P(import.meta.url))}var H=B(),E=process.env.CLAUDE_MEM_DATA_DIR||m(f(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||m(f(),".claude"),se=m(E,"archives"),te=m(E,"logs"),re=m(E,"trash"),ne=m(E,"backups"),oe=m(E,"settings.json"),L=m(E,"claude-mem.db"),ie=m(E,"vector-db"),ae=m(h,"settings.json"),de=m(h,"commands"),pe=m(h,"CLAUDE.md");function A(d){X(d,{recursive:!0})}function C(){return m(H,"..","..")}var N=(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))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[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 o=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),p=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
|
||||
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let T="";if(r){let{sessionId:l,sdkSessionId:b,correlationId:_,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([w,M])=>`${w}=${M}`).join(", ")}}`)}let S=`[${o}] [${i}] [${p}] ${c}${t}${T}${u}`;e===3?console.error(S):console.log(S)}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`})}},g=new O;var R=class{db;constructor(){A(E),this.db=new j(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,
|
||||
@@ -188,7 +188,36 @@ ${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)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
`).all(e,s)}getAllRecentObservations(e=100){return this.db.prepare(`
|
||||
SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
|
||||
FROM observations
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentSummaries(e=50){return this.db.prepare(`
|
||||
SELECT id, request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, project, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllRecentUserPrompts(e=100){return this.db.prepare(`
|
||||
SELECT
|
||||
up.id,
|
||||
up.claude_session_id,
|
||||
s.project,
|
||||
up.prompt_number,
|
||||
up.prompt_text,
|
||||
up.created_at,
|
||||
up.created_at_epoch
|
||||
FROM user_prompts up
|
||||
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
ORDER BY up.created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
@@ -214,12 +243,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id = ?
|
||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id IN (${i})
|
||||
ORDER BY created_at_epoch ${o}
|
||||
${n}
|
||||
ORDER BY created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getSummaryForSession(e){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
@@ -232,7 +261,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,o=new Set;for(let n of t){if(n.files_read)try{let i=JSON.parse(n.files_read);Array.isArray(i)&&i.forEach(p=>r.add(p))}catch{}if(n.files_modified)try{let i=JSON.parse(n.files_modified);Array.isArray(i)&&i.forEach(p=>o.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,n=new Set;for(let o of t){if(o.files_read)try{let i=JSON.parse(o.files_read);Array.isArray(i)&&i.forEach(p=>r.add(p))}catch{}if(o.files_modified)try{let i=JSON.parse(o.files_modified);Array.isArray(i)&&i.forEach(p=>n.add(p))}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 = ?
|
||||
@@ -259,17 +288,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(),i=this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),i=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 i.lastInsertRowid===0||i.changes===0?this.db.prepare(`
|
||||
`).run(e,e,s,t,r.toISOString(),n);return i.lastInsertRowid===0||i.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:i.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?(b.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?(g.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 = ?
|
||||
@@ -278,33 +307,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}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return 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(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,n=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,o=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,o.toISOString(),n),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=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(),n);return{id:Number(u.lastInsertRowid),createdAtEpoch:n}}storeSummary(e,s,t,r){let o=new Date,n=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=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,o.toISOString(),n),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),n);return{id:Number(u.lastInsertRowid),createdAtEpoch:n}}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,n.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}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 = ?
|
||||
@@ -316,12 +345,12 @@ ${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}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).run(e.toISOString(),s).changes}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT * FROM session_summaries
|
||||
WHERE id IN (${i})
|
||||
ORDER BY created_at_epoch ${o}
|
||||
${n}
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
ORDER BY created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT
|
||||
up.*,
|
||||
s.project,
|
||||
@@ -329,46 +358,46 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.id IN (${i})
|
||||
ORDER BY up.created_at_epoch ${o}
|
||||
${n}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,o){let n=o?"AND project = ?":"",i=o?[o]:[],p,c;if(e!==null){let l=`
|
||||
ORDER BY up.created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],p,c;if(e!==null){let l=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${n}
|
||||
WHERE id <= ? ${o}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${n}
|
||||
WHERE id >= ? ${o}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(l).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,c=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
|
||||
`;try{let _=this.db.prepare(l).all(e,...i,t+1),a=this.db.prepare(b).all(e,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,c=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${n}
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${n}
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(l).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,c=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let u=`
|
||||
`;try{let _=this.db.prepare(l).all(s,...i,t),a=this.db.prepare(b).all(s,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,c=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let u=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${n}
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,T=`
|
||||
SELECT *
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${n}
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,R=`
|
||||
`,S=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${n.replace("project","s.project")}
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let l=this.db.prepare(u).all(p,c,...i),S=this.db.prepare(T).all(p,c,...i),_=this.db.prepare(R).all(p,c,...i);return{observations:l,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function W(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(d,e,s={}){let t=W(d,e,s);return JSON.stringify(t)}import y from"path";import{spawn as D}from"child_process";var G=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);async function k(d=3e3){try{return(await fetch(`http://127.0.0.1:${G}/health`,{signal:AbortSignal.timeout(d)})).ok}catch{return!1}}async function j(d=1e4){let e=Date.now(),s=100;for(;Date.now()-e<d;){if(await k(1e3))return!0;await new Promise(t=>setTimeout(t,s))}return!1}async function x(){if(await k(1e3))return;let d=C(),e=y.join(d,"node_modules",".bin","pm2"),s=y.join(d,"ecosystem.config.cjs"),t=D(e,["list","--no-color"],{cwd:d,stdio:["ignore","pipe","ignore"]}),r="";if(t.stdout?.on("data",i=>{r+=i.toString()}),await new Promise((i,p)=>{t.on("error",c=>p(c)),t.on("close",c=>{i()})}),!(r.includes("claude-mem-worker")&&r.includes("online"))){let i=D(e,["start",s],{cwd:d,stdio:"ignore"});await new Promise((p,c)=>{i.on("error",u=>c(u)),i.on("close",u=>{u!==0&&u!==null?c(new Error(`PM2 start command failed with exit code ${u}`)):p()})})}if(!await j(1e4))throw new Error("Worker failed to become healthy after starting")}async function Y(d){if(!d)throw new Error("summaryHook requires input");let{session_id:e}=d;await x();let s=new g,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t);s.close();let o=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);b.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:o,promptNumber:r});try{let n=await fetch(`http://127.0.0.1:${o}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(!n.ok){let i=await n.text();throw b.failure("HOOK","Failed to generate summary",{sessionId:t,status:n.status},i),new Error(`Failed to request summary from worker: ${n.status} ${i}`)}b.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(n){throw n.cause?.code==="ECONNREFUSED"||n.name==="TimeoutError"||n.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):n}console.log(v("Stop",!0))}var f="";U.on("data",d=>f+=d);U.on("end",async()=>{let d=f?JSON.parse(f):void 0;await Y(d)});
|
||||
`;try{let l=this.db.prepare(u).all(p,c,...i),b=this.db.prepare(T).all(p,c,...i),_=this.db.prepare(S).all(p,c,...i);return{observations:l,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(d,e,s={}){let t=$(d,e,s);return JSON.stringify(t)}import y from"path";import{spawn as D}from"child_process";var W=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);async function k(d=100){try{return(await fetch(`http://127.0.0.1:${W}/health`,{signal:AbortSignal.timeout(d)})).ok}catch{return!1}}async function G(d=1e4){let e=Date.now(),s=100;for(;Date.now()-e<d;){if(await k(1e3))return!0;await new Promise(t=>setTimeout(t,s))}return!1}async function x(){if(await k())return;let d=C(),e=y.join(d,"node_modules",".bin","pm2"),s=y.join(d,"ecosystem.config.cjs"),t=D(e,["list","--no-color"],{cwd:d,stdio:["ignore","pipe","ignore"]}),r="";if(t.stdout?.on("data",i=>{r+=i.toString()}),await new Promise((i,p)=>{t.on("error",c=>p(c)),t.on("close",c=>{i()})}),!(r.includes("claude-mem-worker")&&r.includes("online"))){let i=D(e,["start",s],{cwd:d,stdio:"ignore"});await new Promise((p,c)=>{i.on("error",u=>c(u)),i.on("close",u=>{u!==0&&u!==null?c(new Error(`PM2 start command failed with exit code ${u}`)):p()})})}if(!await G(1e4))throw new Error("Worker failed to become healthy after starting")}async function Y(d){if(!d)throw new Error("summaryHook requires input");let{session_id:e}=d;await x();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t);s.close();let n=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);g.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:n,promptNumber:r});try{let o=await fetch(`http://127.0.0.1:${n}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(!o.ok){let i=await o.text();throw g.failure("HOOK","Failed to generate summary",{sessionId:t,status:o.status},i),new Error(`Failed to request summary from worker: ${o.status} ${i}`)}g.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(o){throw o.cause?.code==="ECONNREFUSED"||o.name==="TimeoutError"||o.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):o}console.log(v("Stop",!0))}var I="";U.on("data",d=>I+=d);U.on("end",async()=>{let d=I?JSON.parse(I):void 0;await Y(d)});
|
||||
|
||||
@@ -22,4 +22,7 @@ This message was not added to your startup context, so you can continue working
|
||||
\u{1F4DD} Claude-Mem Context Loaded
|
||||
\u2139\uFE0F Note: This appears as stderr but is informational only
|
||||
|
||||
`+n)}catch(e){console.error(`\u274C Failed to load context display: ${e}`)}process.exit(3);
|
||||
`+n+`
|
||||
|
||||
\u{1F4FA} Watch live in browser http://localhost:37777/ (New! v5.1)
|
||||
`)}catch(e){console.error(`\u274C Failed to load context display: ${e}`)}process.exit(3);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
File diff suppressed because one or more lines are too long
@@ -0,0 +1,755 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>claude-mem viewer</title>
|
||||
<link rel="icon" type="image/webp" href="claude-mem-logomark.webp">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Monaspace Radon';
|
||||
src: url('assets/fonts/monaspace-radon-var.woff2') format('woff2-variations'),
|
||||
url('assets/fonts/monaspace-radon-var.woff') format('woff-variations');
|
||||
font-weight: 200 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Theme Variables - Light Mode */
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #f6f8fa;
|
||||
--color-bg-tertiary: #f0f0f0;
|
||||
--color-bg-header: #f6f8fa;
|
||||
--color-bg-card: #ffffff;
|
||||
--color-bg-card-hover: #f6f8fa;
|
||||
--color-bg-input: #ffffff;
|
||||
--color-bg-button: #0969da;
|
||||
--color-bg-button-hover: #1177e6;
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #fffbf0;
|
||||
--color-bg-prompt: #f6f3fb;
|
||||
--color-bg-stat: #f6f8fa;
|
||||
--color-bg-scrollbar-track: #ffffff;
|
||||
--color-bg-scrollbar-thumb: #d1d5da;
|
||||
--color-bg-scrollbar-thumb-hover: #b1b5ba;
|
||||
|
||||
--color-border-primary: #d0d7de;
|
||||
--color-border-secondary: #d8dee4;
|
||||
--color-border-hover: #0969da;
|
||||
--color-border-focus: #0969da;
|
||||
--color-border-summary: #d4a72c;
|
||||
--color-border-summary-hover: #c29d29;
|
||||
--color-border-prompt: #8250df;
|
||||
--color-border-prompt-hover: #6e40c9;
|
||||
|
||||
--color-text-primary: #24292f;
|
||||
--color-text-secondary: #57606a;
|
||||
--color-text-tertiary: #6e7781;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #24292f;
|
||||
--color-text-title: #24292f;
|
||||
--color-text-subtitle: #57606a;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #8a6116;
|
||||
--color-text-logo: #24292f;
|
||||
|
||||
--color-accent-primary: #0969da;
|
||||
--color-accent-focus: #0969da;
|
||||
--color-accent-success: #1a7f37;
|
||||
--color-accent-error: #d1242f;
|
||||
--color-accent-summary: #9a6700;
|
||||
--color-accent-prompt: #8250df;
|
||||
|
||||
--color-type-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-type-badge-text: #0969da;
|
||||
--color-summary-badge-bg: rgba(154, 103, 0, 0.12);
|
||||
--color-summary-badge-text: #9a6700;
|
||||
--color-prompt-badge-bg: rgba(130, 80, 223, 0.12);
|
||||
--color-prompt-badge-text: #8250df;
|
||||
|
||||
--color-skeleton-base: #d0d7de;
|
||||
--color-skeleton-highlight: #e8ecef;
|
||||
|
||||
--shadow-focus: 0 0 0 2px rgba(9, 105, 218, 0.3);
|
||||
}
|
||||
|
||||
/* Theme Variables - Dark Mode */
|
||||
[data-theme="dark"] {
|
||||
--color-bg-primary: #1e1e1e;
|
||||
--color-bg-secondary: #2d2d2d;
|
||||
--color-bg-tertiary: #252526;
|
||||
--color-bg-header: #252526;
|
||||
--color-bg-card: #2d2d2d;
|
||||
--color-bg-card-hover: #333333;
|
||||
--color-bg-input: #2d2d2d;
|
||||
--color-bg-button: #0969da;
|
||||
--color-bg-button-hover: #1177e6;
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #3d2f00;
|
||||
--color-bg-prompt: #2d1b4e;
|
||||
--color-bg-stat: #2d2d2d;
|
||||
--color-bg-scrollbar-track: #1e1e1e;
|
||||
--color-bg-scrollbar-thumb: #424242;
|
||||
--color-bg-scrollbar-thumb-hover: #4e4e4e;
|
||||
|
||||
--color-border-primary: #404040;
|
||||
--color-border-secondary: #404040;
|
||||
--color-border-hover: #505050;
|
||||
--color-border-focus: #58a6ff;
|
||||
--color-border-summary: #9e6a03;
|
||||
--color-border-summary-hover: #ae7a13;
|
||||
--color-border-prompt: #6e40c9;
|
||||
--color-border-prompt-hover: #8e6cdb;
|
||||
|
||||
--color-text-primary: #cccccc;
|
||||
--color-text-secondary: #a0a0a0;
|
||||
--color-text-tertiary: #6e7681;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #e0e0e0;
|
||||
--color-text-title: #e0e0e0;
|
||||
--color-text-subtitle: #a0a0a0;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #f2cc60;
|
||||
--color-text-logo: #dadada;
|
||||
|
||||
--color-accent-primary: #58a6ff;
|
||||
--color-accent-focus: #58a6ff;
|
||||
--color-accent-success: #16c60c;
|
||||
--color-accent-error: #e74856;
|
||||
--color-accent-summary: #f2cc60;
|
||||
--color-accent-prompt: #8e6cdb;
|
||||
|
||||
--color-type-badge-bg: rgba(88, 166, 255, 0.125);
|
||||
--color-type-badge-text: #58a6ff;
|
||||
--color-summary-badge-bg: rgba(242, 204, 96, 0.125);
|
||||
--color-summary-badge-text: #f2cc60;
|
||||
--color-prompt-badge-bg: rgba(110, 64, 201, 0.125);
|
||||
--color-prompt-badge-text: #8e6cdb;
|
||||
|
||||
--color-skeleton-base: #404040;
|
||||
--color-skeleton-highlight: #505050;
|
||||
|
||||
--shadow-focus: 0 0 0 2px rgba(88, 166, 255, 0.2);
|
||||
}
|
||||
|
||||
/* System preference default */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root:not([data-theme]) {
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #f6f8fa;
|
||||
--color-bg-tertiary: #f0f0f0;
|
||||
--color-bg-header: #f6f8fa;
|
||||
--color-bg-card: #ffffff;
|
||||
--color-bg-card-hover: #f6f8fa;
|
||||
--color-bg-input: #ffffff;
|
||||
--color-bg-button: #0969da;
|
||||
--color-bg-button-hover: #1177e6;
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #fffbf0;
|
||||
--color-bg-prompt: #f6f3fb;
|
||||
--color-bg-stat: #f6f8fa;
|
||||
--color-bg-scrollbar-track: #ffffff;
|
||||
--color-bg-scrollbar-thumb: #d1d5da;
|
||||
--color-bg-scrollbar-thumb-hover: #b1b5ba;
|
||||
|
||||
--color-border-primary: #d0d7de;
|
||||
--color-border-secondary: #d8dee4;
|
||||
--color-border-hover: #0969da;
|
||||
--color-border-focus: #0969da;
|
||||
--color-border-summary: #d4a72c;
|
||||
--color-border-summary-hover: #c29d29;
|
||||
--color-border-prompt: #8250df;
|
||||
--color-border-prompt-hover: #6e40c9;
|
||||
|
||||
--color-text-primary: #24292f;
|
||||
--color-text-secondary: #57606a;
|
||||
--color-text-tertiary: #6e7781;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #24292f;
|
||||
--color-text-title: #24292f;
|
||||
--color-text-subtitle: #57606a;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #8a6116;
|
||||
--color-text-logo: #24292f;
|
||||
|
||||
--color-accent-primary: #0969da;
|
||||
--color-accent-focus: #0969da;
|
||||
--color-accent-success: #1a7f37;
|
||||
--color-accent-error: #d1242f;
|
||||
--color-accent-summary: #9a6700;
|
||||
--color-accent-prompt: #8250df;
|
||||
|
||||
--color-type-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-type-badge-text: #0969da;
|
||||
--color-summary-badge-bg: rgba(154, 103, 0, 0.12);
|
||||
--color-summary-badge-text: #9a6700;
|
||||
--color-prompt-badge-bg: rgba(130, 80, 223, 0.12);
|
||||
--color-prompt-badge-text: #8250df;
|
||||
|
||||
--color-skeleton-base: #d0d7de;
|
||||
--color-skeleton-highlight: #e8ecef;
|
||||
|
||||
--shadow-focus: 0 0 0 2px rgba(9, 105, 218, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) {
|
||||
--color-bg-primary: #1e1e1e;
|
||||
--color-bg-secondary: #2d2d2d;
|
||||
--color-bg-tertiary: #252526;
|
||||
--color-bg-header: #252526;
|
||||
--color-bg-card: #2d2d2d;
|
||||
--color-bg-card-hover: #333333;
|
||||
--color-bg-input: #2d2d2d;
|
||||
--color-bg-button: #0969da;
|
||||
--color-bg-button-hover: #1177e6;
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #3d2f00;
|
||||
--color-bg-prompt: #2d1b4e;
|
||||
--color-bg-stat: #2d2d2d;
|
||||
--color-bg-scrollbar-track: #1e1e1e;
|
||||
--color-bg-scrollbar-thumb: #424242;
|
||||
--color-bg-scrollbar-thumb-hover: #4e4e4e;
|
||||
|
||||
--color-border-primary: #404040;
|
||||
--color-border-secondary: #404040;
|
||||
--color-border-hover: #505050;
|
||||
--color-border-focus: #58a6ff;
|
||||
--color-border-summary: #9e6a03;
|
||||
--color-border-summary-hover: #ae7a13;
|
||||
--color-border-prompt: #6e40c9;
|
||||
--color-border-prompt-hover: #8e6cdb;
|
||||
|
||||
--color-text-primary: #cccccc;
|
||||
--color-text-secondary: #a0a0a0;
|
||||
--color-text-tertiary: #6e7681;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #e0e0e0;
|
||||
--color-text-title: #e0e0e0;
|
||||
--color-text-subtitle: #a0a0a0;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #f2cc60;
|
||||
--color-text-logo: #dadada;
|
||||
|
||||
--color-accent-primary: #58a6ff;
|
||||
--color-accent-focus: #58a6ff;
|
||||
--color-accent-success: #16c60c;
|
||||
--color-accent-error: #e74856;
|
||||
--color-accent-summary: #f2cc60;
|
||||
--color-accent-prompt: #8e6cdb;
|
||||
|
||||
--color-type-badge-bg: rgba(88, 166, 255, 0.125);
|
||||
--color-type-badge-text: #58a6ff;
|
||||
--color-summary-badge-bg: rgba(242, 204, 96, 0.125);
|
||||
--color-summary-badge-text: #f2cc60;
|
||||
--color-prompt-badge-bg: rgba(110, 64, 201, 0.125);
|
||||
--color-prompt-badge-text: #8e6cdb;
|
||||
|
||||
--color-skeleton-base: #404040;
|
||||
--color-skeleton-highlight: #505050;
|
||||
|
||||
--shadow-focus: 0 0 0 2px rgba(88, 166, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||
background: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 400px;
|
||||
height: 100vh;
|
||||
background: var(--color-bg-primary);
|
||||
border-left: 1px solid var(--color-border-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 100;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--color-bg-header);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--color-bg-header);
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-header);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-header);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.logomark {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.logomark.spinning {
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Monaspace Radon', monospace;
|
||||
font-weight: 100;
|
||||
font-size: 20px;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--color-text-logo);
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settings-btn,
|
||||
.theme-toggle-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
padding: 8px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-primary);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-btn:hover,
|
||||
.theme-toggle-btn:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
border-color: var(--color-border-focus);
|
||||
}
|
||||
|
||||
.settings-btn.active {
|
||||
background: var(--color-bg-button);
|
||||
border-color: var(--color-bg-button);
|
||||
color: var(--color-text-button);
|
||||
}
|
||||
|
||||
.settings-icon,
|
||||
.theme-toggle-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent-error);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background: var(--color-accent-success);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
select,
|
||||
input,
|
||||
button {
|
||||
background: var(--color-bg-input);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
padding: 6px 12px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
select:hover,
|
||||
input:hover {
|
||||
border-color: var(--color-border-focus);
|
||||
}
|
||||
|
||||
select:focus,
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-border-focus);
|
||||
box-shadow: var(--shadow-focus);
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--color-bg-button);
|
||||
color: var(--color-text-button);
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: var(--color-bg-button-hover);
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
background: var(--color-bg-button-active);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.feed {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.feed-content {
|
||||
width: 100%;
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px 24px;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 8px;
|
||||
transition: all 0.15s ease;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--color-border-hover);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.card-type {
|
||||
padding: 2px 8px;
|
||||
background: var(--color-type-badge-bg);
|
||||
color: var(--color-type-badge-text);
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 17px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-text-title);
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-subtitle);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-top: 8px;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border-color: var(--color-border-summary);
|
||||
background: var(--color-bg-summary);
|
||||
}
|
||||
|
||||
.summary-card:hover {
|
||||
border-color: var(--color-border-summary-hover);
|
||||
}
|
||||
|
||||
.summary-card .card-type {
|
||||
background: var(--color-summary-badge-bg);
|
||||
color: var(--color-summary-badge-text);
|
||||
}
|
||||
|
||||
.summary-card .card-title {
|
||||
color: var(--color-text-summary);
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
padding: 18px;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 14px;
|
||||
color: var(--color-text-header);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 10px 12px;
|
||||
background: var(--color-bg-stat);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
color: var(--color-text-header);
|
||||
font-weight: 600;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.stats-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-bg-scrollbar-track);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-bg-scrollbar-thumb);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-bg-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
.save-status {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.prompt-card {
|
||||
border-color: var(--color-border-prompt);
|
||||
background: var(--color-bg-prompt);
|
||||
}
|
||||
|
||||
.prompt-card:hover {
|
||||
border-color: var(--color-border-prompt-hover);
|
||||
}
|
||||
|
||||
.prompt-card .card-type {
|
||||
background: var(--color-prompt-badge-bg);
|
||||
color: var(--color-prompt-badge-text);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-top: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.processing-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--color-accent-focus);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid var(--color-border-primary);
|
||||
border-top-color: var(--color-accent-focus);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.summary-skeleton {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.summary-skeleton .processing-indicator {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 16px;
|
||||
background: linear-gradient(90deg, var(--color-skeleton-base) 25%, var(--color-skeleton-highlight) 50%, var(--color-skeleton-base) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
height: 20px;
|
||||
width: 80%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.skeleton-subtitle {
|
||||
height: 16px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.skeleton-subtitle.short {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="viewer-bundle.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+21
-3
@@ -40,14 +40,32 @@ async function buildHooks() {
|
||||
const version = packageJson.version;
|
||||
console.log(`📌 Version: ${version}`);
|
||||
|
||||
// Create output directory
|
||||
console.log('\n📦 Preparing output directory...');
|
||||
// Create output directories
|
||||
console.log('\n📦 Preparing output directories...');
|
||||
const hooksDir = 'plugin/scripts';
|
||||
const uiDir = 'plugin/ui';
|
||||
|
||||
if (!fs.existsSync(hooksDir)) {
|
||||
fs.mkdirSync(hooksDir, { recursive: true });
|
||||
}
|
||||
console.log('✓ Output directory ready');
|
||||
if (!fs.existsSync(uiDir)) {
|
||||
fs.mkdirSync(uiDir, { recursive: true });
|
||||
}
|
||||
console.log('✓ Output directories ready');
|
||||
|
||||
// Build React viewer
|
||||
console.log('\n📋 Building React viewer...');
|
||||
const { spawn } = await import('child_process');
|
||||
const viewerBuild = spawn('node', ['scripts/build-viewer.js'], { stdio: 'inherit' });
|
||||
await new Promise((resolve, reject) => {
|
||||
viewerBuild.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Viewer build failed with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Build worker service
|
||||
console.log(`\n🔧 Building worker service...`);
|
||||
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as esbuild from 'esbuild';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
|
||||
async function buildViewer() {
|
||||
console.log('Building React viewer...');
|
||||
|
||||
try {
|
||||
// Build React app
|
||||
await esbuild.build({
|
||||
entryPoints: [path.join(rootDir, 'src/ui/viewer/index.tsx')],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
target: ['es2020'],
|
||||
format: 'iife',
|
||||
outfile: path.join(rootDir, 'plugin/ui/viewer-bundle.js'),
|
||||
jsx: 'automatic',
|
||||
loader: {
|
||||
'.tsx': 'tsx',
|
||||
'.ts': 'ts'
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
}
|
||||
});
|
||||
|
||||
// Copy HTML template to build output
|
||||
const htmlTemplate = fs.readFileSync(
|
||||
path.join(rootDir, 'src/ui/viewer-template.html'),
|
||||
'utf-8'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, 'plugin/ui/viewer.html'),
|
||||
htmlTemplate
|
||||
);
|
||||
|
||||
// Copy font assets
|
||||
const fontsDir = path.join(rootDir, 'src/ui/viewer/assets/fonts');
|
||||
const outputFontsDir = path.join(rootDir, 'plugin/ui/assets/fonts');
|
||||
|
||||
if (fs.existsSync(fontsDir)) {
|
||||
fs.mkdirSync(outputFontsDir, { recursive: true });
|
||||
const fontFiles = fs.readdirSync(fontsDir);
|
||||
for (const file of fontFiles) {
|
||||
fs.copyFileSync(
|
||||
path.join(fontsDir, file),
|
||||
path.join(outputFontsDir, file)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✓ React viewer built successfully');
|
||||
console.log(' - plugin/ui/viewer-bundle.js');
|
||||
console.log(' - plugin/ui/viewer.html (from viewer-template.html)');
|
||||
console.log(' - plugin/ui/assets/fonts/* (font files)');
|
||||
} catch (error) {
|
||||
console.error('Failed to build viewer:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
buildViewer();
|
||||
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Smart Install Script for claude-mem
|
||||
*
|
||||
* Features:
|
||||
* - Only runs npm install when necessary (version change or missing deps)
|
||||
* - Caches installation state with version marker
|
||||
* - Provides helpful Windows-specific error messages
|
||||
* - Cross-platform compatible (pure Node.js)
|
||||
* - Fast when already installed (just version check)
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Plugin root is parent directory of scripts/
|
||||
const PLUGIN_ROOT = join(__dirname, '..');
|
||||
const PACKAGE_JSON_PATH = join(PLUGIN_ROOT, 'package.json');
|
||||
const VERSION_MARKER_PATH = join(PLUGIN_ROOT, '.install-version');
|
||||
const NODE_MODULES_PATH = join(PLUGIN_ROOT, 'node_modules');
|
||||
const BETTER_SQLITE3_PATH = join(NODE_MODULES_PATH, 'better-sqlite3');
|
||||
|
||||
// Colors for output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m',
|
||||
dim: '\x1b[2m',
|
||||
};
|
||||
|
||||
function log(message, color = colors.reset) {
|
||||
console.error(`${color}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function getPackageVersion() {
|
||||
try {
|
||||
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf-8'));
|
||||
return packageJson.version;
|
||||
} catch (error) {
|
||||
log(`⚠️ Failed to read package.json: ${error.message}`, colors.yellow);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getInstalledVersion() {
|
||||
try {
|
||||
if (existsSync(VERSION_MARKER_PATH)) {
|
||||
return readFileSync(VERSION_MARKER_PATH, 'utf-8').trim();
|
||||
}
|
||||
} catch (error) {
|
||||
// Marker doesn't exist or can't be read
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setInstalledVersion(version) {
|
||||
try {
|
||||
writeFileSync(VERSION_MARKER_PATH, version, 'utf-8');
|
||||
} catch (error) {
|
||||
log(`⚠️ Failed to write version marker: ${error.message}`, colors.yellow);
|
||||
}
|
||||
}
|
||||
|
||||
function needsInstall() {
|
||||
// Check if node_modules exists
|
||||
if (!existsSync(NODE_MODULES_PATH)) {
|
||||
log('📦 Dependencies not found - first time setup', colors.cyan);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if better-sqlite3 is installed
|
||||
if (!existsSync(BETTER_SQLITE3_PATH)) {
|
||||
log('📦 better-sqlite3 missing - reinstalling', colors.cyan);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check version marker
|
||||
const currentVersion = getPackageVersion();
|
||||
const installedVersion = getInstalledVersion();
|
||||
|
||||
if (!installedVersion) {
|
||||
log('📦 No version marker found - installing', colors.cyan);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentVersion !== installedVersion) {
|
||||
log(`📦 Version changed (${installedVersion} → ${currentVersion}) - updating`, colors.cyan);
|
||||
return true;
|
||||
}
|
||||
|
||||
// All good - no install needed
|
||||
log(`✓ Dependencies already installed (v${currentVersion})`, colors.dim);
|
||||
return false;
|
||||
}
|
||||
|
||||
function getWindowsErrorHelp(errorOutput) {
|
||||
// Detect Python version at runtime
|
||||
let pythonStatus = ' Python not detected or version unknown';
|
||||
try {
|
||||
const pythonVersion = execSync('python --version', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
||||
const versionMatch = pythonVersion.match(/Python\s+([\d.]+)/);
|
||||
if (versionMatch) {
|
||||
pythonStatus = ` You have ${versionMatch[0]} installed ✓`;
|
||||
}
|
||||
} catch (error) {
|
||||
// Python not available or failed to detect - use default message
|
||||
}
|
||||
|
||||
const help = [
|
||||
'',
|
||||
'╔══════════════════════════════════════════════════════════════════════╗',
|
||||
'║ Windows Installation Help ║',
|
||||
'╚══════════════════════════════════════════════════════════════════════╝',
|
||||
'',
|
||||
'📋 better-sqlite3 requires build tools to compile native modules.',
|
||||
'',
|
||||
'🔧 Option 1: Install Visual Studio Build Tools (Recommended)',
|
||||
' 1. Download: https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022',
|
||||
' 2. Install "Desktop development with C++"',
|
||||
' 3. Restart your terminal',
|
||||
' 4. Try again',
|
||||
'',
|
||||
'🔧 Option 2: Install via npm (automated)',
|
||||
' Run as Administrator:',
|
||||
' npm install --global windows-build-tools',
|
||||
'',
|
||||
'🐍 Python Requirement:',
|
||||
' Python 3.6+ is required.',
|
||||
pythonStatus,
|
||||
'',
|
||||
];
|
||||
|
||||
// Check for specific error patterns
|
||||
if (errorOutput.includes('MSBuild.exe')) {
|
||||
help.push('❌ MSBuild not found - install Visual Studio Build Tools');
|
||||
}
|
||||
if (errorOutput.includes('MSVS')) {
|
||||
help.push('❌ Visual Studio not detected - install Build Tools');
|
||||
}
|
||||
if (errorOutput.includes('permission') || errorOutput.includes('EPERM')) {
|
||||
help.push('❌ Permission denied - try running as Administrator');
|
||||
}
|
||||
|
||||
help.push('');
|
||||
help.push('📖 Full documentation: https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md');
|
||||
help.push('');
|
||||
|
||||
return help.join('\n');
|
||||
}
|
||||
|
||||
function runNpmInstall() {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
log('', colors.cyan);
|
||||
log('🔨 Installing dependencies...', colors.bright);
|
||||
log('', colors.reset);
|
||||
|
||||
// Try normal install first, then retry with force if it fails
|
||||
const strategies = [
|
||||
{ command: 'npm install', label: 'normal' },
|
||||
{ command: 'npm install --force', label: 'with force flag' },
|
||||
];
|
||||
|
||||
let lastError = null;
|
||||
|
||||
for (const { command, label } of strategies) {
|
||||
try {
|
||||
log(`Attempting install ${label}...`, colors.dim);
|
||||
|
||||
// Run npm install silently
|
||||
execSync(command, {
|
||||
cwd: PLUGIN_ROOT,
|
||||
stdio: 'pipe', // Silent output unless error
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
// Verify better-sqlite3 was installed
|
||||
if (!existsSync(BETTER_SQLITE3_PATH)) {
|
||||
throw new Error('better-sqlite3 installation verification failed');
|
||||
}
|
||||
|
||||
const version = getPackageVersion();
|
||||
setInstalledVersion(version);
|
||||
|
||||
log('', colors.green);
|
||||
log('✅ Dependencies installed successfully!', colors.bright);
|
||||
log(` Version: ${version}`, colors.dim);
|
||||
log('', colors.reset);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
// Continue to next strategy
|
||||
}
|
||||
}
|
||||
|
||||
// All strategies failed - show error
|
||||
log('', colors.red);
|
||||
log('❌ Installation failed after retrying!', colors.bright);
|
||||
log('', colors.reset);
|
||||
|
||||
// Provide Windows-specific help
|
||||
if (isWindows && lastError && lastError.message && lastError.message.includes('better-sqlite3')) {
|
||||
log(getWindowsErrorHelp(lastError.message), colors.yellow);
|
||||
}
|
||||
|
||||
// Show generic error info with troubleshooting steps
|
||||
if (lastError) {
|
||||
if (lastError.stderr) {
|
||||
log('Error output:', colors.dim);
|
||||
log(lastError.stderr.toString(), colors.red);
|
||||
} else if (lastError.message) {
|
||||
log(lastError.message, colors.red);
|
||||
}
|
||||
|
||||
log('', colors.yellow);
|
||||
log('📋 Troubleshooting Steps:', colors.bright);
|
||||
log('', colors.reset);
|
||||
log('1. Check your internet connection', colors.yellow);
|
||||
log('2. Try running: npm cache clean --force', colors.yellow);
|
||||
log('3. Try running: npm install (in plugin directory)', colors.yellow);
|
||||
log('4. Check npm version: npm --version (requires npm 7+)', colors.yellow);
|
||||
log('5. Try updating npm: npm install -g npm@latest', colors.yellow);
|
||||
log('', colors.reset);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should fail when worker startup fails
|
||||
* Returns true if worker failed AND dependencies are missing
|
||||
*/
|
||||
function shouldFailOnWorkerStartup(workerStarted) {
|
||||
return !workerStarted && !existsSync(NODE_MODULES_PATH);
|
||||
}
|
||||
|
||||
function startWorker() {
|
||||
const ECOSYSTEM_CONFIG = join(PLUGIN_ROOT, 'ecosystem.config.cjs');
|
||||
const PM2_PATH = join(PLUGIN_ROOT, 'node_modules', '.bin', 'pm2');
|
||||
|
||||
log('🚀 Starting worker service...', colors.dim);
|
||||
|
||||
try {
|
||||
// Use the full path to PM2 to avoid PATH issues on Windows
|
||||
// PM2 will either start it or report it's already running (both are success cases)
|
||||
execSync(`"${PM2_PATH}" start "${ECOSYSTEM_CONFIG}"`, {
|
||||
cwd: PLUGIN_ROOT,
|
||||
stdio: 'pipe', // Capture output to avoid clutter
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
log('✓ Worker service ready', colors.dim);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
// PM2 errors are often non-critical (e.g., "already running")
|
||||
// Don't fail the entire setup if worker start has issues
|
||||
log(`⚠️ Worker startup issue (non-critical): ${error.message}`, colors.yellow);
|
||||
|
||||
// Check if it's just because worker is already running
|
||||
if (error.message && (error.message.includes('already') || error.message.includes('exist'))) {
|
||||
log('✓ Worker was already running', colors.dim);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Check if we need to install dependencies
|
||||
const installNeeded = needsInstall();
|
||||
|
||||
if (installNeeded) {
|
||||
// Run installation
|
||||
const installSuccess = runNpmInstall();
|
||||
|
||||
if (!installSuccess) {
|
||||
log('', colors.red);
|
||||
log('❌ Installation failed - cannot start worker without dependencies', colors.bright);
|
||||
log('', colors.reset);
|
||||
log('Please resolve the installation issues above and try again.', colors.yellow);
|
||||
log('', colors.reset);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Start/ensure worker is running (only after successful install or if deps already exist)
|
||||
const workerStarted = startWorker();
|
||||
|
||||
if (shouldFailOnWorkerStartup(workerStarted)) {
|
||||
log('', colors.red);
|
||||
log('❌ Worker failed to start and dependencies are missing', colors.bright);
|
||||
log('', colors.reset);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Success - dependencies installed (if needed) and worker running (or already running)
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ Unexpected error: ${error.message}`, colors.red);
|
||||
log('', colors.reset);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -14,7 +14,7 @@ export interface PostToolUseInput {
|
||||
cwd: string;
|
||||
tool_name: string;
|
||||
tool_input: any;
|
||||
tool_output: any;
|
||||
tool_response: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
throw new Error('saveHook requires input');
|
||||
}
|
||||
|
||||
const { session_id, tool_name, tool_input, tool_output } = input;
|
||||
const { session_id, tool_name, tool_input, tool_response } = input;
|
||||
|
||||
if (SKIP_TOOLS.has(tool_name)) {
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
@@ -65,7 +65,7 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
body: JSON.stringify({
|
||||
tool_name,
|
||||
tool_input: tool_input !== undefined ? JSON.stringify(tool_input) : '{}',
|
||||
tool_output: tool_output !== undefined ? JSON.stringify(tool_output) : '{}',
|
||||
tool_response: tool_response !== undefined ? JSON.stringify(tool_response) : '{}',
|
||||
prompt_number: promptNumber
|
||||
}),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
|
||||
@@ -49,7 +49,8 @@ try {
|
||||
console.error(
|
||||
"\n\n📝 Claude-Mem Context Loaded\n" +
|
||||
" ℹ️ Note: This appears as stderr but is informational only\n\n" +
|
||||
output
|
||||
output +
|
||||
"\n\n📺 Watch live in browser http://localhost:37777/ (New! v5.1)\n"
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -565,6 +565,104 @@ export class SessionStore {
|
||||
return stmt.all(project, limit) as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent observations across all projects (for web UI)
|
||||
*/
|
||||
getAllRecentObservations(limit: number = 100): Array<{
|
||||
id: number;
|
||||
type: string;
|
||||
title: string | null;
|
||||
subtitle: string | null;
|
||||
text: string;
|
||||
project: string;
|
||||
prompt_number: number | null;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
|
||||
FROM observations
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
return stmt.all(limit) as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent summaries across all projects (for web UI)
|
||||
*/
|
||||
getAllRecentSummaries(limit: number = 50): Array<{
|
||||
id: number;
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
next_steps: string | null;
|
||||
files_read: string | null;
|
||||
files_edited: string | null;
|
||||
notes: string | null;
|
||||
project: string;
|
||||
prompt_number: number | null;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, project, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
return stmt.all(limit) as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent user prompts across all sessions (for web UI)
|
||||
*/
|
||||
getAllRecentUserPrompts(limit: number = 100): Array<{
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
project: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
up.id,
|
||||
up.claude_session_id,
|
||||
s.project,
|
||||
up.prompt_number,
|
||||
up.prompt_text,
|
||||
up.created_at,
|
||||
up.created_at_epoch
|
||||
FROM user_prompts up
|
||||
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
ORDER BY up.created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
return stmt.all(limit) as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique projects from the database (for web UI project filter)
|
||||
*/
|
||||
getAllProjects(): string[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
ORDER BY project ASC
|
||||
`);
|
||||
|
||||
const rows = stmt.all() as Array<{ project: string }>;
|
||||
return rows.map(row => row.project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent sessions with their status and summary info
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,10 @@ import type { SDKSession } from '../sdk/prompts.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { ensureAllDataDirs } from '../shared/paths.js';
|
||||
import { execSync } from 'child_process';
|
||||
import { readFileSync, writeFileSync, existsSync, statSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { homedir } from 'os';
|
||||
|
||||
const MODEL = process.env.CLAUDE_MEM_MODEL || 'claude-sonnet-4-5';
|
||||
const DISALLOWED_TOOLS = ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch'];
|
||||
@@ -64,7 +68,7 @@ interface ObservationMessage {
|
||||
type: 'observation';
|
||||
tool_name: string;
|
||||
tool_input: string;
|
||||
tool_output: string;
|
||||
tool_response: string;
|
||||
prompt_number: number;
|
||||
}
|
||||
|
||||
@@ -96,14 +100,33 @@ class WorkerService {
|
||||
private port: number = FIXED_PORT;
|
||||
private sessions: Map<number, ActiveSession> = new Map();
|
||||
private chromaSync!: ChromaSync;
|
||||
private sseClients: Set<Response> = new Set();
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
// Serve static files for web UI (viewer-bundle.js, logos, etc.)
|
||||
const uiDir = this.getUIDirectory();
|
||||
this.app.use(express.static(uiDir));
|
||||
|
||||
// Health check
|
||||
this.app.get('/health', this.handleHealth.bind(this));
|
||||
|
||||
// Web UI viewer
|
||||
this.app.get('/', this.handleViewerHTML.bind(this));
|
||||
|
||||
// SSE stream for web UI
|
||||
this.app.get('/stream', this.handleSSEStream.bind(this));
|
||||
|
||||
// API endpoints for web UI
|
||||
this.app.get('/api/stats', this.handleStats.bind(this));
|
||||
this.app.get('/api/settings', this.handleGetSettings.bind(this));
|
||||
this.app.post('/api/settings', this.handlePostSettings.bind(this));
|
||||
this.app.get('/api/observations', this.handleGetObservations.bind(this));
|
||||
this.app.get('/api/summaries', this.handleGetSummaries.bind(this));
|
||||
this.app.get('/api/prompts', this.handleGetPrompts.bind(this));
|
||||
|
||||
// Session endpoints
|
||||
this.app.post('/sessions/:sessionDbId/init', this.handleInit.bind(this));
|
||||
this.app.post('/sessions/:sessionDbId/observations', this.handleObservation.bind(this));
|
||||
@@ -146,6 +169,22 @@ class WorkerService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UI directory path (works in both dev ESM and production CJS)
|
||||
*/
|
||||
private getUIDirectory(): string {
|
||||
let scriptDir: string;
|
||||
if (typeof __dirname !== 'undefined') {
|
||||
// CJS context (production build)
|
||||
scriptDir = __dirname;
|
||||
} else {
|
||||
// ESM context (development)
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
scriptDir = dirname(__filename);
|
||||
}
|
||||
return join(scriptDir, '..', 'ui');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /health
|
||||
*/
|
||||
@@ -153,6 +192,411 @@ class WorkerService {
|
||||
res.json({ status: 'ok' });
|
||||
}
|
||||
|
||||
/**
|
||||
* GET / - Serve viewer HTML
|
||||
*/
|
||||
private handleViewerHTML(_req: Request, res: Response): void {
|
||||
try {
|
||||
const uiPath = join(this.getUIDirectory(), 'viewer.html');
|
||||
const html = readFileSync(uiPath, 'utf-8');
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
} catch (error: any) {
|
||||
logger.error('WORKER', 'Failed to serve viewer HTML', {}, error);
|
||||
res.status(500).send('Failed to load viewer');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /stream - SSE endpoint for web UI
|
||||
*/
|
||||
private handleSSEStream(req: Request, res: Response): void {
|
||||
// Set SSE headers
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
// Add client to set
|
||||
this.sseClients.add(res);
|
||||
logger.info('WORKER', `SSE client connected`, { totalClients: this.sseClients.size });
|
||||
|
||||
// Send only projects list - all data will be loaded via pagination
|
||||
const db = new SessionStore();
|
||||
const allProjects = db.getAllProjects();
|
||||
db.close();
|
||||
|
||||
const initialData = {
|
||||
type: 'initial_load',
|
||||
projects: allProjects,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
res.write(`data: ${JSON.stringify(initialData)}\n\n`);
|
||||
|
||||
// Handle client disconnect
|
||||
req.on('close', () => {
|
||||
this.sseClients.delete(res);
|
||||
logger.info('WORKER', `SSE client disconnected`, { remainingClients: this.sseClients.size });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast SSE event to all connected clients
|
||||
*/
|
||||
private broadcastSSE(event: any): void {
|
||||
if (this.sseClients.size === 0) {
|
||||
return; // No clients connected, skip broadcast
|
||||
}
|
||||
|
||||
const data = `data: ${JSON.stringify(event)}\n\n`;
|
||||
const clientsToRemove: Response[] = [];
|
||||
|
||||
for (const client of this.sseClients) {
|
||||
try {
|
||||
client.write(data);
|
||||
} catch (error) {
|
||||
// Client disconnected, mark for removal
|
||||
clientsToRemove.push(client);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up disconnected clients
|
||||
for (const client of clientsToRemove) {
|
||||
this.sseClients.delete(client);
|
||||
}
|
||||
|
||||
if (clientsToRemove.length > 0) {
|
||||
logger.info('WORKER', `SSE cleaned up disconnected clients`, { count: clientsToRemove.length });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast processing status to SSE clients
|
||||
*/
|
||||
private broadcastProcessingStatus(claudeSessionId: string, isProcessing: boolean): void {
|
||||
this.broadcastSSE({
|
||||
type: 'processing_status',
|
||||
processing: {
|
||||
session_id: claudeSessionId,
|
||||
is_processing: isProcessing
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/stats - Return worker and database stats
|
||||
*/
|
||||
private handleStats(_req: Request, res: Response): void {
|
||||
try {
|
||||
const db = new SessionStore();
|
||||
|
||||
// Get database stats
|
||||
const obsCount = db.db.prepare('SELECT COUNT(*) as count FROM observations').get() as { count: number };
|
||||
const sessionCount = db.db.prepare('SELECT COUNT(*) as count FROM sdk_sessions').get() as { count: number };
|
||||
const summaryCount = db.db.prepare('SELECT COUNT(*) as count FROM session_summaries').get() as { count: number };
|
||||
|
||||
// Get database file size
|
||||
const dbPath = join(homedir(), '.claude-mem', 'claude-mem.db');
|
||||
let dbSize = 0;
|
||||
if (existsSync(dbPath)) {
|
||||
dbSize = statSync(dbPath).size;
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
// Get worker stats
|
||||
const uptime = process.uptime();
|
||||
const version = process.env.npm_package_version || '5.0.3'; // fallback to current version
|
||||
|
||||
res.json({
|
||||
worker: {
|
||||
version,
|
||||
uptime: Math.floor(uptime),
|
||||
activeSessions: this.sessions.size,
|
||||
sseClients: this.sseClients.size,
|
||||
port: this.port
|
||||
},
|
||||
database: {
|
||||
path: dbPath,
|
||||
size: dbSize,
|
||||
observations: obsCount.count,
|
||||
sessions: sessionCount.count,
|
||||
summaries: summaryCount.count
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('WORKER', 'Failed to get stats', {}, error);
|
||||
res.status(500).json({ error: 'Failed to get stats' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/settings - Read settings from ~/.claude/settings.json
|
||||
*/
|
||||
private handleGetSettings(_req: Request, res: Response): void {
|
||||
try {
|
||||
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
||||
|
||||
if (!existsSync(settingsPath)) {
|
||||
// Return defaults if file doesn't exist
|
||||
res.json({
|
||||
CLAUDE_MEM_MODEL: 'claude-haiku-4-5',
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50',
|
||||
CLAUDE_MEM_WORKER_PORT: '37777'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const settingsData = readFileSync(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(settingsData);
|
||||
const env = settings.env || {};
|
||||
|
||||
res.json({
|
||||
CLAUDE_MEM_MODEL: env.CLAUDE_MEM_MODEL || 'claude-haiku-4-5',
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50',
|
||||
CLAUDE_MEM_WORKER_PORT: env.CLAUDE_MEM_WORKER_PORT || '37777'
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('WORKER', 'Failed to read settings', {}, error);
|
||||
res.status(500).json({ error: 'Failed to read settings' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/settings - Update settings in ~/.claude/settings.json
|
||||
*/
|
||||
private handlePostSettings(req: Request, res: Response): void {
|
||||
try {
|
||||
const { CLAUDE_MEM_MODEL, CLAUDE_MEM_CONTEXT_OBSERVATIONS, CLAUDE_MEM_WORKER_PORT } = req.body;
|
||||
|
||||
// Validate inputs
|
||||
const validModels = ['claude-haiku-4-5', 'claude-sonnet-4-5', 'claude-opus-4'];
|
||||
if (CLAUDE_MEM_MODEL && !validModels.includes(CLAUDE_MEM_MODEL)) {
|
||||
res.status(400).json({ success: false, error: `Invalid model name: ${CLAUDE_MEM_MODEL}` });
|
||||
return;
|
||||
}
|
||||
|
||||
if (CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
||||
const obsCount = parseInt(CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
||||
if (isNaN(obsCount) || obsCount < 1 || obsCount > 200) {
|
||||
res.status(400).json({ success: false, error: 'CLAUDE_MEM_CONTEXT_OBSERVATIONS must be between 1 and 200' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (CLAUDE_MEM_WORKER_PORT) {
|
||||
const port = parseInt(CLAUDE_MEM_WORKER_PORT, 10);
|
||||
if (isNaN(port) || port < 1024 || port > 65535) {
|
||||
res.status(400).json({ success: false, error: 'CLAUDE_MEM_WORKER_PORT must be between 1024 and 65535' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Read existing settings
|
||||
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
||||
let settings: any = { env: {} };
|
||||
|
||||
if (existsSync(settingsPath)) {
|
||||
const settingsData = readFileSync(settingsPath, 'utf-8');
|
||||
settings = JSON.parse(settingsData);
|
||||
if (!settings.env) {
|
||||
settings.env = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Update settings
|
||||
if (CLAUDE_MEM_MODEL) {
|
||||
settings.env.CLAUDE_MEM_MODEL = CLAUDE_MEM_MODEL;
|
||||
}
|
||||
if (CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
||||
settings.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS = CLAUDE_MEM_CONTEXT_OBSERVATIONS;
|
||||
}
|
||||
if (CLAUDE_MEM_WORKER_PORT) {
|
||||
settings.env.CLAUDE_MEM_WORKER_PORT = CLAUDE_MEM_WORKER_PORT;
|
||||
}
|
||||
|
||||
// Write back
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
||||
|
||||
logger.info('WORKER', 'Settings updated', {});
|
||||
res.json({ success: true, message: 'Settings updated successfully' });
|
||||
} catch (error: any) {
|
||||
logger.error('WORKER', 'Failed to update settings', {}, error);
|
||||
res.status(500).json({ success: false, error: 'Failed to update settings' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/observations - Paginated observations fetch
|
||||
* Query params: offset (default 0), limit (default 50), project (optional)
|
||||
*/
|
||||
private handleGetObservations(req: Request, res: Response): void {
|
||||
try {
|
||||
const offset = parseInt(req.query.offset as string || '0', 10);
|
||||
const limit = Math.min(parseInt(req.query.limit as string || '50', 10), 100); // Cap at 100
|
||||
const project = req.query.project as string | undefined;
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Build query with optional project filter
|
||||
let query = `
|
||||
SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
|
||||
FROM observations
|
||||
`;
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM observations';
|
||||
const params: any[] = [];
|
||||
const countParams: any[] = [];
|
||||
|
||||
if (project) {
|
||||
query += ' WHERE project = ?';
|
||||
countQuery += ' WHERE project = ?';
|
||||
params.push(project);
|
||||
countParams.push(project);
|
||||
}
|
||||
|
||||
query += ' ORDER BY created_at_epoch DESC LIMIT ? OFFSET ?';
|
||||
params.push(limit, offset);
|
||||
|
||||
const stmt = db.db.prepare(query);
|
||||
const observations = stmt.all(...params);
|
||||
|
||||
// Check if there are more results
|
||||
const countStmt = db.db.prepare(countQuery);
|
||||
const { total } = countStmt.get(...countParams) as { total: number };
|
||||
const hasMore = (offset + limit) < total;
|
||||
|
||||
db.close();
|
||||
|
||||
res.json({
|
||||
observations,
|
||||
hasMore,
|
||||
total,
|
||||
offset,
|
||||
limit
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('WORKER', 'Failed to get observations', {}, error);
|
||||
res.status(500).json({ error: 'Failed to get observations' });
|
||||
}
|
||||
}
|
||||
|
||||
private handleGetSummaries(req: Request, res: Response): void {
|
||||
try {
|
||||
const offset = parseInt(req.query.offset as string || '0', 10);
|
||||
const limit = Math.min(parseInt(req.query.limit as string || '50', 10), 100); // Cap at 100
|
||||
const project = req.query.project as string | undefined;
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Build query with optional project filter
|
||||
// JOIN with sdk_sessions to get claude_session_id (needed for UI matching with processingSessions)
|
||||
let query = `
|
||||
SELECT
|
||||
ss.id,
|
||||
s.claude_session_id as session_id,
|
||||
ss.request,
|
||||
ss.learned,
|
||||
ss.completed,
|
||||
ss.next_steps,
|
||||
ss.project,
|
||||
ss.created_at,
|
||||
ss.created_at_epoch
|
||||
FROM session_summaries ss
|
||||
JOIN sdk_sessions s ON ss.sdk_session_id = s.sdk_session_id
|
||||
`;
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM session_summaries';
|
||||
const params: any[] = [];
|
||||
const countParams: any[] = [];
|
||||
|
||||
if (project) {
|
||||
query += ' WHERE ss.project = ?';
|
||||
countQuery += ' WHERE project = ?';
|
||||
params.push(project);
|
||||
countParams.push(project);
|
||||
}
|
||||
|
||||
query += ' ORDER BY ss.created_at_epoch DESC LIMIT ? OFFSET ?';
|
||||
params.push(limit, offset);
|
||||
|
||||
const stmt = db.db.prepare(query);
|
||||
const summaries = stmt.all(...params);
|
||||
|
||||
// Check if there are more results
|
||||
const countStmt = db.db.prepare(countQuery);
|
||||
const { total } = countStmt.get(...countParams) as { total: number };
|
||||
const hasMore = (offset + limit) < total;
|
||||
|
||||
db.close();
|
||||
|
||||
res.json({
|
||||
summaries,
|
||||
hasMore,
|
||||
total,
|
||||
offset,
|
||||
limit
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('WORKER', 'Failed to get summaries', {}, error);
|
||||
res.status(500).json({ error: 'Failed to get summaries' });
|
||||
}
|
||||
}
|
||||
|
||||
private handleGetPrompts(req: Request, res: Response): void {
|
||||
try {
|
||||
const offset = parseInt(req.query.offset as string || '0', 10);
|
||||
const limit = Math.min(parseInt(req.query.limit as string || '50', 10), 100); // Cap at 100
|
||||
const project = req.query.project as string | undefined;
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Build query with optional project filter - JOIN with sdk_sessions to get project
|
||||
let query = `
|
||||
SELECT up.id, up.claude_session_id, s.project, up.prompt_number, up.prompt_text, up.created_at, up.created_at_epoch
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
`;
|
||||
let countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
`;
|
||||
const params: any[] = [];
|
||||
const countParams: any[] = [];
|
||||
|
||||
if (project) {
|
||||
query += ' WHERE s.project = ?';
|
||||
countQuery += ' WHERE s.project = ?';
|
||||
params.push(project);
|
||||
countParams.push(project);
|
||||
}
|
||||
|
||||
query += ' ORDER BY created_at_epoch DESC LIMIT ? OFFSET ?';
|
||||
params.push(limit, offset);
|
||||
|
||||
const stmt = db.db.prepare(query);
|
||||
const prompts = stmt.all(...params);
|
||||
|
||||
// Check if there are more results
|
||||
const countStmt = db.db.prepare(countQuery);
|
||||
const { total } = countStmt.get(...countParams) as { total: number };
|
||||
const hasMore = (offset + limit) < total;
|
||||
|
||||
db.close();
|
||||
|
||||
res.json({
|
||||
prompts,
|
||||
hasMore,
|
||||
total,
|
||||
offset,
|
||||
limit
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('WORKER', 'Failed to get prompts', {}, error);
|
||||
res.status(500).json({ error: 'Failed to get prompts' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /sessions/:sessionDbId/init
|
||||
* Body: { project, userPrompt }
|
||||
@@ -208,6 +652,21 @@ class WorkerService {
|
||||
|
||||
db.close();
|
||||
|
||||
// Broadcast new prompt to SSE clients (for web UI)
|
||||
if (latestPrompt) {
|
||||
this.broadcastSSE({
|
||||
type: 'new_prompt',
|
||||
prompt: {
|
||||
id: latestPrompt.id,
|
||||
claude_session_id: latestPrompt.claude_session_id,
|
||||
project: latestPrompt.project,
|
||||
prompt_number: latestPrompt.prompt_number,
|
||||
prompt_text: latestPrompt.prompt_text,
|
||||
created_at_epoch: latestPrompt.created_at_epoch
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sync user prompt to Chroma (fire-and-forget, but crash on failure)
|
||||
if (latestPrompt) {
|
||||
this.chromaSync.syncUserPrompt(
|
||||
@@ -242,11 +701,11 @@ class WorkerService {
|
||||
|
||||
/**
|
||||
* POST /sessions/:sessionDbId/observations
|
||||
* Body: { tool_name, tool_input, tool_output, prompt_number }
|
||||
* Body: { tool_name, tool_input, tool_response, prompt_number }
|
||||
*/
|
||||
private handleObservation(req: Request, res: Response): void {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
const { tool_name, tool_input, tool_output, prompt_number } = req.body;
|
||||
const { tool_name, tool_input, tool_response, prompt_number } = req.body;
|
||||
|
||||
let session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
@@ -292,10 +751,13 @@ class WorkerService {
|
||||
type: 'observation',
|
||||
tool_name,
|
||||
tool_input,
|
||||
tool_output,
|
||||
tool_response,
|
||||
prompt_number
|
||||
});
|
||||
|
||||
// Don't broadcast processing status for observations - only for summaries
|
||||
// Observations are processed continuously, skeleton should only show during summary generation
|
||||
|
||||
res.json({ status: 'queued', queueLength: session.pendingMessages.length });
|
||||
}
|
||||
|
||||
@@ -351,6 +813,9 @@ class WorkerService {
|
||||
prompt_number
|
||||
});
|
||||
|
||||
// Notify UI that processing is active
|
||||
this.broadcastProcessingStatus(session.claudeSessionId, true);
|
||||
|
||||
res.json({ status: 'queued', queueLength: session.pendingMessages.length });
|
||||
}
|
||||
|
||||
@@ -550,7 +1015,7 @@ class WorkerService {
|
||||
id: 0,
|
||||
tool_name: message.tool_name,
|
||||
tool_input: message.tool_input,
|
||||
tool_output: message.tool_output,
|
||||
tool_response: message.tool_response,
|
||||
created_at_epoch: Date.now()
|
||||
});
|
||||
|
||||
@@ -612,6 +1077,21 @@ class WorkerService {
|
||||
id
|
||||
});
|
||||
|
||||
// Broadcast to SSE clients (for web UI)
|
||||
this.broadcastSSE({
|
||||
type: 'new_observation',
|
||||
observation: {
|
||||
id,
|
||||
session_id: session.claudeSessionId,
|
||||
type: obs.type,
|
||||
title: obs.title,
|
||||
subtitle: obs.subtitle,
|
||||
project: session.project,
|
||||
prompt_number: promptNumber,
|
||||
created_at_epoch: createdAtEpoch
|
||||
}
|
||||
});
|
||||
|
||||
// Sync to Chroma (non-blocking fire-and-forget, but crash on failure)
|
||||
this.chromaSync.syncObservation(
|
||||
id,
|
||||
@@ -651,6 +1131,27 @@ class WorkerService {
|
||||
const { id, createdAtEpoch } = db.storeSummary(session.claudeSessionId, session.project, summary, promptNumber);
|
||||
logger.success('DB', '📝 SUMMARY STORED IN DATABASE', { sessionId: session.sessionDbId, promptNumber, id });
|
||||
|
||||
// Broadcast to SSE clients (for web UI)
|
||||
this.broadcastSSE({
|
||||
type: 'new_summary',
|
||||
summary: {
|
||||
id,
|
||||
session_id: session.claudeSessionId,
|
||||
request: summary.request,
|
||||
investigated: summary.investigated,
|
||||
learned: summary.learned,
|
||||
completed: summary.completed,
|
||||
next_steps: summary.next_steps,
|
||||
notes: summary.notes,
|
||||
project: session.project,
|
||||
prompt_number: promptNumber,
|
||||
created_at_epoch: createdAtEpoch
|
||||
}
|
||||
});
|
||||
|
||||
// Notify UI that processing is complete (summary is the final step)
|
||||
this.broadcastProcessingStatus(session.claudeSessionId, false);
|
||||
|
||||
// Sync to Chroma (non-blocking fire-and-forget, but crash on failure)
|
||||
this.chromaSync.syncSummary(
|
||||
id,
|
||||
@@ -677,6 +1178,9 @@ class WorkerService {
|
||||
promptNumber,
|
||||
contentSample: content.substring(0, 500)
|
||||
});
|
||||
|
||||
// Still mark processing as complete even if no summary was generated
|
||||
this.broadcastProcessingStatus(session.claudeSessionId, false);
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -7,7 +7,7 @@ const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || "37777", 10);
|
||||
/**
|
||||
* Check if worker is responsive by trying the health endpoint
|
||||
*/
|
||||
async function isWorkerHealthy(timeoutMs: number = 3000): Promise<boolean> {
|
||||
async function isWorkerHealthy(timeoutMs: number = 100): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${FIXED_PORT}/health`, {
|
||||
signal: AbortSignal.timeout(timeoutMs)
|
||||
@@ -42,7 +42,7 @@ async function waitForWorkerHealth(maxWaitMs: number = 10000): Promise<boolean>
|
||||
*/
|
||||
export async function ensureWorkerRunning(): Promise<void> {
|
||||
// First, check if worker is already healthy
|
||||
if (await isWorkerHealthy(1000)) {
|
||||
if (await isWorkerHealthy()) {
|
||||
return; // Worker is already running and responsive
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,755 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>claude-mem viewer</title>
|
||||
<link rel="icon" type="image/webp" href="claude-mem-logomark.webp">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Monaspace Radon';
|
||||
src: url('assets/fonts/monaspace-radon-var.woff2') format('woff2-variations'),
|
||||
url('assets/fonts/monaspace-radon-var.woff') format('woff-variations');
|
||||
font-weight: 200 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Theme Variables - Light Mode */
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #f6f8fa;
|
||||
--color-bg-tertiary: #f0f0f0;
|
||||
--color-bg-header: #f6f8fa;
|
||||
--color-bg-card: #ffffff;
|
||||
--color-bg-card-hover: #f6f8fa;
|
||||
--color-bg-input: #ffffff;
|
||||
--color-bg-button: #0969da;
|
||||
--color-bg-button-hover: #1177e6;
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #fffbf0;
|
||||
--color-bg-prompt: #f6f3fb;
|
||||
--color-bg-stat: #f6f8fa;
|
||||
--color-bg-scrollbar-track: #ffffff;
|
||||
--color-bg-scrollbar-thumb: #d1d5da;
|
||||
--color-bg-scrollbar-thumb-hover: #b1b5ba;
|
||||
|
||||
--color-border-primary: #d0d7de;
|
||||
--color-border-secondary: #d8dee4;
|
||||
--color-border-hover: #0969da;
|
||||
--color-border-focus: #0969da;
|
||||
--color-border-summary: #d4a72c;
|
||||
--color-border-summary-hover: #c29d29;
|
||||
--color-border-prompt: #8250df;
|
||||
--color-border-prompt-hover: #6e40c9;
|
||||
|
||||
--color-text-primary: #24292f;
|
||||
--color-text-secondary: #57606a;
|
||||
--color-text-tertiary: #6e7781;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #24292f;
|
||||
--color-text-title: #24292f;
|
||||
--color-text-subtitle: #57606a;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #8a6116;
|
||||
--color-text-logo: #24292f;
|
||||
|
||||
--color-accent-primary: #0969da;
|
||||
--color-accent-focus: #0969da;
|
||||
--color-accent-success: #1a7f37;
|
||||
--color-accent-error: #d1242f;
|
||||
--color-accent-summary: #9a6700;
|
||||
--color-accent-prompt: #8250df;
|
||||
|
||||
--color-type-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-type-badge-text: #0969da;
|
||||
--color-summary-badge-bg: rgba(154, 103, 0, 0.12);
|
||||
--color-summary-badge-text: #9a6700;
|
||||
--color-prompt-badge-bg: rgba(130, 80, 223, 0.12);
|
||||
--color-prompt-badge-text: #8250df;
|
||||
|
||||
--color-skeleton-base: #d0d7de;
|
||||
--color-skeleton-highlight: #e8ecef;
|
||||
|
||||
--shadow-focus: 0 0 0 2px rgba(9, 105, 218, 0.3);
|
||||
}
|
||||
|
||||
/* Theme Variables - Dark Mode */
|
||||
[data-theme="dark"] {
|
||||
--color-bg-primary: #1e1e1e;
|
||||
--color-bg-secondary: #2d2d2d;
|
||||
--color-bg-tertiary: #252526;
|
||||
--color-bg-header: #252526;
|
||||
--color-bg-card: #2d2d2d;
|
||||
--color-bg-card-hover: #333333;
|
||||
--color-bg-input: #2d2d2d;
|
||||
--color-bg-button: #0969da;
|
||||
--color-bg-button-hover: #1177e6;
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #3d2f00;
|
||||
--color-bg-prompt: #2d1b4e;
|
||||
--color-bg-stat: #2d2d2d;
|
||||
--color-bg-scrollbar-track: #1e1e1e;
|
||||
--color-bg-scrollbar-thumb: #424242;
|
||||
--color-bg-scrollbar-thumb-hover: #4e4e4e;
|
||||
|
||||
--color-border-primary: #404040;
|
||||
--color-border-secondary: #404040;
|
||||
--color-border-hover: #505050;
|
||||
--color-border-focus: #58a6ff;
|
||||
--color-border-summary: #9e6a03;
|
||||
--color-border-summary-hover: #ae7a13;
|
||||
--color-border-prompt: #6e40c9;
|
||||
--color-border-prompt-hover: #8e6cdb;
|
||||
|
||||
--color-text-primary: #cccccc;
|
||||
--color-text-secondary: #a0a0a0;
|
||||
--color-text-tertiary: #6e7681;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #e0e0e0;
|
||||
--color-text-title: #e0e0e0;
|
||||
--color-text-subtitle: #a0a0a0;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #f2cc60;
|
||||
--color-text-logo: #dadada;
|
||||
|
||||
--color-accent-primary: #58a6ff;
|
||||
--color-accent-focus: #58a6ff;
|
||||
--color-accent-success: #16c60c;
|
||||
--color-accent-error: #e74856;
|
||||
--color-accent-summary: #f2cc60;
|
||||
--color-accent-prompt: #8e6cdb;
|
||||
|
||||
--color-type-badge-bg: rgba(88, 166, 255, 0.125);
|
||||
--color-type-badge-text: #58a6ff;
|
||||
--color-summary-badge-bg: rgba(242, 204, 96, 0.125);
|
||||
--color-summary-badge-text: #f2cc60;
|
||||
--color-prompt-badge-bg: rgba(110, 64, 201, 0.125);
|
||||
--color-prompt-badge-text: #8e6cdb;
|
||||
|
||||
--color-skeleton-base: #404040;
|
||||
--color-skeleton-highlight: #505050;
|
||||
|
||||
--shadow-focus: 0 0 0 2px rgba(88, 166, 255, 0.2);
|
||||
}
|
||||
|
||||
/* System preference default */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root:not([data-theme]) {
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #f6f8fa;
|
||||
--color-bg-tertiary: #f0f0f0;
|
||||
--color-bg-header: #f6f8fa;
|
||||
--color-bg-card: #ffffff;
|
||||
--color-bg-card-hover: #f6f8fa;
|
||||
--color-bg-input: #ffffff;
|
||||
--color-bg-button: #0969da;
|
||||
--color-bg-button-hover: #1177e6;
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #fffbf0;
|
||||
--color-bg-prompt: #f6f3fb;
|
||||
--color-bg-stat: #f6f8fa;
|
||||
--color-bg-scrollbar-track: #ffffff;
|
||||
--color-bg-scrollbar-thumb: #d1d5da;
|
||||
--color-bg-scrollbar-thumb-hover: #b1b5ba;
|
||||
|
||||
--color-border-primary: #d0d7de;
|
||||
--color-border-secondary: #d8dee4;
|
||||
--color-border-hover: #0969da;
|
||||
--color-border-focus: #0969da;
|
||||
--color-border-summary: #d4a72c;
|
||||
--color-border-summary-hover: #c29d29;
|
||||
--color-border-prompt: #8250df;
|
||||
--color-border-prompt-hover: #6e40c9;
|
||||
|
||||
--color-text-primary: #24292f;
|
||||
--color-text-secondary: #57606a;
|
||||
--color-text-tertiary: #6e7781;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #24292f;
|
||||
--color-text-title: #24292f;
|
||||
--color-text-subtitle: #57606a;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #8a6116;
|
||||
--color-text-logo: #24292f;
|
||||
|
||||
--color-accent-primary: #0969da;
|
||||
--color-accent-focus: #0969da;
|
||||
--color-accent-success: #1a7f37;
|
||||
--color-accent-error: #d1242f;
|
||||
--color-accent-summary: #9a6700;
|
||||
--color-accent-prompt: #8250df;
|
||||
|
||||
--color-type-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-type-badge-text: #0969da;
|
||||
--color-summary-badge-bg: rgba(154, 103, 0, 0.12);
|
||||
--color-summary-badge-text: #9a6700;
|
||||
--color-prompt-badge-bg: rgba(130, 80, 223, 0.12);
|
||||
--color-prompt-badge-text: #8250df;
|
||||
|
||||
--color-skeleton-base: #d0d7de;
|
||||
--color-skeleton-highlight: #e8ecef;
|
||||
|
||||
--shadow-focus: 0 0 0 2px rgba(9, 105, 218, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) {
|
||||
--color-bg-primary: #1e1e1e;
|
||||
--color-bg-secondary: #2d2d2d;
|
||||
--color-bg-tertiary: #252526;
|
||||
--color-bg-header: #252526;
|
||||
--color-bg-card: #2d2d2d;
|
||||
--color-bg-card-hover: #333333;
|
||||
--color-bg-input: #2d2d2d;
|
||||
--color-bg-button: #0969da;
|
||||
--color-bg-button-hover: #1177e6;
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #3d2f00;
|
||||
--color-bg-prompt: #2d1b4e;
|
||||
--color-bg-stat: #2d2d2d;
|
||||
--color-bg-scrollbar-track: #1e1e1e;
|
||||
--color-bg-scrollbar-thumb: #424242;
|
||||
--color-bg-scrollbar-thumb-hover: #4e4e4e;
|
||||
|
||||
--color-border-primary: #404040;
|
||||
--color-border-secondary: #404040;
|
||||
--color-border-hover: #505050;
|
||||
--color-border-focus: #58a6ff;
|
||||
--color-border-summary: #9e6a03;
|
||||
--color-border-summary-hover: #ae7a13;
|
||||
--color-border-prompt: #6e40c9;
|
||||
--color-border-prompt-hover: #8e6cdb;
|
||||
|
||||
--color-text-primary: #cccccc;
|
||||
--color-text-secondary: #a0a0a0;
|
||||
--color-text-tertiary: #6e7681;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #e0e0e0;
|
||||
--color-text-title: #e0e0e0;
|
||||
--color-text-subtitle: #a0a0a0;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #f2cc60;
|
||||
--color-text-logo: #dadada;
|
||||
|
||||
--color-accent-primary: #58a6ff;
|
||||
--color-accent-focus: #58a6ff;
|
||||
--color-accent-success: #16c60c;
|
||||
--color-accent-error: #e74856;
|
||||
--color-accent-summary: #f2cc60;
|
||||
--color-accent-prompt: #8e6cdb;
|
||||
|
||||
--color-type-badge-bg: rgba(88, 166, 255, 0.125);
|
||||
--color-type-badge-text: #58a6ff;
|
||||
--color-summary-badge-bg: rgba(242, 204, 96, 0.125);
|
||||
--color-summary-badge-text: #f2cc60;
|
||||
--color-prompt-badge-bg: rgba(110, 64, 201, 0.125);
|
||||
--color-prompt-badge-text: #8e6cdb;
|
||||
|
||||
--color-skeleton-base: #404040;
|
||||
--color-skeleton-highlight: #505050;
|
||||
|
||||
--shadow-focus: 0 0 0 2px rgba(88, 166, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||
background: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 400px;
|
||||
height: 100vh;
|
||||
background: var(--color-bg-primary);
|
||||
border-left: 1px solid var(--color-border-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 100;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--color-bg-header);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--color-bg-header);
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-header);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-header);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.logomark {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.logomark.spinning {
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Monaspace Radon', monospace;
|
||||
font-weight: 100;
|
||||
font-size: 20px;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--color-text-logo);
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settings-btn,
|
||||
.theme-toggle-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
padding: 8px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-primary);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-btn:hover,
|
||||
.theme-toggle-btn:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
border-color: var(--color-border-focus);
|
||||
}
|
||||
|
||||
.settings-btn.active {
|
||||
background: var(--color-bg-button);
|
||||
border-color: var(--color-bg-button);
|
||||
color: var(--color-text-button);
|
||||
}
|
||||
|
||||
.settings-icon,
|
||||
.theme-toggle-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent-error);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background: var(--color-accent-success);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
select,
|
||||
input,
|
||||
button {
|
||||
background: var(--color-bg-input);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
padding: 6px 12px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
select:hover,
|
||||
input:hover {
|
||||
border-color: var(--color-border-focus);
|
||||
}
|
||||
|
||||
select:focus,
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-border-focus);
|
||||
box-shadow: var(--shadow-focus);
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--color-bg-button);
|
||||
color: var(--color-text-button);
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: var(--color-bg-button-hover);
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
background: var(--color-bg-button-active);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.feed {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.feed-content {
|
||||
width: 100%;
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px 24px;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 8px;
|
||||
transition: all 0.15s ease;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--color-border-hover);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.card-type {
|
||||
padding: 2px 8px;
|
||||
background: var(--color-type-badge-bg);
|
||||
color: var(--color-type-badge-text);
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 17px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-text-title);
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-subtitle);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-top: 8px;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border-color: var(--color-border-summary);
|
||||
background: var(--color-bg-summary);
|
||||
}
|
||||
|
||||
.summary-card:hover {
|
||||
border-color: var(--color-border-summary-hover);
|
||||
}
|
||||
|
||||
.summary-card .card-type {
|
||||
background: var(--color-summary-badge-bg);
|
||||
color: var(--color-summary-badge-text);
|
||||
}
|
||||
|
||||
.summary-card .card-title {
|
||||
color: var(--color-text-summary);
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
padding: 18px;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 14px;
|
||||
color: var(--color-text-header);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 10px 12px;
|
||||
background: var(--color-bg-stat);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
color: var(--color-text-header);
|
||||
font-weight: 600;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.stats-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-bg-scrollbar-track);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-bg-scrollbar-thumb);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-bg-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
.save-status {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.prompt-card {
|
||||
border-color: var(--color-border-prompt);
|
||||
background: var(--color-bg-prompt);
|
||||
}
|
||||
|
||||
.prompt-card:hover {
|
||||
border-color: var(--color-border-prompt-hover);
|
||||
}
|
||||
|
||||
.prompt-card .card-type {
|
||||
background: var(--color-prompt-badge-bg);
|
||||
color: var(--color-prompt-badge-text);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-top: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.processing-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--color-accent-focus);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid var(--color-border-primary);
|
||||
border-top-color: var(--color-accent-focus);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.summary-skeleton {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.summary-skeleton .processing-indicator {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 16px;
|
||||
background: linear-gradient(90deg, var(--color-skeleton-base) 25%, var(--color-skeleton-highlight) 50%, var(--color-skeleton-base) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
height: 20px;
|
||||
width: 80%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.skeleton-subtitle {
|
||||
height: 16px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.skeleton-subtitle.short {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="viewer-bundle.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Header } from './components/Header';
|
||||
import { Feed } from './components/Feed';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
import { useSSE } from './hooks/useSSE';
|
||||
import { useSettings } from './hooks/useSettings';
|
||||
import { useStats } from './hooks/useStats';
|
||||
import { usePagination } from './hooks/usePagination';
|
||||
import { useTheme } from './hooks/useTheme';
|
||||
import { Observation, Summary, UserPrompt } from './types';
|
||||
import { mergeAndDeduplicateByProject } from './utils/data';
|
||||
|
||||
export function App() {
|
||||
const [currentFilter, setCurrentFilter] = useState('');
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [paginatedObservations, setPaginatedObservations] = useState<Observation[]>([]);
|
||||
const [paginatedSummaries, setPaginatedSummaries] = useState<Summary[]>([]);
|
||||
const [paginatedPrompts, setPaginatedPrompts] = useState<UserPrompt[]>([]);
|
||||
|
||||
const { observations, summaries, prompts, projects, processingSessions, isConnected } = useSSE();
|
||||
const { settings, saveSettings, isSaving, saveStatus } = useSettings();
|
||||
const { stats } = useStats();
|
||||
const { preference, resolvedTheme, setThemePreference } = useTheme();
|
||||
const pagination = usePagination(currentFilter);
|
||||
|
||||
// Reset paginated data when filter changes
|
||||
useEffect(() => {
|
||||
setPaginatedObservations([]);
|
||||
setPaginatedSummaries([]);
|
||||
setPaginatedPrompts([]);
|
||||
}, [currentFilter]);
|
||||
|
||||
// Merge real-time data with paginated data, removing duplicates and filtering by project
|
||||
const allObservations = useMemo(
|
||||
() => mergeAndDeduplicateByProject(observations, paginatedObservations, currentFilter),
|
||||
[observations, paginatedObservations, currentFilter]
|
||||
);
|
||||
|
||||
const allSummaries = useMemo(
|
||||
() => mergeAndDeduplicateByProject(summaries, paginatedSummaries, currentFilter),
|
||||
[summaries, paginatedSummaries, currentFilter]
|
||||
);
|
||||
|
||||
const allPrompts = useMemo(
|
||||
() => mergeAndDeduplicateByProject(prompts, paginatedPrompts, currentFilter),
|
||||
[prompts, paginatedPrompts, currentFilter]
|
||||
);
|
||||
|
||||
// Toggle sidebar
|
||||
const toggleSidebar = useCallback(() => {
|
||||
setSidebarOpen(prev => !prev);
|
||||
}, []);
|
||||
|
||||
// Handle loading more data
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
try {
|
||||
const [newObservations, newSummaries, newPrompts] = await Promise.all([
|
||||
pagination.observations.loadMore(),
|
||||
pagination.summaries.loadMore(),
|
||||
pagination.prompts.loadMore()
|
||||
]);
|
||||
|
||||
if (newObservations.length > 0) {
|
||||
setPaginatedObservations(prev => [...prev, ...newObservations]);
|
||||
}
|
||||
if (newSummaries.length > 0) {
|
||||
setPaginatedSummaries(prev => [...prev, ...newSummaries]);
|
||||
}
|
||||
if (newPrompts.length > 0) {
|
||||
setPaginatedPrompts(prev => [...prev, ...newPrompts]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load more data:', error);
|
||||
}
|
||||
}, [pagination]);
|
||||
|
||||
// Load first page when filter changes or pagination handlers update
|
||||
useEffect(() => {
|
||||
handleLoadMore();
|
||||
}, [currentFilter, handleLoadMore]);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="main-col">
|
||||
<Header
|
||||
isConnected={isConnected}
|
||||
projects={projects}
|
||||
currentFilter={currentFilter}
|
||||
onFilterChange={setCurrentFilter}
|
||||
onSettingsToggle={toggleSidebar}
|
||||
sidebarOpen={sidebarOpen}
|
||||
isProcessing={processingSessions.size > 0}
|
||||
themePreference={preference}
|
||||
onThemeChange={setThemePreference}
|
||||
/>
|
||||
<Feed
|
||||
observations={allObservations}
|
||||
summaries={allSummaries}
|
||||
prompts={allPrompts}
|
||||
processingSessions={processingSessions}
|
||||
onLoadMore={handleLoadMore}
|
||||
isLoading={pagination.observations.isLoading || pagination.summaries.isLoading || pagination.prompts.isLoading}
|
||||
hasMore={pagination.observations.hasMore || pagination.summaries.hasMore || pagination.prompts.hasMore}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Sidebar
|
||||
isOpen={sidebarOpen}
|
||||
settings={settings}
|
||||
stats={stats}
|
||||
isSaving={isSaving}
|
||||
saveStatus={saveStatus}
|
||||
isConnected={isConnected}
|
||||
onSave={saveSettings}
|
||||
onClose={toggleSidebar}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,63 @@
|
||||
import React, { Component, ReactNode, ErrorInfo } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error('[ErrorBoundary] Caught error:', error, errorInfo);
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{ padding: '20px', color: '#ff6b6b', backgroundColor: '#1a1a1a', minHeight: '100vh' }}>
|
||||
<h1 style={{ fontSize: '24px', marginBottom: '10px' }}>Something went wrong</h1>
|
||||
<p style={{ marginBottom: '10px', color: '#8b949e' }}>
|
||||
The application encountered an error. Please refresh the page to try again.
|
||||
</p>
|
||||
{this.state.error && (
|
||||
<details style={{ marginTop: '20px', color: '#8b949e' }}>
|
||||
<summary style={{ cursor: 'pointer', marginBottom: '10px' }}>Error details</summary>
|
||||
<pre style={{
|
||||
backgroundColor: '#0d1117',
|
||||
padding: '10px',
|
||||
borderRadius: '6px',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{this.state.error.toString()}
|
||||
{this.state.errorInfo && '\n\n' + this.state.errorInfo.componentStack}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, { useMemo, useRef, useEffect } from 'react';
|
||||
import { Observation, Summary, UserPrompt, FeedItem } from '../types';
|
||||
import { ObservationCard } from './ObservationCard';
|
||||
import { SummaryCard } from './SummaryCard';
|
||||
import { SummarySkeleton } from './SummarySkeleton';
|
||||
import { PromptCard } from './PromptCard';
|
||||
import { UI } from '../constants/ui';
|
||||
|
||||
interface FeedProps {
|
||||
observations: Observation[];
|
||||
summaries: Summary[];
|
||||
prompts: UserPrompt[];
|
||||
processingSessions: Set<string>;
|
||||
onLoadMore: () => void;
|
||||
isLoading: boolean;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export function Feed({ observations, summaries, prompts, processingSessions, onLoadMore, isLoading, hasMore }: FeedProps) {
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||
const onLoadMoreRef = useRef(onLoadMore);
|
||||
|
||||
// Keep the callback ref up to date
|
||||
useEffect(() => {
|
||||
onLoadMoreRef.current = onLoadMore;
|
||||
}, [onLoadMore]);
|
||||
|
||||
// Set up intersection observer for infinite scroll
|
||||
useEffect(() => {
|
||||
const element = loadMoreRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const first = entries[0];
|
||||
if (first.isIntersecting && hasMore && !isLoading) {
|
||||
onLoadMoreRef.current?.();
|
||||
}
|
||||
},
|
||||
{ threshold: UI.LOAD_MORE_THRESHOLD }
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
|
||||
return () => {
|
||||
if (element) {
|
||||
observer.unobserve(element);
|
||||
}
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [hasMore, isLoading]);
|
||||
|
||||
const items = useMemo<FeedItem[]>(() => {
|
||||
// Create a set of session IDs that already have summaries
|
||||
const sessionsWithSummaries = new Set(summaries.map(s => s.session_id));
|
||||
|
||||
// Find the most recent prompt for each processing session
|
||||
const sessionPrompts = new Map<string, UserPrompt>();
|
||||
prompts.forEach(p => {
|
||||
const existing = sessionPrompts.get(p.claude_session_id);
|
||||
if (!existing || p.created_at_epoch > existing.created_at_epoch) {
|
||||
sessionPrompts.set(p.claude_session_id, p);
|
||||
}
|
||||
});
|
||||
|
||||
// Create skeleton items for sessions being processed that don't have summaries yet
|
||||
const skeletons: FeedItem[] = [];
|
||||
processingSessions.forEach(sessionId => {
|
||||
if (!sessionsWithSummaries.has(sessionId)) {
|
||||
const prompt = sessionPrompts.get(sessionId);
|
||||
skeletons.push({
|
||||
itemType: 'skeleton',
|
||||
id: sessionId, // Don't add prefix - key construction adds itemType already
|
||||
session_id: sessionId,
|
||||
project: prompt?.project,
|
||||
// Always use current time so skeletons appear at top of feed
|
||||
created_at_epoch: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Data is already filtered by App.tsx - no need to filter again
|
||||
const combined = [
|
||||
...observations.map(o => ({ ...o, itemType: 'observation' as const })),
|
||||
...summaries.map(s => ({ ...s, itemType: 'summary' as const })),
|
||||
...prompts.map(p => ({ ...p, itemType: 'prompt' as const })),
|
||||
...skeletons
|
||||
];
|
||||
|
||||
return combined
|
||||
.sort((a, b) => b.created_at_epoch - a.created_at_epoch);
|
||||
}, [observations, summaries, prompts, processingSessions]);
|
||||
|
||||
return (
|
||||
<div className="feed">
|
||||
<div className="feed-content">
|
||||
{items.map(item => {
|
||||
const key = `${item.itemType}-${item.id}`;
|
||||
if (item.itemType === 'observation') {
|
||||
return <ObservationCard key={key} observation={item} />;
|
||||
} else if (item.itemType === 'summary') {
|
||||
return <SummaryCard key={key} summary={item} />;
|
||||
} else if (item.itemType === 'skeleton') {
|
||||
return <SummarySkeleton key={key} sessionId={item.session_id} project={item.project} />;
|
||||
} else {
|
||||
return <PromptCard key={key} prompt={item} />;
|
||||
}
|
||||
})}
|
||||
{items.length === 0 && !isLoading && (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#8b949e' }}>
|
||||
No items to display
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: '#8b949e' }}>
|
||||
<div className="spinner" style={{ display: 'inline-block', marginRight: '10px' }}></div>
|
||||
Loading more...
|
||||
</div>
|
||||
)}
|
||||
{hasMore && !isLoading && items.length > 0 && (
|
||||
<div ref={loadMoreRef} style={{ height: '20px', margin: '10px 0' }} />
|
||||
)}
|
||||
{!hasMore && items.length > 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: '#8b949e', fontSize: '14px' }}>
|
||||
No more items to load
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
import { ThemePreference } from '../hooks/useTheme';
|
||||
|
||||
interface HeaderProps {
|
||||
isConnected: boolean;
|
||||
projects: string[];
|
||||
currentFilter: string;
|
||||
onFilterChange: (filter: string) => void;
|
||||
onSettingsToggle: () => void;
|
||||
sidebarOpen: boolean;
|
||||
isProcessing: boolean;
|
||||
themePreference: ThemePreference;
|
||||
onThemeChange: (theme: ThemePreference) => void;
|
||||
}
|
||||
|
||||
export function Header({
|
||||
isConnected,
|
||||
projects,
|
||||
currentFilter,
|
||||
onFilterChange,
|
||||
onSettingsToggle,
|
||||
sidebarOpen,
|
||||
isProcessing,
|
||||
themePreference,
|
||||
onThemeChange
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<div className="header">
|
||||
<h1>
|
||||
<img src="claude-mem-logomark.webp" alt="" className={`logomark ${isProcessing ? 'spinning' : ''}`} />
|
||||
<span className="logo-text">claude-mem</span>
|
||||
</h1>
|
||||
<div className="status">
|
||||
<a
|
||||
href="https://github.com/thedotmack/claude-mem/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="GitHub"
|
||||
style={{
|
||||
display: 'block',
|
||||
padding: '8px 4px 8px 8px',
|
||||
color: '#a0a0a0',
|
||||
transition: 'color 0.2s',
|
||||
lineHeight: 0
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.color = '#ffffff'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.color = '#a0a0a0'}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://x.com/Claude_Memory"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="X (Twitter)"
|
||||
style={{
|
||||
display: 'block',
|
||||
padding: '8px 8px 8px 4px',
|
||||
color: '#a0a0a0',
|
||||
transition: 'color 0.2s',
|
||||
lineHeight: 0
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.color = '#ffffff'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.color = '#a0a0a0'}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<select
|
||||
value={currentFilter}
|
||||
onChange={e => onFilterChange(e.target.value)}
|
||||
>
|
||||
<option value="">All Projects</option>
|
||||
{projects.map(project => (
|
||||
<option key={project} value={project}>{project}</option>
|
||||
))}
|
||||
</select>
|
||||
<ThemeToggle
|
||||
preference={themePreference}
|
||||
onThemeChange={onThemeChange}
|
||||
/>
|
||||
<button
|
||||
className={`settings-btn ${sidebarOpen ? 'active' : ''}`}
|
||||
onClick={onSettingsToggle}
|
||||
title="Settings"
|
||||
>
|
||||
<svg className="settings-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Observation } from '../types';
|
||||
import { formatDate } from '../utils/formatters';
|
||||
|
||||
interface ObservationCardProps {
|
||||
observation: Observation;
|
||||
}
|
||||
|
||||
export function ObservationCard({ observation }: ObservationCardProps) {
|
||||
const date = formatDate(observation.created_at_epoch);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<span className="card-type">{observation.type}</span>
|
||||
<span>{observation.project}</span>
|
||||
</div>
|
||||
<div className="card-title">{observation.title || 'Untitled'}</div>
|
||||
{observation.subtitle && (
|
||||
<div className="card-subtitle">{observation.subtitle}</div>
|
||||
)}
|
||||
<div className="card-meta">#{observation.id} • {date}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { UserPrompt } from '../types';
|
||||
import { formatDate } from '../utils/formatters';
|
||||
|
||||
interface PromptCardProps {
|
||||
prompt: UserPrompt;
|
||||
}
|
||||
|
||||
export function PromptCard({ prompt }: PromptCardProps) {
|
||||
return (
|
||||
<div className="card prompt-card">
|
||||
<div className="card-header">
|
||||
<span className="card-type">Prompt</span>
|
||||
<span>{prompt.project}</span>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
{prompt.prompt_text}
|
||||
</div>
|
||||
<div className="card-meta">
|
||||
{formatDate(prompt.created_at_epoch)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Settings, Stats } from '../types';
|
||||
import { DEFAULT_SETTINGS } from '../constants/settings';
|
||||
import { formatUptime, formatBytes } from '../utils/formatters';
|
||||
|
||||
interface SidebarProps {
|
||||
isOpen: boolean;
|
||||
settings: Settings;
|
||||
stats: Stats;
|
||||
isSaving: boolean;
|
||||
saveStatus: string;
|
||||
isConnected: boolean;
|
||||
onSave: (settings: Settings) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConnected, onSave, onClose }: SidebarProps) {
|
||||
const [model, setModel] = useState(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
|
||||
const [contextObs, setContextObs] = useState(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS);
|
||||
const [workerPort, setWorkerPort] = useState(settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT);
|
||||
|
||||
// Update local state when settings change
|
||||
useEffect(() => {
|
||||
setModel(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
|
||||
setContextObs(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS);
|
||||
setWorkerPort(settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT);
|
||||
}, [settings]);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
CLAUDE_MEM_MODEL: model,
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: contextObs,
|
||||
CLAUDE_MEM_WORKER_PORT: workerPort
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`sidebar ${isOpen ? 'open' : ''}`}>
|
||||
<div className="sidebar-header">
|
||||
<h1>Settings</h1>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<span className={`status-dot ${isConnected ? 'connected' : ''}`} />
|
||||
<span style={{ fontSize: '11px', opacity: 0.5, fontWeight: 300 }}>{isConnected ? 'Connected' : 'Disconnected'}</span>
|
||||
</div>
|
||||
<button onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
title="Close settings"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: '1px solid #404040',
|
||||
padding: '8px',
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats-scroll">
|
||||
|
||||
<div className="settings-section">
|
||||
<h3>Environment Variables</h3>
|
||||
<div className="form-group">
|
||||
<label htmlFor="model">CLAUDE_MEM_MODEL</label>
|
||||
<div className="setting-description">
|
||||
Model used for AI compression of tool observations. Haiku is fast and cheap, Sonnet offers better quality, Opus is most capable but expensive.
|
||||
</div>
|
||||
<select
|
||||
id="model"
|
||||
value={model}
|
||||
onChange={e => setModel(e.target.value)}
|
||||
>
|
||||
<option value="claude-haiku-4-5">claude-haiku-4-5</option>
|
||||
<option value="claude-sonnet-4-5">claude-sonnet-4-5</option>
|
||||
<option value="claude-opus-4">claude-opus-4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="contextObs">CLAUDE_MEM_CONTEXT_OBSERVATIONS</label>
|
||||
<div className="setting-description">
|
||||
Number of recent observations to inject at session start. Higher values provide more context but increase token usage. Default: 50
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
id="contextObs"
|
||||
min="1"
|
||||
max="200"
|
||||
value={contextObs}
|
||||
onChange={e => setContextObs(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="workerPort">CLAUDE_MEM_WORKER_PORT</label>
|
||||
<div className="setting-description">
|
||||
Port number for the background worker service. Change only if port 37777 conflicts with another service.
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
id="workerPort"
|
||||
min="1024"
|
||||
max="65535"
|
||||
value={workerPort}
|
||||
onChange={e => setWorkerPort(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{saveStatus && (
|
||||
<div className="save-status">{saveStatus}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h3>Worker Stats</h3>
|
||||
<div className="stats-grid">
|
||||
<div className="stat">
|
||||
<div className="stat-label">Version</div>
|
||||
<div className="stat-value">{stats.worker?.version || '-'}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-label">Uptime</div>
|
||||
<div className="stat-value">{formatUptime(stats.worker?.uptime)}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-label">Active Sessions</div>
|
||||
<div className="stat-value">{stats.worker?.activeSessions || '0'}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-label">SSE Clients</div>
|
||||
<div className="stat-value">{stats.worker?.sseClients || '0'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h3>Database Stats</h3>
|
||||
<div className="stats-grid">
|
||||
<div className="stat">
|
||||
<div className="stat-label">DB Size</div>
|
||||
<div className="stat-value">{formatBytes(stats.database?.size)}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-label">Observations</div>
|
||||
<div className="stat-value">{stats.database?.observations || '0'}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-label">Sessions</div>
|
||||
<div className="stat-value">{stats.database?.sessions || '0'}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-label">Summaries</div>
|
||||
<div className="stat-value">{stats.database?.summaries || '0'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Summary } from '../types';
|
||||
import { formatDate } from '../utils/formatters';
|
||||
|
||||
interface SummaryCardProps {
|
||||
summary: Summary;
|
||||
}
|
||||
|
||||
export function SummaryCard({ summary }: SummaryCardProps) {
|
||||
const date = formatDate(summary.created_at_epoch);
|
||||
|
||||
return (
|
||||
<div className="card summary-card">
|
||||
<div className="card-header">
|
||||
<span className="card-type">SUMMARY</span>
|
||||
<span>{summary.project}</span>
|
||||
</div>
|
||||
{summary.request && (
|
||||
<div className="card-title">Request: {summary.request}</div>
|
||||
)}
|
||||
{summary.learned && (
|
||||
<div className="card-subtitle">Learned: {summary.learned}</div>
|
||||
)}
|
||||
{summary.completed && (
|
||||
<div className="card-subtitle">Completed: {summary.completed}</div>
|
||||
)}
|
||||
{summary.next_steps && (
|
||||
<div className="card-subtitle">Next: {summary.next_steps}</div>
|
||||
)}
|
||||
<div className="card-meta">#{summary.id} • {date}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
interface SummarySkeletonProps {
|
||||
sessionId: string;
|
||||
project?: string;
|
||||
}
|
||||
|
||||
export function SummarySkeleton({ sessionId, project }: SummarySkeletonProps) {
|
||||
return (
|
||||
<div className="card summary-card summary-skeleton">
|
||||
<div className="card-header">
|
||||
<span className="card-type">SUMMARY</span>
|
||||
{project && <span>{project}</span>}
|
||||
<div className="processing-indicator">
|
||||
<div className="spinner"></div>
|
||||
<span>Generating...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="skeleton-line skeleton-title"></div>
|
||||
<div className="skeleton-line skeleton-subtitle"></div>
|
||||
<div className="skeleton-line skeleton-subtitle short"></div>
|
||||
<div className="card-meta">Session: {sessionId}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import { ThemePreference } from '../hooks/useTheme';
|
||||
|
||||
interface ThemeToggleProps {
|
||||
preference: ThemePreference;
|
||||
onThemeChange: (theme: ThemePreference) => void;
|
||||
}
|
||||
|
||||
export function ThemeToggle({ preference, onThemeChange }: ThemeToggleProps) {
|
||||
const cycleTheme = () => {
|
||||
const cycle: ThemePreference[] = ['system', 'light', 'dark'];
|
||||
const currentIndex = cycle.indexOf(preference);
|
||||
const nextIndex = (currentIndex + 1) % cycle.length;
|
||||
onThemeChange(cycle[nextIndex]);
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
switch (preference) {
|
||||
case 'light':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
);
|
||||
case 'dark':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>
|
||||
);
|
||||
case 'system':
|
||||
default:
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||||
<line x1="8" y1="21" x2="16" y2="21"></line>
|
||||
<line x1="12" y1="17" x2="12" y2="21"></line>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
switch (preference) {
|
||||
case 'light':
|
||||
return 'Theme: Light (click for Dark)';
|
||||
case 'dark':
|
||||
return 'Theme: Dark (click for System)';
|
||||
case 'system':
|
||||
default:
|
||||
return 'Theme: System (click for Light)';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="theme-toggle-btn"
|
||||
onClick={cycleTheme}
|
||||
title={getTitle()}
|
||||
aria-label={getTitle()}
|
||||
>
|
||||
{getIcon()}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* API endpoint paths
|
||||
* Centralized to avoid magic strings scattered throughout the codebase
|
||||
*/
|
||||
export const API_ENDPOINTS = {
|
||||
OBSERVATIONS: '/api/observations',
|
||||
SUMMARIES: '/api/summaries',
|
||||
PROMPTS: '/api/prompts',
|
||||
SETTINGS: '/api/settings',
|
||||
STATS: '/api/stats',
|
||||
STREAM: '/stream',
|
||||
} as const;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Default settings values for Claude Memory
|
||||
* Shared across UI components and hooks
|
||||
*/
|
||||
export const DEFAULT_SETTINGS = {
|
||||
CLAUDE_MEM_MODEL: 'claude-haiku-4-5',
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50',
|
||||
CLAUDE_MEM_WORKER_PORT: '37777',
|
||||
} as const;
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Timing constants in milliseconds
|
||||
* All timeout and interval durations used throughout the UI
|
||||
*/
|
||||
export const TIMING = {
|
||||
/** SSE reconnection delay after connection error */
|
||||
SSE_RECONNECT_DELAY_MS: 3000,
|
||||
|
||||
/** Stats refresh interval for worker status polling */
|
||||
STATS_REFRESH_INTERVAL_MS: 10000,
|
||||
|
||||
/** Duration to display save status message before clearing */
|
||||
SAVE_STATUS_DISPLAY_DURATION_MS: 3000,
|
||||
} as const;
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* UI-related constants
|
||||
* Pagination, intersection observer settings, and other UI configuration
|
||||
*/
|
||||
export const UI = {
|
||||
/** Number of observations to load per page */
|
||||
PAGINATION_PAGE_SIZE: 50,
|
||||
|
||||
/** Intersection observer threshold (0-1, percentage of visibility needed to trigger) */
|
||||
LOAD_MORE_THRESHOLD: 0.1,
|
||||
} as const;
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Observation, Summary, UserPrompt } from '../types';
|
||||
import { UI } from '../constants/ui';
|
||||
import { API_ENDPOINTS } from '../constants/api';
|
||||
|
||||
interface PaginationState {
|
||||
isLoading: boolean;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
type DataType = 'observations' | 'summaries' | 'prompts';
|
||||
type DataItem = Observation | Summary | UserPrompt;
|
||||
|
||||
/**
|
||||
* Generic pagination hook for observations, summaries, and prompts
|
||||
*/
|
||||
function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: string) {
|
||||
const [state, setState] = useState<PaginationState>({
|
||||
isLoading: false,
|
||||
hasMore: true
|
||||
});
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
// Reset pagination when filter changes
|
||||
useEffect(() => {
|
||||
setOffset(0);
|
||||
setState({
|
||||
isLoading: false,
|
||||
hasMore: true
|
||||
});
|
||||
}, [currentFilter]);
|
||||
|
||||
/**
|
||||
* Load more items from the API
|
||||
*/
|
||||
const loadMore = useCallback(async (): Promise<DataItem[]> => {
|
||||
// Prevent concurrent requests using state
|
||||
if (state.isLoading || !state.hasMore) {
|
||||
return [];
|
||||
}
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: true }));
|
||||
|
||||
try {
|
||||
// Build query params
|
||||
const params = new URLSearchParams({
|
||||
offset: offset.toString(),
|
||||
limit: UI.PAGINATION_PAGE_SIZE.toString()
|
||||
});
|
||||
|
||||
// Add project filter if present
|
||||
if (currentFilter) {
|
||||
params.append('project', currentFilter);
|
||||
}
|
||||
|
||||
const response = await fetch(`${endpoint}?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load ${dataType}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
hasMore: data.hasMore
|
||||
}));
|
||||
|
||||
setOffset(prev => prev + UI.PAGINATION_PAGE_SIZE);
|
||||
return data[dataType] as DataItem[];
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${dataType}:`, error);
|
||||
setState(prev => ({ ...prev, isLoading: false }));
|
||||
return [];
|
||||
}
|
||||
}, [offset, state.hasMore, state.isLoading, currentFilter, endpoint, dataType]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
loadMore
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for paginating observations
|
||||
*/
|
||||
export function usePagination(currentFilter: string) {
|
||||
const observations = usePaginationFor(API_ENDPOINTS.OBSERVATIONS, 'observations', currentFilter);
|
||||
const summaries = usePaginationFor(API_ENDPOINTS.SUMMARIES, 'summaries', currentFilter);
|
||||
const prompts = usePaginationFor(API_ENDPOINTS.PROMPTS, 'prompts', currentFilter);
|
||||
|
||||
return {
|
||||
observations,
|
||||
summaries,
|
||||
prompts
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Observation, Summary, UserPrompt, StreamEvent } from '../types';
|
||||
import { API_ENDPOINTS } from '../constants/api';
|
||||
import { TIMING } from '../constants/timing';
|
||||
|
||||
export function useSSE() {
|
||||
const [observations, setObservations] = useState<Observation[]>([]);
|
||||
const [summaries, setSummaries] = useState<Summary[]>([]);
|
||||
const [prompts, setPrompts] = useState<UserPrompt[]>([]);
|
||||
const [projects, setProjects] = useState<string[]>([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [processingSessions, setProcessingSessions] = useState<Set<string>>(new Set());
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
const connect = () => {
|
||||
// Clean up existing connection
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
}
|
||||
|
||||
const eventSource = new EventSource(API_ENDPOINTS.STREAM);
|
||||
eventSourceRef.current = eventSource;
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('[SSE] Connected');
|
||||
setIsConnected(true);
|
||||
// Clear any pending reconnect
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('[SSE] Connection error:', error);
|
||||
setIsConnected(false);
|
||||
eventSource.close();
|
||||
|
||||
// Reconnect after delay
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
reconnectTimeoutRef.current = undefined; // Clear before reconnecting
|
||||
console.log('[SSE] Attempting to reconnect...');
|
||||
connect();
|
||||
}, TIMING.SSE_RECONNECT_DELAY_MS);
|
||||
};
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data: StreamEvent = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'initial_load':
|
||||
console.log('[SSE] Initial load:', {
|
||||
projects: data.projects?.length || 0
|
||||
});
|
||||
// Only load projects list - data will come via pagination
|
||||
setProjects(data.projects || []);
|
||||
break;
|
||||
|
||||
case 'new_observation':
|
||||
if (data.observation) {
|
||||
console.log('[SSE] New observation:', data.observation.id);
|
||||
setObservations(prev => [data.observation, ...prev]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'new_summary':
|
||||
if (data.summary) {
|
||||
const summary = data.summary;
|
||||
console.log('[SSE] New summary:', summary.id);
|
||||
setSummaries(prev => [summary, ...prev]);
|
||||
// Mark session as no longer processing (summary is the final step)
|
||||
setProcessingSessions(prev => {
|
||||
const next = new Set(prev);
|
||||
next.delete(summary.session_id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'new_prompt':
|
||||
if (data.prompt) {
|
||||
const prompt = data.prompt;
|
||||
console.log('[SSE] New prompt:', prompt.id);
|
||||
setPrompts(prev => [prompt, ...prev]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'processing_status':
|
||||
if (data.processing) {
|
||||
const processing = data.processing;
|
||||
console.log('[SSE] Processing status:', processing);
|
||||
setProcessingSessions(prev => {
|
||||
const next = new Set(prev);
|
||||
if (processing.is_processing) {
|
||||
next.add(processing.session_id);
|
||||
} else {
|
||||
next.delete(processing.session_id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SSE] Failed to parse message:', error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
}
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { observations, summaries, prompts, projects, processingSessions, isConnected };
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Settings } from '../types';
|
||||
import { DEFAULT_SETTINGS } from '../constants/settings';
|
||||
import { API_ENDPOINTS } from '../constants/api';
|
||||
import { TIMING } from '../constants/timing';
|
||||
|
||||
export function useSettings() {
|
||||
const [settings, setSettings] = useState<Settings>(DEFAULT_SETTINGS);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Load initial settings
|
||||
fetch(API_ENDPOINTS.SETTINGS)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setSettings({
|
||||
CLAUDE_MEM_MODEL: data.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL,
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: data.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS,
|
||||
CLAUDE_MEM_WORKER_PORT: data.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load settings:', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const saveSettings = async (newSettings: Settings) => {
|
||||
setIsSaving(true);
|
||||
setSaveStatus('Saving...');
|
||||
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINTS.SETTINGS, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newSettings)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setSettings(newSettings);
|
||||
setSaveStatus('✓ Saved');
|
||||
setTimeout(() => setSaveStatus(''), TIMING.SAVE_STATUS_DISPLAY_DURATION_MS);
|
||||
} else {
|
||||
setSaveStatus(`✗ Error: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setSaveStatus(`✗ Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { settings, saveSettings, isSaving, saveStatus };
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Stats } from '../types';
|
||||
import { API_ENDPOINTS } from '../constants/api';
|
||||
import { TIMING } from '../constants/timing';
|
||||
|
||||
export function useStats() {
|
||||
const [stats, setStats] = useState<Stats>({});
|
||||
|
||||
useEffect(() => {
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINTS.STATS);
|
||||
const data = await response.json();
|
||||
setStats(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Load immediately
|
||||
loadStats();
|
||||
|
||||
// Refresh periodically
|
||||
const interval = setInterval(loadStats, TIMING.STATS_REFRESH_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return { stats };
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export type ThemePreference = 'system' | 'light' | 'dark';
|
||||
export type ResolvedTheme = 'light' | 'dark';
|
||||
|
||||
const STORAGE_KEY = 'claude-mem-theme';
|
||||
|
||||
function getSystemTheme(): ResolvedTheme {
|
||||
if (typeof window === 'undefined') return 'dark';
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function getStoredPreference(): ThemePreference {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === 'system' || stored === 'light' || stored === 'dark') {
|
||||
return stored;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to read theme preference from localStorage:', e);
|
||||
}
|
||||
return 'system';
|
||||
}
|
||||
|
||||
function resolveTheme(preference: ThemePreference): ResolvedTheme {
|
||||
if (preference === 'system') {
|
||||
return getSystemTheme();
|
||||
}
|
||||
return preference;
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const [preference, setPreference] = useState<ThemePreference>(getStoredPreference);
|
||||
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() =>
|
||||
resolveTheme(getStoredPreference())
|
||||
);
|
||||
|
||||
// Update resolved theme when preference changes
|
||||
useEffect(() => {
|
||||
const newResolvedTheme = resolveTheme(preference);
|
||||
setResolvedTheme(newResolvedTheme);
|
||||
document.documentElement.setAttribute('data-theme', newResolvedTheme);
|
||||
}, [preference]);
|
||||
|
||||
// Listen for system theme changes when preference is 'system'
|
||||
useEffect(() => {
|
||||
if (preference !== 'system') return;
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
const newTheme = e.matches ? 'dark' : 'light';
|
||||
setResolvedTheme(newTheme);
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, [preference]);
|
||||
|
||||
const setThemePreference = (newPreference: ThemePreference) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, newPreference);
|
||||
setPreference(newPreference);
|
||||
} catch (e) {
|
||||
console.warn('Failed to save theme preference to localStorage:', e);
|
||||
// Still update the theme even if localStorage fails
|
||||
setPreference(newPreference);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
preference,
|
||||
resolvedTheme,
|
||||
setThemePreference
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
if (!container) {
|
||||
throw new Error('Root element not found');
|
||||
}
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
@@ -0,0 +1,83 @@
|
||||
export interface Observation {
|
||||
id: number;
|
||||
session_id: string;
|
||||
project: string;
|
||||
type: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
content?: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
id: number;
|
||||
session_id: string;
|
||||
project: string;
|
||||
request?: string;
|
||||
learned?: string;
|
||||
completed?: string;
|
||||
next_steps?: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
export interface UserPrompt {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
project: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
export interface SkeletonItem {
|
||||
id: string;
|
||||
session_id: string;
|
||||
project?: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
export type FeedItem =
|
||||
| (Observation & { itemType: 'observation' })
|
||||
| (Summary & { itemType: 'summary' })
|
||||
| (UserPrompt & { itemType: 'prompt' })
|
||||
| (SkeletonItem & { itemType: 'skeleton' });
|
||||
|
||||
export interface StreamEvent {
|
||||
type: 'initial_load' | 'new_observation' | 'new_summary' | 'new_prompt' | 'processing_status';
|
||||
observations?: Observation[];
|
||||
summaries?: Summary[];
|
||||
prompts?: UserPrompt[];
|
||||
projects?: string[];
|
||||
observation?: Observation;
|
||||
summary?: Summary;
|
||||
prompt?: UserPrompt;
|
||||
processing?: {
|
||||
session_id: string;
|
||||
is_processing: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
CLAUDE_MEM_MODEL: string;
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: string;
|
||||
CLAUDE_MEM_WORKER_PORT: string;
|
||||
}
|
||||
|
||||
export interface WorkerStats {
|
||||
version?: string;
|
||||
uptime?: number;
|
||||
activeSessions?: number;
|
||||
sseClients?: number;
|
||||
}
|
||||
|
||||
export interface DatabaseStats {
|
||||
size?: number;
|
||||
observations?: number;
|
||||
sessions?: number;
|
||||
summaries?: number;
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
worker?: WorkerStats;
|
||||
database?: DatabaseStats;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Data manipulation utility functions
|
||||
* Used for merging and deduplicating real-time and paginated data
|
||||
*/
|
||||
|
||||
/**
|
||||
* Merge real-time SSE items with paginated items, removing duplicates and filtering by project
|
||||
* @param liveItems - Items from SSE stream
|
||||
* @param paginatedItems - Items from pagination API (already filtered by project)
|
||||
* @param projectFilter - Current project filter (empty string = all projects)
|
||||
* @returns Merged and deduplicated array
|
||||
*/
|
||||
export function mergeAndDeduplicateByProject<T extends { id: number; project?: string }>(
|
||||
liveItems: T[],
|
||||
paginatedItems: T[],
|
||||
projectFilter: string
|
||||
): T[] {
|
||||
// Filter SSE items by current project (pagination is already filtered)
|
||||
const filteredLive = projectFilter
|
||||
? liveItems.filter(item => item.project === projectFilter)
|
||||
: liveItems;
|
||||
|
||||
// Deduplicate using Set
|
||||
const seen = new Set<number>();
|
||||
return [...filteredLive, ...paginatedItems].filter(item => {
|
||||
if (seen.has(item.id)) return false;
|
||||
seen.add(item.id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Formatting utility functions
|
||||
* Used across UI components for consistent display
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format epoch timestamp to locale string
|
||||
* @param epoch - Timestamp in milliseconds since epoch
|
||||
* @returns Formatted date string
|
||||
*/
|
||||
export function formatDate(epoch: number): string {
|
||||
return new Date(epoch).toLocaleString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format seconds into hours and minutes
|
||||
* @param seconds - Uptime in seconds
|
||||
* @returns Formatted string like "12h 34m" or "-" if no value
|
||||
*/
|
||||
export function formatUptime(seconds?: number): string {
|
||||
if (!seconds) return '-';
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes into human-readable size
|
||||
* @param bytes - Size in bytes
|
||||
* @returns Formatted string like "1.5 MB" or "-" if no value
|
||||
*/
|
||||
export function formatBytes(bytes?: number): string {
|
||||
if (!bytes) return '-';
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
Reference in New Issue
Block a user