Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5482052c16 | |||
| ccb7001ff0 | |||
| edc20a12a1 | |||
| 45c35c6bdd | |||
| 47ee0838e3 | |||
| 809c9e6639 | |||
| 9646527d66 | |||
| f27c73b469 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.2",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
**Current Version**: 5.2.0
|
||||
**Current Version**: 5.2.2
|
||||
|
||||
## Critical Architecture Knowledge
|
||||
|
||||
|
||||
@@ -27,6 +27,16 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/thedotmack/claude-mem">
|
||||
<picture>
|
||||
<img src="docs/cm-preview.gif" alt="Claude-Mem Preview" width="800">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#quick-start">Quick Start</a> •
|
||||
<a href="#how-it-works">How It Works</a> •
|
||||
@@ -56,6 +66,7 @@ Start a new Claude Code session in the terminal and enter the following commands
|
||||
Restart Claude Code. Context from previous sessions will automatically appear in new sessions.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- 🧠 **Persistent Memory** - Context survives across sessions
|
||||
- 📊 **Progressive Disclosure** - Layered memory retrieval with token cost visibility
|
||||
- 🔍 **9 Search Tools** - Query your project history via MCP
|
||||
@@ -70,21 +81,25 @@ Restart Claude Code. Context from previous sessions will automatically appear in
|
||||
📚 **[View Full Documentation](docs/)** - Browse markdown docs on GitHub
|
||||
|
||||
💻 **Local Preview**: Run Mintlify docs locally:
|
||||
|
||||
```bash
|
||||
cd docs
|
||||
npx mintlify dev
|
||||
```
|
||||
|
||||
### Getting Started
|
||||
|
||||
- **[Installation Guide](docs/installation.mdx)** - Quick start & advanced installation
|
||||
- **[Usage Guide](docs/usage/getting-started.mdx)** - How Claude-Mem works automatically
|
||||
- **[MCP Search Tools](docs/usage/search-tools.mdx)** - Query your project history
|
||||
|
||||
### Best Practices
|
||||
|
||||
- **[Context Engineering](docs/context-engineering.mdx)** - AI agent context optimization principles
|
||||
- **[Progressive Disclosure](docs/progressive-disclosure.mdx)** - Philosophy behind Claude-Mem's context priming strategy
|
||||
|
||||
### Architecture
|
||||
|
||||
- **[Overview](docs/architecture/overview.mdx)** - System components & data flow
|
||||
- **[Architecture Evolution](docs/architecture-evolution.mdx)** - The journey from v3 to v5
|
||||
- **[Hooks Architecture](docs/hooks-architecture.mdx)** - How Claude-Mem uses lifecycle hooks
|
||||
@@ -95,6 +110,7 @@ npx mintlify dev
|
||||
- **[Viewer UI](docs/VIEWER.md)** - Web-based memory stream visualization
|
||||
|
||||
### Configuration & Development
|
||||
|
||||
- **[Configuration](docs/configuration.mdx)** - Environment variables & settings
|
||||
- **[Development](docs/development.mdx)** - Building, testing, contributing
|
||||
- **[Troubleshooting](docs/troubleshooting.mdx)** - Common issues & solutions
|
||||
@@ -126,6 +142,7 @@ npx mintlify dev
|
||||
```
|
||||
|
||||
**Core Components:**
|
||||
|
||||
1. **7 Lifecycle Hook Scripts** - smart-install, context-hook, user-message-hook, new-hook, save-hook, summary-hook, cleanup-hook
|
||||
2. **Worker Service** - HTTP API on port 37777 with web viewer UI, managed by PM2
|
||||
3. **SQLite Database** - Stores sessions, observations, summaries with FTS5 full-text search
|
||||
@@ -151,6 +168,7 @@ Claude-Mem provides 9 specialized search tools:
|
||||
9. **get_timeline_by_query** - Search for observations and get timeline context around best match
|
||||
|
||||
**Example Queries:**
|
||||
|
||||
```
|
||||
search_observations with query="authentication" and type="decision"
|
||||
find_by_file with filePath="worker-service.ts"
|
||||
@@ -167,12 +185,14 @@ See [MCP Search Tools Guide](docs/usage/search-tools.mdx) for detailed examples.
|
||||
## What's New in v5.1.2
|
||||
|
||||
**🎨 Theme Toggle (v5.1.2):**
|
||||
|
||||
- Light/dark mode support in viewer UI
|
||||
- System preference detection
|
||||
- Persistent theme settings across sessions
|
||||
- Smooth transitions between themes
|
||||
|
||||
**🖥️ Web-Based Viewer UI (v5.1.0):**
|
||||
|
||||
- Real-time memory stream visualization at http://localhost:37777
|
||||
- Server-Sent Events (SSE) for instant updates
|
||||
- Infinite scroll pagination with automatic deduplication
|
||||
@@ -181,11 +201,13 @@ See [MCP Search Tools Guide](docs/usage/search-tools.mdx) for detailed examples.
|
||||
- Auto-reconnection with exponential backoff
|
||||
|
||||
**⚡ Smart Install Caching (v5.0.3):**
|
||||
|
||||
- Eliminated redundant npm installs on every session (2-5s → 10ms)
|
||||
- Caches version in `.install-version` file
|
||||
- Only runs npm install when needed (first time, version change, missing deps)
|
||||
|
||||
**🔍 Hybrid Search Architecture (v5.0.0):**
|
||||
|
||||
- Chroma vector database for semantic search
|
||||
- Combined with FTS5 keyword search
|
||||
- Intelligent context retrieval with 90-day recency filtering
|
||||
@@ -206,6 +228,7 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
## Key Benefits
|
||||
|
||||
### Progressive Disclosure Context
|
||||
|
||||
- **Layered memory retrieval** mirrors human memory patterns
|
||||
- **Layer 1 (Index)**: See what observations exist with token costs at session start
|
||||
- **Layer 2 (Details)**: Fetch full narratives on-demand via MCP search
|
||||
@@ -214,21 +237,25 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
- **Type indicators**: Visual cues (🔴 critical, 🟤 decision, 🔵 informational) highlight observation importance
|
||||
|
||||
### Automatic Memory
|
||||
|
||||
- Context automatically injected when Claude starts
|
||||
- No manual commands or configuration needed
|
||||
- Works transparently in the background
|
||||
|
||||
### Full History Search
|
||||
|
||||
- Search across all sessions and observations
|
||||
- FTS5 full-text search for fast queries
|
||||
- Citations link back to specific observations
|
||||
|
||||
### Structured Observations
|
||||
|
||||
- AI-powered extraction of learnings
|
||||
- Categorized by type (decision, bugfix, feature, etc.)
|
||||
- Tagged with concepts and file references
|
||||
|
||||
### Multi-Prompt Sessions
|
||||
|
||||
- Sessions span multiple user prompts
|
||||
- Context preserved across `/clear` commands
|
||||
- Track entire conversation threads
|
||||
@@ -238,11 +265,13 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
## Configuration
|
||||
|
||||
**Model Selection:**
|
||||
|
||||
```bash
|
||||
./claude-mem-settings.sh
|
||||
```
|
||||
|
||||
**Environment Variables:**
|
||||
|
||||
- `CLAUDE_MEM_MODEL` - AI model for processing (default: claude-sonnet-4-5)
|
||||
- `CLAUDE_MEM_WORKER_PORT` - Worker port (default: 37777)
|
||||
- `CLAUDE_MEM_DATA_DIR` - Data directory override (dev only)
|
||||
@@ -277,6 +306,7 @@ See [Development Guide](docs/development.mdx) for detailed instructions.
|
||||
## Troubleshooting
|
||||
|
||||
**Common Issues:**
|
||||
|
||||
- Worker not starting → `npm run worker:restart`
|
||||
- No context appearing → `npm run test:context`
|
||||
- Database issues → `sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"`
|
||||
@@ -309,6 +339,7 @@ Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
|
||||
See the [LICENSE](LICENSE) file for full details.
|
||||
|
||||
**What This Means:**
|
||||
|
||||
- You can use, modify, and distribute this software freely
|
||||
- If you modify and deploy on a network server, you must make your source code available
|
||||
- Derivative works must also be licensed under AGPL-3.0
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.2",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.2",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as I}from"process";import 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(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 m="";n!=null&&(this.level===0&&typeof n=="object"?m=`
|
||||
`+JSON.stringify(n,null,2):m=" "+this.formatData(n));let T="";if(r){let{sessionId:u,sdkSessionId:b,correlationId:p,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([y,D])=>`${y}=${D}`).join(", ")}}`)}let g=`[${o}] [${i}] [${d}] ${_}${t}${T}${m}`;e===3?console.error(g):console.log(g)}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 S=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):m=" "+this.formatData(n));let T="";if(r){let{sessionId:u,sdkSessionId:b,correlationId:p,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([y,D])=>`${y}=${D}`).join(", ")}}`)}let S=`[${o}] [${i}] [${d}] ${_}${t}${T}${m}`;e===3?console.error(S):console.log(S)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var g=class{db;constructor(){L(l),this.db=new 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(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -216,6 +216,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
@@ -390,11 +391,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,g=`
|
||||
`,S=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let u=this.db.prepare(m).all(d,_,...i),b=this.db.prepare(T).all(d,_,...i),p=this.db.prepare(g).all(d,_,...i);return{observations:u,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:p.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(u){return console.error("[SessionStore] Error querying timeline records:",u.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 c=X.join(F(),".claude-mem","settings.json");if(B(c)){let e=JSON.parse(j(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 v(c){console.error("[claude-mem cleanup] Hook fired",{input:c?{session_id:c.session_id,cwd:c.cwd,reason:c.reason}:null}),c||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=c;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new S,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 c="";I.on("data",e=>c+=e),I.on("end",async()=>{let e=c?JSON.parse(c):void 0;await v(e)})}
|
||||
`;try{let u=this.db.prepare(m).all(d,_,...i),b=this.db.prepare(T).all(d,_,...i),p=this.db.prepare(S).all(d,_,...i);return{observations:u,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:p.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(u){return console.error("[SessionStore] Error querying timeline records:",u.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 c=X.join(F(),".claude-mem","settings.json");if(B(c)){let e=JSON.parse(j(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 v(c){console.error("[claude-mem cleanup] Hook fired",{input:c?{session_id:c.session_id,cwd:c.cwd,reason:c.reason}:null}),c||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=c;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close();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 c="";I.on("data",e=>c+=e),I.on("end",async()=>{let e=c?JSON.parse(c):void 0;await v(e)})}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import X from"path";import{stdin as w}from"process";import se from"better-sqlite3";import{join as S,dirname as Q,basename as _e}from"path";import{homedir as j}from"os";import{existsSync as Ee,mkdirSync as z}from"fs";import{fileURLToPath as Z}from"url";function ee(){return typeof __dirname<"u"?__dirname:Q(Z(import.meta.url))}var ge=ee(),I=process.env.CLAUDE_MEM_DATA_DIR||S(j(),".claude-mem"),$=process.env.CLAUDE_CONFIG_DIR||S(j(),".claude"),he=S(I,"archives"),be=S(I,"logs"),Se=S(I,"trash"),Re=S(I,"backups"),fe=S(I,"settings.json"),P=S(I,"claude-mem.db"),Ne=S(I,"vector-db"),Oe=S($,"settings.json"),Ie=S($,"commands"),Le=S($,"CLAUDE.md");function H(p){z(p,{recursive:!0})}var U=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(U||{}),M=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=U[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,i){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),a=U[e].padEnd(5),_=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let b="";i!=null&&(this.level===0&&typeof i=="object"?b=`
|
||||
`+JSON.stringify(i,null,2):b=" "+this.formatData(i));let n="";if(r){let{sessionId:R,sdkSessionId:N,correlationId:l,...c}=r;Object.keys(c).length>0&&(n=` {${Object.entries(c).map(([u,T])=>`${u}=${T}`).join(", ")}}`)}let A=`[${d}] [${a}] [${_}] ${E}${t}${n}${b}`;e===3?console.error(A):console.log(A)}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 M;var D=class{db;constructor(){H(I),this.db=new se(P),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
import X from"path";import{stdin as w}from"process";import se from"better-sqlite3";import{join as f,dirname as Q,basename as _e}from"path";import{homedir as j}from"os";import{existsSync as Ee,mkdirSync as z}from"fs";import{fileURLToPath as Z}from"url";function ee(){return typeof __dirname<"u"?__dirname:Q(Z(import.meta.url))}var ge=ee(),I=process.env.CLAUDE_MEM_DATA_DIR||f(j(),".claude-mem"),$=process.env.CLAUDE_CONFIG_DIR||f(j(),".claude"),he=f(I,"archives"),be=f(I,"logs"),Se=f(I,"trash"),fe=f(I,"backups"),Re=f(I,"settings.json"),P=f(I,"claude-mem.db"),Ne=f(I,"vector-db"),Oe=f($,"settings.json"),Ie=f($,"commands"),Le=f($,"CLAUDE.md");function H(p){z(p,{recursive:!0})}var U=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(U||{}),M=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=U[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,i){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),a=U[e].padEnd(5),_=s.padEnd(6),T="";r?.correlationId?T=`[${r.correlationId}] `:r?.sessionId&&(T=`[session-${r.sessionId}] `);let S="";i!=null&&(this.level===0&&typeof i=="object"?S=`
|
||||
`+JSON.stringify(i,null,2):S=" "+this.formatData(i));let n="";if(r){let{sessionId:R,sdkSessionId:N,correlationId:l,...c}=r;Object.keys(c).length>0&&(n=` {${Object.entries(c).map(([u,g])=>`${u}=${g}`).join(", ")}}`)}let v=`[${d}] [${a}] [${_}] ${T}${t}${n}${S}`;e===3?console.error(v):console.log(v)}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 M;var D=class{db;constructor(){H(I),this.db=new se(P),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -216,6 +216,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
@@ -317,23 +318,23 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let b=this.db.prepare(`
|
||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let S=this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,i.toISOString(),d);return{id:Number(b.lastInsertRowid),createdAtEpoch:d}}storeSummary(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,i.toISOString(),d);return{id:Number(S.lastInsertRowid),createdAtEpoch:d}}storeSummary(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let b=this.db.prepare(`
|
||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let S=this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,i.toISOString(),d);return{id:Number(b.lastInsertRowid),createdAtEpoch:d}}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,i.toISOString(),d);return{id:Number(S.lastInsertRowid),createdAtEpoch:d}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -356,7 +357,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE up.id IN (${a})
|
||||
ORDER BY up.created_at_epoch ${i}
|
||||
${d}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,i){let d=i?"AND project = ?":"",a=i?[i]:[],_,E;if(e!==null){let R=`
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,i){let d=i?"AND project = ?":"",a=i?[i]:[],_,T;if(e!==null){let R=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${d}
|
||||
@@ -368,7 +369,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE id >= ? ${d}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let l=this.db.prepare(R).all(e,...a,t+1),c=this.db.prepare(N).all(e,...a,r+1);if(l.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};_=l.length>0?l[l.length-1].created_at_epoch:s,E=c.length>0?c[c.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary observations:",l.message),{observations:[],sessions:[],prompts:[]}}}else{let R=`
|
||||
`;try{let l=this.db.prepare(R).all(e,...a,t+1),c=this.db.prepare(N).all(e,...a,r+1);if(l.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};_=l.length>0?l[l.length-1].created_at_epoch:s,T=c.length>0?c[c.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary observations:",l.message),{observations:[],sessions:[],prompts:[]}}}else{let R=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${d}
|
||||
@@ -380,7 +381,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE created_at_epoch >= ? ${d}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let l=this.db.prepare(R).all(s,...a,t),c=this.db.prepare(N).all(s,...a,r+1);if(l.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};_=l.length>0?l[l.length-1].created_at_epoch:s,E=c.length>0?c[c.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary timestamps:",l.message),{observations:[],sessions:[],prompts:[]}}}let b=`
|
||||
`;try{let l=this.db.prepare(R).all(s,...a,t),c=this.db.prepare(N).all(s,...a,r+1);if(l.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};_=l.length>0?l[l.length-1].created_at_epoch:s,T=c.length>0?c[c.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary timestamps:",l.message),{observations:[],sessions:[],prompts:[]}}}let S=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${d}
|
||||
@@ -390,13 +391,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${d}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,A=`
|
||||
`,v=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${d.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let R=this.db.prepare(b).all(_,E,...a),N=this.db.prepare(n).all(_,E,...a),l=this.db.prepare(A).all(_,E,...a);return{observations:R,sessions:N.map(c=>({id:c.id,sdk_session_id:c.sdk_session_id,project:c.project,request:c.request,completed:c.completed,next_steps:c.next_steps,created_at:c.created_at,created_at_epoch:c.created_at_epoch})),prompts:l.map(c=>({id:c.id,claude_session_id:c.claude_session_id,project:c.project,prompt:c.prompt_text,created_at:c.created_at,created_at_epoch:c.created_at_epoch}))}}catch(R){return console.error("[SessionStore] Error querying timeline records:",R.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};var te=parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS||"50",10),W=10,o={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m",red:"\x1B[31m"};function re(p){if(!p)return[];let e=JSON.parse(p);return Array.isArray(e)?e:[]}function ne(p){return new Date(p).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function ie(p){return new Date(p).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function oe(p){return new Date(p).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function ae(p){return p?Math.ceil(p.length/4):0}function de(p,e){return X.isAbsolute(p)?X.relative(e,p):p}async function Y(p,e=!1,s=!1){let t=p?.cwd??process.cwd(),r=t?X.basename(t):"unknown-project",i=new D,d=i.db.prepare(`
|
||||
`;try{let R=this.db.prepare(S).all(_,T,...a),N=this.db.prepare(n).all(_,T,...a),l=this.db.prepare(v).all(_,T,...a);return{observations:R,sessions:N.map(c=>({id:c.id,sdk_session_id:c.sdk_session_id,project:c.project,request:c.request,completed:c.completed,next_steps:c.next_steps,created_at:c.created_at,created_at_epoch:c.created_at_epoch})),prompts:l.map(c=>({id:c.id,claude_session_id:c.claude_session_id,project:c.project,prompt:c.prompt_text,created_at:c.created_at,created_at_epoch:c.created_at_epoch}))}}catch(R){return console.error("[SessionStore] Error querying timeline records:",R.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};var te=parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS||"50",10),W=10,o={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m",red:"\x1B[31m"};function re(p){if(!p)return[];let e=JSON.parse(p);return Array.isArray(e)?e:[]}function ne(p){return new Date(p).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function ie(p){return new Date(p).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function oe(p){return new Date(p).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function ae(p){return p?Math.ceil(p.length/4):0}function de(p,e){return X.isAbsolute(p)?X.relative(e,p):p}async function Y(p,e=!1,s=!1){let t=p?.cwd??process.cwd(),r=t?X.basename(t):"unknown-project",i=new D,d=i.db.prepare(`
|
||||
SELECT
|
||||
id, sdk_session_id, type, title, subtitle, narrative,
|
||||
facts, concepts, files_read, files_modified,
|
||||
@@ -406,7 +407,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(r,te),a=i.db.prepare(`
|
||||
SELECT id, sdk_session_id, request, completed, next_steps, created_at, created_at_epoch
|
||||
SELECT id, sdk_session_id, request, investigated, learned, completed, next_steps, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
@@ -418,5 +419,5 @@ ${o.gray}${"\u2500".repeat(60)}${o.reset}
|
||||
${o.dim}No previous sessions found for this project yet.${o.reset}
|
||||
`:`# [${r}] recent context
|
||||
|
||||
No previous sessions found for this project yet.`;let _=d,E=a.slice(0,W),b=_,n=[];if(e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push(`# [${r}] recent context`),n.push("")),b.length>0){e?(n.push(`${o.dim}Legend: \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision${o.reset}`),n.push("")):(n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),n.push("")),e?(n.push(`${o.dim}\u{1F4A1} Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${o.reset}`),n.push(`${o.dim} \u2192 Use MCP search tools to fetch full observation details on-demand (Layer 2)${o.reset}`),n.push(`${o.dim} \u2192 Prefer searching observations over re-reading code for past decisions and learnings${o.reset}`),n.push(`${o.dim} \u2192 Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately${o.reset}`),n.push("")):(n.push("\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts)."),n.push("- Use MCP search tools to fetch full observation details on-demand (Layer 2)"),n.push("- Prefer searching observations over re-reading code for past decisions and learnings"),n.push("- Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately"),n.push(""));let A=a[0]?.id,R=E.map((u,T)=>{let m=T===0?null:a[T+1];return{...u,displayEpoch:m?m.created_at_epoch:u.created_at_epoch,displayTime:m?m.created_at:u.created_at,isMostRecent:u.id===A}}),N=[...b.map(u=>({type:"observation",data:u})),...R.map(u=>({type:"summary",data:u}))];N.sort((u,T)=>{let m=u.type==="observation"?u.data.created_at_epoch:u.data.displayEpoch,L=T.type==="observation"?T.data.created_at_epoch:T.data.displayEpoch;return m-L});let l=new Map;for(let u of N){let T=u.type==="observation"?u.data.created_at:u.data.displayTime,m=oe(T);l.has(m)||l.set(m,[]),l.get(m).push(u)}let c=Array.from(l.entries()).sort((u,T)=>{let m=new Date(u[0]).getTime(),L=new Date(T[0]).getTime();return m-L});for(let[u,T]of c){e?(n.push(`${o.bright}${o.cyan}${u}${o.reset}`),n.push("")):(n.push(`### ${u}`),n.push(""));let m=null,L="",v=!1;for(let x of T)if(x.type==="summary"){v&&(n.push(""),v=!1,m=null,L="");let g=x.data,y=`${g.request||"Session started"} (${ne(g.displayTime)})`,O=g.isMostRecent?"":`claude-mem://session-summary/${g.id}`;if(e){let h=O?`${o.dim}[${O}]${o.reset}`:"";n.push(`\u{1F3AF} ${o.yellow}#S${g.id}${o.reset} ${y} ${h}`)}else{let h=O?` [\u2192](${O})`:"";n.push(`**\u{1F3AF} #S${g.id}** ${y}${h}`)}n.push("")}else{let g=x.data,y=re(g.files_modified),O=y.length>0?de(y[0],t):"General";O!==m&&(v&&n.push(""),e?n.push(`${o.dim}${O}${o.reset}`):n.push(`**${O}**`),e||(n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|")),m=O,v=!0,L="");let h="\u2022";switch(g.type){case"bugfix":h="\u{1F534}";break;case"feature":h="\u{1F7E3}";break;case"refactor":h="\u{1F504}";break;case"change":h="\u2705";break;case"discovery":h="\u{1F535}";break;case"decision":h="\u{1F9E0}";break;default:h="\u2022"}let C=ie(g.created_at),F=g.title||"Untitled",k=ae(g.narrative),B=C!==L,q=B?C:"";if(L=C,e){let K=B?`${o.dim}${C}${o.reset}`:" ".repeat(C.length),J=k>0?`${o.dim}(~${k}t)${o.reset}`:"";n.push(` ${o.dim}#${g.id}${o.reset} ${K} ${h} ${F} ${J}`)}else n.push(`| #${g.id} | ${q||"\u2033"} | ${h} | ${F} | ~${k} |`)}v&&n.push("")}let f=a[0];f&&(f.completed||f.next_steps)&&(f.completed&&(e?n.push(`${o.green}Completed:${o.reset} ${f.completed}`):n.push(`**Completed**: ${f.completed}`),n.push("")),f.next_steps&&(e?n.push(`${o.magenta}Next Steps:${o.reset} ${f.next_steps}`):n.push(`**Next Steps**: ${f.next_steps}`),n.push(""))),e?n.push(`${o.dim}Use claude-mem MCP search to access records with the given ID${o.reset}`):n.push("*Use claude-mem MCP search to access records with the given ID*")}return i.close(),n.join(`
|
||||
No previous sessions found for this project yet.`;let _=d,T=a.slice(0,W),S=_,n=[];if(e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push(`# [${r}] recent context`),n.push("")),S.length>0){e?(n.push(`${o.dim}Legend: \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision${o.reset}`),n.push("")):(n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),n.push("")),e?(n.push(`${o.dim}\u{1F4A1} Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${o.reset}`),n.push(`${o.dim} \u2192 Use MCP search tools to fetch full observation details on-demand (Layer 2)${o.reset}`),n.push(`${o.dim} \u2192 Prefer searching observations over re-reading code for past decisions and learnings${o.reset}`),n.push(`${o.dim} \u2192 Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately${o.reset}`),n.push("")):(n.push("\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts)."),n.push("- Use MCP search tools to fetch full observation details on-demand (Layer 2)"),n.push("- Prefer searching observations over re-reading code for past decisions and learnings"),n.push("- Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately"),n.push(""));let v=a[0]?.id,R=T.map((u,g)=>{let E=g===0?null:a[g+1];return{...u,displayEpoch:E?E.created_at_epoch:u.created_at_epoch,displayTime:E?E.created_at:u.created_at,isMostRecent:u.id===v}}),N=[...S.map(u=>({type:"observation",data:u})),...R.map(u=>({type:"summary",data:u}))];N.sort((u,g)=>{let E=u.type==="observation"?u.data.created_at_epoch:u.data.displayEpoch,L=g.type==="observation"?g.data.created_at_epoch:g.data.displayEpoch;return E-L});let l=new Map;for(let u of N){let g=u.type==="observation"?u.data.created_at:u.data.displayTime,E=oe(g);l.has(E)||l.set(E,[]),l.get(E).push(u)}let c=Array.from(l.entries()).sort((u,g)=>{let E=new Date(u[0]).getTime(),L=new Date(g[0]).getTime();return E-L});for(let[u,g]of c){e?(n.push(`${o.bright}${o.cyan}${u}${o.reset}`),n.push("")):(n.push(`### ${u}`),n.push(""));let E=null,L="",A=!1;for(let x of g)if(x.type==="summary"){A&&(n.push(""),A=!1,E=null,L="");let h=x.data,y=`${h.request||"Session started"} (${ne(h.displayTime)})`,O=h.isMostRecent?"":`claude-mem://session-summary/${h.id}`;if(e){let b=O?`${o.dim}[${O}]${o.reset}`:"";n.push(`\u{1F3AF} ${o.yellow}#S${h.id}${o.reset} ${y} ${b}`)}else{let b=O?` [\u2192](${O})`:"";n.push(`**\u{1F3AF} #S${h.id}** ${y}${b}`)}n.push("")}else{let h=x.data,y=re(h.files_modified),O=y.length>0?de(y[0],t):"General";O!==E&&(A&&n.push(""),e?n.push(`${o.dim}${O}${o.reset}`):n.push(`**${O}**`),e||(n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|")),E=O,A=!0,L="");let b="\u2022";switch(h.type){case"bugfix":b="\u{1F534}";break;case"feature":b="\u{1F7E3}";break;case"refactor":b="\u{1F504}";break;case"change":b="\u2705";break;case"discovery":b="\u{1F535}";break;case"decision":b="\u{1F9E0}";break;default:b="\u2022"}let C=ie(h.created_at),F=h.title||"Untitled",k=ae(h.narrative),B=C!==L,q=B?C:"";if(L=C,e){let K=B?`${o.dim}${C}${o.reset}`:" ".repeat(C.length),J=k>0?`${o.dim}(~${k}t)${o.reset}`:"";n.push(` ${o.dim}#${h.id}${o.reset} ${K} ${b} ${F} ${J}`)}else n.push(`| #${h.id} | ${q||"\u2033"} | ${b} | ${F} | ~${k} |`)}A&&n.push("")}let m=a[0];m&&(m.investigated||m.learned||m.completed||m.next_steps)&&(m.investigated&&(e?n.push(`${o.blue}Investigated:${o.reset} ${m.investigated}`):n.push(`**Investigated**: ${m.investigated}`),n.push("")),m.learned&&(e?n.push(`${o.yellow}Learned:${o.reset} ${m.learned}`):n.push(`**Learned**: ${m.learned}`),n.push("")),m.completed&&(e?n.push(`${o.green}Completed:${o.reset} ${m.completed}`):n.push(`**Completed**: ${m.completed}`),n.push("")),m.next_steps&&(e?n.push(`${o.magenta}Next Steps:${o.reset} ${m.next_steps}`):n.push(`**Next Steps**: ${m.next_steps}`),n.push(""))),e?n.push(`${o.dim}Use claude-mem MCP search to access records with the given ID${o.reset}`):n.push("*Use claude-mem MCP search to access records with the given ID*")}return i.close(),n.join(`
|
||||
`).trimEnd()}var V=process.argv.includes("--index"),ce=process.argv.includes("--colors");if(w.isTTY||ce)Y(void 0,!0,V).then(p=>{console.log(p),process.exit(0)});else{let p="";w.on("data",e=>p+=e),w.on("end",async()=>{let e=p.trim()?JSON.parse(p):void 0,t={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:await Y(e,!1,V)}};console.log(JSON.stringify(t)),process.exit(0)})}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import z from"path";import{stdin as U}from"process";import j from"better-sqlite3";import{join as u,dirname as X,basename as te}from"path";import{homedir as L}from"os";import{existsSync as ie,mkdirSync as F}from"fs";import{fileURLToPath as P}from"url";function H(){return typeof __dirname<"u"?__dirname:X(P(import.meta.url))}var B=H(),m=process.env.CLAUDE_MEM_DATA_DIR||u(L(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||u(L(),".claude"),pe=u(m,"archives"),de=u(m,"logs"),ce=u(m,"trash"),_e=u(m,"backups"),ue=u(m,"settings.json"),A=u(m,"claude-mem.db"),Ee=u(m,"vector-db"),me=u(R,"settings.json"),le=u(R,"commands"),Te=u(R,"CLAUDE.md");function C(a){F(a,{recursive:!0})}function v(){return u(B,"..","..")}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
import z from"path";import{stdin as U}from"process";import j from"better-sqlite3";import{join as u,dirname as X,basename as te}from"path";import{homedir as L}from"os";import{existsSync as ie,mkdirSync as F}from"fs";import{fileURLToPath as H}from"url";function P(){return typeof __dirname<"u"?__dirname:X(H(import.meta.url))}var B=P(),m=process.env.CLAUDE_MEM_DATA_DIR||u(L(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||u(L(),".claude"),pe=u(m,"archives"),de=u(m,"logs"),ce=u(m,"trash"),_e=u(m,"backups"),ue=u(m,"settings.json"),A=u(m,"claude-mem.db"),Ee=u(m,"vector-db"),me=u(R,"settings.json"),le=u(R,"commands"),Te=u(R,"CLAUDE.md");function C(a){F(a,{recursive:!0})}function v(){return u(B,"..","..")}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${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),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
|
||||
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let T="";if(r){let{sessionId:l,sdkSessionId:b,correlationId:_,...p}=r;Object.keys(p).length>0&&(T=` {${Object.entries(p).map(([M,w])=>`${M}=${w}`).join(", ")}}`)}let S=`[${o}] [${i}] [${d}] ${c}${t}${T}${E}`;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`})}},y=new N;var g=class{db;constructor(){C(m),this.db=new j(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
@@ -216,6 +216,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
|
||||
+31
-30
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as U}from"process";import j from"better-sqlite3";import{join as E,dirname as F,basename as te}from"path";import{homedir as C}from"os";import{existsSync as ie,mkdirSync as X}from"fs";import{fileURLToPath as H}from"url";function P(){return typeof __dirname<"u"?__dirname:F(H(import.meta.url))}var B=P(),l=process.env.CLAUDE_MEM_DATA_DIR||E(C(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||E(C(),".claude"),de=E(l,"archives"),pe=E(l,"logs"),ce=E(l,"trash"),_e=E(l,"backups"),ue=E(l,"settings.json"),v=E(l,"claude-mem.db"),Ee=E(l,"vector-db"),me=E(h,"settings.json"),le=E(h,"commands"),Te=E(h,"CLAUDE.md");function y(a){X(a,{recursive:!0})}function D(){return E(B,"..","..")}var N=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),p=s.padEnd(6),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 m="";if(r){let{sessionId:T,sdkSessionId:S,correlationId:_,...d}=r;Object.keys(d).length>0&&(m=` {${Object.entries(d).map(([M,w])=>`${M}=${w}`).join(", ")}}`)}let g=`[${o}] [${i}] [${p}] ${u}${t}${m}${c}`;e===3?console.error(g):console.log(g)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},b=new O;var R=class{db;constructor(){y(l),this.db=new j(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()}initializeSchema(){try{this.db.exec(`
|
||||
import{stdin as U}from"process";import j from"better-sqlite3";import{join as E,dirname as F,basename as te}from"path";import{homedir as C}from"os";import{existsSync as ie,mkdirSync as X}from"fs";import{fileURLToPath as H}from"url";function P(){return typeof __dirname<"u"?__dirname:F(H(import.meta.url))}var B=P(),l=process.env.CLAUDE_MEM_DATA_DIR||E(C(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||E(C(),".claude"),de=E(l,"archives"),pe=E(l,"logs"),ce=E(l,"trash"),_e=E(l,"backups"),ue=E(l,"settings.json"),v=E(l,"claude-mem.db"),Ee=E(l,"vector-db"),me=E(h,"settings.json"),le=E(h,"commands"),Te=E(h,"CLAUDE.md");function y(a){X(a,{recursive:!0})}function D(){return E(B,"..","..")}var N=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let n=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),p=s.padEnd(6),u="";r?.correlationId?u=`[${r.correlationId}] `:r?.sessionId&&(u=`[session-${r.sessionId}] `);let c="";o!=null&&(this.level===0&&typeof o=="object"?c=`
|
||||
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let m="";if(r){let{sessionId:T,sdkSessionId:S,correlationId:_,...d}=r;Object.keys(d).length>0&&(m=` {${Object.entries(d).map(([M,w])=>`${M}=${w}`).join(", ")}}`)}let g=`[${n}] [${i}] [${p}] ${u}${t}${m}${c}`;e===3?console.error(g):console.log(g)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},b=new O;var R=class{db;constructor(){y(l),this.db=new j(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()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -216,6 +216,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
@@ -243,12 +244,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id = ?
|
||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,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",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id IN (${i})
|
||||
ORDER BY created_at_epoch ${n}
|
||||
${o}
|
||||
ORDER BY created_at_epoch ${o}
|
||||
${n}
|
||||
`).all(...e)}getSummaryForSession(e){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
@@ -261,7 +262,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,n=new Set;for(let 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 n of t){if(n.files_read)try{let i=JSON.parse(n.files_read);Array.isArray(i)&&i.forEach(p=>r.add(p))}catch{}if(n.files_modified)try{let i=JSON.parse(n.files_modified);Array.isArray(i)&&i.forEach(p=>o.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -288,11 +289,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),i=this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),i=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),n);return i.lastInsertRowid===0||i.changes===0?this.db.prepare(`
|
||||
`).run(e,e,s,t,r.toISOString(),o);return i.lastInsertRowid===0||i.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:i.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
@@ -307,33 +308,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,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
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,n=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,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(),n),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,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,o.toISOString(),n);return{id:Number(c.lastInsertRowid),createdAtEpoch:n}}storeSummary(e,s,t,r){let o=new Date,n=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(`
|
||||
`).run(e,e,s,o.toISOString(),n),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,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,o.toISOString(),n);return{id:Number(c.lastInsertRowid),createdAtEpoch:n}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -341,12 +342,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE 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",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT * FROM session_summaries
|
||||
WHERE id IN (${i})
|
||||
ORDER BY created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
ORDER BY created_at_epoch ${o}
|
||||
${n}
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT
|
||||
up.*,
|
||||
s.project,
|
||||
@@ -354,46 +355,46 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.id IN (${i})
|
||||
ORDER BY up.created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],p,u;if(e!==null){let T=`
|
||||
ORDER BY up.created_at_epoch ${o}
|
||||
${n}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,o){let n=o?"AND project = ?":"",i=o?[o]:[],p,u;if(e!==null){let T=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${o}
|
||||
WHERE id <= ? ${n}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${o}
|
||||
WHERE id >= ? ${n}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(T).all(e,...i,t+1),d=this.db.prepare(S).all(e,...i,r+1);if(_.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=d.length>0?d[d.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
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
WHERE created_at_epoch <= ? ${n}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
WHERE created_at_epoch >= ? ${n}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(T).all(s,...i,t),d=this.db.prepare(S).all(s,...i,r+1);if(_.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=d.length>0?d[d.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let c=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${n}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,m=`
|
||||
SELECT *
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${n}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,g=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${n.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let T=this.db.prepare(c).all(p,u,...i),S=this.db.prepare(m).all(p,u,...i),_=this.db.prepare(g).all(p,u,...i);return{observations:T,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:_.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(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 f(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import I from"path";import{homedir as W}from"os";import{existsSync as G,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var V=100,q=100,J=1e4;function L(){try{let a=I.join(W(),".claude-mem","settings.json");if(G(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 k(){try{let a=L();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(V)})).ok}catch{return!1}}async function Q(){let a=Date.now();for(;Date.now()-a<J;){if(await k())return!0;await new Promise(e=>setTimeout(e,q))}return!1}async function x(){if(await k())return;let a=D(),e=I.join(a,"node_modules",".bin","pm2"),s=I.join(a,"ecosystem.config.cjs");if(K(`"${e}" restart "${s}"`,{cwd:a,stdio:"pipe"}),!await Q())throw new Error("Worker failed to become healthy after restart")}var z=new Set(["ListMcpResourcesTool"]);async function Z(a){if(!a)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_response:r}=a;if(z.has(s)){console.log(f("PostToolUse",!0));return}await x();let n=new R,o=n.createSDKSession(e,"",""),i=n.getPromptCounter(o);n.close();let p=b.formatTool(s,t),u=L();b.dataIn("HOOK",`PostToolUse: ${p}`,{sessionId:o,workerPort:u});try{let c=await fetch(`http://127.0.0.1:${u}/sessions/${o}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_response:r!==void 0?JSON.stringify(r):"{}",prompt_number:i}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let m=await c.text();throw b.failure("HOOK","Failed to send observation",{sessionId:o,status:c.status},m),new Error(`Failed to send observation to worker: ${c.status} ${m}`)}b.debug("HOOK","Observation sent successfully",{sessionId:o,toolName:s})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var A="";U.on("data",a=>A+=a);U.on("end",async()=>{let a=A?JSON.parse(A):void 0;await Z(a)});
|
||||
`;try{let T=this.db.prepare(c).all(p,u,...i),S=this.db.prepare(m).all(p,u,...i),_=this.db.prepare(g).all(p,u,...i);return{observations:T,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:_.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(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 f(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import I from"path";import{homedir as W}from"os";import{existsSync as G,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var V=100,q=100,J=1e4;function L(){try{let a=I.join(W(),".claude-mem","settings.json");if(G(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 k(){try{let a=L();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(V)})).ok}catch{return!1}}async function Q(){let a=Date.now();for(;Date.now()-a<J;){if(await k())return!0;await new Promise(e=>setTimeout(e,q))}return!1}async function x(){if(await k())return;let a=D(),e=I.join(a,"node_modules",".bin","pm2"),s=I.join(a,"ecosystem.config.cjs");if(K(`"${e}" restart "${s}"`,{cwd:a,stdio:"pipe"}),!await Q())throw new Error("Worker failed to become healthy after restart")}var z=new Set(["ListMcpResourcesTool"]);async function Z(a){if(!a)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_response:r}=a;if(z.has(s)){console.log(f("PostToolUse",!0));return}await x();let o=new R,n=o.createSDKSession(e,"",""),i=o.getPromptCounter(n);o.close();let p=b.formatTool(s,t),u=L();b.dataIn("HOOK",`PostToolUse: ${p}`,{sessionId:n,workerPort:u});try{let c=await fetch(`http://127.0.0.1:${u}/sessions/${n}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_response:r!==void 0?JSON.stringify(r):"{}",prompt_number:i}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let m=await c.text();throw b.failure("HOOK","Failed to send observation",{sessionId:n,status:c.status},m),new Error(`Failed to send observation to worker: ${c.status} ${m}`)}b.debug("HOOK","Observation sent successfully",{sessionId:n,toolName:s})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var A="";U.on("data",a=>A+=a);U.on("end",async()=>{let a=A?JSON.parse(A):void 0;await Z(a)});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import{Server as he}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as _e}from"@modelcontextprotocol/sdk/server/stdio.js";import{Client as fe}from"@modelcontextprotocol/sdk/client/index.js";import{StdioClientTransport as Ee}from"@modelcontextprotocol/sdk/client/stdio.js";import{CallToolRequestSchema as be,ListToolsRequestSchema as ge}from"@modelcontextprotocol/sdk/types.js";import{z as i}from"zod";import{zodToJsonSchema as Te}from"zod-to-json-schema";import{basename as Se}from"path";import pe from"better-sqlite3";import{join as L,dirname as ce,basename as xe}from"path";import{homedir as ee}from"os";import{existsSync as De,mkdirSync as de}from"fs";import{fileURLToPath as le}from"url";function ue(){return typeof __dirname<"u"?__dirname:ce(le(import.meta.url))}var $e=ue(),w=process.env.CLAUDE_MEM_DATA_DIR||L(ee(),".claude-mem"),V=process.env.CLAUDE_CONFIG_DIR||L(ee(),".claude"),ke=L(w,"archives"),Fe=L(w,"logs"),Me=L(w,"trash"),Ue=L(w,"backups"),je=L(w,"settings.json"),X=L(w,"claude-mem.db"),te=L(w,"vector-db"),Be=L(V,"settings.json"),Xe=L(V,"commands"),Pe=L(V,"CLAUDE.md");function P(c){de(c,{recursive:!0})}var G=class{db;constructor(e){e||(P(w),e=X),this.db=new pe(e),this.db.pragma("journal_mode = WAL"),this.ensureFTSTables()}ensureFTSTables(){try{if(this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts'").all().some(r=>r.name==="observations_fts"||r.name==="session_summaries_fts"))return;console.error("[SessionSearch] Creating FTS5 tables..."),this.db.exec(`
|
||||
import{Server as he}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as _e}from"@modelcontextprotocol/sdk/server/stdio.js";import{Client as fe}from"@modelcontextprotocol/sdk/client/index.js";import{StdioClientTransport as Ee}from"@modelcontextprotocol/sdk/client/stdio.js";import{CallToolRequestSchema as be,ListToolsRequestSchema as ge}from"@modelcontextprotocol/sdk/types.js";import{z as i}from"zod";import{zodToJsonSchema as Te}from"zod-to-json-schema";import{basename as Se}from"path";import pe from"better-sqlite3";import{join as L,dirname as ce,basename as xe}from"path";import{homedir as ee}from"os";import{existsSync as De,mkdirSync as de}from"fs";import{fileURLToPath as le}from"url";function ue(){return typeof __dirname<"u"?__dirname:ce(le(import.meta.url))}var $e=ue(),w=process.env.CLAUDE_MEM_DATA_DIR||L(ee(),".claude-mem"),V=process.env.CLAUDE_CONFIG_DIR||L(ee(),".claude"),ke=L(w,"archives"),Fe=L(w,"logs"),Ue=L(w,"trash"),Me=L(w,"backups"),je=L(w,"settings.json"),X=L(w,"claude-mem.db"),te=L(w,"vector-db"),Be=L(V,"settings.json"),Xe=L(V,"commands"),Pe=L(V,"CLAUDE.md");function P(c){de(c,{recursive:!0})}var G=class{db;constructor(e){e||(P(w),e=X),this.db=new pe(e),this.db.pragma("journal_mode = WAL"),this.ensureFTSTables()}ensureFTSTables(){try{if(this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts'").all().some(r=>r.name==="observations_fts"||r.name==="session_summaries_fts"))return;console.error("[SessionSearch] Creating FTS5 tables..."),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
||||
title,
|
||||
subtitle,
|
||||
@@ -352,6 +352,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
ORDER BY project ASC
|
||||
`).all().map(r=>r.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
@@ -532,7 +533,7 @@ ${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
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let f=this.db.prepare(p).all(d,l,...a),h=this.db.prepare(u).all(d,l,...a),b=this.db.prepare(m).all(d,l,...a);return{observations:f,sessions:h.map(_=>({id:_.id,sdk_session_id:_.sdk_session_id,project:_.project,request:_.request,completed:_.completed,next_steps:_.next_steps,created_at:_.created_at,created_at_epoch:_.created_at_epoch})),prompts:b.map(_=>({id:_.id,claude_session_id:_.claude_session_id,project:_.project,prompt:_.prompt_text,created_at:_.created_at,created_at_epoch:_.created_at_epoch}))}}catch(f){return console.error("[SessionStore] Error querying timeline records:",f.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};var $,N,k=null,ye="cm__claude-mem";try{$=new G,N=new H}catch(c){console.error("[search-server] Failed to initialize search:",c.message),process.exit(1)}async function U(c,e,s){if(!k)throw new Error("Chroma client not initialized");let t=(await k.callTool({name:"chroma_query_documents",arguments:{collection_name:ye,query_texts:[c],n_results:e,include:["documents","metadatas","distances"],where:s}})).content[0]?.text||"",n;try{n=JSON.parse(t)}catch(p){return console.error("[search-server] Failed to parse Chroma response as JSON:",p),{ids:[],distances:[],metadatas:[]}}let o=[],a=n.ids?.[0]||[];for(let p of a){let u=p.match(/obs_(\d+)_/),m=p.match(/summary_(\d+)_/),f=p.match(/prompt_(\d+)/),h=null;u?h=parseInt(u[1],10):m?h=parseInt(m[1],10):f&&(h=parseInt(f[1],10)),h!==null&&!o.includes(h)&&o.push(h)}let d=n.distances?.[0]||[],l=n.metadatas?.[0]||[];return{ids:o,distances:d,metadatas:l}}function j(){return`
|
||||
`;try{let f=this.db.prepare(p).all(d,l,...a),h=this.db.prepare(u).all(d,l,...a),b=this.db.prepare(m).all(d,l,...a);return{observations:f,sessions:h.map(_=>({id:_.id,sdk_session_id:_.sdk_session_id,project:_.project,request:_.request,completed:_.completed,next_steps:_.next_steps,created_at:_.created_at,created_at_epoch:_.created_at_epoch})),prompts:b.map(_=>({id:_.id,claude_session_id:_.claude_session_id,project:_.project,prompt:_.prompt_text,created_at:_.created_at,created_at_epoch:_.created_at_epoch}))}}catch(f){return console.error("[SessionStore] Error querying timeline records:",f.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};var $,N,k=null,ye="cm__claude-mem";try{$=new G,N=new H}catch(c){console.error("[search-server] Failed to initialize search:",c.message),process.exit(1)}async function M(c,e,s){if(!k)throw new Error("Chroma client not initialized");let t=(await k.callTool({name:"chroma_query_documents",arguments:{collection_name:ye,query_texts:[c],n_results:e,include:["documents","metadatas","distances"],where:s}})).content[0]?.text||"",n;try{n=JSON.parse(t)}catch(p){return console.error("[search-server] Failed to parse Chroma response as JSON:",p),{ids:[],distances:[],metadatas:[]}}let o=[],a=n.ids?.[0]||[];for(let p of a){let u=p.match(/obs_(\d+)_/),m=p.match(/summary_(\d+)_/),f=p.match(/prompt_(\d+)/),h=null;u?h=parseInt(u[1],10):m?h=parseInt(m[1],10):f&&(h=parseInt(f[1],10)),h!==null&&!o.includes(h)&&o.push(h)}let d=n.distances?.[0]||[],l=n.metadatas?.[0]||[];return{ids:o,distances:d,metadatas:l}}function j(){return`
|
||||
---
|
||||
\u{1F4A1} Search Strategy:
|
||||
ALWAYS search with index format FIRST to get an overview and identify relevant results.
|
||||
@@ -556,7 +557,7 @@ Other tips:
|
||||
`)}function Re(c,e){let s=new Date(c.created_at_epoch).toLocaleString();return`${e+1}. "${c.prompt_text}"
|
||||
Date: ${s} | Prompt #${c.prompt_number}
|
||||
Source: claude-mem://user-prompt/${c.id}`}function ve(c){let e=[];e.push(`## User Prompt #${c.prompt_number}`),e.push(`*Source: claude-mem://user-prompt/${c.id}*`),e.push(""),e.push(c.prompt_text),e.push(""),e.push("---");let s=new Date(c.created_at_epoch).toLocaleString();return e.push(`Date: ${s}`),e.join(`
|
||||
`)}var Oe=i.object({project:i.string().optional().describe("Filter by project name"),type:i.union([i.enum(["decision","bugfix","feature","refactor","discovery","change"]),i.array(i.enum(["decision","bugfix","feature","refactor","discovery","change"]))]).optional().describe("Filter by observation type"),concepts:i.union([i.string(),i.array(i.string())]).optional().describe("Filter by concept tags"),files:i.union([i.string(),i.array(i.string())]).optional().describe("Filter by file paths (partial match)"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional().describe("Start date (ISO string or epoch)"),end:i.union([i.string(),i.number()]).optional().describe("End date (ISO string or epoch)")}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),oe=[{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.',inputSchema:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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)'),...Oe.shape}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search (Chroma + SQLite)");let o=await U(e,100);if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getObservationsByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} observations from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchObservations(e,r)),t.length===0)return{content:[{type:"text",text:`No observations found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} observation(s) matching "${e}":
|
||||
`)}var Oe=i.object({project:i.string().optional().describe("Filter by project name"),type:i.union([i.enum(["decision","bugfix","feature","refactor","discovery","change"]),i.array(i.enum(["decision","bugfix","feature","refactor","discovery","change"]))]).optional().describe("Filter by observation type"),concepts:i.union([i.string(),i.array(i.string())]).optional().describe("Filter by concept tags"),files:i.union([i.string(),i.array(i.string())]).optional().describe("Filter by file paths (partial match)"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional().describe("Start date (ISO string or epoch)"),end:i.union([i.string(),i.number()]).optional().describe("End date (ISO string or epoch)")}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),oe=[{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.',inputSchema:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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)'),...Oe.shape}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search (Chroma + SQLite)");let o=await M(e,100);if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getObservationsByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} observations from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchObservations(e,r)),t.length===0)return{content:[{type:"text",text:`No observations found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} observation(s) matching "${e}":
|
||||
|
||||
`,a=t.map((d,l)=>q(d,l));n=o+a.join(`
|
||||
|
||||
@@ -564,7 +565,7 @@ Other tips:
|
||||
|
||||
---
|
||||
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{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.',inputSchema:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search for sessions");let o=await U(e,100,{doc_type:"session_summary"});if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getSessionSummariesByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} sessions from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchSessions(e,r)),t.length===0)return{content:[{type:"text",text:`No sessions found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} session(s) matching "${e}":
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{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.',inputSchema:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search for sessions");let o=await M(e,100,{doc_type:"session_summary"});if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getSessionSummariesByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} sessions from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchSessions(e,r)),t.length===0)return{content:[{type:"text",text:`No sessions found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} session(s) matching "${e}":
|
||||
|
||||
`,a=t.map((d,l)=>re(d,l));n=o+a.join(`
|
||||
|
||||
@@ -572,7 +573,7 @@ Other tips:
|
||||
|
||||
---
|
||||
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"find_by_concept",description:'Find observations tagged with a specific concept. Available concepts: "discovery", "problem-solution", "what-changed", "how-it-works", "pattern", "gotcha", "change". 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:i.object({concept:i.string().describe("Concept tag to search for. Available: discovery, problem-solution, what-changed, how-it-works, pattern, gotcha, change"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode."),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{concept:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for concept search");let o=$.findByConcept(e,r);if(console.error(`[search-server] Found ${o.length} observations with concept "${e}"`),o.length>0){let a=o.map(p=>p.id),d=await U(e,Math.min(a.length,100)),l=[];for(let p of d.ids)a.includes(p)&&!l.includes(p)&&l.push(p);console.error(`[search-server] Chroma ranked ${l.length} results by semantic relevance`),l.length>0&&(t=N.getObservationsByIds(l,{limit:r.limit||20}),t.sort((p,u)=>l.indexOf(p.id)-l.indexOf(u.id)))}}catch(o){console.error("[search-server] Chroma ranking failed, using SQLite order:",o.message)}if(t.length===0&&(console.error("[search-server] Using SQLite-only concept search"),t=$.findByConcept(e,r)),t.length===0)return{content:[{type:"text",text:`No observations found with concept "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} observation(s) with concept "${e}":
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"find_by_concept",description:'Find observations tagged with a specific concept. Available concepts: "discovery", "problem-solution", "what-changed", "how-it-works", "pattern", "gotcha", "change". 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:i.object({concept:i.string().describe("Concept tag to search for. Available: discovery, problem-solution, what-changed, how-it-works, pattern, gotcha, change"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode."),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{concept:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for concept search");let o=$.findByConcept(e,r);if(console.error(`[search-server] Found ${o.length} observations with concept "${e}"`),o.length>0){let a=o.map(p=>p.id),d=await M(e,Math.min(a.length,100)),l=[];for(let p of d.ids)a.includes(p)&&!l.includes(p)&&l.push(p);console.error(`[search-server] Chroma ranked ${l.length} results by semantic relevance`),l.length>0&&(t=N.getObservationsByIds(l,{limit:r.limit||20}),t.sort((p,u)=>l.indexOf(p.id)-l.indexOf(u.id)))}}catch(o){console.error("[search-server] Chroma ranking failed, using SQLite order:",o.message)}if(t.length===0&&(console.error("[search-server] Using SQLite-only concept search"),t=$.findByConcept(e,r)),t.length===0)return{content:[{type:"text",text:`No observations found with concept "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} observation(s) with concept "${e}":
|
||||
|
||||
`,a=t.map((d,l)=>q(d,l));n=o+a.join(`
|
||||
|
||||
@@ -580,7 +581,7 @@ Other tips:
|
||||
|
||||
---
|
||||
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"find_by_file",description:'Find observations and sessions that reference a specific file path. 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:i.object({filePath:i.string().describe("File path to search for (supports partial matching)"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode."),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{filePath:e,format:s="index",...r}=c,t=[],n=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for file search");let d=$.findByFile(e,r);if(console.error(`[search-server] Found ${d.observations.length} observations, ${d.sessions.length} sessions for file "${e}"`),n=d.sessions,d.observations.length>0){let l=d.observations.map(m=>m.id),p=await U(e,Math.min(l.length,100)),u=[];for(let m of p.ids)l.includes(m)&&!u.includes(m)&&u.push(m);console.error(`[search-server] Chroma ranked ${u.length} observations by semantic relevance`),u.length>0&&(t=N.getObservationsByIds(u,{limit:r.limit||20}),t.sort((m,f)=>u.indexOf(m.id)-u.indexOf(f.id)))}}catch(d){console.error("[search-server] Chroma ranking failed, using SQLite order:",d.message)}if(t.length===0&&n.length===0){console.error("[search-server] Using SQLite-only file search");let d=$.findByFile(e,r);t=d.observations,n=d.sessions}let o=t.length+n.length;if(o===0)return{content:[{type:"text",text:`No results found for file "${e}"`}]};let a;if(s==="index"){let d=`Found ${o} result(s) for file "${e}":
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"find_by_file",description:'Find observations and sessions that reference a specific file path. 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:i.object({filePath:i.string().describe("File path to search for (supports partial matching)"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode."),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{filePath:e,format:s="index",...r}=c,t=[],n=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for file search");let d=$.findByFile(e,r);if(console.error(`[search-server] Found ${d.observations.length} observations, ${d.sessions.length} sessions for file "${e}"`),n=d.sessions,d.observations.length>0){let l=d.observations.map(m=>m.id),p=await M(e,Math.min(l.length,100)),u=[];for(let m of p.ids)l.includes(m)&&!u.includes(m)&&u.push(m);console.error(`[search-server] Chroma ranked ${u.length} observations by semantic relevance`),u.length>0&&(t=N.getObservationsByIds(u,{limit:r.limit||20}),t.sort((m,f)=>u.indexOf(m.id)-u.indexOf(f.id)))}}catch(d){console.error("[search-server] Chroma ranking failed, using SQLite order:",d.message)}if(t.length===0&&n.length===0){console.error("[search-server] Using SQLite-only file search");let d=$.findByFile(e,r);t=d.observations,n=d.sessions}let o=t.length+n.length;if(o===0)return{content:[{type:"text",text:`No results found for file "${e}"`}]};let a;if(s==="index"){let d=`Found ${o} result(s) for file "${e}":
|
||||
|
||||
`,l=[];t.forEach((p,u)=>{l.push(q(p,u))}),n.forEach((p,u)=>{l.push(re(p,u+t.length))}),a=d+l.join(`
|
||||
|
||||
@@ -588,7 +589,7 @@ Other tips:
|
||||
|
||||
---
|
||||
|
||||
`)}return{content:[{type:"text",text:a}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"find_by_type",description:'Find observations of a specific type (decision, bugfix, feature, refactor, discovery, change). 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:i.object({type:i.union([i.enum(["decision","bugfix","feature","refactor","discovery","change"]),i.array(i.enum(["decision","bugfix","feature","refactor","discovery","change"]))]).describe("Observation type(s) to filter by"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode."),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{type:e,format:s="index",...r}=c,t=Array.isArray(e)?e.join(", "):e,n=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for type search");let a=$.findByType(e,r);if(console.error(`[search-server] Found ${a.length} observations with type "${t}"`),a.length>0){let d=a.map(u=>u.id),l=await U(t,Math.min(d.length,100)),p=[];for(let u of l.ids)d.includes(u)&&!p.includes(u)&&p.push(u);console.error(`[search-server] Chroma ranked ${p.length} results by semantic relevance`),p.length>0&&(n=N.getObservationsByIds(p,{limit:r.limit||20}),n.sort((u,m)=>p.indexOf(u.id)-p.indexOf(m.id)))}}catch(a){console.error("[search-server] Chroma ranking failed, using SQLite order:",a.message)}if(n.length===0&&(console.error("[search-server] Using SQLite-only type search"),n=$.findByType(e,r)),n.length===0)return{content:[{type:"text",text:`No observations found with type "${t}"`}]};let o;if(s==="index"){let a=`Found ${n.length} observation(s) with type "${t}":
|
||||
`)}return{content:[{type:"text",text:a}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"find_by_type",description:'Find observations of a specific type (decision, bugfix, feature, refactor, discovery, change). 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:i.object({type:i.union([i.enum(["decision","bugfix","feature","refactor","discovery","change"]),i.array(i.enum(["decision","bugfix","feature","refactor","discovery","change"]))]).describe("Observation type(s) to filter by"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode."),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{type:e,format:s="index",...r}=c,t=Array.isArray(e)?e.join(", "):e,n=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for type search");let a=$.findByType(e,r);if(console.error(`[search-server] Found ${a.length} observations with type "${t}"`),a.length>0){let d=a.map(u=>u.id),l=await M(t,Math.min(d.length,100)),p=[];for(let u of l.ids)d.includes(u)&&!p.includes(u)&&p.push(u);console.error(`[search-server] Chroma ranked ${p.length} results by semantic relevance`),p.length>0&&(n=N.getObservationsByIds(p,{limit:r.limit||20}),n.sort((u,m)=>p.indexOf(u.id)-p.indexOf(m.id)))}}catch(a){console.error("[search-server] Chroma ranking failed, using SQLite order:",a.message)}if(n.length===0&&(console.error("[search-server] Using SQLite-only type search"),n=$.findByType(e,r)),n.length===0)return{content:[{type:"text",text:`No observations found with type "${t}"`}]};let o;if(s==="index"){let a=`Found ${n.length} observation(s) with type "${t}":
|
||||
|
||||
`,d=n.map((l,p)=>q(l,p));o=a+d.join(`
|
||||
|
||||
@@ -599,7 +600,7 @@ Other tips:
|
||||
`);return{content:[{type:"text",text:o}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"get_recent_context",description:"Get recent session context including summaries and observations for a project",inputSchema:i.object({project:i.string().optional().describe("Project name (defaults to current working directory basename)"),limit:i.number().min(1).max(10).default(3).describe("Number of recent sessions to retrieve")}),handler:async c=>{try{let e=c.project||Se(process.cwd()),s=c.limit||3,r=N.getRecentSessionsWithStatus(e,s);if(r.length===0)return{content:[{type:"text",text:`# Recent Session Context
|
||||
|
||||
No previous sessions found for project "${e}".`}]};let t=[];t.push("# Recent Session Context"),t.push(""),t.push(`Showing last ${r.length} session(s) for **${e}**:`),t.push("");for(let n of r)if(n.sdk_session_id){if(t.push("---"),t.push(""),n.has_summary){let o=N.getSummaryForSession(n.sdk_session_id);if(o){let a=o.prompt_number?` (Prompt #${o.prompt_number})`:"";if(t.push(`**Summary${a}**`),t.push(""),o.request&&t.push(`**Request:** ${o.request}`),o.completed&&t.push(`**Completed:** ${o.completed}`),o.learned&&t.push(`**Learned:** ${o.learned}`),o.next_steps&&t.push(`**Next Steps:** ${o.next_steps}`),o.files_read)try{let l=JSON.parse(o.files_read);Array.isArray(l)&&l.length>0&&t.push(`**Files Read:** ${l.join(", ")}`)}catch{o.files_read.trim()&&t.push(`**Files Read:** ${o.files_read}`)}if(o.files_edited)try{let l=JSON.parse(o.files_edited);Array.isArray(l)&&l.length>0&&t.push(`**Files Edited:** ${l.join(", ")}`)}catch{o.files_edited.trim()&&t.push(`**Files Edited:** ${o.files_edited}`)}let d=new Date(o.created_at).toLocaleString();t.push(`**Date:** ${d}`)}}else if(n.status==="active"){t.push("**In Progress**"),t.push(""),n.user_prompt&&t.push(`**Request:** ${n.user_prompt}`);let o=N.getObservationsForSession(n.sdk_session_id);if(o.length>0){t.push(""),t.push(`**Observations (${o.length}):**`);for(let d of o)t.push(`- ${d.title}`)}else t.push(""),t.push("*No observations yet*");t.push(""),t.push("**Status:** Active - summary pending");let a=new Date(n.started_at).toLocaleString();t.push(`**Date:** ${a}`)}else{t.push(`**${n.status.charAt(0).toUpperCase()+n.status.slice(1)}**`),t.push(""),n.user_prompt&&t.push(`**Request:** ${n.user_prompt}`),t.push(""),t.push(`**Status:** ${n.status} - no summary available`);let o=new Date(n.started_at).toLocaleString();t.push(`**Date:** ${o}`)}t.push("")}return{content:[{type:"text",text:t.join(`
|
||||
`)}]}}catch(e){return{content:[{type:"text",text:`Failed to get recent context: ${e.message}`}],isError:!0}}}},{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.',inputSchema:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search for user prompts");let o=await U(e,100,{doc_type:"user_prompt"});if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getUserPromptsByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} user prompts from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchUserPrompts(e,r)),t.length===0)return{content:[{type:"text",text:`No user prompts found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} user prompt(s) matching "${e}":
|
||||
`)}]}}catch(e){return{content:[{type:"text",text:`Failed to get recent context: ${e.message}`}],isError:!0}}}},{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.',inputSchema:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search for user prompts");let o=await M(e,100,{doc_type:"user_prompt"});if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getUserPromptsByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} user prompts from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchUserPrompts(e,r)),t.length===0)return{content:[{type:"text",text:`No user prompts found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} user prompt(s) matching "${e}":
|
||||
|
||||
`,a=t.map((d,l)=>Re(d,l));n=o+a.join(`
|
||||
|
||||
@@ -607,7 +608,7 @@ No previous sessions found for project "${e}".`}]};let t=[];t.push("# Recent Ses
|
||||
|
||||
---
|
||||
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"get_context_timeline",description:'Get a unified timeline of context (observations, sessions, and prompts) around a specific point in time. All record types are interleaved chronologically. Useful for understanding "what was happening when X occurred". Returns depth_before records before anchor + anchor + depth_after records after (total: depth_before + 1 + depth_after mixed records).',inputSchema:i.object({anchor:i.union([i.number().describe("Observation ID to center timeline around"),i.string().describe("Session ID (format: S123) or ISO timestamp to center timeline around")]).describe('Anchor point: observation ID, session ID (e.g., "S123"), or ISO timestamp'),depth_before:i.number().min(0).max(50).default(10).describe("Number of records to retrieve before anchor, not including anchor (default: 10)"),depth_after:i.number().min(0).max(50).default(10).describe("Number of records to retrieve after anchor, not including anchor (default: 10)"),project:i.string().optional().describe("Filter by project name")}),handler:async c=>{try{let f=function(g){return new Date(g).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},h=function(g){return new Date(g).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},b=function(g){return new Date(g).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},_=function(g){return g?Math.ceil(g.length/4):0};var e=f,s=h,r=b,t=_;let{anchor:n,depth_before:o=10,depth_after:a=10,project:d}=c,l,p=n,u;if(typeof n=="number"){let g=N.getObservationById(n);if(!g)return{content:[{type:"text",text:`Observation #${n} not found`}],isError:!0};l=g.created_at_epoch,u=N.getTimelineAroundObservation(n,l,o,a,d)}else if(typeof n=="string")if(n.startsWith("S")||n.startsWith("#S")){let g=n.replace(/^#?S/,""),I=parseInt(g,10),S=N.getSessionSummariesByIds([I]);if(S.length===0)return{content:[{type:"text",text:`Session #${I} not found`}],isError:!0};l=S[0].created_at_epoch,p=`S${I}`,u=N.getTimelineAroundTimestamp(l,o,a,d)}else{let g=new Date(n);if(isNaN(g.getTime()))return{content:[{type:"text",text:`Invalid timestamp: ${n}`}],isError:!0};l=g.getTime(),u=N.getTimelineAroundTimestamp(l,o,a,d)}else return{content:[{type:"text",text:'Invalid anchor: must be observation ID (number), session ID (e.g., "S123"), or ISO timestamp'}],isError:!0};let m=[...u.observations.map(g=>({type:"observation",data:g,epoch:g.created_at_epoch})),...u.sessions.map(g=>({type:"session",data:g,epoch:g.created_at_epoch})),...u.prompts.map(g=>({type:"prompt",data:g,epoch:g.created_at_epoch}))];if(m.sort((g,I)=>g.epoch-I.epoch),m.length===0)return{content:[{type:"text",text:`No context found around ${new Date(l).toLocaleString()} (${o} records before, ${a} records after)`}]};let E=[];E.push(`# Timeline around anchor: ${p}`),E.push(`**Window:** ${o} records before \u2192 ${a} records after | **Items:** ${m.length} (${u.observations.length} obs, ${u.sessions.length} sessions, ${u.prompts.length} prompts)`),E.push(""),E.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),E.push("");let x=new Map;for(let g of m){let I=f(g.epoch);x.has(I)||x.set(I,[]),x.get(I).push(g)}let T=Array.from(x.entries()).sort((g,I)=>{let S=new Date(g[0]).getTime(),O=new Date(I[0]).getTime();return S-O});for(let[g,I]of T){E.push(`### ${g}`),E.push("");let S=null,O="",C=!1;for(let v of I){let F=typeof p=="number"&&v.type==="observation"&&v.data.id===p||typeof p=="string"&&p.startsWith("S")&&v.type==="session"&&`S${v.data.id}`===p;if(v.type==="session"){C&&(E.push(""),C=!1,S=null,O="");let y=v.data,M=y.request||"Session summary",R=`claude-mem://session-summary/${y.id}`,A=F?" \u2190 **ANCHOR**":"";E.push(`**\u{1F3AF} #S${y.id}** ${M} (${b(v.epoch)}) [\u2192](${R})${A}`),E.push("")}else if(v.type==="prompt"){C&&(E.push(""),C=!1,S=null,O="");let y=v.data,M=y.prompt.length>100?y.prompt.substring(0,100)+"...":y.prompt;E.push(`**\u{1F4AC} User Prompt #${y.prompt_number}** (${b(v.epoch)})`),E.push(`> ${M}`),E.push("")}else if(v.type==="observation"){let y=v.data,M="General";M!==S&&(C&&E.push(""),E.push(`**${M}**`),E.push("| ID | Time | T | Title | Tokens |"),E.push("|----|------|---|-------|--------|"),S=M,C=!0,O="");let R="\u2022";switch(y.type){case"bugfix":R="\u{1F534}";break;case"feature":R="\u{1F7E3}";break;case"refactor":R="\u{1F504}";break;case"change":R="\u2705";break;case"discovery":R="\u{1F535}";break;case"decision":R="\u{1F9E0}";break}let A=h(v.epoch),D=y.title||"Untitled",B=_(y.narrative),Y=A!==O?A:"\u2033";O=A;let Z=F?" \u2190 **ANCHOR**":"";E.push(`| #${y.id} | ${Y} | ${R} | ${D}${Z} | ~${B} |`)}}C&&E.push("")}return{content:[{type:"text",text:E.join(`
|
||||
`)}]}}catch(n){return{content:[{type:"text",text:`Timeline query failed: ${n.message}`}],isError:!0}}}},{name:"get_timeline_by_query",description:'Search for observations using natural language and get timeline context around the best match. Two modes: "auto" (default) automatically uses top result as timeline anchor; "interactive" returns top matches for you to choose from. This combines search + timeline into a single operation for faster context discovery.',inputSchema:i.object({query:i.string().describe("Natural language search query to find relevant observations"),mode:i.enum(["auto","interactive"]).default("auto").describe("auto: Automatically use top search result as timeline anchor. interactive: Show top N search results for manual anchor selection."),depth_before:i.number().min(0).max(50).default(10).describe("Number of timeline records before anchor (default: 10)"),depth_after:i.number().min(0).max(50).default(10).describe("Number of timeline records after anchor (default: 10)"),limit:i.number().min(1).max(20).default(5).describe("For interactive mode: number of top search results to display (default: 5)"),project:i.string().optional().describe("Filter by project name")}),handler:async c=>{try{let{query:n,mode:o="auto",depth_before:a=10,depth_after:d=10,limit:l=5,project:p}=c,u=[];if(k)try{console.error("[search-server] Using hybrid semantic search for timeline query");let m=await U(n,100);if(console.error(`[search-server] Chroma returned ${m.ids.length} semantic matches`),m.ids.length>0){let f=Date.now()-7776e6,h=m.ids.filter((b,_)=>{let E=m.metadatas[_];return E&&E.created_at_epoch>f});console.error(`[search-server] ${h.length} results within 90-day window`),h.length>0&&(u=N.getObservationsByIds(h,{orderBy:"date_desc",limit:o==="auto"?1:l}),console.error(`[search-server] Hydrated ${u.length} observations from SQLite`))}}catch(m){console.error("[search-server] Chroma query failed, falling back to FTS5:",m.message)}if(u.length===0&&(console.error("[search-server] Using FTS5 keyword search"),u=$.searchObservations(n,{orderBy:"relevance",limit:o==="auto"?1:l,project:p})),u.length===0)return{content:[{type:"text",text:`No observations found matching "${n}". Try a different search query.`}]};if(o==="interactive"){let m=[];m.push("# Timeline Anchor Search Results"),m.push(""),m.push(`Found ${u.length} observation(s) matching "${n}"`),m.push(""),m.push("To get timeline context around any of these observations, use the `get_context_timeline` tool with the observation ID as the anchor."),m.push(""),m.push(`**Top ${u.length} matches:**`),m.push("");for(let f=0;f<u.length;f++){let h=u[f],b=h.title||`Observation #${h.id}`,_=new Date(h.created_at_epoch).toLocaleString(),E=h.type?`[${h.type}]`:"";m.push(`${f+1}. **${E} ${b}**`),m.push(` - ID: ${h.id}`),m.push(` - Date: ${_}`),h.subtitle&&m.push(` - ${h.subtitle}`),m.push(` - Source: claude-mem://observation/${h.id}`),m.push("")}return{content:[{type:"text",text:m.join(`
|
||||
`)}]}}else{let b=function(S){return new Date(S).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},_=function(S){return new Date(S).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},E=function(S){return new Date(S).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},x=function(S){return S?Math.ceil(S.length/4):0};var e=b,s=_,r=E,t=x;let m=u[0];console.error(`[search-server] Auto mode: Using observation #${m.id} as timeline anchor`);let f=N.getTimelineAroundObservation(m.id,m.created_at_epoch,a,d,p),h=[...f.observations.map(S=>({type:"observation",data:S,epoch:S.created_at_epoch})),...f.sessions.map(S=>({type:"session",data:S,epoch:S.created_at_epoch})),...f.prompts.map(S=>({type:"prompt",data:S,epoch:S.created_at_epoch}))];if(h.sort((S,O)=>S.epoch-O.epoch),h.length===0)return{content:[{type:"text",text:`Found observation #${m.id} matching "${n}", but no timeline context available (${a} records before, ${d} records after).`}]};let T=[];T.push(`# Timeline for query: "${n}"`),T.push(`**Anchor:** Observation #${m.id} - ${m.title||"Untitled"}`),T.push(`**Window:** ${a} records before \u2192 ${d} records after | **Items:** ${h.length} (${f.observations.length} obs, ${f.sessions.length} sessions, ${f.prompts.length} prompts)`),T.push(""),T.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),T.push("");let g=new Map;for(let S of h){let O=b(S.epoch);g.has(O)||g.set(O,[]),g.get(O).push(S)}let I=Array.from(g.entries()).sort((S,O)=>{let C=new Date(S[0]).getTime(),v=new Date(O[0]).getTime();return C-v});for(let[S,O]of I){T.push(`### ${S}`),T.push("");let C=null,v="",F=!1;for(let y of O){let M=y.type==="observation"&&y.data.id===m.id;if(y.type==="session"){F&&(T.push(""),F=!1,C=null,v="");let R=y.data,A=R.request||"Session summary",D=`claude-mem://session-summary/${R.id}`;T.push(`**\u{1F3AF} #S${R.id}** ${A} (${E(y.epoch)}) [\u2192](${D})`),T.push("")}else if(y.type==="prompt"){F&&(T.push(""),F=!1,C=null,v="");let R=y.data,A=R.prompt.length>100?R.prompt.substring(0,100)+"...":R.prompt;T.push(`**\u{1F4AC} User Prompt #${R.prompt_number}** (${E(y.epoch)})`),T.push(`> ${A}`),T.push("")}else if(y.type==="observation"){let R=y.data,A="General";A!==C&&(F&&T.push(""),T.push(`**${A}**`),T.push("| ID | Time | T | Title | Tokens |"),T.push("|----|------|---|-------|--------|"),C=A,F=!0,v="");let D="\u2022";switch(R.type){case"bugfix":D="\u{1F534}";break;case"feature":D="\u{1F7E3}";break;case"refactor":D="\u{1F504}";break;case"change":D="\u2705";break;case"discovery":D="\u{1F535}";break;case"decision":D="\u{1F9E0}";break}let B=_(y.epoch),z=R.title||"Untitled",Y=x(R.narrative),ie=B!==v?B:"\u2033";v=B;let ae=M?" \u2190 **ANCHOR**":"";T.push(`| #${R.id} | ${ie} | ${D} | ${z}${ae} | ~${Y} |`)}}F&&T.push("")}return{content:[{type:"text",text:T.join(`
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"get_context_timeline",description:'Get a unified timeline of context (observations, sessions, and prompts) around a specific point in time. All record types are interleaved chronologically. Useful for understanding "what was happening when X occurred". Returns depth_before records before anchor + anchor + depth_after records after (total: depth_before + 1 + depth_after mixed records).',inputSchema:i.object({anchor:i.union([i.number().describe("Observation ID to center timeline around"),i.string().describe("Session ID (format: S123) or ISO timestamp to center timeline around")]).describe('Anchor point: observation ID, session ID (e.g., "S123"), or ISO timestamp'),depth_before:i.number().min(0).max(50).default(10).describe("Number of records to retrieve before anchor, not including anchor (default: 10)"),depth_after:i.number().min(0).max(50).default(10).describe("Number of records to retrieve after anchor, not including anchor (default: 10)"),project:i.string().optional().describe("Filter by project name")}),handler:async c=>{try{let f=function(g){return new Date(g).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},h=function(g){return new Date(g).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},b=function(g){return new Date(g).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},_=function(g){return g?Math.ceil(g.length/4):0};var e=f,s=h,r=b,t=_;let{anchor:n,depth_before:o=10,depth_after:a=10,project:d}=c,l,p=n,u;if(typeof n=="number"){let g=N.getObservationById(n);if(!g)return{content:[{type:"text",text:`Observation #${n} not found`}],isError:!0};l=g.created_at_epoch,u=N.getTimelineAroundObservation(n,l,o,a,d)}else if(typeof n=="string")if(n.startsWith("S")||n.startsWith("#S")){let g=n.replace(/^#?S/,""),I=parseInt(g,10),S=N.getSessionSummariesByIds([I]);if(S.length===0)return{content:[{type:"text",text:`Session #${I} not found`}],isError:!0};l=S[0].created_at_epoch,p=`S${I}`,u=N.getTimelineAroundTimestamp(l,o,a,d)}else{let g=new Date(n);if(isNaN(g.getTime()))return{content:[{type:"text",text:`Invalid timestamp: ${n}`}],isError:!0};l=g.getTime(),u=N.getTimelineAroundTimestamp(l,o,a,d)}else return{content:[{type:"text",text:'Invalid anchor: must be observation ID (number), session ID (e.g., "S123"), or ISO timestamp'}],isError:!0};let m=[...u.observations.map(g=>({type:"observation",data:g,epoch:g.created_at_epoch})),...u.sessions.map(g=>({type:"session",data:g,epoch:g.created_at_epoch})),...u.prompts.map(g=>({type:"prompt",data:g,epoch:g.created_at_epoch}))];if(m.sort((g,I)=>g.epoch-I.epoch),m.length===0)return{content:[{type:"text",text:`No context found around ${new Date(l).toLocaleString()} (${o} records before, ${a} records after)`}]};let E=[];E.push(`# Timeline around anchor: ${p}`),E.push(`**Window:** ${o} records before \u2192 ${a} records after | **Items:** ${m.length} (${u.observations.length} obs, ${u.sessions.length} sessions, ${u.prompts.length} prompts)`),E.push(""),E.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),E.push("");let x=new Map;for(let g of m){let I=f(g.epoch);x.has(I)||x.set(I,[]),x.get(I).push(g)}let T=Array.from(x.entries()).sort((g,I)=>{let S=new Date(g[0]).getTime(),O=new Date(I[0]).getTime();return S-O});for(let[g,I]of T){E.push(`### ${g}`),E.push("");let S=null,O="",C=!1;for(let v of I){let F=typeof p=="number"&&v.type==="observation"&&v.data.id===p||typeof p=="string"&&p.startsWith("S")&&v.type==="session"&&`S${v.data.id}`===p;if(v.type==="session"){C&&(E.push(""),C=!1,S=null,O="");let y=v.data,U=y.request||"Session summary",R=`claude-mem://session-summary/${y.id}`,A=F?" \u2190 **ANCHOR**":"";E.push(`**\u{1F3AF} #S${y.id}** ${U} (${b(v.epoch)}) [\u2192](${R})${A}`),E.push("")}else if(v.type==="prompt"){C&&(E.push(""),C=!1,S=null,O="");let y=v.data,U=y.prompt.length>100?y.prompt.substring(0,100)+"...":y.prompt;E.push(`**\u{1F4AC} User Prompt #${y.prompt_number}** (${b(v.epoch)})`),E.push(`> ${U}`),E.push("")}else if(v.type==="observation"){let y=v.data,U="General";U!==S&&(C&&E.push(""),E.push(`**${U}**`),E.push("| ID | Time | T | Title | Tokens |"),E.push("|----|------|---|-------|--------|"),S=U,C=!0,O="");let R="\u2022";switch(y.type){case"bugfix":R="\u{1F534}";break;case"feature":R="\u{1F7E3}";break;case"refactor":R="\u{1F504}";break;case"change":R="\u2705";break;case"discovery":R="\u{1F535}";break;case"decision":R="\u{1F9E0}";break}let A=h(v.epoch),D=y.title||"Untitled",B=_(y.narrative),Y=A!==O?A:"\u2033";O=A;let Z=F?" \u2190 **ANCHOR**":"";E.push(`| #${y.id} | ${Y} | ${R} | ${D}${Z} | ~${B} |`)}}C&&E.push("")}return{content:[{type:"text",text:E.join(`
|
||||
`)}]}}catch(n){return{content:[{type:"text",text:`Timeline query failed: ${n.message}`}],isError:!0}}}},{name:"get_timeline_by_query",description:'Search for observations using natural language and get timeline context around the best match. Two modes: "auto" (default) automatically uses top result as timeline anchor; "interactive" returns top matches for you to choose from. This combines search + timeline into a single operation for faster context discovery.',inputSchema:i.object({query:i.string().describe("Natural language search query to find relevant observations"),mode:i.enum(["auto","interactive"]).default("auto").describe("auto: Automatically use top search result as timeline anchor. interactive: Show top N search results for manual anchor selection."),depth_before:i.number().min(0).max(50).default(10).describe("Number of timeline records before anchor (default: 10)"),depth_after:i.number().min(0).max(50).default(10).describe("Number of timeline records after anchor (default: 10)"),limit:i.number().min(1).max(20).default(5).describe("For interactive mode: number of top search results to display (default: 5)"),project:i.string().optional().describe("Filter by project name")}),handler:async c=>{try{let{query:n,mode:o="auto",depth_before:a=10,depth_after:d=10,limit:l=5,project:p}=c,u=[];if(k)try{console.error("[search-server] Using hybrid semantic search for timeline query");let m=await M(n,100);if(console.error(`[search-server] Chroma returned ${m.ids.length} semantic matches`),m.ids.length>0){let f=Date.now()-7776e6,h=m.ids.filter((b,_)=>{let E=m.metadatas[_];return E&&E.created_at_epoch>f});console.error(`[search-server] ${h.length} results within 90-day window`),h.length>0&&(u=N.getObservationsByIds(h,{orderBy:"date_desc",limit:o==="auto"?1:l}),console.error(`[search-server] Hydrated ${u.length} observations from SQLite`))}}catch(m){console.error("[search-server] Chroma query failed, falling back to FTS5:",m.message)}if(u.length===0&&(console.error("[search-server] Using FTS5 keyword search"),u=$.searchObservations(n,{orderBy:"relevance",limit:o==="auto"?1:l,project:p})),u.length===0)return{content:[{type:"text",text:`No observations found matching "${n}". Try a different search query.`}]};if(o==="interactive"){let m=[];m.push("# Timeline Anchor Search Results"),m.push(""),m.push(`Found ${u.length} observation(s) matching "${n}"`),m.push(""),m.push("To get timeline context around any of these observations, use the `get_context_timeline` tool with the observation ID as the anchor."),m.push(""),m.push(`**Top ${u.length} matches:**`),m.push("");for(let f=0;f<u.length;f++){let h=u[f],b=h.title||`Observation #${h.id}`,_=new Date(h.created_at_epoch).toLocaleString(),E=h.type?`[${h.type}]`:"";m.push(`${f+1}. **${E} ${b}**`),m.push(` - ID: ${h.id}`),m.push(` - Date: ${_}`),h.subtitle&&m.push(` - ${h.subtitle}`),m.push(` - Source: claude-mem://observation/${h.id}`),m.push("")}return{content:[{type:"text",text:m.join(`
|
||||
`)}]}}else{let b=function(S){return new Date(S).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},_=function(S){return new Date(S).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},E=function(S){return new Date(S).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},x=function(S){return S?Math.ceil(S.length/4):0};var e=b,s=_,r=E,t=x;let m=u[0];console.error(`[search-server] Auto mode: Using observation #${m.id} as timeline anchor`);let f=N.getTimelineAroundObservation(m.id,m.created_at_epoch,a,d,p),h=[...f.observations.map(S=>({type:"observation",data:S,epoch:S.created_at_epoch})),...f.sessions.map(S=>({type:"session",data:S,epoch:S.created_at_epoch})),...f.prompts.map(S=>({type:"prompt",data:S,epoch:S.created_at_epoch}))];if(h.sort((S,O)=>S.epoch-O.epoch),h.length===0)return{content:[{type:"text",text:`Found observation #${m.id} matching "${n}", but no timeline context available (${a} records before, ${d} records after).`}]};let T=[];T.push(`# Timeline for query: "${n}"`),T.push(`**Anchor:** Observation #${m.id} - ${m.title||"Untitled"}`),T.push(`**Window:** ${a} records before \u2192 ${d} records after | **Items:** ${h.length} (${f.observations.length} obs, ${f.sessions.length} sessions, ${f.prompts.length} prompts)`),T.push(""),T.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),T.push("");let g=new Map;for(let S of h){let O=b(S.epoch);g.has(O)||g.set(O,[]),g.get(O).push(S)}let I=Array.from(g.entries()).sort((S,O)=>{let C=new Date(S[0]).getTime(),v=new Date(O[0]).getTime();return C-v});for(let[S,O]of I){T.push(`### ${S}`),T.push("");let C=null,v="",F=!1;for(let y of O){let U=y.type==="observation"&&y.data.id===m.id;if(y.type==="session"){F&&(T.push(""),F=!1,C=null,v="");let R=y.data,A=R.request||"Session summary",D=`claude-mem://session-summary/${R.id}`;T.push(`**\u{1F3AF} #S${R.id}** ${A} (${E(y.epoch)}) [\u2192](${D})`),T.push("")}else if(y.type==="prompt"){F&&(T.push(""),F=!1,C=null,v="");let R=y.data,A=R.prompt.length>100?R.prompt.substring(0,100)+"...":R.prompt;T.push(`**\u{1F4AC} User Prompt #${R.prompt_number}** (${E(y.epoch)})`),T.push(`> ${A}`),T.push("")}else if(y.type==="observation"){let R=y.data,A="General";A!==C&&(F&&T.push(""),T.push(`**${A}**`),T.push("| ID | Time | T | Title | Tokens |"),T.push("|----|------|---|-------|--------|"),C=A,F=!0,v="");let D="\u2022";switch(R.type){case"bugfix":D="\u{1F534}";break;case"feature":D="\u{1F7E3}";break;case"refactor":D="\u{1F504}";break;case"change":D="\u2705";break;case"discovery":D="\u{1F535}";break;case"decision":D="\u{1F9E0}";break}let B=_(y.epoch),z=R.title||"Untitled",Y=x(R.narrative),ie=B!==v?B:"\u2033";v=B;let ae=U?" \u2190 **ANCHOR**":"";T.push(`| #${R.id} | ${ie} | ${D} | ${z}${ae} | ~${Y} |`)}}F&&T.push("")}return{content:[{type:"text",text:T.join(`
|
||||
`)}]}}}catch(n){return{content:[{type:"text",text:`Timeline query failed: ${n.message}`}],isError:!0}}}}],Q=new he({name:"claude-mem-search",version:"1.0.0"},{capabilities:{tools:{}}});Q.setRequestHandler(ge,async()=>({tools:oe.map(c=>({name:c.name,description:c.description,inputSchema:Te(c.inputSchema)}))}));Q.setRequestHandler(be,async c=>{let e=oe.find(s=>s.name===c.params.name);if(!e)throw new Error(`Unknown tool: ${c.params.name}`);try{return await e.handler(c.params.arguments||{})}catch(s){return{content:[{type:"text",text:`Tool execution failed: ${s.message}`}],isError:!0}}});async function Ie(){let c=new _e;await Q.connect(c),console.error("[search-server] Claude-mem search server started"),setTimeout(async()=>{try{console.error("[search-server] Initializing Chroma client...");let e=new Ee({command:"uvx",args:["chroma-mcp","--client-type","persistent","--data-dir",te],stderr:"ignore"}),s=new fe({name:"claude-mem-search-chroma-client",version:"1.0.0"},{capabilities:{}});await s.connect(e),k=s,console.error("[search-server] Chroma client connected successfully")}catch(e){console.error("[search-server] Failed to initialize Chroma client:",e.message),console.error("[search-server] Falling back to FTS5-only search"),k=null}},0)}Ie().catch(c=>{console.error("[search-server] Fatal error:",c),process.exit(1)});
|
||||
|
||||
@@ -216,6 +216,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
|
||||
@@ -266,6 +266,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let a=Obje
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
ORDER BY project ASC
|
||||
`).all().map(t=>t.project)}getRecentSessionsWithStatus(e,a=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -147,12 +147,12 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
|
||||
// Get recent summaries (optional - may not exist for recent sessions)
|
||||
const recentSummaries = db.db.prepare(`
|
||||
SELECT id, sdk_session_id, request, completed, next_steps, created_at, created_at_epoch
|
||||
SELECT id, sdk_session_id, request, investigated, learned, completed, next_steps, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(project, DISPLAY_SESSION_COUNT + 1) as Array<{ id: number; sdk_session_id: string; request: string | null; completed: string | null; next_steps: string | null; created_at: string; created_at_epoch: number }>;
|
||||
`).all(project, DISPLAY_SESSION_COUNT + 1) as Array<{ id: number; sdk_session_id: string; request: string | null; investigated: string | null; learned: string | null; completed: string | null; next_steps: string | null; created_at: string; created_at_epoch: number }>;
|
||||
|
||||
// If we have neither observations nor summaries, show empty state
|
||||
if (allObservations.length === 0 && recentSummaries.length === 0) {
|
||||
@@ -382,7 +382,25 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
|
||||
// Add full summary details for most recent session
|
||||
const mostRecentSummary = recentSummaries[0];
|
||||
if (mostRecentSummary && (mostRecentSummary.completed || mostRecentSummary.next_steps)) {
|
||||
if (mostRecentSummary && (mostRecentSummary.investigated || mostRecentSummary.learned || mostRecentSummary.completed || mostRecentSummary.next_steps)) {
|
||||
if (mostRecentSummary.investigated) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.blue}Investigated:${colors.reset} ${mostRecentSummary.investigated}`);
|
||||
} else {
|
||||
output.push(`**Investigated**: ${mostRecentSummary.investigated}`);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
|
||||
if (mostRecentSummary.learned) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.yellow}Learned:${colors.reset} ${mostRecentSummary.learned}`);
|
||||
} else {
|
||||
output.push(`**Learned**: ${mostRecentSummary.learned}`);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
|
||||
if (mostRecentSummary.completed) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.green}Completed:${colors.reset} ${mostRecentSummary.completed}`);
|
||||
|
||||
@@ -656,6 +656,7 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
ORDER BY project ASC
|
||||
`);
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ export interface Summary {
|
||||
session_id: string; // claude_session_id (from JOIN)
|
||||
project: string;
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
next_steps: string | null;
|
||||
|
||||
+28
-23
@@ -23,28 +23,30 @@ export function App() {
|
||||
const { preference, resolvedTheme, setThemePreference } = useTheme();
|
||||
const pagination = usePagination(currentFilter);
|
||||
|
||||
// Reset paginated data when filter changes
|
||||
useEffect(() => {
|
||||
setPaginatedObservations([]);
|
||||
setPaginatedSummaries([]);
|
||||
setPaginatedPrompts([]);
|
||||
}, [currentFilter]);
|
||||
// When filtering by project: ONLY use paginated data (API-filtered)
|
||||
// When showing all projects: merge SSE live data with paginated data
|
||||
const allObservations = useMemo(() => {
|
||||
if (currentFilter) {
|
||||
// Project filter active: API handles filtering, ignore SSE items
|
||||
return paginatedObservations;
|
||||
}
|
||||
// No filter: merge SSE + paginated, deduplicate by ID
|
||||
return mergeAndDeduplicateByProject(observations, paginatedObservations);
|
||||
}, [observations, paginatedObservations, currentFilter]);
|
||||
|
||||
// Merge real-time data with paginated data, removing duplicates and filtering by project
|
||||
const allObservations = useMemo(
|
||||
() => mergeAndDeduplicateByProject(observations, paginatedObservations, currentFilter),
|
||||
[observations, paginatedObservations, currentFilter]
|
||||
);
|
||||
const allSummaries = useMemo(() => {
|
||||
if (currentFilter) {
|
||||
return paginatedSummaries;
|
||||
}
|
||||
return mergeAndDeduplicateByProject(summaries, paginatedSummaries);
|
||||
}, [summaries, paginatedSummaries, currentFilter]);
|
||||
|
||||
const allSummaries = useMemo(
|
||||
() => mergeAndDeduplicateByProject(summaries, paginatedSummaries, currentFilter),
|
||||
[summaries, paginatedSummaries, currentFilter]
|
||||
);
|
||||
|
||||
const allPrompts = useMemo(
|
||||
() => mergeAndDeduplicateByProject(prompts, paginatedPrompts, currentFilter),
|
||||
[prompts, paginatedPrompts, currentFilter]
|
||||
);
|
||||
const allPrompts = useMemo(() => {
|
||||
if (currentFilter) {
|
||||
return paginatedPrompts;
|
||||
}
|
||||
return mergeAndDeduplicateByProject(prompts, paginatedPrompts);
|
||||
}, [prompts, paginatedPrompts, currentFilter]);
|
||||
|
||||
// Toggle sidebar
|
||||
const toggleSidebar = useCallback(() => {
|
||||
@@ -72,13 +74,16 @@ export function App() {
|
||||
} catch (error) {
|
||||
console.error('Failed to load more data:', error);
|
||||
}
|
||||
}, [pagination.observations, pagination.summaries, pagination.prompts]);
|
||||
}, [currentFilter, pagination.observations, pagination.summaries, pagination.prompts]);
|
||||
|
||||
// Load first page only when filter changes
|
||||
// Reset paginated data and load first page when filter changes
|
||||
useEffect(() => {
|
||||
setPaginatedObservations([]);
|
||||
setPaginatedSummaries([]);
|
||||
setPaginatedPrompts([]);
|
||||
handleLoadMore();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentFilter]); // Only re-run when filter changes, not when handleLoadMore changes
|
||||
}, [currentFilter]);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
|
||||
@@ -20,6 +20,9 @@ export function SummaryCard({ summary }: SummaryCardProps) {
|
||||
{summary.request && (
|
||||
<div className="card-title">Request: {summary.request}</div>
|
||||
)}
|
||||
{summary.investigated && (
|
||||
<div className="card-subtitle">Investigated: {summary.investigated}</div>
|
||||
)}
|
||||
{summary.learned && (
|
||||
<div className="card-subtitle">Learned: {summary.learned}</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { Observation, Summary, UserPrompt } from '../types';
|
||||
import { UI } from '../constants/ui';
|
||||
import { API_ENDPOINTS } from '../constants/api';
|
||||
@@ -19,42 +19,40 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
|
||||
isLoading: false,
|
||||
hasMore: true
|
||||
});
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
// Use refs to avoid stale closures and prevent infinite loops
|
||||
// Track offset and filter in refs to handle synchronous resets
|
||||
const offsetRef = useRef(0);
|
||||
const lastFilterRef = useRef(currentFilter);
|
||||
const stateRef = useRef(state);
|
||||
const offsetRef = useRef(offset);
|
||||
|
||||
useEffect(() => {
|
||||
stateRef.current = state;
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
offsetRef.current = offset;
|
||||
}, [offset]);
|
||||
|
||||
// Reset pagination when filter changes
|
||||
useEffect(() => {
|
||||
setOffset(0);
|
||||
setState({
|
||||
isLoading: false,
|
||||
hasMore: true
|
||||
});
|
||||
}, [currentFilter]);
|
||||
|
||||
/**
|
||||
* Load more items from the API
|
||||
* Automatically resets offset to 0 if filter has changed
|
||||
*/
|
||||
const loadMore = useCallback(async (): Promise<DataItem[]> => {
|
||||
// Check if filter changed - if so, reset pagination synchronously
|
||||
const filterChanged = lastFilterRef.current !== currentFilter;
|
||||
|
||||
if (filterChanged) {
|
||||
offsetRef.current = 0;
|
||||
lastFilterRef.current = currentFilter;
|
||||
|
||||
// Reset state both in React state and ref synchronously
|
||||
const newState = { isLoading: false, hasMore: true };
|
||||
setState(newState);
|
||||
stateRef.current = newState; // Update ref immediately to avoid stale checks
|
||||
}
|
||||
|
||||
// Prevent concurrent requests using ref (always current)
|
||||
if (stateRef.current.isLoading || !stateRef.current.hasMore) {
|
||||
// Skip this check if we just reset the filter - we want to load the first page
|
||||
if (!filterChanged && (stateRef.current.isLoading || !stateRef.current.hasMore)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: true }));
|
||||
|
||||
try {
|
||||
// Build query params using ref (always current)
|
||||
// Build query params using current offset from ref
|
||||
const params = new URLSearchParams({
|
||||
offset: offsetRef.current.toString(),
|
||||
limit: UI.PAGINATION_PAGE_SIZE.toString()
|
||||
@@ -71,7 +69,7 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
|
||||
throw new Error(`Failed to load ${dataType}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json() as { items: DataItem[], hasMore: boolean };
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
@@ -79,14 +77,16 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
|
||||
hasMore: data.hasMore
|
||||
}));
|
||||
|
||||
setOffset(prev => prev + UI.PAGINATION_PAGE_SIZE);
|
||||
return data.items as DataItem[];
|
||||
// Increment offset after successful load
|
||||
offsetRef.current += UI.PAGINATION_PAGE_SIZE;
|
||||
|
||||
return data.items;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${dataType}:`, error);
|
||||
setState(prev => ({ ...prev, isLoading: false }));
|
||||
return [];
|
||||
}
|
||||
}, [currentFilter, endpoint, dataType]); // Only stable values - no state/offset deps
|
||||
}, [currentFilter, endpoint, dataType]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface Summary {
|
||||
session_id: string;
|
||||
project: string;
|
||||
request?: string;
|
||||
investigated?: string;
|
||||
learned?: string;
|
||||
completed?: string;
|
||||
next_steps?: string;
|
||||
|
||||
@@ -4,25 +4,21 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Merge real-time SSE items with paginated items, removing duplicates and filtering by project
|
||||
* @param liveItems - Items from SSE stream
|
||||
* @param paginatedItems - Items from pagination API (already filtered by project)
|
||||
* @param projectFilter - Current project filter (empty string = all projects)
|
||||
* Merge real-time SSE items with paginated items, removing duplicates by ID
|
||||
* NOTE: This should ONLY be used when no project filter is active.
|
||||
* When filtering, use ONLY paginated data (API-filtered).
|
||||
*
|
||||
* @param liveItems - Items from SSE stream (unfiltered)
|
||||
* @param paginatedItems - Items from pagination API
|
||||
* @returns Merged and deduplicated array
|
||||
*/
|
||||
export function mergeAndDeduplicateByProject<T extends { id: number; project?: string }>(
|
||||
liveItems: T[],
|
||||
paginatedItems: T[],
|
||||
projectFilter: string
|
||||
paginatedItems: T[]
|
||||
): T[] {
|
||||
// Filter SSE items by current project (pagination is already filtered)
|
||||
const filteredLive = projectFilter
|
||||
? liveItems.filter(item => item.project === projectFilter)
|
||||
: liveItems;
|
||||
|
||||
// Deduplicate using Set
|
||||
// Deduplicate by ID
|
||||
const seen = new Set<number>();
|
||||
return [...filteredLive, ...paginatedItems].filter(item => {
|
||||
return [...liveItems, ...paginatedItems].filter(item => {
|
||||
if (seen.has(item.id)) return false;
|
||||
seen.add(item.id);
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user