claude-self-reflect 4.0.3 โ 5.0.4
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/docker-compose.yaml +3 -1
- package/installer/setup-wizard-docker.js +64 -9
- package/mcp-server/src/code_reload_tool.py +207 -98
- package/mcp-server/src/parallel_search.py +7 -6
- package/mcp-server/src/rich_formatting.py +10 -6
- package/mcp-server/src/safe_getters.py +217 -0
- package/mcp-server/src/search_tools.py +30 -8
- package/package.json +1 -1
- package/scripts/ast_grep_final_analyzer.py +16 -6
- package/scripts/csr-status +120 -17
- package/scripts/debug-august-parsing.py +5 -1
- package/scripts/debug-project-resolver.py +3 -3
- package/scripts/import-conversations-unified.py +292 -821
- package/scripts/session_quality_tracker.py +10 -0
- package/scripts/unified_state_manager.py +7 -4
- package/mcp-server/src/test_quality.py +0 -153
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Safe getter utilities for handling None values consistently."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def safe_get_list(
|
|
10
|
+
data: Optional[Dict[str, Any]],
|
|
11
|
+
key: str,
|
|
12
|
+
default: Optional[List] = None
|
|
13
|
+
) -> List[Any]:
|
|
14
|
+
"""
|
|
15
|
+
Safely get a list field from a dictionary, handling None and non-list values.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
data: Dictionary to get value from (can be None)
|
|
19
|
+
key: Key to retrieve
|
|
20
|
+
default: Default value if key not found or value is None
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
A list, either the value, converted value, or default/empty list
|
|
24
|
+
"""
|
|
25
|
+
if data is None:
|
|
26
|
+
return default if default is not None else []
|
|
27
|
+
|
|
28
|
+
value = data.get(key)
|
|
29
|
+
|
|
30
|
+
if value is None:
|
|
31
|
+
return default if default is not None else []
|
|
32
|
+
|
|
33
|
+
# Handle sets and tuples by converting to list
|
|
34
|
+
if isinstance(value, (set, tuple)):
|
|
35
|
+
return list(value)
|
|
36
|
+
|
|
37
|
+
# If it's already a list, return it
|
|
38
|
+
if isinstance(value, list):
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
# If it's not a list-like type, log warning and return empty list
|
|
42
|
+
logger.warning(
|
|
43
|
+
f"Expected list-like type for key '{key}', got {type(value).__name__}. "
|
|
44
|
+
f"Value: {repr(value)[:100]}"
|
|
45
|
+
)
|
|
46
|
+
return default if default is not None else []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def safe_get_str(
|
|
50
|
+
data: Optional[Dict[str, Any]],
|
|
51
|
+
key: str,
|
|
52
|
+
default: str = ""
|
|
53
|
+
) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Safely get a string field from a dictionary.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
data: Dictionary to get value from (can be None)
|
|
59
|
+
key: Key to retrieve
|
|
60
|
+
default: Default value if key not found or value is None
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A string, either the value or the default
|
|
64
|
+
"""
|
|
65
|
+
if data is None:
|
|
66
|
+
return default
|
|
67
|
+
|
|
68
|
+
value = data.get(key)
|
|
69
|
+
|
|
70
|
+
if value is None:
|
|
71
|
+
return default
|
|
72
|
+
|
|
73
|
+
# Convert to string if needed
|
|
74
|
+
return str(value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def safe_get_dict(
|
|
78
|
+
data: Optional[Dict[str, Any]],
|
|
79
|
+
key: str,
|
|
80
|
+
default: Optional[Dict] = None
|
|
81
|
+
) -> Dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Safely get a dictionary field from another dictionary.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
data: Dictionary to get value from (can be None)
|
|
87
|
+
key: Key to retrieve
|
|
88
|
+
default: Default value if key not found or value is None
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
A dictionary, either the value or the default/empty dict
|
|
92
|
+
"""
|
|
93
|
+
if data is None:
|
|
94
|
+
return default if default is not None else {}
|
|
95
|
+
|
|
96
|
+
value = data.get(key)
|
|
97
|
+
|
|
98
|
+
if value is None:
|
|
99
|
+
return default if default is not None else {}
|
|
100
|
+
|
|
101
|
+
if isinstance(value, dict):
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
logger.warning(
|
|
105
|
+
f"Expected dict for key '{key}', got {type(value).__name__}. "
|
|
106
|
+
f"Value: {repr(value)[:100]}"
|
|
107
|
+
)
|
|
108
|
+
return default if default is not None else {}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def safe_get_float(
|
|
112
|
+
data: Optional[Dict[str, Any]],
|
|
113
|
+
key: str,
|
|
114
|
+
default: float = 0.0
|
|
115
|
+
) -> float:
|
|
116
|
+
"""
|
|
117
|
+
Safely get a float field from a dictionary.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
data: Dictionary to get value from (can be None)
|
|
121
|
+
key: Key to retrieve
|
|
122
|
+
default: Default value if key not found or value is None/non-numeric
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
A float, either the converted value or the default
|
|
126
|
+
"""
|
|
127
|
+
if data is None:
|
|
128
|
+
return default
|
|
129
|
+
|
|
130
|
+
value = data.get(key)
|
|
131
|
+
|
|
132
|
+
if value is None:
|
|
133
|
+
return default
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
return float(value)
|
|
137
|
+
except (TypeError, ValueError) as e:
|
|
138
|
+
logger.warning(
|
|
139
|
+
f"Could not convert key '{key}' value to float: {repr(value)[:100]}. "
|
|
140
|
+
f"Error: {e}"
|
|
141
|
+
)
|
|
142
|
+
return default
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def safe_get_int(
|
|
146
|
+
data: Optional[Dict[str, Any]],
|
|
147
|
+
key: str,
|
|
148
|
+
default: int = 0
|
|
149
|
+
) -> int:
|
|
150
|
+
"""
|
|
151
|
+
Safely get an integer field from a dictionary.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
data: Dictionary to get value from (can be None)
|
|
155
|
+
key: Key to retrieve
|
|
156
|
+
default: Default value if key not found or value is None/non-numeric
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
An integer, either the converted value or the default
|
|
160
|
+
"""
|
|
161
|
+
if data is None:
|
|
162
|
+
return default
|
|
163
|
+
|
|
164
|
+
value = data.get(key)
|
|
165
|
+
|
|
166
|
+
if value is None:
|
|
167
|
+
return default
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
return int(value)
|
|
171
|
+
except (TypeError, ValueError) as e:
|
|
172
|
+
logger.warning(
|
|
173
|
+
f"Could not convert key '{key}' value to int: {repr(value)[:100]}. "
|
|
174
|
+
f"Error: {e}"
|
|
175
|
+
)
|
|
176
|
+
return default
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def safe_get_bool(
|
|
180
|
+
data: Optional[Dict[str, Any]],
|
|
181
|
+
key: str,
|
|
182
|
+
default: bool = False
|
|
183
|
+
) -> bool:
|
|
184
|
+
"""
|
|
185
|
+
Safely get a boolean field from a dictionary.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
data: Dictionary to get value from (can be None)
|
|
189
|
+
key: Key to retrieve
|
|
190
|
+
default: Default value if key not found or value is None
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
A boolean, either the value or the default
|
|
194
|
+
"""
|
|
195
|
+
if data is None:
|
|
196
|
+
return default
|
|
197
|
+
|
|
198
|
+
value = data.get(key)
|
|
199
|
+
|
|
200
|
+
if value is None:
|
|
201
|
+
return default
|
|
202
|
+
|
|
203
|
+
if isinstance(value, bool):
|
|
204
|
+
return value
|
|
205
|
+
|
|
206
|
+
# Handle string booleans
|
|
207
|
+
if isinstance(value, str):
|
|
208
|
+
return value.lower() in ('true', '1', 'yes', 'on')
|
|
209
|
+
|
|
210
|
+
# Handle numeric booleans
|
|
211
|
+
try:
|
|
212
|
+
return bool(int(value))
|
|
213
|
+
except (TypeError, ValueError):
|
|
214
|
+
logger.warning(
|
|
215
|
+
f"Could not convert key '{key}' value to bool: {repr(value)[:100]}"
|
|
216
|
+
)
|
|
217
|
+
return default
|
|
@@ -20,6 +20,26 @@ from .rich_formatting import format_search_results_rich
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def is_searchable_collection(name: str) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Check if collection name matches searchable patterns.
|
|
26
|
+
Supports both v3 and v4 collection naming conventions.
|
|
27
|
+
"""
|
|
28
|
+
return (
|
|
29
|
+
# v3 patterns
|
|
30
|
+
name.endswith('_local')
|
|
31
|
+
or name.endswith('_voyage')
|
|
32
|
+
# v4 patterns
|
|
33
|
+
or name.endswith('_384d') # Local v4 collections
|
|
34
|
+
or name.endswith('_1024d') # Cloud v4 collections
|
|
35
|
+
or '_cloud_' in name # Cloud v4 intermediate naming
|
|
36
|
+
# Reflections
|
|
37
|
+
or name.startswith('reflections')
|
|
38
|
+
# CSR prefixed collections
|
|
39
|
+
or name.startswith('csr_')
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
23
43
|
class SearchTools:
|
|
24
44
|
"""Handles all search operations for the MCP server."""
|
|
25
45
|
|
|
@@ -114,6 +134,11 @@ class SearchTools:
|
|
|
114
134
|
# Convert results to dict format
|
|
115
135
|
results = []
|
|
116
136
|
for result in search_results:
|
|
137
|
+
# Guard against None payload
|
|
138
|
+
if result.payload is None:
|
|
139
|
+
logger.warning(f"Result in {collection_name} has None payload, skipping")
|
|
140
|
+
continue
|
|
141
|
+
|
|
117
142
|
results.append({
|
|
118
143
|
'conversation_id': result.payload.get('conversation_id'),
|
|
119
144
|
'timestamp': result.payload.get('timestamp'),
|
|
@@ -274,10 +299,10 @@ class SearchTools:
|
|
|
274
299
|
return "<search_results><message>No collections available</message></search_results>"
|
|
275
300
|
|
|
276
301
|
# Include both conversation collections and reflection collections
|
|
302
|
+
# Use module-level function for consistency
|
|
277
303
|
filtered_collections = [
|
|
278
304
|
c for c in collections
|
|
279
|
-
if (c.name
|
|
280
|
-
c.name.startswith('reflections'))
|
|
305
|
+
if is_searchable_collection(c.name)
|
|
281
306
|
]
|
|
282
307
|
await ctx.debug(f"Searching across {len(filtered_collections)} collections")
|
|
283
308
|
|
|
@@ -403,8 +428,7 @@ class SearchTools:
|
|
|
403
428
|
# Include both conversation collections and reflection collections
|
|
404
429
|
filtered_collections = [
|
|
405
430
|
c for c in collections
|
|
406
|
-
if (c.name
|
|
407
|
-
c.name.startswith('reflections'))
|
|
431
|
+
if is_searchable_collection(c.name)
|
|
408
432
|
]
|
|
409
433
|
|
|
410
434
|
# Quick PARALLEL count across collections
|
|
@@ -493,8 +517,7 @@ class SearchTools:
|
|
|
493
517
|
# Include both conversation collections and reflection collections
|
|
494
518
|
filtered_collections = [
|
|
495
519
|
c for c in collections
|
|
496
|
-
if (c.name
|
|
497
|
-
c.name.startswith('reflections'))
|
|
520
|
+
if is_searchable_collection(c.name)
|
|
498
521
|
]
|
|
499
522
|
|
|
500
523
|
# Gather results for summary using PARALLEL search
|
|
@@ -590,8 +613,7 @@ class SearchTools:
|
|
|
590
613
|
# Include both conversation collections and reflection collections
|
|
591
614
|
filtered_collections = [
|
|
592
615
|
c for c in collections
|
|
593
|
-
if (c.name
|
|
594
|
-
c.name.startswith('reflections'))
|
|
616
|
+
if is_searchable_collection(c.name)
|
|
595
617
|
]
|
|
596
618
|
|
|
597
619
|
# Gather all results using PARALLEL search
|
package/package.json
CHANGED
|
@@ -257,15 +257,18 @@ class FinalASTGrepAnalyzer:
|
|
|
257
257
|
return '\n'.join(report)
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
def run_final_analysis():
|
|
260
|
+
def run_final_analysis(file_path=None):
|
|
261
261
|
"""Run final AST-GREP analysis with unified registry."""
|
|
262
262
|
print("๐ FINAL AST-GREP Analysis with Unified Registry")
|
|
263
263
|
print("=" * 60)
|
|
264
264
|
|
|
265
265
|
analyzer = FinalASTGrepAnalyzer()
|
|
266
266
|
|
|
267
|
-
#
|
|
268
|
-
|
|
267
|
+
# Use provided path or default
|
|
268
|
+
# Use relative path from script location
|
|
269
|
+
script_dir = Path(__file__).parent
|
|
270
|
+
default_path = script_dir.parent / "mcp-server" / "src" / "server.py"
|
|
271
|
+
server_path = file_path if file_path else str(default_path)
|
|
269
272
|
|
|
270
273
|
print(f"\nAnalyzing: {server_path}")
|
|
271
274
|
print("-" * 40)
|
|
@@ -299,14 +302,14 @@ def run_final_analysis():
|
|
|
299
302
|
|
|
300
303
|
# Generate and save report
|
|
301
304
|
report = analyzer.generate_report(result)
|
|
302
|
-
report_path = "
|
|
305
|
+
report_path = script_dir / "final_analysis_report.md"
|
|
303
306
|
with open(report_path, 'w') as f:
|
|
304
307
|
f.write(report)
|
|
305
308
|
|
|
306
309
|
print(f"\n๐ Full report saved to: {report_path}")
|
|
307
310
|
|
|
308
311
|
# Save JSON results
|
|
309
|
-
json_path = "
|
|
312
|
+
json_path = script_dir / "final_analysis_result.json"
|
|
310
313
|
with open(json_path, 'w') as f:
|
|
311
314
|
json.dump(result, f, indent=2)
|
|
312
315
|
|
|
@@ -325,4 +328,11 @@ def run_final_analysis():
|
|
|
325
328
|
|
|
326
329
|
|
|
327
330
|
if __name__ == "__main__":
|
|
328
|
-
|
|
331
|
+
import sys
|
|
332
|
+
if len(sys.argv) > 1:
|
|
333
|
+
# Use provided file path
|
|
334
|
+
file_path = sys.argv[1]
|
|
335
|
+
else:
|
|
336
|
+
# Default to server.py
|
|
337
|
+
file_path = str(default_path) # Use the same default path from above
|
|
338
|
+
run_final_analysis(file_path)
|
package/scripts/csr-status
CHANGED
|
@@ -6,6 +6,7 @@ Standalone script that doesn't require venv activation.
|
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
8
|
import time
|
|
9
|
+
import os
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from datetime import datetime, timedelta
|
|
11
12
|
import sys
|
|
@@ -147,6 +148,51 @@ def format_statusline_quality(critical=0, medium=0, low=0):
|
|
|
147
148
|
|
|
148
149
|
def get_session_health():
|
|
149
150
|
"""Get cached session health with icon-based quality display."""
|
|
151
|
+
# Get project-specific cache based on current directory
|
|
152
|
+
project_dir = os.getcwd()
|
|
153
|
+
project_name = os.path.basename(project_dir) if project_dir else "default"
|
|
154
|
+
|
|
155
|
+
# Check project-specific realtime cache first
|
|
156
|
+
quality_dir = Path.home() / ".claude-self-reflect" / "quality_by_project"
|
|
157
|
+
realtime_cache = quality_dir / f"{project_name}.json"
|
|
158
|
+
|
|
159
|
+
# Fallback to global cache if project-specific doesn't exist
|
|
160
|
+
if not realtime_cache.exists():
|
|
161
|
+
realtime_cache = Path.home() / ".claude-self-reflect" / "realtime_quality.json"
|
|
162
|
+
|
|
163
|
+
if realtime_cache.exists():
|
|
164
|
+
try:
|
|
165
|
+
# Check realtime cache age
|
|
166
|
+
mtime = datetime.fromtimestamp(realtime_cache.stat().st_mtime)
|
|
167
|
+
age = datetime.now() - mtime
|
|
168
|
+
|
|
169
|
+
if age < timedelta(minutes=5): # Fresh realtime data
|
|
170
|
+
with open(realtime_cache, 'r') as f:
|
|
171
|
+
realtime_data = json.load(f)
|
|
172
|
+
|
|
173
|
+
if "session_aggregate" in realtime_data:
|
|
174
|
+
agg = realtime_data["session_aggregate"]
|
|
175
|
+
issues = agg.get("total_issues", {})
|
|
176
|
+
critical = issues.get("critical", 0)
|
|
177
|
+
medium = issues.get("medium", 0)
|
|
178
|
+
low = issues.get("low", 0)
|
|
179
|
+
|
|
180
|
+
# Include score in display if significantly below threshold
|
|
181
|
+
score = agg.get("average_score", 100)
|
|
182
|
+
if critical > 0 or medium > 0 or low > 0:
|
|
183
|
+
# Show issue counts when there are any issues
|
|
184
|
+
return format_statusline_quality(critical, medium, low)
|
|
185
|
+
elif score < 70:
|
|
186
|
+
# Use red icon for scores below threshold (no issues but poor score)
|
|
187
|
+
icon = "๐ด"
|
|
188
|
+
return f"{icon} {score:.0f}%"
|
|
189
|
+
else:
|
|
190
|
+
# Good quality, no issues
|
|
191
|
+
return format_statusline_quality(critical, medium, low)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass # Fall back to old cache system
|
|
194
|
+
|
|
195
|
+
# Fall back to old cache system
|
|
150
196
|
# Check for session edit tracker to show appropriate label
|
|
151
197
|
tracker_file = Path.home() / ".claude-self-reflect" / "current_session_edits.json"
|
|
152
198
|
|
|
@@ -363,8 +409,63 @@ def get_compact_status():
|
|
|
363
409
|
except:
|
|
364
410
|
pass
|
|
365
411
|
|
|
366
|
-
# Get
|
|
367
|
-
|
|
412
|
+
# Get project-specific cache based on current directory
|
|
413
|
+
project_name = os.path.basename(os.getcwd()) if os.getcwd() else "default"
|
|
414
|
+
|
|
415
|
+
# Check project-specific realtime cache first
|
|
416
|
+
quality_dir = Path.home() / ".claude-self-reflect" / "quality_by_project"
|
|
417
|
+
realtime_cache = quality_dir / f"{project_name}.json"
|
|
418
|
+
|
|
419
|
+
# Fallback to global cache if project-specific doesn't exist
|
|
420
|
+
if not realtime_cache.exists():
|
|
421
|
+
realtime_cache = Path.home() / ".claude-self-reflect" / "realtime_quality.json"
|
|
422
|
+
|
|
423
|
+
grade_str = ""
|
|
424
|
+
quality_valid = False
|
|
425
|
+
|
|
426
|
+
if realtime_cache.exists():
|
|
427
|
+
try:
|
|
428
|
+
mtime = datetime.fromtimestamp(realtime_cache.stat().st_mtime)
|
|
429
|
+
age = datetime.now() - mtime
|
|
430
|
+
if age < timedelta(minutes=5): # Fresh realtime data
|
|
431
|
+
with open(realtime_cache, 'r') as f:
|
|
432
|
+
realtime_data = json.load(f)
|
|
433
|
+
|
|
434
|
+
if "session_aggregate" in realtime_data:
|
|
435
|
+
agg = realtime_data["session_aggregate"]
|
|
436
|
+
issues = agg.get("total_issues", {})
|
|
437
|
+
critical = issues.get("critical", 0)
|
|
438
|
+
medium = issues.get("medium", 0)
|
|
439
|
+
low = issues.get("low", 0)
|
|
440
|
+
score = agg.get("average_score", 100)
|
|
441
|
+
|
|
442
|
+
# Get icon based on score and issues
|
|
443
|
+
if score < 70:
|
|
444
|
+
icon = "๐ด" # Red for below threshold
|
|
445
|
+
else:
|
|
446
|
+
icon = get_quality_icon(critical, medium, low)
|
|
447
|
+
|
|
448
|
+
# Build compact display
|
|
449
|
+
if critical > 0 or medium > 0 or low > 0:
|
|
450
|
+
# Show issue counts when there are any issues
|
|
451
|
+
colored_parts = []
|
|
452
|
+
if critical > 0:
|
|
453
|
+
colored_parts.append(f"C:{critical}")
|
|
454
|
+
if medium > 0:
|
|
455
|
+
colored_parts.append(f"M:{medium}")
|
|
456
|
+
if low > 0:
|
|
457
|
+
colored_parts.append(f"L:{low}")
|
|
458
|
+
grade_str = f"[{icon}:{'ยท'.join(colored_parts)}]"
|
|
459
|
+
elif score < 70:
|
|
460
|
+
# Show score when below threshold but no specific issues
|
|
461
|
+
grade_str = f"[{icon}:{score:.0f}%]"
|
|
462
|
+
else:
|
|
463
|
+
grade_str = f"[{icon}]"
|
|
464
|
+
quality_valid = True
|
|
465
|
+
except:
|
|
466
|
+
pass
|
|
467
|
+
|
|
468
|
+
# Setup cache file path for fallback
|
|
368
469
|
project_name = os.path.basename(os.getcwd())
|
|
369
470
|
# Secure sanitization with whitelist approach
|
|
370
471
|
import re
|
|
@@ -372,21 +473,23 @@ def get_compact_status():
|
|
|
372
473
|
cache_dir = Path.home() / ".claude-self-reflect" / "quality_cache"
|
|
373
474
|
cache_file = cache_dir / f"{safe_project_name}.json"
|
|
374
475
|
|
|
375
|
-
#
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
#
|
|
379
|
-
|
|
380
|
-
if
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
476
|
+
# Fall back to old cache if no realtime data
|
|
477
|
+
if not quality_valid:
|
|
478
|
+
|
|
479
|
+
# If the exact cache file doesn't exist, try to find one that ends with this project name
|
|
480
|
+
# This handles cases like "metafora-Atlas-gold.json" for project "Atlas-gold"
|
|
481
|
+
if not cache_file.exists():
|
|
482
|
+
# Look for files ending with the project name
|
|
483
|
+
possible_files = list(cache_dir.glob(f"*-{safe_project_name}.json"))
|
|
484
|
+
if possible_files:
|
|
485
|
+
cache_file = possible_files[0] # Use the first match
|
|
486
|
+
|
|
487
|
+
# Validate cache file path stays within cache directory
|
|
488
|
+
if cache_file.exists() and not str(cache_file.resolve()).startswith(str(cache_dir.resolve())):
|
|
489
|
+
# Security issue - return placeholder
|
|
490
|
+
grade_str = "[...]"
|
|
491
|
+
else:
|
|
492
|
+
cache_file.parent.mkdir(exist_ok=True, parents=True)
|
|
390
493
|
|
|
391
494
|
# Try to get quality data (regenerate if too old or missing)
|
|
392
495
|
quality_valid = False
|
|
@@ -59,7 +59,11 @@ def parse_jsonl_file(file_path):
|
|
|
59
59
|
return messages
|
|
60
60
|
|
|
61
61
|
if __name__ == "__main__":
|
|
62
|
-
|
|
62
|
+
# Use home directory path
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
home = Path.home()
|
|
65
|
+
file_path = home / ".claude" / "projects" / f"-{home}-projects-claude-self-reflect" / "7b3354ed-d6d2-4eab-b328-1fced4bb63bb.jsonl"
|
|
66
|
+
file_path = str(file_path)
|
|
63
67
|
|
|
64
68
|
print(f"Parsing: {file_path}")
|
|
65
69
|
print("=" * 60)
|
|
@@ -48,9 +48,9 @@ from shared.normalization import normalize_project_name
|
|
|
48
48
|
import hashlib
|
|
49
49
|
|
|
50
50
|
test_paths = [
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
51
|
+
str(Path.home() / "projects" / "claude-self-reflect"),
|
|
52
|
+
str(Path.home() / "projects" / "memento"),
|
|
53
|
+
str(Path.home() / "projects" / "cc-enhance")
|
|
54
54
|
]
|
|
55
55
|
|
|
56
56
|
for path in test_paths:
|