claude-self-reflect 2.5.11 → 2.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile.async-importer +26 -0
- package/README.md +5 -2
- package/config/qdrant-config.yaml +59 -0
- package/docker-compose.yaml +48 -7
- package/installer/cli.js +55 -1
- package/mcp-server/src/__main__.py +13 -0
- package/mcp-server/src/embedding_manager.py +237 -0
- package/mcp-server/src/status.py +163 -0
- package/package.json +2 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
FROM python:3.11-slim
|
|
2
|
+
|
|
3
|
+
# Install system dependencies
|
|
4
|
+
RUN apt-get update && apt-get install -y \
|
|
5
|
+
gcc \
|
|
6
|
+
g++ \
|
|
7
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
8
|
+
|
|
9
|
+
# Set working directory
|
|
10
|
+
WORKDIR /app
|
|
11
|
+
|
|
12
|
+
# Copy requirements
|
|
13
|
+
COPY scripts/requirements.txt /app/
|
|
14
|
+
|
|
15
|
+
# Install Python dependencies
|
|
16
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
17
|
+
|
|
18
|
+
# Copy the streaming importer script
|
|
19
|
+
COPY scripts/streaming-importer.py /scripts/
|
|
20
|
+
|
|
21
|
+
# Set environment variables for memory management
|
|
22
|
+
ENV MALLOC_ARENA_MAX=2
|
|
23
|
+
ENV PYTHONUNBUFFERED=1
|
|
24
|
+
|
|
25
|
+
# Run the streaming importer
|
|
26
|
+
CMD ["python", "/scripts/streaming-importer.py"]
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Claude forgets everything. This fixes that.
|
|
|
6
6
|
|
|
7
7
|
Ask Claude about past conversations. Get actual answers. **100% local by default** - your conversations never leave your machine. Cloud-enhanced search available when you need it.
|
|
8
8
|
|
|
9
|
+
**✅ Proven at Scale**: Successfully indexed 682 conversation files with 100% reliability. No data loss, no corruption, just seamless conversation memory that works.
|
|
10
|
+
|
|
9
11
|
**Before**: "I don't have access to previous conversations"
|
|
10
12
|
**After**:
|
|
11
13
|
```
|
|
@@ -129,9 +131,10 @@ Claude: [Searches across ALL your projects]
|
|
|
129
131
|
Recent conversations matter more. Old ones fade. Like your brain, but reliable.
|
|
130
132
|
|
|
131
133
|
### 🚀 Performance
|
|
132
|
-
- **Search**: 200-350ms response time
|
|
133
|
-
- **Import**: 2-second response for new conversations
|
|
134
|
+
- **Search**: 200-350ms response time across 682 indexed conversations
|
|
135
|
+
- **Import**: 2-second response for new conversations
|
|
134
136
|
- **Memory**: 50MB operational target with smart chunking
|
|
137
|
+
- **Scale**: 100% indexing success rate across all conversation types
|
|
135
138
|
|
|
136
139
|
## The Technical Stack
|
|
137
140
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
log_level: INFO
|
|
2
|
+
|
|
3
|
+
storage:
|
|
4
|
+
# Where to store all the data
|
|
5
|
+
storage_path: ./storage
|
|
6
|
+
|
|
7
|
+
# Where to store snapshots
|
|
8
|
+
snapshots_path: ./snapshots
|
|
9
|
+
|
|
10
|
+
# CRITICAL: Store payloads on disk to save memory
|
|
11
|
+
on_disk_payload: true
|
|
12
|
+
|
|
13
|
+
performance:
|
|
14
|
+
# Reduce number of search threads to save memory
|
|
15
|
+
max_search_threads: 2
|
|
16
|
+
|
|
17
|
+
# Conservative CPU budget for optimization
|
|
18
|
+
optimizer_cpu_budget: 1
|
|
19
|
+
|
|
20
|
+
optimizers:
|
|
21
|
+
# Memory-optimized settings
|
|
22
|
+
deleted_threshold: 0.2
|
|
23
|
+
vacuum_min_vector_number: 1000
|
|
24
|
+
default_segment_number: 1
|
|
25
|
+
# Reduce max segment size to prevent memory spikes
|
|
26
|
+
max_segment_size_kb: 50000 # 50MB max
|
|
27
|
+
# Lower indexing threshold to use disk sooner
|
|
28
|
+
indexing_threshold_kb: 1000 # 1MB
|
|
29
|
+
flush_interval_sec: 5
|
|
30
|
+
max_optimization_threads: 1
|
|
31
|
+
|
|
32
|
+
# CRITICAL: Store HNSW indexes on disk
|
|
33
|
+
hnsw_index:
|
|
34
|
+
m: 16
|
|
35
|
+
ef_construct: 100
|
|
36
|
+
full_scan_threshold_kb: 1000
|
|
37
|
+
max_indexing_threads: 2
|
|
38
|
+
# Store HNSW index on disk - CRITICAL for memory savings
|
|
39
|
+
on_disk: true
|
|
40
|
+
|
|
41
|
+
collection:
|
|
42
|
+
# Default vectors storage on disk
|
|
43
|
+
vectors:
|
|
44
|
+
on_disk: true
|
|
45
|
+
|
|
46
|
+
# Single replica to save memory
|
|
47
|
+
replication_factor: 1
|
|
48
|
+
write_consistency_factor: 1
|
|
49
|
+
|
|
50
|
+
service:
|
|
51
|
+
max_request_size_mb: 32
|
|
52
|
+
max_workers: 2
|
|
53
|
+
host: 0.0.0.0
|
|
54
|
+
http_port: 6333
|
|
55
|
+
grpc_port: 6334
|
|
56
|
+
enable_cors: true
|
|
57
|
+
|
|
58
|
+
# Disable telemetry
|
|
59
|
+
telemetry_disabled: true
|
package/docker-compose.yaml
CHANGED
|
@@ -8,7 +8,7 @@ services:
|
|
|
8
8
|
command: chown -R 1000:1000 /config
|
|
9
9
|
volumes:
|
|
10
10
|
- ${CONFIG_PATH:-~/.claude-self-reflect/config}:/config
|
|
11
|
-
profiles: ["watch", "mcp", "import"]
|
|
11
|
+
profiles: ["watch", "mcp", "import", "async"]
|
|
12
12
|
|
|
13
13
|
# Qdrant vector database - the heart of semantic search
|
|
14
14
|
qdrant:
|
|
@@ -104,18 +104,59 @@ services:
|
|
|
104
104
|
- VOYAGE_API_KEY=${VOYAGE_API_KEY:-}
|
|
105
105
|
- VOYAGE_KEY=${VOYAGE_KEY:-}
|
|
106
106
|
- PREFER_LOCAL_EMBEDDINGS=${PREFER_LOCAL_EMBEDDINGS:-true}
|
|
107
|
-
- WATCH_INTERVAL=${WATCH_INTERVAL:-
|
|
108
|
-
- MAX_MEMORY_MB=${MAX_MEMORY_MB:-
|
|
109
|
-
- OPERATIONAL_MEMORY_MB=${OPERATIONAL_MEMORY_MB:-
|
|
110
|
-
- CHUNK_SIZE=${CHUNK_SIZE:-5}
|
|
107
|
+
- WATCH_INTERVAL=${WATCH_INTERVAL:-1} # Aggressive: 5x faster detection (minimum 1 second)
|
|
108
|
+
- MAX_MEMORY_MB=${MAX_MEMORY_MB:-2000} # Ultra conservative to prevent memory leak
|
|
109
|
+
- OPERATIONAL_MEMORY_MB=${OPERATIONAL_MEMORY_MB:-1500} # 1.5GB operational (25% of 8GB)
|
|
110
|
+
- CHUNK_SIZE=${CHUNK_SIZE:-5} # Minimal batch size
|
|
111
|
+
- HOT_WINDOW_MINUTES=${HOT_WINDOW_MINUTES:-15} # Keep files HOT longer
|
|
112
|
+
- MAX_COLD_FILES_PER_CYCLE=${MAX_COLD_FILES_PER_CYCLE:-5} # Single file processing
|
|
113
|
+
- PARALLEL_WORKERS=${PARALLEL_WORKERS:-8} # Enable parallel embedding workers
|
|
111
114
|
- PYTHONUNBUFFERED=1
|
|
112
115
|
- LOGS_DIR=/logs
|
|
113
116
|
- FASTEMBED_CACHE_PATH=/root/.cache/fastembed
|
|
114
117
|
- CURRENT_PROJECT_PATH=${PWD} # Pass current project path for prioritization
|
|
118
|
+
- MALLOC_ARENA_MAX=2 # MEMORY LEAK FIX: Limit glibc malloc arenas
|
|
119
|
+
- THREAD_POOL_WORKERS=${THREAD_POOL_WORKERS:-2} # AsyncEmbedder thread pool size (speed vs stability)
|
|
120
|
+
- THREAD_POOL_RECYCLE_FILES=${THREAD_POOL_RECYCLE_FILES:-50} # Files before recycling thread pool
|
|
115
121
|
restart: unless-stopped
|
|
116
122
|
profiles: ["watch"]
|
|
117
|
-
mem_limit:
|
|
118
|
-
memswap_limit:
|
|
123
|
+
mem_limit: 8g
|
|
124
|
+
memswap_limit: 8g
|
|
125
|
+
|
|
126
|
+
# Async streaming importer - Ground-up async rewrite
|
|
127
|
+
async-importer:
|
|
128
|
+
build:
|
|
129
|
+
context: .
|
|
130
|
+
dockerfile: Dockerfile.async-importer
|
|
131
|
+
container_name: claude-reflection-async
|
|
132
|
+
depends_on:
|
|
133
|
+
- qdrant
|
|
134
|
+
volumes:
|
|
135
|
+
- ${CLAUDE_LOGS_PATH:-~/.claude/projects}:/logs:ro
|
|
136
|
+
- ${CONFIG_PATH:-~/.claude-self-reflect/config}:/config
|
|
137
|
+
- ./scripts:/scripts:ro
|
|
138
|
+
environment:
|
|
139
|
+
- QDRANT_URL=http://qdrant:6333
|
|
140
|
+
- STATE_FILE=/config/imported-files.json
|
|
141
|
+
- VOYAGE_API_KEY=${VOYAGE_API_KEY:-}
|
|
142
|
+
- VOYAGE_KEY=${VOYAGE_KEY:-}
|
|
143
|
+
- PREFER_LOCAL_EMBEDDINGS=${PREFER_LOCAL_EMBEDDINGS:-true}
|
|
144
|
+
- WATCH_INTERVAL=${WATCH_INTERVAL:-5}
|
|
145
|
+
- MAX_MEMORY_MB=${MAX_MEMORY_MB:-2000}
|
|
146
|
+
- OPERATIONAL_MEMORY_MB=${OPERATIONAL_MEMORY_MB:-1500}
|
|
147
|
+
- CHUNK_SIZE=${CHUNK_SIZE:-5}
|
|
148
|
+
- HOT_WINDOW_MINUTES=${HOT_WINDOW_MINUTES:-15}
|
|
149
|
+
- MAX_COLD_FILES_PER_CYCLE=${MAX_COLD_FILES_PER_CYCLE:-5}
|
|
150
|
+
- THREAD_POOL_WORKERS=${THREAD_POOL_WORKERS:-2}
|
|
151
|
+
- PYTHONUNBUFFERED=1
|
|
152
|
+
- LOGS_DIR=/logs
|
|
153
|
+
- FASTEMBED_CACHE_PATH=/root/.cache/fastembed
|
|
154
|
+
- CURRENT_PROJECT_PATH=${PWD}
|
|
155
|
+
- MALLOC_ARENA_MAX=2
|
|
156
|
+
restart: unless-stopped
|
|
157
|
+
profiles: ["async"]
|
|
158
|
+
mem_limit: 4g
|
|
159
|
+
memswap_limit: 4g
|
|
119
160
|
|
|
120
161
|
# MCP server for Claude integration
|
|
121
162
|
mcp-server:
|
package/installer/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ const __dirname = dirname(__filename);
|
|
|
10
10
|
|
|
11
11
|
const commands = {
|
|
12
12
|
setup: 'Run the setup wizard to configure Claude Self-Reflect',
|
|
13
|
+
status: 'Get indexing status as JSON (overall + per-project breakdown)',
|
|
13
14
|
doctor: 'Check your installation and diagnose issues',
|
|
14
15
|
help: 'Show this help message'
|
|
15
16
|
};
|
|
@@ -27,6 +28,53 @@ async function setup() {
|
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
async function status() {
|
|
32
|
+
// Call the Python MCP server's --status command
|
|
33
|
+
const mcpServerPath = join(__dirname, '..', 'mcp-server');
|
|
34
|
+
const venvPython = join(mcpServerPath, 'venv', 'bin', 'python');
|
|
35
|
+
const mcpModule = join(mcpServerPath, 'src');
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const child = spawn(venvPython, ['-m', 'src', '--status'], {
|
|
39
|
+
cwd: mcpServerPath,
|
|
40
|
+
stdio: ['inherit', 'pipe', 'pipe']
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let stdout = '';
|
|
44
|
+
let stderr = '';
|
|
45
|
+
|
|
46
|
+
child.stdout.on('data', (data) => {
|
|
47
|
+
stdout += data.toString();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
child.stderr.on('data', (data) => {
|
|
51
|
+
stderr += data.toString();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
child.on('exit', (code) => {
|
|
55
|
+
if (code === 0) {
|
|
56
|
+
// Output the JSON directly for other tools to parse
|
|
57
|
+
process.stdout.write(stdout);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
} else {
|
|
60
|
+
console.error('Error getting status:', stderr || 'Unknown error');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Handle timeout
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
child.kill('SIGTERM');
|
|
68
|
+
console.error('Status check timed out');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}, 10000); // 10 second timeout
|
|
71
|
+
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Failed to execute status command:', error.message);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
30
78
|
async function doctor() {
|
|
31
79
|
console.log('🔍 Checking Claude Self-Reflect installation...\n');
|
|
32
80
|
|
|
@@ -124,8 +172,11 @@ function help() {
|
|
|
124
172
|
console.log(' claude-self-reflect setup --voyage-key=pa-1234567890');
|
|
125
173
|
console.log(' claude-self-reflect setup --local');
|
|
126
174
|
console.log(' claude-self-reflect setup --debug # For troubleshooting');
|
|
175
|
+
console.log(' claude-self-reflect status # Get indexing status as JSON');
|
|
127
176
|
|
|
128
|
-
console.log('\nFor more information:
|
|
177
|
+
console.log('\nFor more information:');
|
|
178
|
+
console.log(' Documentation: https://github.com/ramakay/claude-self-reflect');
|
|
179
|
+
console.log(' Status API: See docs/api-reference.md#cli-status-interface');
|
|
129
180
|
}
|
|
130
181
|
|
|
131
182
|
// Main
|
|
@@ -135,6 +186,9 @@ switch (command) {
|
|
|
135
186
|
case 'setup':
|
|
136
187
|
setup();
|
|
137
188
|
break;
|
|
189
|
+
case 'status':
|
|
190
|
+
status();
|
|
191
|
+
break;
|
|
138
192
|
case 'doctor':
|
|
139
193
|
doctor();
|
|
140
194
|
break;
|
|
@@ -10,9 +10,22 @@ def main():
|
|
|
10
10
|
"--transport",
|
|
11
11
|
choices=["stdio", "sse"],
|
|
12
12
|
default="stdio",
|
|
13
|
+
help="Transport protocol for MCP server (default: stdio)"
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--status",
|
|
17
|
+
action="store_true",
|
|
18
|
+
help="Get indexing status as JSON with overall and per-project breakdown"
|
|
13
19
|
)
|
|
14
20
|
args = parser.parse_args()
|
|
15
21
|
|
|
22
|
+
# Handle status request with early exit (avoid loading heavy MCP dependencies)
|
|
23
|
+
if args.status:
|
|
24
|
+
from .status import get_status
|
|
25
|
+
import json
|
|
26
|
+
print(json.dumps(get_status()))
|
|
27
|
+
return
|
|
28
|
+
|
|
16
29
|
# Import is done here to make sure environment variables are loaded
|
|
17
30
|
from .server import mcp
|
|
18
31
|
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Robust embedding model manager with proper cache handling."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
from typing import Optional, List, Union
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import threading
|
|
11
|
+
import signal
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
class EmbeddingManager:
|
|
16
|
+
"""Manages embedding models with proper cache and lock handling."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.model = None
|
|
20
|
+
self.model_type = None # 'local' or 'voyage'
|
|
21
|
+
self.voyage_client = None
|
|
22
|
+
|
|
23
|
+
# Configuration
|
|
24
|
+
self.prefer_local = os.getenv('PREFER_LOCAL_EMBEDDINGS', 'true').lower() == 'true'
|
|
25
|
+
self.voyage_key = os.getenv('VOYAGE_KEY') or os.getenv('VOYAGE_KEY-2')
|
|
26
|
+
self.embedding_model = os.getenv('EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2')
|
|
27
|
+
self.download_timeout = int(os.getenv('FASTEMBED_DOWNLOAD_TIMEOUT', '30'))
|
|
28
|
+
|
|
29
|
+
# Set cache directory to our controlled location
|
|
30
|
+
self.cache_dir = Path(__file__).parent.parent / '.fastembed-cache'
|
|
31
|
+
|
|
32
|
+
def _clean_stale_locks(self):
|
|
33
|
+
"""Clean up any stale lock files from previous runs."""
|
|
34
|
+
locks_dir = self.cache_dir / '.locks'
|
|
35
|
+
if locks_dir.exists():
|
|
36
|
+
logger.info(f"Cleaning stale locks in {locks_dir}")
|
|
37
|
+
try:
|
|
38
|
+
# Remove all lock files older than 5 minutes
|
|
39
|
+
import time
|
|
40
|
+
current_time = time.time()
|
|
41
|
+
for lock_file in locks_dir.glob('**/*.lock'):
|
|
42
|
+
try:
|
|
43
|
+
age = current_time - lock_file.stat().st_mtime
|
|
44
|
+
if age > 300: # 5 minutes
|
|
45
|
+
lock_file.unlink()
|
|
46
|
+
logger.debug(f"Removed stale lock: {lock_file.name}")
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.debug(f"Could not remove lock {lock_file}: {e}")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.warning(f"Error cleaning locks: {e}")
|
|
51
|
+
|
|
52
|
+
def initialize(self) -> bool:
|
|
53
|
+
"""Initialize embedding model based on user preference."""
|
|
54
|
+
logger.info("Initializing embedding manager...")
|
|
55
|
+
|
|
56
|
+
# Clean up any stale locks first
|
|
57
|
+
self._clean_stale_locks()
|
|
58
|
+
|
|
59
|
+
if self.prefer_local:
|
|
60
|
+
# User wants local - try local only, don't fallback to cloud
|
|
61
|
+
if self._try_initialize_local():
|
|
62
|
+
return True
|
|
63
|
+
logger.error("Local embeddings failed and user prefers local - not falling back to cloud")
|
|
64
|
+
return False
|
|
65
|
+
else:
|
|
66
|
+
# User prefers Voyage AI
|
|
67
|
+
if self.voyage_key and self._try_initialize_voyage():
|
|
68
|
+
return True
|
|
69
|
+
logger.warning("Voyage AI failed, trying local as fallback...")
|
|
70
|
+
if self._try_initialize_local():
|
|
71
|
+
return True
|
|
72
|
+
logger.error("Both Voyage AI and local embeddings failed")
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def _try_initialize_local(self) -> bool:
|
|
76
|
+
"""Try to initialize local FastEmbed model with timeout and optimizations."""
|
|
77
|
+
try:
|
|
78
|
+
logger.info(f"Attempting to load local model: {self.embedding_model}")
|
|
79
|
+
|
|
80
|
+
# CRITICAL OPTIMIZATION: Set thread limits BEFORE loading model
|
|
81
|
+
# This prevents ONNX Runtime and BLAS from over-subscribing CPU
|
|
82
|
+
os.environ['OMP_NUM_THREADS'] = '1'
|
|
83
|
+
os.environ['MKL_NUM_THREADS'] = '1'
|
|
84
|
+
os.environ['OPENBLAS_NUM_THREADS'] = '1'
|
|
85
|
+
os.environ['NUMEXPR_NUM_THREADS'] = '1'
|
|
86
|
+
logger.info("Set thread limits to prevent CPU over-subscription")
|
|
87
|
+
|
|
88
|
+
# Ensure cache directory exists and is writable
|
|
89
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
|
|
91
|
+
# Set FASTEMBED_CACHE_PATH to our controlled directory
|
|
92
|
+
os.environ['FASTEMBED_CACHE_PATH'] = str(self.cache_dir)
|
|
93
|
+
logger.info(f"Using cache directory: {self.cache_dir}")
|
|
94
|
+
|
|
95
|
+
# Also set HF_HOME to avoid any HuggingFace cache issues
|
|
96
|
+
os.environ['HF_HOME'] = str(self.cache_dir / 'huggingface')
|
|
97
|
+
|
|
98
|
+
model_cache = self.cache_dir / 'models--qdrant--all-MiniLM-L6-v2-onnx'
|
|
99
|
+
|
|
100
|
+
if model_cache.exists():
|
|
101
|
+
logger.info("Model cache found, loading from cache...")
|
|
102
|
+
else:
|
|
103
|
+
logger.info(f"Model cache not found, will download (timeout: {self.download_timeout}s)")
|
|
104
|
+
logger.info("Note: First download may take 1-2 minutes")
|
|
105
|
+
|
|
106
|
+
# Force alternative download if HuggingFace is problematic
|
|
107
|
+
# This uses Qdrant's CDN which is more reliable
|
|
108
|
+
if os.getenv('FASTEMBED_SKIP_HUGGINGFACE', 'true').lower() == 'true':
|
|
109
|
+
os.environ['HF_HUB_OFFLINE'] = '1'
|
|
110
|
+
logger.info("Using alternative download sources (Qdrant CDN)")
|
|
111
|
+
|
|
112
|
+
# Use a thread with timeout for model initialization
|
|
113
|
+
success = False
|
|
114
|
+
error = None
|
|
115
|
+
|
|
116
|
+
def init_model():
|
|
117
|
+
nonlocal success, error
|
|
118
|
+
try:
|
|
119
|
+
from fastembed import TextEmbedding
|
|
120
|
+
# Initialize with optimized settings
|
|
121
|
+
# Note: FastEmbed uses these environment variables internally
|
|
122
|
+
self.model = TextEmbedding(
|
|
123
|
+
model_name=self.embedding_model,
|
|
124
|
+
threads=1 # Single thread per worker to prevent over-subscription
|
|
125
|
+
)
|
|
126
|
+
self.model_type = 'local'
|
|
127
|
+
success = True
|
|
128
|
+
logger.info(f"Successfully initialized local model: {self.embedding_model} with single-thread mode")
|
|
129
|
+
except Exception as e:
|
|
130
|
+
error = e
|
|
131
|
+
logger.error(f"Failed to initialize local model: {e}")
|
|
132
|
+
|
|
133
|
+
# Start initialization in a thread
|
|
134
|
+
thread = threading.Thread(target=init_model)
|
|
135
|
+
thread.daemon = True
|
|
136
|
+
thread.start()
|
|
137
|
+
thread.join(timeout=self.download_timeout)
|
|
138
|
+
|
|
139
|
+
if thread.is_alive():
|
|
140
|
+
logger.error(f"Model initialization timed out after {self.download_timeout}s")
|
|
141
|
+
logger.info("Tip: Set FASTEMBED_SKIP_HUGGINGFACE=true to use alternative download sources")
|
|
142
|
+
# Thread will continue in background but we move on
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
return success
|
|
146
|
+
|
|
147
|
+
except ImportError:
|
|
148
|
+
logger.error("FastEmbed not installed. Install with: pip install fastembed")
|
|
149
|
+
return False
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"Unexpected error initializing local embeddings: {e}")
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
def _try_initialize_voyage(self) -> bool:
|
|
155
|
+
"""Try to initialize Voyage AI client."""
|
|
156
|
+
try:
|
|
157
|
+
logger.info("Attempting to initialize Voyage AI...")
|
|
158
|
+
import voyageai
|
|
159
|
+
self.voyage_client = voyageai.Client(api_key=self.voyage_key)
|
|
160
|
+
|
|
161
|
+
# Test the client with a simple embedding
|
|
162
|
+
test_result = self.voyage_client.embed(
|
|
163
|
+
texts=["test"],
|
|
164
|
+
model="voyage-3",
|
|
165
|
+
input_type="document"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if test_result and test_result.embeddings:
|
|
169
|
+
self.model_type = 'voyage'
|
|
170
|
+
logger.info("Successfully initialized Voyage AI")
|
|
171
|
+
return True
|
|
172
|
+
else:
|
|
173
|
+
logger.error("Voyage AI test embedding failed")
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Failed to initialize Voyage AI: {e}")
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def embed(self, texts: Union[str, List[str]], input_type: str = "document") -> Optional[List[List[float]]]:
|
|
181
|
+
"""Generate embeddings using the active model."""
|
|
182
|
+
if not self.model and not self.voyage_client:
|
|
183
|
+
logger.error("No embedding model initialized")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
# Ensure texts is a list
|
|
187
|
+
if isinstance(texts, str):
|
|
188
|
+
texts = [texts]
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
if self.model_type == 'local':
|
|
192
|
+
# FastEmbed returns a generator, convert to list
|
|
193
|
+
embeddings = list(self.model.embed(texts))
|
|
194
|
+
return [emb.tolist() for emb in embeddings]
|
|
195
|
+
|
|
196
|
+
elif self.model_type == 'voyage':
|
|
197
|
+
result = self.voyage_client.embed(
|
|
198
|
+
texts=texts,
|
|
199
|
+
model="voyage-3-lite" if input_type == "query" else "voyage-3",
|
|
200
|
+
input_type=input_type
|
|
201
|
+
)
|
|
202
|
+
return result.embeddings
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(f"Error generating embeddings: {e}")
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def get_vector_dimension(self) -> int:
|
|
209
|
+
"""Get the dimension of embeddings."""
|
|
210
|
+
if self.model_type == 'local':
|
|
211
|
+
return 384 # all-MiniLM-L6-v2 dimension
|
|
212
|
+
elif self.model_type == 'voyage':
|
|
213
|
+
return 1024 # voyage-3 dimension
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
def get_model_info(self) -> dict:
|
|
217
|
+
"""Get information about the active model."""
|
|
218
|
+
return {
|
|
219
|
+
'type': self.model_type,
|
|
220
|
+
'model': self.embedding_model if self.model_type == 'local' else 'voyage-3',
|
|
221
|
+
'dimension': self.get_vector_dimension(),
|
|
222
|
+
'prefer_local': self.prefer_local,
|
|
223
|
+
'has_voyage_key': bool(self.voyage_key)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# Global instance
|
|
228
|
+
_embedding_manager = None
|
|
229
|
+
|
|
230
|
+
def get_embedding_manager() -> EmbeddingManager:
|
|
231
|
+
"""Get or create the global embedding manager."""
|
|
232
|
+
global _embedding_manager
|
|
233
|
+
if _embedding_manager is None:
|
|
234
|
+
_embedding_manager = EmbeddingManager()
|
|
235
|
+
if not _embedding_manager.initialize():
|
|
236
|
+
raise RuntimeError("Failed to initialize any embedding model")
|
|
237
|
+
return _embedding_manager
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Ultra-fast status checker for Claude Self Reflect indexing progress.
|
|
2
|
+
|
|
3
|
+
This module provides lightweight indexing status without loading heavy MCP dependencies.
|
|
4
|
+
Designed for <20ms execution time to support status bars and shell scripts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_project_name_from_path(file_path: str) -> str:
|
|
13
|
+
"""Extract project name from JSONL file path.
|
|
14
|
+
|
|
15
|
+
Handles paths like:
|
|
16
|
+
- ~/.claude/projects/-Users-ramakrishnanannaswamy-projects-claude-self-reflect/file.jsonl
|
|
17
|
+
- /logs/-Users-ramakrishnanannaswamy-projects-n8n-builder/file.jsonl
|
|
18
|
+
"""
|
|
19
|
+
# Get the directory name containing the JSONL file
|
|
20
|
+
path_obj = Path(file_path)
|
|
21
|
+
dir_name = path_obj.parent.name
|
|
22
|
+
|
|
23
|
+
# Extract project name from dash-encoded path
|
|
24
|
+
# Format: -Users-username-projects-PROJECT_NAME (PROJECT_NAME can have dashes)
|
|
25
|
+
if dir_name.startswith('-') and 'projects' in dir_name:
|
|
26
|
+
parts = dir_name.split('-')
|
|
27
|
+
# Find 'projects' and take everything after it as the project name
|
|
28
|
+
try:
|
|
29
|
+
projects_idx = parts.index('projects')
|
|
30
|
+
if projects_idx + 1 < len(parts):
|
|
31
|
+
# Join all parts after 'projects' to handle multi-part project names
|
|
32
|
+
# like "claude-self-reflect", "n8n-builder", etc.
|
|
33
|
+
project_parts = parts[projects_idx + 1:]
|
|
34
|
+
return '-'.join(project_parts)
|
|
35
|
+
except ValueError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
# Fallback: use the directory name as-is
|
|
39
|
+
return dir_name.lstrip('-')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def normalize_file_path(file_path: str) -> str:
|
|
43
|
+
"""Normalize file paths to handle Docker vs local path differences.
|
|
44
|
+
|
|
45
|
+
Converts:
|
|
46
|
+
- /logs/PROJECT_DIR/file.jsonl -> ~/.claude/projects/PROJECT_DIR/file.jsonl
|
|
47
|
+
- Already normalized paths remain unchanged
|
|
48
|
+
"""
|
|
49
|
+
if file_path.startswith("/logs/"):
|
|
50
|
+
# Convert Docker path to local path
|
|
51
|
+
projects_dir = str(Path.home() / ".claude" / "projects")
|
|
52
|
+
return file_path.replace("/logs/", projects_dir + "/", 1)
|
|
53
|
+
return file_path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_status() -> dict:
|
|
57
|
+
"""Get indexing status with overall stats and per-project breakdown.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
dict: JSON structure with overall and per-project indexing status
|
|
61
|
+
"""
|
|
62
|
+
projects_dir = Path.home() / ".claude" / "projects"
|
|
63
|
+
project_stats = defaultdict(lambda: {"indexed": 0, "total": 0})
|
|
64
|
+
|
|
65
|
+
# Build a mapping of normalized file paths to project names
|
|
66
|
+
file_to_project = {}
|
|
67
|
+
|
|
68
|
+
# Count total JSONL files per project
|
|
69
|
+
if projects_dir.exists():
|
|
70
|
+
for jsonl_file in projects_dir.glob("**/*.jsonl"):
|
|
71
|
+
file_str = str(jsonl_file)
|
|
72
|
+
project_name = extract_project_name_from_path(file_str)
|
|
73
|
+
project_stats[project_name]["total"] += 1
|
|
74
|
+
file_to_project[file_str] = project_name
|
|
75
|
+
|
|
76
|
+
# Read imported-files.json to count indexed files per project
|
|
77
|
+
config_paths = [
|
|
78
|
+
Path.home() / ".claude-self-reflect" / "config" / "imported-files.json",
|
|
79
|
+
Path(__file__).parent.parent.parent / "config" / "imported-files.json",
|
|
80
|
+
Path("/config/imported-files.json") # Docker path
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
imported_files_path = None
|
|
84
|
+
for path in config_paths:
|
|
85
|
+
if path.exists():
|
|
86
|
+
imported_files_path = path
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
if imported_files_path:
|
|
90
|
+
try:
|
|
91
|
+
with open(imported_files_path, 'r') as f:
|
|
92
|
+
data = json.load(f)
|
|
93
|
+
|
|
94
|
+
# The actual structure has imported_files at the top level
|
|
95
|
+
imported_files = data.get("imported_files", {})
|
|
96
|
+
|
|
97
|
+
# Count all files in imported_files object (they are all fully imported)
|
|
98
|
+
for file_path in imported_files.keys():
|
|
99
|
+
normalized_path = normalize_file_path(file_path)
|
|
100
|
+
if normalized_path in file_to_project:
|
|
101
|
+
project_name = file_to_project[normalized_path]
|
|
102
|
+
project_stats[project_name]["indexed"] += 1
|
|
103
|
+
|
|
104
|
+
# Also check file_metadata for partially imported files
|
|
105
|
+
file_metadata = data.get("file_metadata", {})
|
|
106
|
+
for file_path, metadata in file_metadata.items():
|
|
107
|
+
if isinstance(metadata, dict) and metadata.get("position", 0) > 0:
|
|
108
|
+
# Only count if not already in imported_files
|
|
109
|
+
if file_path not in imported_files:
|
|
110
|
+
normalized_path = normalize_file_path(file_path)
|
|
111
|
+
if normalized_path in file_to_project:
|
|
112
|
+
project_name = file_to_project[normalized_path]
|
|
113
|
+
project_stats[project_name]["indexed"] += 1
|
|
114
|
+
|
|
115
|
+
# Also check stream_position if it contains file paths
|
|
116
|
+
stream_position = data.get("stream_position", {})
|
|
117
|
+
if isinstance(stream_position, dict):
|
|
118
|
+
for file_path in stream_position.keys():
|
|
119
|
+
# Skip non-file entries
|
|
120
|
+
if file_path in ["imported_files", "file_metadata"]:
|
|
121
|
+
continue
|
|
122
|
+
# Only count if not already counted
|
|
123
|
+
if file_path not in imported_files:
|
|
124
|
+
normalized_path = normalize_file_path(file_path)
|
|
125
|
+
if normalized_path in file_to_project:
|
|
126
|
+
project_name = file_to_project[normalized_path]
|
|
127
|
+
# Only increment if not already counted
|
|
128
|
+
if project_stats[project_name]["indexed"] < project_stats[project_name]["total"]:
|
|
129
|
+
project_stats[project_name]["indexed"] += 1
|
|
130
|
+
except (json.JSONDecodeError, KeyError, OSError):
|
|
131
|
+
# If config file is corrupted or unreadable, continue with zero indexed counts
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
# Calculate overall stats
|
|
135
|
+
total_all = sum(p["total"] for p in project_stats.values())
|
|
136
|
+
indexed_all = sum(p["indexed"] for p in project_stats.values())
|
|
137
|
+
|
|
138
|
+
# Build response structure
|
|
139
|
+
result = {
|
|
140
|
+
"overall": {
|
|
141
|
+
"percentage": round((indexed_all / total_all * 100) if total_all > 0 else 100.0, 1),
|
|
142
|
+
"indexed": indexed_all,
|
|
143
|
+
"total": total_all,
|
|
144
|
+
"backlog": total_all - indexed_all
|
|
145
|
+
},
|
|
146
|
+
"projects": {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Add per-project stats with percentages
|
|
150
|
+
for project, stats in project_stats.items():
|
|
151
|
+
result["projects"][project] = {
|
|
152
|
+
"percentage": round((stats["indexed"] / stats["total"] * 100) if stats["total"] > 0 else 100.0, 1),
|
|
153
|
+
"indexed": stats["indexed"],
|
|
154
|
+
"total": stats["total"]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
# Allow running as standalone script for testing
|
|
162
|
+
import sys
|
|
163
|
+
print(json.dumps(get_status(), indent=2))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-self-reflect",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.13",
|
|
4
4
|
"description": "Give Claude perfect memory of all your conversations - Installation wizard for Python MCP server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"mcp-server/run-mcp-docker.sh",
|
|
36
36
|
"scripts/import-*.py",
|
|
37
37
|
".claude/agents/*.md",
|
|
38
|
+
"config/qdrant-config.yaml",
|
|
38
39
|
"docker-compose.yaml",
|
|
39
40
|
"Dockerfile.*",
|
|
40
41
|
".env.example",
|