Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2af8db6b82 | |||
| 6a4fa85c10 | |||
| 22f4655a8c | |||
| 79ff1849f0 | |||
| ff28db9d76 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.0.3",
|
||||
"version": "5.1.1",
|
||||
"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
|
||||
```
|
||||
```
|
||||
|
||||
+4
-1
@@ -10,4 +10,7 @@ node_modules/
|
||||
plugin/data/
|
||||
plugin/data.backup/
|
||||
package-lock.json
|
||||
private/
|
||||
private/
|
||||
|
||||
# Generated UI files (built from viewer-template.html)
|
||||
src/ui/viewer.html
|
||||
@@ -1,294 +1,276 @@
|
||||
# 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.1
|
||||
|
||||
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 8 search tools to Claude Code
|
||||
- Configured in `plugin/.mcp.json`
|
||||
- Built to `plugin/search-server.js` (ESM format)
|
||||
|
||||
**REST API Endpoints** (6 total):
|
||||
- Session management endpoints
|
||||
- Observation processing endpoints
|
||||
- Worker port tracking
|
||||
**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
|
||||
|
||||
The worker service runs as a PM2-managed background process that handles AI processing separately from the hook execution, preventing hook timeout issues.
|
||||
## How to Make Changes
|
||||
|
||||
### Database Layer
|
||||
|
||||
**Technology**: SQLite 3 with better-sqlite3 native module
|
||||
**Location**: `~/.claude-mem/claude-mem.db`
|
||||
|
||||
**Note**: SessionStore and SessionSearch use better-sqlite3 as the primary database implementation. Database.ts (which uses bun:sqlite) is legacy code.
|
||||
|
||||
**Core Tables**:
|
||||
- `sdk_sessions` - Session tracking with prompt counters
|
||||
- `session_summaries` - AI-generated session summaries (multiple per session)
|
||||
- `observations` - Captured tool usage with structured fields
|
||||
- `user_prompts` - Raw user prompts with FTS5 search (as of v4.2.0)
|
||||
|
||||
**Schema Features**:
|
||||
- FTS5 (Full-Text Search) virtual tables for fast searching
|
||||
- Automatic sync triggers between main tables and FTS5 tables
|
||||
- Support for multi-prompt sessions (prompt_counter, prompt_number)
|
||||
- Hierarchical observations (title, subtitle, facts, narrative, concepts, files_read, files_modified)
|
||||
- Observation types: decision, bugfix, feature, refactor, discovery, change
|
||||
|
||||
**Database Classes**:
|
||||
- `SessionStore` - CRUD operations for sessions, observations, summaries, user prompts
|
||||
- `SessionSearch` - FTS5 full-text search with 8 search methods
|
||||
|
||||
### MCP Search Server
|
||||
|
||||
**Location**: `src/servers/search-server.ts`
|
||||
**Configuration**: `plugin/.mcp.json`
|
||||
|
||||
Exposes 8 specialized search tools to Claude:
|
||||
|
||||
1. **search_observations** - Full-text search across observations
|
||||
2. **search_sessions** - Full-text search across session summaries
|
||||
3. **search_user_prompts** - Full-text search across raw user prompts (as of v4.2.0)
|
||||
4. **find_by_concept** - Find observations tagged with specific concepts
|
||||
5. **find_by_file** - Find observations referencing specific file paths
|
||||
6. **find_by_type** - Find observations by type (decision/bugfix/feature/etc.)
|
||||
7. **get_recent_context** - Get recent session context including summaries and observations for a project
|
||||
8. **advanced_search** - Combine multiple filters with full-text search
|
||||
|
||||
**Search Pipeline**:
|
||||
```
|
||||
Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Results → Claude
|
||||
```
|
||||
|
||||
**Citations**: All search results use the `claude-mem://` URI scheme for referencing specific observations and sessions.
|
||||
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
- Node.js 18+
|
||||
- Claude Code plugin system
|
||||
|
||||
**Windows Users**: better-sqlite3 v12.x includes prebuilt binaries for most configurations. If installation fails, you may need Visual Studio Build Tools:
|
||||
- Install from: https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022
|
||||
- Select "Desktop development with C++"
|
||||
- Or run as Administrator: `npm install --global windows-build-tools`
|
||||
|
||||
**Note**: The plugin automatically installs dependencies on first launch. Installation is cached and only re-runs when the plugin version changes.
|
||||
|
||||
### 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
|
||||
```
|
||||
@@ -299,7 +281,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
|
||||
@@ -308,260 +290,116 @@ 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 (v5.1.0)
|
||||
|
||||
For detailed version history and changelog, see [CHANGELOG.md](CHANGELOG.md).
|
||||
**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
|
||||
|
||||
**Current Version**: 5.0.3
|
||||
**New Worker Endpoints** (8 HTTP/SSE endpoints, +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
|
||||
- `/health` - Health check endpoint
|
||||
|
||||
### Recent Highlights
|
||||
**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
|
||||
|
||||
#### v5.0.3 (2025-11-05)
|
||||
**Breaking Changes**: None (patch version)
|
||||
**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
|
||||
|
||||
**Fixes**:
|
||||
- Fixed Windows installation with smart caching installer (PR #54: scripts/smart-install.js)
|
||||
- Eliminated redundant npm install executions on every SessionStart (improved from 2-5s to ~10ms)
|
||||
- Added comprehensive Windows troubleshooting with VS Build Tools guidance
|
||||
- Fixed dynamic Python version detection in Windows error messages (scripts/smart-install.js:106-115)
|
||||
**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.
|
||||
|
||||
**Improvements**:
|
||||
- Smart install now caches version state in `.install-version` file
|
||||
- Only runs npm install when needed: first time, version change, or missing dependencies
|
||||
- Enhanced rsync to respect gitignore rules in sync-marketplace (package.json:38)
|
||||
- Better PM2 worker startup verification and management
|
||||
- Cross-platform compatible installer (pure Node.js, no shell dependencies)
|
||||
### Previous Release (v5.0.3)
|
||||
|
||||
**Technical Details**:
|
||||
- New: scripts/smart-install.js (smart caching installer with PM2 worker management)
|
||||
- Modified: plugin/hooks/hooks.json:25 (use smart-install.js instead of raw npm install)
|
||||
- Modified: .gitignore (added .install-version cache file)
|
||||
- Modified: CLAUDE.md (added Windows requirements and troubleshooting section)
|
||||
- Modified: package.json:38 (enhanced sync-marketplace with --filter=':- .gitignore' --exclude=.git)
|
||||
- Root cause: npm install was running on every SessionStart regardless of whether dependencies changed
|
||||
- Impact: 200x faster SessionStart for cached installations (10ms vs 2-5s)
|
||||
**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)
|
||||
|
||||
#### v5.0.2 (2025-11-04)
|
||||
**Breaking Changes**: None (patch version)
|
||||
## Configuration Users Can Set
|
||||
|
||||
**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
|
||||
**Model Selection** (`~/.claude/settings.json`):
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_MODEL": "claude-haiku-4-5" // or sonnet-4-5, opus-4, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
**Context Observation Count** (`~/.claude/settings.json`):
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "50" // default, adjust based on needs
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### v5.0.1 (2025-11-04)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**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)
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
#### v4.3.4 (2025-11-01)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**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
|
||||
|
||||
**Technical Details**:
|
||||
- Modified: plugin/hooks/hooks.json:4 (added `"matcher": "startup|clear|compact"`)
|
||||
- Impact: Hooks now skip execution when resuming existing sessions
|
||||
|
||||
#### v4.3.3 (2025-10-27)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**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"
|
||||
|
||||
**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).
|
||||
|
||||
### Windows Installation Issues
|
||||
### 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.
|
||||
|
||||
**Error: `ERR_MODULE_NOT_FOUND: Cannot find package 'better-sqlite3'`**
|
||||
## File Locations
|
||||
|
||||
This typically means the native module failed to install. Solutions:
|
||||
**Source**: `/Users/alexnewman/Scripts/claude-mem/src/`
|
||||
**Built Plugin**: `/Users/alexnewman/Scripts/claude-mem/plugin/`
|
||||
**Installed Plugin**: `~/.claude/plugins/marketplaces/thedotmack/`
|
||||
**Database**: `~/.claude-mem/claude-mem.db`
|
||||
**Usage Logs**: `~/.claude-mem/usage-logs/usage-YYYY-MM-DD.jsonl`
|
||||
|
||||
1. **Check for prebuilt binaries** (works for 95% of users):
|
||||
- better-sqlite3 v12.x ships with prebuilt binaries
|
||||
- They should install automatically
|
||||
- No build tools needed if prebuilts match your Node.js version
|
||||
## Quick Reference
|
||||
|
||||
2. **If prebuilts don't match your configuration**:
|
||||
- Install Visual Studio Build Tools:
|
||||
- Download: https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022
|
||||
- Select "Desktop development with C++"
|
||||
- Restart terminal after installation
|
||||
- Or use npm (run as Administrator):
|
||||
```bash
|
||||
npm install --global windows-build-tools
|
||||
```
|
||||
|
||||
3. **Verify installation**:
|
||||
```bash
|
||||
cd ~/.claude/plugins/marketplaces/thedotmack
|
||||
npm list better-sqlite3
|
||||
```
|
||||
|
||||
4. **Check build tools** (if needed):
|
||||
- Python 3.6+ required
|
||||
- Visual C++ Build Tools required
|
||||
- See: https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md
|
||||
|
||||
**Smart Install Script**: The plugin uses a cached installation system. It only runs npm install when:
|
||||
- First time setup (no node_modules)
|
||||
- Plugin version changes
|
||||
- Dependencies are missing
|
||||
|
||||
### Worker Service Issues
|
||||
- Check PM2 status: `pm2 list`
|
||||
- View logs: `npm run worker:logs`
|
||||
- Restart worker: `npm run worker:restart`
|
||||
|
||||
### Database Issues
|
||||
- Database location: `~/.claude-mem/claude-mem.db`
|
||||
- Check schema: `sqlite3 <db-path> ".schema"`
|
||||
- FTS5 tables are automatically synchronized via triggers
|
||||
|
||||
### Hook Issues
|
||||
- Hooks output to Claude Code's hook execution log
|
||||
- Check `plugin/scripts/` for built executables
|
||||
|
||||
### Model Configuration Issues
|
||||
- Use `./claude-mem-settings.sh` to manage model settings
|
||||
- Settings stored in `~/.claude/settings.json`
|
||||
- Default fallback: `claude-sonnet-4-5`
|
||||
|
||||
## Citations & References
|
||||
|
||||
This project uses the `claude-mem://` URI scheme for citations:
|
||||
- `claude-mem://observation/{id}` - References specific observations
|
||||
- `claude-mem://session/{id}` - References specific sessions
|
||||
|
||||
All MCP search results include citations, enabling Claude to reference specific historical context.
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0
|
||||
|
||||
## Repository
|
||||
|
||||
https://github.com/thedotmack/claude-mem
|
||||
**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)
|
||||
|
||||
Generated
+93
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-mem",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.0",
|
||||
"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.3",
|
||||
"version": "5.1.1",
|
||||
"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 --filter=':- .gitignore' --exclude=.git ./ ~/.claude/plugins/marketplaces/thedotmack/ # --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.3",
|
||||
"version": "5.1.1",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
@@ -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_output: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_output: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);
|
||||
|
||||
+225
-157
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,512 @@
|
||||
<!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;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||
background: #1e1e1e;
|
||||
color: #cccccc;
|
||||
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: #1e1e1e;
|
||||
border-left: 1px solid #404040;
|
||||
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 #404040;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #252526;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid #404040;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #252526;
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
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: #dadada;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
background: transparent;
|
||||
border: 1px solid #404040;
|
||||
padding: 8px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #cccccc;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-btn:hover {
|
||||
background: #2d2d2d;
|
||||
border-color: #58a6ff;
|
||||
}
|
||||
|
||||
.settings-btn.active {
|
||||
background: #0969da;
|
||||
border-color: #0969da;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #e74856;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background: #16c60c;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
select,
|
||||
input,
|
||||
button {
|
||||
background: #2d2d2d;
|
||||
color: #cccccc;
|
||||
border: 1px solid #404040;
|
||||
padding: 6px 12px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
select:hover,
|
||||
input:hover {
|
||||
border-color: #58a6ff;
|
||||
}
|
||||
|
||||
select:focus,
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #58a6ff;
|
||||
box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
background: #0969da;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #1177e6;
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
background: #0860ca;
|
||||
}
|
||||
|
||||
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: #2d2d2d;
|
||||
border: 1px solid #404040;
|
||||
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: #505050;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.card-type {
|
||||
padding: 2px 8px;
|
||||
background: #58a6ff20;
|
||||
color: #58a6ff;
|
||||
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: #e0e0e0;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 14px;
|
||||
color: #a0a0a0;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
font-size: 12px;
|
||||
color: #6e7681;
|
||||
margin-top: 8px;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border-color: #9e6a03;
|
||||
background: #3d2f00;
|
||||
}
|
||||
|
||||
.summary-card:hover {
|
||||
border-color: #ae7a13;
|
||||
}
|
||||
|
||||
.summary-card .card-type {
|
||||
background: #f2cc6020;
|
||||
color: #f2cc60;
|
||||
}
|
||||
|
||||
.summary-card .card-title {
|
||||
color: #f2cc60;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
padding: 18px;
|
||||
border-bottom: 1px solid #404040;
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 14px;
|
||||
color: #e0e0e0;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 10px 12px;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #8b949e;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
color: #e0e0e0;
|
||||
font-weight: 600;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.stats-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #424242;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4e4e4e;
|
||||
}
|
||||
|
||||
.save-status {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.prompt-card {
|
||||
border-color: #6e40c9;
|
||||
background: #2d1b4e;
|
||||
}
|
||||
|
||||
.prompt-card:hover {
|
||||
border-color: #8e6cdb;
|
||||
}
|
||||
|
||||
.prompt-card .card-type {
|
||||
background: #6e40c920;
|
||||
color: #8e6cdb;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-top: 12px;
|
||||
line-height: 1.6;
|
||||
color: #cccccc;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.processing-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #58a6ff;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #404040;
|
||||
border-top-color: #58a6ff;
|
||||
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, #404040 25%, #505050 50%, #404040 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();
|
||||
@@ -211,13 +211,14 @@ function runNpmInstall() {
|
||||
|
||||
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 pm2 start which works whether worker is running or not
|
||||
// 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 start "${ECOSYSTEM_CONFIG}"`, {
|
||||
execSync(`"${PM2_PATH}" start "${ECOSYSTEM_CONFIG}"`, {
|
||||
cwd: PLUGIN_ROOT,
|
||||
stdio: 'pipe', // Capture output to avoid clutter
|
||||
encoding: 'utf-8',
|
||||
|
||||
@@ -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'];
|
||||
@@ -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(
|
||||
@@ -296,6 +755,9 @@ class WorkerService {
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -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,512 @@
|
||||
<!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;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||
background: #1e1e1e;
|
||||
color: #cccccc;
|
||||
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: #1e1e1e;
|
||||
border-left: 1px solid #404040;
|
||||
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 #404040;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #252526;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid #404040;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #252526;
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
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: #dadada;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
background: transparent;
|
||||
border: 1px solid #404040;
|
||||
padding: 8px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #cccccc;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-btn:hover {
|
||||
background: #2d2d2d;
|
||||
border-color: #58a6ff;
|
||||
}
|
||||
|
||||
.settings-btn.active {
|
||||
background: #0969da;
|
||||
border-color: #0969da;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #e74856;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background: #16c60c;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
select,
|
||||
input,
|
||||
button {
|
||||
background: #2d2d2d;
|
||||
color: #cccccc;
|
||||
border: 1px solid #404040;
|
||||
padding: 6px 12px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
select:hover,
|
||||
input:hover {
|
||||
border-color: #58a6ff;
|
||||
}
|
||||
|
||||
select:focus,
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #58a6ff;
|
||||
box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
background: #0969da;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #1177e6;
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
background: #0860ca;
|
||||
}
|
||||
|
||||
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: #2d2d2d;
|
||||
border: 1px solid #404040;
|
||||
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: #505050;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.card-type {
|
||||
padding: 2px 8px;
|
||||
background: #58a6ff20;
|
||||
color: #58a6ff;
|
||||
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: #e0e0e0;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 14px;
|
||||
color: #a0a0a0;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
font-size: 12px;
|
||||
color: #6e7681;
|
||||
margin-top: 8px;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border-color: #9e6a03;
|
||||
background: #3d2f00;
|
||||
}
|
||||
|
||||
.summary-card:hover {
|
||||
border-color: #ae7a13;
|
||||
}
|
||||
|
||||
.summary-card .card-type {
|
||||
background: #f2cc6020;
|
||||
color: #f2cc60;
|
||||
}
|
||||
|
||||
.summary-card .card-title {
|
||||
color: #f2cc60;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
padding: 18px;
|
||||
border-bottom: 1px solid #404040;
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 14px;
|
||||
color: #e0e0e0;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 10px 12px;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #8b949e;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
color: #e0e0e0;
|
||||
font-weight: 600;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.stats-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #424242;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4e4e4e;
|
||||
}
|
||||
|
||||
.save-status {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.prompt-card {
|
||||
border-color: #6e40c9;
|
||||
background: #2d1b4e;
|
||||
}
|
||||
|
||||
.prompt-card:hover {
|
||||
border-color: #8e6cdb;
|
||||
}
|
||||
|
||||
.prompt-card .card-type {
|
||||
background: #6e40c920;
|
||||
color: #8e6cdb;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-top: 12px;
|
||||
line-height: 1.6;
|
||||
color: #cccccc;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.processing-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #58a6ff;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #404040;
|
||||
border-top-color: #58a6ff;
|
||||
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, #404040 25%, #505050 50%, #404040 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,115 @@
|
||||
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 { 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 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}
|
||||
/>
|
||||
<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,89 @@
|
||||
import React from 'react';
|
||||
|
||||
interface HeaderProps {
|
||||
isConnected: boolean;
|
||||
projects: string[];
|
||||
currentFilter: string;
|
||||
onFilterChange: (filter: string) => void;
|
||||
onSettingsToggle: () => void;
|
||||
sidebarOpen: boolean;
|
||||
isProcessing: boolean;
|
||||
}
|
||||
|
||||
export function Header({
|
||||
isConnected,
|
||||
projects,
|
||||
currentFilter,
|
||||
onFilterChange,
|
||||
onSettingsToggle,
|
||||
sidebarOpen,
|
||||
isProcessing
|
||||
}: 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>
|
||||
<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,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,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