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.
@@ -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
- await ctx.debug(f"Searching across {len(all_collections)} collections")
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 all_collections:
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=point.payload.get('project', collection_name.replace('conv_', '').replace('_voyage', '').replace('_local', '')),
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=point.payload.get('project', collection_name.replace('conv_', '').replace('_voyage', '').replace('_local', '')),
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=point.payload.get('project', collection_name.replace('conv_', '').replace('_voyage', '').replace('_local', '')),
467
+ project_name=point_project,
385
468
  conversation_id=point.payload.get('conversation_id'),
386
469
  collection_name=collection_name
387
470
  ))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-self-reflect",
3
- "version": "2.4.2",
3
+ "version": "2.4.3",
4
4
  "description": "Give Claude perfect memory of all your conversations - Installation wizard for Python MCP server",
5
5
  "keywords": [
6
6
  "claude",