crackerjack 0.30.3__py3-none-any.whl → 0.31.7__py3-none-any.whl

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.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (156) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +227 -299
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +170 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +657 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +409 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +618 -928
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +585 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +826 -0
  40. crackerjack/dynamic_config.py +94 -103
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +433 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +443 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +114 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +621 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +372 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +217 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +565 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/coverage_improvement.py +223 -0
  107. crackerjack/orchestration/execution_strategies.py +341 -0
  108. crackerjack/orchestration/test_progress_streamer.py +636 -0
  109. crackerjack/plugins/__init__.py +15 -0
  110. crackerjack/plugins/base.py +200 -0
  111. crackerjack/plugins/hooks.py +246 -0
  112. crackerjack/plugins/loader.py +335 -0
  113. crackerjack/plugins/managers.py +259 -0
  114. crackerjack/py313.py +8 -3
  115. crackerjack/services/__init__.py +22 -0
  116. crackerjack/services/cache.py +314 -0
  117. crackerjack/services/config.py +358 -0
  118. crackerjack/services/config_integrity.py +99 -0
  119. crackerjack/services/contextual_ai_assistant.py +516 -0
  120. crackerjack/services/coverage_ratchet.py +356 -0
  121. crackerjack/services/debug.py +736 -0
  122. crackerjack/services/dependency_monitor.py +617 -0
  123. crackerjack/services/enhanced_filesystem.py +439 -0
  124. crackerjack/services/file_hasher.py +151 -0
  125. crackerjack/services/filesystem.py +421 -0
  126. crackerjack/services/git.py +176 -0
  127. crackerjack/services/health_metrics.py +611 -0
  128. crackerjack/services/initialization.py +873 -0
  129. crackerjack/services/log_manager.py +286 -0
  130. crackerjack/services/logging.py +174 -0
  131. crackerjack/services/metrics.py +578 -0
  132. crackerjack/services/pattern_cache.py +362 -0
  133. crackerjack/services/pattern_detector.py +515 -0
  134. crackerjack/services/performance_benchmarks.py +653 -0
  135. crackerjack/services/security.py +163 -0
  136. crackerjack/services/server_manager.py +234 -0
  137. crackerjack/services/smart_scheduling.py +144 -0
  138. crackerjack/services/tool_version_service.py +61 -0
  139. crackerjack/services/unified_config.py +437 -0
  140. crackerjack/services/version_checker.py +248 -0
  141. crackerjack/slash_commands/__init__.py +14 -0
  142. crackerjack/slash_commands/init.md +122 -0
  143. crackerjack/slash_commands/run.md +163 -0
  144. crackerjack/slash_commands/status.md +127 -0
  145. crackerjack-0.31.7.dist-info/METADATA +742 -0
  146. crackerjack-0.31.7.dist-info/RECORD +149 -0
  147. crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
  148. crackerjack/.gitignore +0 -34
  149. crackerjack/.libcst.codemod.yaml +0 -18
  150. crackerjack/.pdm.toml +0 -1
  151. crackerjack/crackerjack.py +0 -3805
  152. crackerjack/pyproject.toml +0 -286
  153. crackerjack-0.30.3.dist-info/METADATA +0 -1290
  154. crackerjack-0.30.3.dist-info/RECORD +0 -16
  155. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
  156. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,114 @@
1
+ """Test progress tracking and display functionality.
2
+
3
+ This module handles real-time test execution progress tracking, including collection
4
+ and execution phases. Split from test_manager.py for better separation of concerns.
5
+ """
6
+
7
+ import threading
8
+ import time
9
+ import typing as t
10
+
11
+
12
+ class TestProgress:
13
+ """Tracks test execution progress with thread-safe updates."""
14
+
15
+ def __init__(self) -> None:
16
+ self.total_tests: int = 0
17
+ self.passed: int = 0
18
+ self.failed: int = 0
19
+ self.skipped: int = 0
20
+ self.errors: int = 0
21
+ self.current_test: str = ""
22
+ self.start_time: float = 0
23
+ self.is_complete: bool = False
24
+ self.is_collecting: bool = True
25
+ self.files_discovered: int = 0
26
+ self.collection_status: str = "Starting collection..."
27
+ self._lock = threading.Lock()
28
+ self._seen_files: set[str] = set() # Track seen files to prevent duplicates
29
+
30
+ @property
31
+ def completed(self) -> int:
32
+ """Total completed tests (passed + failed + skipped + errors)."""
33
+ return self.passed + self.failed + self.skipped + self.errors
34
+
35
+ @property
36
+ def elapsed_time(self) -> float:
37
+ """Elapsed time since test start."""
38
+ return time.time() - self.start_time if self.start_time else 0
39
+
40
+ @property
41
+ def eta_seconds(self) -> float | None:
42
+ """Estimated time to completion based on current progress rate."""
43
+ if self.completed <= 0 or self.total_tests <= 0:
44
+ return None
45
+ progress_rate = (
46
+ self.completed / self.elapsed_time if self.elapsed_time > 0 else 0
47
+ )
48
+ remaining = self.total_tests - self.completed
49
+ return remaining / progress_rate if progress_rate > 0 else None
50
+
51
+ def update(self, **kwargs: t.Any) -> None:
52
+ """Thread-safe update of progress attributes."""
53
+ with self._lock:
54
+ for key, value in kwargs.items():
55
+ if hasattr(self, key):
56
+ setattr(self, key, value)
57
+
58
+ def format_progress(self) -> str:
59
+ """Format progress display for Rich output."""
60
+ if self.is_collecting:
61
+ return self._format_collection_progress()
62
+ return self._format_execution_progress()
63
+
64
+ def _format_collection_progress(self) -> str:
65
+ """Format test collection progress display."""
66
+ status_parts = [self.collection_status]
67
+
68
+ if self.files_discovered > 0:
69
+ status_parts.append(f"{self.files_discovered} test files")
70
+
71
+ elapsed = self.elapsed_time
72
+ if elapsed > 1:
73
+ status_parts.append(f"{elapsed:.1f}s")
74
+
75
+ return " | ".join(status_parts)
76
+
77
+ def _format_execution_progress(self) -> str:
78
+ """Format test execution progress display."""
79
+ parts = []
80
+
81
+ # Test progress
82
+ if self.total_tests > 0:
83
+ progress_pct = (self.completed / self.total_tests) * 100
84
+ parts.append(f"{self.completed}/{self.total_tests} ({progress_pct:.1f}%)")
85
+
86
+ # Status counts
87
+ status_parts = []
88
+ if self.passed > 0:
89
+ status_parts.append(f"✅ {self.passed}")
90
+ if self.failed > 0:
91
+ status_parts.append(f"❌ {self.failed}")
92
+ if self.skipped > 0:
93
+ status_parts.append(f"⏭ {self.skipped}")
94
+ if self.errors > 0:
95
+ status_parts.append(f"💥 {self.errors}")
96
+
97
+ if status_parts:
98
+ parts.append(" ".join(status_parts))
99
+
100
+ # Current test (truncated)
101
+ if self.current_test and not self.is_complete:
102
+ test_name = (
103
+ self.current_test[:30] + "..."
104
+ if len(self.current_test) > 30
105
+ else self.current_test
106
+ )
107
+ parts.append(f"Running: {test_name}")
108
+
109
+ # Timing
110
+ elapsed = self.elapsed_time
111
+ if elapsed > 1:
112
+ parts.append(f"{elapsed:.1f}s")
113
+
114
+ return " | ".join(parts)
File without changes
@@ -0,0 +1,336 @@
1
+ import asyncio
2
+ import json
3
+ import time
4
+ import typing as t
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass
10
+ class ErrorPattern:
11
+ pattern_id: str
12
+ error_type: str
13
+ error_code: str
14
+ message_pattern: str
15
+ file_pattern: str | None = None
16
+ common_fixes: list[str] | None = None
17
+ auto_fixable: bool = False
18
+ frequency: int = 1
19
+ last_seen: float | None = None
20
+
21
+ def __post_init__(self) -> None:
22
+ if self.common_fixes is None:
23
+ self.common_fixes = []
24
+ if self.last_seen is None:
25
+ self.last_seen = time.time()
26
+
27
+ def to_dict(self) -> dict[str, t.Any]:
28
+ return asdict(self)
29
+
30
+
31
+ @dataclass
32
+ class FixResult:
33
+ fix_id: str
34
+ pattern_id: str
35
+ success: bool
36
+ files_affected: list[str]
37
+ time_taken: float
38
+ error_message: str | None = None
39
+
40
+ def to_dict(self) -> dict[str, t.Any]:
41
+ return asdict(self)
42
+
43
+
44
+ class ErrorCache:
45
+ def __init__(self, cache_dir: Path | None = None) -> None:
46
+ self._lock = asyncio.Lock()
47
+ self.cache_dir = cache_dir or Path.home() / ".cache" / "crackerjack-mcp"
48
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
49
+ self.patterns_file = self.cache_dir / "error_patterns.json"
50
+ self.fixes_file = self.cache_dir / "fix_results.json"
51
+ self.patterns: dict[str, ErrorPattern] = {}
52
+ self.fix_results: list[FixResult] = []
53
+ self._load_cache()
54
+
55
+ async def add_pattern(self, pattern: ErrorPattern) -> None:
56
+ async with self._lock:
57
+ existing = self.patterns.get(pattern.pattern_id)
58
+ if existing:
59
+ self._update_existing_pattern(existing, pattern)
60
+ else:
61
+ self.patterns[pattern.pattern_id] = pattern
62
+ self._save_patterns()
63
+
64
+ def _update_existing_pattern(
65
+ self,
66
+ existing: ErrorPattern,
67
+ pattern: ErrorPattern,
68
+ ) -> None:
69
+ existing.frequency += 1
70
+ existing.last_seen = time.time()
71
+ if pattern.common_fixes:
72
+ self._merge_fixes(existing, pattern.common_fixes)
73
+
74
+ def _merge_fixes(self, existing: ErrorPattern, new_fixes: list[str]) -> None:
75
+ for fix in new_fixes:
76
+ if fix not in (existing.common_fixes or []):
77
+ if existing.common_fixes is None:
78
+ existing.common_fixes = []
79
+ existing.common_fixes.append(fix)
80
+
81
+ def get_pattern(self, pattern_id: str) -> ErrorPattern | None:
82
+ return self.patterns.get(pattern_id)
83
+
84
+ def find_patterns_by_type(self, error_type: str) -> list[ErrorPattern]:
85
+ return [
86
+ pattern
87
+ for pattern in self.patterns.values()
88
+ if pattern.error_type == error_type
89
+ ]
90
+
91
+ def find_patterns_by_code(self, error_code: str) -> list[ErrorPattern]:
92
+ return [
93
+ pattern
94
+ for pattern in self.patterns.values()
95
+ if pattern.error_code == error_code
96
+ ]
97
+
98
+ def get_common_patterns(self, limit: int = 20) -> list[ErrorPattern]:
99
+ patterns = list(self.patterns.values())
100
+ patterns.sort(key=lambda p: p.frequency, reverse=True)
101
+ return patterns[:limit]
102
+
103
+ def get_auto_fixable_patterns(self) -> list[ErrorPattern]:
104
+ return [pattern for pattern in self.patterns.values() if pattern.auto_fixable]
105
+
106
+ async def add_fix_result(self, result: FixResult) -> None:
107
+ async with self._lock:
108
+ self.fix_results.append(result)
109
+ pattern = self.patterns.get(result.pattern_id)
110
+ if pattern and result.success:
111
+ pattern.auto_fixable = True
112
+ fix_command = f"Auto-fix applied for {result.pattern_id}"
113
+ if pattern.common_fixes is None:
114
+ pattern.common_fixes = []
115
+ if fix_command not in pattern.common_fixes:
116
+ pattern.common_fixes.append(fix_command)
117
+ self._save_fixes()
118
+ self._save_patterns()
119
+
120
+ def get_fix_success_rate(self, pattern_id: str) -> float:
121
+ pattern_fixes = [
122
+ result for result in self.fix_results if result.pattern_id == pattern_id
123
+ ]
124
+ if not pattern_fixes:
125
+ return 0.0
126
+ successful = sum(1 for result in pattern_fixes if result.success)
127
+ return successful / len(pattern_fixes)
128
+
129
+ def get_recent_patterns(self, hours: int = 24) -> list[ErrorPattern]:
130
+ cutoff_time = time.time() - (hours * 3600)
131
+
132
+ return [
133
+ pattern
134
+ for pattern in self.patterns.values()
135
+ if (pattern.last_seen or 0) >= cutoff_time
136
+ ]
137
+
138
+ def create_pattern_from_error(
139
+ self,
140
+ error_output: str,
141
+ error_type: str,
142
+ ) -> ErrorPattern | None:
143
+ try:
144
+ lines = error_output.split("\n")
145
+ for line in lines:
146
+ line = line.strip()
147
+ if not self._is_valid_error_line(line):
148
+ continue
149
+ error_code, message_pattern = self._extract_error_info(line, error_type)
150
+ if self._is_meaningful_pattern(error_code, message_pattern):
151
+ return self._create_error_pattern(
152
+ error_type,
153
+ error_code,
154
+ message_pattern,
155
+ )
156
+
157
+ return None
158
+ except Exception:
159
+ return None
160
+
161
+ def _is_valid_error_line(self, line: str) -> bool:
162
+ return bool(line and any(char.isalpha() for char in line))
163
+
164
+ def _extract_error_info(self, line: str, error_type: str) -> tuple[str, str]:
165
+ if error_type == "ruff":
166
+ return self._extract_ruff_info(line)
167
+ if error_type == "pyright":
168
+ return self._extract_pyright_info(line)
169
+ if error_type == "bandit":
170
+ return self._extract_bandit_info(line)
171
+ return "", line
172
+
173
+ def _extract_ruff_info(self, line: str) -> tuple[str, str]:
174
+ error_code = ""
175
+ message_pattern = line
176
+ if ": " in line and any(c.isdigit() for c in line):
177
+ parts = line.split(": ")
178
+ if len(parts) >= 4:
179
+ code_msg = parts[-1].strip()
180
+ if " " in code_msg:
181
+ code_part, msg_part = code_msg.split(" ", 1)
182
+ if code_part.isupper() or code_part[0].isupper():
183
+ error_code = code_part
184
+ message_pattern = msg_part
185
+
186
+ return error_code, message_pattern
187
+
188
+ def _extract_pyright_info(self, line: str) -> tuple[str, str]:
189
+ error_code = ""
190
+ message_pattern = line
191
+ if " - error: " in line:
192
+ parts = line.split(" - error: ")
193
+ if len(parts) >= 2:
194
+ message_pattern = parts[1].strip()
195
+ if "(" in message_pattern and ")" in message_pattern:
196
+ error_code = message_pattern.split("(")[-1].split(")")[0]
197
+
198
+ return error_code, message_pattern
199
+
200
+ def _extract_bandit_info(self, line: str) -> tuple[str, str]:
201
+ error_code = ""
202
+ message_pattern = line
203
+ if "Issue: " in line:
204
+ message_pattern = line.split("Issue: ")[-1].strip()
205
+ if "Test: " in message_pattern:
206
+ parts = message_pattern.split("Test: ")
207
+ message_pattern = parts[0].strip()
208
+ error_code = parts[1].strip() if len(parts) > 1 else ""
209
+
210
+ return error_code, message_pattern
211
+
212
+ def _is_meaningful_pattern(self, error_code: str, message_pattern: str) -> bool:
213
+ return bool(error_code) or len(message_pattern) > 10
214
+
215
+ def _create_error_pattern(
216
+ self,
217
+ error_type: str,
218
+ error_code: str,
219
+ message_pattern: str,
220
+ ) -> ErrorPattern:
221
+ pattern_id = f"{error_type}_{error_code}_{hash(message_pattern) % 10000}"
222
+
223
+ return ErrorPattern(
224
+ pattern_id=pattern_id,
225
+ error_type=error_type,
226
+ error_code=error_code,
227
+ message_pattern=message_pattern,
228
+ auto_fixable=error_type == "ruff",
229
+ )
230
+
231
+ def analyze_output_for_patterns(
232
+ self,
233
+ output: str,
234
+ error_type: str,
235
+ ) -> list[ErrorPattern]:
236
+ patterns: list[ErrorPattern] = []
237
+ sections = output.split("\n\n")
238
+ for section in sections:
239
+ if section.strip():
240
+ pattern = self.create_pattern_from_error(section, error_type)
241
+ if pattern:
242
+ self.add_pattern(pattern)
243
+ patterns.append(pattern)
244
+
245
+ return patterns
246
+
247
+ def get_cache_stats(self) -> dict[str, t.Any]:
248
+ total_patterns = len(self.patterns)
249
+ auto_fixable = len(self.get_auto_fixable_patterns())
250
+ total_fixes = len(self.fix_results)
251
+ successful_fixes = sum(1 for result in self.fix_results if result.success)
252
+ frequencies = [pattern.frequency for pattern in self.patterns.values()]
253
+ avg_frequency = sum(frequencies) / len(frequencies) if frequencies else 0
254
+ type_counts = {}
255
+ for pattern in self.patterns.values():
256
+ type_counts[pattern.error_type] = type_counts.get(pattern.error_type, 0) + 1
257
+
258
+ return {
259
+ "total_patterns": total_patterns,
260
+ "auto_fixable_patterns": auto_fixable,
261
+ "auto_fixable_rate": (auto_fixable / total_patterns) * 100
262
+ if total_patterns
263
+ else 0,
264
+ "total_fix_attempts": total_fixes,
265
+ "successful_fixes": successful_fixes,
266
+ "fix_success_rate": (successful_fixes / total_fixes) * 100
267
+ if total_fixes
268
+ else 0,
269
+ "average_pattern_frequency": avg_frequency,
270
+ "error_types": type_counts,
271
+ }
272
+
273
+ def cleanup_old_patterns(self, days: int = 30) -> int:
274
+ cutoff_time = time.time() - (days * 24 * 3600)
275
+ old_patterns = [
276
+ pattern_id
277
+ for pattern_id, pattern in self.patterns.items()
278
+ if (pattern.last_seen or 0) < cutoff_time
279
+ ]
280
+ for pattern_id in old_patterns:
281
+ del self.patterns[pattern_id]
282
+ if old_patterns:
283
+ self._save_patterns()
284
+
285
+ return len(old_patterns)
286
+
287
+ def export_patterns(self, file_path: Path) -> None:
288
+ export_data = {
289
+ "export_time": time.time(),
290
+ "total_patterns": len(self.patterns),
291
+ "patterns": [pattern.to_dict() for pattern in self.patterns.values()],
292
+ "fix_results": [result.to_dict() for result in self.fix_results],
293
+ "stats": self.get_cache_stats(),
294
+ }
295
+ with file_path.open("w") as f:
296
+ json.dump(export_data, f, indent=2)
297
+
298
+ def _load_cache(self) -> None:
299
+ if self.patterns_file.exists():
300
+ try:
301
+ with self.patterns_file.open("r") as f:
302
+ patterns_data = json.load(f)
303
+ self.patterns = {
304
+ pid: ErrorPattern(**data) for pid, data in patterns_data.items()
305
+ }
306
+ except Exception:
307
+ self.patterns = {}
308
+ if self.fixes_file.exists():
309
+ try:
310
+ with self.fixes_file.open("r") as f:
311
+ fixes_data = json.load(f)
312
+ self.fix_results = [FixResult(**data) for data in fixes_data]
313
+ except Exception:
314
+ self.fix_results = []
315
+
316
+ def _save_patterns(self) -> None:
317
+ try:
318
+ patterns_data = {
319
+ pid: pattern.to_dict() for pid, pattern in self.patterns.items()
320
+ }
321
+ with self.patterns_file.open("w") as f:
322
+ json.dump(patterns_data, f, indent=2)
323
+ except (OSError, json.JSONEncodeError):
324
+ pass
325
+ except Exception:
326
+ pass
327
+
328
+ def _save_fixes(self) -> None:
329
+ try:
330
+ fixes_data = [result.to_dict() for result in self.fix_results]
331
+ with self.fixes_file.open("w") as f:
332
+ json.dump(fixes_data, f, indent=2)
333
+ except (OSError, json.JSONEncodeError):
334
+ pass
335
+ except Exception:
336
+ pass
@@ -0,0 +1,104 @@
1
+ import asyncio
2
+ import socket
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from rich.console import Console
8
+
9
+ from mcp import stdio_client
10
+
11
+ from .progress_monitor import (
12
+ run_crackerjack_with_enhanced_progress as run_crackerjack_with_progress,
13
+ )
14
+
15
+
16
+ def is_mcp_server_running(host: str = "localhost", port: int = 5173) -> bool:
17
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
18
+ try:
19
+ result = sock.connect_ex((host, port))
20
+ return result == 0
21
+ finally:
22
+ sock.close()
23
+
24
+
25
+ async def ensure_mcp_server_running() -> subprocess.Popen | None:
26
+ console = Console()
27
+
28
+ if is_mcp_server_running():
29
+ console.print("[green]✅ MCP server already running[/green]")
30
+ return None
31
+
32
+ console.print("[yellow]🚀 Starting MCP server...[/yellow]")
33
+ server_process = subprocess.Popen(
34
+ [sys.executable, "-m", "crackerjack", "--start-mcp-server"],
35
+ stdout=subprocess.PIPE,
36
+ stderr=subprocess.PIPE,
37
+ start_new_session=True,
38
+ )
39
+
40
+ for _i in range(20):
41
+ if is_mcp_server_running():
42
+ console.print("[green]✅ MCP server started successfully[/green]")
43
+ return server_process
44
+ await asyncio.sleep(0.5)
45
+
46
+ console.print("[red]❌ Failed to start MCP server[/red]")
47
+ server_process.terminate()
48
+ msg = "Failed to start MCP server within timeout period"
49
+ raise RuntimeError(msg)
50
+
51
+
52
+ async def run_with_mcp_server(command: str = "/crackerjack:run") -> None:
53
+ console = Console()
54
+
55
+ server_process = await ensure_mcp_server_running()
56
+
57
+ try:
58
+ server_script = Path(__file__).parent.parent / "__main__.py"
59
+ async with (
60
+ stdio_client(
61
+ sys.executable,
62
+ str(server_script),
63
+ "--start-mcp-server",
64
+ ) as (read_stream, write_stream),
65
+ read_stream.session(
66
+ read_stream=read_stream,
67
+ write_stream=write_stream,
68
+ ) as session,
69
+ ):
70
+ try:
71
+ await run_crackerjack_with_progress(session, command)
72
+ except Exception as e:
73
+ console.print(f"[bold red]Error: {e}[/bold red]")
74
+ sys.exit(1)
75
+ finally:
76
+ if server_process:
77
+ console.print(
78
+ "[yellow]Note: MCP server continues running in background[/yellow]",
79
+ )
80
+
81
+
82
+ def main() -> None:
83
+ import argparse
84
+
85
+ parser = argparse.ArgumentParser(
86
+ description="Run Crackerjack commands through MCP with progress monitoring",
87
+ )
88
+ parser.add_argument(
89
+ "command",
90
+ nargs="?",
91
+ default="/crackerjack:run",
92
+ help="Command to execute (default: /crackerjack:run)",
93
+ )
94
+ args = parser.parse_args()
95
+
96
+ try:
97
+ asyncio.run(run_with_mcp_server(args.command))
98
+ except KeyboardInterrupt:
99
+ Console().print("\n[yellow]Operation cancelled by user[/yellow]")
100
+ sys.exit(1)
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()