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
|
@@ -5,6 +5,7 @@ import subprocess
|
|
|
5
5
|
import sys
|
|
6
6
|
import tempfile
|
|
7
7
|
import time
|
|
8
|
+
import typing as t
|
|
8
9
|
from contextlib import suppress
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
@@ -26,7 +27,7 @@ from .progress_components import (
|
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class AgentStatusPanel(Widget):
|
|
29
|
-
def __init__(self, **kwargs) -> None:
|
|
30
|
+
def __init__(self, **kwargs: t.Any) -> None:
|
|
30
31
|
super().__init__(**kwargs)
|
|
31
32
|
self.border_title = "🤖 AI Agents"
|
|
32
33
|
self.border_title_align = "left"
|
|
@@ -49,13 +50,13 @@ class AgentStatusPanel(Widget):
|
|
|
49
50
|
|
|
50
51
|
agents_table.styles.max_height = "8"
|
|
51
52
|
|
|
52
|
-
def update_agent_data(self, agent_data: dict) -> None:
|
|
53
|
+
def update_agent_data(self, agent_data: dict[str, t.Any]) -> None:
|
|
53
54
|
with suppress(Exception):
|
|
54
55
|
self._update_coordinator_status(agent_data)
|
|
55
56
|
self._update_agents_table(agent_data)
|
|
56
57
|
self._update_stats(agent_data)
|
|
57
58
|
|
|
58
|
-
def _update_coordinator_status(self, data: dict) -> None:
|
|
59
|
+
def _update_coordinator_status(self, data: dict[str, t.Any]) -> None:
|
|
59
60
|
with suppress(Exception):
|
|
60
61
|
activity = data.get("agent_activity", {})
|
|
61
62
|
registry = activity.get("agent_registry", {})
|
|
@@ -68,7 +69,7 @@ class AgentStatusPanel(Widget):
|
|
|
68
69
|
f"Coordinator: {status_emoji} {coordinator_status.title()} ({total_agents} agents)",
|
|
69
70
|
)
|
|
70
71
|
|
|
71
|
-
def _update_agents_table(self, data: dict) -> None:
|
|
72
|
+
def _update_agents_table(self, data: dict[str, t.Any]) -> None:
|
|
72
73
|
with suppress(Exception):
|
|
73
74
|
agents_table = self.query_one("#agents-table", DataTable)
|
|
74
75
|
agents_table.clear()
|
|
@@ -105,7 +106,7 @@ class AgentStatusPanel(Widget):
|
|
|
105
106
|
f"{processing_time: .1f}s" if processing_time > 0 else "-",
|
|
106
107
|
)
|
|
107
108
|
|
|
108
|
-
def _update_stats(self, data: dict) -> None:
|
|
109
|
+
def _update_stats(self, data: dict[str, t.Any]) -> None:
|
|
109
110
|
with suppress(Exception):
|
|
110
111
|
performance = data.get("agent_performance", {})
|
|
111
112
|
total_issues = performance.get("total_issues_processed", 0)
|
|
@@ -152,12 +153,12 @@ class AgentStatusPanel(Widget):
|
|
|
152
153
|
|
|
153
154
|
|
|
154
155
|
class JobPanel(Widget):
|
|
155
|
-
def __init__(self, job_data: dict, **kwargs) -> None:
|
|
156
|
+
def __init__(self, job_data: dict[str, t.Any], **kwargs: t.Any) -> None:
|
|
156
157
|
super().__init__(**kwargs)
|
|
157
158
|
self.job_data = job_data
|
|
158
159
|
self.completion_time = None
|
|
159
160
|
self.iteration_count = job_data.get("iteration", 0)
|
|
160
|
-
self.max_iterations = job_data.get("max_iterations",
|
|
161
|
+
self.max_iterations = job_data.get("max_iterations", 5)
|
|
161
162
|
self.fade_timer = None
|
|
162
163
|
self.remove_timer = None
|
|
163
164
|
self.fade_level = 0
|
|
@@ -417,11 +418,13 @@ class CrackerjackDashboard(App):
|
|
|
417
418
|
|
|
418
419
|
def __init__(self) -> None:
|
|
419
420
|
super().__init__()
|
|
420
|
-
self.progress_dir = Path(tempfile.gettempdir()) / "crackerjack
|
|
421
|
+
self.progress_dir = Path(tempfile.gettempdir()) / "crackerjack-mcp-progress"
|
|
421
422
|
self.websocket_url = "ws://localhost:8675"
|
|
422
423
|
self.refresh_timer = None
|
|
423
|
-
self.
|
|
424
|
-
self.
|
|
424
|
+
self._refresh_counter = 0
|
|
425
|
+
self.dev = False
|
|
426
|
+
self.active_jobs: dict[str, t.Any] = {}
|
|
427
|
+
self.completed_jobs_stats: dict[str, t.Any] = {}
|
|
425
428
|
self.current_polling_method = "File"
|
|
426
429
|
self.timeout_manager = get_timeout_manager()
|
|
427
430
|
|
|
@@ -532,19 +535,14 @@ class CrackerjackDashboard(App):
|
|
|
532
535
|
|
|
533
536
|
async def _refresh_data(self) -> None:
|
|
534
537
|
try:
|
|
535
|
-
# Refresh cycle with timeout protection
|
|
536
538
|
async with self.timeout_manager.timeout_context(
|
|
537
539
|
"network_operations",
|
|
538
|
-
timeout=10.0,
|
|
540
|
+
timeout=10.0,
|
|
539
541
|
strategy=TimeoutStrategy.GRACEFUL_DEGRADATION,
|
|
540
542
|
):
|
|
541
|
-
|
|
542
|
-
self._refresh_counter += 1
|
|
543
|
-
else:
|
|
544
|
-
self._refresh_counter = 0
|
|
543
|
+
self._refresh_counter += 1
|
|
545
544
|
|
|
546
|
-
|
|
547
|
-
if self._refresh_counter % 20 == 0: # Every 10 seconds instead of 5
|
|
545
|
+
if self._refresh_counter % 20 == 0:
|
|
548
546
|
with suppress(Exception):
|
|
549
547
|
await self.timeout_manager.with_timeout(
|
|
550
548
|
"network_operations",
|
|
@@ -553,7 +551,6 @@ class CrackerjackDashboard(App):
|
|
|
553
551
|
strategy=TimeoutStrategy.FAIL_FAST,
|
|
554
552
|
)
|
|
555
553
|
|
|
556
|
-
# Collect data with individual timeouts
|
|
557
554
|
jobs_data = await self.timeout_manager.with_timeout(
|
|
558
555
|
"network_operations",
|
|
559
556
|
self._discover_jobs(),
|
|
@@ -575,7 +572,6 @@ class CrackerjackDashboard(App):
|
|
|
575
572
|
strategy=TimeoutStrategy.FAIL_FAST,
|
|
576
573
|
)
|
|
577
574
|
|
|
578
|
-
# Update UI components
|
|
579
575
|
self.query_one("#services-panel").border_title = "🔧 Services"
|
|
580
576
|
|
|
581
577
|
self._update_jobs_table(jobs_data)
|
|
@@ -586,12 +582,11 @@ class CrackerjackDashboard(App):
|
|
|
586
582
|
self._update_status_bars(jobs_data)
|
|
587
583
|
|
|
588
584
|
except Exception as e:
|
|
589
|
-
# Log error but don't crash the dashboard
|
|
590
585
|
with suppress(Exception):
|
|
591
586
|
console = Console()
|
|
592
587
|
console.print(f"[red]Dashboard refresh error: {e}[/red]")
|
|
593
588
|
|
|
594
|
-
async def _discover_jobs(self) -> dict:
|
|
589
|
+
async def _discover_jobs(self) -> dict[str, t.Any]:
|
|
595
590
|
try:
|
|
596
591
|
result = await self.timeout_manager.with_timeout(
|
|
597
592
|
"network_operations",
|
|
@@ -600,9 +595,9 @@ class CrackerjackDashboard(App):
|
|
|
600
595
|
strategy=TimeoutStrategy.FAIL_FAST,
|
|
601
596
|
)
|
|
602
597
|
self.current_polling_method = result["method"]
|
|
603
|
-
|
|
598
|
+
data_result = result["data"]
|
|
599
|
+
return t.cast(dict[str, t.Any], data_result)
|
|
604
600
|
except Exception:
|
|
605
|
-
# Return empty data structure on timeout or error
|
|
606
601
|
return {
|
|
607
602
|
"active": 0,
|
|
608
603
|
"completed": 0,
|
|
@@ -615,31 +610,31 @@ class CrackerjackDashboard(App):
|
|
|
615
610
|
"current_errors": 0,
|
|
616
611
|
}
|
|
617
612
|
|
|
618
|
-
async def _collect_services_data(self) -> list:
|
|
613
|
+
async def _collect_services_data(self) -> list[t.Any]:
|
|
619
614
|
try:
|
|
620
|
-
|
|
615
|
+
services_data = await self.timeout_manager.with_timeout(
|
|
621
616
|
"network_operations",
|
|
622
617
|
self.service_checker.collect_services_data(),
|
|
623
618
|
timeout=3.0,
|
|
624
619
|
strategy=TimeoutStrategy.FAIL_FAST,
|
|
625
620
|
)
|
|
621
|
+
return t.cast(list[t.Any], services_data)
|
|
626
622
|
except Exception:
|
|
627
|
-
# Return minimal service data on timeout
|
|
628
623
|
return [("Services", "🔴 Timeout", "0")]
|
|
629
624
|
|
|
630
|
-
async def _collect_recent_errors(self) -> list:
|
|
625
|
+
async def _collect_recent_errors(self) -> list[t.Any]:
|
|
631
626
|
try:
|
|
632
|
-
|
|
627
|
+
errors_data = await self.timeout_manager.with_timeout(
|
|
633
628
|
"file_operations",
|
|
634
629
|
self.error_collector.collect_recent_errors(),
|
|
635
630
|
timeout=2.0,
|
|
636
631
|
strategy=TimeoutStrategy.FAIL_FAST,
|
|
637
632
|
)
|
|
633
|
+
return t.cast(list[t.Any], errors_data)
|
|
638
634
|
except Exception:
|
|
639
|
-
# Return empty error list on timeout
|
|
640
635
|
return []
|
|
641
636
|
|
|
642
|
-
def _update_jobs_table(self, jobs_data: dict) -> None:
|
|
637
|
+
def _update_jobs_table(self, jobs_data: dict[str, t.Any]) -> None:
|
|
643
638
|
with suppress(Exception):
|
|
644
639
|
jobs_table = self.query_one("#jobs-table", DataTable)
|
|
645
640
|
jobs_table.clear()
|
|
@@ -681,7 +676,7 @@ class CrackerjackDashboard(App):
|
|
|
681
676
|
],
|
|
682
677
|
)
|
|
683
678
|
|
|
684
|
-
def _update_services_table(self, services_data: list) -> None:
|
|
679
|
+
def _update_services_table(self, services_data: list[t.Any]) -> None:
|
|
685
680
|
with suppress(Exception):
|
|
686
681
|
services_table = self.query_one("#services-table", DataTable)
|
|
687
682
|
services_table.clear()
|
|
@@ -705,7 +700,7 @@ class CrackerjackDashboard(App):
|
|
|
705
700
|
polling_status += " 🟢"
|
|
706
701
|
services_table.add_row("Polling", polling_status, "")
|
|
707
702
|
|
|
708
|
-
def _update_errors_table(self, errors_data: list) -> None:
|
|
703
|
+
def _update_errors_table(self, errors_data: list[t.Any]) -> None:
|
|
709
704
|
with suppress(Exception):
|
|
710
705
|
errors_table = self.query_one("#errors-table", DataTable)
|
|
711
706
|
errors_table.clear()
|
|
@@ -767,7 +762,7 @@ class CrackerjackDashboard(App):
|
|
|
767
762
|
],
|
|
768
763
|
)
|
|
769
764
|
|
|
770
|
-
def _update_agent_panel(self, jobs_data: dict) -> None:
|
|
765
|
+
def _update_agent_panel(self, jobs_data: dict[str, t.Any]) -> None:
|
|
771
766
|
with suppress(Exception):
|
|
772
767
|
agent_panel = self.query_one("#agent - status-panel", AgentStatusPanel)
|
|
773
768
|
|
|
@@ -780,7 +775,7 @@ class CrackerjackDashboard(App):
|
|
|
780
775
|
if agent_data:
|
|
781
776
|
agent_panel.update_agent_data(agent_data)
|
|
782
777
|
|
|
783
|
-
def _update_job_panels(self, jobs_data: dict) -> None:
|
|
778
|
+
def _update_job_panels(self, jobs_data: dict[str, t.Any]) -> None:
|
|
784
779
|
with suppress(Exception):
|
|
785
780
|
container = self.query_one("#job - discovery-container")
|
|
786
781
|
current_job_ids = self._get_current_job_ids(jobs_data)
|
|
@@ -789,14 +784,14 @@ class CrackerjackDashboard(App):
|
|
|
789
784
|
self._update_or_create_panels(jobs_data, container)
|
|
790
785
|
self._handle_placeholder_visibility(container)
|
|
791
786
|
|
|
792
|
-
def _get_current_job_ids(self, jobs_data: dict) -> set:
|
|
787
|
+
def _get_current_job_ids(self, jobs_data: dict[str, t.Any]) -> set[t.Any]:
|
|
793
788
|
return (
|
|
794
789
|
{job["job_id"] for job in jobs_data["individual_jobs"]}
|
|
795
790
|
if jobs_data["individual_jobs"]
|
|
796
791
|
else set()
|
|
797
792
|
)
|
|
798
793
|
|
|
799
|
-
def _remove_obsolete_panels(self, current_job_ids: set) -> None:
|
|
794
|
+
def _remove_obsolete_panels(self, current_job_ids: set[t.Any]) -> None:
|
|
800
795
|
jobs_to_remove = []
|
|
801
796
|
for job_id, panel in self.active_jobs.items():
|
|
802
797
|
panel_status = panel.job_data.get("status", "").lower()
|
|
@@ -811,7 +806,9 @@ class CrackerjackDashboard(App):
|
|
|
811
806
|
panel = self.active_jobs.pop(job_id)
|
|
812
807
|
panel.remove()
|
|
813
808
|
|
|
814
|
-
def _update_or_create_panels(
|
|
809
|
+
def _update_or_create_panels(
|
|
810
|
+
self, jobs_data: dict[str, t.Any], container: t.Any
|
|
811
|
+
) -> None:
|
|
815
812
|
if not jobs_data["individual_jobs"]:
|
|
816
813
|
return
|
|
817
814
|
|
|
@@ -822,7 +819,7 @@ class CrackerjackDashboard(App):
|
|
|
822
819
|
else:
|
|
823
820
|
self._create_new_panel(job, container)
|
|
824
821
|
|
|
825
|
-
def _update_existing_panel(self, job: dict) -> None:
|
|
822
|
+
def _update_existing_panel(self, job: dict[str, t.Any]) -> None:
|
|
826
823
|
existing_panel = self.active_jobs[job["job_id"]]
|
|
827
824
|
existing_panel.job_data = job
|
|
828
825
|
existing_panel.iteration_count = job.get("iteration", 0)
|
|
@@ -833,7 +830,7 @@ class CrackerjackDashboard(App):
|
|
|
833
830
|
self._handle_job_completion(existing_panel, job)
|
|
834
831
|
self._update_panel_border(existing_panel)
|
|
835
832
|
|
|
836
|
-
def _update_panel_title(self, panel, job: dict) -> None:
|
|
833
|
+
def _update_panel_title(self, panel: t.Any, job: dict[str, t.Any]) -> None:
|
|
837
834
|
project_name = job.get("project", "crackerjack")
|
|
838
835
|
status = job.get("status", "").lower()
|
|
839
836
|
|
|
@@ -846,25 +843,25 @@ class CrackerjackDashboard(App):
|
|
|
846
843
|
else:
|
|
847
844
|
panel.border_subtitle = ""
|
|
848
845
|
|
|
849
|
-
def _handle_job_completion(self, panel, job: dict) -> None:
|
|
846
|
+
def _handle_job_completion(self, panel: t.Any, job: dict[str, t.Any]) -> None:
|
|
850
847
|
job_status = job.get("status", "").lower()
|
|
851
848
|
if job_status in ("completed", "failed") and panel.completion_time is None:
|
|
852
849
|
panel.completion_time = time.time()
|
|
853
850
|
panel.fade_timer = panel.set_timer(300.0, panel._start_fade)
|
|
854
851
|
panel.remove_timer = panel.set_timer(1200.0, panel._remove_panel)
|
|
855
852
|
|
|
856
|
-
def _update_panel_border(self, panel) -> None:
|
|
853
|
+
def _update_panel_border(self, panel: t.Any) -> None:
|
|
857
854
|
new_border = panel._calculate_border_style()
|
|
858
855
|
if new_border != panel.border_style:
|
|
859
856
|
panel.border_style = new_border
|
|
860
857
|
panel.refresh()
|
|
861
858
|
|
|
862
|
-
def _create_new_panel(self, job: dict, container) -> None:
|
|
859
|
+
def _create_new_panel(self, job: dict[str, t.Any], container: t.Any) -> None:
|
|
863
860
|
job_panel = JobPanel(job)
|
|
864
861
|
self.active_jobs[job["job_id"]] = job_panel
|
|
865
862
|
container.mount(job_panel)
|
|
866
863
|
|
|
867
|
-
def _handle_placeholder_visibility(self, container) -> None:
|
|
864
|
+
def _handle_placeholder_visibility(self, container: t.Any) -> None:
|
|
868
865
|
has_placeholder = bool(container.query("#no - jobs-label"))
|
|
869
866
|
|
|
870
867
|
if not self.active_jobs and not has_placeholder:
|
|
@@ -877,7 +874,7 @@ class CrackerjackDashboard(App):
|
|
|
877
874
|
elif self.active_jobs and has_placeholder:
|
|
878
875
|
container.query("#no - jobs-label").remove()
|
|
879
876
|
|
|
880
|
-
def _update_status_bars(self, jobs_data: dict) -> None:
|
|
877
|
+
def _update_status_bars(self, jobs_data: dict[str, t.Any]) -> None:
|
|
881
878
|
pass
|
|
882
879
|
|
|
883
880
|
def action_refresh(self) -> None:
|
|
@@ -908,7 +905,7 @@ class CrackerjackDashboard(App):
|
|
|
908
905
|
def _restore_terminal_fallback(self) -> None:
|
|
909
906
|
self.terminal_restorer.restore_terminal()
|
|
910
907
|
|
|
911
|
-
def _signal_handler(self, _signum, _frame) -> None:
|
|
908
|
+
def _signal_handler(self, _signum: t.Any, _frame: t.Any) -> None:
|
|
912
909
|
with suppress(Exception):
|
|
913
910
|
self._restore_terminal()
|
|
914
911
|
self._cleanup_started_services()
|
|
@@ -941,18 +938,18 @@ class JobMetrics:
|
|
|
941
938
|
self.completion_time: float | None = None
|
|
942
939
|
|
|
943
940
|
self.iteration = 0
|
|
944
|
-
self.max_iterations =
|
|
941
|
+
self.max_iterations = 5
|
|
945
942
|
self.current_stage = "Initializing"
|
|
946
943
|
self.status = "running"
|
|
947
944
|
self.message = ""
|
|
948
945
|
|
|
949
|
-
self.stages_completed = set()
|
|
950
|
-
self.stages_failed = set()
|
|
946
|
+
self.stages_completed: set[str] = set()
|
|
947
|
+
self.stages_failed: set[str] = set()
|
|
951
948
|
|
|
952
|
-
self.errors = []
|
|
953
|
-
self.warnings = []
|
|
954
|
-
self.hook_failures = []
|
|
955
|
-
self.test_failures = []
|
|
949
|
+
self.errors: list[str] = []
|
|
950
|
+
self.warnings: list[str] = []
|
|
951
|
+
self.hook_failures: list[str] = []
|
|
952
|
+
self.test_failures: list[str] = []
|
|
956
953
|
|
|
957
954
|
|
|
958
955
|
async def run_progress_monitor(
|
crackerjack/mcp/rate_limiter.py
CHANGED
|
@@ -35,10 +35,10 @@ class RateLimiter:
|
|
|
35
35
|
self.requests_per_hour = requests_per_hour
|
|
36
36
|
|
|
37
37
|
self.minute_windows: dict[str, deque[float]] = defaultdict(
|
|
38
|
-
lambda: deque(maxlen=requests_per_minute),
|
|
38
|
+
lambda: deque(maxlen=requests_per_minute), # type: ignore[misc]
|
|
39
39
|
)
|
|
40
40
|
self.hour_windows: dict[str, deque[float]] = defaultdict(
|
|
41
|
-
lambda: deque(maxlen=requests_per_hour),
|
|
41
|
+
lambda: deque(maxlen=requests_per_hour), # type: ignore[misc]
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
self.global_minute_window: deque[float] = deque(maxlen=requests_per_minute * 10)
|
|
@@ -110,7 +110,7 @@ class RateLimiter:
|
|
|
110
110
|
self._cleanup_global_windows(minute_cutoff, hour_cutoff)
|
|
111
111
|
|
|
112
112
|
def _cleanup_client_windows(self, minute_cutoff: float, hour_cutoff: float) -> None:
|
|
113
|
-
for client_id in list(self.minute_windows.keys()):
|
|
113
|
+
for client_id in list[t.Any](self.minute_windows.keys()):
|
|
114
114
|
minute_window = self.minute_windows[client_id]
|
|
115
115
|
hour_window = self.hour_windows[client_id]
|
|
116
116
|
|
|
@@ -200,7 +200,7 @@ class ResourceMonitor:
|
|
|
200
200
|
stale_jobs = []
|
|
201
201
|
|
|
202
202
|
async with self._lock:
|
|
203
|
-
for job_id, start_time in list(self.active_jobs.items()):
|
|
203
|
+
for job_id, start_time in list[t.Any](self.active_jobs.items()):
|
|
204
204
|
if now - start_time > max_duration:
|
|
205
205
|
stale_jobs.append(job_id)
|
|
206
206
|
del self.active_jobs[job_id]
|
|
@@ -234,7 +234,7 @@ class ResourceMonitor:
|
|
|
234
234
|
if not progress_dir.exists():
|
|
235
235
|
return True
|
|
236
236
|
|
|
237
|
-
file_count = len(list(progress_dir.glob("job-* .json")))
|
|
237
|
+
file_count = len(list[t.Any](progress_dir.glob("job-* .json")))
|
|
238
238
|
if file_count > self.config.max_progress_files:
|
|
239
239
|
console.print(
|
|
240
240
|
f"[red]🚫 Progress files ({file_count}) exceed limit ({self.config.max_progress_files})[/ red]",
|
|
@@ -272,7 +272,7 @@ class RateLimitMiddleware:
|
|
|
272
272
|
)
|
|
273
273
|
self.resource_monitor = ResourceMonitor(self.config)
|
|
274
274
|
|
|
275
|
-
self._cleanup_task: asyncio.Task | None = None
|
|
275
|
+
self._cleanup_task: asyncio.Task[None] | None = None
|
|
276
276
|
self._running = False
|
|
277
277
|
|
|
278
278
|
async def start(self) -> None:
|
crackerjack/mcp/server_core.py
CHANGED
|
@@ -7,12 +7,17 @@ from typing import Final
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
|
-
|
|
10
|
+
import tomli
|
|
11
|
+
except ImportError:
|
|
12
|
+
tomli = None # type: ignore[assignment]
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from fastmcp import FastMCP
|
|
11
16
|
|
|
12
17
|
_mcp_available = True
|
|
13
18
|
except ImportError:
|
|
14
19
|
_mcp_available = False
|
|
15
|
-
FastMCP = None
|
|
20
|
+
FastMCP = None # type: ignore[misc,assignment,no-redef]
|
|
16
21
|
|
|
17
22
|
MCP_AVAILABLE: Final[bool] = _mcp_available
|
|
18
23
|
|
|
@@ -37,6 +42,41 @@ from .tools import (
|
|
|
37
42
|
console = Console()
|
|
38
43
|
|
|
39
44
|
|
|
45
|
+
def _load_mcp_config(project_path: Path) -> dict[str, t.Any]:
|
|
46
|
+
pyproject_path = project_path / "pyproject.toml"
|
|
47
|
+
|
|
48
|
+
if not pyproject_path.exists() or not tomli:
|
|
49
|
+
return {
|
|
50
|
+
"http_port": 8676,
|
|
51
|
+
"http_host": "127.0.0.1",
|
|
52
|
+
"websocket_port": 8675,
|
|
53
|
+
"http_enabled": False,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with pyproject_path.open("rb") as f:
|
|
58
|
+
pyproject_data = tomli.load(f)
|
|
59
|
+
|
|
60
|
+
crackerjack_config = pyproject_data.get("tool", {}).get("crackerjack", {})
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"http_port": crackerjack_config.get("mcp_http_port", 8676),
|
|
64
|
+
"http_host": crackerjack_config.get("mcp_http_host", "127.0.0.1"),
|
|
65
|
+
"websocket_port": crackerjack_config.get("mcp_websocket_port", 8675),
|
|
66
|
+
"http_enabled": crackerjack_config.get("mcp_http_enabled", False),
|
|
67
|
+
}
|
|
68
|
+
except Exception as e:
|
|
69
|
+
console.print(
|
|
70
|
+
f"[yellow]Warning: Failed to load MCP config from pyproject.toml: {e}[/yellow]"
|
|
71
|
+
)
|
|
72
|
+
return {
|
|
73
|
+
"http_port": 8676,
|
|
74
|
+
"http_host": "127.0.0.1",
|
|
75
|
+
"websocket_port": 8675,
|
|
76
|
+
"http_enabled": False,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
40
80
|
class MCPOptions:
|
|
41
81
|
def __init__(self, **kwargs: t.Any) -> None:
|
|
42
82
|
self.commit: bool = False
|
|
@@ -77,11 +117,14 @@ async def _start_websocket_server() -> bool:
|
|
|
77
117
|
return False
|
|
78
118
|
|
|
79
119
|
|
|
80
|
-
def create_mcp_server() -> t.Any | None:
|
|
120
|
+
def create_mcp_server(config: dict[str, t.Any] | None = None) -> t.Any | None:
|
|
81
121
|
if not MCP_AVAILABLE or FastMCP is None:
|
|
82
122
|
return None
|
|
83
123
|
|
|
84
|
-
|
|
124
|
+
if config is None:
|
|
125
|
+
config = {"http_port": 8676, "http_host": "127.0.0.1"}
|
|
126
|
+
|
|
127
|
+
mcp_app = FastMCP("crackerjack-mcp-server", streamable_http_path="/mcp")
|
|
85
128
|
|
|
86
129
|
from crackerjack.slash_commands import get_slash_command_path
|
|
87
130
|
|
|
@@ -128,6 +171,8 @@ def handle_mcp_server_command(
|
|
|
128
171
|
stop: bool = False,
|
|
129
172
|
restart: bool = False,
|
|
130
173
|
websocket_port: int | None = None,
|
|
174
|
+
http_mode: bool = False,
|
|
175
|
+
http_port: int | None = None,
|
|
131
176
|
) -> None:
|
|
132
177
|
if stop or restart:
|
|
133
178
|
console.print("[yellow]Stopping MCP servers...[/ yellow]")
|
|
@@ -157,7 +202,7 @@ def handle_mcp_server_command(
|
|
|
157
202
|
if start or restart:
|
|
158
203
|
console.print("[green]Starting MCP server...[/ green]")
|
|
159
204
|
try:
|
|
160
|
-
main(".", websocket_port)
|
|
205
|
+
main(".", websocket_port, http_mode, http_port)
|
|
161
206
|
except Exception as e:
|
|
162
207
|
console.print(f"[red]Failed to start MCP server: {e}[/ red]")
|
|
163
208
|
|
|
@@ -177,45 +222,116 @@ def _stop_websocket_server() -> None:
|
|
|
177
222
|
pass
|
|
178
223
|
|
|
179
224
|
|
|
180
|
-
def
|
|
225
|
+
def _merge_config_with_args(
|
|
226
|
+
mcp_config: dict[str, t.Any],
|
|
227
|
+
http_port: int | None,
|
|
228
|
+
http_mode: bool,
|
|
229
|
+
) -> dict[str, t.Any]:
|
|
230
|
+
if http_port:
|
|
231
|
+
mcp_config["http_port"] = http_port
|
|
232
|
+
if http_mode:
|
|
233
|
+
mcp_config["http_enabled"] = True
|
|
234
|
+
return mcp_config
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _setup_server_context(
|
|
238
|
+
project_path: Path,
|
|
239
|
+
websocket_port: int | None,
|
|
240
|
+
) -> MCPServerContext:
|
|
241
|
+
config = MCPServerConfig(
|
|
242
|
+
project_path=project_path,
|
|
243
|
+
rate_limit_config=RateLimitConfig(),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
context = MCPServerContext(config)
|
|
247
|
+
context.console = console
|
|
248
|
+
|
|
249
|
+
if websocket_port:
|
|
250
|
+
context.websocket_server_port = websocket_port
|
|
251
|
+
|
|
252
|
+
_initialize_context(context)
|
|
253
|
+
return context
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _print_server_info(
|
|
257
|
+
project_path: Path,
|
|
258
|
+
mcp_config: dict[str, t.Any],
|
|
259
|
+
websocket_port: int | None,
|
|
260
|
+
http_mode: bool,
|
|
261
|
+
) -> None:
|
|
262
|
+
console.print("[green]Starting Crackerjack MCP Server...[/ green]")
|
|
263
|
+
console.print(f"Project path: {project_path}")
|
|
264
|
+
|
|
265
|
+
if mcp_config.get("http_enabled", False) or http_mode:
|
|
266
|
+
console.print(
|
|
267
|
+
f"[cyan]HTTP Mode: http: //{mcp_config['http_host']}: {mcp_config['http_port']}/mcp[/ cyan]"
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
console.print("[cyan]STDIO Mode[/ cyan]")
|
|
271
|
+
|
|
272
|
+
if websocket_port:
|
|
273
|
+
console.print(f"WebSocket port: {websocket_port}")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _run_mcp_server(
|
|
277
|
+
mcp_app: t.Any, mcp_config: dict[str, t.Any], http_mode: bool
|
|
278
|
+
) -> None:
|
|
279
|
+
console.print("[yellow]MCP app created, about to run...[/ yellow]")
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
if mcp_config.get("http_enabled", False) or http_mode:
|
|
283
|
+
host = mcp_config.get("http_host", "127.0.0.1")
|
|
284
|
+
port = mcp_config.get("http_port", 8676)
|
|
285
|
+
mcp_app.run(transport="streamable-http", host=host, port=port)
|
|
286
|
+
else:
|
|
287
|
+
mcp_app.run()
|
|
288
|
+
except Exception as e:
|
|
289
|
+
console.print(f"[red]MCP run failed: {e}[/ red]")
|
|
290
|
+
import traceback
|
|
291
|
+
|
|
292
|
+
traceback.print_exc()
|
|
293
|
+
raise
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def main(
|
|
297
|
+
project_path_arg: str = ".",
|
|
298
|
+
websocket_port: int | None = None,
|
|
299
|
+
http_mode: bool = False,
|
|
300
|
+
http_port: int | None = None,
|
|
301
|
+
) -> None:
|
|
181
302
|
if not MCP_AVAILABLE:
|
|
182
303
|
return
|
|
183
304
|
|
|
184
305
|
try:
|
|
185
306
|
project_path = Path(project_path_arg).resolve()
|
|
186
307
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
rate_limit_config=RateLimitConfig(),
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
context = MCPServerContext(config)
|
|
193
|
-
context.console = console
|
|
194
|
-
|
|
195
|
-
if websocket_port:
|
|
196
|
-
context.websocket_server_port = websocket_port
|
|
308
|
+
mcp_config = _load_mcp_config(project_path)
|
|
309
|
+
mcp_config = _merge_config_with_args(mcp_config, http_port, http_mode)
|
|
197
310
|
|
|
198
|
-
|
|
311
|
+
_setup_server_context(project_path, websocket_port)
|
|
199
312
|
|
|
200
|
-
mcp_app = create_mcp_server()
|
|
313
|
+
mcp_app = create_mcp_server(mcp_config)
|
|
201
314
|
if not mcp_app:
|
|
202
315
|
console.print("[red]Failed to create MCP server[/ red]")
|
|
203
316
|
return
|
|
204
317
|
|
|
205
|
-
|
|
206
|
-
|
|
318
|
+
_print_server_info(project_path, mcp_config, websocket_port, http_mode)
|
|
319
|
+
|
|
320
|
+
# Auto-start WebSocket server if websocket_port is specified
|
|
207
321
|
if websocket_port:
|
|
208
|
-
|
|
322
|
+
import asyncio
|
|
209
323
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
324
|
+
try:
|
|
325
|
+
asyncio.run(_start_websocket_server())
|
|
326
|
+
console.print(
|
|
327
|
+
f"[green]✅ WebSocket server auto-started on port {websocket_port}[/green]"
|
|
328
|
+
)
|
|
329
|
+
except Exception as e:
|
|
330
|
+
console.print(
|
|
331
|
+
f"[yellow]⚠️ WebSocket server auto-start failed: {e}[/yellow]"
|
|
332
|
+
)
|
|
216
333
|
|
|
217
|
-
|
|
218
|
-
raise
|
|
334
|
+
_run_mcp_server(mcp_app, mcp_config, http_mode)
|
|
219
335
|
|
|
220
336
|
except KeyboardInterrupt:
|
|
221
337
|
console.print("Server stopped by user")
|
|
@@ -232,7 +348,20 @@ def main(project_path_arg: str = ".", websocket_port: int | None = None) -> None
|
|
|
232
348
|
if __name__ == "__main__":
|
|
233
349
|
import sys
|
|
234
350
|
|
|
235
|
-
project_path =
|
|
236
|
-
websocket_port =
|
|
351
|
+
project_path = "."
|
|
352
|
+
websocket_port = None
|
|
353
|
+
http_mode = "--http" in sys.argv
|
|
354
|
+
http_port = None
|
|
355
|
+
|
|
356
|
+
non_flag_args = [arg for arg in sys.argv[1:] if not arg.startswith("--")]
|
|
357
|
+
if non_flag_args:
|
|
358
|
+
project_path = non_flag_args[0]
|
|
359
|
+
if len(non_flag_args) > 1 and non_flag_args[1].isdigit():
|
|
360
|
+
websocket_port = int(non_flag_args[1])
|
|
361
|
+
|
|
362
|
+
if "--http-port" in sys.argv:
|
|
363
|
+
port_idx = sys.argv.index("--http-port")
|
|
364
|
+
if port_idx + 1 < len(sys.argv):
|
|
365
|
+
http_port = int(sys.argv[port_idx + 1])
|
|
237
366
|
|
|
238
|
-
main(project_path, websocket_port)
|
|
367
|
+
main(project_path, websocket_port, http_mode, http_port)
|
|
@@ -34,11 +34,12 @@ class ServiceConfig:
|
|
|
34
34
|
self.max_restarts = max_restarts
|
|
35
35
|
self.restart_window = restart_window
|
|
36
36
|
|
|
37
|
-
self.process: subprocess.Popen | None = None
|
|
37
|
+
self.process: subprocess.Popen[bytes] | None = None
|
|
38
38
|
self.restart_count = 0
|
|
39
39
|
self.restart_timestamps: list[float] = []
|
|
40
40
|
self.last_health_check = 0.0
|
|
41
41
|
self.is_healthy = False
|
|
42
|
+
self._port_acknowledged = False
|
|
42
43
|
self.last_error: str | None = None
|
|
43
44
|
|
|
44
45
|
|