crackerjack 0.33.0__py3-none-any.whl → 0.33.2__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 +4 -13
- 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 +104 -204
- 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 +171 -174
- 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 +44 -8
- crackerjack/managers/test_command_builder.py +1 -15
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +98 -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 +17 -16
- 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 +173 -32
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +8 -10
- 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 +0 -2
- crackerjack/mixins/error_handling.py +1 -70
- crackerjack/models/config.py +12 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +122 -122
- crackerjack/models/resource_protocols.py +55 -210
- 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 +6 -35
- 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 +15 -9
- crackerjack/services/config_merge.py +19 -80
- 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 +27 -25
- 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 +8 -25
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +11 -30
- 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 +19 -87
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +9 -67
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +18 -59
- crackerjack/services/performance_cache.py +20 -81
- crackerjack/services/performance_monitor.py +27 -95
- 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 +618 -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 +9 -41
- crackerjack/services/security_logger.py +12 -24
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- 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.33.0.dist-info → crackerjack-0.33.2.dist-info}/METADATA +196 -25
- crackerjack-0.33.2.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.33.0.dist-info/RECORD +0 -187
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/WHEEL +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/licenses/LICENSE +0 -0
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
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
|
+
import typing as t
|
|
3
4
|
from contextlib import suppress
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from pathlib import Path
|
|
@@ -29,7 +30,7 @@ class MetricCard(Widget):
|
|
|
29
30
|
value: str = " --",
|
|
30
31
|
trend: str = "",
|
|
31
32
|
color: str = "white",
|
|
32
|
-
**kwargs,
|
|
33
|
+
**kwargs: t.Any,
|
|
33
34
|
) -> None:
|
|
34
35
|
super().__init__(**kwargs)
|
|
35
36
|
self.label = label
|
|
@@ -43,7 +44,7 @@ class MetricCard(Widget):
|
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class AgentActivityWidget(Widget):
|
|
46
|
-
def __init__(self, **kwargs) -> None:
|
|
47
|
+
def __init__(self, **kwargs: t.Any) -> None:
|
|
47
48
|
super().__init__(**kwargs)
|
|
48
49
|
self.border_title = "🤖 AI Agent Activity"
|
|
49
50
|
self.border_title_align = "left"
|
|
@@ -93,7 +94,7 @@ class AgentActivityWidget(Widget):
|
|
|
93
94
|
table.zebra_stripes = True
|
|
94
95
|
table.styles.max_height = 6
|
|
95
96
|
|
|
96
|
-
def update_metrics(self, data: dict) -> None:
|
|
97
|
+
def update_metrics(self, data: dict[str, t.Any]) -> None:
|
|
97
98
|
with suppress(Exception):
|
|
98
99
|
activity = data.get("agent_activity", {})
|
|
99
100
|
activity.get("agent_registry", {})
|
|
@@ -116,14 +117,14 @@ class AgentActivityWidget(Widget):
|
|
|
116
117
|
self.query_one(
|
|
117
118
|
"#confidence-metric",
|
|
118
119
|
MetricCard,
|
|
119
|
-
).value = f"{avg_confidence
|
|
120
|
+
).value = f"{avg_confidence: .0%}"
|
|
120
121
|
self.query_one("#cache-hits-metric", MetricCard).value = str(cache_hits)
|
|
121
122
|
|
|
122
123
|
self._update_coordinator_status(activity)
|
|
123
124
|
|
|
124
125
|
self._update_agent_table(active_agents)
|
|
125
126
|
|
|
126
|
-
def _update_coordinator_status(self, activity: dict) -> None:
|
|
127
|
+
def _update_coordinator_status(self, activity: dict[str, t.Any]) -> None:
|
|
127
128
|
status = activity.get("coordinator_status", "idle")
|
|
128
129
|
total_agents = activity.get("agent_registry", {}).get("total_agents", 0)
|
|
129
130
|
|
|
@@ -135,7 +136,7 @@ class AgentActivityWidget(Widget):
|
|
|
135
136
|
f"{icon} Coordinator: {status.title()} ({total_agents} agents available)",
|
|
136
137
|
)
|
|
137
138
|
|
|
138
|
-
def _update_agent_table(self, agents: list) -> None:
|
|
139
|
+
def _update_agent_table(self, agents: list[t.Any]) -> None:
|
|
139
140
|
table = self.query_one("#agents-detail-table", DataTable)
|
|
140
141
|
table.clear()
|
|
141
142
|
|
|
@@ -147,8 +148,8 @@ class AgentActivityWidget(Widget):
|
|
|
147
148
|
name = agent.get("agent_type", "Unknown")
|
|
148
149
|
status = agent.get("status", "idle")
|
|
149
150
|
issue_type = agent.get("issue_type", "-")
|
|
150
|
-
confidence = f"{agent.get('confidence', 0)
|
|
151
|
-
time_elapsed = f"{agent.get('processing_time', 0)
|
|
151
|
+
confidence = f"{agent.get('confidence', 0): .0%}"
|
|
152
|
+
time_elapsed = f"{agent.get('processing_time', 0): .1f}s"
|
|
152
153
|
|
|
153
154
|
status_emoji = {
|
|
154
155
|
"processing": "🔄",
|
|
@@ -176,7 +177,7 @@ class AgentActivityWidget(Widget):
|
|
|
176
177
|
|
|
177
178
|
|
|
178
179
|
class JobProgressPanel(Widget):
|
|
179
|
-
def __init__(self, job_data: dict, **kwargs) -> None:
|
|
180
|
+
def __init__(self, job_data: dict[str, t.Any], **kwargs) -> None:
|
|
180
181
|
super().__init__(**kwargs)
|
|
181
182
|
self.job_data = job_data
|
|
182
183
|
self.start_time = time.time()
|
|
@@ -198,10 +199,12 @@ class JobProgressPanel(Widget):
|
|
|
198
199
|
|
|
199
200
|
with Horizontal():
|
|
200
201
|
with Vertical(id="job-progress-section"):
|
|
201
|
-
|
|
202
|
+
for widget in self._compose_progress_section():
|
|
203
|
+
yield widget
|
|
202
204
|
|
|
203
205
|
with Vertical(id="job-metrics-section"):
|
|
204
|
-
|
|
206
|
+
for widget in self._compose_metrics_section():
|
|
207
|
+
yield widget
|
|
205
208
|
|
|
206
209
|
def _compose_progress_section(self) -> ComposeResult:
|
|
207
210
|
iteration = self.job_data.get("iteration", 1)
|
|
@@ -247,16 +250,16 @@ class JobProgressPanel(Widget):
|
|
|
247
250
|
if total_issues > 0:
|
|
248
251
|
success_rate = (fixed / total_issues) * 100
|
|
249
252
|
yield Label(
|
|
250
|
-
f"Success Rate: {success_rate
|
|
253
|
+
f"Success Rate: {success_rate: .1f}%",
|
|
251
254
|
classes="success-rate",
|
|
252
255
|
)
|
|
253
256
|
|
|
254
257
|
def _format_time(self, seconds: float) -> str:
|
|
255
258
|
if seconds < 60:
|
|
256
|
-
return f"{seconds
|
|
259
|
+
return f"{seconds: .0f}s"
|
|
257
260
|
if seconds < 3600:
|
|
258
|
-
return f"{seconds / 60
|
|
259
|
-
return f"{seconds / 3600
|
|
261
|
+
return f"{seconds / 60: .0f}m {seconds % 60: .0f}s"
|
|
262
|
+
return f"{seconds / 3600: .0f}h {(seconds % 3600) / 60: .0f}m"
|
|
260
263
|
|
|
261
264
|
|
|
262
265
|
class ServiceHealthPanel(Widget):
|
|
@@ -279,7 +282,7 @@ class ServiceHealthPanel(Widget):
|
|
|
279
282
|
)
|
|
280
283
|
table.zebra_stripes = True
|
|
281
284
|
|
|
282
|
-
def update_services(self, services: list[dict]) -> None:
|
|
285
|
+
def update_services(self, services: list[dict[str, t.Any]]) -> None:
|
|
283
286
|
table = self.query_one("#services-table", DataTable)
|
|
284
287
|
table.clear()
|
|
285
288
|
|
|
@@ -308,7 +311,7 @@ class ServiceHealthPanel(Widget):
|
|
|
308
311
|
|
|
309
312
|
if isinstance(last_check, int | float):
|
|
310
313
|
last_check_str = datetime.fromtimestamp(last_check).strftime(
|
|
311
|
-
"%H
|
|
314
|
+
"%H: %M: %S",
|
|
312
315
|
)
|
|
313
316
|
else:
|
|
314
317
|
last_check_str = str(last_check)
|
|
@@ -323,12 +326,12 @@ class ServiceHealthPanel(Widget):
|
|
|
323
326
|
|
|
324
327
|
def _format_uptime(self, seconds: float) -> str:
|
|
325
328
|
if seconds < 60:
|
|
326
|
-
return f"{seconds
|
|
329
|
+
return f"{seconds: .0f}s"
|
|
327
330
|
if seconds < 3600:
|
|
328
|
-
return f"{seconds / 60
|
|
331
|
+
return f"{seconds / 60: .0f}m"
|
|
329
332
|
if seconds < 86400:
|
|
330
|
-
return f"{seconds / 3600
|
|
331
|
-
return f"{seconds / 86400
|
|
333
|
+
return f"{seconds / 3600: .1f}h"
|
|
334
|
+
return f"{seconds / 86400: .1f}d"
|
|
332
335
|
|
|
333
336
|
|
|
334
337
|
class EnhancedCrackerjackDashboard(App):
|
|
@@ -336,7 +339,7 @@ class EnhancedCrackerjackDashboard(App):
|
|
|
336
339
|
CSS_PATH = Path(__file__).parent / "enhanced_progress_monitor.tcss"
|
|
337
340
|
|
|
338
341
|
def __init__(
|
|
339
|
-
self, progress_dir: Path, websocket_url: str = "ws
|
|
342
|
+
self, progress_dir: Path, websocket_url: str = "ws: //localhost: 8675"
|
|
340
343
|
) -> None:
|
|
341
344
|
super().__init__()
|
|
342
345
|
self.progress_dir = progress_dir
|
|
@@ -367,7 +370,7 @@ class EnhancedCrackerjackDashboard(App):
|
|
|
367
370
|
jobs_result = await self.data_collector.discover_jobs()
|
|
368
371
|
jobs_data = jobs_result.get("data", {})
|
|
369
372
|
|
|
370
|
-
services = self.service_manager.
|
|
373
|
+
services = self.service_manager.collect_services_data()
|
|
371
374
|
self.query_one("#service-panel", ServiceHealthPanel).update_services(
|
|
372
375
|
services,
|
|
373
376
|
)
|
|
@@ -385,8 +388,8 @@ class EnhancedCrackerjackDashboard(App):
|
|
|
385
388
|
except Exception as e:
|
|
386
389
|
self.console.print(f"[red]Dashboard update error: {e}[/]")
|
|
387
390
|
|
|
388
|
-
def _aggregate_agent_data(self, jobs: list[dict]) -> dict:
|
|
389
|
-
aggregated = {
|
|
391
|
+
def _aggregate_agent_data(self, jobs: list[dict[str, t.Any]]) -> dict[str, t.Any]:
|
|
392
|
+
aggregated: dict[str, dict[str, t.Any]] = {
|
|
390
393
|
"agent_activity": {
|
|
391
394
|
"active_agents": [],
|
|
392
395
|
"coordinator_status": "idle",
|
|
@@ -418,7 +421,7 @@ class EnhancedCrackerjackDashboard(App):
|
|
|
418
421
|
|
|
419
422
|
return aggregated
|
|
420
423
|
|
|
421
|
-
def _update_job_panels(self, jobs: list[dict]) -> None:
|
|
424
|
+
def _update_job_panels(self, jobs: list[dict[str, t.Any]]) -> None:
|
|
422
425
|
container = self.query_one("#jobs-container", Container)
|
|
423
426
|
|
|
424
427
|
with suppress(Exception):
|
|
@@ -445,7 +448,7 @@ class EnhancedCrackerjackDashboard(App):
|
|
|
445
448
|
|
|
446
449
|
async def run_enhanced_progress_monitor(
|
|
447
450
|
progress_dir: Path | None = None,
|
|
448
|
-
websocket_url: str = "ws
|
|
451
|
+
websocket_url: str = "ws: //localhost: 8675",
|
|
449
452
|
dev_mode: bool = False,
|
|
450
453
|
) -> None:
|
|
451
454
|
if progress_dir is None:
|
|
@@ -474,6 +477,6 @@ if __name__ == "__main__":
|
|
|
474
477
|
import tempfile
|
|
475
478
|
|
|
476
479
|
progress_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else None
|
|
477
|
-
websocket_url = sys.argv[2] if len(sys.argv) > 2 else "ws
|
|
480
|
+
websocket_url = sys.argv[2] if len(sys.argv) > 2 else "ws: //localhost: 8675"
|
|
478
481
|
|
|
479
482
|
asyncio.run(run_enhanced_progress_monitor(progress_dir, websocket_url))
|