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.
- package/.claude/agents/claude-self-reflect-test.md +992 -510
- package/.claude/agents/reflection-specialist.md +59 -3
- package/README.md +14 -5
- package/installer/cli.js +16 -0
- package/installer/postinstall.js +14 -0
- package/installer/statusline-setup.js +289 -0
- package/mcp-server/run-mcp.sh +73 -5
- package/mcp-server/src/app_context.py +64 -0
- package/mcp-server/src/config.py +57 -0
- package/mcp-server/src/connection_pool.py +286 -0
- package/mcp-server/src/decay_manager.py +106 -0
- package/mcp-server/src/embedding_manager.py +64 -40
- package/mcp-server/src/embeddings_old.py +141 -0
- package/mcp-server/src/models.py +64 -0
- package/mcp-server/src/parallel_search.py +305 -0
- package/mcp-server/src/project_resolver.py +5 -0
- package/mcp-server/src/reflection_tools.py +211 -0
- package/mcp-server/src/rich_formatting.py +196 -0
- package/mcp-server/src/search_tools.py +874 -0
- package/mcp-server/src/server.py +127 -1720
- package/mcp-server/src/temporal_design.py +132 -0
- package/mcp-server/src/temporal_tools.py +604 -0
- package/mcp-server/src/temporal_utils.py +384 -0
- package/mcp-server/src/utils.py +150 -67
- package/package.json +15 -1
- package/scripts/add-timestamp-indexes.py +134 -0
- package/scripts/ast_grep_final_analyzer.py +325 -0
- package/scripts/ast_grep_unified_registry.py +556 -0
- package/scripts/check-collections.py +29 -0
- package/scripts/csr-status +366 -0
- package/scripts/debug-august-parsing.py +76 -0
- package/scripts/debug-import-single.py +91 -0
- package/scripts/debug-project-resolver.py +82 -0
- package/scripts/debug-temporal-tools.py +135 -0
- package/scripts/delta-metadata-update.py +547 -0
- package/scripts/import-conversations-unified.py +157 -25
- package/scripts/precompact-hook.sh +33 -0
- package/scripts/session_quality_tracker.py +481 -0
- package/scripts/streaming-watcher.py +1578 -0
- package/scripts/update_patterns.py +334 -0
- 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()
|