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,498 @@
1
+ """
2
+ Socket.IO Server Manager Service
3
+ =================================
4
+
5
+ WHY: This service extracts Socket.IO server management from run.py to provide a
6
+ centralized, reusable service for managing Socket.IO server lifecycle. This reduces
7
+ run.py complexity by ~200-250 lines and provides a clean interface for server management.
8
+
9
+ DESIGN DECISIONS:
10
+ - Interface-based design (ISocketIOManager) for testability and flexibility
11
+ - Integrates with existing PortManager for port allocation
12
+ - Provides server process management with graceful shutdown
13
+ - Handles dependency checking and installation prompts
14
+ - Manages server lifecycle (start, stop, status, wait)
15
+ - Thread-safe server state management
16
+ """
17
+
18
+ import os
19
+ import signal
20
+ import socket
21
+ import subprocess
22
+ import sys
23
+ import threading
24
+ import time
25
+ from abc import ABC, abstractmethod
26
+ from dataclasses import dataclass
27
+ from typing import Optional, Tuple
28
+
29
+ from ...core.logger import get_logger
30
+ from ...core.unified_paths import get_package_root, get_scripts_dir
31
+ from ...services.port_manager import PortManager
32
+ from ...utils.dependency_manager import ensure_socketio_dependencies
33
+
34
+
35
+ @dataclass
36
+ class ServerInfo:
37
+ """Information about a running Socket.IO server."""
38
+
39
+ port: int
40
+ pid: Optional[int]
41
+ is_running: bool
42
+ launch_time: Optional[float]
43
+ url: str
44
+
45
+
46
+ # Interface
47
+ class ISocketIOManager(ABC):
48
+ """Interface for Socket.IO server management."""
49
+
50
+ @abstractmethod
51
+ def start_server(
52
+ self, port: Optional[int] = None, timeout: int = 30
53
+ ) -> Tuple[bool, ServerInfo]:
54
+ """
55
+ Start the Socket.IO server.
56
+
57
+ Args:
58
+ port: Optional specific port to use. If None, finds available port.
59
+ timeout: Maximum time to wait for server startup
60
+
61
+ Returns:
62
+ Tuple of (success, ServerInfo)
63
+ """
64
+
65
+ @abstractmethod
66
+ def stop_server(self, port: Optional[int] = None, timeout: int = 10) -> bool:
67
+ """
68
+ Stop the Socket.IO server.
69
+
70
+ Args:
71
+ port: Optional specific port to stop. If None, stops all servers.
72
+ timeout: Maximum time to wait for graceful shutdown
73
+
74
+ Returns:
75
+ True if server was stopped successfully
76
+ """
77
+
78
+ @abstractmethod
79
+ def is_server_running(self, port: int) -> bool:
80
+ """
81
+ Check if a Socket.IO server is running on the specified port.
82
+
83
+ Args:
84
+ port: Port to check
85
+
86
+ Returns:
87
+ True if server is running on the port
88
+ """
89
+
90
+ @abstractmethod
91
+ def get_server_info(self, port: int) -> ServerInfo:
92
+ """
93
+ Get information about a server on the specified port.
94
+
95
+ Args:
96
+ port: Port to check
97
+
98
+ Returns:
99
+ ServerInfo object with server details
100
+ """
101
+
102
+ @abstractmethod
103
+ def wait_for_server(self, port: int, timeout: int = 30) -> bool:
104
+ """
105
+ Wait for a server to be ready on the specified port.
106
+
107
+ Args:
108
+ port: Port to wait for
109
+ timeout: Maximum time to wait in seconds
110
+
111
+ Returns:
112
+ True if server became ready within timeout
113
+ """
114
+
115
+ @abstractmethod
116
+ def find_available_port(self, preferred_port: int = 8765) -> int:
117
+ """
118
+ Find an available port for the Socket.IO server.
119
+
120
+ Args:
121
+ preferred_port: Preferred port to try first
122
+
123
+ Returns:
124
+ Available port number
125
+ """
126
+
127
+ @abstractmethod
128
+ def ensure_dependencies(self) -> Tuple[bool, Optional[str]]:
129
+ """
130
+ Ensure Socket.IO dependencies are installed.
131
+
132
+ Returns:
133
+ Tuple of (success, error_message)
134
+ """
135
+
136
+
137
+ # Implementation
138
+ class SocketIOManager(ISocketIOManager):
139
+ """Socket.IO server manager implementation."""
140
+
141
+ def __init__(self, logger=None):
142
+ """
143
+ Initialize the Socket.IO manager.
144
+
145
+ Args:
146
+ logger: Optional logger instance
147
+ """
148
+ self.logger = logger or get_logger("SocketIOManager")
149
+ self.port_manager = PortManager()
150
+ self._server_processes = {} # port -> subprocess.Popen
151
+ self._lock = threading.Lock()
152
+
153
+ def start_server(
154
+ self, port: Optional[int] = None, timeout: int = 30
155
+ ) -> Tuple[bool, ServerInfo]:
156
+ """
157
+ Start the Socket.IO server.
158
+
159
+ WHY: Provides a unified way to start Socket.IO servers with proper error handling,
160
+ port management, and process lifecycle control.
161
+
162
+ Args:
163
+ port: Optional specific port to use. If None, finds available port.
164
+ timeout: Maximum time to wait for server startup
165
+
166
+ Returns:
167
+ Tuple of (success, ServerInfo)
168
+ """
169
+ with self._lock:
170
+ # Determine port to use
171
+ target_port = port if port else self.find_available_port()
172
+
173
+ # Check if server already running on this port
174
+ if self.is_server_running(target_port):
175
+ self.logger.info(
176
+ f"Socket.IO server already running on port {target_port}"
177
+ )
178
+ return True, self.get_server_info(target_port)
179
+
180
+ # Ensure dependencies are available
181
+ deps_ok, error_msg = self.ensure_dependencies()
182
+ if not deps_ok:
183
+ self.logger.error(f"Socket.IO dependencies not available: {error_msg}")
184
+ return False, ServerInfo(
185
+ port=target_port,
186
+ pid=None,
187
+ is_running=False,
188
+ launch_time=None,
189
+ url=f"http://localhost:{target_port}",
190
+ )
191
+
192
+ # Start the server
193
+ try:
194
+ self.logger.info(f"Starting Socket.IO server on port {target_port}")
195
+
196
+ # Get the socketio daemon script path
197
+ scripts_dir = get_scripts_dir()
198
+ daemon_script = scripts_dir / "socketio_daemon_wrapper.py"
199
+
200
+ if not daemon_script.exists():
201
+ self.logger.error(
202
+ f"Socket.IO daemon script not found: {daemon_script}"
203
+ )
204
+ return False, ServerInfo(
205
+ port=target_port,
206
+ pid=None,
207
+ is_running=False,
208
+ launch_time=None,
209
+ url=f"http://localhost:{target_port}",
210
+ )
211
+
212
+ # Start the server process
213
+ env = os.environ.copy()
214
+ env["PYTHONPATH"] = str(get_package_root())
215
+
216
+ process = subprocess.Popen(
217
+ [sys.executable, str(daemon_script), "--port", str(target_port)],
218
+ env=env,
219
+ stdout=subprocess.PIPE,
220
+ stderr=subprocess.PIPE,
221
+ start_new_session=True, # Detach from parent process group
222
+ )
223
+
224
+ # Store process reference
225
+ self._server_processes[target_port] = process
226
+
227
+ # Wait for server to be ready
228
+ if self.wait_for_server(target_port, timeout):
229
+ self.logger.info(
230
+ f"Socket.IO server started successfully on port {target_port}"
231
+ )
232
+ return True, self.get_server_info(target_port)
233
+ self.logger.error(
234
+ f"Socket.IO server failed to start within {timeout} seconds"
235
+ )
236
+ # Clean up failed process
237
+ self._cleanup_process(target_port)
238
+ return False, ServerInfo(
239
+ port=target_port,
240
+ pid=None,
241
+ is_running=False,
242
+ launch_time=None,
243
+ url=f"http://localhost:{target_port}",
244
+ )
245
+
246
+ except Exception as e:
247
+ self.logger.error(f"Failed to start Socket.IO server: {e}")
248
+ return False, ServerInfo(
249
+ port=target_port,
250
+ pid=None,
251
+ is_running=False,
252
+ launch_time=None,
253
+ url=f"http://localhost:{target_port}",
254
+ )
255
+
256
+ def stop_server(self, port: Optional[int] = None, timeout: int = 10) -> bool:
257
+ """
258
+ Stop the Socket.IO server.
259
+
260
+ WHY: Provides graceful shutdown of Socket.IO servers with proper cleanup
261
+ and process termination.
262
+
263
+ Args:
264
+ port: Optional specific port to stop. If None, stops all servers.
265
+ timeout: Maximum time to wait for graceful shutdown
266
+
267
+ Returns:
268
+ True if server was stopped successfully
269
+ """
270
+ with self._lock:
271
+ if port:
272
+ return self._stop_server_on_port(port, timeout)
273
+ # Stop all managed servers
274
+ success = True
275
+ for server_port in list(self._server_processes.keys()):
276
+ if not self._stop_server_on_port(server_port, timeout):
277
+ success = False
278
+ return success
279
+
280
+ def _stop_server_on_port(self, port: int, timeout: int) -> bool:
281
+ """Stop server on specific port."""
282
+ try:
283
+ # Check if we have a process reference
284
+ if port in self._server_processes:
285
+ process = self._server_processes[port]
286
+
287
+ # Send graceful shutdown signal
288
+ if process.poll() is None: # Process is still running
289
+ self.logger.info(f"Stopping Socket.IO server on port {port}")
290
+ process.terminate()
291
+
292
+ # Wait for graceful shutdown
293
+ try:
294
+ process.wait(timeout=timeout)
295
+ self.logger.info(
296
+ f"Socket.IO server on port {port} stopped gracefully"
297
+ )
298
+ except subprocess.TimeoutExpired:
299
+ # Force kill if graceful shutdown failed
300
+ self.logger.warning(
301
+ f"Force killing Socket.IO server on port {port}"
302
+ )
303
+ process.kill()
304
+ process.wait()
305
+
306
+ # Clean up process reference
307
+ del self._server_processes[port]
308
+ return True
309
+
310
+ # Try to stop server by port (external process)
311
+ process_info = self.port_manager.get_process_on_port(port)
312
+ if process_info and process_info.is_ours:
313
+ try:
314
+ os.kill(process_info.pid, signal.SIGTERM)
315
+ time.sleep(2) # Give it time to shut down
316
+
317
+ # Check if still running
318
+ try:
319
+ os.kill(process_info.pid, 0) # Check if process exists
320
+ # Still running, force kill
321
+ os.kill(process_info.pid, signal.SIGKILL)
322
+ except ProcessLookupError:
323
+ pass # Process already terminated
324
+
325
+ self.logger.info(
326
+ f"Stopped Socket.IO server (PID: {process_info.pid}) on port {port}"
327
+ )
328
+ return True
329
+ except Exception as e:
330
+ self.logger.error(f"Failed to stop server on port {port}: {e}")
331
+ return False
332
+
333
+ return True # No server to stop
334
+
335
+ except Exception as e:
336
+ self.logger.error(f"Error stopping server on port {port}: {e}")
337
+ return False
338
+
339
+ def _cleanup_process(self, port: int):
340
+ """Clean up failed or terminated process."""
341
+ if port in self._server_processes:
342
+ process = self._server_processes[port]
343
+ try:
344
+ if process.poll() is None:
345
+ process.kill()
346
+ process.wait()
347
+ except:
348
+ pass
349
+ del self._server_processes[port]
350
+
351
+ def is_server_running(self, port: int) -> bool:
352
+ """
353
+ Check if a Socket.IO server is running on the specified port.
354
+
355
+ WHY: Quick check to determine if a server is already running to avoid
356
+ conflicts and duplicates.
357
+
358
+ Args:
359
+ port: Port to check
360
+
361
+ Returns:
362
+ True if server is running on the port
363
+ """
364
+ # First check our managed processes
365
+ if port in self._server_processes:
366
+ process = self._server_processes[port]
367
+ if process.poll() is None:
368
+ return True
369
+ # Process terminated, clean up
370
+ self._cleanup_process(port)
371
+
372
+ # Check if port is in use (by any process)
373
+ process_info = self.port_manager.get_process_on_port(port)
374
+ if process_info:
375
+ # Check if it's a Socket.IO server
376
+ return (
377
+ "socketio" in process_info.cmdline.lower()
378
+ or "socket-io" in process_info.cmdline.lower()
379
+ or process_info.is_daemon
380
+ )
381
+
382
+ return False
383
+
384
+ def get_server_info(self, port: int) -> ServerInfo:
385
+ """
386
+ Get information about a server on the specified port.
387
+
388
+ Args:
389
+ port: Port to check
390
+
391
+ Returns:
392
+ ServerInfo object with server details
393
+ """
394
+ is_running = self.is_server_running(port)
395
+ pid = None
396
+ launch_time = None
397
+
398
+ if is_running:
399
+ # Get process info
400
+ if port in self._server_processes:
401
+ process = self._server_processes[port]
402
+ pid = process.pid
403
+ launch_time = time.time() # Approximate
404
+ else:
405
+ process_info = self.port_manager.get_process_on_port(port)
406
+ if process_info:
407
+ pid = process_info.pid
408
+
409
+ return ServerInfo(
410
+ port=port,
411
+ pid=pid,
412
+ is_running=is_running,
413
+ launch_time=launch_time,
414
+ url=f"http://localhost:{port}",
415
+ )
416
+
417
+ def wait_for_server(self, port: int, timeout: int = 30) -> bool:
418
+ """
419
+ Wait for a server to be ready on the specified port.
420
+
421
+ WHY: Ensures the server is fully started and ready to accept connections
422
+ before proceeding with other operations.
423
+
424
+ Args:
425
+ port: Port to wait for
426
+ timeout: Maximum time to wait in seconds
427
+
428
+ Returns:
429
+ True if server became ready within timeout
430
+ """
431
+ start_time = time.time()
432
+
433
+ while time.time() - start_time < timeout:
434
+ # Try to connect to the server
435
+ try:
436
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
437
+ sock.settimeout(1)
438
+ result = sock.connect_ex(("localhost", port))
439
+ if result == 0:
440
+ self.logger.debug(f"Socket.IO server ready on port {port}")
441
+ return True
442
+ except Exception:
443
+ pass
444
+
445
+ time.sleep(0.5)
446
+
447
+ return False
448
+
449
+ def find_available_port(self, preferred_port: int = 8765) -> int:
450
+ """
451
+ Find an available port for the Socket.IO server.
452
+
453
+ WHY: Automatically finds an available port to avoid conflicts when the
454
+ preferred port is already in use.
455
+
456
+ Args:
457
+ preferred_port: Preferred port to try first
458
+
459
+ Returns:
460
+ Available port number
461
+ """
462
+ # Try preferred port first
463
+ if self.port_manager.is_port_available(preferred_port):
464
+ return preferred_port
465
+
466
+ # Find alternative port
467
+ available_port = self.port_manager.get_available_port(preferred_port)
468
+ self.logger.info(f"Port {preferred_port} unavailable, using {available_port}")
469
+ return available_port
470
+
471
+ def ensure_dependencies(self) -> Tuple[bool, Optional[str]]:
472
+ """
473
+ Ensure Socket.IO dependencies are installed.
474
+
475
+ WHY: Verifies that required Socket.IO packages are available before
476
+ attempting to start the server.
477
+
478
+ Returns:
479
+ Tuple of (success, error_message)
480
+ """
481
+ try:
482
+ # Use existing dependency manager
483
+ success, error_msg = ensure_socketio_dependencies(self.logger)
484
+
485
+ if not success:
486
+ # Provide helpful error message
487
+ if error_msg:
488
+ return False, error_msg
489
+ return False, (
490
+ "Socket.IO dependencies not installed. "
491
+ "Install with: pip install python-socketio aiohttp python-engineio "
492
+ "or pip install claude-mpm[monitor]"
493
+ )
494
+
495
+ return True, None
496
+
497
+ except Exception as e:
498
+ return False, str(e)