Compare commits

...

7 Commits

Author SHA1 Message Date
Alex Newman 60d5f8fbf1 chore: Bump version to 6.0.3
Version bump for patch release.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 13:37:30 -05:00
Alex Newman c0778bef00 docs: Align search documentation with hybrid ChromaDB architecture (#116)
* feat: Add discovery_tokens for ROI tracking in observations and session summaries

- Introduced `discovery_tokens` column in `observations` and `session_summaries` tables to track token costs associated with discovering and creating each observation and summary.
- Updated relevant services and hooks to calculate and display ROI metrics based on discovery tokens.
- Enhanced context economics reporting to include savings from reusing previous observations.
- Implemented migration to ensure the new column is added to existing tables.
- Adjusted data models and sync processes to accommodate the new `discovery_tokens` field.

* refactor: streamline context hook by removing unused functions and updating terminology

- Removed the estimateTokens and getObservations helper functions as they were not utilized.
- Updated the legend and output messages to replace "discovery" with "work" for clarity.
- Changed the emoji representation for different observation types to better reflect their purpose.
- Enhanced output formatting for improved readability and understanding of token usage.

* Refactor user-message-hook and context-hook for improved clarity and functionality

- Updated user-message-hook.js to enhance error messaging and improve variable naming for clarity.
- Modified context-hook.ts to include a new column key section, improved context index instructions, and added emoji icons for observation types.
- Adjusted footer messages in context-hook.ts to emphasize token savings and access to past research.
- Changed user-message-hook.ts to update the feedback and support message for clarity.

* fix: Critical ROI tracking fixes from PR review

Addresses critical findings from PR #111 review:

1. **Fixed incorrect discovery token calculation** (src/services/worker/SDKAgent.ts)
   - Changed from passing cumulative total to per-response delta
   - Now correctly tracks token cost for each observation/summary
   - Captures token state before/after response processing
   - Prevents all observations getting inflated cumulative values

2. **Fixed schema version mismatch** (src/services/sqlite/SessionStore.ts)
   - Changed ensureDiscoveryTokensColumn() from version 11 to version 7
   - Now matches migration007 definition in migrations.ts
   - Ensures consistent version tracking across migration system

These fixes ensure ROI metrics accurately reflect token costs.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Update search documentation to reflect hybrid ChromaDB architecture

The backend correctly implements ChromaDB-first semantic search with SQLite
temporal ordering and FTS5 fallback, but documentation incorrectly described
it as "FTS5 full-text search". This fix aligns all skill guides and tool
descriptions with the actual implementation.

Changes:
- Update SKILL.md to describe hybrid architecture with ChromaDB primary
- Update observations.md title and query parameter descriptions
- Update all three search tool descriptions in search-server.ts:
  * search_observations
  * search_sessions
  * search_user_prompts

All tools now correctly document:
- ChromaDB semantic search (primary ranking)
- 90-day recency filter
- SQLite temporal ordering
- FTS5 fallback (when ChromaDB unavailable)

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Add discovery_tokens column to observations and session_summaries tables

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-16 13:36:17 -05:00
Alex Newman 3cbc041c8b feat: Add ROI tracking with discovery_tokens for observations and session summaries (#111)
* feat: Add discovery_tokens for ROI tracking in observations and session summaries

- Introduced `discovery_tokens` column in `observations` and `session_summaries` tables to track token costs associated with discovering and creating each observation and summary.
- Updated relevant services and hooks to calculate and display ROI metrics based on discovery tokens.
- Enhanced context economics reporting to include savings from reusing previous observations.
- Implemented migration to ensure the new column is added to existing tables.
- Adjusted data models and sync processes to accommodate the new `discovery_tokens` field.

* refactor: streamline context hook by removing unused functions and updating terminology

- Removed the estimateTokens and getObservations helper functions as they were not utilized.
- Updated the legend and output messages to replace "discovery" with "work" for clarity.
- Changed the emoji representation for different observation types to better reflect their purpose.
- Enhanced output formatting for improved readability and understanding of token usage.

* Refactor user-message-hook and context-hook for improved clarity and functionality

- Updated user-message-hook.js to enhance error messaging and improve variable naming for clarity.
- Modified context-hook.ts to include a new column key section, improved context index instructions, and added emoji icons for observation types.
- Adjusted footer messages in context-hook.ts to emphasize token savings and access to past research.
- Changed user-message-hook.ts to update the feedback and support message for clarity.

* fix: Critical ROI tracking fixes from PR review

Addresses critical findings from PR #111 review:

1. **Fixed incorrect discovery token calculation** (src/services/worker/SDKAgent.ts)
   - Changed from passing cumulative total to per-response delta
   - Now correctly tracks token cost for each observation/summary
   - Captures token state before/after response processing
   - Prevents all observations getting inflated cumulative values

2. **Fixed schema version mismatch** (src/services/sqlite/SessionStore.ts)
   - Changed ensureDiscoveryTokensColumn() from version 11 to version 7
   - Now matches migration007 definition in migrations.ts
   - Ensures consistent version tracking across migration system

These fixes ensure ROI metrics accurately reflect token costs.

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-15 19:34:53 -05:00
Copilot 0f96476987 Fix documentation links to point to docs.claude-mem.ai (#114)
* Initial plan

* Fix documentation links to point to docs.claude-mem.ai

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
2025-11-15 16:29:11 -05:00
Alex Newman cd6f883020 docs: update CHANGELOG.md for v6.0.2 2025-11-14 15:44:46 -05:00
Alex Newman 9fb7383ab3 chore: bump version to 6.0.2
Updated user message hook with Claude-Mem community discussion link.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 15:44:02 -05:00
Alex Newman e8e7fc81af docs: update CHANGELOG.md for v6.0.1 2025-11-14 15:06:54 -05:00
28 changed files with 1069 additions and 390 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [ "plugins": [
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "6.0.1", "version": "6.0.3",
"source": "./plugin", "source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions" "description": "Persistent memory system for Claude Code - context compression across sessions"
} }
+29
View File
@@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [6.0.2] - 2025-11-14
## Changes
- Updated user message hook with Claude-Mem community discussion link for better user engagement and support
## What's Changed
- Enhanced startup context messaging with community connection information
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v6.0.1...v6.0.2
## [6.0.1] - 2025-11-14
## UI Enhancements
### Changes
- Refined color theme with warmer tones for better visual hierarchy
- New observation card blue/teal theme with distinct light/dark mode values
- Added 8 SVG icon assets for summary card sections (thick and thin variants)
- Enhanced summary card component with icon support for completed, investigated, learned, and next-steps sections
- Updated build system to handle icon asset copying
### Visual Improvements
- Unified color palette refinements across all UI components
- Improved card type differentiation: gold/amber for summaries, purple for prompts, blue/teal for observations
- Better visual consistency in viewer UI
Full changelog: https://github.com/thedotmack/claude-mem/compare/v6.0.0...v6.0.1
## [6.0.0] - 2025-11-13 ## [6.0.0] - 2025-11-13
## What's New ## What's New
+1 -1
View File
@@ -6,7 +6,7 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
**Your Role**: You are working on the plugin itself. When users interact with Claude Code with this plugin installed, your observations get captured and become their persistent memory. **Your Role**: You are working on the plugin itself. When users interact with Claude Code with this plugin installed, your observations get captured and become their persistent memory.
**Current Version**: 6.0.1 **Current Version**: 6.0.3
## IMPORTANT: Skills Are Auto-Invoked ## IMPORTANT: Skills Are Auto-Invoked
+434
View File
@@ -0,0 +1,434 @@
# Hybrid Search Architecture: Problem-Solution Document
**Date:** 2025-01-15
**Author:** Claude Code (Session handoff document)
**Purpose:** Comprehensive fix guide for hybrid search architecture documentation and implementation
---
## Executive Summary
The claude-mem hybrid search architecture is **correctly implemented in code** but **incorrectly documented** in skill guides. Additionally, the workflow is missing the final "instant context timeline" step that completes the human memory analogy.
**Quick Status:**
- ✅ Backend code (`search-server.ts`): ChromaDB first, SQLite temporal sort
- ❌ Skill operation guides: Describe FTS5 as primary search method
- ❌ Missing feature: Automatic timeline context retrieval (before/after observations)
- ✅ Landing page: Recently corrected
- ⚠️ Documentation: Needs validation and potential refinement
---
## The Intended Architecture (User's Vision)
### Storage Flow
```
User Action
1. SQLite Insert (FAST, synchronous)
- Immediate persistence
- Available for querying instantly
2. ChromaDB Sync (BACKGROUND, asynchronous)
- Worker generates embeddings
- Takes time but doesn't block user
- Uses OpenAI text-embedding-3-small
```
**Why this design:**
- Users don't wait for embedding generation
- SQLite provides immediate access
- ChromaDB catches up in background for semantic search
### Search Flow (3-Layer Sequential Architecture)
```
User Query: "How did we implement authentication?"
LAYER 1: Semantic Retrieval (ChromaDB)
- Vector similarity search
- Returns observation IDs (not full records)
- Top 100 semantic matches
- 90-day recency filter applied
LAYER 2: Temporal Ordering (SQLite)
- Takes IDs from Layer 1
- Hydrates full records from SQLite
- Sorts by created_at_epoch DESC
- Returns NEWEST relevant observation
LAYER 3: Instant Context Timeline (SQLite) [MISSING IN CURRENT IMPLEMENTATION]
- Takes top observation ID from Layer 2
- Retrieves N observations BEFORE that point
- Retrieves N observations AFTER that point
- Provides temporal context: "what led here" + "what happened next"
Present to User
- Most relevant observation
- Timeline showing before/after context
- Mimics human memory
```
**Why ChromaDB can't do it alone:**
- ChromaDB doesn't efficiently support date range queries sorted by time
- SQLite excels at temporal operations (ORDER BY created_at_epoch)
- Need both: ChromaDB for semantic, SQLite for temporal
**Why the timeline matters:**
> LLMs don't experience time linearly like humans do. Humans remember: "I did X, which led to Y, then Z happened." The instant context timeline gives LLMs this temporal awareness that humans experience naturally.
### Fallback Behavior
```
IF ChromaDB unavailable OR no results:
FTS5 Keyword Search (SQLite)
- Full-text search on observations_fts
- Basic keyword matching
- Ensures backward compatibility
- Fallback for older systems
```
**FTS5 is NOT "optional"** - it's the fallback mechanism for when ChromaDB isn't available or returns no results.
---
## Current State Analysis
### ✅ What's Correct: Backend Implementation
**File:** `/Users/alexnewman/Scripts/claude-mem/src/servers/search-server.ts`
**Lines:** 360-396 (search_observations handler)
The code DOES implement Layers 1 & 2 correctly:
```typescript
// Step 1: ChromaDB semantic search (top 100)
if (chromaClient) {
const chromaResults = await queryChroma(query, 100);
// Step 2: Filter by 90-day recency
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
const recentIds = chromaResults.ids.filter((_id, idx) => {
const meta = chromaResults.metadatas[idx];
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
// Step 3: Hydrate from SQLite with temporal ordering
results = store.getObservationsByIds(recentIds, {
orderBy: 'date_desc',
limit
});
}
// Fallback to FTS5 if ChromaDB unavailable
if (results.length === 0) {
results = search.searchObservations(query, options); // FTS5
}
```
**What this gets right:**
- ChromaDB semantic search FIRST (not FTS5)
- 90-day recency filter
- SQLite temporal ordering (`orderBy: 'date_desc'`)
- FTS5 fallback for reliability
### ❌ What's Wrong: Skill Operation Guides
**File:** `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/observations.md`
**Current Title:** "Search Observations (Full-Text)"
**Current Description:** "Search all observations using natural language queries."
**Current Line 351:** `query: z.string().describe('Search query for FTS5 full-text search')`
**The Problem:**
- Describes FTS5 as the search method
- No mention of ChromaDB semantic search
- Misleading title "Full-Text" implies keyword-only
- Examples don't show the ChromaDB → SQLite flow
**Impact:**
- Claude thinks it's doing FTS5 keyword search
- Doesn't understand it's semantic vector search
- Can't explain the architecture to users correctly
### ⚠️ What's Missing: Layer 3 (Instant Context Timeline)
The current implementation stops at Layer 2 (temporal ordering). It doesn't automatically:
1. Identify the MOST relevant observation (it returns a sorted list)
2. Retrieve observations BEFORE that point in time
3. Retrieve observations AFTER that point in time
4. Present the timeline context to the user
**Why this matters:**
The timeline is the **killer feature** that mimics human memory. Without it, users get:
- ❌ A sorted list of relevant observations
- ❌ No context about what led there
- ❌ No context about what happened next
With timeline, users get:
- ✅ The MOST relevant observation
- ✅ Context: "You did A and B before this"
- ✅ Context: "After this, you did C and D"
- ✅ Complete narrative like human memory
### 📋 Documentation Status
**Recently Fixed (✅):**
- `/Users/alexnewman/Scripts/claude-mem/docs/context/mem-search-technical-architecture.md`
- Now describes 3-layer sequential flow
- Includes human memory analogy
- Positions ChromaDB as primary
**Landing Page (✅):**
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/Features.tsx`
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/QuickBenefits.tsx`
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/Architecture.tsx`
- All updated to describe ChromaDB-first architecture
- "Remember Like a Human" messaging added
- Timeline feature highlighted
**Needs Review:**
- SKILL.md technical notes (line 172)
- All operation guides in `/operations/` directory
- Common workflows documentation
---
## Required Fixes
### Fix 1: Update Skill Operation Guides
**Files to modify:**
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/observations.md`
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/common-workflows.md`
**Changes needed:**
1. **observations.md:**
- Change title: "Search Observations (Full-Text)" → "Search Observations (Semantic + Temporal)"
- Update description: Explain ChromaDB semantic search as primary
- Update command examples to explain hybrid flow
- Add note: "Uses ChromaDB vector search with SQLite temporal ordering. FTS5 used as fallback."
2. **common-workflows.md:**
- Update "Workflow 2: Finding Specific Bug Fixes" to explain ChromaDB → SQLite flow
- Add new workflow: "Workflow N: Getting Timeline Context Around Relevant Observations"
**Example of corrected observations.md header:**
```markdown
# Search Observations (Semantic + Temporal)
Search observations using ChromaDB vector similarity with SQLite temporal ordering.
## Architecture
**3-Layer Hybrid Search:**
1. **ChromaDB semantic retrieval** - Finds what's semantically relevant (vector similarity)
2. **90-day recency filter** - Prioritizes recent work
3. **SQLite temporal ordering** - Sorts by time, returns newest relevant
**Fallback:** If ChromaDB unavailable, falls back to FTS5 keyword search.
## When to Use
- User asks: "How did we implement authentication?"
- User asks: "What bugs did we fix?"
- Looking for past work by meaning/topic (not just keywords)
```
### Fix 2: Implement Layer 3 (Instant Context Timeline)
**Option A: Add to existing search_observations handler**
Modify `/Users/alexnewman/Scripts/claude-mem/src/servers/search-server.ts` line ~396:
```typescript
// After getting sorted results, if user wants timeline context
if (results.length > 0 && options.includeTimeline) {
const topObservation = results[0];
const depth_before = options.timelineDepthBefore || 5;
const depth_after = options.timelineDepthAfter || 5;
// Get observations before and after
const timeline = store.getTimelineContext(
topObservation.id,
depth_before,
depth_after
);
return {
topResult: topObservation,
timeline: timeline,
format: format
};
}
```
**Option B: Use existing timeline-by-query operation**
The `/api/timeline/by-query` endpoint already implements search + timeline. Could:
1. Make it the DEFAULT recommended operation in skill guides
2. Update operation guides to emphasize this as primary workflow
3. Position observations search as "timeline-less" alternative
**Recommendation:** Option B is faster - leverage existing `timeline-by-query` endpoint and update skill guides to make it the primary workflow.
### Fix 3: Update SKILL.md Technical Notes
**File:** `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/SKILL.md`
**Line 172:**
**Current:**
```markdown
- **Search engine:** FTS5 full-text search + structured filters
```
**Change to:**
```markdown
- **Search engine:** ChromaDB vector search (primary) + SQLite temporal ordering + instant context timeline (3-layer sequential architecture)
```
### Fix 4: Update search_observations Description
**File:** `/Users/alexnewman/Scripts/claude-mem/src/servers/search-server.ts`
**Line 349:**
**Current:**
```typescript
description: 'Search observations using full-text search across titles, narratives...'
```
**Change to:**
```typescript
description: 'Search observations using hybrid semantic search (ChromaDB vector similarity + SQLite temporal ordering). Falls back to FTS5 keyword search if ChromaDB unavailable. IMPORTANT: Always use index format first...'
```
**Line 351:**
**Current:**
```typescript
query: z.string().describe('Search query for FTS5 full-text search'),
```
**Change to:**
```typescript
query: z.string().describe('Search query (semantic vector search via ChromaDB, falls back to FTS5 if unavailable)'),
```
---
## Implementation Checklist
Use this checklist when executing fixes:
### Phase 1: Core Documentation
- [ ] Update `observations.md` title and description
- [ ] Update `observations.md` architecture explanation
- [ ] Update `observations.md` examples to mention ChromaDB
- [ ] Update `common-workflows.md` to explain hybrid flow
- [ ] Update `SKILL.md` line 172 technical notes
- [ ] Verify all operation guides mention ChromaDB correctly
### Phase 2: Backend Updates
- [ ] Update `search-server.ts` search_observations description (line 349)
- [ ] Update `search-server.ts` query parameter description (line 351)
- [ ] Add code comments explaining 3-layer flow
- [ ] Consider adding `includeTimeline` option to search_observations
### Phase 3: Timeline Integration
- [ ] Review timeline-by-query operation
- [ ] Update skill guides to recommend timeline-by-query as primary workflow
- [ ] Add example: "When you need context, use timeline-by-query instead of observations search"
- [ ] Update quick reference table in SKILL.md to highlight timeline-by-query
### Phase 4: Validation
- [ ] Test search behavior with ChromaDB enabled
- [ ] Test fallback behavior with ChromaDB disabled
- [ ] Verify skill guides accurately describe behavior
- [ ] Ensure landing page messaging aligns with skill guides
- [ ] Check that human memory analogy is consistent everywhere
---
## Key Messaging (Use Consistently)
### Value Proposition
"3-layer hybrid search mimics human memory: ChromaDB semantic retrieval finds what's relevant → SQLite temporal ordering identifies when → instant context timeline shows what led there and what came next."
### Technical Architecture
"ChromaDB vector search handles semantic understanding (what's relevant), SQLite handles temporal queries (when it happened, what's newest), and timeline context provides before/after observations (what led there, what happened next)."
### Why It Matters
"LLMs don't experience time linearly like humans do. Claude-mem gives them temporal context: not just 'you implemented authentication,' but 'you researched OAuth libraries, then implemented JWT auth, then fixed a token expiration bug.' Complete narrative, like human memory."
### ChromaDB Role
"ChromaDB is the PRIMARY search mechanism for semantic understanding. FTS5 is the FALLBACK for backward compatibility and reliability when ChromaDB is unavailable."
---
## Files Reference
**Skill Guides (Primary Fixes):**
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/SKILL.md`
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/observations.md`
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/timeline-by-query.md`
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/common-workflows.md`
**Backend Code (Minor Updates):**
- `/Users/alexnewman/Scripts/claude-mem/src/servers/search-server.ts`
**Documentation (Validation):**
- `/Users/alexnewman/Scripts/claude-mem/docs/context/mem-search-technical-architecture.md`
**Landing Page (Already Fixed):**
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/Features.tsx`
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/QuickBenefits.tsx`
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/Architecture.tsx`
---
## Questions for User (If Needed)
1. **Timeline Integration Approach:**
- Option A: Modify search_observations to add `includeTimeline` parameter
- Option B: Emphasize timeline-by-query as primary workflow in guides
- User preference?
2. **Backward Compatibility:**
- Should FTS5 fallback be MORE prominent in docs for older systems?
- Or keep it as "implementation detail"?
3. **Progressive Disclosure:**
- Should timeline context ALWAYS be included?
- Or only when user explicitly asks for context?
---
## Success Criteria
When these fixes are complete:
1. ✅ Skill operation guides accurately describe ChromaDB-first architecture
2. ✅ No references to "FTS5 as primary search method"
3. ✅ Timeline feature integrated into standard workflow
4. ✅ Human memory analogy present in key documentation
5. ✅ Consistent messaging across skill guides, docs, and landing page
6. ✅ Backend code comments explain 3-layer flow clearly
7. ✅ Users understand: "This is semantic search with temporal context, not just keyword search"
---
## Notes for Next Claude
- The user has already clarified the architecture thoroughly
- Backend code is already correct - focus on documentation/guides
- Landing page recently updated - validate for consistency
- Timeline-by-query endpoint already exists - leverage it
- Key insight: This mimics human memory through temporal context
- ChromaDB is PRIMARY, not optional. FTS5 is FALLBACK, not primary.
**Start with:** Reading this document fully, then update skill operation guides first (highest impact).
+21 -22
View File
@@ -89,31 +89,30 @@ npx mintlify dev
### Getting Started ### Getting Started
- **[Installation Guide](docs/installation.mdx)** - Quick start & advanced installation - **[Installation Guide](https://docs.claude-mem.ai/installation)** - Quick start & advanced installation
- **[Usage Guide](docs/usage/getting-started.mdx)** - How Claude-Mem works automatically - **[Usage Guide](https://docs.claude-mem.ai/usage/getting-started)** - How Claude-Mem works automatically
- **[Search Tools](docs/usage/search-tools.mdx)** - Query your project history with natural language - **[Search Tools](https://docs.claude-mem.ai/usage/search-tools)** - Query your project history with natural language
### Best Practices ### Best Practices
- **[Context Engineering](docs/context-engineering.mdx)** - AI agent context optimization principles - **[Context Engineering](https://docs.claude-mem.ai/context-engineering)** - AI agent context optimization principles
- **[Progressive Disclosure](docs/progressive-disclosure.mdx)** - Philosophy behind Claude-Mem's context priming strategy - **[Progressive Disclosure](https://docs.claude-mem.ai/progressive-disclosure)** - Philosophy behind Claude-Mem's context priming strategy
### Architecture ### Architecture
- **[Overview](docs/architecture/overview.mdx)** - System components & data flow - **[Overview](https://docs.claude-mem.ai/architecture/overview)** - System components & data flow
- **[Architecture Evolution](docs/architecture-evolution.mdx)** - The journey from v3 to v5 - **[Architecture Evolution](https://docs.claude-mem.ai/architecture-evolution)** - The journey from v3 to v5
- **[Hooks Architecture](docs/hooks-architecture.mdx)** - How Claude-Mem uses lifecycle hooks - **[Hooks Architecture](https://docs.claude-mem.ai/hooks-architecture)** - How Claude-Mem uses lifecycle hooks
- **[Hooks Reference](docs/architecture/hooks.mdx)** - 7 hook scripts explained - **[Hooks Reference](https://docs.claude-mem.ai/architecture/hooks)** - 7 hook scripts explained
- **[Worker Service](docs/architecture/worker-service.mdx)** - HTTP API & PM2 management - **[Worker Service](https://docs.claude-mem.ai/architecture/worker-service)** - HTTP API & PM2 management
- **[Database](docs/architecture/database.mdx)** - SQLite schema & FTS5 search - **[Database](https://docs.claude-mem.ai/architecture/database)** - SQLite schema & FTS5 search
- **[MCP Search](docs/architecture/mcp-search.mdx)** - 9 search tools & examples - **[Search Architecture](https://docs.claude-mem.ai/architecture/search-architecture)** - Hybrid search with Chroma vector database
- **[Viewer UI](docs/VIEWER.md)** - Web-based memory stream visualization
### Configuration & Development ### Configuration & Development
- **[Configuration](docs/configuration.mdx)** - Environment variables & settings - **[Configuration](https://docs.claude-mem.ai/configuration)** - Environment variables & settings
- **[Development](docs/development.mdx)** - Building, testing, contributing - **[Development](https://docs.claude-mem.ai/development)** - Building, testing, contributing
- **[Troubleshooting](docs/troubleshooting.mdx)** - Common issues & solutions - **[Troubleshooting](https://docs.claude-mem.ai/troubleshooting)** - Common issues & solutions
--- ---
@@ -150,7 +149,7 @@ npx mintlify dev
5. **mem-search Skill** - Natural language queries with progressive disclosure (~2,250 token savings vs MCP) 5. **mem-search Skill** - Natural language queries with progressive disclosure (~2,250 token savings vs MCP)
6. **Chroma Vector Database** - Hybrid semantic + keyword search for intelligent context retrieval 6. **Chroma Vector Database** - Hybrid semantic + keyword search for intelligent context retrieval
See [Architecture Overview](docs/architecture/overview.mdx) for details. See [Architecture Overview](https://docs.claude-mem.ai/architecture/overview) for details.
--- ---
@@ -186,7 +185,7 @@ Claude-Mem provides intelligent search through the mem-search skill that auto-in
"What was happening when we added the viewer UI?" "What was happening when we added the viewer UI?"
``` ```
See [Search Tools Guide](docs/usage/search-tools.mdx) for detailed examples. See [Search Tools Guide](https://docs.claude-mem.ai/usage/search-tools) for detailed examples.
--- ---
@@ -278,7 +277,7 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
- `CLAUDE_MEM_WORKER_PORT` - Worker port (default: 37777) - `CLAUDE_MEM_WORKER_PORT` - Worker port (default: 37777)
- `CLAUDE_MEM_DATA_DIR` - Data directory override (dev only) - `CLAUDE_MEM_DATA_DIR` - Data directory override (dev only)
See [Configuration Guide](docs/configuration.mdx) for details. See [Configuration Guide](https://docs.claude-mem.ai/configuration) for details.
--- ---
@@ -301,7 +300,7 @@ npm run worker:start
npm run worker:logs npm run worker:logs
``` ```
See [Development Guide](docs/development.mdx) for detailed instructions. See [Development Guide](https://docs.claude-mem.ai/development) for detailed instructions.
--- ---
@@ -318,7 +317,7 @@ If you're experiencing issues, describe the problem to Claude and the troublesho
- Database issues → `sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"` - Database issues → `sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"`
- Search not working → Check FTS5 tables exist - Search not working → Check FTS5 tables exist
See [Troubleshooting Guide](docs/troubleshooting.mdx) for complete solutions. See [Troubleshooting Guide](https://docs.claude-mem.ai/troubleshooting) for complete solutions.
--- ---
@@ -332,7 +331,7 @@ Contributions are welcome! Please:
4. Update documentation 4. Update documentation
5. Submit a Pull Request 5. Submit a Pull Request
See [Development Guide](docs/development.mdx) for contribution workflow. See [Development Guide](https://docs.claude-mem.ai/development) for contribution workflow.
--- ---
@@ -148,16 +148,19 @@ When Claude invokes the skill:
## Search Architecture ## Search Architecture
### Hybrid Search System ### 3-Layer Hybrid Search System
claude-mem uses a **hybrid search architecture** combining: claude-mem uses a **3-layer sequential search architecture** that mimics human long-term memory:
1. **SQLite FTS5 (Full-Text Search)** - Keyword-based search **Storage Flow (Write Path):**
2. **ChromaDB (Vector Search)** - Semantic similarity search 1. **SQLite First** - Data written synchronously to SQLite (fast, immediate access)
2. **ChromaDB Background Sync** - Worker asynchronously generates embeddings and syncs to ChromaDB
**Search Flow (Read Path - Sequential, NOT parallel):**
``` ```
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
Search Request Flow 3-Layer Sequential Search Flow
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
@@ -166,61 +169,70 @@ claude-mem uses a **hybrid search architecture** combining:
│ /api/search/* │ │ /api/search/* │
└─────────────────────────┘ └─────────────────────────┘
┌─────────────┴─────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ SessionSearch (FTS5) │ │ ChromaSync (Vector DB) │
│ │ │ │
│ Full-text keyword │ │ Semantic similarity │
│ search on: │ │ search on: │
│ - titles │ │ - narratives │
│ - narratives │ │ - facts │
│ - facts │ │ - file content │
│ - concepts │ │ │
│ │ │ Embeddings: │
│ SQLite DB: │ │ - text-embedding-3-small│
│ observations_fts │ │ - 90-day recency filter │
│ sessions_fts │ │ │
│ prompts_fts │ │ ChromaDB: │
│ │ │ observations collection │
└──────────────────────────┘ └──────────────────────────┘
│ │
└─────────────┬─────────────┘
─────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
│ Merged Results LAYER 1: Semantic Retrieval (ChromaDB)
│ - Deduplicated │ ─────────────────────────────────────────────────────────
│ - Sorted by relevance │ Vector similarity search finds semantically relevant items
│ - Formatted (index/full) Returns: observation IDs in index format (~50-100 tokens)
└─────────────────────────┘ │ Filter: 90-day recency prioritizes recent work │
│ Output: List of relevant observation IDs │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ LAYER 2: Temporal Ordering (SQLite) │
│ ───────────────────────────────────────────────────────── │
│ Takes observation IDs from Layer 1 │
│ Sorts by created_at timestamp (fast SQLite temporal query) │
│ Identifies: MOST RECENT relevant observation │
│ Why: ChromaDB doesn't easily query by date range sorted │
│ Output: Top observation ID by time │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ LAYER 3: Instant Context Timeline (SQLite) │
│ ───────────────────────────────────────────────────────── │
│ Uses top observation ID from Layer 2 as anchor │
│ Retrieves N observations BEFORE and AFTER that point │
│ Provides: "what led here" + "what happened next" context │
│ This is the KILLER FEATURE: mimics human memory │
│ Output: Timeline with temporal context │
└─────────────────────────────────────────────────────────────┘
``` ```
**Why This Architecture Exists:**
The problem: LLMs don't experience time linearly like humans do. Finding semantically relevant information isn't enough—you need temporal context.
The solution:
- **ChromaDB** for "what's relevant" (semantic understanding)
- **SQLite** for "when did it happen" (temporal ordering with fast date-range queries)
- **Timeline** for "what was the context" (before/after observations)
Together, they mimic how humans recall: "I did X, which led to Y, then Z happened."
**Human Memory Analogy:**
Humans don't just remember isolated facts. They remember sequences: what they did before something, what happened after. The instant context timeline gives LLMs this same temporal awareness that humans experience naturally.
### Search Types ### Search Types
#### 1. Full-Text Search (FTS5) #### 1. Vector Search (ChromaDB) - PRIMARY Search Layer
**How it works:** **Role:** Layer 1 - Semantic Retrieval
- Uses SQLite FTS5 virtual tables for instant keyword matching
- Supports boolean operators: `AND`, `OR`, `NOT`, `NEAR`, `*` (wildcard)
- Ranks results by BM25 relevance scoring
- Sub-100ms performance on 8,000+ observations
**Example query:**
```sql
-- User asks: "How did we implement JWT authentication?"
SELECT * FROM observations_fts
WHERE observations_fts MATCH 'JWT AND authentication'
ORDER BY rank
LIMIT 20;
```
#### 2. Vector Search (ChromaDB)
**How it works:** **How it works:**
- Text is embedded using OpenAI's `text-embedding-3-small` model - Text is embedded using OpenAI's `text-embedding-3-small` model
- Vector similarity search finds semantically related content - Vector similarity search finds semantically related content, not just keyword matches
- 90-day recency filter prioritizes recent work - 90-day recency filter prioritizes recent work
- Combined with keyword search for hybrid results - Returns observation IDs for temporal processing in Layer 2
**Why it's primary:**
- Understands meaning, not just keywords ("auth flow" matches "JWT implementation")
- Finds relevant work even when you don't know exact terms used
- Semantic understanding crucial for LLM memory retrieval
**Example query:** **Example query:**
```python ```python
@@ -230,6 +242,37 @@ collection.query(
n_results=20, n_results=20,
where={"created_at": {"$gte": ninety_days_ago}} where={"created_at": {"$gte": ninety_days_ago}}
) )
# Returns: observation IDs semantically related to login/auth
```
#### 2. Full-Text Search (FTS5) - Supporting Layer
**Role:** Layer 2 & 3 - Temporal Ordering and Timeline Context
**How it works:**
- Uses SQLite FTS5 virtual tables for instant keyword matching
- Supports boolean operators: `AND`, `OR`, `NOT`, `NEAR`, `*` (wildcard)
- Fast temporal queries with date-range sorting
- Sub-100ms performance on 8,000+ observations
**Why it's supporting:**
- ChromaDB handles semantic "what's relevant"
- SQLite/FTS5 handles temporal "when did it happen" and "what came before/after"
- Optimized for timeline queries and date-based sorting
**Example query:**
```sql
-- Takes observation IDs from ChromaDB, sorts by time
SELECT * FROM observations
WHERE id IN (/* IDs from ChromaDB */)
ORDER BY created_at_epoch DESC
LIMIT 1;
-- Then retrieves timeline context around that observation
SELECT * FROM observations
WHERE created_at_epoch < anchor_timestamp
ORDER BY created_at_epoch DESC
LIMIT 10; -- "what led here"
``` ```
#### 3. Structured Filters #### 3. Structured Filters
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "6.0.1", "version": "6.0.3",
"description": "Memory compression system for Claude Code - persist context across sessions", "description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [ "keywords": [
"claude", "claude",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "6.0.1", "version": "6.0.3",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions", "description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": { "author": {
"name": "Alex Newman" "name": "Alex Newman"
+19 -19
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import{stdin as I}from"process";import w from"better-sqlite3";import{join as E,dirname as k,basename as W}from"path";import{homedir as O}from"os";import{existsSync as K,mkdirSync as x}from"fs";import{fileURLToPath as U}from"url";function M(){return typeof __dirname<"u"?__dirname:k(U(import.meta.url))}var q=M(),l=process.env.CLAUDE_MEM_DATA_DIR||E(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||E(O(),".claude"),J=E(l,"archives"),Q=E(l,"logs"),z=E(l,"trash"),Z=E(l,"backups"),ee=E(l,"settings.json"),f=E(l,"claude-mem.db"),se=E(l,"vector-db"),te=E(R,"settings.json"),re=E(R,"commands"),ne=E(R,"CLAUDE.md");function L(p){x(p,{recursive:!0})}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message} import{stdin as I}from"process";import w from"better-sqlite3";import{join as E,dirname as k,basename as G}from"path";import{homedir as O}from"os";import{existsSync as K,mkdirSync as x}from"fs";import{fileURLToPath as U}from"url";function M(){return typeof __dirname<"u"?__dirname:k(U(import.meta.url))}var q=M(),m=process.env.CLAUDE_MEM_DATA_DIR||E(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||E(O(),".claude"),J=E(m,"archives"),Q=E(m,"logs"),z=E(m,"trash"),Z=E(m,"backups"),ee=E(m,"settings.json"),f=E(m,"claude-mem.db"),se=E(m,"vector-db"),te=E(R,"settings.json"),re=E(R,"commands"),ne=E(R,"CLAUDE.md");function L(c){x(c,{recursive:!0})}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=h[e].padEnd(5),d=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=` ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=h[e].padEnd(5),d=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let T="";n!=null&&(this.level===0&&typeof n=="object"?T=`
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let T="";if(r){let{sessionId:m,sdkSessionId:S,correlationId:c,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([y,D])=>`${y}=${D}`).join(", ")}}`)}let b=`[${o}] [${i}] [${d}] ${_}${t}${T}${u}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var g=class{db;constructor(){L(l),this.db=new w(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(` `+JSON.stringify(n,null,2):T=" "+this.formatData(n));let u="";if(r){let{sessionId:l,sdkSessionId:S,correlationId:p,...a}=r;Object.keys(a).length>0&&(u=` {${Object.entries(a).map(([y,D])=>`${y}=${D}`).join(", ")}}`)}let b=`[${o}] [${i}] [${d}] ${_}${t}${u}${T}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var g=class{db;constructor(){L(m),this.db=new w(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions ( CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL, version INTEGER UNIQUE NOT NULL,
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts_fts(rowid, prompt_text) INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text); VALUES (new.id, new.prompt_text);
END; END;
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(` `),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;this.db.pragma("table_info(observations)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString())}catch(e){console.error("[SessionStore] Discovery tokens migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT SELECT
request, investigated, learned, completed, next_steps, request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at files_read, files_edited, notes, prompt_number, created_at
@@ -312,29 +312,29 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts INSERT INTO user_prompts
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch) (claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(` `).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r,n=0){let o=new Date,i=o.getTime();this.db.prepare(`
SELECT id FROM sdk_sessions WHERE sdk_session_id = ? SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
`).get(e)||(this.db.prepare(` `).get(e)||(this.db.prepare(`
INSERT INTO sdk_sessions INSERT INTO sdk_sessions
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active') VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(` `).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
INSERT INTO observations INSERT INTO observations
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
files_read, files_modified, prompt_number, created_at, created_at_epoch) files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(` `).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n,o.toISOString(),i);return{id:Number(u.lastInsertRowid),createdAtEpoch:i}}storeSummary(e,s,t,r,n=0){let o=new Date,i=o.getTime();this.db.prepare(`
SELECT id FROM sdk_sessions WHERE sdk_session_id = ? SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
`).get(e)||(this.db.prepare(` `).get(e)||(this.db.prepare(`
INSERT INTO sdk_sessions INSERT INTO sdk_sessions
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active') VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(` `).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
INSERT INTO session_summaries INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed, (sdk_session_id, project, request, investigated, learned, completed,
next_steps, notes, prompt_number, created_at, created_at_epoch) next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(` `).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n,o.toISOString(),i);return{id:Number(u.lastInsertRowid),createdAtEpoch:i}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
UPDATE sdk_sessions UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ? SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ? WHERE id = ?
@@ -357,7 +357,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
WHERE up.id IN (${i}) WHERE up.id IN (${i})
ORDER BY up.created_at_epoch ${n} ORDER BY up.created_at_epoch ${n}
${o} ${o}
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],d,_;if(e!==null){let m=` `).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],d,_;if(e!==null){let l=`
SELECT id, created_at_epoch SELECT id, created_at_epoch
FROM observations FROM observations
WHERE id <= ? ${o} WHERE id <= ? ${o}
@@ -369,7 +369,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
WHERE id >= ? ${o} WHERE id >= ? ${o}
ORDER BY id ASC ORDER BY id ASC
LIMIT ? LIMIT ?
`;try{let c=this.db.prepare(m).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary observations:",c.message),{observations:[],sessions:[],prompts:[]}}}else{let m=` `;try{let p=this.db.prepare(l).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(p.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=p.length>0?p[p.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(p){return console.error("[SessionStore] Error getting boundary observations:",p.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
SELECT created_at_epoch SELECT created_at_epoch
FROM observations FROM observations
WHERE created_at_epoch <= ? ${o} WHERE created_at_epoch <= ? ${o}
@@ -381,12 +381,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
WHERE created_at_epoch >= ? ${o} WHERE created_at_epoch >= ? ${o}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
LIMIT ? LIMIT ?
`;try{let c=this.db.prepare(m).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary timestamps:",c.message),{observations:[],sessions:[],prompts:[]}}}let u=` `;try{let p=this.db.prepare(l).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(p.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=p.length>0?p[p.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(p){return console.error("[SessionStore] Error getting boundary timestamps:",p.message),{observations:[],sessions:[],prompts:[]}}}let T=`
SELECT * SELECT *
FROM observations FROM observations
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o} WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
`,T=` `,u=`
SELECT * SELECT *
FROM session_summaries FROM session_summaries
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o} WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
@@ -397,5 +397,5 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")} WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
ORDER BY up.created_at_epoch ASC ORDER BY up.created_at_epoch ASC
`;try{let m=this.db.prepare(u).all(d,_,...i),S=this.db.prepare(T).all(d,_,...i),c=this.db.prepare(b).all(d,_,...i);return{observations:m,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:c.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(m){return console.error("[SessionStore] Error querying timeline records:",m.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};import X from"path";import{homedir as F}from"os";import{existsSync as B,readFileSync as j}from"fs";function C(){try{let p=X.join(F(),".claude-mem","settings.json");if(B(p)){let e=JSON.parse(j(p,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function v(p){console.error("[claude-mem cleanup] Hook fired",{input:p?{session_id:p.session_id,cwd:p.cwd,reason:p.reason}:null}),p||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(` `;try{let l=this.db.prepare(T).all(d,_,...i),S=this.db.prepare(u).all(d,_,...i),p=this.db.prepare(b).all(d,_,...i);return{observations:l,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:p.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};import F from"path";import{homedir as X}from"os";import{existsSync as B,readFileSync as H}from"fs";function v(){try{let c=F.join(X(),".claude-mem","settings.json");if(B(c)){let e=JSON.parse(H(c,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function C(c){console.error("[claude-mem cleanup] Hook fired",{input:c?{session_id:c.session_id,cwd:c.cwd,reason:c.reason}:null}),c||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=p;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close();try{let n=r.worker_port||C();await fetch(`http://127.0.0.1:${n}/sessions/${r.id}/complete`,{method:"POST",signal:AbortSignal.timeout(1e3)}),console.error("[claude-mem cleanup] Worker notified to stop processing indicator")}catch(n){console.error("[claude-mem cleanup] Failed to notify worker (non-critical):",n)}console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)v(void 0);else{let p="";I.on("data",e=>p+=e),I.on("end",async()=>{let e=p?JSON.parse(p):void 0;await v(e)})} Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=c;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close();try{let n=r.worker_port||v();await fetch(`http://127.0.0.1:${n}/sessions/${r.id}/complete`,{method:"POST",signal:AbortSignal.timeout(1e3)}),console.error("[claude-mem cleanup] Worker notified to stop processing indicator")}catch(n){console.error("[claude-mem cleanup] Failed to notify worker (non-critical):",n)}console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)C(void 0);else{let c="";I.on("data",e=>c+=e),I.on("end",async()=>{let e=c?JSON.parse(c):void 0;await C(e)})}
File diff suppressed because one or more lines are too long
+25 -25
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import Y from"path";import{stdin as D}from"process";import F from"better-sqlite3";import{join as E,dirname as U,basename as J}from"path";import{homedir as f}from"os";import{existsSync as ee,mkdirSync as M}from"fs";import{fileURLToPath as w}from"url";function X(){return typeof __dirname<"u"?__dirname:U(w(import.meta.url))}var te=X(),m=process.env.CLAUDE_MEM_DATA_DIR||E(f(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||E(f(),".claude"),re=E(m,"archives"),ne=E(m,"logs"),oe=E(m,"trash"),ie=E(m,"backups"),ae=E(m,"settings.json"),L=E(m,"claude-mem.db"),pe=E(m,"vector-db"),de=E(h,"settings.json"),ce=E(h,"commands"),_e=E(h,"CLAUDE.md");function A(p){M(p,{recursive:!0})}var N=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message} import Y from"path";import{stdin as D}from"process";import X from"better-sqlite3";import{join as m,dirname as U,basename as J}from"path";import{homedir as f}from"os";import{existsSync as ee,mkdirSync as M}from"fs";import{fileURLToPath as w}from"url";function F(){return typeof __dirname<"u"?__dirname:U(w(import.meta.url))}var te=F(),l=process.env.CLAUDE_MEM_DATA_DIR||m(f(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||m(f(),".claude"),re=m(l,"archives"),ne=m(l,"logs"),oe=m(l,"trash"),ie=m(l,"backups"),ae=m(l,"settings.json"),L=m(l,"claude-mem.db"),de=m(l,"vector-db"),pe=m(h,"settings.json"),ce=m(h,"commands"),_e=m(h,"CLAUDE.md");function A(d){M(d,{recursive:!0})}var N=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),d=s.padEnd(6),u="";r?.correlationId?u=`[${r.correlationId}] `:r?.sessionId&&(u=`[session-${r.sessionId}] `);let c="";n!=null&&(this.level===0&&typeof n=="object"?c=` ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),p=s.padEnd(6),u="";r?.correlationId?u=`[${r.correlationId}] `:r?.sessionId&&(u=`[session-${r.sessionId}] `);let c="";n!=null&&(this.level===0&&typeof n=="object"?c=`
`+JSON.stringify(n,null,2):c=" "+this.formatData(n));let l="";if(r){let{sessionId:T,sdkSessionId:b,correlationId:_,...a}=r;Object.keys(a).length>0&&(l=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let S=`[${o}] [${i}] [${d}] ${u}${t}${l}${c}`;e===3?console.error(S):console.log(S)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},C=new O;var g=class{db;constructor(){A(m),this.db=new F(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(` `+JSON.stringify(n,null,2):c=" "+this.formatData(n));let E="";if(r){let{sessionId:T,sdkSessionId:S,correlationId:_,...a}=r;Object.keys(a).length>0&&(E=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let b=`[${o}] [${i}] [${p}] ${u}${t}${E}${c}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},v=new O;var R=class{db;constructor(){A(l),this.db=new X(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions ( CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL, 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_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_project ON session_summaries(project);
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC); 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(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(` `),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(`
CREATE TABLE session_summaries_new ( CREATE TABLE session_summaries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL, sdk_session_id TEXT NOT NULL,
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts_fts(rowid, prompt_text) INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text); VALUES (new.id, new.prompt_text);
END; END;
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(` `),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;this.db.pragma("table_info(observations)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString())}catch(e){console.error("[SessionStore] Discovery tokens migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT SELECT
request, investigated, learned, completed, next_steps, request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at files_read, files_edited, notes, prompt_number, created_at
@@ -262,7 +262,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT files_read, files_modified SELECT files_read, files_modified
FROM observations FROM observations
WHERE sdk_session_id = ? WHERE sdk_session_id = ?
`).all(e),r=new Set,n=new Set;for(let o of t){if(o.files_read)try{let i=JSON.parse(o.files_read);Array.isArray(i)&&i.forEach(d=>r.add(d))}catch{}if(o.files_modified)try{let i=JSON.parse(o.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(` `).all(e),r=new Set,n=new Set;for(let o of t){if(o.files_read)try{let i=JSON.parse(o.files_read);Array.isArray(i)&&i.forEach(p=>r.add(p))}catch{}if(o.files_modified)try{let i=JSON.parse(o.files_modified);Array.isArray(i)&&i.forEach(p=>n.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
SELECT id, claude_session_id, sdk_session_id, project, user_prompt SELECT id, claude_session_id, sdk_session_id, project, user_prompt
FROM sdk_sessions FROM sdk_sessions
WHERE id = ? WHERE id = ?
@@ -299,7 +299,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
UPDATE sdk_sessions UPDATE sdk_sessions
SET sdk_session_id = ? SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL WHERE id = ? AND sdk_session_id IS NULL
`).run(s,e).changes===0?(C.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(` `).run(s,e).changes===0?(v.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
UPDATE sdk_sessions UPDATE sdk_sessions
SET worker_port = ? SET worker_port = ?
WHERE id = ? WHERE id = ?
@@ -312,29 +312,29 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts INSERT INTO user_prompts
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch) (claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(` `).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r,n=0){let o=new Date,i=o.getTime();this.db.prepare(`
SELECT id FROM sdk_sessions WHERE sdk_session_id = ? SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
`).get(e)||(this.db.prepare(` `).get(e)||(this.db.prepare(`
INSERT INTO sdk_sessions INSERT INTO sdk_sessions
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active') VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(` `).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
INSERT INTO observations INSERT INTO observations
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
files_read, files_modified, prompt_number, created_at, created_at_epoch) files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(c.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(` `).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n,o.toISOString(),i);return{id:Number(E.lastInsertRowid),createdAtEpoch:i}}storeSummary(e,s,t,r,n=0){let o=new Date,i=o.getTime();this.db.prepare(`
SELECT id FROM sdk_sessions WHERE sdk_session_id = ? SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
`).get(e)||(this.db.prepare(` `).get(e)||(this.db.prepare(`
INSERT INTO sdk_sessions INSERT INTO sdk_sessions
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active') VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(` `).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
INSERT INTO session_summaries INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed, (sdk_session_id, project, request, investigated, learned, completed,
next_steps, notes, prompt_number, created_at, created_at_epoch) next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),o);return{id:Number(c.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(` `).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n,o.toISOString(),i);return{id:Number(E.lastInsertRowid),createdAtEpoch:i}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
UPDATE sdk_sessions UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ? SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ? WHERE id = ?
@@ -357,47 +357,47 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
WHERE up.id IN (${i}) WHERE up.id IN (${i})
ORDER BY up.created_at_epoch ${n} ORDER BY up.created_at_epoch ${n}
${o} ${o}
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],d,u;if(e!==null){let T=` `).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],p,u;if(e!==null){let T=`
SELECT id, created_at_epoch SELECT id, created_at_epoch
FROM observations FROM observations
WHERE id <= ? ${o} WHERE id <= ? ${o}
ORDER BY id DESC ORDER BY id DESC
LIMIT ? LIMIT ?
`,b=` `,S=`
SELECT id, created_at_epoch SELECT id, created_at_epoch
FROM observations FROM observations
WHERE id >= ? ${o} WHERE id >= ? ${o}
ORDER BY id ASC ORDER BY id ASC
LIMIT ? LIMIT ?
`;try{let _=this.db.prepare(T).all(e,...i,t+1),a=this.db.prepare(b).all(e,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=` `;try{let _=this.db.prepare(T).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
SELECT created_at_epoch SELECT created_at_epoch
FROM observations FROM observations
WHERE created_at_epoch <= ? ${o} WHERE created_at_epoch <= ? ${o}
ORDER BY created_at_epoch DESC ORDER BY created_at_epoch DESC
LIMIT ? LIMIT ?
`,b=` `,S=`
SELECT created_at_epoch SELECT created_at_epoch
FROM observations FROM observations
WHERE created_at_epoch >= ? ${o} WHERE created_at_epoch >= ? ${o}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
LIMIT ? LIMIT ?
`;try{let _=this.db.prepare(T).all(s,...i,t),a=this.db.prepare(b).all(s,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let c=` `;try{let _=this.db.prepare(T).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let c=`
SELECT * SELECT *
FROM observations FROM observations
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o} WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
`,l=` `,E=`
SELECT * SELECT *
FROM session_summaries FROM session_summaries
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o} WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
`,S=` `,b=`
SELECT up.*, s.project, s.sdk_session_id SELECT up.*, s.project, s.sdk_session_id
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")} WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
ORDER BY up.created_at_epoch ASC ORDER BY up.created_at_epoch ASC
`;try{let T=this.db.prepare(c).all(d,u,...i),b=this.db.prepare(l).all(d,u,...i),_=this.db.prepare(S).all(d,u,...i);return{observations:T,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function P(p,e,s){return p==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:p==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:p==="UserPromptSubmit"||p==="PostToolUse"?{continue:!0,suppressOutput:!0}:p==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(p,e,s={}){let t=P(p,e,s);return JSON.stringify(t)}import H from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function R(){try{let p=H.join(B(),".claude-mem","settings.json");if(j(p)){let e=JSON.parse($(p,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let p=R();return(await fetch(`http://127.0.0.1:${p}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function y(){if(await G())return;let p=R();throw new Error(`Worker service is not responding on port ${p}. `;try{let T=this.db.prepare(c).all(p,u,...i),S=this.db.prepare(E).all(p,u,...i),_=this.db.prepare(b).all(p,u,...i);return{observations:T,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function H(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function C(d,e,s={}){let t=H(d,e,s);return JSON.stringify(t)}import P from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as $}from"fs";var G=100;function g(){try{let d=P.join(B(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse($(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function W(){try{let d=g();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(G)})).ok}catch{return!1}}async function y(){if(await W())return;let d=g();throw new Error(`Worker service is not responding on port ${d}.
If you just updated the plugin, PM2's watch mode should restart automatically. If you just updated the plugin, PM2's watch mode should restart automatically.
If the problem persists, run: pm2 restart claude-mem-worker`)}async function K(p){if(!p)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=p,r=Y.basename(s);await y();let n=new g,o=n.createSDKSession(e,r,t),i=n.incrementPromptCounter(o);n.saveUserPrompt(e,i,t),console.error(`[new-hook] Session ${o}, prompt #${i}`),n.close();let d=R(),u=t.startsWith("/")?t.substring(1):t;try{let c=await fetch(`http://127.0.0.1:${d}/sessions/${o}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:u,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let l=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${l}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(v("UserPromptSubmit",!0))}var I="";D.on("data",p=>I+=p);D.on("end",async()=>{let p=I?JSON.parse(I):void 0;await K(p)}); If the problem persists, run: pm2 restart claude-mem-worker`)}async function K(d){if(!d)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=d,r=Y.basename(s);await y();let n=new R,o=n.createSDKSession(e,r,t),i=n.incrementPromptCounter(o);n.saveUserPrompt(e,i,t),console.error(`[new-hook] Session ${o}, prompt #${i}`),n.close();let p=g(),u=t.startsWith("/")?t.substring(1):t;try{let c=await fetch(`http://127.0.0.1:${p}/sessions/${o}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:u,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let E=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${E}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(C("UserPromptSubmit",!0))}var I="";D.on("data",d=>I+=d);D.on("end",async()=>{let d=I?JSON.parse(I):void 0;await K(d)});
+43 -43
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import{stdin as D}from"process";import X from"better-sqlite3";import{join as m,dirname as U,basename as J}from"path";import{homedir as A}from"os";import{existsSync as ee,mkdirSync as M}from"fs";import{fileURLToPath as w}from"url";function F(){return typeof __dirname<"u"?__dirname:U(w(import.meta.url))}var te=F(),l=process.env.CLAUDE_MEM_DATA_DIR||m(A(),".claude-mem"),N=process.env.CLAUDE_CONFIG_DIR||m(A(),".claude"),re=m(l,"archives"),ne=m(l,"logs"),oe=m(l,"trash"),ie=m(l,"backups"),ae=m(l,"settings.json"),C=m(l,"claude-mem.db"),de=m(l,"vector-db"),pe=m(N,"settings.json"),ce=m(N,"commands"),_e=m(N,"CLAUDE.md");function v(d){M(d,{recursive:!0})}var O=(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))(O||{}),I=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message} import{stdin as D}from"process";import X from"better-sqlite3";import{join as m,dirname as U,basename as J}from"path";import{homedir as A}from"os";import{existsSync as ee,mkdirSync as M}from"fs";import{fileURLToPath as w}from"url";function F(){return typeof __dirname<"u"?__dirname:U(w(import.meta.url))}var te=F(),T=process.env.CLAUDE_MEM_DATA_DIR||m(A(),".claude-mem"),N=process.env.CLAUDE_CONFIG_DIR||m(A(),".claude"),re=m(T,"archives"),oe=m(T,"logs"),ne=m(T,"trash"),ie=m(T,"backups"),ae=m(T,"settings.json"),v=m(T,"claude-mem.db"),de=m(T,"vector-db"),pe=m(N,"settings.json"),ce=m(N,"commands"),_e=m(N,"CLAUDE.md");function C(d){M(d,{recursive:!0})}var O=(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))(O||{}),I=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=O[e].padEnd(5),p=s.padEnd(6),u="";r?.correlationId?u=`[${r.correlationId}] `:r?.sessionId&&(u=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=` ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),n=O[e].padEnd(5),p=s.padEnd(6),u="";r?.correlationId?u=`[${r.correlationId}] `:r?.sessionId&&(u=`[session-${r.sessionId}] `);let E="";o!=null&&(this.level===0&&typeof o=="object"?E=`
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let c="";if(r){let{sessionId:T,sdkSessionId:g,correlationId:_,...a}=r;Object.keys(a).length>0&&(c=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let b=`[${o}] [${i}] [${p}] ${u}${t}${c}${E}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},S=new I;var R=class{db;constructor(){v(l),this.db=new X(C),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(` `+JSON.stringify(o,null,2):E=" "+this.formatData(o));let c="";if(r){let{sessionId:b,sdkSessionId:g,correlationId:_,...a}=r;Object.keys(a).length>0&&(c=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let l=`[${i}] [${n}] [${p}] ${u}${t}${c}${E}`;e===3?console.error(l):console.log(l)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},S=new I;var R=class{db;constructor(){C(T),this.db=new X(v),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions ( CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL, version INTEGER UNIQUE NOT NULL,
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts_fts(rowid, prompt_text) INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text); VALUES (new.id, new.prompt_text);
END; END;
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(` `),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;this.db.pragma("table_info(observations)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString())}catch(e){console.error("[SessionStore] Discovery tokens migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT SELECT
request, investigated, learned, completed, next_steps, request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at files_read, files_edited, notes, prompt_number, created_at
@@ -244,12 +244,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT * SELECT *
FROM observations FROM observations
WHERE id = ? WHERE id = ?
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(` `).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",n=e.map(()=>"?").join(",");return this.db.prepare(`
SELECT * SELECT *
FROM observations FROM observations
WHERE id IN (${i}) WHERE id IN (${n})
ORDER BY created_at_epoch ${n} ORDER BY created_at_epoch ${o}
${o} ${i}
`).all(...e)}getSummaryForSession(e){return this.db.prepare(` `).all(...e)}getSummaryForSession(e){return this.db.prepare(`
SELECT SELECT
request, investigated, learned, completed, next_steps, request, investigated, learned, completed, next_steps,
@@ -262,7 +262,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT files_read, files_modified SELECT files_read, files_modified
FROM observations FROM observations
WHERE sdk_session_id = ? WHERE sdk_session_id = ?
`).all(e),r=new Set,n=new Set;for(let o of t){if(o.files_read)try{let i=JSON.parse(o.files_read);Array.isArray(i)&&i.forEach(p=>r.add(p))}catch{}if(o.files_modified)try{let i=JSON.parse(o.files_modified);Array.isArray(i)&&i.forEach(p=>n.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(` `).all(e),r=new Set,o=new Set;for(let i of t){if(i.files_read)try{let n=JSON.parse(i.files_read);Array.isArray(n)&&n.forEach(p=>r.add(p))}catch{}if(i.files_modified)try{let n=JSON.parse(i.files_modified);Array.isArray(n)&&n.forEach(p=>o.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
SELECT id, claude_session_id, sdk_session_id, project, user_prompt SELECT id, claude_session_id, sdk_session_id, project, user_prompt
FROM sdk_sessions FROM sdk_sessions
WHERE id = ? WHERE id = ?
@@ -289,13 +289,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT prompt_counter FROM sdk_sessions WHERE id = ? SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(` `).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
SELECT prompt_counter FROM sdk_sessions WHERE id = ? SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),i=this.db.prepare(` `).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),n=this.db.prepare(`
INSERT OR IGNORE INTO sdk_sessions INSERT OR IGNORE INTO sdk_sessions
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status) (claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, ?, 'active') VALUES (?, ?, ?, ?, ?, ?, 'active')
`).run(e,e,s,t,r.toISOString(),n);return i.lastInsertRowid===0||i.changes===0?this.db.prepare(` `).run(e,e,s,t,r.toISOString(),o);return n.lastInsertRowid===0||n.changes===0?this.db.prepare(`
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1 SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
`).get(e).id:i.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(` `).get(e).id:n.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
UPDATE sdk_sessions UPDATE sdk_sessions
SET sdk_session_id = ? SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL WHERE id = ? AND sdk_session_id IS NULL
@@ -308,33 +308,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
FROM sdk_sessions FROM sdk_sessions
WHERE id = ? WHERE id = ?
LIMIT 1 LIMIT 1
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(` `).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
INSERT INTO user_prompts INSERT INTO user_prompts
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch) (claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(` `).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r,o=0){let i=new Date,n=i.getTime();this.db.prepare(`
SELECT id FROM sdk_sessions WHERE sdk_session_id = ? SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
`).get(e)||(this.db.prepare(` `).get(e)||(this.db.prepare(`
INSERT INTO sdk_sessions INSERT INTO sdk_sessions
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active') VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(` `).run(e,e,s,i.toISOString(),n),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(`
INSERT INTO observations INSERT INTO observations
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
files_read, files_modified, prompt_number, created_at, created_at_epoch) files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(` `).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o,i.toISOString(),n);return{id:Number(c.lastInsertRowid),createdAtEpoch:n}}storeSummary(e,s,t,r,o=0){let i=new Date,n=i.getTime();this.db.prepare(`
SELECT id FROM sdk_sessions WHERE sdk_session_id = ? SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
`).get(e)||(this.db.prepare(` `).get(e)||(this.db.prepare(`
INSERT INTO sdk_sessions INSERT INTO sdk_sessions
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active') VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(` `).run(e,e,s,i.toISOString(),n),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(`
INSERT INTO session_summaries INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed, (sdk_session_id, project, request, investigated, learned, completed,
next_steps, notes, prompt_number, created_at, created_at_epoch) next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(` `).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o,i.toISOString(),n);return{id:Number(c.lastInsertRowid),createdAtEpoch:n}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
UPDATE sdk_sessions UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ? SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ? WHERE id = ?
@@ -342,62 +342,62 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
UPDATE sdk_sessions UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ? SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE id = ? WHERE id = ?
`).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(` `).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",n=e.map(()=>"?").join(",");return this.db.prepare(`
SELECT * FROM session_summaries SELECT * FROM session_summaries
WHERE id IN (${i}) WHERE id IN (${n})
ORDER BY created_at_epoch ${n} ORDER BY created_at_epoch ${o}
${o} ${i}
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(` `).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",n=e.map(()=>"?").join(",");return this.db.prepare(`
SELECT SELECT
up.*, up.*,
s.project, s.project,
s.sdk_session_id s.sdk_session_id
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.id IN (${i}) WHERE up.id IN (${n})
ORDER BY up.created_at_epoch ${n} ORDER BY up.created_at_epoch ${o}
${o} ${i}
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],p,u;if(e!==null){let T=` `).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,o){let i=o?"AND project = ?":"",n=o?[o]:[],p,u;if(e!==null){let b=`
SELECT id, created_at_epoch SELECT id, created_at_epoch
FROM observations FROM observations
WHERE id <= ? ${o} WHERE id <= ? ${i}
ORDER BY id DESC ORDER BY id DESC
LIMIT ? LIMIT ?
`,g=` `,g=`
SELECT id, created_at_epoch SELECT id, created_at_epoch
FROM observations FROM observations
WHERE id >= ? ${o} WHERE id >= ? ${i}
ORDER BY id ASC ORDER BY id ASC
LIMIT ? LIMIT ?
`;try{let _=this.db.prepare(T).all(e,...i,t+1),a=this.db.prepare(g).all(e,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=` `;try{let _=this.db.prepare(b).all(e,...n,t+1),a=this.db.prepare(g).all(e,...n,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let b=`
SELECT created_at_epoch SELECT created_at_epoch
FROM observations FROM observations
WHERE created_at_epoch <= ? ${o} WHERE created_at_epoch <= ? ${i}
ORDER BY created_at_epoch DESC ORDER BY created_at_epoch DESC
LIMIT ? LIMIT ?
`,g=` `,g=`
SELECT created_at_epoch SELECT created_at_epoch
FROM observations FROM observations
WHERE created_at_epoch >= ? ${o} WHERE created_at_epoch >= ? ${i}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
LIMIT ? LIMIT ?
`;try{let _=this.db.prepare(T).all(s,...i,t),a=this.db.prepare(g).all(s,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let E=` `;try{let _=this.db.prepare(b).all(s,...n,t),a=this.db.prepare(g).all(s,...n,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let E=`
SELECT * SELECT *
FROM observations FROM observations
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o} WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
`,c=` `,c=`
SELECT * SELECT *
FROM session_summaries FROM session_summaries
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o} WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
`,b=` `,l=`
SELECT up.*, s.project, s.sdk_session_id SELECT up.*, s.project, s.sdk_session_id
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")} WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${i.replace("project","s.project")}
ORDER BY up.created_at_epoch ASC ORDER BY up.created_at_epoch ASC
`;try{let T=this.db.prepare(E).all(p,u,...i),g=this.db.prepare(c).all(p,u,...i),_=this.db.prepare(b).all(p,u,...i);return{observations:T,sessions:g.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function H(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(d,e,s={}){let t=H(d,e,s);return JSON.stringify(t)}import P from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function h(){try{let d=P.join(B(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse($(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let d=h();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function y(){if(await G())return;let d=h();throw new Error(`Worker service is not responding on port ${d}. `;try{let b=this.db.prepare(E).all(p,u,...n),g=this.db.prepare(c).all(p,u,...n),_=this.db.prepare(l).all(p,u,...n);return{observations:b,sessions:g.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(b){return console.error("[SessionStore] Error querying timeline records:",b.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function H(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(d,e,s={}){let t=H(d,e,s);return JSON.stringify(t)}import P from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as G}from"fs";var W=100;function h(){try{let d=P.join(B(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse(G(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function $(){try{let d=h();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function y(){if(await $())return;let d=h();throw new Error(`Worker service is not responding on port ${d}.
If you just updated the plugin, PM2's watch mode should restart automatically. If you just updated the plugin, PM2's watch mode should restart automatically.
If the problem persists, run: pm2 restart claude-mem-worker`)}var Y=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function K(d){if(!d)throw new Error("saveHook requires input");let{session_id:e,cwd:s,tool_name:t,tool_input:r,tool_response:n}=d;if(Y.has(t)){console.log(f("PostToolUse",!0));return}await y();let o=new R,i=o.createSDKSession(e,"",""),p=o.getPromptCounter(i);o.close();let u=S.formatTool(t,r),E=h();S.dataIn("HOOK",`PostToolUse: ${u}`,{sessionId:i,workerPort:E});try{let c=await fetch(`http://127.0.0.1:${E}/sessions/${i}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:r!==void 0?JSON.stringify(r):"{}",tool_response:n!==void 0?JSON.stringify(n):"{}",prompt_number:p,cwd:s||""}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let b=await c.text();throw S.failure("HOOK","Failed to send observation",{sessionId:i,status:c.status},b),new Error(`Failed to send observation to worker: ${c.status} ${b}`)}S.debug("HOOK","Observation sent successfully",{sessionId:i,toolName:t})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var L="";D.on("data",d=>L+=d);D.on("end",async()=>{let d=L?JSON.parse(L):void 0;await K(d)}); If the problem persists, run: pm2 restart claude-mem-worker`)}var Y=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function K(d){if(!d)throw new Error("saveHook requires input");let{session_id:e,cwd:s,tool_name:t,tool_input:r,tool_response:o}=d;if(Y.has(t)){console.log(f("PostToolUse",!0));return}await y();let i=new R,n=i.createSDKSession(e,"",""),p=i.getPromptCounter(n);i.close();let u=S.formatTool(t,r),E=h();S.dataIn("HOOK",`PostToolUse: ${u}`,{sessionId:n,workerPort:E});try{let c=await fetch(`http://127.0.0.1:${E}/sessions/${n}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:r!==void 0?JSON.stringify(r):"{}",tool_response:o!==void 0?JSON.stringify(o):"{}",prompt_number:p,cwd:s||""}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let l=await c.text();throw S.failure("HOOK","Failed to send observation",{sessionId:n,status:c.status},l),new Error(`Failed to send observation to worker: ${c.status} ${l}`)}S.debug("HOOK","Observation sent successfully",{sessionId:n,toolName:t})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var L="";D.on("data",d=>L+=d);D.on("end",async()=>{let d=L?JSON.parse(L):void 0;await K(d)});
+38 -38
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import{stdin as k}from"process";import{readFileSync as x,existsSync as U}from"fs";import B from"better-sqlite3";import{join as m,dirname as F,basename as ne}from"path";import{homedir as A}from"os";import{existsSync as de,mkdirSync as X}from"fs";import{fileURLToPath as H}from"url";function j(){return typeof __dirname<"u"?__dirname:F(H(import.meta.url))}var ce=j(),E=process.env.CLAUDE_MEM_DATA_DIR||m(A(),".claude-mem"),f=process.env.CLAUDE_CONFIG_DIR||m(A(),".claude"),ue=m(E,"archives"),_e=m(E,"logs"),me=m(E,"trash"),le=m(E,"backups"),Ee=m(E,"settings.json"),y=m(E,"claude-mem.db"),Te=m(E,"vector-db"),ge=m(f,"settings.json"),Se=m(f,"commands"),be=m(f,"CLAUDE.md");function C(i){X(i,{recursive:!0})}var O=(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))(O||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message} import{stdin as k}from"process";import{readFileSync as x,existsSync as U}from"fs";import j from"better-sqlite3";import{join as m,dirname as F,basename as ne}from"path";import{homedir as y}from"os";import{existsSync as de,mkdirSync as X}from"fs";import{fileURLToPath as H}from"url";function B(){return typeof __dirname<"u"?__dirname:F(H(import.meta.url))}var ce=B(),l=process.env.CLAUDE_MEM_DATA_DIR||m(y(),".claude-mem"),f=process.env.CLAUDE_CONFIG_DIR||m(y(),".claude"),ue=m(l,"archives"),_e=m(l,"logs"),me=m(l,"trash"),Ee=m(l,"backups"),le=m(l,"settings.json"),A=m(l,"claude-mem.db"),Te=m(l,"vector-db"),ge=m(f,"settings.json"),be=m(f,"commands"),Se=m(f,"CLAUDE.md");function v(a){X(a,{recursive:!0})}var O=(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))(O||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),o=O[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=` ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),o=O[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let l="";if(r){let{sessionId:T,sdkSessionId:S,correlationId:_,...p}=r;Object.keys(p).length>0&&(l=` {${Object.entries(p).map(([M,w])=>`${M}=${w}`).join(", ")}}`)}let b=`[${a}] [${o}] [${d}] ${c}${t}${l}${u}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},g=new N;var R=class{db;constructor(){C(E),this.db=new B(y),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(` `+JSON.stringify(n,null,2):u=" "+this.formatData(n));let E="";if(r){let{sessionId:T,sdkSessionId:S,correlationId:_,...p}=r;Object.keys(p).length>0&&(E=` {${Object.entries(p).map(([M,w])=>`${M}=${w}`).join(", ")}}`)}let b=`[${i}] [${o}] [${d}] ${c}${t}${E}${u}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},g=new N;var R=class{db;constructor(){v(l),this.db=new j(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions ( CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL, version INTEGER UNIQUE NOT NULL,
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts_fts(rowid, prompt_text) INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text); VALUES (new.id, new.prompt_text);
END; END;
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(` `),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;this.db.pragma("table_info(observations)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString())}catch(e){console.error("[SessionStore] Discovery tokens migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT SELECT
request, investigated, learned, completed, next_steps, request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at files_read, files_edited, notes, prompt_number, created_at
@@ -244,12 +244,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT * SELECT *
FROM observations FROM observations
WHERE id = ? WHERE id = ?
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",a=r?`LIMIT ${r}`:"",o=e.map(()=>"?").join(",");return this.db.prepare(` `).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",o=e.map(()=>"?").join(",");return this.db.prepare(`
SELECT * SELECT *
FROM observations FROM observations
WHERE id IN (${o}) WHERE id IN (${o})
ORDER BY created_at_epoch ${n} ORDER BY created_at_epoch ${n}
${a} ${i}
`).all(...e)}getSummaryForSession(e){return this.db.prepare(` `).all(...e)}getSummaryForSession(e){return this.db.prepare(`
SELECT SELECT
request, investigated, learned, completed, next_steps, request, investigated, learned, completed, next_steps,
@@ -262,7 +262,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
SELECT files_read, files_modified SELECT files_read, files_modified
FROM observations FROM observations
WHERE sdk_session_id = ? WHERE sdk_session_id = ?
`).all(e),r=new Set,n=new Set;for(let a of t){if(a.files_read)try{let o=JSON.parse(a.files_read);Array.isArray(o)&&o.forEach(d=>r.add(d))}catch{}if(a.files_modified)try{let o=JSON.parse(a.files_modified);Array.isArray(o)&&o.forEach(d=>n.add(d))}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 o=JSON.parse(i.files_read);Array.isArray(o)&&o.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let o=JSON.parse(i.files_modified);Array.isArray(o)&&o.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
SELECT id, claude_session_id, sdk_session_id, project, user_prompt SELECT id, claude_session_id, sdk_session_id, project, user_prompt
FROM sdk_sessions FROM sdk_sessions
WHERE id = ? WHERE id = ?
@@ -312,29 +312,29 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts INSERT INTO user_prompts
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch) (claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,a=n.getTime();this.db.prepare(` `).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r,n=0){let i=new Date,o=i.getTime();this.db.prepare(`
SELECT id FROM sdk_sessions WHERE sdk_session_id = ? SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
`).get(e)||(this.db.prepare(` `).get(e)||(this.db.prepare(`
INSERT INTO sdk_sessions INSERT INTO sdk_sessions
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active') VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,e,s,n.toISOString(),a),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(` `).run(e,e,s,i.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
INSERT INTO observations INSERT INTO observations
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
files_read, files_modified, prompt_number, created_at, created_at_epoch) files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),a);return{id:Number(u.lastInsertRowid),createdAtEpoch:a}}storeSummary(e,s,t,r){let n=new Date,a=n.getTime();this.db.prepare(` `).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n,i.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r,n=0){let i=new Date,o=i.getTime();this.db.prepare(`
SELECT id FROM sdk_sessions WHERE sdk_session_id = ? SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
`).get(e)||(this.db.prepare(` `).get(e)||(this.db.prepare(`
INSERT INTO sdk_sessions INSERT INTO sdk_sessions
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active') VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,e,s,n.toISOString(),a),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(` `).run(e,e,s,i.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
INSERT INTO session_summaries INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed, (sdk_session_id, project, request, investigated, learned, completed,
next_steps, notes, prompt_number, created_at, created_at_epoch) next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),a);return{id:Number(u.lastInsertRowid),createdAtEpoch: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,i.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
UPDATE sdk_sessions UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ? SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ? WHERE id = ?
@@ -342,12 +342,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
UPDATE sdk_sessions UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ? SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE id = ? WHERE id = ?
`).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",a=r?`LIMIT ${r}`:"",o=e.map(()=>"?").join(",");return this.db.prepare(` `).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",o=e.map(()=>"?").join(",");return this.db.prepare(`
SELECT * FROM session_summaries SELECT * FROM session_summaries
WHERE id IN (${o}) WHERE id IN (${o})
ORDER BY created_at_epoch ${n} ORDER BY created_at_epoch ${n}
${a} ${i}
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",a=r?`LIMIT ${r}`:"",o=e.map(()=>"?").join(",");return this.db.prepare(` `).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",o=e.map(()=>"?").join(",");return this.db.prepare(`
SELECT SELECT
up.*, up.*,
s.project, s.project,
@@ -356,63 +356,63 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.id IN (${o}) WHERE up.id IN (${o})
ORDER BY up.created_at_epoch ${n} ORDER BY up.created_at_epoch ${n}
${a} ${i}
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let a=n?"AND project = ?":"",o=n?[n]:[],d,c;if(e!==null){let T=` `).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let i=n?"AND project = ?":"",o=n?[n]:[],d,c;if(e!==null){let T=`
SELECT id, created_at_epoch SELECT id, created_at_epoch
FROM observations FROM observations
WHERE id <= ? ${a} WHERE id <= ? ${i}
ORDER BY id DESC ORDER BY id DESC
LIMIT ? LIMIT ?
`,S=` `,S=`
SELECT id, created_at_epoch SELECT id, created_at_epoch
FROM observations FROM observations
WHERE id >= ? ${a} WHERE id >= ? ${i}
ORDER BY id ASC ORDER BY id ASC
LIMIT ? LIMIT ?
`;try{let _=this.db.prepare(T).all(e,...o,t+1),p=this.db.prepare(S).all(e,...o,r+1);if(_.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,c=p.length>0?p[p.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=` `;try{let _=this.db.prepare(T).all(e,...o,t+1),p=this.db.prepare(S).all(e,...o,r+1);if(_.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,c=p.length>0?p[p.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
SELECT created_at_epoch SELECT created_at_epoch
FROM observations FROM observations
WHERE created_at_epoch <= ? ${a} WHERE created_at_epoch <= ? ${i}
ORDER BY created_at_epoch DESC ORDER BY created_at_epoch DESC
LIMIT ? LIMIT ?
`,S=` `,S=`
SELECT created_at_epoch SELECT created_at_epoch
FROM observations FROM observations
WHERE created_at_epoch >= ? ${a} WHERE created_at_epoch >= ? ${i}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
LIMIT ? LIMIT ?
`;try{let _=this.db.prepare(T).all(s,...o,t),p=this.db.prepare(S).all(s,...o,r+1);if(_.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,c=p.length>0?p[p.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let u=` `;try{let _=this.db.prepare(T).all(s,...o,t),p=this.db.prepare(S).all(s,...o,r+1);if(_.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,c=p.length>0?p[p.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let u=`
SELECT * SELECT *
FROM observations FROM observations
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${a} WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
`,l=` `,E=`
SELECT * SELECT *
FROM session_summaries FROM session_summaries
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${a} WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
`,b=` `,b=`
SELECT up.*, s.project, s.sdk_session_id SELECT up.*, s.project, s.sdk_session_id
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${a.replace("project","s.project")} WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${i.replace("project","s.project")}
ORDER BY up.created_at_epoch ASC ORDER BY up.created_at_epoch ASC
`;try{let T=this.db.prepare(u).all(d,c,...o),S=this.db.prepare(l).all(d,c,...o),_=this.db.prepare(b).all(d,c,...o);return{observations:T,sessions:S.map(p=>({id:p.id,sdk_session_id:p.sdk_session_id,project:p.project,request:p.request,completed:p.completed,next_steps:p.next_steps,created_at:p.created_at,created_at_epoch:p.created_at_epoch})),prompts:_.map(p=>({id:p.id,claude_session_id:p.claude_session_id,project:p.project,prompt:p.prompt_text,created_at:p.created_at,created_at_epoch:p.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function P(i,e,s){return i==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:i==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:i==="UserPromptSubmit"||i==="PostToolUse"?{continue:!0,suppressOutput:!0}:i==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(i,e,s={}){let t=P(i,e,s);return JSON.stringify(t)}import $ from"path";import{homedir as W}from"os";import{existsSync as G,readFileSync as Y}from"fs";var K=100;function h(){try{let i=$.join(W(),".claude-mem","settings.json");if(G(i)){let e=JSON.parse(Y(i,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function q(){try{let i=h();return(await fetch(`http://127.0.0.1:${i}/health`,{signal:AbortSignal.timeout(K)})).ok}catch{return!1}}async function D(){if(await q())return;let i=h();throw new Error(`Worker service is not responding on port ${i}. `;try{let T=this.db.prepare(u).all(d,c,...o),S=this.db.prepare(E).all(d,c,...o),_=this.db.prepare(b).all(d,c,...o);return{observations:T,sessions:S.map(p=>({id:p.id,sdk_session_id:p.sdk_session_id,project:p.project,request:p.request,completed:p.completed,next_steps:p.next_steps,created_at:p.created_at,created_at_epoch:p.created_at_epoch})),prompts:_.map(p=>({id:p.id,claude_session_id:p.claude_session_id,project:p.project,prompt:p.prompt_text,created_at:p.created_at,created_at_epoch:p.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function P(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function C(a,e,s={}){let t=P(a,e,s);return JSON.stringify(t)}import $ from"path";import{homedir as G}from"os";import{existsSync as W,readFileSync as Y}from"fs";var K=100;function h(){try{let a=$.join(G(),".claude-mem","settings.json");if(W(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function q(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(K)})).ok}catch{return!1}}async function D(){if(await q())return;let a=h();throw new Error(`Worker service is not responding on port ${a}.
If you just updated the plugin, PM2's watch mode should restart automatically. If you just updated the plugin, PM2's watch mode should restart automatically.
If the problem persists, run: pm2 restart claude-mem-worker`)}import{appendFileSync as V}from"fs";import{homedir as J}from"os";import{join as Q}from"path";var z=Q(J(),".claude-mem","silent.log");function I(i,e,s=""){let t=new Date().toISOString(),o=((new Error().stack||"").split(` If the problem persists, run: pm2 restart claude-mem-worker`)}import{appendFileSync as V}from"fs";import{homedir as J}from"os";import{join as Q}from"path";var z=Q(J(),".claude-mem","silent.log");function I(a,e,s=""){let t=new Date().toISOString(),o=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),d=o?`${o[1].split("/").pop()}:${o[2]}`:"unknown",c=`[${t}] [${d}] ${i}`;if(e!==void 0)try{c+=` ${JSON.stringify(e)}`}catch(u){c+=` [stringify error: ${u}]`}c+=` `)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),d=o?`${o[1].split("/").pop()}:${o[2]}`:"unknown",c=`[${t}] [${d}] ${a}`;if(e!==void 0)try{c+=` ${JSON.stringify(e)}`}catch(u){c+=` [stringify error: ${u}]`}c+=`
`;try{V(z,c)}catch(u){console.error("[silent-debug] Failed to write to log:",u)}return s}function Z(i){if(!i||!U(i))return"";try{let e=x(i,"utf-8").trim();if(!e)return"";let s=e.split(` `;try{V(z,c)}catch(u){console.error("[silent-debug] Failed to write to log:",u)}return s}function Z(a){if(!a||!U(a))return"";try{let e=x(a,"utf-8").trim();if(!e)return"";let s=e.split(`
`);for(let t=s.length-1;t>=0;t--)try{let r=JSON.parse(s[t]);if(r.type==="user"&&r.message?.content){let n=r.message.content;if(typeof n=="string")return n;if(Array.isArray(n))return n.filter(o=>o.type==="text").map(o=>o.text).join(` `);for(let t=s.length-1;t>=0;t--)try{let r=JSON.parse(s[t]);if(r.type==="user"&&r.message?.content){let n=r.message.content;if(typeof n=="string")return n;if(Array.isArray(n))return n.filter(o=>o.type==="text").map(o=>o.text).join(`
`)}}catch{continue}}catch(e){g.error("HOOK","Failed to read transcript",{transcriptPath:i},e)}return""}function ee(i){if(!i||!U(i))return"";try{let e=x(i,"utf-8").trim();if(!e)return"";let s=e.split(` `)}}catch{continue}}catch(e){g.error("HOOK","Failed to read transcript",{transcriptPath:a},e)}return""}function ee(a){if(!a||!U(a))return"";try{let e=x(a,"utf-8").trim();if(!e)return"";let s=e.split(`
`);for(let t=s.length-1;t>=0;t--)try{let r=JSON.parse(s[t]);if(r.type==="assistant"&&r.message?.content){let n="",a=r.message.content;return typeof a=="string"?n=a:Array.isArray(a)&&(n=a.filter(d=>d.type==="text").map(d=>d.text).join(` `);for(let t=s.length-1;t>=0;t--)try{let r=JSON.parse(s[t]);if(r.type==="assistant"&&r.message?.content){let n="",i=r.message.content;return typeof i=="string"?n=i:Array.isArray(i)&&(n=i.filter(d=>d.type==="text").map(d=>d.text).join(`
`)),n=n.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),n=n.replace(/\n{3,}/g,` `)),n=n.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),n=n.replace(/\n{3,}/g,`
`).trim(),n}}catch{continue}}catch(e){g.error("HOOK","Failed to read transcript",{transcriptPath:i},e)}return""}async function se(i){if(!i)throw new Error("summaryHook requires input");let{session_id:e}=i;await D();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t),n=s.db.prepare(` `).trim(),n}}catch{continue}}catch(e){g.error("HOOK","Failed to read transcript",{transcriptPath:a},e)}return""}async function se(a){if(!a)throw new Error("summaryHook requires input");let{session_id:e}=a;await D();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t),n=s.db.prepare(`
SELECT id, claude_session_id, sdk_session_id, project SELECT id, claude_session_id, sdk_session_id, project
FROM sdk_sessions WHERE id = ? FROM sdk_sessions WHERE id = ?
`).get(t),a=s.db.prepare(` `).get(t),i=s.db.prepare(`
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM observations FROM observations
WHERE sdk_session_id = ? WHERE sdk_session_id = ?
`).get(n?.sdk_session_id);I("[summary-hook] Session diagnostics",{claudeSessionId:e,sessionDbId:t,sdkSessionId:n?.sdk_session_id,project:n?.project,promptNumber:r,observationCount:a?.count||0,transcriptPath:i.transcript_path}),s.close();let o=h(),d=Z(i.transcript_path||""),c=ee(i.transcript_path||"");I("[summary-hook] Extracted messages",{hasLastUserMessage:!!d,hasLastAssistantMessage:!!c,lastAssistantPreview:c.substring(0,200),lastAssistantLength:c.length}),g.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:o,promptNumber:r,hasLastUserMessage:!!d,hasLastAssistantMessage:!!c});try{let u=await fetch(`http://127.0.0.1:${o}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r,last_user_message:d,last_assistant_message:c}),signal:AbortSignal.timeout(2e3)});if(!u.ok){let l=await u.text();throw g.failure("HOOK","Failed to generate summary",{sessionId:t,status:u.status},l),new Error(`Failed to request summary from worker: ${u.status} ${l}`)}g.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(u){throw u.cause?.code==="ECONNREFUSED"||u.name==="TimeoutError"||u.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):u}finally{await fetch(`http://127.0.0.1:${o}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})})}console.log(v("Stop",!0))}var L="";k.on("data",i=>L+=i);k.on("end",async()=>{let i=L?JSON.parse(L):void 0;await se(i)}); `).get(n?.sdk_session_id);I("[summary-hook] Session diagnostics",{claudeSessionId:e,sessionDbId:t,sdkSessionId:n?.sdk_session_id,project:n?.project,promptNumber:r,observationCount:i?.count||0,transcriptPath:a.transcript_path}),s.close();let o=h(),d=Z(a.transcript_path||""),c=ee(a.transcript_path||"");I("[summary-hook] Extracted messages",{hasLastUserMessage:!!d,hasLastAssistantMessage:!!c,lastAssistantPreview:c.substring(0,200),lastAssistantLength:c.length}),g.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:o,promptNumber:r,hasLastUserMessage:!!d,hasLastAssistantMessage:!!c});try{let u=await fetch(`http://127.0.0.1:${o}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r,last_user_message:d,last_assistant_message:c}),signal:AbortSignal.timeout(2e3)});if(!u.ok){let E=await u.text();throw g.failure("HOOK","Failed to generate summary",{sessionId:t,status:u.status},E),new Error(`Failed to request summary from worker: ${u.status} ${E}`)}g.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(u){throw u.cause?.code==="ECONNREFUSED"||u.name==="TimeoutError"||u.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):u}finally{await fetch(`http://127.0.0.1:${o}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})})}console.log(C("Stop",!0))}var L="";k.on("data",a=>L+=a);k.on("end",async()=>{let a=L?JSON.parse(L):void 0;await se(a)});
+6 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
import{execSync as p}from"child_process";import{join as r}from"path";import{homedir as s}from"os";import{existsSync as u}from"fs";import i from"path";import{homedir as a}from"os";import{existsSync as c,readFileSync as l}from"fs";function n(){try{let e=i.join(a(),".claude-mem","settings.json");if(c(e)){let t=JSON.parse(l(e,"utf-8")),o=parseInt(t.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(o))return o}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}var m=r(s(),".claude","plugins","marketplaces","thedotmack"),d=r(m,"node_modules");u(d)||(console.error(` import{execSync as u}from"child_process";import{join as r}from"path";import{homedir as s}from"os";import{existsSync as l}from"fs";import i from"path";import{homedir as a}from"os";import{existsSync as c,readFileSync as p}from"fs";function n(){try{let e=i.join(a(),".claude-mem","settings.json");if(c(e)){let t=JSON.parse(p(e,"utf-8")),o=parseInt(t.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(o))return o}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}var m=r(s(),".claude","plugins","marketplaces","thedotmack"),d=r(m,"node_modules");l(d)||(console.error(`
--- ---
\u{1F389} Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for \u{1F389} Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
user messages in Claude Code UI until a better method is provided. user messages in Claude Code UI until a better method is provided.
@@ -17,12 +17,15 @@ Dependencies have been installed in the background. This only happens once.
Thank you for installing Claude-Mem! Thank you for installing Claude-Mem!
This message was not added to your startup context, so you can continue working as normal. This message was not added to your startup context, so you can continue working as normal.
`),process.exit(3));try{let e=r(s(),".claude","plugins","marketplaces","thedotmack","plugin","scripts","context-hook.js"),t=p(`node "${e}" --colors`,{encoding:"utf8"}),o=n();console.error(` `),process.exit(3));try{let e=r(s(),".claude","plugins","marketplaces","thedotmack","plugin","scripts","context-hook.js"),t=u(`node "${e}" --colors`,{encoding:"utf8"}),o=n();console.error(`
\u{1F4DD} Claude-Mem Context Loaded \u{1F4DD} Claude-Mem Context Loaded
\u2139\uFE0F Note: This appears as stderr but is informational only \u2139\uFE0F Note: This appears as stderr but is informational only
`+t+` `+t+`
\u{1F4FA} Watch live in browser http://localhost:${o}/ (New! v5.1) \u{1F4AC} Feedback & Support
https://github.com/thedotmack/claude-mem/discussions/110
\u{1F4FA} Watch live in browser http://localhost:${o}/
`)}catch(e){console.error(`\u274C Failed to load context display: ${e}`)}process.exit(3); `)}catch(e){console.error(`\u274C Failed to load context display: ${e}`)}process.exit(3);
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -169,7 +169,7 @@ For guidelines on presenting search results to users, see [operations/formatting
- **Port:** Default 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`) - **Port:** Default 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`)
- **Response format:** Always JSON - **Response format:** Always JSON
- **Search engine:** FTS5 full-text search + structured filters - **Search engine:** ChromaDB semantic search (primary ranking) + SQLite FTS5 (fallback) + 90-day recency filter + temporal ordering (hybrid architecture)
- **All operations:** HTTP GET with query parameters - **All operations:** HTTP GET with query parameters
- **Worker:** PM2-managed background process - **Worker:** PM2-managed background process
@@ -1,4 +1,4 @@
# Search Observations (Full-Text) # Search Observations (Semantic + Full-Text Hybrid)
Search all observations using natural language queries. Search all observations using natural language queries.
@@ -17,7 +17,7 @@ curl -s "http://localhost:37777/api/search/observations?query=authentication&for
## Parameters ## Parameters
- **query** (required): Search terms (e.g., "authentication", "bug fix", "database migration") - **query** (required): Natural language search query - uses semantic search (ChromaDB) for ranking with SQLite FTS5 fallback (e.g., "authentication", "bug fix", "database migration")
- **format**: "index" (summary) or "full" (complete details). Default: "full" - **format**: "index" (summary) or "full" (complete details). Default: "full"
- **limit**: Number of results (default: 20, max: 100) - **limit**: Number of results (default: 20, max: 100)
- **project**: Filter by project name (optional) - **project**: Filter by project name (optional)
+112 -55
View File
@@ -71,6 +71,7 @@ interface Observation {
concepts: string | null; concepts: string | null;
files_read: string | null; files_read: string | null;
files_modified: string | null; files_modified: string | null;
discovery_tokens: number | null;
created_at: string; created_at: string;
created_at_epoch: number; created_at_epoch: number;
} }
@@ -130,12 +131,6 @@ function formatDate(dateStr: string): string {
}); });
} }
// Helper: Estimate token count for text
function estimateTokens(text: string | null): number {
if (!text) return 0;
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
}
// Helper: Convert absolute paths to relative paths // Helper: Convert absolute paths to relative paths
function toRelativePath(filePath: string, cwd: string): string { function toRelativePath(filePath: string, cwd: string): string {
if (path.isAbsolute(filePath)) { if (path.isAbsolute(filePath)) {
@@ -154,24 +149,6 @@ function renderSummaryField(label: string, value: string | null, color: string,
return [`**${label}**: ${value}`, '']; return [`**${label}**: ${value}`, ''];
} }
// Helper: Get all observations for given sessions
function getObservations(db: SessionStore, sessionIds: string[]): Observation[] {
if (sessionIds.length === 0) return [];
const placeholders = sessionIds.map(() => '?').join(',');
const observations = db.db.prepare(`
SELECT
id, sdk_session_id, type, title, subtitle, narrative,
facts, concepts, files_read, files_modified,
created_at, created_at_epoch
FROM observations
WHERE sdk_session_id IN (${placeholders})
ORDER BY created_at_epoch DESC
`).all(...sessionIds) as Observation[];
return observations;
}
/** /**
* Context Hook Main Logic * Context Hook Main Logic
*/ */
@@ -187,7 +164,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
const allObservations = db.db.prepare(` const allObservations = db.db.prepare(`
SELECT SELECT
id, sdk_session_id, type, title, subtitle, narrative, id, sdk_session_id, type, title, subtitle, narrative,
facts, concepts, files_read, files_modified, facts, concepts, files_read, files_modified, discovery_tokens,
created_at, created_at_epoch created_at, created_at_epoch
FROM observations FROM observations
WHERE project = ? WHERE project = ?
@@ -239,25 +216,74 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
if (timelineObs.length > 0) { if (timelineObs.length > 0) {
// Legend/Key // Legend/Key
if (useColors) { if (useColors) {
output.push(`${colors.dim}Legend: 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | 🧠 decision${colors.reset}`); output.push(`${colors.dim}Legend: 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | ⚖️ decision${colors.reset}`);
output.push('');
} else { } else {
output.push(`**Legend:** 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | 🧠 decision`); output.push(`**Legend:** 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | ⚖️ decision`);
output.push('');
} }
output.push('');
// Progressive Disclosure Usage Instructions // Column Key
if (useColors) { if (useColors) {
output.push(`${colors.dim}💡 Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${colors.reset}`); output.push(`${colors.bright}💡 Column Key${colors.reset}`);
output.push(`${colors.dim} → Use MCP search tools to fetch full observation details on-demand (Layer 2)${colors.reset}`); output.push(`${colors.dim} Read: Tokens to read this observation (cost to learn it now)${colors.reset}`);
output.push(`${colors.dim} → Prefer searching observations over re-reading code for past decisions and learnings${colors.reset}`); output.push(`${colors.dim} Work: Tokens spent on work that produced this record (🔍 research, 🛠️ building, ⚖️ deciding)${colors.reset}`);
output.push(`${colors.dim} → Critical types (🔴 bugfix, 🧠 decision) often worth fetching immediately${colors.reset}`); } else {
output.push(`💡 **Column Key**:`);
output.push(`- **Read**: Tokens to read this observation (cost to learn it now)`);
output.push(`- **Work**: Tokens spent on work that produced this record (🔍 research, 🛠️ building, ⚖️ deciding)`);
}
output.push('');
// Context Index Usage Instructions
if (useColors) {
output.push(`${colors.dim}💡 Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${colors.reset}`);
output.push('');
output.push(`${colors.dim}When you need implementation details, rationale, or debugging context:${colors.reset}`);
output.push(`${colors.dim} - Use the mem-search skill to fetch full observations on-demand${colors.reset}`);
output.push(`${colors.dim} - Critical types (🔴 bugfix, ⚖️ decision) often need detailed fetching${colors.reset}`);
output.push(`${colors.dim} - Trust this index over re-reading code for past decisions and learnings${colors.reset}`);
} else {
output.push(`💡 **Context Index:** This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.`);
output.push('');
output.push(`When you need implementation details, rationale, or debugging context:`);
output.push(`- Use the mem-search skill to fetch full observations on-demand`);
output.push(`- Critical types (🔴 bugfix, ⚖️ decision) often need detailed fetching`);
output.push(`- Trust this index over re-reading code for past decisions and learnings`);
}
output.push('');
// Section 1: Aggregate ROI Metrics
const totalObservations = observations.length;
const totalReadTokens = observations.reduce((sum, obs) => {
// Estimate read tokens from observation size
const obsSize = (obs.title?.length || 0) +
(obs.subtitle?.length || 0) +
(obs.narrative?.length || 0) +
JSON.stringify(obs.facts || []).length;
return sum + Math.ceil(obsSize / CHARS_PER_TOKEN_ESTIMATE);
}, 0);
const totalDiscoveryTokens = observations.reduce((sum, obs) => sum + (obs.discovery_tokens || 0), 0);
const savings = totalDiscoveryTokens - totalReadTokens;
const savingsPercent = totalDiscoveryTokens > 0
? Math.round((savings / totalDiscoveryTokens) * 100)
: 0;
// Display Context Economics section
if (useColors) {
output.push(`${colors.bright}${colors.cyan}📊 Context Economics${colors.reset}`);
output.push(`${colors.dim} Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)${colors.reset}`);
output.push(`${colors.dim} Work investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions${colors.reset}`);
if (totalDiscoveryTokens > 0) {
output.push(`${colors.green} Your savings: ${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)${colors.reset}`);
}
output.push(''); output.push('');
} else { } else {
output.push(`💡 **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts).`); output.push(`📊 **Context Economics**:`);
output.push(`- Use MCP search tools to fetch full observation details on-demand (Layer 2)`); output.push(`- Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)`);
output.push(`- Prefer searching observations over re-reading code for past decisions and learnings`); output.push(`- Work investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions`);
output.push(`- Critical types (🔴 bugfix, 🧠 decision) often worth fetching immediately`); if (totalDiscoveryTokens > 0) {
output.push(`- Your savings: ${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)`);
}
output.push(''); output.push('');
} }
@@ -380,8 +406,8 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
// Table header (markdown only) // Table header (markdown only)
if (!useColors) { if (!useColors) {
output.push(`| ID | Time | T | Title | Tokens |`); output.push(`| ID | Time | T | Title | Read | Work |`);
output.push(`|----|------|---|-------|--------|`); output.push(`|----|------|---|-------|------|------|`);
} }
currentFile = file; currentFile = file;
@@ -389,10 +415,11 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
lastTime = ''; lastTime = '';
} }
// Render observation row const time = formatTime(obs.created_at);
let icon = '•'; const title = obs.title || 'Untitled';
// Map observation type to emoji // Map observation type to emoji icon
let icon = '•';
switch (obs.type) { switch (obs.type) {
case 'bugfix': case 'bugfix':
icon = '🔴'; icon = '🔴';
@@ -410,15 +437,40 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
icon = '🔵'; icon = '🔵';
break; break;
case 'decision': case 'decision':
icon = '🧠'; icon = '⚖️';
break; break;
default: default:
icon = '•'; icon = '•';
} }
const time = formatTime(obs.created_at); // Section 2: Calculate read tokens (estimate from observation size)
const title = obs.title || 'Untitled'; const obsSize = (obs.title?.length || 0) +
const tokens = estimateTokens(obs.narrative); (obs.subtitle?.length || 0) +
(obs.narrative?.length || 0) +
JSON.stringify(obs.facts || []).length;
const readTokens = Math.ceil(obsSize / CHARS_PER_TOKEN_ESTIMATE);
// Get discovery tokens (handle old observations without this field)
const discoveryTokens = obs.discovery_tokens || 0;
// Map observation type to work emoji
let workEmoji = '🔍'; // default to research/discovery
switch (obs.type) {
case 'discovery':
workEmoji = '🔍'; // research/exploration
break;
case 'change':
case 'feature':
case 'bugfix':
case 'refactor':
workEmoji = '🛠️'; // building/modifying
break;
case 'decision':
workEmoji = '⚖️'; // decision-making
break;
}
const discoveryDisplay = discoveryTokens > 0 ? `${workEmoji} ${discoveryTokens.toLocaleString()}` : '-';
const showTime = time !== lastTime; const showTime = time !== lastTime;
const timeDisplay = showTime ? time : ''; const timeDisplay = showTime ? time : '';
@@ -426,10 +478,11 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
if (useColors) { if (useColors) {
const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length); const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length);
const tokensPart = tokens > 0 ? `${colors.dim}(~${tokens}t)${colors.reset}` : ''; const readPart = readTokens > 0 ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${tokensPart}`); const discoveryPart = discoveryTokens > 0 ? `${colors.dim}(${workEmoji} ${discoveryTokens.toLocaleString()}t)${colors.reset}` : '';
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${readPart} ${discoveryPart}`);
} else { } else {
output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${tokens} |`); output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${readTokens} | ${discoveryDisplay} |`);
} }
} }
} }
@@ -456,11 +509,15 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
output.push(...renderSummaryField('Next Steps', mostRecentSummary.next_steps, colors.magenta, useColors)); output.push(...renderSummaryField('Next Steps', mostRecentSummary.next_steps, colors.magenta, useColors));
} }
// Footer with MCP search instructions // Footer with token savings message
if (useColors) { if (totalDiscoveryTokens > 0 && savings > 0) {
output.push(`${colors.dim}Use claude-mem MCP search to access records with the given ID${colors.reset}`); const workTokensK = Math.round(totalDiscoveryTokens / 1000);
} else { output.push('');
output.push(`*Use claude-mem MCP search to access records with the given ID*`); if (useColors) {
output.push(`${colors.dim}💰 Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use claude-mem search to access memories by ID instead of re-reading files.${colors.reset}`);
} else {
output.push(`💰 Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use claude-mem search to access memories by ID instead of re-reading files.`);
}
} }
} }
+2 -1
View File
@@ -52,7 +52,8 @@ try {
"\n\n📝 Claude-Mem Context Loaded\n" + "\n\n📝 Claude-Mem Context Loaded\n" +
" ️ Note: This appears as stderr but is informational only\n\n" + " ️ Note: This appears as stderr but is informational only\n\n" +
output + output +
`\n\n📺 Watch live in browser http://localhost:${port}/ (New! v5.1)\n` "\n\n💬 Feedback & Support\nhttps://github.com/thedotmack/claude-mem/discussions/110\n" +
`\n📺 Watch live in browser http://localhost:${port}/\n`
); );
} catch (error) { } catch (error) {
+6 -6
View File
@@ -346,9 +346,9 @@ const filterSchema = z.object({
const tools = [ const tools = [
{ {
name: 'search_observations', name: 'search_observations',
description: 'Search observations using full-text search across titles, narratives, facts, and concepts. IMPORTANT: Always use index format first (default) to get an overview with minimal token usage, then use format: "full" only for specific items of interest.', description: 'Search observations using hybrid semantic + full-text search (ChromaDB primary, SQLite FTS5 fallback). IMPORTANT: Always use index format first (default) to get an overview with minimal token usage, then use format: "full" only for specific items of interest.',
inputSchema: z.object({ inputSchema: z.object({
query: z.string().describe('Search query for FTS5 full-text search'), query: z.string().describe('Natural language search query (semantic ranking via ChromaDB, FTS5 fallback)'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'),
...filterSchema.shape ...filterSchema.shape
}), }),
@@ -434,9 +434,9 @@ const tools = [
}, },
{ {
name: 'search_sessions', name: 'search_sessions',
description: 'Search session summaries using full-text search across requests, completions, learnings, and notes. IMPORTANT: Always use index format first (default) to get an overview with minimal token usage, then use format: "full" only for specific items of interest.', description: 'Search session summaries using hybrid semantic + full-text search (ChromaDB primary, SQLite FTS5 fallback). IMPORTANT: Always use index format first (default) to get an overview with minimal token usage, then use format: "full" only for specific items of interest.',
inputSchema: z.object({ inputSchema: z.object({
query: z.string().describe('Search query for FTS5 full-text search'), query: z.string().describe('Natural language search query (semantic ranking via ChromaDB, FTS5 fallback)'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateRange: z.object({
@@ -1000,9 +1000,9 @@ const tools = [
}, },
{ {
name: 'search_user_prompts', name: 'search_user_prompts',
description: 'Search raw user prompts with full-text search. Use this to find what the user actually said/requested across all sessions. IMPORTANT: Always use index format first (default) to get an overview with minimal token usage, then use format: "full" only for specific items of interest.', description: 'Search raw user prompts using hybrid semantic + full-text search (ChromaDB primary, SQLite FTS5 fallback). Use this to find what the user actually said/requested across all sessions. IMPORTANT: Always use index format first (default) to get an overview with minimal token usage, then use format: "full" only for specific items of interest.',
inputSchema: z.object({ inputSchema: z.object({
query: z.string().describe('Search query for FTS5 full-text search'), query: z.string().describe('Natural language search query (semantic ranking via ChromaDB, FTS5 fallback)'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for truncated prompts/dates (default, RECOMMENDED for initial search), "full" for complete prompt text (use only after reviewing index results)'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for truncated prompts/dates (default, RECOMMENDED for initial search), "full" for complete prompt text (use only after reviewing index results)'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateRange: z.object({
+6 -4
View File
@@ -264,6 +264,7 @@ export class SessionSearch {
const sql = ` const sql = `
SELECT SELECT
o.*, o.*,
o.discovery_tokens,
observations_fts.rank as rank observations_fts.rank as rank
FROM observations o FROM observations o
JOIN observations_fts ON o.id = observations_fts.rowid JOIN observations_fts ON o.id = observations_fts.rowid
@@ -326,6 +327,7 @@ export class SessionSearch {
const sql = ` const sql = `
SELECT SELECT
s.*, s.*,
s.discovery_tokens,
session_summaries_fts.rank as rank session_summaries_fts.rank as rank
FROM session_summaries s FROM session_summaries s
JOIN session_summaries_fts ON s.id = session_summaries_fts.rowid JOIN session_summaries_fts ON s.id = session_summaries_fts.rowid
@@ -368,7 +370,7 @@ export class SessionSearch {
const orderClause = this.buildOrderClause(orderBy, false); const orderClause = this.buildOrderClause(orderBy, false);
const sql = ` const sql = `
SELECT o.* SELECT o.*, o.discovery_tokens
FROM observations o FROM observations o
WHERE ${filterClause} WHERE ${filterClause}
${orderClause} ${orderClause}
@@ -396,7 +398,7 @@ export class SessionSearch {
const orderClause = this.buildOrderClause(orderBy, false); const orderClause = this.buildOrderClause(orderBy, false);
const observationsSql = ` const observationsSql = `
SELECT o.* SELECT o.*, o.discovery_tokens
FROM observations o FROM observations o
WHERE ${filterClause} WHERE ${filterClause}
${orderClause} ${orderClause}
@@ -440,7 +442,7 @@ export class SessionSearch {
sessionParams.push(`%${filePath}%`, `%${filePath}%`); sessionParams.push(`%${filePath}%`, `%${filePath}%`);
const sessionsSql = ` const sessionsSql = `
SELECT s.* SELECT s.*, s.discovery_tokens
FROM session_summaries s FROM session_summaries s
WHERE ${baseConditions.join(' AND ')} WHERE ${baseConditions.join(' AND ')}
ORDER BY s.created_at_epoch DESC ORDER BY s.created_at_epoch DESC
@@ -470,7 +472,7 @@ export class SessionSearch {
const orderClause = this.buildOrderClause(orderBy, false); const orderClause = this.buildOrderClause(orderBy, false);
const sql = ` const sql = `
SELECT o.* SELECT o.*, o.discovery_tokens
FROM observations o FROM observations o
WHERE ${filterClause} WHERE ${filterClause}
${orderClause} ${orderClause}
+45 -6
View File
@@ -28,6 +28,7 @@ export class SessionStore {
this.addObservationHierarchicalFields(); this.addObservationHierarchicalFields();
this.makeObservationsTextNullable(); this.makeObservationsTextNullable();
this.createUserPromptsTable(); this.createUserPromptsTable();
this.ensureDiscoveryTokensColumn();
} }
/** /**
@@ -492,6 +493,40 @@ export class SessionStore {
} }
} }
/**
* Ensure discovery_tokens column exists (migration 7)
*/
private ensureDiscoveryTokensColumn(): void {
try {
// Check if migration already applied
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(7) as {version: number} | undefined;
if (applied) return;
// Check if discovery_tokens column exists in observations table
const observationsInfo = this.db.pragma('table_info(observations)');
const obsHasDiscoveryTokens = (observationsInfo as any[]).some((col: any) => col.name === 'discovery_tokens');
if (!obsHasDiscoveryTokens) {
this.db.exec('ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
console.error('[SessionStore] Added discovery_tokens column to observations table');
}
// Check if discovery_tokens column exists in session_summaries table
const summariesInfo = this.db.pragma('table_info(session_summaries)');
const sumHasDiscoveryTokens = (summariesInfo as any[]).some((col: any) => col.name === 'discovery_tokens');
if (!sumHasDiscoveryTokens) {
this.db.exec('ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
console.error('[SessionStore] Added discovery_tokens column to session_summaries table');
}
// Record migration
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(7, new Date().toISOString());
} catch (error: any) {
console.error('[SessionStore] Discovery tokens migration error:', error.message);
}
}
/** /**
* Get recent session summaries for a project * Get recent session summaries for a project
*/ */
@@ -1074,7 +1109,8 @@ export class SessionStore {
files_read: string[]; files_read: string[];
files_modified: string[]; files_modified: string[];
}, },
promptNumber?: number promptNumber?: number,
discoveryTokens: number = 0
): { id: number; createdAtEpoch: number } { ): { id: number; createdAtEpoch: number } {
const now = new Date(); const now = new Date();
const nowEpoch = now.getTime(); const nowEpoch = now.getTime();
@@ -1105,8 +1141,8 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO observations INSERT INTO observations
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
files_read, files_modified, prompt_number, created_at, created_at_epoch) files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`); `);
const result = stmt.run( const result = stmt.run(
@@ -1121,6 +1157,7 @@ export class SessionStore {
JSON.stringify(observation.files_read), JSON.stringify(observation.files_read),
JSON.stringify(observation.files_modified), JSON.stringify(observation.files_modified),
promptNumber || null, promptNumber || null,
discoveryTokens,
now.toISOString(), now.toISOString(),
nowEpoch nowEpoch
); );
@@ -1146,7 +1183,8 @@ export class SessionStore {
next_steps: string; next_steps: string;
notes: string | null; notes: string | null;
}, },
promptNumber?: number promptNumber?: number,
discoveryTokens: number = 0
): { id: number; createdAtEpoch: number } { ): { id: number; createdAtEpoch: number } {
const now = new Date(); const now = new Date();
const nowEpoch = now.getTime(); const nowEpoch = now.getTime();
@@ -1177,8 +1215,8 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO session_summaries INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed, (sdk_session_id, project, request, investigated, learned, completed,
next_steps, notes, prompt_number, created_at, created_at_epoch) next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`); `);
const result = stmt.run( const result = stmt.run(
@@ -1191,6 +1229,7 @@ export class SessionStore {
summary.next_steps, summary.next_steps,
summary.notes, summary.notes,
promptNumber || null, promptNumber || null,
discoveryTokens,
now.toISOString(), now.toISOString(),
nowEpoch nowEpoch
); );
+26 -1
View File
@@ -471,6 +471,30 @@ export const migration006: Migration = {
} }
}; };
/**
* Migration 007 - Add discovery_tokens column for ROI metrics
* Tracks token cost of discovering/creating each observation and summary
*/
export const migration007: Migration = {
version: 7,
up: (db: Database) => {
// Add discovery_tokens to observations table
db.run(`ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0`);
// Add discovery_tokens to session_summaries table
db.run(`ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0`);
console.log('✅ Added discovery_tokens columns for ROI tracking');
},
down: (db: Database) => {
// Note: SQLite doesn't support DROP COLUMN in all versions
// In production, would need to recreate tables without these columns
console.log('⚠️ Warning: SQLite ALTER TABLE DROP COLUMN not fully supported');
console.log('⚠️ To rollback, manually recreate the observations and session_summaries tables');
}
};
/** /**
* All migrations in order * All migrations in order
*/ */
@@ -480,5 +504,6 @@ export const migrations: Migration[] = [
migration003, migration003,
migration004, migration004,
migration005, migration005,
migration006 migration006,
migration007
]; ];
+2
View File
@@ -215,6 +215,7 @@ export interface ObservationRow {
files_read: string | null; // JSON array files_read: string | null; // JSON array
files_modified: string | null; // JSON array files_modified: string | null; // JSON array
prompt_number: number | null; prompt_number: number | null;
discovery_tokens: number; // ROI metrics: tokens spent discovering this observation
created_at: string; created_at: string;
created_at_epoch: number; created_at_epoch: number;
} }
@@ -232,6 +233,7 @@ export interface SessionSummaryRow {
files_edited: string | null; // JSON array files_edited: string | null; // JSON array
notes: string | null; notes: string | null;
prompt_number: number | null; prompt_number: number | null;
discovery_tokens: number; // ROI metrics: cumulative tokens spent in this session
created_at: string; created_at: string;
created_at_epoch: number; created_at_epoch: number;
} }
+8 -2
View File
@@ -36,6 +36,7 @@ interface StoredObservation {
files_read: string | null; // JSON files_read: string | null; // JSON
files_modified: string | null; // JSON files_modified: string | null; // JSON
prompt_number: number; prompt_number: number;
discovery_tokens: number; // ROI metrics
created_at: string; created_at: string;
created_at_epoch: number; created_at_epoch: number;
} }
@@ -51,6 +52,7 @@ interface StoredSummary {
next_steps: string | null; next_steps: string | null;
notes: string | null; notes: string | null;
prompt_number: number; prompt_number: number;
discovery_tokens: number; // ROI metrics
created_at: string; created_at: string;
created_at_epoch: number; created_at_epoch: number;
} }
@@ -345,7 +347,8 @@ export class ChromaSync {
project: string, project: string,
obs: ParsedObservation, obs: ParsedObservation,
promptNumber: number, promptNumber: number,
createdAtEpoch: number createdAtEpoch: number,
discoveryTokens: number = 0
): Promise<void> { ): Promise<void> {
// Convert ParsedObservation to StoredObservation format // Convert ParsedObservation to StoredObservation format
const stored: StoredObservation = { const stored: StoredObservation = {
@@ -362,6 +365,7 @@ export class ChromaSync {
files_read: JSON.stringify(obs.files_read), files_read: JSON.stringify(obs.files_read),
files_modified: JSON.stringify(obs.files_modified), files_modified: JSON.stringify(obs.files_modified),
prompt_number: promptNumber, prompt_number: promptNumber,
discovery_tokens: discoveryTokens,
created_at: new Date(createdAtEpoch * 1000).toISOString(), created_at: new Date(createdAtEpoch * 1000).toISOString(),
created_at_epoch: createdAtEpoch created_at_epoch: createdAtEpoch
}; };
@@ -387,7 +391,8 @@ export class ChromaSync {
project: string, project: string,
summary: ParsedSummary, summary: ParsedSummary,
promptNumber: number, promptNumber: number,
createdAtEpoch: number createdAtEpoch: number,
discoveryTokens: number = 0
): Promise<void> { ): Promise<void> {
// Convert ParsedSummary to StoredSummary format // Convert ParsedSummary to StoredSummary format
const stored: StoredSummary = { const stored: StoredSummary = {
@@ -401,6 +406,7 @@ export class ChromaSync {
next_steps: summary.next_steps, next_steps: summary.next_steps,
notes: summary.notes, notes: summary.notes,
prompt_number: promptNumber, prompt_number: promptNumber,
discovery_tokens: discoveryTokens,
created_at: new Date(createdAtEpoch * 1000).toISOString(), created_at: new Date(createdAtEpoch * 1000).toISOString(),
created_at_epoch: createdAtEpoch created_at_epoch: createdAtEpoch
}; };
+2
View File
@@ -19,6 +19,8 @@ export interface ActiveSession {
generatorPromise: Promise<void> | null; generatorPromise: Promise<void> | null;
lastPromptNumber: number; lastPromptNumber: number;
startTime: number; startTime: number;
cumulativeInputTokens: number; // Track input tokens for discovery cost
cumulativeOutputTokens: number; // Track output tokens for discovery cost
} }
export interface PendingMessage { export interface PendingMessage {
+40 -7
View File
@@ -85,6 +85,34 @@ export class SDKAgent {
const responseSize = textContent.length; const responseSize = textContent.length;
// Capture token state BEFORE updating (for delta calculation)
const tokensBeforeResponse = session.cumulativeInputTokens + session.cumulativeOutputTokens;
// Extract and track token usage
const usage = message.message.usage;
if (usage) {
session.cumulativeInputTokens += usage.input_tokens || 0;
session.cumulativeOutputTokens += usage.output_tokens || 0;
// Cache creation counts as discovery, cache read doesn't
if (usage.cache_creation_input_tokens) {
session.cumulativeInputTokens += usage.cache_creation_input_tokens;
}
logger.debug('SDK', 'Token usage captured', {
sessionId: session.sessionDbId,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
cacheCreation: usage.cache_creation_input_tokens || 0,
cacheRead: usage.cache_read_input_tokens || 0,
cumulativeInput: session.cumulativeInputTokens,
cumulativeOutput: session.cumulativeOutputTokens
});
}
// Calculate discovery tokens (delta for this response only)
const discoveryTokens = (session.cumulativeInputTokens + session.cumulativeOutputTokens) - tokensBeforeResponse;
// Only log non-empty responses (filter out noise) // Only log non-empty responses (filter out noise)
if (responseSize > 0) { if (responseSize > 0) {
const truncatedResponse = responseSize > 100 const truncatedResponse = responseSize > 100
@@ -95,8 +123,8 @@ export class SDKAgent {
promptNumber: session.lastPromptNumber promptNumber: session.lastPromptNumber
}, truncatedResponse); }, truncatedResponse);
// Parse and process response // Parse and process response with discovery token delta
await this.processSDKResponse(session, textContent, worker); await this.processSDKResponse(session, textContent, worker, discoveryTokens);
} }
} }
@@ -218,8 +246,9 @@ export class SDKAgent {
/** /**
* Process SDK response text (parse XML, save to database, sync to Chroma) * Process SDK response text (parse XML, save to database, sync to Chroma)
* @param discoveryTokens - Token cost for discovering this response (delta, not cumulative)
*/ */
private async processSDKResponse(session: ActiveSession, text: string, worker?: any): Promise<void> { private async processSDKResponse(session: ActiveSession, text: string, worker: any | undefined, discoveryTokens: number): Promise<void> {
// Parse observations // Parse observations
const observations = parseObservations(text, session.claudeSessionId); const observations = parseObservations(text, session.claudeSessionId);
@@ -229,7 +258,8 @@ export class SDKAgent {
session.claudeSessionId, session.claudeSessionId,
session.project, session.project,
obs, obs,
session.lastPromptNumber session.lastPromptNumber,
discoveryTokens
); );
// Log observation details // Log observation details
@@ -253,7 +283,8 @@ export class SDKAgent {
session.project, session.project,
obs, obs,
session.lastPromptNumber, session.lastPromptNumber,
createdAtEpoch createdAtEpoch,
discoveryTokens
).then(() => { ).then(() => {
const chromaDuration = Date.now() - chromaStart; const chromaDuration = Date.now() - chromaStart;
logger.debug('CHROMA', 'Observation synced', { logger.debug('CHROMA', 'Observation synced', {
@@ -305,7 +336,8 @@ export class SDKAgent {
session.claudeSessionId, session.claudeSessionId,
session.project, session.project,
summary, summary,
session.lastPromptNumber session.lastPromptNumber,
discoveryTokens
); );
// Log summary details // Log summary details
@@ -326,7 +358,8 @@ export class SDKAgent {
session.project, session.project,
summary, summary,
session.lastPromptNumber, session.lastPromptNumber,
createdAtEpoch createdAtEpoch,
discoveryTokens
).then(() => { ).then(() => {
const chromaDuration = Date.now() - chromaStart; const chromaDuration = Date.now() - chromaStart;
logger.debug('CHROMA', 'Summary synced', { logger.debug('CHROMA', 'Summary synced', {
+3 -1
View File
@@ -89,7 +89,9 @@ export class SessionManager {
abortController: new AbortController(), abortController: new AbortController(),
generatorPromise: null, generatorPromise: null,
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptCounter(sessionDbId), lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptCounter(sessionDbId),
startTime: Date.now() startTime: Date.now(),
cumulativeInputTokens: 0,
cumulativeOutputTokens: 0
}; };
this.sessions.set(sessionDbId, session); this.sessions.set(sessionDbId, session);