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.
- package/.claude/agents/csr-validator.md +87 -1
- package/.env.example +15 -0
- package/README.md +59 -0
- package/docker-compose.yaml +18 -9
- package/installer/setup-wizard-docker.js +87 -6
- package/installer/update-manager.js +88 -1
- package/mcp-server/src/standalone_client.py +314 -0
- package/package.json +1 -1
- package/src/runtime/hooks/__init__.py +21 -0
- package/src/runtime/hooks/ralph_state.py +397 -0
- package/src/runtime/hooks/session_end_hook.py +245 -0
- package/src/runtime/hooks/session_start_hook.py +259 -0
- package/src/runtime/precompact-hook.sh +60 -3
- package/src/runtime/unified_state_manager.py +35 -10
|
@@ -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
|
@@ -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
|
+
]
|