fix: Remove all session validation to prevent continuation errors after /exit

Root cause: Hooks provide session_id as the source of truth. We were adding
unnecessary validation (checking if sessions exist, checking status, etc.)
which caused 409 conflicts when continuing sessions after /exit.

Changes:
1. worker-service.ts: Removed 409 "Session already exists" check in handleInit
2. SessionStore.ts: Made createSDKSession idempotent using INSERT OR IGNORE
3. new-hook.ts: Simplified to just call createSDKSession - no findActiveSDKSession,
   no reactivateSession logic, no status management
4. save-hook.ts: Removed session validation, use fixed port instead of session.worker_port
5. summary-hook.ts: Removed session validation, use fixed port instead of session.worker_port

Philosophy: Hooks manage lifecycle, we just save data with whatever session_id
they give us. No validation, no status checks, no guessing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-10-21 21:44:58 -04:00
parent e073c0b75f
commit c27f07023c
17 changed files with 3706 additions and 170 deletions
File diff suppressed because it is too large Load Diff
+297
View File
@@ -0,0 +1,297 @@
# Claude Never Forgets
**Give Claude a memory that spans your entire project.**
---
## The Problem
```
You: "Remember that bug we fixed last Tuesday with the auth flow?"
Claude: "I don't have access to previous conversations..."
```
Every `/clear` wipes Claude's memory. Every new session starts from zero. You repeat yourself constantly.
**Until now.**
---
## What Changes
### Before claude-mem
```typescript
// Monday: You debug an issue
You: "Why is the database connection failing?"
Claude: [Helps you fix it]
// Wednesday: Similar issue appears
You: "The database is timing out again"
Claude: "Let me investigate..." [Starts from scratch]
```
### After claude-mem
```typescript
// Monday: You debug an issue
You: "Why is the database connection failing?"
Claude: [Helps you fix it]
Remembers: connection pool exhaustion pattern
// Wednesday: Similar issue appears
You: "The database is timing out again"
Claude: "Based on Monday's session, this looks like the same
connection pool issue. Let me check the pool size config..."
```
Claude **remembers**. Claude **learns**. Claude gets **better** over time.
---
## Real Examples
### 1. **Context Across Sessions**
**Without claude-mem:**
```
Session 1: "We use Redux for state management"
Session 2: "What state management do you use?" ❌
```
**With claude-mem:**
```
Session 1: "We use Redux for state management"
Session 2: Claude already knows you use Redux ✓
Suggests Redux patterns automatically ✓
References your store structure ✓
```
### 2. **Architectural Memory**
**Your third session of the day:**
```
You: "Add a new API endpoint for user preferences"
Claude: "I see from previous sessions that:
- Your API follows REST conventions in src/api/
- You use Zod for validation
- Auth middleware is required for user routes
- You prefer async/await over promises
I'll create the endpoint following these patterns..."
```
**No explaining. No repeating. Just building.**
### 3. **Bug Pattern Recognition**
```
Week 1: Fixed race condition in webhook handler
Week 2: Different race condition in event processor
Claude: "This looks similar to the webhook race condition
we fixed last week. The same solution should work..."
```
---
## How It Works
```
┌─────────────────┐
│ You code with │
│ Claude today │
└────────┬────────┘
┌─────────────────────────────┐
│ claude-mem captures & │
│ compresses everything │
│ into structured memories │
└────────┬────────────────────┘
┌─────────────────────────────┐
│ Tomorrow, Claude starts │
│ with full context of │
│ your project history │
└─────────────────────────────┘
```
**Automatic. Zero effort. Always on.**
---
## What Gets Remembered
**Decisions**: "Why did we choose this architecture?"
**Bugs Fixed**: "How did we solve this before?"
**Code Patterns**: "What's our convention for this?"
**File Changes**: "What did we modify last session?"
**Refactorings**: "What was the old implementation?"
**Dependencies**: "Which libraries are we using?"
Everything Claude does with you gets compressed into **searchable, reusable memory**.
---
## Powerful Search
Ask Claude to search your project history:
```
You: "Find all the database migrations we did"
Claude: [Searches across all sessions]
"I found 7 database-related changes:
- March 15: Added user_preferences table
- March 12: Migration for OAuth tokens
- March 8: Index optimization on sessions
..."
You: "What decisions did we make about authentication?"
Claude: [Retrieves decision observations]
"We decided to use JWT tokens because..."
```
7 specialized search tools. Instant recall. Full project history.
---
## The Numbers
| Metric | Before | After |
|--------|--------|-------|
| Context repetition | Every session | Never |
| Onboarding time | 5-10 min per session | 0 seconds |
| Bug re-investigation | Common | Rare |
| Architectural questions | "What did we decide?" | Claude already knows |
| Code pattern consistency | Manual enforcement | Automatic |
---
## Installation
### Quick Start (2 minutes)
```bash
# 1. Clone and install
git clone https://github.com/thedotmack/claude-mem.git
cd claude-mem
# 2. Add to Claude Code
/plugin marketplace add .claude-plugin/marketplace.json
# 3. Install
/plugin install claude-mem
```
**Done.** Claude now has memory.
---
## Configuration
Choose your AI model (controls cost vs. quality of memory compression):
```bash
./claude-mem-settings.sh
```
**Models:**
- `claude-haiku-4-5` - Fast & cheap
- `claude-sonnet-4-5` - Balanced (default) ✓
- `claude-opus-4` - Maximum quality
---
## Under The Hood
**Simple architecture, powerful results:**
1. **Hooks** capture every tool Claude uses
2. **Worker service** compresses observations with AI
3. **SQLite database** stores structured memories
4. **MCP server** makes everything searchable
5. **Context injection** gives Claude the right memories at the right time
**Zero maintenance. Runs in the background. Just works.**
---
## Use Cases
### Solo Developers
- Never lose context between coding sessions
- Build on past decisions automatically
- Remember why you made each choice
### Team Projects
- Share architectural knowledge across sessions
- Maintain consistency in code patterns
- Document decisions as they happen
### Learning & Experiments
- Track what you tried and what worked
- Build a personal knowledge base
- Learn from past mistakes
### Large Refactors
- Remember what you changed across multiple sessions
- Track progress on multi-day tasks
- Maintain context through interruptions
---
## What Developers Say
> *"I used to spend 10 minutes every morning explaining my project to Claude. Now it just knows."*
> *"It's like having a teammate who was actually there for every line of code."*
> *"The search is incredible. I can ask about decisions we made weeks ago."*
---
## FAQ
**Does this slow down Claude?**
No. Memory processing happens in the background. Claude responds instantly.
**How much does it cost?**
Minimal. Memory compression uses your chosen model (default: Sonnet 4.5). Typical cost: $0.01-0.05 per coding session.
**Where is data stored?**
Locally in `~/.claude-mem/claude-mem.db`. Fully private. Never leaves your machine.
**Can I search my memories?**
Yes. 7 specialized search tools available through Claude.
**Does it work with existing projects?**
Yes. Starts learning immediately when installed.
**What if I want to forget something?**
Delete observations directly from the SQLite database, or start fresh by removing the DB file.
---
## Get Started
```bash
git clone https://github.com/thedotmack/claude-mem.git
cd claude-mem
/plugin marketplace add .claude-plugin/marketplace.json
/plugin install claude-mem
```
**Give Claude a memory. Transform how you code.**
---
## Learn More
- [Technical Documentation](./CLAUDE.md)
- [GitHub Repository](https://github.com/thedotmack/claude-mem)
- [Report Issues](https://github.com/thedotmack/claude-mem/issues)
**License**: AGPL-3.0
**Version**: 4.1.0
**Author**: Alex Newman ([@thedotmack](https://github.com/thedotmack))
+283
View File
@@ -0,0 +1,283 @@
# Magic UI Components Catalog
Complete catalog of Magic UI components with descriptions and use cases.
## Animation Components
### 1. **Animated List**
- **Purpose**: Animates list items sequentially with delay
- **Use Case**: Showcasing events, notifications, feature lists
- **Key Props**: `delay` (ms between items)
- **Effect**: Staggered reveal animation
### 2. **Text Animate**
- **Purpose**: Sophisticated text animation effects
- **Animation Types**:
- `blurIn` - Characters fade from blur
- `slideUp` - Words slide up
- `scaleUp` - Text scales up
- `fadeIn` - Lines fade in
- `slideLeft` - Characters slide from left
- **Animate By**: `character`, `word`, `text`, `line`
- **Use Case**: Hero headlines, feature announcements, storytelling
- **Customization**: Delay, duration, custom motion variants
### 3. **Flip Text**
- **Purpose**: Vertical flip animation for text
- **Key Props**: `duration`, `delayMultiple`, custom variants
- **Use Case**: Eye-catching headlines, call-to-actions
### 4. **Morphing Text**
- **Purpose**: Dynamic text transitions between multiple strings
- **Key Props**: `texts` array (strings to morph between)
- **Use Case**: Dynamic value propositions, rotating benefits
### 5. **Word Rotate**
- **Purpose**: Vertical rotation of words
- **Key Props**: `words` (string array), `duration` (2500ms default)
- **Use Case**: Rotating feature names, dynamic headlines
### 6. **Aurora Text**
- **Purpose**: Beautiful aurora text effect
- **Key Props**: `colors` array, `speed` multiplier
- **Default Colors**: `["#FF0080", "#7928CA", "#0070F3", "#38bdf8"]`
- **Use Case**: Premium headlines, brand emphasis
## Visual Effect Components
### 7. **Orbiting Circles**
- **Purpose**: Circles moving in orbit along circular paths
- **Key Props**:
- `radius` (orbit size)
- `duration` (animation speed)
- `reverse` (direction)
- `delay`, `path` (show orbit path)
- `iconSize`, `speed`
- **Use Case**: Technology visualization, ecosystem diagrams, feature satellites
### 8. **Particles**
- **Purpose**: Animated particle background with depth and interactivity
- **Use Case**: Hero sections, immersive backgrounds
### 9. **Confetti**
- **Purpose**: Celebration confetti effect
- **Key Props**:
- `particleCount`, `angle`, `spread`
- `startVelocity`, `decay`, `gravity`
- `colors`, `shapes` (square, circle, star)
- `origin` point
- **Includes**: `ConfettiButton` wrapper component
- **Use Case**: Success states, milestones, achievements
### 10. **Border Beam**
- **Purpose**: Animated beam effect along borders
- **Key Props**: `reverse`, spring animations
- **Use Case**: Card highlights, section emphasis
### 11. **Shine Border**
- **Purpose**: Animated shining border effect
- **Key Props**: `color` array, `borderWidth`, `duration`
- **Use Case**: Premium cards, CTAs, feature boxes
### 12. **Magic Card**
- **Purpose**: Spotlight effect following mouse cursor with border highlights
- **Key Props**:
- `gradientSize` (200 default)
- `gradientColor` (#262626 default)
- `gradientOpacity` (0.8)
- `gradientFrom`/`gradientTo` (border colors)
- **Use Case**: Interactive feature cards, pricing tables
## Background Components
### 13. **Grid Beams**
- **Purpose**: Dynamic grid background with animated light beams
- **Key Props**:
- `gridSize` (40px default)
- `gridColor` (rgba)
- `rayCount` (15 default)
- `rayOpacity` (0.35)
- `raySpeed`, `rayLength` (45vh)
- `gridFadeStart`/`gridFadeEnd` (%)
- `backgroundColor`
- **Use Case**: Hero sections, feature backgrounds, immersive layouts
### 14. **Warp Background**
- **Purpose**: Warped perspective grid effect
- **Key Props**:
- `perspective` (depth)
- `beamsPerSide` (4 default)
- `beamSize` (thickness)
- `beamDuration` (speed)
- `gridColor`
- **Use Case**: Futuristic hero sections, tech-focused pages
### 15. **Dot Pattern**
- **Purpose**: Customizable dotted background pattern (SVG)
- **Key Props**: `width`, `height`, `cx`, `cy`, `cr` (dot radius)
- **Effects**: Supports glow effects
- **Use Case**: Subtle backgrounds, section dividers
## Interactive Components
### 16. **Dock**
- **Purpose**: macOS-style dock with magnification effect
- **Key Props**:
- `iconMagnification` (zoom amount)
- `iconDistance` (hover range)
- `direction` (middle, start, end)
- **Child Component**: `DockIcon`
- **Use Case**: Navigation, tool showcases, social links
### 17. **Scratch To Reveal**
- **Purpose**: Interactive scratch-off effect revealing hidden content
- **Key Props**:
- `width`, `height`
- `minScratchPercentage` (50 default, completion threshold)
- `onComplete` callback
- `gradientColors`
- **Use Case**: Interactive reveals, gamification, teasers
### 18. **Highlighter**
- **Purpose**: Animated text highlighting and underlining
- **Key Props**:
- `color`
- `strokeWidth`
- `action` (underline, highlight)
- `animationDuration`
- `iterations`, `padding`
- **Use Case**: Emphasis on key phrases, call-outs
## Layout/Display Components
### 19. **Marquee**
- **Purpose**: Scrolling content (horizontal or vertical)
- **Key Props**:
- `reverse` (direction)
- `pauseOnHover`
- `vertical` (orientation)
- `repeat` (count)
- **Effects**: 3D perspective option
- **Use Case**: Testimonials, logo clouds, infinite scrollers
### 20. **Safari**
- **Purpose**: Safari browser mockup for showcasing
- **Key Props**:
- `url` (address bar)
- `imageSrc` or `videoSrc`
- `width` (1203 default), `height` (753)
- `mode` (default, simple)
- **Use Case**: Product demos, website previews
### 21. **Bento Grid**
- **Purpose**: Grid layout for organizing content
- **Use Case**: Feature showcases, portfolios
### 22. **Avatar Circles**
- **Purpose**: Overlapping circles of avatars
- **Key Props**: `numPeople` (99 default, shown in last circle)
- **Use Case**: Social proof, team displays, user counts
## Timeline Components
### 23. **Arc Timeline**
- **Purpose**: Curved timeline visualizing milestones
- **Key Props**:
- `data` (array of {time, title})
- `arcConfig`:
- `circleWidth` (5000 default)
- `angleBetweenMinorSteps` (0.35)
- `lineCountFillBetweenSteps` (10)
- `boundaryPlaceholderLinesCount` (50)
- `defaultActiveStep` ({time, stepIndex})
- **Use Case**: Project roadmaps, product evolution, version history
## Button Components
### 24. **Shiny Button**
- **Purpose**: Button with shiny effect
- **Features**: Dark/light mode support
- **Use Case**: Primary CTAs, important actions
### 25. **Pulsating Button**
- **Purpose**: Button with pulsing wave animation
- **Key Props**: `pulseColor` (RGB), `duration`
- **Use Case**: Attention-grabbing CTAs, urgent actions
### 26. **Rainbow Button**
- **Purpose**: Rainbow gradient button effect
- **Variants**: Default, Outline
- **Use Case**: Premium CTAs, playful actions
## Theme Components
### 27. **Animated Theme Toggler**
- **Purpose**: Smooth animated light/dark mode toggle
- **Built With**: Tailwind CSS
- **Use Case**: Theme switching UI
---
## Installation
```bash
# Via CLI (recommended)
npx shadcn-ui@latest add [component-name]
# Manual installation
npm install magicui
# or
yarn add magicui
```
## Component Categories Summary
| Category | Components | Best For |
|----------|-----------|----------|
| **Text Animation** | Animated List, Text Animate, Flip Text, Morphing Text, Word Rotate, Aurora Text | Headlines, feature lists, dynamic content |
| **Visual Effects** | Orbiting Circles, Particles, Confetti, Border Beam, Shine Border, Magic Card | Visual interest, interactivity, emphasis |
| **Backgrounds** | Grid Beams, Warp Background, Dot Pattern | Hero sections, immersive layouts |
| **Interactive** | Dock, Scratch To Reveal, Highlighter | User engagement, gamification |
| **Layout** | Marquee, Safari, Bento Grid, Avatar Circles | Content organization, showcases |
| **Timeline** | Arc Timeline | Roadmaps, history, progression |
| **Buttons** | Shiny Button, Pulsating Button, Rainbow Button | CTAs, actions |
| **Utility** | Animated Theme Toggler | UI controls |
## Design Philosophy
Magic UI components focus on:
- **Delight**: Unexpected animations that create joy
- **Fluidity**: Smooth, natural motion
- **Performance**: Optimized for web performance
- **Flexibility**: Highly customizable via props
- **Modern**: Built with React, Tailwind CSS, Framer Motion
## Use Case Recommendations by Landing Page Section
### Hero Sections
- Grid Beams or Warp Background (background)
- Aurora Text or Morphing Text (headline)
- Pulsating Button or Rainbow Button (CTA)
- Orbiting Circles (tech visualization)
### Feature Showcases
- Bento Grid (layout)
- Magic Card (individual features)
- Animated List (feature details)
- Highlighter (emphasis)
### Social Proof
- Marquee (testimonials/logos)
- Avatar Circles (user counts)
### Product Demos
- Safari (browser mockups)
- Border Beam or Shine Border (emphasis)
### Roadmaps/Progress
- Arc Timeline (milestones)
### Interactive Elements
- Scratch To Reveal (teasers)
- Dock (navigation/tools)
- Confetti (celebrations)
+471
View File
@@ -0,0 +1,471 @@
# Magic UI Landing Page Creative Ideas
Creative applications of Magic UI components for each section of the claude-mem landing page.
## Section 1: HERO - "Claude Never Forgets"
### Idea 1: "Fading Memory" Effect ⭐ WINNER
**Components**: Morphing Text, Grid Beams, Orbiting Circles, Scratch To Reveal
**Vision**:
- **Headline**: Morphing Text rotating between:
- "Claude Never Forgets"
- "Claude Always Remembers"
- "Claude Learns Forever"
- **Background**: Grid Beams in blue/purple gradient representing the "memory grid"
- **Central Visual**: Orbiting Circles around a central "brain/database" icon
- Inner orbit: File icons (representing code files)
- Middle orbit: Lightbulb icons (decisions)
- Outer orbit: Bug icons (fixes)
- **Problem Statement**: Scratch To Reveal overlay
- User must scratch to reveal: "Every /clear wipes Claude's memory"
- Makes the pain point visceral and interactive
- **CTA**: Pulsating Button "Give Claude a Memory"
### Idea 2: "Memory Timeline" Hero
**Components**: Warp Background, Arc Timeline, Aurora Text, Word Rotate
**Vision**:
- Warp Background creating perspective depth
- Arc Timeline showing "Session 1 → Session 2 → Session 3" with memories persisting across
- Aurora Text for main headline with flowing gradient
- Word Rotate cycling pain points: "Repeating" → "Explaining" → "Re-discovering" → "Forgetting"
### Idea 3: "Brain Storage" Visualization
**Components**: Particles, Magic Card, Highlighter, Pulsating Button
**Vision**:
- Particles background (subtle, neuron-like firing)
- Central Magic Card with spotlight effect containing headline
- Highlighter emphasizing "Never" and "Forgets" with animated underlines
- Pulsating Button for CTA with urgency
---
## Section 2: BEFORE/AFTER Comparison
### Idea 1: "Split Screen Wipe" ⭐ WINNER
**Components**: Safari, Text Animate, Border Beam, Scratch To Reveal
**Vision**:
- Two Safari browser mockups side by side
- **Left (Before)**: Conversation fades/blurs out showing memory loss
- **Right (After)**: Sharp, persistent content with Border Beam highlighting
- Text Animate with slideUp for conversations appearing sequentially
- Optional: Scratch To Reveal over "Before" side to uncover the painful reality
### Idea 2: "Memory Decay Visualization"
**Components**: Text Animate, Shine Border, Magic Card, Animated Theme Toggler
**Vision**:
- Before side: Text with progressive fade-out animation (opacity decreasing)
- After side: Text with Shine Border, glowing persistently
- Magic Card on After side with spotlight following mouse
- Animated Theme Toggler metaphorically switching "forgetting" → "remembering"
### Idea 3: "Conversation Replay"
**Components**: Animated List, Border Beam, Highlighter
**Vision**:
- Animated List showing conversation history
- Before: List items fade out and disappear sequentially
- After: List items persist, Border Beam appears on referenced items
- Highlighter underlining key persistent information on After side
---
## Section 3: REAL EXAMPLES (3 Scenarios)
### Idea 1: "Tabbed Experience"
**Components**: Bento Grid, Magic Card, Text Animate, Flip Text, Confetti
**Vision**:
- Bento Grid layout with 3 Magic Cards (one per scenario)
- Each card has spotlight effect on hover
- Text Animate (blurIn) for code examples appearing
- Flip Text for headings revealing scenario names
- Confetti burst when clicking through to third example (pattern recognized!)
### Idea 2: "Timeline Story" ⭐ WINNER
**Components**: Arc Timeline, Orbiting Circles, Highlighter, Animated List
**Vision**:
- Arc Timeline showing progression: "Monday Session" → "Wednesday Session"
- Each timeline node expands to show the scenario details
- Orbiting Circles around active node showing:
- Files involved (icons in orbit)
- Decisions made (decision icons)
- Bugs fixed (bug icons)
- Highlighter emphasizing key remembered details in the expanded content
- Animated List revealing the bulleted "Claude remembers" items
### Idea 3: "Memory Card Flip"
**Components**: Scratch To Reveal, Magic Card, Pulsating Button, Word Rotate
**Vision**:
- Three cards with Scratch To Reveal overlay
- Scratch to reveal what Claude remembers from previous sessions
- Magic Card underneath with gradient borders
- Pulsating Button to cycle through scenarios
- Word Rotate showing memory types: "Patterns" → "Decisions" → "Context" → "Architecture"
---
## Section 4: HOW IT WORKS (Pipeline)
### Idea 1: "Animated Data Flow"
**Components**: Magic Card, Orbiting Circles, Border Beam, Text Animate, Particles
**Vision**:
- Three connected Magic Cards showing pipeline stages:
1. "You code with Claude today"
2. "claude-mem captures & compresses"
3. "Tomorrow, Claude starts with context"
- Orbiting Circles representing data flowing between stages
- Border Beam animating along connections between cards
- Text Animate (slideUp) for each stage description on scroll
- Particles flowing from one stage to the next
### Idea 2: "Arc Timeline as Process"
**Components**: Arc Timeline, Orbiting Circles, Warp Background, Animated List, Shine Border
**Vision**:
- Arc Timeline showing 5 steps of memory system
- Each node has Orbiting Circles showing components (hooks, worker, DB, MCP, context)
- Warp Background creating depth
- Animated List revealing sub-bullets under each stage
- Shine Border on active/hovered stage
### Idea 3: "Layered Depth Model" ⭐ WINNER
**Components**: Grid Beams, Magic Card, Border Beam, Morphing Text, Highlighter
**Vision**:
- Grid Beams background representing the "storage layer"
- Three floating Magic Cards in z-space (perspective/depth):
- Top layer: "Hooks capture"
- Middle layer: "AI compresses"
- Bottom layer: "Database stores"
- Arrows with Border Beam animation showing data flow downward
- Morphing Text showing state transformation: "Capturing" → "Compressing" → "Storing" → "Retrieving"
- Highlighter emphasizing "Automatic. Zero effort. Always on."
---
## Section 5: WHAT GETS REMEMBERED (Feature List)
### Idea 1: "Checkbox Delight"
**Components**: Animated List, Text Animate, Highlighter, Confetti, Magic Card
**Vision**:
- Animated List of 6 checkmark items
- Each item appears with Text Animate (blurIn by character)
- Highlighter underlining key words: "Decisions", "Bugs Fixed", "Code Patterns", etc.
- Confetti burst when all items are visible (celebration of completeness)
- Magic Card container with subtle spotlight effect
### Idea 2: "Memory Bank Slots" ⭐ WINNER
**Components**: Bento Grid, Scratch To Reveal, Shine Border, Orbiting Circles, Pulsating Button
**Vision**:
- Bento Grid of 6 cards (one per memory type)
- Each card has Scratch To Reveal to discover what gets saved (gamification!)
- Shine Border appears around revealed cards
- Orbiting Circles showing example icons around each card type
- Pulsating indicator on most important memory types
### Idea 3: "Collector Animation"
**Components**: Magic Card, Border Beam, Flip Text, Aurora Text, Dot Pattern
**Vision**:
- Six Magic Cards arranged in grid
- Border Beam cascading through cards in sequence
- Each card flips (Flip Text) to reveal icon + description
- Aurora Text for section heading
- Dot Pattern background with subtle glow on active items
---
## Section 6: POWERFUL SEARCH
### Idea 1: "Live Search Demo" ⭐ WINNER
**Components**: Safari, Text Animate, Animated List, Highlighter, Border Beam, Morphing Text
**Vision**:
- Safari browser mockup showing search interface
- Text Animate typing out search queries with realistic timing:
- "Find all database migrations..."
- "What decisions about authentication..."
- Animated List revealing search results sequentially
- Highlighter emphasizing matched keywords in results
- Border Beam around result cards
- Morphing Text cycling search types: "Migrations" → "Decisions" → "Patterns" → "Bugs"
### Idea 2: "Search Radar"
**Components**: Orbiting Circles, Magic Card, Particles, Text Animate, Grid Beams
**Vision**:
- Orbiting Circles representing different search dimensions:
- Inner orbit: File search
- Middle orbit: Concept search
- Outer orbit: Type/date filters
- Central Magic Card with search query
- Particles radiating outward to show search happening
- Results appear with Text Animate (slideUp)
- Grid Beams background representing indexed database
### Idea 3: "Memory Retrieval Visualization"
**Components**: Warp Background, Aurora Text, Magic Card, Shine Border, Dock, Confetti
**Vision**:
- Warp Background creating depth into "memory storage"
- Search query in Aurora Text
- Seven Magic Cards (one per search tool) with Shine Border
- Dock component at bottom showing 7 search tool icons
- Confetti when "perfect match" is found
---
## Section 7: THE NUMBERS (Metrics Table)
### Idea 1: "Counting Animation"
**Components**: Safari, Text Animate, Aurora Text, Confetti, Border Beam
**Vision**:
- Safari mockup showing comparison table
- Text Animate for each metric value counting up
- "Before" values in faded text
- "After" values with Aurora Text effect (glowing)
- Confetti when hovering over dramatic improvements
- Border Beam highlighting entire "After" column
### Idea 2: "Progress Bar Transformation" ⭐ WINNER
**Components**: Magic Card, Morphing Text, Shine Border, Pulsating Button
**Vision**:
- Five Magic Cards, one per metric
- Animated progress bars showing transformation:
- Before (low/red) → After (high/green)
- Visual representation of improvement
- Morphing Text cycling through metrics
- Shine Border on cards with biggest improvements
- Pulsating Button: "See the Difference"
### Idea 3: "Flip Cards Reveal"
**Components**: Scratch To Reveal, Magic Card, Highlighter, Animated List
**Vision**:
- Five cards with Scratch To Reveal
- Scratch "Before" to reveal "After" metrics
- Each card has Magic Card spotlight effect
- Highlighter emphasizing: "Never", "0 seconds", "Rare"
- Animated List showing additional benefits below table
---
## Section 8: INSTALLATION (Quick Start)
### Idea 1: "Copy-Paste Delight" ⭐ WINNER
**Components**: Safari, Text Animate, Shiny Button, Confetti, Animated List, Highlighter
**Vision**:
- Safari mockup showing terminal window
- Text Animate typing out commands with realistic timing (typewriter effect)
- Shiny Button next to each command for copy
- Confetti celebration when installation completes
- Animated List showing 3 steps with checkmarks appearing
- Highlighter emphasizing "2 minutes"
### Idea 2: "Progress Stepper"
**Components**: Arc Timeline, Border Beam, Magic Card, Pulsating Button, Word Rotate
**Vision**:
- Arc Timeline with 3 nodes: Clone → Add → Install
- Each step expands to show code when active
- Border Beam connecting steps as they complete
- Magic Card for code blocks with spotlight
- Pulsating Button for "Get Started"
- Word Rotate: "Simple" → "Fast" → "Easy" → "Done"
### Idea 3: "Interactive Terminal"
**Components**: Grid Beams, Safari, Text Animate, Orbiting Circles, Rainbow Button, Shine Border
**Vision**:
- Grid Beams background (tech aesthetic)
- Safari terminal mockup
- Text Animate simulating command execution
- Orbiting Circles showing installed components appearing
- Rainbow Button for final "Installation Complete" CTA
- Shine Border around success message
---
## Section 9: UNDER THE HOOD (Architecture)
### Idea 1: "System Diagram"
**Components**: Bento Grid, Magic Card, Orbiting Circles, Border Beam, Dot Pattern, Text Animate, Highlighter
**Vision**:
- Bento Grid showing 5 architecture components:
- Hooks, Worker, Database, MCP, Context Injection
- Each grid cell is a Magic Card with spotlight
- Orbiting Circles showing data flow between components
- Border Beam animating along connections
- Dot Pattern background
- Text Animate for each component description
- Highlighter on "Zero maintenance. Just works."
### Idea 2: "Layered Stack" ⭐ WINNER
**Components**: Magic Card, Warp Background, Morphing Text, Particles, Shine Border
**Vision**:
- Five Magic Cards stacked with perspective (z-depth)
- Each layer slides out on scroll to reveal architecture:
1. Hooks (top)
2. Worker Service
3. SQLite Database (center)
4. MCP Server
5. Context Injection (bottom)
- Warp Background creating depth
- Morphing Text showing active layer: "Hooks" → "Worker" → "SQLite" → "MCP" → "Context"
- Particles flowing between layers
- Shine Border on active layer
### Idea 3: "Technical Orbits"
**Components**: Orbiting Circles, Grid Beams, Magic Card, Text Animate
**Vision**:
- Central "SQLite DB" icon
- Orbiting Circles representing different systems:
- Inner orbit: 5 hooks (SessionStart, UserPrompt, PostTool, Summary, SessionEnd)
- Middle orbit: Worker service (PM2 process)
- Outer orbit: MCP server (7 search tools)
- Farthest orbit: Context injection
- Grid Beams background
- Magic Card for each orbit explanation
- Text Animate (slideUp) for technical details
---
## Section 10: USE CASES (User Types)
### Idea 1: "User Journey Cards"
**Components**: Bento Grid, Magic Card, Avatar Circles, Animated List, Flip Text, Border Beam
**Vision**:
- Four Magic Cards in Bento Grid layout
- Each card has different gradient colors (gradientFrom/To)
- Avatar Circles showing user type icon
- Animated List of benefits per user type
- Flip Text for headings revealing user types
- Border Beam highlighting active/hovered card
### Idea 2: "Role Selector" ⭐ WINNER
**Components**: Dock, Magic Card, Text Animate, Highlighter, Confetti
**Vision**:
- Dock component with 4 icons representing user types:
- Solo Developer icon
- Team icon
- Learning/Student icon
- Large Refactor icon
- Click icon to expand that use case
- Magic Card expands with spotlight effect
- Text Animate revealing use case details
- Highlighter emphasizing key benefits
- Confetti celebration when selecting "your" use case (engagement!)
### Idea 3: "Story Scenarios"
**Components**: Arc Timeline, Safari, Orbiting Circles, Morphing Text, Shine Border, Animated List
**Vision**:
- Arc Timeline showing progression for each user type
- Safari mockup showing actual usage scenario
- Orbiting Circles around timeline nodes showing features being used
- Morphing Text cycling through user types
- Shine Border on selected use case
- Animated List showing specific benefits
---
## Section 11: FAQ
### Idea 1: "Expandable Cards"
**Components**: Magic Card, Text Animate, Border Beam, Highlighter, Dot Pattern, Animated List
**Vision**:
- Six Magic Cards with questions visible
- Click to expand with Text Animate (blurIn)
- Border Beam appears on expanded card
- Highlighter emphasizing key answer points
- Dot Pattern background
- Animated List for multi-point answers
### Idea 2: "Scratch to Answer" ⭐ WINNER
**Components**: Scratch To Reveal, Confetti, Magic Card, Morphing Text
**Vision**:
- Questions visible, answers hidden under scratch surface
- Scratch To Reveal to see answers (highly engaging!)
- Confetti on revealing particularly important answers (e.g., "Fully private")
- Magic Card container with spotlight
- Morphing Text cycling through common concerns: "Cost?" → "Speed?" → "Privacy?" → "Storage?"
### Idea 3: "Interactive Q&A"
**Components**: Bento Grid, Border Beam, Shine Border, Text Animate, Pulsating Button, Avatar Circles
**Vision**:
- Bento Grid of question cards
- Hover triggers Border Beam
- Click expands with Shine Border
- Text Animate typing out answers
- Pulsating Button for "More Questions?"
- Avatar Circles showing "5,000+ developers trust claude-mem"
---
## BONUS: Testimonials Section (Not in Original)
### Idea 1: "Social Proof Marquee"
**Components**: Marquee (3D), Magic Card, Avatar Circles, Highlighter, Shine Border
**Vision**:
- Marquee component in 3D mode scrolling developer testimonials
- Each testimonial in a Magic Card
- Avatar Circles showing total developer count
- Highlighter on impactful quote fragments
- Shine Border around featured testimonial
---
## Component Usage Summary
| Component | Times Used (Winners) | Primary Purpose |
|-----------|---------------------|-----------------|
| **Magic Card** | 9 | Spotlight effects, containers |
| **Text Animate** | 7 | Typing effects, reveals |
| **Border Beam** | 6 | Connections, highlights |
| **Highlighter** | 6 | Emphasis on key text |
| **Animated List** | 5 | Sequential reveals |
| **Confetti** | 5 | Celebrations, milestones |
| **Shine Border** | 5 | Premium emphasis |
| **Morphing Text** | 4 | Dynamic headlines |
| **Orbiting Circles** | 4 | Data flow, ecosystems |
| **Safari** | 4 | Browser mockups |
| **Scratch To Reveal** | 4 | Interactive discovery |
| **Pulsating Button** | 4 | CTAs |
| **Grid Beams** | 3 | Tech backgrounds |
| **Warp Background** | 3 | Depth, perspective |
| **Arc Timeline** | 2 | Progress, history |
| **Dock** | 1 | Navigation selector |
| **Aurora Text** | 1 | Premium headlines |
| **Bento Grid** | 1 | Layout organization |
## Design Principles
1. **Show, Don't Tell**: Use animations to demonstrate concepts (memory persistence, data flow)
2. **Interactive Discovery**: Scratch-to-reveal and interactive elements engage users
3. **Visual Metaphors**: Orbiting circles for data flow, layers for architecture
4. **Celebration**: Confetti at key moments creates joy
5. **Progressive Disclosure**: Animated lists and timelines reveal information naturally
6. **Spatial Depth**: Warp backgrounds and z-space create dimensional understanding
7. **Consistent Magic**: Reuse components (Magic Card, Border Beam) for cohesion
+389
View File
@@ -0,0 +1,389 @@
# Landing Page Ideas - Ranked Analysis
Ranking criteria (1-10 scale):
1. **Creativity**: How novel and unexpected is the approach?
2. **Storytelling**: How effectively does it communicate the value?
3. **Intuitive**: How easily will users understand it?
4. **Feasibility**: How practical is implementation?
5. **Delight**: How much joy does it create?
---
## HERO SECTION Rankings
### Idea 1: "Fading Memory" Effect ⭐ WINNER (42/50)
- **Creativity**: 9/10 - Scratch-to-reveal pain point is unexpected
- **Storytelling**: 9/10 - Morphing text and orbiting circles tell complete story
- **Intuitive**: 8/10 - Orbiting content makes memory concept clear
- **Feasibility**: 7/10 - Multiple complex components need coordination
- **Delight**: 9/10 - Interactive scratch creates engagement
**Why it wins**: The scratch-to-reveal pain point makes the problem visceral. Orbiting circles create immediate visual understanding of what gets remembered.
### Idea 2: "Memory Timeline" Hero (39/50)
- **Creativity**: 7/10
- **Storytelling**: 8/10
- **Intuitive**: 9/10
- **Feasibility**: 8/10
- **Delight**: 7/10
### Idea 3: "Brain Storage" Visualization (36/50)
- **Creativity**: 6/10
- **Storytelling**: 7/10
- **Intuitive**: 7/10
- **Feasibility**: 9/10
- **Delight**: 7/10
---
## BEFORE/AFTER COMPARISON Rankings
### Idea 1: "Split Screen Wipe" ⭐ WINNER (44/50)
- **Creativity**: 8/10 - Clean comparison approach
- **Storytelling**: 10/10 - Side-by-side is the clearest possible comparison
- **Intuitive**: 10/10 - Immediately obvious what's different
- **Feasibility**: 8/10 - Safari mockups are well-supported
- **Delight**: 8/10 - Fade vs persist is satisfying
**Why it wins**: Safari browser mockups provide immediate familiarity. The contrast between fading (Before) and persistent with Border Beam (After) is crystal clear storytelling.
### Idea 2: "Memory Decay Visualization" (40/50)
- **Creativity**: 9/10
- **Storytelling**: 9/10
- **Intuitive**: 8/10
- **Feasibility**: 7/10
- **Delight**: 7/10
### Idea 3: "Conversation Replay" (40/50)
- **Creativity**: 7/10
- **Storytelling**: 8/10
- **Intuitive**: 9/10
- **Feasibility**: 9/10
- **Delight**: 7/10
---
## REAL EXAMPLES Rankings
### Idea 2: "Timeline Story" ⭐ WINNER (44/50)
- **Creativity**: 9/10 - Arc timeline for session progression is novel
- **Storytelling**: 10/10 - Timeline naturally shows progression over time
- **Intuitive**: 9/10 - Timeline metaphor is universally understood
- **Feasibility**: 7/10 - Complex interaction between timeline and orbiting elements
- **Delight**: 9/10 - Orbiting context around nodes is magical
**Why it wins**: Arc Timeline naturally communicates the "across sessions" aspect. Orbiting circles showing related context (files, decisions, bugs) is brilliant visual storytelling.
### Idea 1: "Tabbed Experience" (41/50)
- **Creativity**: 7/10
- **Storytelling**: 8/10
- **Intuitive**: 9/10
- **Feasibility**: 9/10
- **Delight**: 8/10
### Idea 3: "Memory Card Flip" (35/50)
- **Creativity**: 8/10
- **Storytelling**: 7/10
- **Intuitive**: 6/10
- **Feasibility**: 6/10
- **Delight**: 8/10
---
## HOW IT WORKS Rankings
### Idea 3: "Layered Depth Model" ⭐ WINNER (43/50)
- **Creativity**: 9/10 - Spatial depth for understanding layers is clever
- **Storytelling**: 9/10 - Visual depth communicates layered architecture
- **Intuitive**: 10/10 - Layers immediately convey hierarchy and flow
- **Feasibility**: 6/10 - Z-space perspective requires careful implementation
- **Delight**: 9/10 - Morphing text showing transformation is satisfying
**Why it wins**: Depth/perspective creates intuitive understanding of the layered architecture. Morphing text showing state transformation ("Capturing" → "Storing") tells the story perfectly.
### Idea 1: "Animated Data Flow" (42/50)
- **Creativity**: 8/10
- **Storytelling**: 9/10
- **Intuitive**: 9/10
- **Feasibility**: 8/10
- **Delight**: 8/10
### Idea 2: "Arc Timeline as Process" (37/50)
- **Creativity**: 7/10
- **Storytelling**: 8/10
- **Intuitive**: 8/10
- **Feasibility**: 7/10
- **Delight**: 7/10
---
## WHAT GETS REMEMBERED Rankings
### Idea 2: "Memory Bank Slots" ⭐ WINNER (41/50)
- **Creativity**: 9/10 - Memory bank metaphor with scratch-to-reveal
- **Storytelling**: 8/10 - Discovery process tells the story
- **Intuitive**: 8/10 - Bank slot metaphor is clear
- **Feasibility**: 7/10 - Six scratch-to-reveal elements need optimization
- **Delight**: 9/10 - Gamification through scratching is highly engaging
**Why it wins**: Scratch-to-reveal gamification makes exploring features fun. Discovering what gets saved creates memorable engagement.
### Idea 3: "Collector Animation" (41/50 - tied)
- **Creativity**: 8/10
- **Storytelling**: 8/10
- **Intuitive**: 9/10
- **Feasibility**: 8/10
- **Delight**: 8/10
### Idea 1: "Checkbox Delight" (40/50)
- **Creativity**: 6/10
- **Storytelling**: 7/10
- **Intuitive**: 9/10
- **Feasibility**: 10/10
- **Delight**: 8/10
---
## POWERFUL SEARCH Rankings
### Idea 1: "Live Search Demo" ⭐ WINNER (44/50)
- **Creativity**: 7/10 - Straightforward but effective
- **Storytelling**: 10/10 - Actually showing search in action is perfect
- **Intuitive**: 10/10 - Real search demo is immediately clear
- **Feasibility**: 9/10 - Safari + Text Animate + Animated List is achievable
- **Delight**: 8/10 - Watching search happen is satisfying
**Why it wins**: Showing actual search with real queries and results is the most effective storytelling. Users immediately understand the capability.
### Idea 2: "Search Radar" (39/50)
- **Creativity**: 9/10
- **Storytelling**: 8/10
- **Intuitive**: 7/10
- **Feasibility**: 6/10
- **Delight**: 9/10
### Idea 3: "Memory Retrieval Visualization" (38/50)
- **Creativity**: 8/10
- **Storytelling**: 7/10
- **Intuitive**: 8/10
- **Feasibility**: 8/10
- **Delight**: 7/10
---
## THE NUMBERS Rankings
### Idea 2: "Progress Bar Transformation" ⭐ WINNER (45/50)
- **Creativity**: 8/10 - Progress bars are familiar but effective
- **Storytelling**: 10/10 - Visual transformation from bad to good is powerful
- **Intuitive**: 10/10 - Everyone understands progress bars
- **Feasibility**: 8/10 - Animated progress bars are well-supported
- **Delight**: 9/10 - Watching bars transform is satisfying
**Why it wins**: Visual transformation of progress bars (red/low → green/high) is incredibly effective storytelling. Everyone intuitively understands the improvement.
### Idea 1: "Counting Animation" (42/50)
- **Creativity**: 7/10
- **Storytelling**: 9/10
- **Intuitive**: 9/10
- **Feasibility**: 9/10
- **Delight**: 8/10
### Idea 3: "Flip Cards Reveal" (39/50)
- **Creativity**: 8/10
- **Storytelling**: 8/10
- **Intuitive**: 7/10
- **Feasibility**: 7/10
- **Delight**: 9/10
---
## INSTALLATION Rankings
### Idea 1: "Copy-Paste Delight" ⭐ WINNER (44/50)
- **Creativity**: 7/10 - Simple but right
- **Storytelling**: 8/10 - Shows exactly what to do
- **Intuitive**: 10/10 - Terminal + copy buttons is universal pattern
- **Feasibility**: 10/10 - Very achievable
- **Delight**: 9/10 - Confetti celebration seals the deal
**Why it wins**: Simplicity wins for installation instructions. Confetti celebration when done creates satisfying completion.
### Idea 2: "Progress Stepper" (42/50)
- **Creativity**: 8/10
- **Storytelling**: 9/10
- **Intuitive**: 9/10
- **Feasibility**: 8/10
- **Delight**: 8/10
### Idea 3: "Interactive Terminal" (41/50)
- **Creativity**: 9/10
- **Storytelling**: 8/10
- **Intuitive**: 8/10
- **Feasibility**: 7/10
- **Delight**: 9/10
---
## UNDER THE HOOD Rankings
### Idea 2: "Layered Stack" ⭐ WINNER (44/50)
- **Creativity**: 9/10 - Layers with z-depth is clever
- **Storytelling**: 9/10 - Stacking shows dependency hierarchy
- **Intuitive**: 10/10 - Stack metaphor is perfect for architecture
- **Feasibility**: 7/10 - Perspective effects require work
- **Delight**: 9/10 - Slides revealing layers is satisfying
**Why it wins**: Layered stack with perspective visually explains the architecture hierarchy perfectly. Each layer sliding out to reveal itself tells the dependency story.
### Idea 3: "Technical Orbits" (43/50)
- **Creativity**: 10/10
- **Storytelling**: 8/10
- **Intuitive**: 9/10
- **Feasibility**: 8/10
- **Delight**: 8/10
### Idea 1: "System Diagram" (40/50)
- **Creativity**: 7/10
- **Storytelling**: 8/10
- **Intuitive**: 9/10
- **Feasibility**: 9/10
- **Delight**: 7/10
---
## USE CASES Rankings
### Idea 2: "Role Selector" ⭐ WINNER (46/50)
- **Creativity**: 9/10 - Dock as role selector is novel
- **Storytelling**: 9/10 - Interactive selection tells personalized stories
- **Intuitive**: 10/10 - Dock interface is familiar and clear
- **Feasibility**: 8/10 - Dock + expanding cards is achievable
- **Delight**: 10/10 - Confetti for "your" use case is pure joy
**Why it wins**: Interactive Dock selector with confetti when you find "your" use case creates personal connection and delight. Highest delight score overall!
### Idea 3: "Story Scenarios" (41/50)
- **Creativity**: 8/10
- **Storytelling**: 10/10
- **Intuitive**: 8/10
- **Feasibility**: 7/10
- **Delight**: 8/10
### Idea 1: "User Journey Cards" (40/50)
- **Creativity**: 7/10
- **Storytelling**: 8/10
- **Intuitive**: 9/10
- **Feasibility**: 9/10
- **Delight**: 7/10
---
## FAQ Rankings
### Idea 2: "Scratch to Answer" ⭐ WINNER (42/50)
- **Creativity**: 9/10 - Making FAQ interactive is fresh
- **Storytelling**: 8/10 - Discovery process engages
- **Intuitive**: 8/10 - Scratch metaphor is understood
- **Feasibility**: 7/10 - Multiple scratch elements need optimization
- **Delight**: 10/10 - Scratching makes FAQs fun!
**Why it wins**: Scratch-to-reveal makes FAQs engaging instead of boring. Confetti on important answers (like "Fully private") creates moments of delight.
### Idea 3: "Interactive Q&A" (41/50)
- **Creativity**: 7/10
- **Storytelling**: 8/10
- **Intuitive**: 9/10
- **Feasibility**: 9/10
- **Delight**: 8/10
### Idea 1: "Expandable Cards" (40/50)
- **Creativity**: 6/10
- **Storytelling**: 7/10
- **Intuitive**: 10/10
- **Feasibility**: 10/10
- **Delight**: 7/10
---
## Overall Component Winners
### Most Effective Components
1. **Magic Card** (9 winning sections) - Versatile spotlight effects
2. **Text Animate** (7 sections) - Essential for reveals and typing effects
3. **Border Beam** (6 sections) - Perfect for connections and highlights
4. **Scratch To Reveal** (4 sections) - Highest delight factor
5. **Confetti** (5 sections) - Celebration moments
### Highest Delight Components
1. Scratch To Reveal - 10/10 in FAQ, 9/10 in Features
2. Confetti - Creates joy at key moments
3. Dock - 10/10 for use case selection
4. Orbiting Circles - 9/10 for data visualization
### Best Storytelling Components
1. Arc Timeline - Perfect for progression narratives
2. Safari - Immediately familiar, great for demos
3. Progress Bars - Universal understanding of improvement
4. Layered Stack - Architecture hierarchy storytelling
---
## Implementation Priority
### High Priority (Core Experience)
1. **Hero** - First impression is critical
2. **Before/After** - Core value proposition
3. **Installation** - Conversion point
4. **The Numbers** - Proof of value
### Medium Priority (Supporting Narrative)
1. **Real Examples** - Concrete use cases
2. **How It Works** - Understanding the system
3. **Powerful Search** - Feature highlight
### Lower Priority (Deep Dive)
1. **What Gets Remembered** - Feature details
2. **Under The Hood** - Technical deep dive
3. **Use Cases** - Audience segmentation
4. **FAQ** - Support content
---
## Technical Considerations
### Performance Concerns
- **Scratch To Reveal**: 4 instances across page - needs optimization
- **Orbiting Circles**: Multiple orbits can be CPU intensive
- **Particles**: Use sparingly, can impact performance
- **Z-space/Perspective**: May have cross-browser issues
### Accessibility Considerations
- **Scratch To Reveal**: Needs keyboard alternative
- **Confetti**: Should respect `prefers-reduced-motion`
- **Animations**: All should be pausable/stoppable
- **Color Contrast**: Aurora/gradient text needs testing
### Browser Compatibility
- **Safari Component**: Meta - using Safari to show Safari
- **Grid Beams/Warp**: Check WebGL support
- **Perspective transforms**: Test in Firefox, Safari
---
## Final Winning Lineup
1. **Hero**: Fading Memory (42/50)
2. **Before/After**: Split Screen Wipe (44/50)
3. **Real Examples**: Timeline Story (44/50)
4. **How It Works**: Layered Depth Model (43/50)
5. **What Gets Remembered**: Memory Bank Slots (41/50)
6. **Powerful Search**: Live Search Demo (44/50)
7. **The Numbers**: Progress Bar Transformation (45/50) ⭐ HIGHEST SCORE
8. **Installation**: Copy-Paste Delight (44/50)
9. **Under The Hood**: Layered Stack (44/50)
10. **Use Cases**: Role Selector (46/50) ⭐ HIGHEST DELIGHT
11. **FAQ**: Scratch to Answer (42/50)
**Average Score**: 43.5/50 (87%)
**Total Delight**: 94/110 (85%)
+11 -9
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import P from"better-sqlite3";import{join as a,dirname as U,basename as q}from"path";import{homedir as R}from"os";import{existsSync as z,mkdirSync as w}from"fs";import{fileURLToPath as X}from"url";function M(){return typeof __dirname<"u"?__dirname:U(X(import.meta.url))}var F=M(),p=process.env.CLAUDE_MEM_DATA_DIR||a(R(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||a(R(),".claude"),ee=a(p,"archives"),se=a(p,"logs"),te=a(p,"trash"),re=a(p,"backups"),ne=a(p,"settings.json"),I=a(p,"claude-mem.db"),oe=a(u,"settings.json"),ie=a(u,"commands"),ae=a(u,"CLAUDE.md");function O(o){w(o,{recursive:!0})}function L(){return a(F,"..","..")}var _=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(_||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=_[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),d=_[e].padEnd(5),c=s.padEnd(6),m="";r?.correlationId?m=`[${r.correlationId}] `:r?.sessionId&&(m=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let b="";if(r){let{sessionId:B,sdkSessionId:G,correlationId:$,...N}=r;Object.keys(N).length>0&&(b=` {${Object.entries(N).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let h=`[${i}] [${d}] [${c}] ${m}${t}${b}${E}`;e===3?console.error(h):console.log(h)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},v=new T;var l=class{db;constructor(){O(p),this.db=new P(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
import P from"better-sqlite3";import{join as c,dirname as U,basename as q}from"path";import{homedir as R}from"os";import{existsSync as z,mkdirSync as w}from"fs";import{fileURLToPath as X}from"url";function M(){return typeof __dirname<"u"?__dirname:U(X(import.meta.url))}var F=M(),p=process.env.CLAUDE_MEM_DATA_DIR||c(R(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||c(R(),".claude"),ee=c(p,"archives"),se=c(p,"logs"),te=c(p,"trash"),re=c(p,"backups"),ne=c(p,"settings.json"),I=c(p,"claude-mem.db"),oe=c(u,"settings.json"),ie=c(u,"commands"),ae=c(u,"CLAUDE.md");function O(o){w(o,{recursive:!0})}function L(){return c(F,"..","..")}var _=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(_||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=_[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=_[e].padEnd(5),d=s.padEnd(6),l="";r?.correlationId?l=`[${r.correlationId}] `:r?.sessionId&&(l=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let b="";if(r){let{sessionId:B,sdkSessionId:G,correlationId:$,...N}=r;Object.keys(N).length>0&&(b=` {${Object.entries(N).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let h=`[${i}] [${a}] [${d}] ${l}${t}${b}${E}`;e===3?console.error(h):console.log(h)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},v=new T;var m=class{db;constructor(){O(p),this.db=new P(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(c=>c.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE session_summaries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
@@ -185,7 +185,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT files_read, files_modified
FROM observations
WHERE sdk_session_id = ?
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let d=JSON.parse(i.files_read);Array.isArray(d)&&d.forEach(c=>r.add(c))}catch{}if(i.files_modified)try{let d=JSON.parse(i.files_modified);Array.isArray(d)&&d.forEach(c=>n.add(c))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
SELECT id, sdk_session_id, project, user_prompt
FROM sdk_sessions
WHERE id = ?
@@ -212,11 +212,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
INSERT INTO sdk_sessions
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),a=this.db.prepare(`
INSERT OR IGNORE INTO sdk_sessions
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
`).run(e,s,t,r.toISOString(),n);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL
@@ -252,4 +254,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE status = 'active'
`).run(e.toISOString(),s).changes}close(){this.db.close()}};import g from"path";import{existsSync as S}from"fs";import{spawn as H}from"child_process";var W=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),j=`http://127.0.0.1:${W}/health`;async function A(){try{return(await fetch(j,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await A())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=g.join(o,"plugin","scripts","worker-service.cjs");if(!S(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(o,"ecosystem.config.cjs"),t=g.join(o,"node_modules",".bin","pm2");if(!S(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!S(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=H(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await A())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function C(o){try{console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s}),await k()||console.error("[claude-mem cleanup] Worker not available - skipping HTTP cleanup");let r=new l,n=r.findActiveSDKSession(e);n||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),r.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:n.id,sdk_session_id:n.sdk_session_id,project:n.project,worker_port:n.worker_port});try{r.markSessionCompleted(n.id),console.error("[claude-mem cleanup] Session marked as completed in database")}catch(i){console.error("[claude-mem cleanup] Failed to mark session as completed:",i)}r.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(e){console.error("[claude-mem cleanup] Unexpected error in hook",{error:e.message,stack:e.stack,name:e.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}import{stdin as D}from"process";var f="";D.on("data",o=>f+=o);D.on("end",async()=>{try{let o=f.trim()?JSON.parse(f):void 0;await C(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s}),await k()||console.error("[claude-mem cleanup] Worker not available - skipping HTTP cleanup");let r=new m,n=r.findActiveSDKSession(e);n||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),r.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:n.id,sdk_session_id:n.sdk_session_id,project:n.project,worker_port:n.worker_port});try{r.markSessionCompleted(n.id),console.error("[claude-mem cleanup] Session marked as completed in database")}catch(i){console.error("[claude-mem cleanup] Failed to mark session as completed:",i)}r.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(e){console.error("[claude-mem cleanup] Unexpected error in hook",{error:e.message,stack:e.stack,name:e.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}import{stdin as D}from"process";var f="";D.on("data",o=>f+=o);D.on("end",async()=>{try{let o=f.trim()?JSON.parse(f):void 0;await C(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
+12 -10
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import q from"path";import W from"better-sqlite3";import{join as l,dirname as M,basename as V}from"path";import{homedir as v}from"os";import{existsSync as Z,mkdirSync as X}from"fs";import{fileURLToPath as F}from"url";function P(){return typeof __dirname<"u"?__dirname:M(F(import.meta.url))}var j=P(),E=process.env.CLAUDE_MEM_DATA_DIR||l(v(),".claude-mem"),b=process.env.CLAUDE_CONFIG_DIR||l(v(),".claude"),se=l(E,"archives"),te=l(E,"logs"),re=l(E,"trash"),ne=l(E,"backups"),ie=l(E,"settings.json"),A=l(E,"claude-mem.db"),oe=l(b,"settings.json"),ae=l(b,"commands"),de=l(b,"CLAUDE.md");function y(p){X(p,{recursive:!0})}function D(){return l(j,"..","..")}var S=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(S||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=S[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let r=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&r.command){let n=r.command.length>50?r.command.substring(0,50)+"...":r.command;return`${e}(${n})`}if(e==="Read"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${e}(${n})`}if(e==="Edit"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${e}(${n})`}if(e==="Write"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${e}(${n})`}return e}catch{return e}}log(e,t,r,n,o){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),s=S[e].padEnd(5),c=t.padEnd(6),T="";n?.correlationId?T=`[${n.correlationId}] `:n?.sessionId&&(T=`[session-${n.sessionId}] `);let a="";o!=null&&(this.level===0&&typeof o=="object"?a=`
`+JSON.stringify(o,null,2):a=" "+this.formatData(o));let m="";if(n){let{sessionId:_,sdkSessionId:f,correlationId:C,...h}=n;Object.keys(h).length>0&&(m=` {${Object.entries(h).map(([U,w])=>`${U}=${w}`).join(", ")}}`)}let u=`[${d}] [${s}] [${c}] ${T}${r}${m}${a}`;e===3?console.error(u):console.log(u)}debug(e,t,r,n){this.log(0,e,t,r,n)}info(e,t,r,n){this.log(1,e,t,r,n)}warn(e,t,r,n){this.log(2,e,t,r,n)}error(e,t,r,n){this.log(3,e,t,r,n)}dataIn(e,t,r,n){this.info(e,`\u2192 ${t}`,r,n)}dataOut(e,t,r,n){this.info(e,`\u2190 ${t}`,r,n)}success(e,t,r,n){this.info(e,`\u2713 ${t}`,r,n)}failure(e,t,r,n){this.error(e,`\u2717 ${t}`,r,n)}timing(e,t,r,n){this.info(e,`\u23F1 ${t}`,n,{duration:`${r}ms`})}},k=new N;var g=class{db;constructor(){y(E),this.db=new W(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
import q from"path";import W from"better-sqlite3";import{join as l,dirname as M,basename as V}from"path";import{homedir as v}from"os";import{existsSync as Z,mkdirSync as X}from"fs";import{fileURLToPath as F}from"url";function P(){return typeof __dirname<"u"?__dirname:M(F(import.meta.url))}var j=P(),E=process.env.CLAUDE_MEM_DATA_DIR||l(v(),".claude-mem"),b=process.env.CLAUDE_CONFIG_DIR||l(v(),".claude"),se=l(E,"archives"),te=l(E,"logs"),re=l(E,"trash"),ne=l(E,"backups"),ie=l(E,"settings.json"),A=l(E,"claude-mem.db"),oe=l(b,"settings.json"),ae=l(b,"commands"),de=l(b,"CLAUDE.md");function y(p){X(p,{recursive:!0})}function k(){return l(j,"..","..")}var S=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(S||{}),R=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=S[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let r=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&r.command){let n=r.command.length>50?r.command.substring(0,50)+"...":r.command;return`${e}(${n})`}if(e==="Read"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${e}(${n})`}if(e==="Edit"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${e}(${n})`}if(e==="Write"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${e}(${n})`}return e}catch{return e}}log(e,t,r,n,o){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),s=S[e].padEnd(5),c=t.padEnd(6),_="";n?.correlationId?_=`[${n.correlationId}] `:n?.sessionId&&(_=`[session-${n.sessionId}] `);let a="";o!=null&&(this.level===0&&typeof o=="object"?a=`
`+JSON.stringify(o,null,2):a=" "+this.formatData(o));let m="";if(n){let{sessionId:T,sdkSessionId:f,correlationId:C,...h}=n;Object.keys(h).length>0&&(m=` {${Object.entries(h).map(([U,w])=>`${U}=${w}`).join(", ")}}`)}let u=`[${d}] [${s}] [${c}] ${_}${r}${m}${a}`;e===3?console.error(u):console.log(u)}debug(e,t,r,n){this.log(0,e,t,r,n)}info(e,t,r,n){this.log(1,e,t,r,n)}warn(e,t,r,n){this.log(2,e,t,r,n)}error(e,t,r,n){this.log(3,e,t,r,n)}dataIn(e,t,r,n){this.info(e,`\u2192 ${t}`,r,n)}dataOut(e,t,r,n){this.info(e,`\u2190 ${t}`,r,n)}success(e,t,r,n){this.info(e,`\u2713 ${t}`,r,n)}failure(e,t,r,n){this.error(e,`\u2717 ${t}`,r,n)}timing(e,t,r,n){this.info(e,`\u23F1 ${t}`,n,{duration:`${r}ms`})}},D=new R;var g=class{db;constructor(){y(E),this.db=new W(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -212,15 +212,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||0}createSDKSession(e,t,r){let n=new Date,o=n.getTime();return this.db.prepare(`
INSERT INTO sdk_sessions
`).get(e)?.prompt_counter||0}createSDKSession(e,t,r){let n=new Date,o=n.getTime(),s=this.db.prepare(`
INSERT OR IGNORE INTO sdk_sessions
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,t,r,n.toISOString(),o).lastInsertRowid}updateSDKSessionId(e,t){return this.db.prepare(`
`).run(e,t,r,n.toISOString(),o);return s.lastInsertRowid===0||s.changes===0?this.db.prepare(`
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
`).get(e).id:s.lastInsertRowid}updateSDKSessionId(e,t){return this.db.prepare(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL
`).run(t,e).changes===0?(k.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
`).run(t,e).changes===0?(D.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
UPDATE sdk_sessions
SET worker_port = ?
WHERE id = ?
@@ -251,13 +253,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE status = 'active'
`).run(e.toISOString(),t).changes}close(){this.db.close()}};import R from"path";import{existsSync as I}from"fs";import{spawn as H}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),G=`http://127.0.0.1:${B}/health`;async function $(){try{return(await fetch(G,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function x(){try{if(await $())return!0;console.error("[claude-mem] Worker not responding, starting...");let p=D(),e=R.join(p,"plugin","scripts","worker-service.cjs");if(!I(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let t=R.join(p,"ecosystem.config.cjs"),r=R.join(p,"node_modules",".bin","pm2");if(!I(r))throw new Error(`PM2 binary not found at ${r}. This is a bundled dependency - try running: npm install`);if(!I(t))throw new Error(`PM2 ecosystem config not found at ${t}. Plugin installation may be corrupted.`);let n=H(r,["start",t],{detached:!0,stdio:"ignore",cwd:p});n.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),n.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(d=>setTimeout(d,500)),await $())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(p){return console.error(`[claude-mem] Failed to start worker: ${p.message}`),!1}}var i={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m"};function O(p,e=!1,t=!1){x();let r=p?.cwd??process.cwd(),n=r?q.basename(r):"unknown-project",o=new g;try{let d=o.getRecentSummariesWithSessionInfo(n,3);if(d.length===0)return e?`
`).run(e.toISOString(),t).changes}close(){this.db.close()}};import N from"path";import{existsSync as I}from"fs";import{spawn as H}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),G=`http://127.0.0.1:${B}/health`;async function $(){try{return(await fetch(G,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function x(){try{if(await $())return!0;console.error("[claude-mem] Worker not responding, starting...");let p=k(),e=N.join(p,"plugin","scripts","worker-service.cjs");if(!I(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let t=N.join(p,"ecosystem.config.cjs"),r=N.join(p,"node_modules",".bin","pm2");if(!I(r))throw new Error(`PM2 binary not found at ${r}. This is a bundled dependency - try running: npm install`);if(!I(t))throw new Error(`PM2 ecosystem config not found at ${t}. Plugin installation may be corrupted.`);let n=H(r,["start",t],{detached:!0,stdio:"ignore",cwd:p});n.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),n.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(d=>setTimeout(d,500)),await $())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(p){return console.error(`[claude-mem] Failed to start worker: ${p.message}`),!1}}var i={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m"};function O(p,e=!1,t=!1){x();let r=p?.cwd??process.cwd(),n=r?q.basename(r):"unknown-project",o=new g;try{let d=o.getRecentSummariesWithSessionInfo(n,3);if(d.length===0)return e?`
${i.bright}${i.cyan}\u{1F4DD} [${n}] recent context${i.reset}
${i.gray}${"\u2500".repeat(60)}${i.reset}
${i.dim}No previous summaries found for this project yet.${i.reset}
`:`# [${n}] recent context
No previous summaries found for this project yet.`;let s=[];if(t){if(e?(s.push(""),s.push(`${i.bright}${i.cyan}\u{1F4DD} [${n}] recent context${i.reset}`),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),s.push("")):(s.push(`# [${n}] recent context`),s.push("")),d.length>1){e?(s.push(`${i.bright}${i.dim}Previous Requests:${i.reset}`),s.push("")):(s.push("**Previous Requests:**"),s.push(""));for(let _=d.length-1;_>=1;_--){let f=d[_],h=new Date(f.created_at).toLocaleString();e?s.push(`${i.dim}\u2022 ${h}:${i.reset} ${f.request||"(no request)"}`):s.push(`- ${h}: ${f.request||"(no request)"}`)}e?(s.push(""),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),s.push("")):(s.push(""),s.push("---"),s.push(""))}let a=d[0];a.request&&(e?(s.push(`${i.bright}${i.yellow}Request:${i.reset} ${a.request}`),s.push("")):(s.push(`**Request:** ${a.request}`),s.push(""))),a.learned&&(e?(s.push(`${i.bright}${i.blue}Learned:${i.reset} ${a.learned}`),s.push("")):(s.push(`**Learned:** ${a.learned}`),s.push(""))),a.completed&&(e?(s.push(`${i.bright}${i.green}Completed:${i.reset} ${a.completed}`),s.push("")):(s.push(`**Completed:** ${a.completed}`),s.push(""))),a.next_steps&&(e?(s.push(`${i.bright}${i.magenta}Next Steps:${i.reset} ${a.next_steps}`),s.push("")):(s.push(`**Next Steps:** ${a.next_steps}`),s.push("")));let m=o.getFilesForSession(a.sdk_session_id);m.filesRead.length>0&&(e?s.push(`${i.dim}Files Read: ${m.filesRead.join(", ")}${i.reset}`):s.push(`**Files Read:** ${m.filesRead.join(", ")}`)),m.filesModified.length>0&&(e?s.push(`${i.dim}Files Modified: ${m.filesModified.join(", ")}${i.reset}`):s.push(`**Files Modified:** ${m.filesModified.join(", ")}`));let u=new Date(a.created_at).toLocaleString();return e?s.push(`${i.dim}Date: ${u}${i.reset}`):s.push(`**Date:** ${u}`),e&&(s.push(""),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`)),s.join(`
`)}e?(s.push(""),s.push(`${i.bright}${i.cyan}\u{1F4DD} [${n}] recent context${i.reset}`),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`)):(s.push(`# [${n}] recent context`),s.push(""));let c=null,T=!0;for(let a of d){c!==null&&a.sdk_session_id!==c?e?(s.push(""),s.push(`${i.dim}${"\u2500".repeat(23)} New Session ${"\u2500".repeat(24)}${i.reset}`),s.push("")):(s.push(""),s.push("--- New Session ---"),s.push("")):T?e&&s.push(""):e?(s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),s.push("")):(s.push("---"),s.push("")),T=!1,a.request&&(e?(s.push(`${i.bright}${i.yellow}Request:${i.reset} ${a.request}`),s.push("")):(s.push(`**Request:** ${a.request}`),s.push(""))),a.learned&&(e?(s.push(`${i.bright}${i.blue}Learned:${i.reset} ${a.learned}`),s.push("")):(s.push(`**Learned:** ${a.learned}`),s.push(""))),a.completed&&(e?(s.push(`${i.bright}${i.green}Completed:${i.reset} ${a.completed}`),s.push("")):(s.push(`**Completed:** ${a.completed}`),s.push(""))),a.next_steps&&(e?(s.push(`${i.bright}${i.magenta}Next Steps:${i.reset} ${a.next_steps}`),s.push("")):(s.push(`**Next Steps:** ${a.next_steps}`),s.push("")));let u=o.getFilesForSession(a.sdk_session_id);u.filesRead.length>0&&(e?s.push(`${i.dim}Files Read: ${u.filesRead.join(", ")}${i.reset}`):s.push(`**Files Read:** ${u.filesRead.join(", ")}`)),u.filesModified.length>0&&(e?s.push(`${i.dim}Files Modified: ${u.filesModified.join(", ")}${i.reset}`):s.push(`**Files Modified:** ${u.filesModified.join(", ")}`));let _=new Date(a.created_at).toLocaleString();e?s.push(`${i.dim}Date: ${_}${i.reset}`):s.push(`**Date:** ${_}`),e||s.push(""),c=a.sdk_session_id}return e&&(s.push(""),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`)),s.join(`
No previous summaries found for this project yet.`;let s=[];if(t){if(e?(s.push(""),s.push(`${i.bright}${i.cyan}\u{1F4DD} [${n}] recent context${i.reset}`),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),s.push("")):(s.push(`# [${n}] recent context`),s.push("")),d.length>1){e?(s.push(`${i.bright}${i.dim}Previous Requests:${i.reset}`),s.push("")):(s.push("**Previous Requests:**"),s.push(""));for(let T=d.length-1;T>=1;T--){let f=d[T],h=new Date(f.created_at).toLocaleString();e?s.push(`${i.dim}\u2022 ${h}:${i.reset} ${f.request||"(no request)"}`):s.push(`- ${h}: ${f.request||"(no request)"}`)}e?(s.push(""),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),s.push("")):(s.push(""),s.push("---"),s.push(""))}let a=d[0];a.request&&(e?(s.push(`${i.bright}${i.yellow}Request:${i.reset} ${a.request}`),s.push("")):(s.push(`**Request:** ${a.request}`),s.push(""))),a.learned&&(e?(s.push(`${i.bright}${i.blue}Learned:${i.reset} ${a.learned}`),s.push("")):(s.push(`**Learned:** ${a.learned}`),s.push(""))),a.completed&&(e?(s.push(`${i.bright}${i.green}Completed:${i.reset} ${a.completed}`),s.push("")):(s.push(`**Completed:** ${a.completed}`),s.push(""))),a.next_steps&&(e?(s.push(`${i.bright}${i.magenta}Next Steps:${i.reset} ${a.next_steps}`),s.push("")):(s.push(`**Next Steps:** ${a.next_steps}`),s.push("")));let m=o.getFilesForSession(a.sdk_session_id);m.filesRead.length>0&&(e?s.push(`${i.dim}Files Read: ${m.filesRead.join(", ")}${i.reset}`):s.push(`**Files Read:** ${m.filesRead.join(", ")}`)),m.filesModified.length>0&&(e?s.push(`${i.dim}Files Modified: ${m.filesModified.join(", ")}${i.reset}`):s.push(`**Files Modified:** ${m.filesModified.join(", ")}`));let u=new Date(a.created_at).toLocaleString();return e?s.push(`${i.dim}Date: ${u}${i.reset}`):s.push(`**Date:** ${u}`),e&&(s.push(""),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`)),s.join(`
`)}e?(s.push(""),s.push(`${i.bright}${i.cyan}\u{1F4DD} [${n}] recent context${i.reset}`),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`)):(s.push(`# [${n}] recent context`),s.push(""));let c=null,_=!0;for(let a of d){c!==null&&a.sdk_session_id!==c?e?(s.push(""),s.push(`${i.dim}${"\u2500".repeat(23)} New Session ${"\u2500".repeat(24)}${i.reset}`),s.push("")):(s.push(""),s.push("--- New Session ---"),s.push("")):_?e&&s.push(""):e?(s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),s.push("")):(s.push("---"),s.push("")),_=!1,a.request&&(e?(s.push(`${i.bright}${i.yellow}Request:${i.reset} ${a.request}`),s.push("")):(s.push(`**Request:** ${a.request}`),s.push(""))),a.learned&&(e?(s.push(`${i.bright}${i.blue}Learned:${i.reset} ${a.learned}`),s.push("")):(s.push(`**Learned:** ${a.learned}`),s.push(""))),a.completed&&(e?(s.push(`${i.bright}${i.green}Completed:${i.reset} ${a.completed}`),s.push("")):(s.push(`**Completed:** ${a.completed}`),s.push(""))),a.next_steps&&(e?(s.push(`${i.bright}${i.magenta}Next Steps:${i.reset} ${a.next_steps}`),s.push("")):(s.push(`**Next Steps:** ${a.next_steps}`),s.push("")));let u=o.getFilesForSession(a.sdk_session_id);u.filesRead.length>0&&(e?s.push(`${i.dim}Files Read: ${u.filesRead.join(", ")}${i.reset}`):s.push(`**Files Read:** ${u.filesRead.join(", ")}`)),u.filesModified.length>0&&(e?s.push(`${i.dim}Files Modified: ${u.filesModified.join(", ")}${i.reset}`):s.push(`**Files Modified:** ${u.filesModified.join(", ")}`));let T=new Date(a.created_at).toLocaleString();e?s.push(`${i.dim}Date: ${T}${i.reset}`):s.push(`**Date:** ${T}`),e||s.push(""),c=a.sdk_session_id}return e&&(s.push(""),s.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`)),s.join(`
`)}finally{o.close()}}import{stdin as L}from"process";try{let p=process.argv.includes("--index");if(L.isTTY){let e=O(void 0,!0,p);console.log(e),process.exit(0)}else{let e="";L.on("data",t=>e+=t),L.on("end",()=>{let t=e.trim()?JSON.parse(e):void 0,n={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:O(t,!1,p)}};console.log(JSON.stringify(n)),process.exit(0)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)}
+11 -9
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import K from"path";import $ from"better-sqlite3";import{join as p,dirname as M,basename as z}from"path";import{homedir as h}from"os";import{existsSync as te,mkdirSync as P}from"fs";import{fileURLToPath as F}from"url";function H(){return typeof __dirname<"u"?__dirname:M(F(import.meta.url))}var W=H(),m=process.env.CLAUDE_MEM_DATA_DIR||p(h(),".claude-mem"),T=process.env.CLAUDE_CONFIG_DIR||p(h(),".claude"),ne=p(m,"archives"),oe=p(m,"logs"),ie=p(m,"trash"),ae=p(m,"backups"),de=p(m,"settings.json"),O=p(m,"claude-mem.db"),pe=p(T,"settings.json"),ce=p(T,"commands"),ue=p(T,"CLAUDE.md");function I(o){P(o,{recursive:!0})}function L(){return p(W,"..","..")}var g=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(g||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=g[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),d=g[e].padEnd(5),a=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let l="";n!=null&&(this.level===0&&typeof n=="object"?l=`
`+JSON.stringify(n,null,2):l=" "+this.formatData(n));let c="";if(r){let{sessionId:Y,sdkSessionId:q,correlationId:V,...R}=r;Object.keys(R).length>0&&(c=` {${Object.entries(R).map(([w,X])=>`${w}=${X}`).join(", ")}}`)}let u=`[${i}] [${d}] [${a}] ${E}${t}${c}${l}`;e===3?console.error(u):console.log(u)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new S;var _=class{db;constructor(){I(m),this.db=new $(O),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
import Y from"path";import $ from"better-sqlite3";import{join as p,dirname as M,basename as z}from"path";import{homedir as N}from"os";import{existsSync as te,mkdirSync as P}from"fs";import{fileURLToPath as F}from"url";function H(){return typeof __dirname<"u"?__dirname:M(F(import.meta.url))}var W=H(),c=process.env.CLAUDE_MEM_DATA_DIR||p(N(),".claude-mem"),_=process.env.CLAUDE_CONFIG_DIR||p(N(),".claude"),ne=p(c,"archives"),oe=p(c,"logs"),ie=p(c,"trash"),ae=p(c,"backups"),de=p(c,"settings.json"),O=p(c,"claude-mem.db"),pe=p(_,"settings.json"),ce=p(_,"commands"),ue=p(_,"CLAUDE.md");function I(o){P(o,{recursive:!0})}function L(){return p(W,"..","..")}var T=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(T||{}),g=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),d=s.padEnd(6),m="";r?.correlationId?m=`[${r.correlationId}] `:r?.sessionId&&(m=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let E="";if(r){let{sessionId:K,sdkSessionId:q,correlationId:V,...h}=r;Object.keys(h).length>0&&(E=` {${Object.entries(h).map(([w,X])=>`${w}=${X}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${m}${t}${E}${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`})}},A=new g;var l=class{db;constructor(){I(c),this.db=new $(O),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(a=>a.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(a=>a.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(a=>a.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE session_summaries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
@@ -185,7 +185,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT files_read, files_modified
FROM observations
WHERE sdk_session_id = ?
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let d=JSON.parse(i.files_read);Array.isArray(d)&&d.forEach(a=>r.add(a))}catch{}if(i.files_modified)try{let d=JSON.parse(i.files_modified);Array.isArray(d)&&d.forEach(a=>n.add(a))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
SELECT id, sdk_session_id, project, user_prompt
FROM sdk_sessions
WHERE id = ?
@@ -212,11 +212,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
INSERT INTO sdk_sessions
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),a=this.db.prepare(`
INSERT OR IGNORE INTO sdk_sessions
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
`).run(e,s,t,r.toISOString(),n);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL
@@ -251,4 +253,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE status = 'active'
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function j(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(o,e,s={}){let t=j(o,e,s);return JSON.stringify(t)}import f from"path";import{existsSync as b}from"fs";import{spawn as B}from"child_process";var C=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),G=`http://127.0.0.1:${C}/health`;async function k(){try{return(await fetch(G,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=f.join(o,"plugin","scripts","worker-service.cjs");if(!b(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=f.join(o,"ecosystem.config.cjs"),t=f.join(o,"node_modules",".bin","pm2");if(!b(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!b(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=B(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await k())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}function y(){return C}async function x(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=K.basename(s);if(!await D())throw new Error("Worker service failed to start or become healthy");let i=new _;try{let d=i.findActiveSDKSession(e),a,E=!1;if(d){a=d.id;let c=i.incrementPromptCounter(a);console.error(`[new-hook] Continuing session ${a}, prompt #${c}`)}else{let c=i.findAnySDKSession(e);if(c){a=c.id,i.reactivateSession(a,t);let u=i.incrementPromptCounter(a);E=!0,console.error(`[new-hook] Reactivated session ${a}, prompt #${u}`)}else{a=i.createSDKSession(e,r,t);let u=i.incrementPromptCounter(a);E=!0,console.error(`[new-hook] Created new session ${a}, prompt #${u}`)}}let l=y();if(E){let c=await fetch(`http://127.0.0.1:${l}/sessions/${a}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let u=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${u}`)}}console.log(v("UserPromptSubmit",!0))}finally{i.close()}}import{stdin as U}from"process";var N="";U.on("data",o=>N+=o);U.on("end",async()=>{let o=N.trim()?JSON.parse(N):void 0;await x(o),process.exit(0)});
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function j(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(o,e,s={}){let t=j(o,e,s);return JSON.stringify(t)}import S from"path";import{existsSync as f}from"fs";import{spawn as B}from"child_process";var C=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),G=`http://127.0.0.1:${C}/health`;async function k(){try{return(await fetch(G,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=S.join(o,"plugin","scripts","worker-service.cjs");if(!f(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=S.join(o,"ecosystem.config.cjs"),t=S.join(o,"node_modules",".bin","pm2");if(!f(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!f(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=B(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await k())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}function y(){return C}async function x(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=Y.basename(s);if(!await D())throw new Error("Worker service failed to start or become healthy");let i=new l;try{let a=i.createSDKSession(e,r,t),d=i.incrementPromptCounter(a);console.error(`[new-hook] Session ${a}, prompt #${d}`);let m=y(),u=await fetch(`http://127.0.0.1:${m}/sessions/${a}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!u.ok){let E=await u.text();throw new Error(`Failed to initialize session: ${u.status} ${E}`)}console.log(v("UserPromptSubmit",!0))}finally{i.close()}}import{stdin as U}from"process";var b="";U.on("data",o=>b+=o);U.on("end",async()=>{let o=b.trim()?JSON.parse(b):void 0;await x(o),process.exit(0)});
+13 -11
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import H from"better-sqlite3";import{join as p,dirname as w,basename as Q}from"path";import{homedir as I}from"os";import{existsSync as se,mkdirSync as X}from"fs";import{fileURLToPath as M}from"url";function P(){return typeof __dirname<"u"?__dirname:w(M(import.meta.url))}var F=P(),u=process.env.CLAUDE_MEM_DATA_DIR||p(I(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||p(I(),".claude"),re=p(u,"archives"),oe=p(u,"logs"),ne=p(u,"trash"),ie=p(u,"backups"),ae=p(u,"settings.json"),L=p(u,"claude-mem.db"),de=p(g,"settings.json"),pe=p(g,"commands"),ce=p(g,"CLAUDE.md");function v(n){X(n,{recursive:!0})}function A(){return p(F,"..","..")}var f=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(f||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=f[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),i=f[e].padEnd(5),d=s.padEnd(6),l="";r?.correlationId?l=`[${r.correlationId}] `:r?.sessionId&&(l=`[session-${r.sessionId}] `);let c="";o!=null&&(this.level===0&&typeof o=="object"?c=`
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let m="";if(r){let{sessionId:K,sdkSessionId:Y,correlationId:q,...O}=r;Object.keys(O).length>0&&(m=` {${Object.entries(O).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let h=`[${a}] [${i}] [${d}] ${l}${t}${m}${c}`;e===3?console.error(h):console.log(h)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},E=new S;var _=class{db;constructor(){v(u),this.db=new H(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
import H from"better-sqlite3";import{join as p,dirname as w,basename as Q}from"path";import{homedir as I}from"os";import{existsSync as se,mkdirSync as X}from"fs";import{fileURLToPath as M}from"url";function P(){return typeof __dirname<"u"?__dirname:w(M(import.meta.url))}var F=P(),c=process.env.CLAUDE_MEM_DATA_DIR||p(I(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||p(I(),".claude"),re=p(c,"archives"),ne=p(c,"logs"),oe=p(c,"trash"),ie=p(c,"backups"),ae=p(c,"settings.json"),L=p(c,"claude-mem.db"),de=p(g,"settings.json"),pe=p(g,"commands"),ce=p(g,"CLAUDE.md");function v(o){X(o,{recursive:!0})}function A(){return p(F,"..","..")}var S=(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))(S||{}),f=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=S[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),i=S[e].padEnd(5),d=s.padEnd(6),m="";r?.correlationId?m=`[${r.correlationId}] `:r?.sessionId&&(m=`[session-${r.sessionId}] `);let l="";n!=null&&(this.level===0&&typeof n=="object"?l=`
`+JSON.stringify(n,null,2):l=" "+this.formatData(n));let u="";if(r){let{sessionId:K,sdkSessionId:Y,correlationId:q,...O}=r;Object.keys(O).length>0&&(u=` {${Object.entries(O).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let _=`[${a}] [${i}] [${d}] ${m}${t}${u}${l}`;e===3?console.error(_):console.log(_)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},E=new f;var T=class{db;constructor(){v(c),this.db=new H(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -185,7 +185,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT files_read, files_modified
FROM observations
WHERE sdk_session_id = ?
`).all(e),r=new Set,o=new Set;for(let a of t){if(a.files_read)try{let i=JSON.parse(a.files_read);Array.isArray(i)&&i.forEach(d=>r.add(d))}catch{}if(a.files_modified)try{let i=JSON.parse(a.files_modified);Array.isArray(i)&&i.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
`).all(e),r=new Set,n=new Set;for(let a of t){if(a.files_read)try{let i=JSON.parse(a.files_read);Array.isArray(i)&&i.forEach(d=>r.add(d))}catch{}if(a.files_modified)try{let i=JSON.parse(a.files_modified);Array.isArray(i)&&i.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
SELECT id, sdk_session_id, project, user_prompt
FROM sdk_sessions
WHERE id = ?
@@ -212,11 +212,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
INSERT INTO sdk_sessions
`).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, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
`).run(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
@@ -229,17 +231,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
FROM sdk_sessions
WHERE id = ?
LIMIT 1
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let o=new Date,a=o.getTime();this.db.prepare(`
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let n=new Date,a=n.getTime();this.db.prepare(`
INSERT INTO observations
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
files_read, files_modified, prompt_number, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),a)}storeSummary(e,s,t,r){let o=new Date,a=o.getTime();this.db.prepare(`
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),a)}storeSummary(e,s,t,r){let n=new Date,a=n.getTime();this.db.prepare(`
INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed,
next_steps, notes, prompt_number, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),a)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),a)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
@@ -251,4 +253,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE status = 'active'
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function W(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function T(n,e,s={}){let t=W(n,e,s);return JSON.stringify(t)}import b from"path";import{existsSync as N}from"fs";import{spawn as $}from"child_process";var j=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${j}/health`;async function k(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=A(),e=b.join(n,"plugin","scripts","worker-service.cjs");if(!N(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=b.join(n,"ecosystem.config.cjs"),t=b.join(n,"node_modules",".bin","pm2");if(!N(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!N(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=$(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(a=>setTimeout(a,500)),await k())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}var G=new Set(["ListMcpResourcesTool"]);async function y(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(G.has(s)){console.log(T("PostToolUse",!0));return}if(!await C())throw new Error("Worker service failed to start or become healthy");let a=new _,i=a.findActiveSDKSession(e);if(!i){a.close(),console.log(T("PostToolUse",!0));return}if(!i.worker_port)throw a.close(),E.error("HOOK","No worker port for session",{sessionId:i.id}),new Error("No worker port for session - session may not be properly initialized");let d=a.getPromptCounter(i.id);a.close();let l=E.formatTool(s,t);E.dataIn("HOOK",`PostToolUse: ${l}`,{sessionId:i.id,workerPort:i.worker_port});let c=await fetch(`http://127.0.0.1:${i.worker_port}/sessions/${i.id}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_output:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let m=await c.text();throw E.failure("HOOK","Failed to send observation",{sessionId:i.id,status:c.status},m),new Error(`Failed to send observation to worker: ${c.status} ${m}`)}E.debug("HOOK","Observation sent successfully",{sessionId:i.id,toolName:s}),console.log(T("PostToolUse",!0))}import{stdin as D}from"process";var R="";D.on("data",n=>R+=n);D.on("end",async()=>{let n=R.trim()?JSON.parse(R):void 0;await y(n),process.exit(0)});
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function W(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function b(o,e,s={}){let t=W(o,e,s);return JSON.stringify(t)}import R from"path";import{existsSync as N}from"fs";import{spawn as $}from"child_process";var j=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${j}/health`;async function k(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=A(),e=R.join(o,"plugin","scripts","worker-service.cjs");if(!N(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=R.join(o,"ecosystem.config.cjs"),t=R.join(o,"node_modules",".bin","pm2");if(!N(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!N(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=$(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(a=>setTimeout(a,500)),await k())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}var G=new Set(["ListMcpResourcesTool"]);async function D(o){if(!o)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=o;if(G.has(s)){console.log(b("PostToolUse",!0));return}if(!await C())throw new Error("Worker service failed to start or become healthy");let a=new T,i=a.createSDKSession(e,"",""),d=a.getPromptCounter(i);a.close();let m=E.formatTool(s,t),l=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);E.dataIn("HOOK",`PostToolUse: ${m}`,{sessionId:i,workerPort:l});let u=await fetch(`http://127.0.0.1:${l}/sessions/${i}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_output:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(!u.ok){let _=await u.text();throw E.failure("HOOK","Failed to send observation",{sessionId:i,status:u.status},_),new Error(`Failed to send observation to worker: ${u.status} ${_}`)}E.debug("HOOK","Observation sent successfully",{sessionId:i,toolName:s}),console.log(b("PostToolUse",!0))}import{stdin as y}from"process";var h="";y.on("data",o=>h+=o);y.on("end",async()=>{let o=h.trim()?JSON.parse(h):void 0;await D(o),process.exit(0)});
File diff suppressed because one or more lines are too long
+14 -12
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import H from"better-sqlite3";import{join as d,dirname as w,basename as J}from"path";import{homedir as I}from"os";import{existsSync as ee,mkdirSync as X}from"fs";import{fileURLToPath as M}from"url";function P(){return typeof __dirname<"u"?__dirname:w(M(import.meta.url))}var F=P(),c=process.env.CLAUDE_MEM_DATA_DIR||d(I(),".claude-mem"),_=process.env.CLAUDE_CONFIG_DIR||d(I(),".claude"),te=d(c,"archives"),re=d(c,"logs"),ne=d(c,"trash"),oe=d(c,"backups"),ie=d(c,"settings.json"),L=d(c,"claude-mem.db"),ae=d(_,"settings.json"),de=d(_,"commands"),pe=d(_,"CLAUDE.md");function A(o){X(o,{recursive:!0})}function v(){return d(F,"..","..")}var T=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(T||{}),g=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),p=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let l="";n!=null&&(this.level===0&&typeof n=="object"?l=`
`+JSON.stringify(n,null,2):l=" "+this.formatData(n));let N="";if(r){let{sessionId:G,sdkSessionId:K,correlationId:Y,...O}=r;Object.keys(O).length>0&&(N=` {${Object.entries(O).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let h=`[${i}] [${a}] [${p}] ${E}${t}${N}${l}`;e===3?console.error(h):console.log(h)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},u=new g;var m=class{db;constructor(){A(c),this.db=new H(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
import H from"better-sqlite3";import{join as p,dirname as w,basename as J}from"path";import{homedir as O}from"os";import{existsSync as ee,mkdirSync as X}from"fs";import{fileURLToPath as M}from"url";function P(){return typeof __dirname<"u"?__dirname:w(M(import.meta.url))}var F=P(),c=process.env.CLAUDE_MEM_DATA_DIR||p(O(),".claude-mem"),_=process.env.CLAUDE_CONFIG_DIR||p(O(),".claude"),te=p(c,"archives"),re=p(c,"logs"),ne=p(c,"trash"),oe=p(c,"backups"),ie=p(c,"settings.json"),I=p(c,"claude-mem.db"),ae=p(_,"settings.json"),de=p(_,"commands"),pe=p(_,"CLAUDE.md");function L(o){X(o,{recursive:!0})}function A(){return p(F,"..","..")}var T=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(T||{}),g=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),i=T[e].padEnd(5),d=s.padEnd(6),m="";r?.correlationId?m=`[${r.correlationId}] `:r?.sessionId&&(m=`[session-${r.sessionId}] `);let l="";n!=null&&(this.level===0&&typeof n=="object"?l=`
`+JSON.stringify(n,null,2):l=" "+this.formatData(n));let R="";if(r){let{sessionId:G,sdkSessionId:K,correlationId:Y,...h}=r;Object.keys(h).length>0&&(R=` {${Object.entries(h).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let N=`[${a}] [${i}] [${d}] ${m}${t}${R}${l}`;e===3?console.error(N):console.log(N)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},u=new g;var E=class{db;constructor(){L(c),this.db=new H(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(p=>p.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE session_summaries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
@@ -185,7 +185,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT files_read, files_modified
FROM observations
WHERE sdk_session_id = ?
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(p=>r.add(p))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(p=>n.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
`).all(e),r=new Set,n=new Set;for(let a of t){if(a.files_read)try{let i=JSON.parse(a.files_read);Array.isArray(i)&&i.forEach(d=>r.add(d))}catch{}if(a.files_modified)try{let i=JSON.parse(a.files_modified);Array.isArray(i)&&i.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
SELECT id, sdk_session_id, project, user_prompt
FROM sdk_sessions
WHERE id = ?
@@ -212,11 +212,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
INSERT INTO sdk_sessions
`).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, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
`).run(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
@@ -229,17 +231,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
FROM sdk_sessions
WHERE id = ?
LIMIT 1
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let n=new Date,a=n.getTime();this.db.prepare(`
INSERT INTO observations
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
files_read, files_modified, prompt_number, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),i)}storeSummary(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),a)}storeSummary(e,s,t,r){let n=new Date,a=n.getTime();this.db.prepare(`
INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed,
next_steps, notes, prompt_number, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),a)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
@@ -251,4 +253,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE status = 'active'
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function W(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function S(o,e,s={}){let t=W(o,e,s);return JSON.stringify(t)}import f from"path";import{existsSync as b}from"fs";import{spawn as j}from"child_process";var $=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${$}/health`;async function k(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=v(),e=f.join(o,"plugin","scripts","worker-service.cjs");if(!b(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=f.join(o,"ecosystem.config.cjs"),t=f.join(o,"node_modules",".bin","pm2");if(!b(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!b(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=j(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await k())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function y(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o;if(!await C())throw new Error("Worker service failed to start or become healthy");let t=new m,r=t.findActiveSDKSession(e);if(!r){t.close(),console.log(S("Stop",!0));return}if(!r.worker_port)throw t.close(),u.error("HOOK","No worker port for session",{sessionId:r.id}),new Error("No worker port for session - session may not be properly initialized");let n=t.getPromptCounter(r.id);t.close(),u.dataIn("HOOK","Stop: Requesting summary",{sessionId:r.id,workerPort:r.worker_port,promptNumber:n});let i=await fetch(`http://127.0.0.1:${r.worker_port}/sessions/${r.id}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:n}),signal:AbortSignal.timeout(2e3)});if(!i.ok){let a=await i.text();throw u.failure("HOOK","Failed to generate summary",{sessionId:r.id,status:i.status},a),new Error(`Failed to request summary from worker: ${i.status} ${a}`)}u.debug("HOOK","Summary request sent successfully",{sessionId:r.id}),console.log(S("Stop",!0))}import{stdin as D}from"process";var R="";D.on("data",o=>R+=o);D.on("end",async()=>{let o=R.trim()?JSON.parse(R):void 0;await y(o),process.exit(0)});
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function W(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(o,e,s={}){let t=W(o,e,s);return JSON.stringify(t)}import S from"path";import{existsSync as f}from"fs";import{spawn as j}from"child_process";var $=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${$}/health`;async function k(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=A(),e=S.join(o,"plugin","scripts","worker-service.cjs");if(!f(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=S.join(o,"ecosystem.config.cjs"),t=S.join(o,"node_modules",".bin","pm2");if(!f(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!f(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=j(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(a=>setTimeout(a,500)),await k())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function y(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o;if(!await C())throw new Error("Worker service failed to start or become healthy");let t=new E,r=t.createSDKSession(e,"",""),n=t.getPromptCounter(r);t.close();let a=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);u.dataIn("HOOK","Stop: Requesting summary",{sessionId:r,workerPort:a,promptNumber:n});let i=await fetch(`http://127.0.0.1:${a}/sessions/${r}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:n}),signal:AbortSignal.timeout(2e3)});if(!i.ok){let d=await i.text();throw u.failure("HOOK","Failed to generate summary",{sessionId:r,status:i.status},d),new Error(`Failed to request summary from worker: ${i.status} ${d}`)}u.debug("HOOK","Summary request sent successfully",{sessionId:r}),console.log(v("Stop",!0))}import{stdin as D}from"process";var b="";D.on("data",o=>b+=o);D.on("end",async()=>{let o=b.trim()?JSON.parse(b):void 0;await y(o),process.exit(0)});
File diff suppressed because one or more lines are too long
+14 -43
View File
@@ -31,54 +31,25 @@ export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
const db = new SessionStore();
try {
// Check for any existing session (active, failed, or completed)
let existing = db.findActiveSDKSession(session_id);
let sessionDbId: number;
let isNewSession = false;
if (existing) {
// Session already active, increment prompt counter
sessionDbId = existing.id;
const promptNumber = db.incrementPromptCounter(sessionDbId);
console.error(`[new-hook] Continuing session ${sessionDbId}, prompt #${promptNumber}`);
} else {
// Check for inactive sessions we can reuse
const inactive = db.findAnySDKSession(session_id);
if (inactive) {
// Reactivate the existing session
sessionDbId = inactive.id;
db.reactivateSession(sessionDbId, prompt);
const promptNumber = db.incrementPromptCounter(sessionDbId);
isNewSession = true;
console.error(`[new-hook] Reactivated session ${sessionDbId}, prompt #${promptNumber}`);
} else {
// Create new session
sessionDbId = db.createSDKSession(session_id, project, prompt);
const promptNumber = db.incrementPromptCounter(sessionDbId);
isNewSession = true;
console.error(`[new-hook] Created new session ${sessionDbId}, prompt #${promptNumber}`);
}
}
// Just save session_id for indexing - no validation, no state management
const sessionDbId = db.createSDKSession(session_id, project, prompt);
const promptNumber = db.incrementPromptCounter(sessionDbId);
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
// Get fixed port
const port = getWorkerPort();
// Only initialize worker on new sessions
if (isNewSession) {
// Initialize session via HTTP
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project, userPrompt: prompt }),
signal: AbortSignal.timeout(5000)
});
// Initialize session via HTTP
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project, userPrompt: prompt }),
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to initialize session: ${response.status} ${errorText}`);
}
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to initialize session: ${response.status} ${errorText}`);
}
console.log(createHookResponse('UserPromptSubmit', true));
+11 -20
View File
@@ -40,32 +40,23 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
}
const db = new SessionStore();
const session = db.findActiveSDKSession(session_id);
if (!session) {
db.close();
console.log(createHookResponse('PostToolUse', true));
return;
}
if (!session.worker_port) {
db.close();
logger.error('HOOK', 'No worker port for session', { sessionId: session.id });
throw new Error('No worker port for session - session may not be properly initialized');
}
// Get current prompt number for this session
const promptNumber = db.getPromptCounter(session.id);
// Get or create session - no validation, just use the session_id from hook
const sessionDbId = db.createSDKSession(session_id, '', ''); // project and prompt not needed for observations
const promptNumber = db.getPromptCounter(sessionDbId);
db.close();
const toolStr = logger.formatTool(tool_name, tool_input);
// Use fixed worker port - no session.worker_port validation needed
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
sessionId: session.id,
workerPort: session.worker_port
sessionId: sessionDbId,
workerPort: FIXED_PORT
});
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/observations`, {
const response = await fetch(`http://127.0.0.1:${FIXED_PORT}/sessions/${sessionDbId}/observations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -80,12 +71,12 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
if (!response.ok) {
const errorText = await response.text();
logger.failure('HOOK', 'Failed to send observation', {
sessionId: session.id,
sessionId: sessionDbId,
status: response.status
}, errorText);
throw new Error(`Failed to send observation to worker: ${response.status} ${errorText}`);
}
logger.debug('HOOK', 'Observation sent successfully', { sessionId: session.id, toolName: tool_name });
logger.debug('HOOK', 'Observation sent successfully', { sessionId: sessionDbId, toolName: tool_name });
console.log(createHookResponse('PostToolUse', true));
}
+11 -20
View File
@@ -27,31 +27,22 @@ export async function summaryHook(input?: StopInput): Promise<void> {
}
const db = new SessionStore();
const session = db.findActiveSDKSession(session_id);
if (!session) {
db.close();
console.log(createHookResponse('Stop', true));
return;
}
if (!session.worker_port) {
db.close();
logger.error('HOOK', 'No worker port for session', { sessionId: session.id });
throw new Error('No worker port for session - session may not be properly initialized');
}
// Get current prompt number
const promptNumber = db.getPromptCounter(session.id);
// Get or create session - no validation, just use the session_id from hook
const sessionDbId = db.createSDKSession(session_id, '', '');
const promptNumber = db.getPromptCounter(sessionDbId);
db.close();
// Use fixed worker port - no session.worker_port validation needed
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
logger.dataIn('HOOK', 'Stop: Requesting summary', {
sessionId: session.id,
workerPort: session.worker_port,
sessionId: sessionDbId,
workerPort: FIXED_PORT,
promptNumber
});
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/summarize`, {
const response = await fetch(`http://127.0.0.1:${FIXED_PORT}/sessions/${sessionDbId}/summarize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt_number: promptNumber }),
@@ -61,12 +52,12 @@ export async function summaryHook(input?: StopInput): Promise<void> {
if (!response.ok) {
const errorText = await response.text();
logger.failure('HOOK', 'Failed to generate summary', {
sessionId: session.id,
sessionId: sessionDbId,
status: response.status
}, errorText);
throw new Error(`Failed to request summary from worker: ${response.status} ${errorText}`);
}
logger.debug('HOOK', 'Summary request sent successfully', { sessionId: session.id });
logger.debug('HOOK', 'Summary request sent successfully', { sessionId: sessionDbId });
console.log(createHookResponse('Stop', true));
}
+13 -2
View File
@@ -706,19 +706,30 @@ export class SessionStore {
}
/**
* Create a new SDK session
* Create a new SDK session (idempotent - returns existing session ID if already exists)
*/
createSDKSession(claudeSessionId: string, project: string, userPrompt: string): number {
const now = new Date();
const nowEpoch = now.getTime();
// Try to insert - will be ignored if session already exists
const stmt = this.db.prepare(`
INSERT INTO sdk_sessions
INSERT OR IGNORE INTO sdk_sessions
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`);
const result = stmt.run(claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch);
// If lastInsertRowid is 0, insert was ignored (session exists), so fetch existing ID
if (result.lastInsertRowid === 0 || result.changes === 0) {
const selectStmt = this.db.prepare(`
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
`);
const existing = selectStmt.get(claudeSessionId) as { id: number } | undefined;
return existing!.id;
}
return result.lastInsertRowid as number;
}
-5
View File
@@ -118,11 +118,6 @@ class WorkerService {
const correlationId = logger.sessionId(sessionDbId);
logger.info('WORKER', 'Session init', { correlationId, project });
if (this.sessions.has(sessionDbId)) {
res.status(409).json({ error: 'Session already exists' });
return;
}
// Create session state
const session: ActiveSession = {
sessionDbId,