claude-mpm 4.1.2__py3-none-any.whl → 4.1.3__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.
Files changed (53) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/engineer.json +33 -11
  3. claude_mpm/cli/commands/agents.py +556 -1009
  4. claude_mpm/cli/commands/memory.py +248 -927
  5. claude_mpm/cli/commands/run.py +139 -484
  6. claude_mpm/cli/startup_logging.py +76 -0
  7. claude_mpm/core/agent_registry.py +6 -10
  8. claude_mpm/core/framework_loader.py +114 -595
  9. claude_mpm/core/logging_config.py +2 -4
  10. claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
  11. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
  12. claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
  13. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
  14. claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
  15. claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
  16. claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
  17. claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
  18. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
  19. claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
  20. claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
  21. claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
  22. claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
  23. claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
  24. claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
  25. claude_mpm/services/agents/memory/memory_file_service.py +103 -0
  26. claude_mpm/services/agents/memory/memory_format_service.py +201 -0
  27. claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
  28. claude_mpm/services/agents/registry/__init__.py +1 -1
  29. claude_mpm/services/cli/__init__.py +18 -0
  30. claude_mpm/services/cli/agent_cleanup_service.py +407 -0
  31. claude_mpm/services/cli/agent_dependency_service.py +395 -0
  32. claude_mpm/services/cli/agent_listing_service.py +463 -0
  33. claude_mpm/services/cli/agent_output_formatter.py +605 -0
  34. claude_mpm/services/cli/agent_validation_service.py +589 -0
  35. claude_mpm/services/cli/dashboard_launcher.py +424 -0
  36. claude_mpm/services/cli/memory_crud_service.py +617 -0
  37. claude_mpm/services/cli/memory_output_formatter.py +604 -0
  38. claude_mpm/services/cli/session_manager.py +513 -0
  39. claude_mpm/services/cli/socketio_manager.py +498 -0
  40. claude_mpm/services/cli/startup_checker.py +370 -0
  41. claude_mpm/services/core/cache_manager.py +311 -0
  42. claude_mpm/services/core/memory_manager.py +637 -0
  43. claude_mpm/services/core/path_resolver.py +498 -0
  44. claude_mpm/services/core/service_container.py +520 -0
  45. claude_mpm/services/core/service_interfaces.py +436 -0
  46. claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
  47. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/METADATA +1 -1
  48. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/RECORD +52 -22
  49. claude_mpm/cli/commands/run_config_checker.py +0 -159
  50. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/WHEEL +0 -0
  51. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/entry_points.txt +0 -0
  52. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/licenses/LICENSE +0 -0
  53. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,424 @@
1
+ """
2
+ Dashboard Launcher Service
3
+ ===========================
4
+
5
+ WHY: This service provides a centralized way to manage dashboard launching across
6
+ the application, particularly for Socket.IO monitoring and other web dashboards.
7
+ By extracting this logic from run.py, we reduce complexity and create a reusable
8
+ service for any command that needs to launch a dashboard.
9
+
10
+ DESIGN DECISIONS:
11
+ - Interface-based design (IDashboardLauncher) for testability and flexibility
12
+ - Support for multiple browser types and launch methods
13
+ - Port management integration for finding available ports
14
+ - Graceful fallback when browser launch fails
15
+ - Platform-specific optimizations for tab reuse
16
+ - Process management for standalone servers
17
+ """
18
+
19
+ import os
20
+ import platform
21
+ import socket
22
+ import subprocess
23
+ import sys
24
+ import time
25
+ import urllib.error
26
+ import urllib.request
27
+ import webbrowser
28
+ from abc import ABC, abstractmethod
29
+ from typing import Tuple
30
+
31
+ from ...core.logger import get_logger
32
+ from ...core.unified_paths import get_package_root
33
+ from ...services.port_manager import PortManager
34
+
35
+
36
+ # Interface
37
+ class IDashboardLauncher(ABC):
38
+ """Interface for dashboard launching service."""
39
+
40
+ @abstractmethod
41
+ def launch_dashboard(
42
+ self, port: int = 8765, monitor_mode: bool = True
43
+ ) -> Tuple[bool, bool]:
44
+ """
45
+ Launch the web dashboard.
46
+
47
+ Args:
48
+ port: Port number for the dashboard server
49
+ monitor_mode: Whether to open in monitor mode
50
+
51
+ Returns:
52
+ Tuple of (success, browser_opened)
53
+ """
54
+
55
+ @abstractmethod
56
+ def is_dashboard_running(self, port: int = 8765) -> bool:
57
+ """
58
+ Check if dashboard server is running.
59
+
60
+ Args:
61
+ port: Port to check
62
+
63
+ Returns:
64
+ True if dashboard is running on the specified port
65
+ """
66
+
67
+ @abstractmethod
68
+ def get_dashboard_url(self, port: int = 8765) -> str:
69
+ """
70
+ Get the dashboard URL.
71
+
72
+ Args:
73
+ port: Port number
74
+
75
+ Returns:
76
+ Dashboard URL string
77
+ """
78
+
79
+ @abstractmethod
80
+ def stop_dashboard(self, port: int = 8765) -> bool:
81
+ """
82
+ Stop the dashboard server.
83
+
84
+ Args:
85
+ port: Port of the server to stop
86
+
87
+ Returns:
88
+ True if successfully stopped
89
+ """
90
+
91
+ @abstractmethod
92
+ def wait_for_dashboard(self, port: int = 8765, timeout: int = 30) -> bool:
93
+ """
94
+ Wait for dashboard to be ready.
95
+
96
+ Args:
97
+ port: Port to check
98
+ timeout: Maximum time to wait in seconds
99
+
100
+ Returns:
101
+ True if dashboard became ready within timeout
102
+ """
103
+
104
+
105
+ # Implementation
106
+ class DashboardLauncher(IDashboardLauncher):
107
+ """Dashboard launcher service implementation."""
108
+
109
+ def __init__(self, logger=None):
110
+ """
111
+ Initialize the dashboard launcher.
112
+
113
+ Args:
114
+ logger: Optional logger instance
115
+ """
116
+ self.logger = logger or get_logger("DashboardLauncher")
117
+ self.port_manager = PortManager()
118
+
119
+ def launch_dashboard(
120
+ self, port: int = 8765, monitor_mode: bool = True
121
+ ) -> Tuple[bool, bool]:
122
+ """
123
+ Launch the web dashboard.
124
+
125
+ WHY: Provides a unified way to launch dashboards with proper error handling,
126
+ browser management, and server lifecycle control.
127
+
128
+ Args:
129
+ port: Port number for the dashboard server
130
+ monitor_mode: Whether to open in monitor mode
131
+
132
+ Returns:
133
+ Tuple of (success, browser_opened)
134
+ """
135
+ try:
136
+ # Verify dependencies for Socket.IO dashboard
137
+ if monitor_mode:
138
+ if not self._verify_socketio_dependencies():
139
+ return False, False
140
+
141
+ self.logger.info(
142
+ f"Launching dashboard (port: {port}, monitor: {monitor_mode})"
143
+ )
144
+
145
+ # Clean up dead instances and check for existing servers
146
+ self.port_manager.cleanup_dead_instances()
147
+ active_instances = self.port_manager.list_active_instances()
148
+
149
+ # Determine the port to use
150
+ server_port = self._determine_server_port(port, active_instances)
151
+ server_running = self.is_dashboard_running(server_port)
152
+
153
+ # Get dashboard URL
154
+ dashboard_url = self.get_dashboard_url(server_port)
155
+
156
+ if server_running:
157
+ self.logger.info(
158
+ f"Dashboard server already running on port {server_port}"
159
+ )
160
+ print(f"✅ Dashboard server already running on port {server_port}")
161
+ print(f"📊 Dashboard: {dashboard_url}")
162
+ else:
163
+ # Start the server
164
+ print("🔧 Starting dashboard server...")
165
+ if not self._start_dashboard_server(server_port):
166
+ print("❌ Failed to start dashboard server")
167
+ self._print_troubleshooting_tips(server_port)
168
+ return False, False
169
+
170
+ print("✅ Dashboard server started successfully")
171
+ print(f"📊 Dashboard: {dashboard_url}")
172
+
173
+ # Open browser unless suppressed
174
+ browser_opened = False
175
+ if not self._is_browser_suppressed():
176
+ print("🌐 Opening dashboard in browser...")
177
+ browser_opened = self._open_browser(dashboard_url)
178
+ if not browser_opened:
179
+ print("⚠️ Could not open browser automatically")
180
+ print(f"📊 Please open manually: {dashboard_url}")
181
+ else:
182
+ print("🌐 Browser opening suppressed (CLAUDE_MPM_NO_BROWSER=1)")
183
+ self.logger.info("Browser opening suppressed by environment variable")
184
+
185
+ return True, browser_opened
186
+
187
+ except Exception as e:
188
+ self.logger.error(f"Failed to launch dashboard: {e}")
189
+ print(f"❌ Failed to launch dashboard: {e}")
190
+ return False, False
191
+
192
+ def is_dashboard_running(self, port: int = 8765) -> bool:
193
+ """
194
+ Check if dashboard server is running.
195
+
196
+ WHY: Prevents duplicate server launches and helps determine if we need
197
+ to start a new server or connect to an existing one.
198
+
199
+ Args:
200
+ port: Port to check
201
+
202
+ Returns:
203
+ True if dashboard is running on the specified port
204
+ """
205
+ try:
206
+ # First, do a basic TCP connection check
207
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
208
+ s.settimeout(2.0)
209
+ result = s.connect_ex(("127.0.0.1", port))
210
+ if result != 0:
211
+ self.logger.debug(f"TCP connection to port {port} failed")
212
+ return False
213
+
214
+ # If TCP connection succeeds, try HTTP health check
215
+ try:
216
+ response = urllib.request.urlopen(
217
+ f"http://localhost:{port}/status", timeout=5
218
+ )
219
+ if response.getcode() == 200:
220
+ self.logger.debug(f"Dashboard health check passed on port {port}")
221
+ return True
222
+ except Exception as e:
223
+ self.logger.debug(f"HTTP health check failed for port {port}: {e}")
224
+ # Server is listening but may not be fully ready yet
225
+ return True # Still consider it running if TCP works
226
+
227
+ except Exception as e:
228
+ self.logger.debug(f"Error checking dashboard on port {port}: {e}")
229
+
230
+ return False
231
+
232
+ def get_dashboard_url(self, port: int = 8765) -> str:
233
+ """
234
+ Get the dashboard URL.
235
+
236
+ Args:
237
+ port: Port number
238
+
239
+ Returns:
240
+ Dashboard URL string
241
+ """
242
+ return f"http://localhost:{port}"
243
+
244
+ def stop_dashboard(self, port: int = 8765) -> bool:
245
+ """
246
+ Stop the dashboard server.
247
+
248
+ WHY: Provides clean shutdown of dashboard servers to free up ports
249
+ and resources.
250
+
251
+ Args:
252
+ port: Port of the server to stop
253
+
254
+ Returns:
255
+ True if successfully stopped
256
+ """
257
+ try:
258
+ daemon_script = get_package_root() / "scripts" / "socketio_daemon.py"
259
+ if not daemon_script.exists():
260
+ self.logger.error(f"Daemon script not found: {daemon_script}")
261
+ return False
262
+
263
+ # Stop the daemon
264
+ result = subprocess.run(
265
+ [sys.executable, str(daemon_script), "stop", "--port", str(port)],
266
+ capture_output=True,
267
+ text=True,
268
+ timeout=10,
269
+ check=False,
270
+ )
271
+
272
+ if result.returncode == 0:
273
+ self.logger.info(f"Dashboard server stopped on port {port}")
274
+ return True
275
+
276
+ self.logger.warning(f"Failed to stop dashboard server: {result.stderr}")
277
+ return False
278
+
279
+ except Exception as e:
280
+ self.logger.error(f"Error stopping dashboard server: {e}")
281
+ return False
282
+
283
+ def wait_for_dashboard(self, port: int = 8765, timeout: int = 30) -> bool:
284
+ """
285
+ Wait for dashboard to be ready.
286
+
287
+ WHY: Ensures the dashboard is fully operational before attempting to
288
+ open it in a browser, preventing "connection refused" errors.
289
+
290
+ Args:
291
+ port: Port to check
292
+ timeout: Maximum time to wait in seconds
293
+
294
+ Returns:
295
+ True if dashboard became ready within timeout
296
+ """
297
+ start_time = time.time()
298
+ while time.time() - start_time < timeout:
299
+ if self.is_dashboard_running(port):
300
+ return True
301
+ time.sleep(0.5)
302
+ return False
303
+
304
+ # Private helper methods
305
+ def _verify_socketio_dependencies(self) -> bool:
306
+ """Verify Socket.IO dependencies are available."""
307
+ try:
308
+ import aiohttp
309
+ import engineio
310
+ import socketio
311
+
312
+ self.logger.debug("Socket.IO dependencies verified")
313
+ return True
314
+ except ImportError as e:
315
+ self.logger.error(f"Socket.IO dependencies not available: {e}")
316
+ print(f"❌ Socket.IO dependencies missing: {e}")
317
+ print(" Install with: pip install python-socketio aiohttp python-engineio")
318
+ return False
319
+
320
+ def _determine_server_port(
321
+ self, requested_port: int, active_instances: list
322
+ ) -> int:
323
+ """Determine which port to use for the server."""
324
+ if active_instances:
325
+ # Prefer port 8765 if available
326
+ for instance in active_instances:
327
+ if instance.get("port") == 8765:
328
+ return 8765
329
+ # Otherwise use first active instance
330
+ return active_instances[0].get("port", requested_port)
331
+ return requested_port
332
+
333
+ def _start_dashboard_server(self, port: int) -> bool:
334
+ """Start the dashboard server."""
335
+ try:
336
+ daemon_script = get_package_root() / "scripts" / "socketio_daemon.py"
337
+ if not daemon_script.exists():
338
+ self.logger.error(f"Daemon script not found: {daemon_script}")
339
+ return False
340
+
341
+ # Start the daemon
342
+ result = subprocess.run(
343
+ [sys.executable, str(daemon_script), "start", "--port", str(port)],
344
+ capture_output=True,
345
+ text=True,
346
+ timeout=30,
347
+ check=False,
348
+ )
349
+
350
+ if result.returncode == 0:
351
+ self.logger.info(f"Dashboard server started on port {port}")
352
+ # Wait for server to be ready
353
+ return self.wait_for_dashboard(port, timeout=10)
354
+
355
+ self.logger.error(f"Failed to start dashboard server: {result.stderr}")
356
+ return False
357
+
358
+ except Exception as e:
359
+ self.logger.error(f"Error starting dashboard server: {e}")
360
+ return False
361
+
362
+ def _is_browser_suppressed(self) -> bool:
363
+ """Check if browser opening is suppressed."""
364
+ return os.environ.get("CLAUDE_MPM_NO_BROWSER") == "1"
365
+
366
+ def _open_browser(self, url: str) -> bool:
367
+ """
368
+ Open URL in browser with platform-specific optimizations.
369
+
370
+ WHY: Different platforms have different ways to reuse browser tabs.
371
+ This method tries platform-specific approaches before falling back
372
+ to the standard webbrowser module.
373
+ """
374
+ try:
375
+ system = platform.system().lower()
376
+
377
+ if system == "darwin": # macOS
378
+ try:
379
+ # Try to open in existing tab with -g flag (background)
380
+ subprocess.run(["open", "-g", url], check=True, timeout=5)
381
+ self.logger.info("Opened browser on macOS")
382
+ return True
383
+ except Exception:
384
+ pass
385
+
386
+ elif system == "linux":
387
+ try:
388
+ # Try xdg-open for Linux
389
+ subprocess.run(["xdg-open", url], check=True, timeout=5)
390
+ self.logger.info("Opened browser on Linux")
391
+ return True
392
+ except Exception:
393
+ pass
394
+
395
+ elif system == "windows":
396
+ try:
397
+ # Try to use existing browser window
398
+ webbrowser.get().open(url, new=0)
399
+ self.logger.info("Opened browser on Windows")
400
+ return True
401
+ except Exception:
402
+ pass
403
+
404
+ # Fallback to standard webbrowser module
405
+ webbrowser.open(url, new=0, autoraise=True)
406
+ self.logger.info("Opened browser using webbrowser module")
407
+ return True
408
+
409
+ except Exception as e:
410
+ self.logger.warning(f"Browser opening failed: {e}")
411
+ try:
412
+ # Final fallback
413
+ webbrowser.open(url)
414
+ return True
415
+ except Exception:
416
+ return False
417
+
418
+ def _print_troubleshooting_tips(self, port: int):
419
+ """Print troubleshooting tips for dashboard launch failures."""
420
+ print("💡 Troubleshooting tips:")
421
+ print(f" - Check if port {port} is already in use")
422
+ print(" - Verify Socket.IO dependencies: pip install python-socketio aiohttp")
423
+ print(" - Try a different port with --websocket-port")
424
+ print(" - Check firewall settings for localhost connections")