crackerjack 0.29.0__py3-none-any.whl → 0.31.4__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 (158) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -253
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -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 +652 -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 +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +670 -0
  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 +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +577 -0
  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 +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -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 +615 -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 +370 -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 +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -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/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/.pre-commit-config-ai.yaml +0 -149
  151. crackerjack/.pre-commit-config-fast.yaml +0 -69
  152. crackerjack/.pre-commit-config.yaml +0 -114
  153. crackerjack/crackerjack.py +0 -4140
  154. crackerjack/pyproject.toml +0 -285
  155. crackerjack-0.29.0.dist-info/METADATA +0 -1289
  156. crackerjack-0.29.0.dist-info/RECORD +0 -17
  157. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  158. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,501 @@
1
+ import asyncio
2
+ import socket
3
+ import subprocess
4
+ import sys
5
+ import time
6
+ from typing import Any
7
+
8
+ import aiohttp
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ console = Console()
13
+
14
+
15
+ watchdog_event_queue: asyncio.Queue[dict[str, Any]] | None = None
16
+
17
+
18
+ class ServiceConfig:
19
+ def __init__(
20
+ self,
21
+ name: str,
22
+ command: list[str],
23
+ health_check_url: str | None = None,
24
+ health_check_interval: float = 30.0,
25
+ restart_delay: float = 5.0,
26
+ max_restarts: int = 10,
27
+ restart_window: float = 300.0,
28
+ ) -> None:
29
+ self.name = name
30
+ self.command = command
31
+ self.health_check_url = health_check_url
32
+ self.health_check_interval = health_check_interval
33
+ self.restart_delay = restart_delay
34
+ self.max_restarts = max_restarts
35
+ self.restart_window = restart_window
36
+
37
+ self.process: subprocess.Popen | None = None
38
+ self.restart_count = 0
39
+ self.restart_timestamps: list[float] = []
40
+ self.last_health_check = 0.0
41
+ self.is_healthy = False
42
+ self.last_error: str | None = None
43
+
44
+
45
+ class ServiceWatchdog:
46
+ def __init__(
47
+ self,
48
+ services: list[ServiceConfig],
49
+ event_queue: asyncio.Queue[dict[str, Any]] | None = None,
50
+ ) -> None:
51
+ self.services = services
52
+ self.is_running = True
53
+ self.session: aiohttp.ClientSession | None = None
54
+ self.event_queue = event_queue
55
+
56
+ global watchdog_event_queue
57
+ if event_queue:
58
+ watchdog_event_queue = event_queue
59
+
60
+ async def start(self) -> None:
61
+ self.session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10.0))
62
+
63
+ for service in self.services:
64
+ await self._start_service(service)
65
+
66
+ monitor_tasks = [
67
+ asyncio.create_task(self._monitor_service(service))
68
+ for service in self.services
69
+ ]
70
+
71
+ status_task = asyncio.create_task(self._display_status())
72
+
73
+ try:
74
+ await asyncio.gather(*monitor_tasks, status_task)
75
+ finally:
76
+ await self._cleanup()
77
+
78
+ async def stop(self) -> None:
79
+ self.is_running = False
80
+
81
+ for service in self.services:
82
+ if service.process:
83
+ console.print(f"[yellow]🛑 Stopping {service.name}...[/yellow]")
84
+ service.process.terminate()
85
+ try:
86
+ service.process.wait(timeout=10)
87
+ except subprocess.TimeoutExpired:
88
+ service.process.kill()
89
+
90
+ if self.session:
91
+ await self.session.close()
92
+
93
+ def _is_port_in_use(self, port: int) -> bool:
94
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
95
+ try:
96
+ s.bind(("127.0.0.1", port))
97
+ return False
98
+ except OSError:
99
+ return True
100
+
101
+ async def _start_service(self, service: ServiceConfig) -> bool:
102
+ try:
103
+ if await self._check_websocket_server_running(service):
104
+ return True
105
+
106
+ if not await self._launch_service_process(service):
107
+ return False
108
+
109
+ return await self._finalize_service_startup(service)
110
+
111
+ except Exception as e:
112
+ return await self._handle_service_start_error(service, e)
113
+
114
+ async def _check_websocket_server_running(self, service: ServiceConfig) -> bool:
115
+ if "websocket - server" in " ".join(service.command):
116
+ if self._is_port_in_use(8675):
117
+ await self._emit_event(
118
+ "port_in_use",
119
+ service.name,
120
+ "Port 8675 already in use (server already running)",
121
+ )
122
+ service.is_healthy = True
123
+ return True
124
+ return False
125
+
126
+ async def _launch_service_process(self, service: ServiceConfig) -> bool:
127
+ service.process = subprocess.Popen(
128
+ service.command,
129
+ stdout=subprocess.PIPE,
130
+ stderr=subprocess.PIPE,
131
+ text=True,
132
+ )
133
+ service.last_error = None
134
+ await asyncio.sleep(2)
135
+
136
+ return await self._check_process_startup_success(service)
137
+
138
+ async def _check_process_startup_success(self, service: ServiceConfig) -> bool:
139
+ exit_code = service.process.poll()
140
+ if exit_code is not None:
141
+ return await self._handle_process_died(service, exit_code)
142
+ return True
143
+
144
+ async def _handle_process_died(
145
+ self,
146
+ service: ServiceConfig,
147
+ exit_code: int,
148
+ ) -> bool:
149
+ stdout, stderr = service.process.communicate()
150
+ error_msg = f"Process died (exit: {exit_code})"
151
+ if stderr and stderr.strip():
152
+ error_msg += f" - {stderr.strip()[:50]}"
153
+ service.last_error = error_msg
154
+ await self._emit_event("process_died", service.name, error_msg)
155
+ return False
156
+
157
+ async def _finalize_service_startup(self, service: ServiceConfig) -> bool:
158
+ if service.health_check_url:
159
+ service.is_healthy = await self._health_check(service)
160
+ else:
161
+ service.is_healthy = True
162
+
163
+ if service.is_healthy:
164
+ await self._emit_event("started", service.name, "Running")
165
+
166
+ return service.is_healthy
167
+
168
+ async def _handle_service_start_error(
169
+ self,
170
+ service: ServiceConfig,
171
+ error: Exception,
172
+ ) -> bool:
173
+ service.last_error = str(error)
174
+ await self._emit_event(
175
+ "start_error",
176
+ service.name,
177
+ f"Failed: {str(error)[:30]}",
178
+ )
179
+ return False
180
+
181
+ async def _handle_websocket_server_monitoring(self, service: ServiceConfig) -> bool:
182
+ if "websocket - server" not in " ".join(service.command):
183
+ return False
184
+
185
+ if self._is_port_in_use(8675):
186
+ if await self._verify_websocket_server_health():
187
+ service.is_healthy = True
188
+ if (
189
+ not hasattr(service, "_port_acknowledged")
190
+ or not service._port_acknowledged
191
+ ):
192
+ service._port_acknowledged = True
193
+ await self._emit_event(
194
+ "port_healthy",
195
+ service.name,
196
+ "WebSocket server verified healthy",
197
+ )
198
+ await asyncio.sleep(30)
199
+ return True
200
+ service.is_healthy = False
201
+ await self._emit_event(
202
+ "port_hijacked",
203
+ service.name,
204
+ "Port 8675 occupied by different service",
205
+ )
206
+ await self._restart_service(service)
207
+ return False
208
+ service._port_acknowledged = False
209
+ if service.process and service.process.poll() is None:
210
+ await self._emit_event(
211
+ "port_unavailable",
212
+ service.name,
213
+ "Process running but port 8675 not available",
214
+ )
215
+ await self._restart_service(service)
216
+ return False
217
+
218
+ async def _check_process_health(self, service: ServiceConfig) -> bool:
219
+ process_running = service.process and service.process.poll() is None
220
+ if not process_running:
221
+ if service.process:
222
+ exit_code = service.process.poll()
223
+ await self._emit_event(
224
+ "died",
225
+ service.name,
226
+ f"Process died (exit: {exit_code})",
227
+ )
228
+ else:
229
+ await self._emit_event("not_started", service.name, "Not started")
230
+ await self._restart_service(service)
231
+ return False
232
+ return True
233
+
234
+ async def _perform_health_check_if_needed(self, service: ServiceConfig) -> bool:
235
+ if not service.health_check_url:
236
+ return True
237
+
238
+ current_time = time.time()
239
+ if current_time - service.last_health_check >= service.health_check_interval:
240
+ service.is_healthy = await self._health_check(service)
241
+ service.last_health_check = current_time
242
+
243
+ if not service.is_healthy:
244
+ await self._emit_event(
245
+ "health_fail",
246
+ service.name,
247
+ "Health check failed",
248
+ )
249
+ await self._restart_service(service)
250
+ return False
251
+ return True
252
+
253
+ async def _monitor_service(self, service: ServiceConfig) -> None:
254
+ while self.is_running:
255
+ try:
256
+ if await self._execute_monitoring_cycle(service):
257
+ await asyncio.sleep(5.0)
258
+ else:
259
+ continue
260
+
261
+ except Exception as e:
262
+ await self._handle_monitoring_error(service, e)
263
+
264
+ async def _execute_monitoring_cycle(self, service: ServiceConfig) -> bool:
265
+ if await self._handle_websocket_server_monitoring(service):
266
+ return True
267
+
268
+ if not await self._check_process_health(service):
269
+ return False
270
+
271
+ return await self._perform_health_check_if_needed(service)
272
+
273
+ async def _handle_monitoring_error(
274
+ self,
275
+ service: ServiceConfig,
276
+ error: Exception,
277
+ ) -> None:
278
+ service.last_error = str(error)
279
+ console.print(f"[red]❌ Error monitoring {service.name}: {error}[/red]")
280
+ await asyncio.sleep(10.0)
281
+
282
+ async def _health_check(self, service: ServiceConfig) -> bool:
283
+ if not service.health_check_url or not self.session:
284
+ return True
285
+
286
+ try:
287
+ async with self.session.get(service.health_check_url) as response:
288
+ return response.status == 200
289
+ except Exception:
290
+ return False
291
+
292
+ async def _verify_websocket_server_health(self) -> bool:
293
+ if not self.session:
294
+ return False
295
+
296
+ with suppress(Exception):
297
+ async with self.session.get("http: // localhost: 8675 / ") as response:
298
+ if response.status == 200:
299
+ data = await response.json()
300
+
301
+ return (
302
+ "message" in data
303
+ and "Crackerjack" in data.get("message", "")
304
+ and "progress_dir" in data
305
+ )
306
+ return False
307
+
308
+ async def _restart_service(self, service: ServiceConfig) -> None:
309
+ current_time = time.time()
310
+ reason = self._determine_restart_reason(service)
311
+
312
+ await self._emit_event("restarting", service.name, f"Restarting - {reason}")
313
+
314
+ if not await self._check_restart_rate_limit(service, current_time):
315
+ return
316
+
317
+ await self._terminate_existing_process(service)
318
+ await self._wait_before_restart(service)
319
+ await self._execute_service_restart(service, current_time)
320
+
321
+ def _determine_restart_reason(self, service: ServiceConfig) -> str:
322
+ return (
323
+ "Process died"
324
+ if not service.process or service.process.poll() is not None
325
+ else "Health failed"
326
+ )
327
+
328
+ async def _check_restart_rate_limit(
329
+ self,
330
+ service: ServiceConfig,
331
+ current_time: float,
332
+ ) -> bool:
333
+ service.restart_timestamps = [
334
+ ts
335
+ for ts in service.restart_timestamps
336
+ if current_time - ts < service.restart_window
337
+ ]
338
+
339
+ if len(service.restart_timestamps) >= service.max_restarts:
340
+ console.print(
341
+ f"[red]🚨 {service.name} exceeded restart limit ({service.max_restarts} in {service.restart_window}s)[/red]",
342
+ )
343
+ service.last_error = "Restart rate limit exceeded"
344
+ await asyncio.sleep(60)
345
+ return False
346
+ return True
347
+
348
+ async def _terminate_existing_process(self, service: ServiceConfig) -> None:
349
+ if not service.process:
350
+ return
351
+
352
+ try:
353
+ console.print(
354
+ f"[yellow]🔪 Terminating existing {service.name} process (PID: {service.process.pid})[/yellow]",
355
+ )
356
+ service.process.terminate()
357
+ service.process.wait(timeout=10)
358
+ except subprocess.TimeoutExpired:
359
+ console.print(f"[red]💀 Force killing {service.name} process[/red]")
360
+ service.process.kill()
361
+ except Exception as e:
362
+ console.print(f"[yellow]⚠️ Error terminating {service.name}: {e}[/yellow]")
363
+
364
+ async def _wait_before_restart(self, service: ServiceConfig) -> None:
365
+ console.print(
366
+ f"[yellow]⏳ Waiting {service.restart_delay}s before restarting {service.name}...[/yellow]",
367
+ )
368
+ await asyncio.sleep(service.restart_delay)
369
+
370
+ async def _execute_service_restart(
371
+ self,
372
+ service: ServiceConfig,
373
+ current_time: float,
374
+ ) -> None:
375
+ service.restart_timestamps.append(current_time)
376
+ service.restart_count += 1
377
+
378
+ success = await self._start_service(service)
379
+ if not success:
380
+ await self._emit_event("restart_failed", service.name, "Restart failed")
381
+
382
+ async def _display_status(self) -> None:
383
+ while self.is_running:
384
+ try:
385
+ await self._update_status_display()
386
+ await asyncio.sleep(10.0)
387
+
388
+ except Exception as e:
389
+ console.print(f"[red]Error updating display: {e}[/red]")
390
+ await asyncio.sleep(5.0)
391
+
392
+ async def _update_status_display(self) -> None:
393
+ console.clear()
394
+ table = self._create_status_table()
395
+
396
+ for service in self.services:
397
+ status = self._get_service_status(service)
398
+ health = self._get_service_health(service)
399
+ restarts = str(service.restart_count)
400
+ error = self._format_error_message(service.last_error)
401
+
402
+ table.add_row(service.name, status, health, restarts, error)
403
+
404
+ console.print(table)
405
+ console.print("\n[dim]Press Ctrl + C to stop monitoring[/dim]")
406
+
407
+ def _create_status_table(self) -> Table:
408
+ table = Table(title="🔍 Crackerjack Service Watchdog")
409
+ table.add_column("Service", style="cyan", no_wrap=True)
410
+ table.add_column("Status", style="white")
411
+ table.add_column("Health", style="white")
412
+ table.add_column("Restarts", style="white")
413
+ table.add_column("Last Error", style="red")
414
+ return table
415
+
416
+ def _get_service_status(self, service: ServiceConfig) -> str:
417
+ if service.process and service.process.poll() is None:
418
+ return "[green]✅ Running[/green]"
419
+ return "[red]❌ Stopped[/red]"
420
+
421
+ def _get_service_health(self, service: ServiceConfig) -> str:
422
+ if service.health_check_url:
423
+ return (
424
+ "[green]🟢 Healthy[/green]"
425
+ if service.is_healthy
426
+ else "[red]🔴 Unhealthy[/red]"
427
+ )
428
+ return "[dim]N / A[/dim]"
429
+
430
+ def _format_error_message(self, error_message: str | None) -> str:
431
+ error = error_message or "[dim]None[/dim]"
432
+ if len(error) > 30:
433
+ error = error[:27] + "..."
434
+ return error
435
+
436
+ async def _emit_event(
437
+ self,
438
+ event_type: str,
439
+ service_name: str,
440
+ message: str,
441
+ ) -> None:
442
+ if self.event_queue:
443
+ from contextlib import suppress
444
+
445
+ with suppress(Exception):
446
+ event = {
447
+ "type": event_type,
448
+ "service": service_name,
449
+ "message": message,
450
+ "timestamp": time.time(),
451
+ }
452
+ await self.event_queue.put(event)
453
+
454
+ async def _cleanup(self) -> None:
455
+ if self.session and not self.session.closed:
456
+ await self.session.close()
457
+
458
+
459
+ async def create_default_watchdog(
460
+ event_queue: asyncio.Queue[dict[str, Any]] | None = None,
461
+ ) -> ServiceWatchdog:
462
+ python_path = sys.executable
463
+
464
+ services = [
465
+ ServiceConfig(
466
+ name="MCP Server",
467
+ command=[
468
+ python_path,
469
+ " - m",
470
+ "crackerjack",
471
+ " -- start - mcp - server",
472
+ ],
473
+ ),
474
+ ServiceConfig(
475
+ name="WebSocket Server",
476
+ command=[
477
+ python_path,
478
+ " - m",
479
+ "crackerjack",
480
+ " -- websocket - server",
481
+ ],
482
+ health_check_url="http: // localhost: 8675 / ",
483
+ ),
484
+ ]
485
+
486
+ return ServiceWatchdog(services, event_queue)
487
+
488
+
489
+ async def main() -> None:
490
+ watchdog = await create_default_watchdog()
491
+
492
+ try:
493
+ await watchdog.start()
494
+ except KeyboardInterrupt:
495
+ console.print("\n[yellow]🛑 Shutting down watchdog...[/yellow]")
496
+ finally:
497
+ await watchdog.stop()
498
+
499
+
500
+ if __name__ == "__main__":
501
+ asyncio.run(main())