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.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +227 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +170 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +657 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +409 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +585 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +826 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +433 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +443 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +114 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +621 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +372 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +217 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +565 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/coverage_improvement.py +223 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +358 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +356 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +421 -0
- crackerjack/services/git.py +176 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +873 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.7.dist-info/METADATA +742 -0
- crackerjack-0.31.7.dist-info/RECORD +149 -0
- crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
- {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
|
crackerjack/mcp/cache.py
ADDED
|
@@ -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()
|