claude-self-reflect 3.2.4 → 3.3.1

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 (41) hide show
  1. package/.claude/agents/claude-self-reflect-test.md +992 -510
  2. package/.claude/agents/reflection-specialist.md +59 -3
  3. package/README.md +14 -5
  4. package/installer/cli.js +16 -0
  5. package/installer/postinstall.js +14 -0
  6. package/installer/statusline-setup.js +289 -0
  7. package/mcp-server/run-mcp.sh +73 -5
  8. package/mcp-server/src/app_context.py +64 -0
  9. package/mcp-server/src/config.py +57 -0
  10. package/mcp-server/src/connection_pool.py +286 -0
  11. package/mcp-server/src/decay_manager.py +106 -0
  12. package/mcp-server/src/embedding_manager.py +64 -40
  13. package/mcp-server/src/embeddings_old.py +141 -0
  14. package/mcp-server/src/models.py +64 -0
  15. package/mcp-server/src/parallel_search.py +305 -0
  16. package/mcp-server/src/project_resolver.py +5 -0
  17. package/mcp-server/src/reflection_tools.py +211 -0
  18. package/mcp-server/src/rich_formatting.py +196 -0
  19. package/mcp-server/src/search_tools.py +874 -0
  20. package/mcp-server/src/server.py +127 -1720
  21. package/mcp-server/src/temporal_design.py +132 -0
  22. package/mcp-server/src/temporal_tools.py +604 -0
  23. package/mcp-server/src/temporal_utils.py +384 -0
  24. package/mcp-server/src/utils.py +150 -67
  25. package/package.json +15 -1
  26. package/scripts/add-timestamp-indexes.py +134 -0
  27. package/scripts/ast_grep_final_analyzer.py +325 -0
  28. package/scripts/ast_grep_unified_registry.py +556 -0
  29. package/scripts/check-collections.py +29 -0
  30. package/scripts/csr-status +366 -0
  31. package/scripts/debug-august-parsing.py +76 -0
  32. package/scripts/debug-import-single.py +91 -0
  33. package/scripts/debug-project-resolver.py +82 -0
  34. package/scripts/debug-temporal-tools.py +135 -0
  35. package/scripts/delta-metadata-update.py +547 -0
  36. package/scripts/import-conversations-unified.py +157 -25
  37. package/scripts/precompact-hook.sh +33 -0
  38. package/scripts/session_quality_tracker.py +481 -0
  39. package/scripts/streaming-watcher.py +1578 -0
  40. package/scripts/update_patterns.py +334 -0
  41. package/scripts/utils.py +39 -0
@@ -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)
@@ -0,0 +1,305 @@
1
+ """
2
+ Parallel search implementation for Qdrant collections.
3
+ This module implements asyncio.gather-based parallel searching to improve performance.
4
+ """
5
+
6
+ import asyncio
7
+ import time
8
+ from typing import List, Dict, Any, Optional, Tuple
9
+ from datetime import datetime
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ async def search_single_collection(
15
+ collection_name: str,
16
+ query: str,
17
+ query_embeddings: Dict[str, List[float]],
18
+ qdrant_client: Any,
19
+ ctx: Any,
20
+ limit: int,
21
+ min_score: float,
22
+ should_use_decay: bool,
23
+ target_project: str,
24
+ generate_embedding_func: Any,
25
+ constants: Dict[str, Any]
26
+ ) -> Tuple[str, List[Any], Dict[str, Any]]:
27
+ """
28
+ Search a single collection and return results.
29
+
30
+ Returns:
31
+ Tuple of (collection_name, results, timing_info)
32
+ """
33
+ collection_timing = {'name': collection_name, 'start': time.time()}
34
+ results = []
35
+
36
+ try:
37
+ # Determine embedding type for this collection
38
+ embedding_type_for_collection = 'voyage' if collection_name.endswith('_voyage') else 'local'
39
+ logger.debug(f"Collection {collection_name} needs {embedding_type_for_collection} embedding")
40
+
41
+ # Generate or retrieve cached embedding for this type
42
+ if embedding_type_for_collection not in query_embeddings:
43
+ try:
44
+ query_embeddings[embedding_type_for_collection] = await generate_embedding_func(
45
+ query, force_type=embedding_type_for_collection
46
+ )
47
+ except Exception as e:
48
+ await ctx.debug(f"Failed to generate {embedding_type_for_collection} embedding: {e}")
49
+ collection_timing['error'] = str(e)
50
+ collection_timing['end'] = time.time()
51
+ return collection_name, results, collection_timing
52
+
53
+ query_embedding = query_embeddings[embedding_type_for_collection]
54
+
55
+ # Check if this is a reflections collection
56
+ is_reflection_collection = collection_name.startswith('reflections_')
57
+
58
+ # Import necessary models
59
+ from qdrant_client import models
60
+
61
+ # Determine which decay method to use
62
+ USE_NATIVE_DECAY = constants.get('USE_NATIVE_DECAY', False)
63
+ NATIVE_DECAY_AVAILABLE = constants.get('NATIVE_DECAY_AVAILABLE', False)
64
+ DECAY_SCALE_DAYS = constants.get('DECAY_SCALE_DAYS', 90)
65
+ DECAY_WEIGHT = constants.get('DECAY_WEIGHT', 0.3)
66
+
67
+ # NOTE: Native decay API is not available in current Qdrant, fall back to client-side
68
+ # The Fusion/RankFusion API was experimental and removed, always use client-side decay
69
+ if should_use_decay and False: # Disabled until Qdrant provides stable decay API
70
+ # This code path is intentionally disabled
71
+ pass
72
+ else:
73
+ # Standard search without native decay or client-side decay
74
+ search_results = await qdrant_client.search(
75
+ collection_name=collection_name,
76
+ query_vector=query_embedding,
77
+ limit=limit * 3 if should_use_decay else limit, # Get more results for client-side decay
78
+ score_threshold=min_score if not should_use_decay else 0.0,
79
+ with_payload=True
80
+ )
81
+
82
+ # Debug: Log search results
83
+ logger.debug(f"Search of {collection_name} returned {len(search_results)} results")
84
+
85
+ if should_use_decay and not USE_NATIVE_DECAY:
86
+ # Apply client-side decay
87
+ await ctx.debug(f"Using CLIENT-SIDE decay for {collection_name}")
88
+ decay_results = []
89
+
90
+ for point in search_results:
91
+ try:
92
+ raw_timestamp = point.payload.get('timestamp', datetime.now().isoformat())
93
+ clean_timestamp = raw_timestamp.replace('Z', '+00:00') if raw_timestamp.endswith('Z') else raw_timestamp
94
+
95
+ # Calculate age and decay
96
+ if 'timestamp' in point.payload:
97
+ try:
98
+ point_time = datetime.fromisoformat(clean_timestamp)
99
+ if point_time.tzinfo is None:
100
+ from zoneinfo import ZoneInfo
101
+ point_time = point_time.replace(tzinfo=ZoneInfo('UTC'))
102
+
103
+ now = datetime.now(ZoneInfo('UTC'))
104
+ age_ms = (now - point_time).total_seconds() * 1000
105
+
106
+ # Exponential decay with configurable half-life
107
+ half_life_ms = DECAY_SCALE_DAYS * 24 * 60 * 60 * 1000
108
+ decay_factor = 0.5 ** (age_ms / half_life_ms)
109
+
110
+ # Apply multiplicative decay
111
+ adjusted_score = point.score * ((1 - DECAY_WEIGHT) + DECAY_WEIGHT * decay_factor)
112
+ except Exception as e:
113
+ await ctx.debug(f"Error calculating decay: {e}")
114
+ adjusted_score = point.score
115
+ else:
116
+ adjusted_score = point.score
117
+
118
+ # Only include if above min_score after decay
119
+ if adjusted_score >= min_score:
120
+ decay_results.append((adjusted_score, point))
121
+
122
+ except Exception as e:
123
+ await ctx.debug(f"Error applying decay to point: {e}")
124
+ decay_results.append((point.score, point))
125
+
126
+ # Sort by adjusted score and take top results
127
+ decay_results.sort(key=lambda x: x[0], reverse=True)
128
+
129
+ # Convert to SearchResult format
130
+ for adjusted_score, point in decay_results[:limit]:
131
+ raw_timestamp = point.payload.get('timestamp', datetime.now().isoformat())
132
+ clean_timestamp = raw_timestamp.replace('Z', '+00:00') if raw_timestamp.endswith('Z') else raw_timestamp
133
+
134
+ point_project = point.payload.get('project', collection_name.replace('conv_', '').replace('_voyage', '').replace('_local', ''))
135
+
136
+ # Apply project filtering
137
+ if target_project != 'all' and not is_reflection_collection:
138
+ if point_project != target_project:
139
+ normalized_target = target_project.replace('-', '_')
140
+ normalized_point = point_project.replace('-', '_')
141
+ if not (normalized_point == normalized_target or
142
+ point_project.endswith(f"/{target_project}") or
143
+ point_project.endswith(f"-{target_project}") or
144
+ normalized_point.endswith(f"_{normalized_target}") or
145
+ normalized_point.endswith(f"/{normalized_target}")):
146
+ logger.debug(f"Filtering out point: project '{point_project}' != target '{target_project}'")
147
+ continue
148
+ logger.debug(f"Keeping point: project '{point_project}' matches target '{target_project}'")
149
+
150
+ # Create SearchResult with consistent structure
151
+ search_result = {
152
+ 'id': str(point.id),
153
+ 'score': adjusted_score,
154
+ 'timestamp': clean_timestamp,
155
+ 'role': point.payload.get('start_role', point.payload.get('role', 'unknown')),
156
+ 'excerpt': (point.payload.get('text', '')[:350] + '...'
157
+ if len(point.payload.get('text', '')) > 350
158
+ else point.payload.get('text', '')),
159
+ 'project_name': point_project,
160
+ 'conversation_id': point.payload.get('conversation_id'),
161
+ 'base_conversation_id': point.payload.get('base_conversation_id'),
162
+ 'collection_name': collection_name,
163
+ 'raw_payload': point.payload, # Renamed from 'payload' for consistency
164
+ 'code_patterns': point.payload.get('code_patterns'),
165
+ 'files_analyzed': point.payload.get('files_analyzed'),
166
+ 'tools_used': list(point.payload.get('tools_used', [])) if isinstance(point.payload.get('tools_used'), set) else point.payload.get('tools_used'),
167
+ 'concepts': point.payload.get('concepts')
168
+ }
169
+ results.append(search_result)
170
+ else:
171
+ # Process standard search results without decay
172
+ logger.debug(f"Processing {len(search_results)} results from {collection_name}")
173
+ for point in search_results:
174
+ raw_timestamp = point.payload.get('timestamp', datetime.now().isoformat())
175
+ clean_timestamp = raw_timestamp.replace('Z', '+00:00') if raw_timestamp.endswith('Z') else raw_timestamp
176
+
177
+ point_project = point.payload.get('project', collection_name.replace('conv_', '').replace('_voyage', '').replace('_local', ''))
178
+
179
+ # Apply project filtering
180
+ if target_project != 'all' and not is_reflection_collection:
181
+ if point_project != target_project:
182
+ normalized_target = target_project.replace('-', '_')
183
+ normalized_point = point_project.replace('-', '_')
184
+ if not (normalized_point == normalized_target or
185
+ point_project.endswith(f"/{target_project}") or
186
+ point_project.endswith(f"-{target_project}") or
187
+ normalized_point.endswith(f"_{normalized_target}") or
188
+ normalized_point.endswith(f"/{normalized_target}")):
189
+ logger.debug(f"Filtering out point: project '{point_project}' != target '{target_project}'")
190
+ continue
191
+ logger.debug(f"Keeping point: project '{point_project}' matches target '{target_project}'")
192
+
193
+ # Create SearchResult as dictionary (consistent with other branches)
194
+ search_result = {
195
+ 'id': str(point.id),
196
+ 'score': point.score,
197
+ 'timestamp': clean_timestamp,
198
+ 'role': point.payload.get('start_role', point.payload.get('role', 'unknown')),
199
+ 'excerpt': (point.payload.get('text', '')[:350] + '...'
200
+ if len(point.payload.get('text', '')) > 350
201
+ else point.payload.get('text', '')),
202
+ 'project_name': point_project,
203
+ 'conversation_id': point.payload.get('conversation_id'),
204
+ 'base_conversation_id': point.payload.get('base_conversation_id'),
205
+ 'collection_name': collection_name,
206
+ 'raw_payload': point.payload,
207
+ 'code_patterns': point.payload.get('code_patterns'),
208
+ 'files_analyzed': point.payload.get('files_analyzed'),
209
+ 'tools_used': list(point.payload.get('tools_used', [])) if isinstance(point.payload.get('tools_used'), set) else point.payload.get('tools_used'),
210
+ 'concepts': point.payload.get('concepts')
211
+ }
212
+ results.append(search_result)
213
+
214
+ except Exception as e:
215
+ await ctx.debug(f"Error searching {collection_name}: {str(e)}")
216
+ collection_timing['error'] = str(e)
217
+
218
+ collection_timing['end'] = time.time()
219
+ logger.debug(f"Collection {collection_name} returning {len(results)} results after filtering")
220
+ return collection_name, results, collection_timing
221
+
222
+
223
+ async def parallel_search_collections(
224
+ collections_to_search: List[str],
225
+ query: str,
226
+ qdrant_client: Any,
227
+ ctx: Any,
228
+ limit: int,
229
+ min_score: float,
230
+ should_use_decay: bool,
231
+ target_project: str,
232
+ generate_embedding_func: Any,
233
+ constants: Dict[str, Any],
234
+ max_concurrent: int = 10
235
+ ) -> Tuple[List[Any], List[Dict[str, Any]]]:
236
+ """
237
+ Search multiple collections in parallel using asyncio.gather.
238
+
239
+ Args:
240
+ collections_to_search: List of collection names to search
241
+ query: Search query
242
+ qdrant_client: Qdrant client instance
243
+ ctx: Context for debugging
244
+ limit: Maximum results per collection
245
+ min_score: Minimum similarity score
246
+ should_use_decay: Whether to apply time decay
247
+ target_project: Project filter ('all' or specific project)
248
+ generate_embedding_func: Function to generate embeddings
249
+ constants: Dictionary of constants (USE_NATIVE_DECAY, etc.)
250
+ max_concurrent: Maximum concurrent searches
251
+
252
+ Returns:
253
+ Tuple of (all_results, collection_timings)
254
+ """
255
+ await ctx.debug(f"Starting parallel search across {len(collections_to_search)} collections")
256
+
257
+ # Shared cache for embeddings
258
+ query_embeddings = {}
259
+
260
+ # Create semaphore to limit concurrent operations
261
+ semaphore = asyncio.Semaphore(max_concurrent)
262
+
263
+ async def search_with_semaphore(collection_name: str) -> Tuple[str, List[Any], Dict[str, Any]]:
264
+ """Search with concurrency limit"""
265
+ async with semaphore:
266
+ return await search_single_collection(
267
+ collection_name=collection_name,
268
+ query=query,
269
+ query_embeddings=query_embeddings,
270
+ qdrant_client=qdrant_client,
271
+ ctx=ctx,
272
+ limit=limit,
273
+ min_score=min_score,
274
+ should_use_decay=should_use_decay,
275
+ target_project=target_project,
276
+ generate_embedding_func=generate_embedding_func,
277
+ constants=constants
278
+ )
279
+
280
+ # Launch all searches in parallel
281
+ search_tasks = [
282
+ search_with_semaphore(collection_name)
283
+ for collection_name in collections_to_search
284
+ ]
285
+
286
+ # Wait for all searches to complete
287
+ search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
288
+
289
+ # Process results
290
+ all_results = []
291
+ collection_timings = []
292
+
293
+ for result in search_results:
294
+ if isinstance(result, Exception):
295
+ # Handle exceptions from gather
296
+ logger.error(f"Search task failed: {result}")
297
+ continue
298
+
299
+ collection_name, results, timing = result
300
+ all_results.extend(results)
301
+ collection_timings.append(timing)
302
+
303
+ await ctx.debug(f"Parallel search complete: {len(all_results)} total results")
304
+
305
+ return all_results, collection_timings
@@ -69,6 +69,11 @@ class ProjectResolver:
69
69
  Returns:
70
70
  List of collection names that match the project
71
71
  """
72
+ # Special case: 'all' returns all conversation collections
73
+ if user_project_name == 'all':
74
+ collection_names = self._get_collection_names()
75
+ return collection_names # Return all conv_ collections
76
+
72
77
  if user_project_name in self._cache:
73
78
  # Check if cache entry is still valid
74
79
  if time() - self._cache_ttl.get(user_project_name, 0) < self._cache_duration: