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,366 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Claude Self-Reflect Status for CC Statusline
4
+ Standalone script that doesn't require venv activation.
5
+ """
6
+
7
+ import json
8
+ import time
9
+ from pathlib import Path
10
+ from datetime import datetime, timedelta
11
+ import sys
12
+
13
+ # Configuration
14
+ CYCLE_FILE = Path.home() / ".claude-self-reflect" / "statusline_cycle.json"
15
+ CYCLE_INTERVAL = 5 # seconds between cycles
16
+
17
+
18
+ def get_import_status():
19
+ """Get current import/indexing status."""
20
+ state_file = Path.home() / ".claude-self-reflect" / "config" / "imported-files.json"
21
+
22
+ if not state_file.exists():
23
+ return "šŸ“š CSR: Not configured"
24
+
25
+ try:
26
+ with open(state_file, 'r') as f:
27
+ state = json.load(f)
28
+
29
+ imported = len(state.get("imported_files", {}))
30
+
31
+ # Count total JSONL files
32
+ claude_dir = Path.home() / ".claude" / "projects"
33
+ total = 0
34
+ if claude_dir.exists():
35
+ for project_dir in claude_dir.iterdir():
36
+ if project_dir.is_dir():
37
+ total += len(list(project_dir.glob("*.jsonl")))
38
+
39
+ if total == 0:
40
+ return "šŸ“š CSR: No files"
41
+
42
+ percent = min(100, (imported / total * 100))
43
+
44
+ # Color coding
45
+ if percent >= 95:
46
+ emoji = "āœ…"
47
+ elif percent >= 50:
48
+ emoji = "šŸ”„"
49
+ else:
50
+ emoji = "ā³"
51
+
52
+ return f"{emoji} CSR: {percent:.0f}% indexed"
53
+
54
+ except Exception:
55
+ return "šŸ“š CSR: Error"
56
+
57
+
58
+ def get_session_health():
59
+ """Get cached session health."""
60
+ cache_file = Path.home() / ".claude-self-reflect" / "session_quality.json"
61
+
62
+ if not cache_file.exists():
63
+ # Fall back to import status if no health data
64
+ return get_import_status()
65
+
66
+ try:
67
+ # Check cache age
68
+ mtime = datetime.fromtimestamp(cache_file.stat().st_mtime)
69
+ age = datetime.now() - mtime
70
+
71
+ if age > timedelta(minutes=5):
72
+ # Fall back to import status if stale
73
+ return get_import_status()
74
+
75
+ with open(cache_file, 'r') as f:
76
+ data = json.load(f)
77
+
78
+ if data.get('status') != 'success':
79
+ # Fall back to import status if no session
80
+ return get_import_status()
81
+
82
+ summary = data['summary']
83
+ grade = summary['quality_grade']
84
+ issues = summary['total_issues']
85
+
86
+ # Color coding
87
+ if grade in ['A+', 'A']:
88
+ emoji = '🟢'
89
+ elif grade in ['B', 'C']:
90
+ emoji = '🟔'
91
+ else:
92
+ emoji = 'šŸ”“'
93
+
94
+ if issues > 0:
95
+ return f"{emoji} Code: {grade} ({issues})"
96
+ else:
97
+ return f"{emoji} Code: {grade}"
98
+
99
+ except Exception:
100
+ return get_import_status()
101
+
102
+
103
+ def get_current_cycle():
104
+ """Determine which metric to show based on cycle."""
105
+ # Read or create cycle state
106
+ cycle_state = {"last_update": 0, "current": "import"}
107
+
108
+ if CYCLE_FILE.exists():
109
+ try:
110
+ with open(CYCLE_FILE, 'r') as f:
111
+ cycle_state = json.load(f)
112
+ except:
113
+ pass
114
+
115
+ # Check if it's time to cycle
116
+ now = time.time()
117
+ if now - cycle_state["last_update"] >= CYCLE_INTERVAL:
118
+ # Toggle between import and health
119
+ cycle_state["current"] = "health" if cycle_state["current"] == "import" else "import"
120
+ cycle_state["last_update"] = now
121
+
122
+ # Save state
123
+ CYCLE_FILE.parent.mkdir(exist_ok=True)
124
+ with open(CYCLE_FILE, 'w') as f:
125
+ json.dump(cycle_state, f)
126
+
127
+ return cycle_state["current"]
128
+
129
+
130
+ def get_compact_status():
131
+ """Get both import and quality in compact format: [100%][🟢:A+]"""
132
+ import subprocess
133
+ import os
134
+ import re
135
+ import shutil
136
+
137
+ # Get project-specific status using claude-self-reflect status command
138
+ import_pct = "?"
139
+ time_behind = ""
140
+
141
+ try:
142
+ # Get current working directory to determine project
143
+ cwd = os.getcwd()
144
+ project_name = os.path.basename(cwd)
145
+
146
+ # Get status from claude-self-reflect with secure path
147
+ import shutil
148
+ csr_binary = shutil.which("claude-self-reflect")
149
+ if not csr_binary or not os.path.isfile(csr_binary):
150
+ # Fallback if binary not found
151
+ import_pct = "?"
152
+ return f"[{import_pct}]"
153
+
154
+ result = subprocess.run(
155
+ [csr_binary, "status"],
156
+ capture_output=True,
157
+ text=True,
158
+ timeout=2
159
+ )
160
+
161
+ if result.returncode == 0:
162
+ status_data = json.loads(result.stdout)
163
+
164
+ # Try to find project-specific percentage
165
+ project_pct = None
166
+ encoded_path = None
167
+
168
+ # Try exact project name FIRST
169
+ if project_name in status_data.get("projects", {}):
170
+ project_pct = status_data["projects"][project_name].get("percentage")
171
+ encoded_path = project_name # Use project name for file lookup
172
+
173
+ # Only try encoded path if exact match not found
174
+ elif project_pct is None:
175
+ # Convert path to encoded format
176
+ encoded_path = cwd.replace("/", "-")
177
+ if encoded_path.startswith("-"):
178
+ encoded_path = encoded_path[1:] # Remove leading dash
179
+
180
+ if encoded_path in status_data.get("projects", {}):
181
+ project_pct = status_data["projects"][encoded_path].get("percentage")
182
+
183
+ # Use project percentage if found, otherwise use overall
184
+ if project_pct is not None:
185
+ pct = int(project_pct)
186
+ else:
187
+ pct = int(status_data.get("overall", {}).get("percentage", 0))
188
+
189
+ import_pct = f"{pct}%"
190
+
191
+ # Only show time behind if NOT at 100%
192
+ # This indicates how old the unindexed files are
193
+ if pct < 100:
194
+ # Check for newest unindexed file
195
+ state_file = Path.home() / ".claude-self-reflect" / "config" / "imported-files.json"
196
+ if state_file.exists():
197
+ with open(state_file, 'r') as f:
198
+ state = json.load(f)
199
+
200
+ # Find project directory
201
+ claude_dir = Path.home() / ".claude" / "projects"
202
+ if encoded_path:
203
+ project_dir = claude_dir / encoded_path
204
+ if not project_dir.exists() and not encoded_path.startswith("-"):
205
+ project_dir = claude_dir / f"-{encoded_path}"
206
+
207
+ if project_dir.exists():
208
+ # Find the newest UNINDEXED file
209
+ newest_unindexed_time = None
210
+ for jsonl_file in project_dir.glob("*.jsonl"):
211
+ file_key = str(jsonl_file)
212
+ # Only check unindexed files
213
+ if file_key not in state.get("imported_files", {}):
214
+ file_time = datetime.fromtimestamp(jsonl_file.stat().st_mtime)
215
+ if newest_unindexed_time is None or file_time > newest_unindexed_time:
216
+ newest_unindexed_time = file_time
217
+
218
+ # Calculate how behind we are
219
+ if newest_unindexed_time:
220
+ age = datetime.now() - newest_unindexed_time
221
+ if age < timedelta(minutes=5):
222
+ time_behind = " <5m"
223
+ elif age < timedelta(hours=1):
224
+ time_behind = f" {int(age.total_seconds() / 60)}m"
225
+ elif age < timedelta(days=1):
226
+ time_behind = f" {int(age.total_seconds() / 3600)}h"
227
+ else:
228
+ time_behind = f" {int(age.days)}d"
229
+ except:
230
+ # Fallback to simple file counting
231
+ state_file = Path.home() / ".claude-self-reflect" / "config" / "imported-files.json"
232
+ if state_file.exists():
233
+ try:
234
+ with open(state_file, 'r') as f:
235
+ state = json.load(f)
236
+ imported = len(state.get("imported_files", {}))
237
+
238
+ claude_dir = Path.home() / ".claude" / "projects"
239
+ total = 0
240
+ if claude_dir.exists():
241
+ for project_dir in claude_dir.iterdir():
242
+ if project_dir.is_dir():
243
+ total += len(list(project_dir.glob("*.jsonl")))
244
+
245
+ if total > 0:
246
+ pct = min(100, int(imported / total * 100))
247
+ import_pct = f"{pct}%"
248
+ except:
249
+ pass
250
+
251
+ # Get quality grade - PER PROJECT cache
252
+ # BUG FIX: Cache must be per-project, not global!
253
+ project_name = os.path.basename(os.getcwd())
254
+ # Secure sanitization with whitelist approach
255
+ import re
256
+ safe_project_name = re.sub(r'[^a-zA-Z0-9_-]', '_', project_name)[:100]
257
+ cache_dir = Path.home() / ".claude-self-reflect" / "quality_cache"
258
+ cache_file = cache_dir / f"{safe_project_name}.json"
259
+
260
+ # Validate cache file path stays within cache directory
261
+ if cache_file.exists() and not str(cache_file.resolve()).startswith(str(cache_dir.resolve())):
262
+ # Security issue - return placeholder
263
+ grade_str = "[...]"
264
+ else:
265
+ cache_file.parent.mkdir(exist_ok=True, parents=True)
266
+ grade_str = ""
267
+
268
+ # Try to get quality data (regenerate if too old or missing)
269
+ quality_valid = False
270
+
271
+ if cache_file.exists():
272
+ try:
273
+ mtime = datetime.fromtimestamp(cache_file.stat().st_mtime)
274
+ age = datetime.now() - mtime
275
+
276
+ # Use quality data up to 24 hours old (more reasonable)
277
+ if age < timedelta(hours=24):
278
+ with open(cache_file, 'r') as f:
279
+ data = json.load(f)
280
+
281
+ if data.get('status') == 'success':
282
+ summary = data['summary']
283
+ grade = summary['quality_grade']
284
+ issues = summary.get('total_issues', 0)
285
+ scope = data.get('scope_label', 'Core') # Get scope label
286
+
287
+ # GPT-5 fix: Remove forced downgrades, trust the analyzer's grade
288
+ # Grade should reflect actual quality metrics, not arbitrary thresholds
289
+
290
+ # Pick emoji based on grade
291
+ if grade in ['A+', 'A']:
292
+ emoji = '🟢'
293
+ elif grade in ['B', 'C']:
294
+ emoji = '🟔'
295
+ else:
296
+ emoji = 'šŸ”“'
297
+
298
+ # Simple, clear display without confusing scope labels
299
+ grade_str = f"[{emoji}:{grade}/{issues}]"
300
+ quality_valid = True
301
+ except:
302
+ pass
303
+
304
+ # If no valid quality data, show last known value or placeholder
305
+ if not quality_valid and not grade_str:
306
+ # Try to use last known value from cache even if expired
307
+ try:
308
+ if cache_file.exists():
309
+ with open(cache_file, 'r') as f:
310
+ old_data = json.load(f)
311
+ if old_data.get('status') == 'success':
312
+ old_grade = old_data['summary']['quality_grade']
313
+ old_issues = old_data['summary'].get('total_issues', 0)
314
+ # Show with dimmed indicator that it's old
315
+ if old_grade in ['A+', 'A']:
316
+ emoji = '🟢'
317
+ elif old_grade in ['B', 'C']:
318
+ emoji = '🟔'
319
+ else:
320
+ emoji = 'šŸ”“'
321
+ grade_str = f"[{emoji}:{old_grade}/{old_issues}]"
322
+ else:
323
+ grade_str = "[...]"
324
+ else:
325
+ grade_str = "[...]"
326
+ except:
327
+ grade_str = "[...]"
328
+
329
+ # Add mini progress bar if not 100%
330
+ bar_str = ""
331
+ if import_pct != "?" and import_pct != "100%":
332
+ pct_num = int(import_pct.rstrip('%'))
333
+ filled = int(pct_num * 4 / 100) # 4-char mini bar
334
+ empty = 4 - filled
335
+ bar_str = "ā–ˆ" * filled + "ā–‘" * empty + " "
336
+
337
+ # Return compact format with bar, percentage, time behind, and grade
338
+ return f"[{bar_str}{import_pct}{time_behind}]{grade_str}"
339
+
340
+ def main():
341
+ """Main entry point for CC statusline."""
342
+ # Check for forced mode
343
+ if len(sys.argv) > 1:
344
+ if sys.argv[1] == "--import":
345
+ print(get_import_status())
346
+ elif sys.argv[1] == "--health":
347
+ print(get_session_health())
348
+ elif sys.argv[1] == "--quality-only":
349
+ # Only show quality, not import (to avoid duplication with MCP status)
350
+ health = get_session_health()
351
+ # Only show if it's actual quality data, not fallback to import
352
+ if "Code:" in health:
353
+ print(health)
354
+ elif sys.argv[1] == "--compact":
355
+ print(get_compact_status())
356
+ else:
357
+ # Default to compact mode
358
+ print(get_compact_status())
359
+ return
360
+
361
+ # Default to compact format (no cycling)
362
+ print(get_compact_status())
363
+
364
+
365
+ if __name__ == "__main__":
366
+ main()
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """Debug why August files aren't parsing properly."""
3
+
4
+ import json
5
+ import sys
6
+
7
+ def parse_jsonl_file(file_path):
8
+ """Parse JSONL file and extract messages."""
9
+ messages = []
10
+
11
+ with open(file_path, 'r', encoding='utf-8') as f:
12
+ for line_num, line in enumerate(f, 1):
13
+ line = line.strip()
14
+ if not line:
15
+ continue
16
+
17
+ try:
18
+ data = json.loads(line)
19
+
20
+ # Skip summary messages
21
+ if data.get('type') == 'summary':
22
+ print(f"Line {line_num}: Skipping summary")
23
+ continue
24
+
25
+ # Handle messages with type user/assistant at root level
26
+ if data.get('type') in ['user', 'assistant']:
27
+ if 'message' in data and data['message']:
28
+ msg = data['message']
29
+ if msg.get('role') and msg.get('content'):
30
+ content = msg['content']
31
+ if isinstance(content, list):
32
+ text_parts = []
33
+ for item in content:
34
+ if isinstance(item, dict) and item.get('type') == 'text':
35
+ text_parts.append(item.get('text', ''))
36
+ elif isinstance(item, str):
37
+ text_parts.append(item)
38
+ content = '\n'.join(text_parts)
39
+
40
+ if content:
41
+ messages.append({
42
+ 'role': msg['role'],
43
+ 'content': content[:200] + '...' if len(content) > 200 else content,
44
+ 'line': line_num
45
+ })
46
+ print(f"Line {line_num}: Extracted {msg['role']} message ({len(content)} chars)")
47
+ else:
48
+ print(f"Line {line_num}: Empty content for {msg['role']}")
49
+ else:
50
+ print(f"Line {line_num}: Missing role or content in message field")
51
+ else:
52
+ print(f"Line {line_num}: No message field for type={data.get('type')}")
53
+ else:
54
+ print(f"Line {line_num}: Unknown type={data.get('type')}")
55
+
56
+ except Exception as e:
57
+ print(f"Line {line_num}: Parse error - {e}")
58
+
59
+ return messages
60
+
61
+ if __name__ == "__main__":
62
+ file_path = "/Users/ramakrishnanannaswamy/.claude/projects/-Users-ramakrishnanannaswamy-projects-claude-self-reflect/7b3354ed-d6d2-4eab-b328-1fced4bb63bb.jsonl"
63
+
64
+ print(f"Parsing: {file_path}")
65
+ print("=" * 60)
66
+
67
+ messages = parse_jsonl_file(file_path)
68
+
69
+ print("\n" + "=" * 60)
70
+ print(f"Total messages extracted: {len(messages)}")
71
+
72
+ if messages:
73
+ print("\nFirst 5 messages:")
74
+ for i, msg in enumerate(messages[:5]):
75
+ print(f"\n{i+1}. Line {msg['line']}: {msg['role']}")
76
+ print(f" Content: {msg['content'][:100]}...")
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Debug import of a single file with summary messages
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+
9
+ # Target file
10
+ test_file = Path.home() / '.claude/projects/-Users-ramakrishnanannaswamy-projects-claude-self-reflect/c072a61e-aebb-4c85-960b-c5ffeafa7115.jsonl'
11
+
12
+ print(f"Analyzing: {test_file.name}\n")
13
+
14
+ # Read and analyze the file
15
+ all_messages = []
16
+ summary_count = 0
17
+ user_count = 0
18
+ assistant_count = 0
19
+ other_count = 0
20
+
21
+ with open(test_file, 'r') as f:
22
+ for i, line in enumerate(f, 1):
23
+ if line.strip():
24
+ try:
25
+ data = json.loads(line)
26
+ msg_type = data.get('type', 'unknown')
27
+
28
+ print(f"Line {i}: type={msg_type}", end="")
29
+
30
+ # Check what would be extracted
31
+ if msg_type == 'summary':
32
+ summary_count += 1
33
+ print(f" -> SKIPPED (summary)")
34
+ continue
35
+
36
+ # Check for messages with type user/assistant
37
+ if msg_type in ['user', 'assistant']:
38
+ if 'message' in data and data['message']:
39
+ msg = data['message']
40
+ if msg.get('role') and msg.get('content'):
41
+ all_messages.append(msg)
42
+ if msg_type == 'user':
43
+ user_count += 1
44
+ else:
45
+ assistant_count += 1
46
+
47
+ # Extract a preview of content
48
+ content = msg.get('content', '')
49
+ if isinstance(content, list) and len(content) > 0:
50
+ first_item = content[0]
51
+ if isinstance(first_item, dict):
52
+ preview = str(first_item.get('content', first_item.get('text', '')))[:50]
53
+ else:
54
+ preview = str(first_item)[:50]
55
+ else:
56
+ preview = str(content)[:50]
57
+
58
+ print(f" -> EXTRACTED (role={msg['role']}, preview: {preview}...)")
59
+ else:
60
+ print(f" -> NO role/content in message")
61
+ else:
62
+ print(f" -> NO message field")
63
+ else:
64
+ other_count += 1
65
+ print(f" -> OTHER TYPE")
66
+
67
+ except json.JSONDecodeError as e:
68
+ print(f"Line {i}: INVALID JSON - {e}")
69
+
70
+ print(f"\n=== SUMMARY ===")
71
+ print(f"Total lines: {i}")
72
+ print(f"Summaries (skipped): {summary_count}")
73
+ print(f"User messages: {user_count}")
74
+ print(f"Assistant messages: {assistant_count}")
75
+ print(f"Other types: {other_count}")
76
+ print(f"Total extracted messages: {len(all_messages)}")
77
+
78
+ # Check for Memento content
79
+ memento_found = False
80
+ for msg in all_messages:
81
+ content = str(msg.get('content', ''))
82
+ if 'memento' in content.lower():
83
+ memento_found = True
84
+ break
85
+
86
+ print(f"\nMemento content found in messages: {memento_found}")
87
+
88
+ if len(all_messages) > 0:
89
+ print(f"\nāœ… File SHOULD be importable with {len(all_messages)} messages")
90
+ else:
91
+ print(f"\nāŒ File would result in ZERO messages imported")
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env python3
2
+ """Test ProjectResolver to see if it's finding collections correctly."""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+ sys.path.insert(0, str(Path(__file__).parent.parent / 'mcp-server' / 'src'))
7
+
8
+ from qdrant_client import QdrantClient
9
+ from project_resolver import ProjectResolver
10
+
11
+ # Connect to Qdrant
12
+ client = QdrantClient(url="http://localhost:6333")
13
+
14
+ # Create resolver
15
+ resolver = ProjectResolver(client)
16
+
17
+ # Test projects
18
+ test_projects = [
19
+ "claude-self-reflect",
20
+ "memento",
21
+ "cc-enhance",
22
+ "all"
23
+ ]
24
+
25
+ print("=== Testing ProjectResolver ===\n")
26
+
27
+ for project in test_projects:
28
+ print(f"Project: '{project}'")
29
+ collections = resolver.find_collections_for_project(project)
30
+ print(f" Found {len(collections)} collections")
31
+
32
+ if collections:
33
+ # Show first 3 collections
34
+ for coll in collections[:3]:
35
+ try:
36
+ info = client.get_collection(coll)
37
+ suffix = "_local" if coll.endswith("_local") else "_voyage"
38
+ print(f" - {coll}: {info.points_count} points ({suffix})")
39
+ except:
40
+ print(f" - {coll}: <error getting info>")
41
+ else:
42
+ print(" - No collections found!")
43
+ print()
44
+
45
+ # Also test the normalization directly
46
+ print("\n=== Testing Direct Normalization ===")
47
+ from shared.normalization import normalize_project_name
48
+ import hashlib
49
+
50
+ test_paths = [
51
+ "/Users/ramakrishnanannaswamy/projects/claude-self-reflect",
52
+ "/Users/ramakrishnanannaswamy/projects/memento",
53
+ "/Users/ramakrishnanannaswamy/projects/cc-enhance"
54
+ ]
55
+
56
+ for path in test_paths:
57
+ normalized = normalize_project_name(path)
58
+ name_hash = hashlib.md5(normalized.encode()).hexdigest()[:8]
59
+ collection_local = f"conv_{name_hash}_local"
60
+ collection_voyage = f"conv_{name_hash}_voyage"
61
+
62
+ print(f"Path: {path}")
63
+ print(f" Normalized: {normalized}")
64
+ print(f" Hash: {name_hash}")
65
+ print(f" Expected collections:")
66
+ print(f" - {collection_local}")
67
+ print(f" - {collection_voyage}")
68
+
69
+ # Check if these exist
70
+ all_collections = [c.name for c in client.get_collections().collections]
71
+ if collection_local in all_collections:
72
+ info = client.get_collection(collection_local)
73
+ print(f" āœ“ {collection_local} exists with {info.points_count} points")
74
+ else:
75
+ print(f" āœ— {collection_local} not found")
76
+
77
+ if collection_voyage in all_collections:
78
+ info = client.get_collection(collection_voyage)
79
+ print(f" āœ“ {collection_voyage} exists with {info.points_count} points")
80
+ else:
81
+ print(f" āœ— {collection_voyage} not found")
82
+ print()