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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (156) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +227 -299
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +170 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +657 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +409 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +618 -928
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +585 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +826 -0
  40. crackerjack/dynamic_config.py +94 -103
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +433 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +443 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +114 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +621 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +372 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +217 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +565 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/coverage_improvement.py +223 -0
  107. crackerjack/orchestration/execution_strategies.py +341 -0
  108. crackerjack/orchestration/test_progress_streamer.py +636 -0
  109. crackerjack/plugins/__init__.py +15 -0
  110. crackerjack/plugins/base.py +200 -0
  111. crackerjack/plugins/hooks.py +246 -0
  112. crackerjack/plugins/loader.py +335 -0
  113. crackerjack/plugins/managers.py +259 -0
  114. crackerjack/py313.py +8 -3
  115. crackerjack/services/__init__.py +22 -0
  116. crackerjack/services/cache.py +314 -0
  117. crackerjack/services/config.py +358 -0
  118. crackerjack/services/config_integrity.py +99 -0
  119. crackerjack/services/contextual_ai_assistant.py +516 -0
  120. crackerjack/services/coverage_ratchet.py +356 -0
  121. crackerjack/services/debug.py +736 -0
  122. crackerjack/services/dependency_monitor.py +617 -0
  123. crackerjack/services/enhanced_filesystem.py +439 -0
  124. crackerjack/services/file_hasher.py +151 -0
  125. crackerjack/services/filesystem.py +421 -0
  126. crackerjack/services/git.py +176 -0
  127. crackerjack/services/health_metrics.py +611 -0
  128. crackerjack/services/initialization.py +873 -0
  129. crackerjack/services/log_manager.py +286 -0
  130. crackerjack/services/logging.py +174 -0
  131. crackerjack/services/metrics.py +578 -0
  132. crackerjack/services/pattern_cache.py +362 -0
  133. crackerjack/services/pattern_detector.py +515 -0
  134. crackerjack/services/performance_benchmarks.py +653 -0
  135. crackerjack/services/security.py +163 -0
  136. crackerjack/services/server_manager.py +234 -0
  137. crackerjack/services/smart_scheduling.py +144 -0
  138. crackerjack/services/tool_version_service.py +61 -0
  139. crackerjack/services/unified_config.py +437 -0
  140. crackerjack/services/version_checker.py +248 -0
  141. crackerjack/slash_commands/__init__.py +14 -0
  142. crackerjack/slash_commands/init.md +122 -0
  143. crackerjack/slash_commands/run.md +163 -0
  144. crackerjack/slash_commands/status.md +127 -0
  145. crackerjack-0.31.7.dist-info/METADATA +742 -0
  146. crackerjack-0.31.7.dist-info/RECORD +149 -0
  147. crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
  148. crackerjack/.gitignore +0 -34
  149. crackerjack/.libcst.codemod.yaml +0 -18
  150. crackerjack/.pdm.toml +0 -1
  151. crackerjack/crackerjack.py +0 -3805
  152. crackerjack/pyproject.toml +0 -286
  153. crackerjack-0.30.3.dist-info/METADATA +0 -1290
  154. crackerjack-0.30.3.dist-info/RECORD +0 -16
  155. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
  156. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,621 @@
1
+ import asyncio
2
+ import contextlib
3
+ import io
4
+ import os
5
+ import subprocess
6
+ import tempfile
7
+ import time
8
+ import typing as t
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from types import TracebackType
12
+
13
+ from rich.console import Console
14
+
15
+ from crackerjack.core.workflow_orchestrator import WorkflowOrchestrator
16
+
17
+ from .cache import ErrorCache
18
+ from .rate_limiter import RateLimitConfig, RateLimitMiddleware
19
+ from .state import StateManager
20
+
21
+
22
+ class BatchedStateSaver:
23
+ def __init__(self, debounce_delay: float = 1.0, max_batch_size: int = 10) -> None:
24
+ self.debounce_delay = debounce_delay
25
+ self.max_batch_size = max_batch_size
26
+
27
+ self._pending_saves: dict[str, t.Callable[[], None]] = {}
28
+ self._last_save_time: dict[str, float] = {}
29
+
30
+ self._save_task: asyncio.Task | None = None
31
+ self._running = False
32
+ self._lock = asyncio.Lock()
33
+
34
+ async def start(self) -> None:
35
+ if self._running:
36
+ return
37
+
38
+ self._running = True
39
+ self._save_task = asyncio.create_task(self._save_loop())
40
+
41
+ async def stop(self) -> None:
42
+ self._running = False
43
+
44
+ if self._save_task:
45
+ self._save_task.cancel()
46
+ with contextlib.suppress(asyncio.CancelledError):
47
+ await self._save_task
48
+
49
+ await self._flush_saves()
50
+
51
+ async def schedule_save(
52
+ self,
53
+ save_id: str,
54
+ save_func: t.Callable[[], None],
55
+ ) -> None:
56
+ async with self._lock:
57
+ self._pending_saves[save_id] = save_func
58
+ self._last_save_time[save_id] = time.time()
59
+
60
+ if len(self._pending_saves) >= self.max_batch_size:
61
+ await self._flush_saves()
62
+
63
+ async def _save_loop(self) -> None:
64
+ while self._running:
65
+ try:
66
+ await asyncio.sleep(self.debounce_delay)
67
+ ready_saves = await self._get_ready_saves()
68
+
69
+ if ready_saves:
70
+ await self._execute_saves(ready_saves)
71
+
72
+ except asyncio.CancelledError:
73
+ break
74
+ except Exception:
75
+ await asyncio.sleep(1)
76
+
77
+ async def _get_ready_saves(self) -> list[str]:
78
+ now = time.time()
79
+ ready_saves = []
80
+
81
+ async with self._lock:
82
+ for save_id, last_time in list(self._last_save_time.items()):
83
+ if now - last_time >= self.debounce_delay:
84
+ ready_saves.append(save_id)
85
+
86
+ return ready_saves
87
+
88
+ async def _execute_saves(self, save_ids: list[str]) -> None:
89
+ async with self._lock:
90
+ saves_to_execute = []
91
+
92
+ for save_id in save_ids:
93
+ if save_id in self._pending_saves:
94
+ saves_to_execute.append((save_id, self._pending_saves.pop(save_id)))
95
+ self._last_save_time.pop(save_id, None)
96
+
97
+ for save_id, save_func in saves_to_execute:
98
+ with contextlib.suppress(Exception):
99
+ save_func()
100
+
101
+ async def _flush_saves(self) -> None:
102
+ async with self._lock:
103
+ save_ids = list(self._pending_saves.keys())
104
+
105
+ if save_ids:
106
+ await self._execute_saves(save_ids)
107
+
108
+ def get_stats(self) -> dict[str, t.Any]:
109
+ return {
110
+ "running": self._running,
111
+ "pending_saves": len(self._pending_saves),
112
+ "debounce_delay": self.debounce_delay,
113
+ "max_batch_size": self.max_batch_size,
114
+ }
115
+
116
+
117
+ @dataclass
118
+ class MCPServerConfig:
119
+ project_path: Path
120
+ progress_dir: Path | None = None
121
+ rate_limit_config: RateLimitConfig | None = None
122
+ stdio_mode: bool = True
123
+ state_dir: Path | None = None
124
+ cache_dir: Path | None = None
125
+
126
+
127
+ class MCPServerContext:
128
+ def __init__(self, config: MCPServerConfig) -> None:
129
+ self.config = config
130
+
131
+ self.console: Console | None = None
132
+ self.cli_runner = None
133
+ self.state_manager: StateManager | None = None
134
+ self.error_cache: ErrorCache | None = None
135
+ self.rate_limiter: RateLimitMiddleware | None = None
136
+ self.batched_saver: BatchedStateSaver = BatchedStateSaver()
137
+
138
+ self.progress_dir = config.progress_dir or (
139
+ Path(tempfile.gettempdir()) / "crackerjack-mcp-progress"
140
+ )
141
+ self.progress_queue: asyncio.Queue[dict[str, t.Any]] = asyncio.Queue(
142
+ maxsize=1000,
143
+ )
144
+
145
+ self.websocket_server_process: subprocess.Popen[bytes] | None = None
146
+ self.websocket_server_port: int = int(
147
+ os.environ.get("CRACKERJACK_WEBSOCKET_PORT", "8675"),
148
+ )
149
+ self._websocket_process_lock = asyncio.Lock()
150
+ self._websocket_cleanup_registered = False
151
+ self._websocket_health_check_task: asyncio.Task | None = None
152
+
153
+ self._initialized = False
154
+ self._startup_tasks: list[t.Callable[[], t.Awaitable[None]]] = []
155
+ self._shutdown_tasks: list[t.Callable[[], t.Awaitable[None]]] = []
156
+
157
+ async def initialize(self) -> None:
158
+ if self._initialized:
159
+ return
160
+
161
+ try:
162
+ if self.config.stdio_mode:
163
+ null_file = io.StringIO()
164
+ self.console = Console(file=null_file, force_terminal=False)
165
+ else:
166
+ self.console = Console(force_terminal=True)
167
+
168
+ self.progress_dir.mkdir(exist_ok=True)
169
+
170
+ self.cli_runner = WorkflowOrchestrator(
171
+ console=self.console,
172
+ pkg_path=self.config.project_path,
173
+ )
174
+
175
+ self.state_manager = StateManager(
176
+ self.config.state_dir or Path.home() / ".cache" / "crackerjack-mcp",
177
+ self.batched_saver,
178
+ )
179
+
180
+ self.error_cache = ErrorCache(
181
+ self.config.cache_dir or Path.home() / ".cache" / "crackerjack-mcp",
182
+ )
183
+
184
+ self.rate_limiter = RateLimitMiddleware(self.config.rate_limit_config)
185
+
186
+ await self.batched_saver.start()
187
+
188
+ for task in self._startup_tasks:
189
+ await task()
190
+
191
+ self._initialized = True
192
+
193
+ except Exception as e:
194
+ self.cli_runner = None
195
+ self.state_manager = None
196
+ self.error_cache = None
197
+ self.rate_limiter = None
198
+ msg = f"Failed to initialize MCP server context: {e}"
199
+ raise RuntimeError(msg) from e
200
+
201
+ async def shutdown(self) -> None:
202
+ if not self._initialized:
203
+ return
204
+
205
+ for task in reversed(self._shutdown_tasks):
206
+ try:
207
+ await task()
208
+ except Exception as e:
209
+ if self.console:
210
+ self.console.print(f"[red]Error during shutdown: {e}[/red]")
211
+
212
+ if self._websocket_health_check_task:
213
+ self._websocket_health_check_task.cancel()
214
+ with contextlib.suppress(asyncio.CancelledError):
215
+ await self._websocket_health_check_task
216
+ self._websocket_health_check_task = None
217
+
218
+ await self._stop_websocket_server()
219
+
220
+ if self.rate_limiter:
221
+ await self.rate_limiter.stop()
222
+
223
+ await self.batched_saver.stop()
224
+
225
+ self._initialized = False
226
+
227
+ def add_startup_task(self, task: t.Callable[[], t.Awaitable[None]]) -> None:
228
+ self._startup_tasks.append(task)
229
+
230
+ def add_shutdown_task(self, task: t.Callable[[], t.Awaitable[None]]) -> None:
231
+ self._shutdown_tasks.append(task)
232
+
233
+ def validate_job_id(self, job_id: str) -> bool:
234
+ if not job_id:
235
+ return False
236
+
237
+ import uuid
238
+ from contextlib import suppress
239
+
240
+ with suppress(ValueError):
241
+ uuid.UUID(job_id)
242
+ return True
243
+
244
+ import re
245
+
246
+ if not re.match(r"^[a-zA-Z0-9_-]+$", job_id):
247
+ return False
248
+
249
+ if ".." in job_id or "/" in job_id or "\\" in job_id:
250
+ return False
251
+
252
+ import os
253
+
254
+ return os.path.basename(job_id) == job_id
255
+
256
+ async def check_websocket_server_running(self) -> bool:
257
+ import socket
258
+
259
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
260
+ sock.settimeout(1.0)
261
+ result = sock.connect_ex(("localhost", self.websocket_server_port))
262
+ return result == 0
263
+
264
+ async def start_websocket_server(self) -> bool:
265
+ async with self._websocket_process_lock:
266
+ if await self._check_existing_websocket_server():
267
+ return True
268
+
269
+ if self.console:
270
+ self.console.print(
271
+ f"🚀 Starting WebSocket server on localhost:{self.websocket_server_port}...",
272
+ )
273
+
274
+ try:
275
+ await self._spawn_websocket_process()
276
+ await self._register_websocket_cleanup()
277
+ return await self._wait_for_websocket_startup()
278
+
279
+ except Exception as e:
280
+ if self.console:
281
+ self.console.print(f"❌ Failed to start WebSocket server: {e}")
282
+ await self._cleanup_dead_websocket_process()
283
+ return False
284
+
285
+ async def _check_existing_websocket_server(self) -> bool:
286
+ if (
287
+ self.websocket_server_process
288
+ and self.websocket_server_process.poll() is None
289
+ ):
290
+ if await self.check_websocket_server_running():
291
+ if self.console:
292
+ self.console.print(
293
+ f"✅ WebSocket server already running on port {self.websocket_server_port}",
294
+ )
295
+ return True
296
+ await self._cleanup_dead_websocket_process()
297
+
298
+ if await self.check_websocket_server_running():
299
+ if self.console:
300
+ self.console.print(
301
+ f"⚠️ Port {self.websocket_server_port} already in use by another process",
302
+ )
303
+ return True
304
+
305
+ return False
306
+
307
+ async def _spawn_websocket_process(self) -> None:
308
+ import sys
309
+
310
+ self.websocket_server_process = subprocess.Popen(
311
+ [
312
+ sys.executable,
313
+ "-m",
314
+ "crackerjack",
315
+ "--start-websocket-server",
316
+ "--websocket-port",
317
+ str(self.websocket_server_port),
318
+ ],
319
+ stdout=subprocess.DEVNULL,
320
+ stderr=subprocess.DEVNULL,
321
+ start_new_session=True,
322
+ )
323
+
324
+ async def _register_websocket_cleanup(self) -> None:
325
+ if not self._websocket_cleanup_registered:
326
+ self.add_shutdown_task(self._stop_websocket_server)
327
+ self._websocket_cleanup_registered = True
328
+
329
+ if not self._websocket_health_check_task:
330
+ self._websocket_health_check_task = asyncio.create_task(
331
+ self._websocket_health_monitor(),
332
+ )
333
+
334
+ async def _wait_for_websocket_startup(self) -> bool:
335
+ max_attempts = 10
336
+ for _attempt in range(max_attempts):
337
+ await asyncio.sleep(0.5)
338
+
339
+ if self.websocket_server_process.poll() is not None:
340
+ return_code = self.websocket_server_process.returncode
341
+ if self.console:
342
+ self.console.print(
343
+ f"❌ WebSocket server process died during startup (exit code: {return_code})",
344
+ )
345
+ self.websocket_server_process = None
346
+ return False
347
+
348
+ if await self.check_websocket_server_running():
349
+ if self.console:
350
+ self.console.print(
351
+ f"✅ WebSocket server started successfully on port {self.websocket_server_port}",
352
+ )
353
+ self.console.print(
354
+ f"📊 Progress available at: ws://localhost:{self.websocket_server_port}/ws/progress/{{job_id}}",
355
+ )
356
+ return True
357
+
358
+ if self.console:
359
+ self.console.print(
360
+ f"❌ WebSocket server failed to start within {max_attempts * 0.5}s",
361
+ )
362
+ await self._cleanup_dead_websocket_process()
363
+ return False
364
+
365
+ async def _cleanup_dead_websocket_process(self) -> None:
366
+ if self.websocket_server_process:
367
+ try:
368
+ if self.websocket_server_process.poll() is None:
369
+ self.websocket_server_process.terminate()
370
+ try:
371
+ self.websocket_server_process.wait(timeout=2)
372
+ except subprocess.TimeoutExpired:
373
+ self.websocket_server_process.kill()
374
+ self.websocket_server_process.wait(timeout=1)
375
+
376
+ if self.console:
377
+ self.console.print("🧹 Cleaned up dead WebSocket server process")
378
+ except Exception as e:
379
+ if self.console:
380
+ self.console.print(f"⚠️ Error cleaning up WebSocket process: {e}")
381
+ finally:
382
+ self.websocket_server_process = None
383
+
384
+ async def _stop_websocket_server(self) -> None:
385
+ async with self._websocket_process_lock:
386
+ if not self.websocket_server_process:
387
+ return
388
+
389
+ try:
390
+ if self.websocket_server_process.poll() is None:
391
+ await self._terminate_live_websocket_process()
392
+ else:
393
+ await self._handle_dead_websocket_process_cleanup()
394
+
395
+ except Exception as e:
396
+ if self.console:
397
+ self.console.print(f"⚠️ Error stopping WebSocket server: {e}")
398
+ finally:
399
+ self.websocket_server_process = None
400
+
401
+ async def _terminate_live_websocket_process(self) -> None:
402
+ if self.console:
403
+ self.console.print("🛑 Stopping WebSocket server...")
404
+
405
+ self.websocket_server_process.terminate()
406
+
407
+ if await self._wait_for_graceful_termination():
408
+ return
409
+
410
+ await self._force_kill_websocket_process()
411
+
412
+ async def _wait_for_graceful_termination(self) -> bool:
413
+ try:
414
+ self.websocket_server_process.wait(timeout=5)
415
+ if self.console:
416
+ self.console.print("✅ WebSocket server stopped gracefully")
417
+ return True
418
+ except subprocess.TimeoutExpired:
419
+ return False
420
+
421
+ async def _force_kill_websocket_process(self) -> None:
422
+ if self.console:
423
+ self.console.print("⚡ Force killing unresponsive WebSocket server...")
424
+
425
+ self.websocket_server_process.kill()
426
+
427
+ try:
428
+ self.websocket_server_process.wait(timeout=2)
429
+ if self.console:
430
+ self.console.print("💀 WebSocket server force killed")
431
+ except subprocess.TimeoutExpired:
432
+ if self.console:
433
+ self.console.print("⚠️ WebSocket server process may be zombified")
434
+
435
+ async def _handle_dead_websocket_process_cleanup(self) -> None:
436
+ if self.console:
437
+ self.console.print("💀 WebSocket server process was already dead")
438
+
439
+ async def get_websocket_server_status(self) -> dict[str, t.Any]:
440
+ async with self._websocket_process_lock:
441
+ status = {
442
+ "port": self.websocket_server_port,
443
+ "process_exists": self.websocket_server_process is not None,
444
+ "process_alive": False,
445
+ "server_responding": False,
446
+ "process_id": None,
447
+ "return_code": None,
448
+ }
449
+
450
+ if self.websocket_server_process:
451
+ status["process_id"] = self.websocket_server_process.pid
452
+ poll_result = self.websocket_server_process.poll()
453
+ status["process_alive"] = poll_result is None
454
+ if poll_result is not None:
455
+ status["return_code"] = poll_result
456
+
457
+ status["server_responding"] = await self.check_websocket_server_running()
458
+
459
+ return status
460
+
461
+ async def _websocket_health_monitor(self) -> None:
462
+ while True:
463
+ try:
464
+ await asyncio.sleep(30)
465
+ await self._check_and_restart_websocket()
466
+ except asyncio.CancelledError:
467
+ break
468
+ except Exception as e:
469
+ if self.console:
470
+ self.console.print(f"⚠️ Error in WebSocket health monitor: {e}")
471
+ await asyncio.sleep(60)
472
+
473
+ async def _check_and_restart_websocket(self) -> None:
474
+ async with self._websocket_process_lock:
475
+ if not self.websocket_server_process:
476
+ return
477
+
478
+ if self.websocket_server_process.poll() is not None:
479
+ await self._handle_dead_websocket_process()
480
+ return
481
+
482
+ if not await self.check_websocket_server_running():
483
+ await self._handle_unresponsive_websocket_server()
484
+
485
+ async def _handle_dead_websocket_process(self) -> None:
486
+ return_code = self.websocket_server_process.returncode
487
+ if self.console:
488
+ self.console.print(
489
+ f"⚠️ WebSocket server process died (exit code: {return_code}), attempting restart...",
490
+ )
491
+ self.websocket_server_process = None
492
+ await self._restart_websocket_server()
493
+
494
+ async def _handle_unresponsive_websocket_server(self) -> None:
495
+ if self.console:
496
+ self.console.print("⚠️ WebSocket server not responding, restarting...")
497
+ await self._cleanup_dead_websocket_process()
498
+ await self._restart_websocket_server()
499
+
500
+ async def _restart_websocket_server(self) -> None:
501
+ if await self.start_websocket_server():
502
+ if self.console:
503
+ self.console.print("✅ WebSocket server restarted successfully")
504
+ elif self.console:
505
+ self.console.print("❌ Failed to restart WebSocket server")
506
+
507
+ def safe_print(self, *args, **kwargs) -> None:
508
+ if not self.config.stdio_mode and self.console:
509
+ self.console.print(*args, **kwargs)
510
+
511
+ def create_progress_file_path(self, job_id: str) -> Path:
512
+ if not self.validate_job_id(job_id):
513
+ msg = f"Invalid job_id: {job_id}"
514
+ raise ValueError(msg)
515
+ return self.progress_dir / f"job-{job_id}.json"
516
+
517
+ async def schedule_state_save(
518
+ self,
519
+ save_id: str,
520
+ save_func: t.Callable[[], None],
521
+ ) -> None:
522
+ await self.batched_saver.schedule_save(save_id, save_func)
523
+
524
+ def get_current_time(self) -> str:
525
+ """Get current timestamp as string for progress tracking."""
526
+ import datetime
527
+
528
+ return datetime.datetime.now().isoformat()
529
+
530
+ def get_context_stats(self) -> dict[str, t.Any]:
531
+ return {
532
+ "initialized": self._initialized,
533
+ "stdio_mode": self.config.stdio_mode,
534
+ "project_path": str(self.config.project_path),
535
+ "progress_dir": str(self.progress_dir),
536
+ "components": {
537
+ "cli_runner": self.cli_runner is not None,
538
+ "state_manager": self.state_manager is not None,
539
+ "error_cache": self.error_cache is not None,
540
+ "rate_limiter": self.rate_limiter is not None,
541
+ "batched_saver": self.batched_saver is not None,
542
+ },
543
+ "websocket_server": {
544
+ "port": self.websocket_server_port,
545
+ "process_exists": self.websocket_server_process is not None,
546
+ "health_monitor_running": self._websocket_health_check_task is not None
547
+ and not self._websocket_health_check_task.done(),
548
+ "cleanup_registered": self._websocket_cleanup_registered,
549
+ },
550
+ "progress_queue": {
551
+ "maxsize": self.progress_queue.maxsize,
552
+ "current_size": self.progress_queue.qsize(),
553
+ "full": self.progress_queue.full(),
554
+ },
555
+ "startup_tasks": len(self._startup_tasks),
556
+ "shutdown_tasks": len(self._shutdown_tasks),
557
+ "batched_saving": self.batched_saver.get_stats(),
558
+ }
559
+
560
+
561
+ class MCPContextManager:
562
+ def __init__(self, config: MCPServerConfig) -> None:
563
+ self.context = MCPServerContext(config)
564
+
565
+ async def __aenter__(self) -> MCPServerContext:
566
+ await self.context.initialize()
567
+ return self.context
568
+
569
+ async def __aexit__(
570
+ self,
571
+ exc_type: type[BaseException] | None,
572
+ exc_val: BaseException | None,
573
+ _exc_tb: TracebackType | None,
574
+ ) -> None:
575
+ await self.context.shutdown()
576
+
577
+
578
+ _global_context: MCPServerContext | None = None
579
+
580
+
581
+ def get_context() -> MCPServerContext:
582
+ if _global_context is None:
583
+ msg = "MCP server context not initialized. Call set_context() first."
584
+ raise RuntimeError(
585
+ msg,
586
+ )
587
+ return _global_context
588
+
589
+
590
+ def set_context(context: MCPServerContext) -> None:
591
+ global _global_context
592
+ _global_context = context
593
+
594
+
595
+ def clear_context() -> None:
596
+ global _global_context
597
+ _global_context = None
598
+
599
+
600
+ def get_console() -> Console:
601
+ return get_context().console or Console()
602
+
603
+
604
+ def get_state_manager() -> StateManager | None:
605
+ return get_context().state_manager
606
+
607
+
608
+ def get_error_cache() -> ErrorCache | None:
609
+ return get_context().error_cache
610
+
611
+
612
+ def get_rate_limiter() -> RateLimitMiddleware | None:
613
+ return get_context().rate_limiter
614
+
615
+
616
+ def safe_print(*args, **kwargs) -> None:
617
+ get_context().safe_print(*args, **kwargs)
618
+
619
+
620
+ def validate_job_id(job_id: str) -> bool:
621
+ return get_context().validate_job_id(job_id)