claude-self-reflect 3.3.0 → 4.0.0
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 +525 -11
- package/.claude/agents/quality-fixer.md +314 -0
- package/.claude/agents/reflection-specialist.md +40 -1
- 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 +45 -7
- package/mcp-server/src/code_reload_tool.py +271 -0
- package/mcp-server/src/embedding_manager.py +60 -26
- package/mcp-server/src/enhanced_tool_registry.py +407 -0
- package/mcp-server/src/mode_switch_tool.py +181 -0
- package/mcp-server/src/parallel_search.py +24 -85
- package/mcp-server/src/project_resolver.py +20 -2
- package/mcp-server/src/reflection_tools.py +60 -13
- package/mcp-server/src/rich_formatting.py +103 -0
- package/mcp-server/src/search_tools.py +180 -79
- package/mcp-server/src/security_patches.py +555 -0
- package/mcp-server/src/server.py +318 -240
- package/mcp-server/src/status.py +13 -8
- package/mcp-server/src/temporal_tools.py +10 -3
- package/mcp-server/src/test_quality.py +153 -0
- package/package.json +6 -1
- package/scripts/ast_grep_final_analyzer.py +328 -0
- package/scripts/ast_grep_unified_registry.py +710 -0
- package/scripts/csr-status +511 -0
- package/scripts/import-conversations-unified.py +114 -28
- package/scripts/session_quality_tracker.py +661 -0
- package/scripts/streaming-watcher.py +140 -5
- package/scripts/update_patterns.py +334 -0
|
@@ -484,7 +484,20 @@ class QdrantService:
|
|
|
484
484
|
|
|
485
485
|
def __init__(self, config: Config, embedding_provider: EmbeddingProvider):
|
|
486
486
|
self.config = config
|
|
487
|
-
|
|
487
|
+
|
|
488
|
+
# Security: Validate Qdrant URL for remote connections
|
|
489
|
+
from urllib.parse import urlparse
|
|
490
|
+
parsed = urlparse(config.qdrant_url)
|
|
491
|
+
host = (parsed.hostname or "").lower()
|
|
492
|
+
|
|
493
|
+
if config.require_tls_for_remote and host not in ("localhost", "127.0.0.1", "qdrant") and parsed.scheme != "https":
|
|
494
|
+
raise ValueError(f"Insecure QDRANT_URL for remote host: {config.qdrant_url} (use https:// or set QDRANT_REQUIRE_TLS_FOR_REMOTE=false)")
|
|
495
|
+
|
|
496
|
+
# Initialize with API key if provided
|
|
497
|
+
self.client = AsyncQdrantClient(
|
|
498
|
+
url=config.qdrant_url,
|
|
499
|
+
api_key=config.qdrant_api_key if hasattr(config, 'qdrant_api_key') else None
|
|
500
|
+
)
|
|
488
501
|
self.embedding_provider = embedding_provider
|
|
489
502
|
self._collection_cache: Dict[str, float] = {}
|
|
490
503
|
self.request_semaphore = asyncio.Semaphore(config.max_concurrent_qdrant)
|
|
@@ -979,7 +992,66 @@ class StreamingWatcher:
|
|
|
979
992
|
text_parts.append(item.get('text', ''))
|
|
980
993
|
return ' '.join(text_parts)
|
|
981
994
|
return str(content) if content else ''
|
|
982
|
-
|
|
995
|
+
|
|
996
|
+
def _update_quality_cache(self, pattern_analysis: Dict, avg_score: float, project_name: str = None):
|
|
997
|
+
"""Update quality cache file for statusline display - PER PROJECT."""
|
|
998
|
+
try:
|
|
999
|
+
# Determine project name from current context or use provided
|
|
1000
|
+
if not project_name:
|
|
1001
|
+
# Try to infer from analyzed files
|
|
1002
|
+
files = pattern_analysis.get('files', [])
|
|
1003
|
+
if files and len(files) > 0:
|
|
1004
|
+
# Extract project from first file path
|
|
1005
|
+
first_file = Path(files[0])
|
|
1006
|
+
# Walk up to find .git directory or use immediate parent
|
|
1007
|
+
for parent in first_file.parents:
|
|
1008
|
+
if (parent / '.git').exists():
|
|
1009
|
+
project_name = parent.name
|
|
1010
|
+
break
|
|
1011
|
+
else:
|
|
1012
|
+
project_name = 'unknown'
|
|
1013
|
+
else:
|
|
1014
|
+
project_name = 'unknown'
|
|
1015
|
+
|
|
1016
|
+
# Sanitize project name for filename
|
|
1017
|
+
safe_project_name = project_name.replace('/', '-').replace(' ', '_')
|
|
1018
|
+
cache_dir = Path.home() / ".claude-self-reflect" / "quality_cache"
|
|
1019
|
+
cache_dir.mkdir(exist_ok=True, parents=True)
|
|
1020
|
+
cache_file = cache_dir / f"{safe_project_name}.json"
|
|
1021
|
+
|
|
1022
|
+
# Calculate total issues from pattern analysis
|
|
1023
|
+
total_issues = sum(p.get('count', 0) for p in pattern_analysis.get('issues', []))
|
|
1024
|
+
|
|
1025
|
+
# Determine grade based on score
|
|
1026
|
+
if avg_score >= 0.95:
|
|
1027
|
+
grade = 'A+' if total_issues < 10 else 'A'
|
|
1028
|
+
elif avg_score >= 0.8:
|
|
1029
|
+
grade = 'B'
|
|
1030
|
+
elif avg_score >= 0.6:
|
|
1031
|
+
grade = 'C'
|
|
1032
|
+
else:
|
|
1033
|
+
grade = 'D'
|
|
1034
|
+
|
|
1035
|
+
cache_data = {
|
|
1036
|
+
'status': 'success',
|
|
1037
|
+
'session_id': 'watcher',
|
|
1038
|
+
'timestamp': datetime.now().isoformat(),
|
|
1039
|
+
'summary': {
|
|
1040
|
+
'files_analyzed': len(pattern_analysis.get('files', [])),
|
|
1041
|
+
'avg_quality_score': round(avg_score, 3),
|
|
1042
|
+
'total_issues': total_issues,
|
|
1043
|
+
'quality_grade': grade
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
with open(cache_file, 'w') as f:
|
|
1048
|
+
json.dump(cache_data, f, indent=2)
|
|
1049
|
+
|
|
1050
|
+
logger.debug(f"Updated quality cache: {grade}/{total_issues}")
|
|
1051
|
+
|
|
1052
|
+
except Exception as e:
|
|
1053
|
+
logger.debug(f"Failed to update quality cache: {e}")
|
|
1054
|
+
|
|
983
1055
|
async def process_file(self, file_path: Path) -> bool:
|
|
984
1056
|
"""Process a single file."""
|
|
985
1057
|
try:
|
|
@@ -1034,7 +1106,70 @@ class StreamingWatcher:
|
|
|
1034
1106
|
|
|
1035
1107
|
# Extract metadata
|
|
1036
1108
|
tool_usage = extract_tool_usage_from_conversation(all_messages)
|
|
1037
|
-
|
|
1109
|
+
|
|
1110
|
+
# MANDATORY AST-GREP Analysis for HOT files
|
|
1111
|
+
pattern_analysis = {}
|
|
1112
|
+
avg_quality_score = 0.0
|
|
1113
|
+
freshness_level, _ = self.categorize_freshness(file_path)
|
|
1114
|
+
|
|
1115
|
+
# Analyze code quality for HOT files (current session)
|
|
1116
|
+
if freshness_level == FreshnessLevel.HOT and (tool_usage.get('files_edited') or tool_usage.get('files_analyzed')):
|
|
1117
|
+
try:
|
|
1118
|
+
# Import analyzer (lazy import to avoid startup overhead)
|
|
1119
|
+
from ast_grep_final_analyzer import FinalASTGrepAnalyzer
|
|
1120
|
+
from update_patterns import check_and_update_patterns
|
|
1121
|
+
|
|
1122
|
+
# Update patterns (24h cache, <100ms)
|
|
1123
|
+
check_and_update_patterns()
|
|
1124
|
+
|
|
1125
|
+
# Create analyzer
|
|
1126
|
+
if not hasattr(self, '_ast_analyzer'):
|
|
1127
|
+
self._ast_analyzer = FinalASTGrepAnalyzer()
|
|
1128
|
+
|
|
1129
|
+
# Analyze edited files from this session
|
|
1130
|
+
files_to_analyze = list(set(
|
|
1131
|
+
tool_usage.get('files_edited', [])[:5] +
|
|
1132
|
+
tool_usage.get('files_analyzed', [])[:5]
|
|
1133
|
+
))
|
|
1134
|
+
|
|
1135
|
+
quality_scores = []
|
|
1136
|
+
for file_ref in files_to_analyze:
|
|
1137
|
+
if file_ref and any(file_ref.endswith(ext) for ext in ['.py', '.ts', '.js', '.tsx', '.jsx']):
|
|
1138
|
+
try:
|
|
1139
|
+
if os.path.exists(file_ref):
|
|
1140
|
+
result = self._ast_analyzer.analyze_file(file_ref)
|
|
1141
|
+
metrics = result['quality_metrics']
|
|
1142
|
+
pattern_analysis[file_ref] = {
|
|
1143
|
+
'score': metrics['quality_score'],
|
|
1144
|
+
'good_patterns': metrics['good_patterns_found'],
|
|
1145
|
+
'bad_patterns': metrics['bad_patterns_found'],
|
|
1146
|
+
'issues': metrics['total_issues']
|
|
1147
|
+
}
|
|
1148
|
+
quality_scores.append(metrics['quality_score'])
|
|
1149
|
+
|
|
1150
|
+
# Log quality issues for HOT files
|
|
1151
|
+
if metrics['quality_score'] < 0.6:
|
|
1152
|
+
logger.warning(f"⚠️ Quality issue in {os.path.basename(file_ref)}: {metrics['quality_score']:.1%} ({metrics['total_issues']} issues)")
|
|
1153
|
+
except Exception as e:
|
|
1154
|
+
logger.debug(f"Could not analyze {file_ref}: {e}")
|
|
1155
|
+
|
|
1156
|
+
if quality_scores:
|
|
1157
|
+
avg_quality_score = sum(quality_scores) / len(quality_scores)
|
|
1158
|
+
logger.info(f"📊 Session quality: {avg_quality_score:.1%} for {len(quality_scores)} files")
|
|
1159
|
+
|
|
1160
|
+
# Update quality cache for statusline - watcher handles this automatically!
|
|
1161
|
+
# Pass project name from current file being processed
|
|
1162
|
+
project_name = file_path.parent.name if file_path else None
|
|
1163
|
+
self._update_quality_cache(pattern_analysis, avg_quality_score, project_name)
|
|
1164
|
+
|
|
1165
|
+
except Exception as e:
|
|
1166
|
+
logger.debug(f"AST analysis not available: {e}")
|
|
1167
|
+
|
|
1168
|
+
# Add pattern analysis to tool_usage metadata
|
|
1169
|
+
if pattern_analysis:
|
|
1170
|
+
tool_usage['pattern_analysis'] = pattern_analysis
|
|
1171
|
+
tool_usage['avg_quality_score'] = round(avg_quality_score, 3)
|
|
1172
|
+
|
|
1038
1173
|
# Build text
|
|
1039
1174
|
text_parts = []
|
|
1040
1175
|
for msg in all_messages:
|
|
@@ -1043,7 +1178,7 @@ class StreamingWatcher:
|
|
|
1043
1178
|
text = self._extract_message_text(content)
|
|
1044
1179
|
if text:
|
|
1045
1180
|
text_parts.append(f"{role}: {text}")
|
|
1046
|
-
|
|
1181
|
+
|
|
1047
1182
|
combined_text = "\n\n".join(text_parts)
|
|
1048
1183
|
if not combined_text.strip():
|
|
1049
1184
|
logger.warning(f"No textual content in {file_path}, marking as processed")
|
|
@@ -1057,7 +1192,7 @@ class StreamingWatcher:
|
|
|
1057
1192
|
}
|
|
1058
1193
|
self.stats["files_processed"] += 1
|
|
1059
1194
|
return True
|
|
1060
|
-
|
|
1195
|
+
|
|
1061
1196
|
concepts = extract_concepts(combined_text, tool_usage)
|
|
1062
1197
|
|
|
1063
1198
|
# Now we know we have content, ensure collection exists
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Auto-update AST-GREP patterns from official catalog.
|
|
4
|
+
MANDATORY feature - runs on every import to ensure latest patterns.
|
|
5
|
+
Fast execution: <1 second with caching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import yaml
|
|
11
|
+
import re
|
|
12
|
+
import hashlib
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
from typing import Dict, List, Any, Optional
|
|
16
|
+
import subprocess
|
|
17
|
+
import tempfile
|
|
18
|
+
import shutil
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
logging.basicConfig(level=logging.INFO)
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Configuration
|
|
25
|
+
CACHE_DIR = Path.home() / ".claude-self-reflect" / "cache" / "patterns"
|
|
26
|
+
CACHE_FILE = CACHE_DIR / "pattern_cache.json"
|
|
27
|
+
REGISTRY_FILE = Path(__file__).parent / "unified_registry.json"
|
|
28
|
+
CATALOG_REPO = "https://github.com/ast-grep/ast-grep.github.io.git"
|
|
29
|
+
CATALOG_PATH = "website/catalog"
|
|
30
|
+
CACHE_HOURS = 24 # Check for updates once per day
|
|
31
|
+
|
|
32
|
+
# Ensure cache directory exists
|
|
33
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PatternUpdater:
|
|
37
|
+
"""Updates AST-GREP patterns from official catalog."""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self.patterns = {}
|
|
41
|
+
self.stats = {
|
|
42
|
+
'total_patterns': 0,
|
|
43
|
+
'new_patterns': 0,
|
|
44
|
+
'updated_patterns': 0,
|
|
45
|
+
'languages': set()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def should_update(self) -> bool:
|
|
49
|
+
"""Check if patterns need updating based on cache age."""
|
|
50
|
+
if not CACHE_FILE.exists():
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
with open(CACHE_FILE, 'r') as f:
|
|
55
|
+
cache = json.load(f)
|
|
56
|
+
|
|
57
|
+
cached_time = datetime.fromisoformat(cache.get('timestamp', '2000-01-01'))
|
|
58
|
+
if datetime.now() - cached_time > timedelta(hours=CACHE_HOURS):
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
# Also check if registry file is missing
|
|
62
|
+
if not REGISTRY_FILE.exists():
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
return False
|
|
66
|
+
except:
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
def fetch_catalog_patterns(self) -> Dict[str, List[Dict]]:
|
|
70
|
+
"""Fetch latest patterns from AST-GREP GitHub catalog."""
|
|
71
|
+
patterns_by_lang = {}
|
|
72
|
+
|
|
73
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
74
|
+
repo_path = Path(tmpdir) / "ast-grep-catalog"
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Clone or pull the repository (shallow clone for speed)
|
|
78
|
+
logger.info("Fetching latest AST-GREP patterns from GitHub...")
|
|
79
|
+
# Use shorter timeout to avoid blocking analysis
|
|
80
|
+
timeout = int(os.environ.get("AST_GREP_CATALOG_TIMEOUT", "10"))
|
|
81
|
+
subprocess.run(
|
|
82
|
+
["git", "clone", "--depth", "1", "--single-branch",
|
|
83
|
+
CATALOG_REPO, str(repo_path)],
|
|
84
|
+
check=True,
|
|
85
|
+
capture_output=True,
|
|
86
|
+
timeout=timeout
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
catalog_dir = repo_path / CATALOG_PATH
|
|
90
|
+
|
|
91
|
+
# Process each language directory
|
|
92
|
+
for lang_dir in catalog_dir.iterdir():
|
|
93
|
+
if lang_dir.is_dir() and not lang_dir.name.startswith('.'):
|
|
94
|
+
language = lang_dir.name
|
|
95
|
+
patterns_by_lang[language] = []
|
|
96
|
+
self.stats['languages'].add(language)
|
|
97
|
+
|
|
98
|
+
# Process each pattern file
|
|
99
|
+
for pattern_file in lang_dir.glob("*.md"):
|
|
100
|
+
if pattern_file.name == "index.md":
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
pattern = self._parse_pattern_file(pattern_file, language)
|
|
104
|
+
if pattern:
|
|
105
|
+
patterns_by_lang[language].append(pattern)
|
|
106
|
+
self.stats['total_patterns'] += 1
|
|
107
|
+
|
|
108
|
+
logger.info(f"Fetched {self.stats['total_patterns']} patterns for {len(self.stats['languages'])} languages")
|
|
109
|
+
|
|
110
|
+
except subprocess.TimeoutExpired:
|
|
111
|
+
logger.warning("GitHub fetch timed out, using cached patterns")
|
|
112
|
+
return {}
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.warning(f"Failed to fetch from GitHub: {e}, using cached patterns")
|
|
115
|
+
return {}
|
|
116
|
+
|
|
117
|
+
return patterns_by_lang
|
|
118
|
+
|
|
119
|
+
def _parse_pattern_file(self, file_path: Path, language: str) -> Optional[Dict]:
|
|
120
|
+
"""Parse a single pattern file from the catalog."""
|
|
121
|
+
try:
|
|
122
|
+
content = file_path.read_text()
|
|
123
|
+
|
|
124
|
+
# Extract YAML block
|
|
125
|
+
yaml_match = re.search(r'```yaml\n(.*?)\n```', content, re.DOTALL)
|
|
126
|
+
if not yaml_match:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
yaml_content = yaml_match.group(1)
|
|
130
|
+
pattern_data = yaml.safe_load(yaml_content)
|
|
131
|
+
|
|
132
|
+
# Extract metadata
|
|
133
|
+
title_match = re.search(r'^## (.+?)(?:\s*<Badge.*?>)?$', content, re.MULTILINE)
|
|
134
|
+
title = title_match.group(1).strip() if title_match else file_path.stem
|
|
135
|
+
|
|
136
|
+
# Extract description
|
|
137
|
+
desc_match = re.search(r'### Description\n\n(.+?)(?=\n###|\n```|\Z)', content, re.DOTALL)
|
|
138
|
+
description = desc_match.group(1).strip() if desc_match else ""
|
|
139
|
+
|
|
140
|
+
# Build pattern object
|
|
141
|
+
pattern = {
|
|
142
|
+
'id': pattern_data.get('id', file_path.stem),
|
|
143
|
+
'title': title,
|
|
144
|
+
'description': description,
|
|
145
|
+
'language': pattern_data.get('language', language),
|
|
146
|
+
'file': file_path.name,
|
|
147
|
+
'has_fix': 'fix' in pattern_data
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Extract rule
|
|
151
|
+
if 'rule' in pattern_data:
|
|
152
|
+
rule = pattern_data['rule']
|
|
153
|
+
if isinstance(rule, dict):
|
|
154
|
+
if 'pattern' in rule:
|
|
155
|
+
pattern['pattern'] = rule['pattern']
|
|
156
|
+
if 'any' in rule:
|
|
157
|
+
pattern['patterns'] = rule['any']
|
|
158
|
+
pattern['match_type'] = 'any'
|
|
159
|
+
if 'all' in rule:
|
|
160
|
+
pattern['patterns'] = rule['all']
|
|
161
|
+
pattern['match_type'] = 'all'
|
|
162
|
+
if 'inside' in rule:
|
|
163
|
+
pattern['inside'] = rule['inside']
|
|
164
|
+
|
|
165
|
+
# Add fix if present
|
|
166
|
+
if 'fix' in pattern_data:
|
|
167
|
+
pattern['fix'] = pattern_data['fix']
|
|
168
|
+
|
|
169
|
+
# Determine quality based on type
|
|
170
|
+
pattern['quality'] = self._determine_quality(pattern)
|
|
171
|
+
pattern['weight'] = self._calculate_weight(pattern)
|
|
172
|
+
|
|
173
|
+
return pattern
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.debug(f"Failed to parse {file_path}: {e}")
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
def _determine_quality(self, pattern: Dict) -> str:
|
|
180
|
+
"""Determine pattern quality."""
|
|
181
|
+
if pattern.get('has_fix'):
|
|
182
|
+
return 'good'
|
|
183
|
+
|
|
184
|
+
# Patterns that detect issues are "bad" (they find bad code)
|
|
185
|
+
if any(word in pattern.get('id', '').lower()
|
|
186
|
+
for word in ['no-', 'missing-', 'avoid-', 'deprecated']):
|
|
187
|
+
return 'bad'
|
|
188
|
+
|
|
189
|
+
return 'neutral'
|
|
190
|
+
|
|
191
|
+
def _calculate_weight(self, pattern: Dict) -> int:
|
|
192
|
+
"""Calculate pattern weight for scoring."""
|
|
193
|
+
quality = pattern.get('quality', 'neutral')
|
|
194
|
+
weights = {
|
|
195
|
+
'good': 3,
|
|
196
|
+
'neutral': 1,
|
|
197
|
+
'bad': -3
|
|
198
|
+
}
|
|
199
|
+
return weights.get(quality, 1)
|
|
200
|
+
|
|
201
|
+
def merge_with_custom_patterns(self, catalog_patterns: Dict) -> Dict:
|
|
202
|
+
"""Merge catalog patterns with custom local patterns."""
|
|
203
|
+
# Load existing registry if it exists
|
|
204
|
+
existing_patterns = {}
|
|
205
|
+
if REGISTRY_FILE.exists():
|
|
206
|
+
try:
|
|
207
|
+
with open(REGISTRY_FILE, 'r') as f:
|
|
208
|
+
registry = json.load(f)
|
|
209
|
+
existing_patterns = registry.get('patterns', {})
|
|
210
|
+
except:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
# Keep custom Python patterns (our manual additions)
|
|
214
|
+
custom_categories = [
|
|
215
|
+
'python_async', 'python_error_handling', 'python_logging',
|
|
216
|
+
'python_typing', 'python_antipatterns', 'python_qdrant', 'python_mcp'
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
merged = {}
|
|
220
|
+
for category in custom_categories:
|
|
221
|
+
if category in existing_patterns:
|
|
222
|
+
merged[category] = existing_patterns[category]
|
|
223
|
+
|
|
224
|
+
# Add catalog patterns
|
|
225
|
+
for language, patterns in catalog_patterns.items():
|
|
226
|
+
category_name = f"{language}_catalog"
|
|
227
|
+
merged[category_name] = patterns
|
|
228
|
+
|
|
229
|
+
return merged
|
|
230
|
+
|
|
231
|
+
def save_registry(self, patterns: Dict):
|
|
232
|
+
"""Save updated pattern registry."""
|
|
233
|
+
registry = {
|
|
234
|
+
'source': 'unified-ast-grep-auto-updated',
|
|
235
|
+
'version': '3.0.0',
|
|
236
|
+
'timestamp': datetime.now().isoformat(),
|
|
237
|
+
'patterns': patterns,
|
|
238
|
+
'stats': {
|
|
239
|
+
'total_patterns': sum(len(p) for p in patterns.values()),
|
|
240
|
+
'categories': list(patterns.keys()),
|
|
241
|
+
'languages': list(self.stats['languages']),
|
|
242
|
+
'last_update': datetime.now().isoformat()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
with open(REGISTRY_FILE, 'w') as f:
|
|
247
|
+
json.dump(registry, f, indent=2)
|
|
248
|
+
|
|
249
|
+
logger.info(f"Saved {registry['stats']['total_patterns']} patterns to {REGISTRY_FILE}")
|
|
250
|
+
|
|
251
|
+
def update_cache(self):
|
|
252
|
+
"""Update cache file with timestamp."""
|
|
253
|
+
cache_data = {
|
|
254
|
+
'timestamp': datetime.now().isoformat(),
|
|
255
|
+
'stats': {
|
|
256
|
+
'total_patterns': self.stats['total_patterns'],
|
|
257
|
+
'languages': list(self.stats['languages'])
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
with open(CACHE_FILE, 'w') as f:
|
|
262
|
+
json.dump(cache_data, f)
|
|
263
|
+
|
|
264
|
+
def update_patterns(self, force: bool = False) -> bool:
|
|
265
|
+
"""Main update function - FAST with caching."""
|
|
266
|
+
# Check if update needed (< 10ms)
|
|
267
|
+
if not force and not self.should_update():
|
|
268
|
+
logger.debug("Patterns are up to date (cached)")
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
logger.info("Updating AST-GREP patterns...")
|
|
272
|
+
|
|
273
|
+
# Fetch from GitHub (only when cache expired)
|
|
274
|
+
catalog_patterns = self.fetch_catalog_patterns()
|
|
275
|
+
|
|
276
|
+
if catalog_patterns:
|
|
277
|
+
# Merge with custom patterns
|
|
278
|
+
merged_patterns = self.merge_with_custom_patterns(catalog_patterns)
|
|
279
|
+
|
|
280
|
+
# Save updated registry
|
|
281
|
+
self.save_registry(merged_patterns)
|
|
282
|
+
|
|
283
|
+
# Update cache timestamp
|
|
284
|
+
self.update_cache()
|
|
285
|
+
|
|
286
|
+
logger.info(f"✅ Pattern update complete: {self.stats['total_patterns']} patterns")
|
|
287
|
+
return True
|
|
288
|
+
else:
|
|
289
|
+
logger.info("Using existing patterns (GitHub unavailable)")
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def check_and_update_patterns(force: bool = False) -> bool:
|
|
294
|
+
"""
|
|
295
|
+
Quick pattern update check - MANDATORY but FAST.
|
|
296
|
+
Called on every import, uses 24-hour cache.
|
|
297
|
+
"""
|
|
298
|
+
updater = PatternUpdater()
|
|
299
|
+
return updater.update_patterns(force=force)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def install_time_update():
|
|
303
|
+
"""Run during package installation - forces update."""
|
|
304
|
+
logger.info("Installing AST-GREP patterns...")
|
|
305
|
+
updater = PatternUpdater()
|
|
306
|
+
updater.update_patterns(force=True)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
import sys
|
|
311
|
+
|
|
312
|
+
# Allow --force flag for manual updates
|
|
313
|
+
force = "--force" in sys.argv
|
|
314
|
+
|
|
315
|
+
if force:
|
|
316
|
+
print("Forcing pattern update from GitHub...")
|
|
317
|
+
else:
|
|
318
|
+
print("Checking for pattern updates (24-hour cache)...")
|
|
319
|
+
|
|
320
|
+
success = check_and_update_patterns(force=force)
|
|
321
|
+
|
|
322
|
+
if success:
|
|
323
|
+
print("✅ Patterns updated successfully")
|
|
324
|
+
else:
|
|
325
|
+
print("✅ Patterns are up to date")
|
|
326
|
+
|
|
327
|
+
# Show stats
|
|
328
|
+
if REGISTRY_FILE.exists():
|
|
329
|
+
with open(REGISTRY_FILE, 'r') as f:
|
|
330
|
+
registry = json.load(f)
|
|
331
|
+
stats = registry.get('stats', {})
|
|
332
|
+
print(f" Total patterns: {stats.get('total_patterns', 0)}")
|
|
333
|
+
print(f" Languages: {', '.join(stats.get('languages', []))}")
|
|
334
|
+
print(f" Last update: {stats.get('last_update', 'Unknown')}")
|