crackerjack 0.32.0__py3-none-any.whl → 0.33.1__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/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +64 -6
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +257 -218
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +558 -240
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +66 -13
- crackerjack/managers/test_command_builder.py +5 -17
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +109 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +161 -32
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +174 -33
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +15 -12
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +3 -0
- crackerjack/mixins/error_handling.py +145 -0
- crackerjack/models/config.py +21 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +176 -107
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/models/task.py +3 -0
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +90 -105
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +18 -11
- crackerjack/services/config_merge.py +30 -85
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +41 -17
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +41 -45
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +30 -33
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +409 -0
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +416 -0
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +250 -576
- crackerjack/services/performance_cache.py +382 -0
- crackerjack/services/performance_monitor.py +565 -0
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +605 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +61 -30
- crackerjack/services/security_logger.py +18 -22
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/terminal_utils.py +0 -0
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +197 -26
- crackerjack-0.33.1.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.32.0.dist-info/RECORD +0 -180
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -172,7 +172,7 @@ class TestManagementImpl:
|
|
|
172
172
|
self.console.print("[yellow]⚠️[/ yellow] Coverage ratchet disabled")
|
|
173
173
|
|
|
174
174
|
def get_coverage_ratchet_status(self) -> dict[str, t.Any]:
|
|
175
|
-
return self.coverage_ratchet.
|
|
175
|
+
return self.coverage_ratchet.get_ratchet_data()
|
|
176
176
|
|
|
177
177
|
def _run_test_command(
|
|
178
178
|
self,
|
|
@@ -482,9 +482,8 @@ class TestManagementImpl:
|
|
|
482
482
|
self._handle_running_test(line, progress)
|
|
483
483
|
|
|
484
484
|
def _handle_collection_completion(self, line: str, progress: TestProgress) -> bool:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
): # REGEX OK: parsing pytest collection output
|
|
485
|
+
match = re.search(r"collected (\d+) items?", line)
|
|
486
|
+
if match:
|
|
488
487
|
progress.update(
|
|
489
488
|
total_tests=int(match.group(1)),
|
|
490
489
|
is_collecting=False,
|
|
@@ -728,7 +727,7 @@ class TestManagementImpl:
|
|
|
728
727
|
import os
|
|
729
728
|
|
|
730
729
|
cpu_count = os.cpu_count() or 1
|
|
731
|
-
test_files = list(self.pkg_path.glob("tests/test_*.py"))
|
|
730
|
+
test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
|
|
732
731
|
if len(test_files) < 5:
|
|
733
732
|
return min(2, cpu_count)
|
|
734
733
|
|
|
@@ -737,7 +736,7 @@ class TestManagementImpl:
|
|
|
737
736
|
def _get_test_timeout(self, options: OptionsProtocol) -> int:
|
|
738
737
|
if options.test_timeout > 0:
|
|
739
738
|
return options.test_timeout
|
|
740
|
-
test_files = list(self.pkg_path.glob("tests/test_*.py"))
|
|
739
|
+
test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
|
|
741
740
|
base_timeout = 300
|
|
742
741
|
|
|
743
742
|
import math
|
|
@@ -1020,7 +1019,9 @@ class TestManagementImpl:
|
|
|
1020
1019
|
test_dir = self.pkg_path / "tests"
|
|
1021
1020
|
if not test_dir.exists():
|
|
1022
1021
|
issues.append("tests directory not found")
|
|
1023
|
-
test_files =
|
|
1022
|
+
test_files = (
|
|
1023
|
+
list[t.Any](test_dir.glob("test_ *.py")) if test_dir.exists() else []
|
|
1024
|
+
)
|
|
1024
1025
|
if not test_files:
|
|
1025
1026
|
issues.append("no test files found")
|
|
1026
1027
|
if issues:
|
|
@@ -1035,7 +1036,7 @@ class TestManagementImpl:
|
|
|
1035
1036
|
test_dir = self.pkg_path / "tests"
|
|
1036
1037
|
if not test_dir.exists():
|
|
1037
1038
|
return {"test_files": 0, "total_tests": 0, "test_lines": 0}
|
|
1038
|
-
test_files = list(test_dir.glob("test_ *.py"))
|
|
1039
|
+
test_files = list[t.Any](test_dir.glob("test_ *.py"))
|
|
1039
1040
|
total_lines = 0
|
|
1040
1041
|
total_tests = 0
|
|
1041
1042
|
for test_file in test_files:
|
|
@@ -1070,5 +1071,5 @@ class TestManagementImpl:
|
|
|
1070
1071
|
return None
|
|
1071
1072
|
|
|
1072
1073
|
def has_tests(self) -> bool:
|
|
1073
|
-
test_files = list(self.pkg_path.glob("tests/test_*.py"))
|
|
1074
|
+
test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
|
|
1074
1075
|
return len(test_files) > 0
|
crackerjack/mcp/cache.py
CHANGED
|
@@ -97,7 +97,7 @@ class ErrorCache:
|
|
|
97
97
|
]
|
|
98
98
|
|
|
99
99
|
def get_common_patterns(self, limit: int = 20) -> list[ErrorPattern]:
|
|
100
|
-
patterns = list(self.patterns.values())
|
|
100
|
+
patterns = list[t.Any](self.patterns.values())
|
|
101
101
|
patterns.sort(key=lambda p: p.frequency, reverse=True)
|
|
102
102
|
return patterns[:limit]
|
|
103
103
|
|
|
@@ -252,7 +252,7 @@ class ErrorCache:
|
|
|
252
252
|
successful_fixes = sum(1 for result in self.fix_results if result.success)
|
|
253
253
|
frequencies = [pattern.frequency for pattern in self.patterns.values()]
|
|
254
254
|
avg_frequency = sum(frequencies) / len(frequencies) if frequencies else 0
|
|
255
|
-
type_counts = {}
|
|
255
|
+
type_counts: dict[str, int] = {}
|
|
256
256
|
for pattern in self.patterns.values():
|
|
257
257
|
type_counts[pattern.error_type] = type_counts.get(pattern.error_type, 0) + 1
|
|
258
258
|
|
crackerjack/mcp/client_runner.py
CHANGED
|
@@ -22,7 +22,7 @@ def is_mcp_server_running(host: str = "localhost", port: int = 5173) -> bool:
|
|
|
22
22
|
sock.close()
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
async def ensure_mcp_server_running() -> subprocess.Popen | None:
|
|
25
|
+
async def ensure_mcp_server_running() -> subprocess.Popen[bytes] | None:
|
|
26
26
|
console = Console()
|
|
27
27
|
|
|
28
28
|
if is_mcp_server_running():
|
crackerjack/mcp/context.py
CHANGED
|
@@ -33,7 +33,7 @@ class BatchedStateSaver:
|
|
|
33
33
|
self._pending_saves: dict[str, t.Callable[[], None]] = {}
|
|
34
34
|
self._last_save_time: dict[str, float] = {}
|
|
35
35
|
|
|
36
|
-
self._save_task: asyncio.Task | None = None
|
|
36
|
+
self._save_task: asyncio.Task[None] | None = None
|
|
37
37
|
self._running = False
|
|
38
38
|
self._lock = asyncio.Lock()
|
|
39
39
|
|
|
@@ -85,7 +85,7 @@ class BatchedStateSaver:
|
|
|
85
85
|
ready_saves = []
|
|
86
86
|
|
|
87
87
|
async with self._lock:
|
|
88
|
-
for save_id, last_time in list(self._last_save_time.items()):
|
|
88
|
+
for save_id, last_time in list[t.Any](self._last_save_time.items()):
|
|
89
89
|
if now - last_time >= self.debounce_delay:
|
|
90
90
|
ready_saves.append(save_id)
|
|
91
91
|
|
|
@@ -106,7 +106,7 @@ class BatchedStateSaver:
|
|
|
106
106
|
|
|
107
107
|
async def _flush_saves(self) -> None:
|
|
108
108
|
async with self._lock:
|
|
109
|
-
save_ids = list(self._pending_saves.keys())
|
|
109
|
+
save_ids = list[t.Any](self._pending_saves.keys())
|
|
110
110
|
|
|
111
111
|
if save_ids:
|
|
112
112
|
await self._execute_saves(save_ids)
|
|
@@ -130,7 +130,6 @@ class MCPServerConfig:
|
|
|
130
130
|
cache_dir: Path | None = None
|
|
131
131
|
|
|
132
132
|
def __post_init__(self) -> None:
|
|
133
|
-
# Validate all paths using secure path validation
|
|
134
133
|
self.project_path = SecurePathValidator.validate_safe_path(self.project_path)
|
|
135
134
|
|
|
136
135
|
if self.progress_dir:
|
|
@@ -149,13 +148,12 @@ class MCPServerContext:
|
|
|
149
148
|
def __init__(self, config: MCPServerConfig) -> None:
|
|
150
149
|
self.config = config
|
|
151
150
|
|
|
152
|
-
# Resource management
|
|
153
151
|
self.resource_manager = ResourceManager()
|
|
154
152
|
self.network_manager = NetworkResourceManager()
|
|
155
153
|
register_global_resource_manager(self.resource_manager)
|
|
156
154
|
|
|
157
155
|
self.console: Console | None = None
|
|
158
|
-
self.cli_runner = None
|
|
156
|
+
self.cli_runner: WorkflowOrchestrator | None = None
|
|
159
157
|
self.state_manager: StateManager | None = None
|
|
160
158
|
self.error_cache: ErrorCache | None = None
|
|
161
159
|
self.rate_limiter: RateLimitMiddleware | None = None
|
|
@@ -174,61 +172,172 @@ class MCPServerContext:
|
|
|
174
172
|
)
|
|
175
173
|
self._websocket_process_lock = asyncio.Lock()
|
|
176
174
|
self._websocket_cleanup_registered = False
|
|
177
|
-
self._websocket_health_check_task: asyncio.Task | None = None
|
|
175
|
+
self._websocket_health_check_task: asyncio.Task[None] | None = None
|
|
178
176
|
|
|
179
177
|
self._initialized = False
|
|
180
178
|
self._startup_tasks: list[t.Callable[[], t.Awaitable[None]]] = []
|
|
181
179
|
self._shutdown_tasks: list[t.Callable[[], t.Awaitable[None]]] = []
|
|
182
180
|
|
|
183
|
-
async def
|
|
184
|
-
|
|
185
|
-
return
|
|
186
|
-
|
|
181
|
+
async def _auto_setup_git_working_directory(self) -> None:
|
|
182
|
+
"""Auto-detect and setup git working directory for enhanced DX."""
|
|
187
183
|
try:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
self.
|
|
191
|
-
else:
|
|
192
|
-
self.console = Console(force_terminal=True)
|
|
184
|
+
git_root = await self._detect_git_repository()
|
|
185
|
+
if git_root:
|
|
186
|
+
await self._log_git_detection(git_root)
|
|
193
187
|
|
|
194
|
-
|
|
188
|
+
except Exception as e:
|
|
189
|
+
self._handle_git_setup_failure(e)
|
|
195
190
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
pkg_path=self.config.project_path,
|
|
199
|
-
)
|
|
191
|
+
async def _detect_git_repository(self) -> Path | None:
|
|
192
|
+
"""Detect if we're in a git repository and return the root path."""
|
|
200
193
|
|
|
201
|
-
|
|
202
|
-
self.config.state_dir or Path.home() / ".cache" / "crackerjack-mcp",
|
|
203
|
-
self.batched_saver,
|
|
204
|
-
)
|
|
194
|
+
current_dir = Path.cwd()
|
|
205
195
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
196
|
+
# Check if we're in a git repository
|
|
197
|
+
if not self._is_git_repository(current_dir):
|
|
198
|
+
return None
|
|
209
199
|
|
|
210
|
-
|
|
200
|
+
return self._get_git_root_directory(current_dir)
|
|
211
201
|
|
|
212
|
-
|
|
202
|
+
def _is_git_repository(self, current_dir: Path) -> bool:
|
|
203
|
+
"""Check if the current directory is within a git repository."""
|
|
204
|
+
import subprocess
|
|
213
205
|
|
|
214
|
-
|
|
215
|
-
|
|
206
|
+
git_check = subprocess.run(
|
|
207
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
208
|
+
capture_output=True,
|
|
209
|
+
text=True,
|
|
210
|
+
cwd=current_dir,
|
|
211
|
+
)
|
|
212
|
+
return git_check.returncode == 0
|
|
213
|
+
|
|
214
|
+
def _get_git_root_directory(self, current_dir: Path) -> Path | None:
|
|
215
|
+
"""Get the git repository root directory."""
|
|
216
|
+
import subprocess
|
|
217
|
+
|
|
218
|
+
git_root_result = subprocess.run(
|
|
219
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
220
|
+
capture_output=True,
|
|
221
|
+
text=True,
|
|
222
|
+
cwd=current_dir,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if git_root_result.returncode == 0:
|
|
226
|
+
git_root = Path(git_root_result.stdout.strip())
|
|
227
|
+
return git_root if git_root.exists() else None
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
async def _log_git_detection(self, git_root: Path) -> None:
|
|
231
|
+
"""Log git repository detection to stderr and console."""
|
|
232
|
+
|
|
233
|
+
# Log to stderr for Claude to see
|
|
234
|
+
self._log_to_stderr(git_root)
|
|
235
|
+
|
|
236
|
+
# Log to console if available
|
|
237
|
+
self._log_to_console(git_root)
|
|
238
|
+
|
|
239
|
+
def _log_to_stderr(self, git_root: Path) -> None:
|
|
240
|
+
"""Log git detection messages to stderr."""
|
|
241
|
+
import sys
|
|
242
|
+
|
|
243
|
+
print(
|
|
244
|
+
f"📍 Crackerjack MCP: Git repository detected at {git_root}",
|
|
245
|
+
file=sys.stderr,
|
|
246
|
+
)
|
|
247
|
+
print(
|
|
248
|
+
f"💡 Tip: Auto-setup git working directory with: git_set_working_dir('{git_root}')",
|
|
249
|
+
file=sys.stderr,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def _log_to_console(self, git_root: Path) -> None:
|
|
253
|
+
"""Log git detection messages to console if available."""
|
|
254
|
+
if self.console:
|
|
255
|
+
self.console.print(f"🔧 Auto-detected git repository: {git_root}")
|
|
256
|
+
self.console.print(
|
|
257
|
+
f"💡 Recommend: Use `mcp__git__git_set_working_dir` with path='{git_root}'"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def _handle_git_setup_failure(self, error: Exception) -> None:
|
|
261
|
+
"""Handle git setup failure with graceful fallback."""
|
|
262
|
+
if self.console:
|
|
263
|
+
self.console.print(
|
|
264
|
+
f"[dim]Git auto-setup failed (non-critical): {error}[/dim]"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
async def initialize(self) -> None:
|
|
268
|
+
if self._initialized:
|
|
269
|
+
return
|
|
216
270
|
|
|
271
|
+
try:
|
|
272
|
+
await self._perform_initialization_sequence()
|
|
217
273
|
self._initialized = True
|
|
218
274
|
|
|
219
275
|
except Exception as e:
|
|
220
|
-
self.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
276
|
+
self._handle_initialization_failure(e)
|
|
277
|
+
|
|
278
|
+
async def _perform_initialization_sequence(self) -> None:
|
|
279
|
+
"""Perform the complete initialization sequence."""
|
|
280
|
+
self._setup_console()
|
|
281
|
+
self._setup_directories()
|
|
282
|
+
await self._initialize_components()
|
|
283
|
+
await self._finalize_initialization()
|
|
284
|
+
|
|
285
|
+
def _handle_initialization_failure(self, error: Exception) -> None:
|
|
286
|
+
"""Handle initialization failure with cleanup and error propagation."""
|
|
287
|
+
self._cleanup_failed_initialization()
|
|
288
|
+
msg = f"Failed to initialize MCP server context: {error}"
|
|
289
|
+
raise RuntimeError(msg) from error
|
|
290
|
+
|
|
291
|
+
def _setup_console(self) -> None:
|
|
292
|
+
"""Setup console based on configuration mode."""
|
|
293
|
+
if self.config.stdio_mode:
|
|
294
|
+
null_file = io.StringIO()
|
|
295
|
+
self.console = Console(file=null_file, force_terminal=False)
|
|
296
|
+
else:
|
|
297
|
+
self.console = Console(force_terminal=True)
|
|
298
|
+
|
|
299
|
+
def _setup_directories(self) -> None:
|
|
300
|
+
"""Setup required directories."""
|
|
301
|
+
self.progress_dir.mkdir(exist_ok=True)
|
|
302
|
+
|
|
303
|
+
async def _initialize_components(self) -> None:
|
|
304
|
+
"""Initialize all service components."""
|
|
305
|
+
self.cli_runner = WorkflowOrchestrator(
|
|
306
|
+
console=self.console,
|
|
307
|
+
pkg_path=self.config.project_path,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
self.state_manager = StateManager(
|
|
311
|
+
self.config.state_dir or Path.home() / ".cache" / "crackerjack-mcp",
|
|
312
|
+
self.batched_saver,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
self.error_cache = ErrorCache(
|
|
316
|
+
self.config.cache_dir or Path.home() / ".cache" / "crackerjack-mcp",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
self.rate_limiter = RateLimitMiddleware(self.config.rate_limit_config)
|
|
320
|
+
await self.batched_saver.start()
|
|
321
|
+
|
|
322
|
+
async def _finalize_initialization(self) -> None:
|
|
323
|
+
"""Complete initialization with optional setup and startup tasks."""
|
|
324
|
+
# Auto-setup git working directory for enhanced DX
|
|
325
|
+
await self._auto_setup_git_working_directory()
|
|
326
|
+
|
|
327
|
+
for task in self._startup_tasks:
|
|
328
|
+
await task()
|
|
329
|
+
|
|
330
|
+
def _cleanup_failed_initialization(self) -> None:
|
|
331
|
+
"""Cleanup components after failed initialization."""
|
|
332
|
+
self.cli_runner = None
|
|
333
|
+
self.state_manager = None
|
|
334
|
+
self.error_cache = None
|
|
335
|
+
self.rate_limiter = None
|
|
226
336
|
|
|
227
337
|
async def shutdown(self) -> None:
|
|
228
338
|
if not self._initialized:
|
|
229
339
|
return
|
|
230
340
|
|
|
231
|
-
# Run custom shutdown tasks first
|
|
232
341
|
for task in reversed(self._shutdown_tasks):
|
|
233
342
|
try:
|
|
234
343
|
await task()
|
|
@@ -236,24 +345,19 @@ class MCPServerContext:
|
|
|
236
345
|
if self.console:
|
|
237
346
|
self.console.print(f"[red]Error during shutdown: {e}[/red]")
|
|
238
347
|
|
|
239
|
-
# Cancel health check task
|
|
240
348
|
if self._websocket_health_check_task:
|
|
241
349
|
self._websocket_health_check_task.cancel()
|
|
242
350
|
with contextlib.suppress(asyncio.CancelledError):
|
|
243
351
|
await self._websocket_health_check_task
|
|
244
352
|
self._websocket_health_check_task = None
|
|
245
353
|
|
|
246
|
-
# Stop WebSocket server
|
|
247
354
|
await self._stop_websocket_server()
|
|
248
355
|
|
|
249
|
-
# Stop rate limiter
|
|
250
356
|
if self.rate_limiter:
|
|
251
357
|
await self.rate_limiter.stop()
|
|
252
358
|
|
|
253
|
-
# Stop batched saver
|
|
254
359
|
await self.batched_saver.stop()
|
|
255
360
|
|
|
256
|
-
# Clean up all managed resources
|
|
257
361
|
try:
|
|
258
362
|
await self.network_manager.cleanup_all()
|
|
259
363
|
except Exception as e:
|
|
@@ -314,21 +418,31 @@ class MCPServerContext:
|
|
|
314
418
|
if await self._check_existing_websocket_server():
|
|
315
419
|
return True
|
|
316
420
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
f"🚀 Starting WebSocket server on localhost: {self.websocket_server_port}...",
|
|
320
|
-
)
|
|
421
|
+
self._print_websocket_startup_message()
|
|
422
|
+
return await self._attempt_websocket_startup()
|
|
321
423
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
424
|
+
def _print_websocket_startup_message(self) -> None:
|
|
425
|
+
"""Print websocket server startup message."""
|
|
426
|
+
if self.console:
|
|
427
|
+
self.console.print(
|
|
428
|
+
f"🚀 Starting WebSocket server on localhost: {self.websocket_server_port}...",
|
|
429
|
+
)
|
|
326
430
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
431
|
+
async def _attempt_websocket_startup(self) -> bool:
|
|
432
|
+
"""Attempt to start the websocket server with error handling."""
|
|
433
|
+
try:
|
|
434
|
+
await self._spawn_websocket_process()
|
|
435
|
+
await self._register_websocket_cleanup()
|
|
436
|
+
return await self._wait_for_websocket_startup()
|
|
437
|
+
except Exception as e:
|
|
438
|
+
await self._handle_websocket_startup_failure(e)
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
async def _handle_websocket_startup_failure(self, error: Exception) -> None:
|
|
442
|
+
"""Handle websocket server startup failure."""
|
|
443
|
+
if self.console:
|
|
444
|
+
self.console.print(f"❌ Failed to start WebSocket server: {error}")
|
|
445
|
+
await self._cleanup_dead_websocket_process()
|
|
332
446
|
|
|
333
447
|
async def _check_existing_websocket_server(self) -> bool:
|
|
334
448
|
if (
|
|
@@ -369,7 +483,6 @@ class MCPServerContext:
|
|
|
369
483
|
start_new_session=True,
|
|
370
484
|
)
|
|
371
485
|
|
|
372
|
-
# Register the process with the network resource manager for automatic cleanup
|
|
373
486
|
if self.websocket_server_process:
|
|
374
487
|
managed_process = self.network_manager.create_subprocess(
|
|
375
488
|
self.websocket_server_process, timeout=30.0
|
|
@@ -391,7 +504,10 @@ class MCPServerContext:
|
|
|
391
504
|
for _attempt in range(max_attempts):
|
|
392
505
|
await asyncio.sleep(0.5)
|
|
393
506
|
|
|
394
|
-
if
|
|
507
|
+
if (
|
|
508
|
+
self.websocket_server_process is not None
|
|
509
|
+
and self.websocket_server_process.poll() is not None
|
|
510
|
+
):
|
|
395
511
|
return_code = self.websocket_server_process.returncode
|
|
396
512
|
if self.console:
|
|
397
513
|
self.console.print(
|
|
@@ -420,7 +536,10 @@ class MCPServerContext:
|
|
|
420
536
|
async def _cleanup_dead_websocket_process(self) -> None:
|
|
421
537
|
if self.websocket_server_process:
|
|
422
538
|
try:
|
|
423
|
-
if
|
|
539
|
+
if (
|
|
540
|
+
self.websocket_server_process is not None
|
|
541
|
+
and self.websocket_server_process.poll() is None
|
|
542
|
+
):
|
|
424
543
|
self.websocket_server_process.terminate()
|
|
425
544
|
try:
|
|
426
545
|
self.websocket_server_process.wait(timeout=2)
|
|
@@ -457,7 +576,8 @@ class MCPServerContext:
|
|
|
457
576
|
if self.console:
|
|
458
577
|
self.console.print("🛑 Stopping WebSocket server...")
|
|
459
578
|
|
|
460
|
-
self.websocket_server_process
|
|
579
|
+
if self.websocket_server_process is not None:
|
|
580
|
+
self.websocket_server_process.terminate()
|
|
461
581
|
|
|
462
582
|
if await self._wait_for_graceful_termination():
|
|
463
583
|
return
|
|
@@ -466,7 +586,8 @@ class MCPServerContext:
|
|
|
466
586
|
|
|
467
587
|
async def _wait_for_graceful_termination(self) -> bool:
|
|
468
588
|
try:
|
|
469
|
-
self.websocket_server_process
|
|
589
|
+
if self.websocket_server_process is not None:
|
|
590
|
+
self.websocket_server_process.wait(timeout=5)
|
|
470
591
|
if self.console:
|
|
471
592
|
self.console.print("✅ WebSocket server stopped gracefully")
|
|
472
593
|
return True
|
|
@@ -477,10 +598,12 @@ class MCPServerContext:
|
|
|
477
598
|
if self.console:
|
|
478
599
|
self.console.print("⚡ Force killing unresponsive WebSocket server...")
|
|
479
600
|
|
|
480
|
-
self.websocket_server_process
|
|
601
|
+
if self.websocket_server_process is not None:
|
|
602
|
+
self.websocket_server_process.kill()
|
|
481
603
|
|
|
482
604
|
try:
|
|
483
|
-
self.websocket_server_process
|
|
605
|
+
if self.websocket_server_process is not None:
|
|
606
|
+
self.websocket_server_process.wait(timeout=2)
|
|
484
607
|
if self.console:
|
|
485
608
|
self.console.print("💀 WebSocket server force killed")
|
|
486
609
|
except subprocess.TimeoutExpired:
|
|
@@ -538,7 +661,8 @@ class MCPServerContext:
|
|
|
538
661
|
await self._handle_unresponsive_websocket_server()
|
|
539
662
|
|
|
540
663
|
async def _handle_dead_websocket_process(self) -> None:
|
|
541
|
-
|
|
664
|
+
if self.websocket_server_process is not None:
|
|
665
|
+
return_code = self.websocket_server_process.returncode
|
|
542
666
|
if self.console:
|
|
543
667
|
self.console.print(
|
|
544
668
|
f"⚠️ WebSocket server process died (exit code: {return_code}), attempting restart...",
|
|
@@ -559,7 +683,7 @@ class MCPServerContext:
|
|
|
559
683
|
elif self.console:
|
|
560
684
|
self.console.print("❌ Failed to restart WebSocket server")
|
|
561
685
|
|
|
562
|
-
def safe_print(self, *args, **kwargs) -> None:
|
|
686
|
+
def safe_print(self, *args: t.Any, **kwargs: t.Any) -> None:
|
|
563
687
|
if not self.config.stdio_mode and self.console:
|
|
564
688
|
self.console.print(*args, **kwargs)
|
|
565
689
|
|
|
@@ -568,7 +692,6 @@ class MCPServerContext:
|
|
|
568
692
|
msg = f"Invalid job_id: {job_id}"
|
|
569
693
|
raise ValueError(msg)
|
|
570
694
|
|
|
571
|
-
# Use secure path joining to prevent directory traversal
|
|
572
695
|
return SecurePathValidator.secure_path_join(
|
|
573
696
|
self.progress_dir, f"job-{job_id}.json"
|
|
574
697
|
)
|
|
@@ -671,7 +794,7 @@ def get_rate_limiter() -> RateLimitMiddleware | None:
|
|
|
671
794
|
return get_context().rate_limiter
|
|
672
795
|
|
|
673
796
|
|
|
674
|
-
def safe_print(*args, **kwargs) -> None:
|
|
797
|
+
def safe_print(*args: t.Any, **kwargs: t.Any) -> None:
|
|
675
798
|
get_context().safe_print(*args, **kwargs)
|
|
676
799
|
|
|
677
800
|
|
crackerjack/mcp/dashboard.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import time
|
|
2
|
+
import typing as t
|
|
2
3
|
from collections import deque
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from pathlib import Path
|
|
@@ -74,7 +75,7 @@ class MetricCard(Static):
|
|
|
74
75
|
value: str = " --",
|
|
75
76
|
trend: str = "",
|
|
76
77
|
status: str = "",
|
|
77
|
-
**kwargs,
|
|
78
|
+
**kwargs: t.Any,
|
|
78
79
|
) -> None:
|
|
79
80
|
super().__init__(**kwargs)
|
|
80
81
|
self.label = label
|
|
@@ -205,7 +206,7 @@ class PerformanceWidget(Static):
|
|
|
205
206
|
}
|
|
206
207
|
"""
|
|
207
208
|
|
|
208
|
-
def __init__(self, **kwargs) -> None:
|
|
209
|
+
def __init__(self, **kwargs: t.Any) -> None:
|
|
209
210
|
super().__init__(**kwargs)
|
|
210
211
|
self.cpu_history: deque[float] = deque(maxlen=50)
|
|
211
212
|
self.memory_history: deque[float] = deque(maxlen=50)
|
|
@@ -307,7 +308,7 @@ class CrackerjackDashboard(App):
|
|
|
307
308
|
}
|
|
308
309
|
"""
|
|
309
310
|
|
|
310
|
-
def __init__(self, **kwargs) -> None:
|
|
311
|
+
def __init__(self, **kwargs: t.Any) -> None:
|
|
311
312
|
super().__init__(**kwargs)
|
|
312
313
|
|
|
313
314
|
self.job_collector = JobDataCollector()
|
|
@@ -463,7 +464,8 @@ class CrackerjackDashboard(App):
|
|
|
463
464
|
"http: / / localhost: 8675 / api / jobs"
|
|
464
465
|
) as response:
|
|
465
466
|
if response.status == 200:
|
|
466
|
-
|
|
467
|
+
json_result = await response.json()
|
|
468
|
+
return t.cast(dict[str, t.Any], json_result)
|
|
467
469
|
return {}
|
|
468
470
|
except Exception as e:
|
|
469
471
|
self.log(f"Error fetching WebSocket jobs: {e}")
|
|
@@ -471,7 +473,7 @@ class CrackerjackDashboard(App):
|
|
|
471
473
|
|
|
472
474
|
async def _collect_jobs_from_filesystem(self) -> dict[str, Any]:
|
|
473
475
|
try:
|
|
474
|
-
jobs = {}
|
|
476
|
+
jobs: dict[str, Any] = {}
|
|
475
477
|
|
|
476
478
|
import tempfile
|
|
477
479
|
|