claude-self-reflect 7.0.0 → 7.1.8

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.
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CSR Standalone Client - For use outside the MCP server context.
4
+
5
+ This client provides search and store_reflection functionality
6
+ for hooks and scripts that need to interact with CSR without
7
+ going through the MCP protocol.
8
+
9
+ Usage:
10
+ from mcp_server.src.standalone_client import CSRStandaloneClient
11
+
12
+ client = CSRStandaloneClient()
13
+ results = client.search("docker issues", limit=5)
14
+ client.store_reflection("Key insight here", tags=["insight"])
15
+ """
16
+
17
+ import os
18
+ import uuid
19
+ import hashlib
20
+ import logging
21
+ from typing import List, Dict, Any, Optional
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class CSRStandaloneClient:
29
+ """Standalone CSR client for hooks and scripts."""
30
+
31
+ def __init__(
32
+ self,
33
+ qdrant_url: str = None,
34
+ qdrant_api_key: str = None,
35
+ prefer_local: bool = None
36
+ ):
37
+ """Initialize the standalone client.
38
+
39
+ Args:
40
+ qdrant_url: Qdrant server URL (default: from env or localhost:6333)
41
+ qdrant_api_key: Qdrant API key (default: from env)
42
+ prefer_local: Use local embeddings (default: from env or True)
43
+ """
44
+ self.qdrant_url = qdrant_url or os.getenv('QDRANT_URL', 'http://localhost:6333')
45
+ self.qdrant_api_key = qdrant_api_key or os.getenv('QDRANT_API_KEY')
46
+
47
+ if prefer_local is None:
48
+ self.prefer_local = os.getenv('PREFER_LOCAL_EMBEDDINGS', 'true').lower() == 'true'
49
+ else:
50
+ self.prefer_local = prefer_local
51
+
52
+ self._client = None
53
+ self._embedding_manager = None
54
+
55
+ def _get_client(self):
56
+ """Get or create Qdrant client (synchronous)."""
57
+ if self._client is None:
58
+ from qdrant_client import QdrantClient
59
+
60
+ # Parse URL for host/port
61
+ import urllib.parse
62
+ parsed = urllib.parse.urlparse(self.qdrant_url)
63
+ host = parsed.hostname or 'localhost'
64
+ port = parsed.port or 6333
65
+
66
+ self._client = QdrantClient(
67
+ host=host,
68
+ port=port,
69
+ api_key=self.qdrant_api_key if self.qdrant_api_key else None,
70
+ timeout=30
71
+ )
72
+ return self._client
73
+
74
+ def _get_embedding_manager(self):
75
+ """Get or create embedding manager."""
76
+ if self._embedding_manager is None:
77
+ # Try to use the project's embedding manager
78
+ try:
79
+ from .embedding_manager import EmbeddingManager
80
+ self._embedding_manager = EmbeddingManager()
81
+ except ImportError:
82
+ # Fallback: create minimal embedding functionality
83
+ self._embedding_manager = self._create_fallback_embeddings()
84
+ return self._embedding_manager
85
+
86
+ def _create_fallback_embeddings(self):
87
+ """Create fallback embedding functionality using fastembed."""
88
+ class FallbackEmbeddings:
89
+ def __init__(self):
90
+ self._model = None
91
+
92
+ def get_model(self):
93
+ if self._model is None:
94
+ try:
95
+ from fastembed import TextEmbedding
96
+ self._model = TextEmbedding("BAAI/bge-small-en-v1.5")
97
+ except ImportError:
98
+ raise ImportError("fastembed not installed. Run: pip install fastembed")
99
+ return self._model
100
+
101
+ def embed(self, text: str) -> List[float]:
102
+ model = self.get_model()
103
+ embeddings = list(model.embed([text]))
104
+ return list(embeddings[0])
105
+
106
+ @property
107
+ def dimension(self) -> int:
108
+ return 384
109
+
110
+ return FallbackEmbeddings()
111
+
112
+ def search(
113
+ self,
114
+ query: str,
115
+ limit: int = 5,
116
+ min_score: float = 0.3,
117
+ project: str = None
118
+ ) -> List[Dict[str, Any]]:
119
+ """Search for relevant conversations.
120
+
121
+ Args:
122
+ query: Search query
123
+ limit: Maximum results to return
124
+ min_score: Minimum similarity score
125
+ project: Project name filter (optional)
126
+
127
+ Returns:
128
+ List of search results with content and metadata
129
+ """
130
+ client = self._get_client()
131
+ embeddings = self._get_embedding_manager()
132
+
133
+ # Generate query embedding
134
+ query_vector = embeddings.embed(query)
135
+
136
+ # Find searchable collections
137
+ collections = client.get_collections().collections
138
+ searchable = [
139
+ c.name for c in collections
140
+ if self._is_searchable_collection(c.name)
141
+ ]
142
+
143
+ if not searchable:
144
+ logger.warning("No searchable collections found")
145
+ return []
146
+
147
+ # Filter by project if specified
148
+ if project and project != 'all':
149
+ project_norm = self._normalize_project_name(project)
150
+ searchable = [c for c in searchable if project_norm in c]
151
+
152
+ # Prioritize reflections collections (where Ralph state is stored)
153
+ reflections = [c for c in searchable if c.startswith('reflections')]
154
+ others = [c for c in searchable if not c.startswith('reflections')]
155
+ searchable = reflections + others
156
+
157
+ results = []
158
+ for collection_name in searchable[:8]: # Search up to 8 collections
159
+ try:
160
+ search_results = client.search(
161
+ collection_name=collection_name,
162
+ query_vector=query_vector,
163
+ limit=limit,
164
+ score_threshold=min_score
165
+ )
166
+
167
+ for hit in search_results:
168
+ payload = hit.payload or {}
169
+ results.append({
170
+ 'score': hit.score,
171
+ 'content': payload.get('content', ''),
172
+ 'preview': payload.get('preview', payload.get('content', '')[:200]),
173
+ 'metadata': {
174
+ 'collection': collection_name,
175
+ 'conversation_id': payload.get('conversation_id', ''),
176
+ 'timestamp': payload.get('timestamp', ''),
177
+ 'project': payload.get('project', ''),
178
+ }
179
+ })
180
+ except Exception as e:
181
+ logger.debug(f"Error searching {collection_name}: {e}")
182
+ continue
183
+
184
+ # Sort by score and limit
185
+ results.sort(key=lambda x: x['score'], reverse=True)
186
+ return results[:limit]
187
+
188
+ def store_reflection(
189
+ self,
190
+ content: str,
191
+ tags: List[str] = None
192
+ ) -> str:
193
+ """Store a reflection/insight.
194
+
195
+ Args:
196
+ content: The reflection content
197
+ tags: Optional tags for categorization
198
+
199
+ Returns:
200
+ ID of stored reflection
201
+ """
202
+ tags = tags or []
203
+ client = self._get_client()
204
+ embeddings = self._get_embedding_manager()
205
+
206
+ # Determine collection name
207
+ collection_name = f"reflections_{'local' if self.prefer_local else 'voyage'}"
208
+
209
+ # Ensure collection exists
210
+ try:
211
+ client.get_collection(collection_name)
212
+ except Exception:
213
+ # Create collection
214
+ from qdrant_client.models import VectorParams, Distance
215
+ client.create_collection(
216
+ collection_name=collection_name,
217
+ vectors_config=VectorParams(
218
+ size=embeddings.dimension,
219
+ distance=Distance.COSINE
220
+ )
221
+ )
222
+
223
+ # Generate embedding
224
+ vector = embeddings.embed(content)
225
+
226
+ # Generate ID
227
+ reflection_id = hashlib.sha256(
228
+ f"{content}{datetime.now().isoformat()}".encode()
229
+ ).hexdigest()[:16]
230
+
231
+ # Store
232
+ from qdrant_client.models import PointStruct
233
+ client.upsert(
234
+ collection_name=collection_name,
235
+ points=[
236
+ PointStruct(
237
+ id=str(uuid.uuid4()),
238
+ vector=vector,
239
+ payload={
240
+ "content": content,
241
+ "tags": tags,
242
+ "timestamp": datetime.now(timezone.utc).isoformat(),
243
+ "reflection_id": reflection_id,
244
+ "type": "reflection"
245
+ }
246
+ )
247
+ ]
248
+ )
249
+
250
+ logger.info(f"Stored reflection: {reflection_id}")
251
+ return reflection_id
252
+
253
+ def _is_searchable_collection(self, name: str) -> bool:
254
+ """Check if collection is searchable."""
255
+ return (
256
+ name.endswith('_local')
257
+ or name.endswith('_voyage')
258
+ or name.endswith('_384d')
259
+ or name.endswith('_1024d')
260
+ or '_cloud_' in name
261
+ or name.startswith('reflections')
262
+ or name.startswith('csr_')
263
+ )
264
+
265
+ def _normalize_project_name(self, name: str) -> str:
266
+ """Normalize project name for collection matching."""
267
+ import re
268
+ # Convert to lowercase, replace special chars
269
+ normalized = name.lower()
270
+ normalized = re.sub(r'[^a-z0-9]', '_', normalized)
271
+ normalized = re.sub(r'_+', '_', normalized)
272
+ return normalized.strip('_')
273
+
274
+ def test_connection(self) -> bool:
275
+ """Test if CSR is accessible.
276
+
277
+ Returns:
278
+ True if connection successful
279
+ """
280
+ try:
281
+ client = self._get_client()
282
+ client.get_collections()
283
+ return True
284
+ except Exception as e:
285
+ logger.error(f"Connection test failed: {e}")
286
+ return False
287
+
288
+
289
+ # Convenience function for quick searches
290
+ def quick_search(query: str, limit: int = 3) -> List[Dict[str, Any]]:
291
+ """Quick search without creating client instance."""
292
+ client = CSRStandaloneClient()
293
+ return client.search(query, limit=limit)
294
+
295
+
296
+ if __name__ == "__main__":
297
+ # Test the client
298
+ import sys
299
+
300
+ client = CSRStandaloneClient()
301
+
302
+ if client.test_connection():
303
+ print("✓ CSR connection successful")
304
+
305
+ if len(sys.argv) > 1:
306
+ query = " ".join(sys.argv[1:])
307
+ results = client.search(query, limit=3)
308
+ print(f"\nResults for '{query}':")
309
+ for i, r in enumerate(results, 1):
310
+ print(f"\n{i}. Score: {r['score']:.2f}")
311
+ print(f" {r['preview'][:100]}...")
312
+ else:
313
+ print("✗ CSR connection failed")
314
+ sys.exit(1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-self-reflect",
3
- "version": "7.0.0",
3
+ "version": "7.1.8",
4
4
  "description": "Give Claude perfect memory of all your conversations - Installation wizard for Python MCP server",
5
5
  "keywords": [
6
6
  "claude",
@@ -0,0 +1,21 @@
1
+ """Ralph memory hooks for Claude Code integration."""
2
+
3
+ from .ralph_state import (
4
+ RalphState,
5
+ load_state,
6
+ save_state,
7
+ is_ralph_session,
8
+ get_ralph_state_path,
9
+ load_ralph_session_state,
10
+ parse_ralph_wiggum_state,
11
+ )
12
+
13
+ __all__ = [
14
+ 'RalphState',
15
+ 'load_state',
16
+ 'save_state',
17
+ 'is_ralph_session',
18
+ 'get_ralph_state_path',
19
+ 'load_ralph_session_state',
20
+ 'parse_ralph_wiggum_state',
21
+ ]