claude-self-reflect 3.3.0 → 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.
@@ -4,6 +4,7 @@ import os
4
4
  import json
5
5
  import logging
6
6
  import time
7
+ import html
7
8
  from typing import Optional, List, Dict, Any
8
9
  from datetime import datetime, timezone
9
10
  from pathlib import Path
@@ -168,20 +169,23 @@ class SearchTools:
168
169
  if include_raw:
169
170
  output += f"**Raw Payload:**\n```json\n{json.dumps(result.get('payload', {}), indent=2)}\n```\n\n"
170
171
  else:
171
- # XML format (default)
172
- output = f"<search_results>\n<query>{query}</query>\n<count>{len(results)}</count>\n"
172
+ # XML format (default) with proper escaping
173
+ def _esc(x): return html.escape(str(x), quote=False)
174
+
175
+ output = f"<search_results>\n<query>{_esc(query)}</query>\n<count>{len(results)}</count>\n"
173
176
  for i, result in enumerate(results, 1):
174
177
  output += f"<result index=\"{i}\">\n"
175
178
  output += f" <score>{result['score']:.3f}</score>\n"
176
- output += f" <timestamp>{result.get('timestamp', 'N/A')}</timestamp>\n"
177
- output += f" <conversation_id>{result.get('conversation_id', 'N/A')}</conversation_id>\n"
179
+ output += f" <timestamp>{_esc(result.get('timestamp', 'N/A'))}</timestamp>\n"
180
+ output += f" <conversation_id>{_esc(result.get('conversation_id', 'N/A'))}</conversation_id>\n"
178
181
  if not brief:
179
182
  # Handle both 'content' and 'excerpt' fields
180
- content = result.get('content', result.get('excerpt', ''))
183
+ content = result.get('content', result.get('excerpt', result.get('text', '')))
181
184
  truncated = content[:500] + ('...' if len(content) > 500 else '')
182
185
  output += f" <content><![CDATA[{truncated}]]></content>\n"
183
186
  if include_raw:
184
- output += f" <raw_payload>{json.dumps(result.get('payload', {}))}</raw_payload>\n"
187
+ # Use CDATA for large JSON payloads
188
+ output += f" <raw_payload><![CDATA[{json.dumps(result.get('payload', {}), ensure_ascii=False)}]]></raw_payload>\n"
185
189
  output += "</result>\n"
186
190
  output += "</search_results>"
187
191
 
@@ -394,22 +398,29 @@ class SearchTools:
394
398
  top_result = max(all_results, key=lambda x: x.get('score', 0)) if all_results else None
395
399
  top_score = top_result.get('score', 0) if top_result else 0
396
400
 
397
- # Format quick search response
401
+ # Format quick search response with proper XML escaping
402
+ def _esc(x): return html.escape(str(x), quote=False)
403
+
398
404
  if not top_result:
399
405
  return "<quick_search><count>0</count><message>No matches found</message></quick_search>"
400
-
406
+
407
+ # Get preview text and ensure we have content fallbacks
408
+ preview_text = top_result.get('excerpt', top_result.get('content', top_result.get('text', '')))[:200]
409
+
401
410
  return f"""<quick_search>
402
- <count>{collections_with_matches} collections with matches</count>
411
+ <count>{collections_with_matches}</count>
412
+ <collections_with_matches>{collections_with_matches}</collections_with_matches>
403
413
  <top_result>
404
414
  <score>{top_result['score']:.3f}</score>
405
- <timestamp>{top_result.get('timestamp', 'N/A')}</timestamp>
406
- <preview>{top_result.get('excerpt', top_result.get('content', ''))[:200]}...</preview>
415
+ <timestamp>{_esc(top_result.get('timestamp', 'N/A'))}</timestamp>
416
+ <preview><![CDATA[{preview_text}...]]></preview>
407
417
  </top_result>
408
418
  </quick_search>"""
409
-
419
+
410
420
  except Exception as e:
411
421
  logger.error(f"Quick search failed: {e}", exc_info=True)
412
- return f"<quick_search><error>Quick search failed: {str(e)}</error></quick_search>"
422
+ def _esc(x): return html.escape(str(x), quote=False)
423
+ return f"<quick_search><error>Quick search failed: {_esc(str(e))}</error></quick_search>"
413
424
 
414
425
  async def search_summary(
415
426
  self,
@@ -606,46 +617,83 @@ class SearchTools:
606
617
  project: Optional[str] = None
607
618
  ) -> str:
608
619
  """Search for conversations that analyzed a specific file."""
609
-
620
+
610
621
  await ctx.debug(f"Searching for file: {file_path}, project={project}")
611
-
622
+
612
623
  try:
613
- # Normalize file path
614
- normalized_path = str(Path(file_path).resolve())
615
-
624
+ # Create multiple path variants to match how paths are stored
625
+ # Import uses normalize_file_path which replaces /Users/ with ~/
626
+ path_variants = set()
627
+
628
+ # Original path
629
+ path_variants.add(file_path)
630
+
631
+ # Basename only
632
+ path_variants.add(os.path.basename(file_path))
633
+
634
+ # Try to resolve if it's a valid path
635
+ try:
636
+ resolved_path = str(Path(file_path).resolve())
637
+ path_variants.add(resolved_path)
638
+
639
+ # Convert resolved path to ~/ format (matching how import stores it)
640
+ home_dir = str(Path.home())
641
+ if resolved_path.startswith(home_dir):
642
+ tilde_path = resolved_path.replace(home_dir, '~', 1)
643
+ path_variants.add(tilde_path)
644
+
645
+ # Also try with /Users/ replaced by ~/
646
+ if '/Users/' in resolved_path:
647
+ path_variants.add(resolved_path.replace('/Users/', '~/', 1))
648
+ except:
649
+ pass
650
+
651
+ # If path starts with ~, also try expanded version
652
+ if file_path.startswith('~'):
653
+ expanded = os.path.expanduser(file_path)
654
+ path_variants.add(expanded)
655
+
656
+ # Convert all to forward slashes for consistency
657
+ path_variants = {p.replace('\\', '/') for p in path_variants if p}
658
+
659
+ await ctx.debug(f"Searching with path variants: {list(path_variants)}")
660
+
616
661
  # Search for file mentions in metadata
617
662
  collections_response = await self.qdrant_client.get_collections()
618
663
  collections = collections_response.collections
619
-
620
- # Define async function to search a single collection
664
+
665
+ # Define async function to search a single collection using scroll
621
666
  async def search_collection(collection_name: str):
622
667
  try:
623
- # Search by payload filter
624
- search_results = await self.qdrant_client.search(
668
+ from qdrant_client import models
669
+
670
+ # Use scroll with proper filter for metadata-only search
671
+ results, _ = await self.qdrant_client.scroll(
625
672
  collection_name=collection_name,
626
- query_vector=[0] * 384, # Dummy vector for metadata search
627
- limit=limit,
628
- query_filter={
629
- "must": [
630
- {
631
- "key": "files_analyzed",
632
- "match": {"any": [normalized_path]}
633
- }
673
+ scroll_filter=models.Filter(
674
+ should=[
675
+ models.FieldCondition(
676
+ key="files_analyzed",
677
+ match=models.MatchValue(value=path_variant)
678
+ )
679
+ for path_variant in path_variants
634
680
  ]
635
- }
681
+ ),
682
+ limit=limit,
683
+ with_payload=True
636
684
  )
637
-
638
- results = []
639
- for result in search_results:
640
- results.append({
641
- 'conversation_id': result.payload.get('conversation_id'),
642
- 'timestamp': result.payload.get('timestamp'),
643
- 'content': result.payload.get('content', ''),
644
- 'files_analyzed': result.payload.get('files_analyzed', []),
645
- 'score': result.score
685
+
686
+ formatted_results = []
687
+ for point in results:
688
+ formatted_results.append({
689
+ 'conversation_id': point.payload.get('conversation_id'),
690
+ 'timestamp': point.payload.get('timestamp'),
691
+ 'content': point.payload.get('content', point.payload.get('text', '')),
692
+ 'files_analyzed': point.payload.get('files_analyzed', []),
693
+ 'score': 1.0 # No score in scroll, use 1.0 for found items
646
694
  })
647
- return results
648
-
695
+ return formatted_results
696
+
649
697
  except Exception as e:
650
698
  await ctx.debug(f"Error searching {collection_name}: {e}")
651
699
  return []
@@ -82,10 +82,17 @@ class TemporalTools:
82
82
 
83
83
  # Filter collections by project
84
84
  if target_project != 'all':
85
+ # Use asyncio.to_thread to avoid blocking the event loop
86
+ import asyncio
85
87
  from qdrant_client import QdrantClient as SyncQdrantClient
86
- sync_client = SyncQdrantClient(url=self.qdrant_url)
87
- resolver = ProjectResolver(sync_client)
88
- project_collections = resolver.find_collections_for_project(target_project)
88
+
89
+ def get_project_collections():
90
+ sync_client = SyncQdrantClient(url=self.qdrant_url)
91
+ resolver = ProjectResolver(sync_client)
92
+ return resolver.find_collections_for_project(target_project)
93
+
94
+ # Run sync client in thread pool to avoid blocking
95
+ project_collections = await asyncio.to_thread(get_project_collections)
89
96
 
90
97
  if not project_collections:
91
98
  normalized_name = self.normalize_project_name(target_project)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-self-reflect",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "description": "Give Claude perfect memory of all your conversations - Installation wizard for Python MCP server",
5
5
  "keywords": [
6
6
  "claude",
@@ -35,6 +35,11 @@
35
35
  },
36
36
  "files": [
37
37
  "installer/*.js",
38
+ "scripts/csr-status",
39
+ "scripts/session_quality_tracker.py",
40
+ "scripts/ast_grep_final_analyzer.py",
41
+ "scripts/ast_grep_unified_registry.py",
42
+ "scripts/update_patterns.py",
38
43
  "mcp-server/src/**/*.py",
39
44
  "mcp-server/pyproject.toml",
40
45
  "mcp-server/run-mcp.sh",
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ FINAL AST-GREP Analyzer with Unified Registry
4
+ MANDATORY: Uses ast-grep-py + unified pattern registry
5
+ NO regex fallbacks, NO simplifications
6
+ """
7
+
8
+ import ast_grep_py as sg
9
+ from pathlib import Path
10
+ from typing import Dict, List, Any, Optional
11
+ from datetime import datetime
12
+ import json
13
+ import sys
14
+
15
+ # Import the unified registry
16
+ sys.path.append(str(Path(__file__).parent))
17
+ from ast_grep_unified_registry import get_unified_registry
18
+
19
+ class FinalASTGrepAnalyzer:
20
+ """
21
+ Final production-ready AST-GREP analyzer.
22
+ MANDATORY components:
23
+ - ast-grep-py for AST matching
24
+ - Unified pattern registry (custom + catalog)
25
+ - NO regex patterns
26
+ - NO fallbacks
27
+ """
28
+
29
+ def __init__(self):
30
+ """Initialize with unified registry."""
31
+ self.registry = get_unified_registry()
32
+ all_patterns = self.registry.get_all_patterns()
33
+
34
+ print(f"āœ… Loaded unified registry with {len(all_patterns)} patterns")
35
+ print(f" Languages: Python, TypeScript, JavaScript")
36
+ print(f" Good patterns: {len(self.registry.get_good_patterns())}")
37
+ print(f" Bad patterns: {len(self.registry.get_bad_patterns())}")
38
+
39
+ def analyze_file(self, file_path: str) -> Dict[str, Any]:
40
+ """
41
+ Analyze a file using unified AST-GREP patterns.
42
+ Returns detailed quality metrics and pattern matches.
43
+ """
44
+ if not Path(file_path).exists():
45
+ raise FileNotFoundError(f"File not found: {file_path}")
46
+
47
+ # Detect language from file extension
48
+ language = self._detect_language(file_path)
49
+
50
+ with open(file_path, 'r', encoding='utf-8') as f:
51
+ content = f.read()
52
+
53
+ # Create SgRoot for the detected language
54
+ sg_language = self._get_sg_language(language)
55
+ root = sg.SgRoot(content, sg_language)
56
+ node = root.root()
57
+
58
+ # Get patterns for this language
59
+ language_patterns = self.registry.get_patterns_by_language(language)
60
+
61
+ # Track all matches
62
+ all_matches = []
63
+ pattern_errors = []
64
+ matches_by_category = {}
65
+
66
+ # Process each pattern
67
+ for pattern_def in language_patterns:
68
+ try:
69
+ pattern_str = pattern_def.get("pattern", "")
70
+ if not pattern_str:
71
+ continue
72
+
73
+ # Find matches using ast-grep-py
74
+ matches = node.find_all(pattern=pattern_str)
75
+
76
+ if matches:
77
+ category = pattern_def.get('category', 'unknown')
78
+ if category not in matches_by_category:
79
+ matches_by_category[category] = []
80
+
81
+ match_info = {
82
+ 'category': category,
83
+ 'id': pattern_def['id'],
84
+ 'description': pattern_def.get('description', ''),
85
+ 'quality': pattern_def.get('quality', 'neutral'),
86
+ 'weight': pattern_def.get('weight', 0),
87
+ 'count': len(matches),
88
+ 'locations': [
89
+ {
90
+ 'line': m.range().start.line + 1,
91
+ 'column': m.range().start.column,
92
+ 'text': m.text()[:80]
93
+ } for m in matches[:5] # First 5 examples
94
+ ]
95
+ }
96
+
97
+ matches_by_category[category].append(match_info)
98
+ all_matches.append(match_info)
99
+
100
+ except Exception as e:
101
+ # Record all pattern errors for debugging
102
+ pattern_errors.append({
103
+ 'pattern_id': pattern_def.get('id', '<unknown>'),
104
+ 'category': pattern_def.get('category', 'unknown'),
105
+ 'error': str(e)[:200]
106
+ })
107
+
108
+ # Calculate quality score
109
+ quality_score = self.registry.calculate_quality_score(all_matches)
110
+
111
+ # Count good vs bad patterns
112
+ good_matches = [m for m in all_matches if m['quality'] == 'good']
113
+ bad_matches = [m for m in all_matches if m['quality'] == 'bad']
114
+
115
+ good_count = sum(m['count'] for m in good_matches)
116
+ bad_count = sum(m['count'] for m in bad_matches)
117
+
118
+ return {
119
+ 'file': file_path,
120
+ 'timestamp': datetime.now().isoformat(),
121
+ 'language': language,
122
+ 'engine': 'ast-grep-py + unified registry',
123
+ 'registry_info': {
124
+ 'total_patterns_available': len(language_patterns),
125
+ 'patterns_matched': len(all_matches),
126
+ 'patterns_errored': len(pattern_errors),
127
+ 'categories_found': list(matches_by_category.keys())
128
+ },
129
+ 'matches_by_category': matches_by_category,
130
+ 'all_matches': all_matches,
131
+ 'errors': pattern_errors[:5], # First 5 errors only
132
+ 'quality_metrics': {
133
+ 'quality_score': round(quality_score, 3),
134
+ 'good_patterns_found': good_count,
135
+ 'bad_patterns_found': bad_count,
136
+ 'unique_patterns_matched': len(all_matches),
137
+ 'total_issues': bad_count,
138
+ 'total_good_practices': good_count
139
+ },
140
+ 'recommendations': self._generate_recommendations(matches_by_category, quality_score)
141
+ }
142
+
143
+ def _detect_language(self, file_path: str) -> str:
144
+ """Detect language from file extension."""
145
+ ext = Path(file_path).suffix.lower()
146
+ lang_map = {
147
+ '.py': 'python',
148
+ '.ts': 'typescript',
149
+ '.tsx': 'tsx',
150
+ '.js': 'javascript',
151
+ '.jsx': 'jsx'
152
+ }
153
+ return lang_map.get(ext, 'python')
154
+
155
+ def _get_sg_language(self, language: str) -> str:
156
+ """Get ast-grep language identifier."""
157
+ # ast-grep-py uses different language identifiers
158
+ sg_map = {
159
+ 'python': 'python',
160
+ 'typescript': 'typescript',
161
+ 'tsx': 'tsx',
162
+ 'javascript': 'javascript',
163
+ 'jsx': 'jsx'
164
+ }
165
+ return sg_map.get(language, 'python')
166
+
167
+ def _generate_recommendations(self, matches: Dict, score: float) -> List[str]:
168
+ """Generate actionable recommendations based on matches."""
169
+ recommendations = []
170
+
171
+ if score < 0.3:
172
+ recommendations.append("šŸ”“ Critical: Code quality needs immediate attention")
173
+ elif score < 0.6:
174
+ recommendations.append("🟔 Warning: Several anti-patterns detected")
175
+ else:
176
+ recommendations.append("🟢 Good: Code follows most best practices")
177
+
178
+ # Check for specific issues
179
+ for category, category_matches in matches.items():
180
+ if 'antipatterns' in category:
181
+ total = sum(m['count'] for m in category_matches)
182
+ if total > 0:
183
+ recommendations.append(f"Fix {total} anti-patterns in {category}")
184
+
185
+ if 'logging' in category:
186
+ prints = sum(m['count'] for m in category_matches if 'print' in m['id'])
187
+ if prints > 0:
188
+ recommendations.append(f"Replace {prints} print statements with logger")
189
+
190
+ if 'error' in category:
191
+ bare = sum(m['count'] for m in category_matches if 'broad' in m['id'] or 'bare' in m['id'])
192
+ if bare > 0:
193
+ recommendations.append(f"Fix {bare} bare except clauses")
194
+
195
+ return recommendations
196
+
197
+ def generate_report(self, result: Dict[str, Any]) -> str:
198
+ """Generate a comprehensive analysis report."""
199
+ report = []
200
+ report.append("# AST-GREP Pattern Analysis Report")
201
+ report.append(f"\n**File**: {result['file']}")
202
+ report.append(f"**Language**: {result['language']}")
203
+ report.append(f"**Timestamp**: {result['timestamp']}")
204
+ report.append(f"**Engine**: {result['engine']}")
205
+
206
+ # Quality overview
207
+ metrics = result['quality_metrics']
208
+ score = metrics['quality_score']
209
+ emoji = "🟢" if score > 0.7 else "🟔" if score > 0.4 else "šŸ”“"
210
+
211
+ report.append("\n## Quality Overview")
212
+ report.append(f"- **Quality Score**: {emoji} {score:.1%}")
213
+ report.append(f"- **Good Practices**: {metrics['good_patterns_found']}")
214
+ report.append(f"- **Issues Found**: {metrics['total_issues']}")
215
+ report.append(f"- **Unique Patterns Matched**: {metrics['unique_patterns_matched']}")
216
+
217
+ # Recommendations
218
+ if result['recommendations']:
219
+ report.append("\n## Recommendations")
220
+ for rec in result['recommendations']:
221
+ report.append(f"- {rec}")
222
+
223
+ # Pattern matches by category
224
+ report.append("\n## Pattern Matches by Category")
225
+ for category, matches in result['matches_by_category'].items():
226
+ if matches:
227
+ total = sum(m['count'] for m in matches)
228
+ report.append(f"\n### {category} ({len(matches)} patterns, {total} matches)")
229
+
230
+ # Sort by count descending
231
+ sorted_matches = sorted(matches, key=lambda x: x['count'], reverse=True)
232
+
233
+ for match in sorted_matches[:5]: # Top 5 per category
234
+ quality_emoji = "āœ…" if match['quality'] == 'good' else "āŒ" if match['quality'] == 'bad' else "⚪"
235
+ report.append(f"- {quality_emoji} **{match['id']}**: {match['count']} instances")
236
+ report.append(f" - {match['description']}")
237
+ if match['locations']:
238
+ loc = match['locations'][0]
239
+ report.append(f" - Example (line {loc['line']}): `{loc['text'][:50]}...`")
240
+
241
+ # Registry info
242
+ report.append("\n## Pattern Registry Statistics")
243
+ info = result['registry_info']
244
+ report.append(f"- **Patterns Available**: {info['total_patterns_available']}")
245
+ report.append(f"- **Patterns Matched**: {info['patterns_matched']}")
246
+ report.append(f"- **Categories Found**: {', '.join(info['categories_found'])}")
247
+
248
+ report.append("\n## Compliance")
249
+ report.append("āœ… Using unified AST-GREP registry (custom + catalog)")
250
+ report.append("āœ… Using ast-grep-py for AST matching")
251
+ report.append("āœ… NO regex patterns or fallbacks")
252
+ report.append("āœ… Production-ready pattern analysis")
253
+
254
+ return '\n'.join(report)
255
+
256
+
257
+ def run_final_analysis():
258
+ """Run final AST-GREP analysis with unified registry."""
259
+ print("šŸš€ FINAL AST-GREP Analysis with Unified Registry")
260
+ print("=" * 60)
261
+
262
+ analyzer = FinalASTGrepAnalyzer()
263
+
264
+ # Analyze server.py
265
+ server_path = "/Users/ramakrishnanannaswamy/projects/claude-self-reflect/mcp-server/src/server.py"
266
+
267
+ print(f"\nAnalyzing: {server_path}")
268
+ print("-" * 40)
269
+
270
+ try:
271
+ result = analyzer.analyze_file(server_path)
272
+
273
+ # Display results
274
+ metrics = result['quality_metrics']
275
+ score = metrics['quality_score']
276
+
277
+ print(f"\nšŸ“Š Analysis Results:")
278
+ print(f" Language: {result['language']}")
279
+ print(f" Quality Score: {score:.1%}")
280
+ print(f" Good Practices: {metrics['good_patterns_found']}")
281
+ print(f" Issues: {metrics['total_issues']}")
282
+ print(f" Patterns Matched: {metrics['unique_patterns_matched']}")
283
+
284
+ print(f"\nšŸ’” Recommendations:")
285
+ for rec in result['recommendations']:
286
+ print(f" {rec}")
287
+
288
+ # Top issues
289
+ bad_patterns = [m for m in result['all_matches'] if m['quality'] == 'bad']
290
+ if bad_patterns:
291
+ print(f"\nāš ļø Top Issues to Fix:")
292
+ sorted_bad = sorted(bad_patterns, key=lambda x: x['count'] * abs(x['weight']), reverse=True)
293
+ for pattern in sorted_bad[:5]:
294
+ print(f" - {pattern['id']}: {pattern['count']} instances")
295
+ print(f" {pattern['description']}")
296
+
297
+ # Generate and save report
298
+ report = analyzer.generate_report(result)
299
+ report_path = "/Users/ramakrishnanannaswamy/projects/claude-self-reflect/scripts/final_analysis_report.md"
300
+ with open(report_path, 'w') as f:
301
+ f.write(report)
302
+
303
+ print(f"\nšŸ“ Full report saved to: {report_path}")
304
+
305
+ # Save JSON results
306
+ json_path = "/Users/ramakrishnanannaswamy/projects/claude-self-reflect/scripts/final_analysis_result.json"
307
+ with open(json_path, 'w') as f:
308
+ json.dump(result, f, indent=2)
309
+
310
+ print(f"šŸ“Š JSON results saved to: {json_path}")
311
+
312
+ print("\nāœ… Final AST-GREP analysis complete!")
313
+ print(" - Unified registry with 41 patterns")
314
+ print(" - Support for Python, TypeScript, JavaScript")
315
+ print(" - Ready for production integration")
316
+
317
+ return result
318
+
319
+ except Exception as e:
320
+ print(f"\nāŒ Analysis failed: {e}")
321
+ raise
322
+
323
+
324
+ if __name__ == "__main__":
325
+ run_final_analysis()