claude-self-reflect 3.2.4 → 3.3.0

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.
Files changed (33) hide show
  1. package/.claude/agents/claude-self-reflect-test.md +595 -528
  2. package/.claude/agents/reflection-specialist.md +59 -3
  3. package/README.md +14 -5
  4. package/mcp-server/run-mcp.sh +49 -5
  5. package/mcp-server/src/app_context.py +64 -0
  6. package/mcp-server/src/config.py +57 -0
  7. package/mcp-server/src/connection_pool.py +286 -0
  8. package/mcp-server/src/decay_manager.py +106 -0
  9. package/mcp-server/src/embedding_manager.py +64 -40
  10. package/mcp-server/src/embeddings_old.py +141 -0
  11. package/mcp-server/src/models.py +64 -0
  12. package/mcp-server/src/parallel_search.py +371 -0
  13. package/mcp-server/src/project_resolver.py +5 -0
  14. package/mcp-server/src/reflection_tools.py +206 -0
  15. package/mcp-server/src/rich_formatting.py +196 -0
  16. package/mcp-server/src/search_tools.py +826 -0
  17. package/mcp-server/src/server.py +127 -1720
  18. package/mcp-server/src/temporal_design.py +132 -0
  19. package/mcp-server/src/temporal_tools.py +597 -0
  20. package/mcp-server/src/temporal_utils.py +384 -0
  21. package/mcp-server/src/utils.py +150 -67
  22. package/package.json +10 -1
  23. package/scripts/add-timestamp-indexes.py +134 -0
  24. package/scripts/check-collections.py +29 -0
  25. package/scripts/debug-august-parsing.py +76 -0
  26. package/scripts/debug-import-single.py +91 -0
  27. package/scripts/debug-project-resolver.py +82 -0
  28. package/scripts/debug-temporal-tools.py +135 -0
  29. package/scripts/delta-metadata-update.py +547 -0
  30. package/scripts/import-conversations-unified.py +53 -2
  31. package/scripts/precompact-hook.sh +33 -0
  32. package/scripts/streaming-watcher.py +1443 -0
  33. package/scripts/utils.py +39 -0
@@ -0,0 +1,106 @@
1
+ """Decay calculation manager for Claude Self-Reflect MCP server."""
2
+
3
+ import math
4
+ from datetime import datetime, timezone
5
+ from typing import List, Tuple, Optional
6
+ try:
7
+ from .config import (
8
+ USE_DECAY,
9
+ DECAY_SCALE_DAYS,
10
+ DECAY_WEIGHT,
11
+ USE_NATIVE_DECAY,
12
+ logger
13
+ )
14
+ except ImportError:
15
+ # Fallback for direct execution
16
+ import os
17
+ import logging
18
+ USE_DECAY = os.getenv('USE_DECAY', 'false').lower() == 'true'
19
+ DECAY_SCALE_DAYS = float(os.getenv('DECAY_SCALE_DAYS', '90'))
20
+ DECAY_WEIGHT = float(os.getenv('DECAY_WEIGHT', '0.3'))
21
+ USE_NATIVE_DECAY = os.getenv('USE_NATIVE_DECAY', 'false').lower() == 'true'
22
+ logger = logging.getLogger(__name__)
23
+
24
+ class DecayManager:
25
+ """Manages memory decay calculations for search results."""
26
+
27
+ def __init__(self):
28
+ self.scale_ms = DECAY_SCALE_DAYS * 24 * 60 * 60 * 1000
29
+ self.weight = DECAY_WEIGHT
30
+ self.use_decay = USE_DECAY
31
+ self.use_native = USE_NATIVE_DECAY
32
+
33
+ def calculate_decay_score(
34
+ self,
35
+ base_score: float,
36
+ timestamp: str
37
+ ) -> float:
38
+ """Calculate decayed score for a single result."""
39
+ if not self.use_decay:
40
+ return base_score
41
+
42
+ try:
43
+ # Parse timestamp
44
+ if timestamp.endswith('Z'):
45
+ timestamp = timestamp.replace('Z', '+00:00')
46
+
47
+ result_time = datetime.fromisoformat(timestamp)
48
+ if result_time.tzinfo is None:
49
+ result_time = result_time.replace(tzinfo=timezone.utc)
50
+
51
+ # Calculate age
52
+ now = datetime.now(timezone.utc)
53
+ age_ms = (now - result_time).total_seconds() * 1000
54
+
55
+ # Calculate decay factor using half-life formula
56
+ # decay = exp(-ln(2) * age / half_life)
57
+ decay_factor = math.exp(-0.693147 * age_ms / self.scale_ms)
58
+
59
+ # Apply decay with weight
60
+ final_score = base_score * (1 - self.weight) + base_score * self.weight * decay_factor
61
+
62
+ return final_score
63
+
64
+ except Exception as e:
65
+ logger.error(f"Failed to calculate decay: {e}")
66
+ return base_score
67
+
68
+ def apply_decay_to_results(
69
+ self,
70
+ results: List[Tuple[float, str, dict]]
71
+ ) -> List[Tuple[float, str, dict]]:
72
+ """Apply decay to a list of results and re-sort."""
73
+ if not self.use_decay:
74
+ return results
75
+
76
+ decayed_results = []
77
+ for score, id_str, payload in results:
78
+ timestamp = payload.get('timestamp', datetime.now().isoformat())
79
+ decayed_score = self.calculate_decay_score(score, timestamp)
80
+ decayed_results.append((decayed_score, id_str, payload))
81
+
82
+ # Re-sort by decayed score
83
+ decayed_results.sort(key=lambda x: x[0], reverse=True)
84
+
85
+ return decayed_results
86
+
87
+ def get_native_decay_config(self) -> Optional[dict]:
88
+ """Get configuration for native Qdrant decay."""
89
+ if not self.use_native:
90
+ return None
91
+
92
+ return {
93
+ 'scale_seconds': self.scale_ms / 1000,
94
+ 'weight': self.weight,
95
+ 'midpoint': 0.5 # Half-life semantics
96
+ }
97
+
98
+ def should_use_decay(self, explicit_setting: Optional[int] = None) -> bool:
99
+ """Determine if decay should be used for a query."""
100
+ if explicit_setting is not None:
101
+ if explicit_setting == 1:
102
+ return True
103
+ elif explicit_setting == 0:
104
+ return False
105
+
106
+ return self.use_decay
@@ -16,16 +16,16 @@ class EmbeddingManager:
16
16
  """Manages embedding models with proper cache and lock handling."""
17
17
 
18
18
  def __init__(self):
19
- self.model = None
20
- self.model_type = None # 'local' or 'voyage'
19
+ self.local_model = None
21
20
  self.voyage_client = None
22
-
21
+ self.model_type = None # Default model type ('local' or 'voyage')
22
+
23
23
  # Configuration
24
24
  self.prefer_local = os.getenv('PREFER_LOCAL_EMBEDDINGS', 'true').lower() == 'true'
25
25
  self.voyage_key = os.getenv('VOYAGE_KEY') or os.getenv('VOYAGE_KEY-2')
26
26
  self.embedding_model = os.getenv('EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2')
27
27
  self.download_timeout = int(os.getenv('FASTEMBED_DOWNLOAD_TIMEOUT', '30'))
28
-
28
+
29
29
  # Set cache directory to our controlled location
30
30
  self.cache_dir = Path(__file__).parent.parent / '.fastembed-cache'
31
31
 
@@ -50,27 +50,35 @@ class EmbeddingManager:
50
50
  logger.warning(f"Error cleaning locks: {e}")
51
51
 
52
52
  def initialize(self) -> bool:
53
- """Initialize embedding model based on user preference."""
54
- logger.info("Initializing embedding manager...")
55
-
53
+ """Initialize BOTH embedding models to support mixed collections."""
54
+ logger.info("Initializing embedding manager for dual-mode support...")
55
+
56
56
  # Clean up any stale locks first
57
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
58
+
59
+ # Initialize both models for mixed collection support
60
+ local_success = self._try_initialize_local()
61
+ voyage_success = False
62
+
63
+ if self.voyage_key:
64
+ voyage_success = self._try_initialize_voyage()
65
+
66
+ # Set default model type based on preference and availability
67
+ if self.prefer_local and local_success:
68
+ self.model_type = 'local'
69
+ logger.info("Default model set to LOCAL embeddings")
70
+ elif voyage_success:
71
+ self.model_type = 'voyage'
72
+ logger.info("Default model set to VOYAGE embeddings")
73
+ elif local_success:
74
+ self.model_type = 'local'
75
+ logger.info("Default model set to LOCAL embeddings (fallback)")
65
76
  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")
77
+ logger.error("Failed to initialize any embedding model")
73
78
  return False
79
+
80
+ logger.info(f"Embedding models available - Local: {local_success}, Voyage: {voyage_success}")
81
+ return True
74
82
 
75
83
  def _try_initialize_local(self) -> bool:
76
84
  """Try to initialize local FastEmbed model with timeout and optimizations."""
@@ -119,11 +127,10 @@ class EmbeddingManager:
119
127
  from fastembed import TextEmbedding
120
128
  # Initialize with optimized settings
121
129
  # Note: FastEmbed uses these environment variables internally
122
- self.model = TextEmbedding(
130
+ self.local_model = TextEmbedding(
123
131
  model_name=self.embedding_model,
124
132
  threads=1 # Single thread per worker to prevent over-subscription
125
133
  )
126
- self.model_type = 'local'
127
134
  success = True
128
135
  logger.info(f"Successfully initialized local model: {self.embedding_model} with single-thread mode")
129
136
  except Exception as e:
@@ -177,39 +184,48 @@ class EmbeddingManager:
177
184
  logger.error(f"Failed to initialize Voyage AI: {e}")
178
185
  return False
179
186
 
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")
187
+ def embed(self, texts: Union[str, List[str]], input_type: str = "document", force_type: str = None) -> Optional[List[List[float]]]:
188
+ """Generate embeddings using the specified or default model."""
189
+ # Determine which model to use
190
+ use_type = force_type if force_type else self.model_type
191
+ logger.debug(f"Embedding with: force_type={force_type}, self.model_type={self.model_type}, use_type={use_type}")
192
+
193
+ if use_type == 'local' and not self.local_model:
194
+ logger.error("Local model not initialized")
184
195
  return None
185
-
196
+ elif use_type == 'voyage' and not self.voyage_client:
197
+ logger.error("Voyage client not initialized")
198
+ return None
199
+
186
200
  # Ensure texts is a list
187
201
  if isinstance(texts, str):
188
202
  texts = [texts]
189
-
203
+
190
204
  try:
191
- if self.model_type == 'local':
205
+ if use_type == 'local':
192
206
  # FastEmbed returns a generator, convert to list
193
- embeddings = list(self.model.embed(texts))
207
+ embeddings = list(self.local_model.embed(texts))
194
208
  return [emb.tolist() for emb in embeddings]
195
-
196
- elif self.model_type == 'voyage':
209
+
210
+ elif use_type == 'voyage':
211
+ # Always use voyage-3 for consistency with collection dimensions (1024)
197
212
  result = self.voyage_client.embed(
198
213
  texts=texts,
199
- model="voyage-3-lite" if input_type == "query" else "voyage-3",
214
+ model="voyage-3",
200
215
  input_type=input_type
201
216
  )
202
217
  return result.embeddings
203
-
218
+
204
219
  except Exception as e:
205
- logger.error(f"Error generating embeddings: {e}")
220
+ logger.error(f"Error generating embeddings with {use_type}: {e}")
206
221
  return None
207
222
 
208
- def get_vector_dimension(self) -> int:
209
- """Get the dimension of embeddings."""
210
- if self.model_type == 'local':
223
+ def get_vector_dimension(self, force_type: str = None) -> int:
224
+ """Get the dimension of embeddings for a specific type."""
225
+ use_type = force_type if force_type else self.model_type
226
+ if use_type == 'local':
211
227
  return 384 # all-MiniLM-L6-v2 dimension
212
- elif self.model_type == 'voyage':
228
+ elif use_type == 'voyage':
213
229
  return 1024 # voyage-3 dimension
214
230
  return 0
215
231
 
@@ -222,6 +238,14 @@ class EmbeddingManager:
222
238
  'prefer_local': self.prefer_local,
223
239
  'has_voyage_key': bool(self.voyage_key)
224
240
  }
241
+
242
+ async def generate_embedding(self, text: str, force_type: str = None) -> Optional[List[float]]:
243
+ """Generate embedding for a single text (async wrapper for compatibility)."""
244
+ # Use the force_type if specified, otherwise use default
245
+ result = self.embed(text, input_type="query", force_type=force_type)
246
+ if result and len(result) > 0:
247
+ return result[0]
248
+ return None
225
249
 
226
250
 
227
251
  # Global instance
@@ -0,0 +1,141 @@
1
+ """Embedding generation module for Claude Self-Reflect MCP server."""
2
+
3
+ import os
4
+ import voyageai
5
+ from typing import Dict, List, Optional, Any
6
+ from fastembed import TextEmbedding
7
+ from config import (
8
+ VOYAGE_API_KEY,
9
+ VOYAGE_MODEL,
10
+ LOCAL_MODEL,
11
+ PREFER_LOCAL_EMBEDDINGS,
12
+ logger
13
+ )
14
+
15
+ class EmbeddingManager:
16
+ """Manages embedding generation for both local and Voyage AI models."""
17
+
18
+ def __init__(self):
19
+ self.local_model = None
20
+ self.voyage_client = None
21
+ self.embedding_cache = {}
22
+
23
+ # Initialize based on preference
24
+ if PREFER_LOCAL_EMBEDDINGS or not VOYAGE_API_KEY:
25
+ self._init_local_model()
26
+
27
+ if VOYAGE_API_KEY:
28
+ self._init_voyage_client()
29
+
30
+ def _init_local_model(self):
31
+ """Initialize local FastEmbed model."""
32
+ try:
33
+ self.local_model = TextEmbedding(
34
+ model_name=LOCAL_MODEL,
35
+ cache_dir=str(os.path.expanduser("~/.cache/fastembed"))
36
+ )
37
+ logger.info(f"Initialized local embedding model: {LOCAL_MODEL}")
38
+ except Exception as e:
39
+ logger.error(f"Failed to initialize local model: {e}")
40
+
41
+ def _init_voyage_client(self):
42
+ """Initialize Voyage AI client."""
43
+ try:
44
+ self.voyage_client = voyageai.Client(api_key=VOYAGE_API_KEY)
45
+ logger.info("Initialized Voyage AI client")
46
+ except Exception as e:
47
+ logger.error(f"Failed to initialize Voyage client: {e}")
48
+
49
+ async def generate_embedding(
50
+ self,
51
+ text: str,
52
+ embedding_type: Optional[str] = None
53
+ ) -> Optional[List[float]]:
54
+ """Generate embedding for text using specified or default model."""
55
+
56
+ # Use cache if available
57
+ cache_key = f"{embedding_type or 'default'}:{text[:100]}"
58
+ if cache_key in self.embedding_cache:
59
+ return self.embedding_cache[cache_key]
60
+
61
+ # Determine which model to use
62
+ use_local = True
63
+ if embedding_type:
64
+ use_local = 'local' in embedding_type
65
+ elif not PREFER_LOCAL_EMBEDDINGS and self.voyage_client:
66
+ use_local = False
67
+
68
+ try:
69
+ if use_local and self.local_model:
70
+ # Generate local embedding
71
+ embeddings = list(self.local_model.embed([text]))
72
+ if embeddings:
73
+ embedding = list(embeddings[0])
74
+ self.embedding_cache[cache_key] = embedding
75
+ return embedding
76
+
77
+ elif self.voyage_client:
78
+ # Generate Voyage embedding
79
+ result = self.voyage_client.embed(
80
+ [text],
81
+ model=VOYAGE_MODEL,
82
+ input_type="document"
83
+ )
84
+ if result.embeddings:
85
+ embedding = result.embeddings[0]
86
+ self.embedding_cache[cache_key] = embedding
87
+ return embedding
88
+
89
+ except Exception as e:
90
+ logger.error(f"Failed to generate embedding: {e}")
91
+
92
+ return None
93
+
94
+ async def generate_embeddings_batch(
95
+ self,
96
+ texts: List[str],
97
+ embedding_type: Optional[str] = None
98
+ ) -> Dict[str, List[float]]:
99
+ """Generate embeddings for multiple texts efficiently."""
100
+ results = {}
101
+
102
+ # Determine which model to use
103
+ use_local = True
104
+ if embedding_type:
105
+ use_local = 'local' in embedding_type
106
+ elif not PREFER_LOCAL_EMBEDDINGS and self.voyage_client:
107
+ use_local = False
108
+
109
+ try:
110
+ if use_local and self.local_model:
111
+ # Batch process with local model
112
+ embeddings = list(self.local_model.embed(texts))
113
+ for text, embedding in zip(texts, embeddings):
114
+ results[text] = list(embedding)
115
+
116
+ elif self.voyage_client:
117
+ # Batch process with Voyage
118
+ result = self.voyage_client.embed(
119
+ texts,
120
+ model=VOYAGE_MODEL,
121
+ input_type="document"
122
+ )
123
+ for text, embedding in zip(texts, result.embeddings):
124
+ results[text] = embedding
125
+
126
+ except Exception as e:
127
+ logger.error(f"Failed to generate batch embeddings: {e}")
128
+
129
+ return results
130
+
131
+ def get_embedding_dimension(self, embedding_type: str = "local") -> int:
132
+ """Get the dimension of embeddings for a given type."""
133
+ if "local" in embedding_type:
134
+ return 384 # all-MiniLM-L6-v2 dimension
135
+ else:
136
+ return 1024 # voyage-3-lite dimension
137
+
138
+ def clear_cache(self):
139
+ """Clear the embedding cache."""
140
+ self.embedding_cache.clear()
141
+ logger.info("Cleared embedding cache")
@@ -0,0 +1,64 @@
1
+ """Pydantic models for Claude Self-Reflect MCP server."""
2
+
3
+ from typing import Optional, List, Dict, Any, Set
4
+ from datetime import datetime
5
+ from pydantic import BaseModel, Field
6
+
7
+ class SearchResult(BaseModel):
8
+ """Model for search results."""
9
+ id: str
10
+ score: float
11
+ timestamp: str
12
+ role: str
13
+ excerpt: str
14
+ project_name: str
15
+ conversation_id: Optional[str] = None
16
+ base_conversation_id: Optional[str] = None
17
+ collection_name: str
18
+ raw_payload: Optional[Dict[str, Any]] = None
19
+ code_patterns: Optional[Dict[str, List[str]]] = None
20
+ files_analyzed: Optional[List[str]] = None
21
+ files_edited: Optional[List[str]] = None
22
+ tools_used: Optional[List[str]] = None
23
+ concepts: Optional[List[str]] = None
24
+
25
+ class ConversationGroup(BaseModel):
26
+ """Model for grouped conversations."""
27
+ conversation_id: str
28
+ base_conversation_id: str
29
+ timestamp: datetime
30
+ message_count: int
31
+ excerpts: List[str]
32
+ files: Set[str] = Field(default_factory=set)
33
+ tools: Set[str] = Field(default_factory=set)
34
+ concepts: Set[str] = Field(default_factory=set)
35
+
36
+ class WorkSession(BaseModel):
37
+ """Model for work sessions."""
38
+ start_time: datetime
39
+ end_time: datetime
40
+ conversations: List[ConversationGroup]
41
+ total_messages: int
42
+ files_touched: Set[str] = Field(default_factory=set)
43
+ tools_used: Set[str] = Field(default_factory=set)
44
+ concepts: Set[str] = Field(default_factory=set)
45
+
46
+ class ActivityStats(BaseModel):
47
+ """Model for activity statistics."""
48
+ total_conversations: int
49
+ total_messages: int
50
+ unique_files: int
51
+ unique_tools: int
52
+ peak_hour: Optional[str] = None
53
+ peak_day: Optional[str] = None
54
+
55
+ class TimelineEntry(BaseModel):
56
+ """Model for timeline entries."""
57
+ period: str
58
+ start_time: datetime
59
+ end_time: datetime
60
+ conversation_count: int
61
+ message_count: int
62
+ files: Set[str] = Field(default_factory=set)
63
+ tools: Set[str] = Field(default_factory=set)
64
+ concepts: Set[str] = Field(default_factory=set)