claude-self-reflect 2.4.2 → 2.4.3
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reflect-tester
|
|
3
3
|
description: Comprehensive testing specialist for validating reflection system functionality. Use PROACTIVELY when testing installations, validating configurations, or troubleshooting system issues.
|
|
4
|
-
tools: Read, Bash, Grep, LS, WebFetch, ListMcpResourcesTool
|
|
4
|
+
tools: Read, Bash, Grep, LS, WebFetch, ListMcpResourcesTool, mcp__claude-self-reflect__reflect_on_past, mcp__claude-self-reflect__store_reflection
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Reflect Tester Agent
|
|
@@ -132,6 +132,28 @@ curl -s http://localhost:6333/collections | jq '.result.collections[] | {name, v
|
|
|
132
132
|
|
|
133
133
|
#### 5.2 Tool Functionality Tests
|
|
134
134
|
|
|
135
|
+
**Project-Scoped Search Test (NEW)**:
|
|
136
|
+
Test the new project-scoped search functionality:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Test 1: Default search (project-scoped)
|
|
140
|
+
# Should only return results from current project
|
|
141
|
+
results = await reflect_on_past("Docker setup", limit=5, min_score=0.0)
|
|
142
|
+
# Verify: All results should be from current project (claude-self-reflect)
|
|
143
|
+
|
|
144
|
+
# Test 2: Explicit project search
|
|
145
|
+
results = await reflect_on_past("Docker setup", project="claude-self-reflect", limit=5, min_score=0.0)
|
|
146
|
+
# Should match Test 1 results
|
|
147
|
+
|
|
148
|
+
# Test 3: Cross-project search
|
|
149
|
+
results = await reflect_on_past("Docker setup", project="all", limit=5, min_score=0.0)
|
|
150
|
+
# Should include results from multiple projects
|
|
151
|
+
|
|
152
|
+
# Test 4: Different project search
|
|
153
|
+
results = await reflect_on_past("configuration", project="reflections", limit=5, min_score=0.0)
|
|
154
|
+
# Should only return results from the "reflections" project
|
|
155
|
+
```
|
|
156
|
+
|
|
135
157
|
**Local Embeddings Test**:
|
|
136
158
|
```python
|
|
137
159
|
# Store reflection with local embeddings
|
|
@@ -41,7 +41,7 @@ You are a conversation memory specialist for the Claude Self Reflect project. Yo
|
|
|
41
41
|
Search for relevant past conversations using semantic similarity.
|
|
42
42
|
|
|
43
43
|
```javascript
|
|
44
|
-
// Basic search
|
|
44
|
+
// Basic search (searches current project by default)
|
|
45
45
|
{
|
|
46
46
|
query: "streaming importer fixes",
|
|
47
47
|
limit: 5,
|
|
@@ -55,8 +55,29 @@ Search for relevant past conversations using semantic similarity.
|
|
|
55
55
|
min_score: 0.05, // Common threshold for relevant results
|
|
56
56
|
use_decay: 1 // Apply time-based relevance (1=enable, 0=disable, -1=default)
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
// Search specific project (NEW in v2.4.3)
|
|
60
|
+
{
|
|
61
|
+
query: "Docker setup",
|
|
62
|
+
project: "ShopifyMCPMockShop", // Use actual folder name
|
|
63
|
+
limit: 5
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Cross-project search (NEW in v2.4.3)
|
|
67
|
+
{
|
|
68
|
+
query: "error handling patterns",
|
|
69
|
+
project: "all", // Search across all projects
|
|
70
|
+
limit: 10
|
|
71
|
+
}
|
|
58
72
|
```
|
|
59
73
|
|
|
74
|
+
#### Default Behavior: Project-Scoped Search (NEW in v2.4.3)
|
|
75
|
+
**IMPORTANT**: Searches are now scoped to the current project by default:
|
|
76
|
+
- Auto-detects current project from your working directory
|
|
77
|
+
- Only returns results from that project unless you specify otherwise
|
|
78
|
+
- Use `project: "all"` to explicitly search across all projects
|
|
79
|
+
- Use `project: "ProjectName"` to search a specific project (use the actual folder name)
|
|
80
|
+
|
|
60
81
|
### store_reflection
|
|
61
82
|
Save important insights and decisions for future retrieval.
|
|
62
83
|
|
package/README.md
CHANGED
|
@@ -123,6 +123,30 @@ Once installed, just talk naturally:
|
|
|
123
123
|
|
|
124
124
|
The reflection specialist automatically activates. No special commands needed.
|
|
125
125
|
|
|
126
|
+
## Project-Scoped Search (New in v2.4.3)
|
|
127
|
+
|
|
128
|
+
Searches are now **project-aware by default**. When you ask about past conversations, Claude automatically searches within your current project:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
# In project "ShopifyMCPMockShop"
|
|
132
|
+
You: "What authentication method did we implement?"
|
|
133
|
+
Claude: [Searches only ShopifyMCPMockShop conversations]
|
|
134
|
+
|
|
135
|
+
# Need to search across all projects?
|
|
136
|
+
You: "Search all projects for WebSocket implementations"
|
|
137
|
+
Claude: [Searches across all your projects]
|
|
138
|
+
|
|
139
|
+
# Search a specific project
|
|
140
|
+
You: "Find Docker setup discussions in claude-self-reflect project"
|
|
141
|
+
Claude: [Searches only claude-self-reflect conversations]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Key behaviors:**
|
|
145
|
+
- **Default**: Searches current project based on your working directory
|
|
146
|
+
- **Cross-project**: Ask for "all projects" or "across projects"
|
|
147
|
+
- **Specific project**: Mention the project name explicitly
|
|
148
|
+
- **Privacy**: Each project's conversations remain isolated
|
|
149
|
+
|
|
126
150
|
## Memory Decay
|
|
127
151
|
|
|
128
152
|
Recent conversations matter more. Old ones fade. Like your brain, but reliable.
|
package/mcp-server/src/server.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import Any, Optional, List, Dict, Union
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
import json
|
|
9
9
|
import numpy as np
|
|
10
|
+
import hashlib
|
|
10
11
|
|
|
11
12
|
from fastmcp import FastMCP, Context
|
|
12
13
|
from pydantic import BaseModel, Field
|
|
@@ -149,7 +150,8 @@ async def reflect_on_past(
|
|
|
149
150
|
query: str = Field(description="The search query to find semantically similar conversations"),
|
|
150
151
|
limit: int = Field(default=5, description="Maximum number of results to return"),
|
|
151
152
|
min_score: float = Field(default=0.7, description="Minimum similarity score (0-1)"),
|
|
152
|
-
use_decay: Union[int, str] = Field(default=-1, description="Apply time-based decay: 1=enable, 0=disable, -1=use environment default (accepts int or str)")
|
|
153
|
+
use_decay: Union[int, str] = Field(default=-1, description="Apply time-based decay: 1=enable, 0=disable, -1=use environment default (accepts int or str)"),
|
|
154
|
+
project: Optional[str] = Field(default=None, description="Search specific project only. If not provided, searches current project based on working directory. Use 'all' to search across all projects.")
|
|
153
155
|
) -> str:
|
|
154
156
|
"""Search for relevant past conversations using semantic search with optional time decay."""
|
|
155
157
|
|
|
@@ -167,7 +169,37 @@ async def reflect_on_past(
|
|
|
167
169
|
else ENABLE_MEMORY_DECAY # -1 or any other value
|
|
168
170
|
)
|
|
169
171
|
|
|
172
|
+
# Determine project scope
|
|
173
|
+
target_project = project
|
|
174
|
+
if project is None:
|
|
175
|
+
# Try to detect current project from working directory
|
|
176
|
+
cwd = os.getcwd()
|
|
177
|
+
# Extract project name from path (e.g., /Users/.../projects/project-name)
|
|
178
|
+
path_parts = Path(cwd).parts
|
|
179
|
+
if 'projects' in path_parts:
|
|
180
|
+
idx = path_parts.index('projects')
|
|
181
|
+
if idx + 1 < len(path_parts):
|
|
182
|
+
target_project = path_parts[idx + 1]
|
|
183
|
+
elif '.claude' in path_parts:
|
|
184
|
+
# If we're in a .claude directory, go up to find project
|
|
185
|
+
for i, part in enumerate(path_parts):
|
|
186
|
+
if part == '.claude' and i > 0:
|
|
187
|
+
target_project = path_parts[i - 1]
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
# If still no project detected, use the last directory name
|
|
191
|
+
if target_project is None:
|
|
192
|
+
target_project = Path(cwd).name
|
|
193
|
+
|
|
194
|
+
# For project matching, we need to handle the dash-encoded format
|
|
195
|
+
# Convert folder name to the format used in stored data
|
|
196
|
+
if target_project != 'all':
|
|
197
|
+
# The stored format uses full path with dashes, so we need to construct it
|
|
198
|
+
# For now, let's try to match based on the end of the project name
|
|
199
|
+
pass # We'll handle this differently in the filtering logic
|
|
200
|
+
|
|
170
201
|
await ctx.debug(f"Searching for: {query}")
|
|
202
|
+
await ctx.debug(f"Project scope: {target_project if target_project != 'all' else 'all projects'}")
|
|
171
203
|
await ctx.debug(f"Decay enabled: {should_use_decay}")
|
|
172
204
|
await ctx.debug(f"Native decay mode: {USE_NATIVE_DECAY}")
|
|
173
205
|
await ctx.debug(f"ENABLE_MEMORY_DECAY env: {ENABLE_MEMORY_DECAY}")
|
|
@@ -182,13 +214,34 @@ async def reflect_on_past(
|
|
|
182
214
|
if not all_collections:
|
|
183
215
|
return "No conversation collections found. Please import conversations first."
|
|
184
216
|
|
|
185
|
-
|
|
217
|
+
# Filter collections by project if not searching all
|
|
218
|
+
project_collections = [] # Define at this scope for later use
|
|
219
|
+
if target_project != 'all':
|
|
220
|
+
# Generate the collection name pattern for this project
|
|
221
|
+
project_hash = hashlib.md5(target_project.encode()).hexdigest()[:8]
|
|
222
|
+
project_collections = [
|
|
223
|
+
c for c in all_collections
|
|
224
|
+
if c.startswith(f"conv_{project_hash}_")
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
if not project_collections:
|
|
228
|
+
# Try to find collections with project metadata
|
|
229
|
+
# Fall back to searching all collections but filtering by project metadata
|
|
230
|
+
await ctx.debug(f"No collections found for project hash {project_hash}, will filter by metadata")
|
|
231
|
+
collections_to_search = all_collections
|
|
232
|
+
else:
|
|
233
|
+
await ctx.debug(f"Found {len(project_collections)} collections for project {target_project}")
|
|
234
|
+
collections_to_search = project_collections
|
|
235
|
+
else:
|
|
236
|
+
collections_to_search = all_collections
|
|
237
|
+
|
|
238
|
+
await ctx.debug(f"Searching across {len(collections_to_search)} collections")
|
|
186
239
|
await ctx.debug(f"Using {'local' if PREFER_LOCAL_EMBEDDINGS or not voyage_client else 'Voyage AI'} embeddings")
|
|
187
240
|
|
|
188
241
|
all_results = []
|
|
189
242
|
|
|
190
243
|
# Search each collection
|
|
191
|
-
for collection_name in
|
|
244
|
+
for collection_name in collections_to_search:
|
|
192
245
|
try:
|
|
193
246
|
if should_use_decay and USE_NATIVE_DECAY and NATIVE_DECAY_AVAILABLE:
|
|
194
247
|
# Use native Qdrant decay with newer API
|
|
@@ -285,13 +338,23 @@ async def reflect_on_past(
|
|
|
285
338
|
raw_timestamp = point.payload.get('timestamp', datetime.now().isoformat())
|
|
286
339
|
clean_timestamp = raw_timestamp.replace('Z', '+00:00') if raw_timestamp.endswith('Z') else raw_timestamp
|
|
287
340
|
|
|
341
|
+
# Check project filter if we're searching all collections but want specific project
|
|
342
|
+
point_project = point.payload.get('project', collection_name.replace('conv_', '').replace('_voyage', '').replace('_local', ''))
|
|
343
|
+
|
|
344
|
+
# Handle project matching - check if the target project name appears at the end of the stored project path
|
|
345
|
+
if target_project != 'all' and not project_collections:
|
|
346
|
+
# The stored project name is like "-Users-ramakrishnanannaswamy-projects-ShopifyMCPMockShop"
|
|
347
|
+
# We want to match just "ShopifyMCPMockShop"
|
|
348
|
+
if not point_project.endswith(f"-{target_project}") and point_project != target_project:
|
|
349
|
+
continue # Skip results from other projects
|
|
350
|
+
|
|
288
351
|
all_results.append(SearchResult(
|
|
289
352
|
id=str(point.id),
|
|
290
353
|
score=point.score, # Score already includes decay
|
|
291
354
|
timestamp=clean_timestamp,
|
|
292
355
|
role=point.payload.get('start_role', point.payload.get('role', 'unknown')),
|
|
293
356
|
excerpt=(point.payload.get('text', '')[:500] + '...'),
|
|
294
|
-
project_name=
|
|
357
|
+
project_name=point_project,
|
|
295
358
|
conversation_id=point.payload.get('conversation_id'),
|
|
296
359
|
collection_name=collection_name
|
|
297
360
|
))
|
|
@@ -350,13 +413,23 @@ async def reflect_on_past(
|
|
|
350
413
|
raw_timestamp = point.payload.get('timestamp', datetime.now().isoformat())
|
|
351
414
|
clean_timestamp = raw_timestamp.replace('Z', '+00:00') if raw_timestamp.endswith('Z') else raw_timestamp
|
|
352
415
|
|
|
416
|
+
# Check project filter if we're searching all collections but want specific project
|
|
417
|
+
point_project = point.payload.get('project', collection_name.replace('conv_', '').replace('_voyage', '').replace('_local', ''))
|
|
418
|
+
|
|
419
|
+
# Handle project matching - check if the target project name appears at the end of the stored project path
|
|
420
|
+
if target_project != 'all' and not project_collections:
|
|
421
|
+
# The stored project name is like "-Users-ramakrishnanannaswamy-projects-ShopifyMCPMockShop"
|
|
422
|
+
# We want to match just "ShopifyMCPMockShop"
|
|
423
|
+
if not point_project.endswith(f"-{target_project}") and point_project != target_project:
|
|
424
|
+
continue # Skip results from other projects
|
|
425
|
+
|
|
353
426
|
all_results.append(SearchResult(
|
|
354
427
|
id=str(point.id),
|
|
355
428
|
score=adjusted_score, # Use adjusted score
|
|
356
429
|
timestamp=clean_timestamp,
|
|
357
430
|
role=point.payload.get('start_role', point.payload.get('role', 'unknown')),
|
|
358
431
|
excerpt=(point.payload.get('text', '')[:500] + '...'),
|
|
359
|
-
project_name=
|
|
432
|
+
project_name=point_project,
|
|
360
433
|
conversation_id=point.payload.get('conversation_id'),
|
|
361
434
|
collection_name=collection_name
|
|
362
435
|
))
|
|
@@ -375,13 +448,23 @@ async def reflect_on_past(
|
|
|
375
448
|
raw_timestamp = point.payload.get('timestamp', datetime.now().isoformat())
|
|
376
449
|
clean_timestamp = raw_timestamp.replace('Z', '+00:00') if raw_timestamp.endswith('Z') else raw_timestamp
|
|
377
450
|
|
|
451
|
+
# Check project filter if we're searching all collections but want specific project
|
|
452
|
+
point_project = point.payload.get('project', collection_name.replace('conv_', '').replace('_voyage', '').replace('_local', ''))
|
|
453
|
+
|
|
454
|
+
# Handle project matching - check if the target project name appears at the end of the stored project path
|
|
455
|
+
if target_project != 'all' and not project_collections:
|
|
456
|
+
# The stored project name is like "-Users-ramakrishnanannaswamy-projects-ShopifyMCPMockShop"
|
|
457
|
+
# We want to match just "ShopifyMCPMockShop"
|
|
458
|
+
if not point_project.endswith(f"-{target_project}") and point_project != target_project:
|
|
459
|
+
continue # Skip results from other projects
|
|
460
|
+
|
|
378
461
|
all_results.append(SearchResult(
|
|
379
462
|
id=str(point.id),
|
|
380
463
|
score=point.score,
|
|
381
464
|
timestamp=clean_timestamp,
|
|
382
465
|
role=point.payload.get('start_role', point.payload.get('role', 'unknown')),
|
|
383
466
|
excerpt=(point.payload.get('text', '')[:500] + '...'),
|
|
384
|
-
project_name=
|
|
467
|
+
project_name=point_project,
|
|
385
468
|
conversation_id=point.payload.get('conversation_id'),
|
|
386
469
|
collection_name=collection_name
|
|
387
470
|
))
|