claude-mpm 3.1.3__py3-none-any.whl → 3.2.1__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 (79) hide show
  1. claude_mpm/__init__.py +3 -3
  2. claude_mpm/__main__.py +0 -17
  3. claude_mpm/agents/INSTRUCTIONS.md +81 -18
  4. claude_mpm/agents/backups/INSTRUCTIONS.md +238 -0
  5. claude_mpm/agents/base_agent.json +1 -1
  6. claude_mpm/agents/templates/pm.json +25 -0
  7. claude_mpm/agents/templates/research.json +2 -1
  8. claude_mpm/cli/__init__.py +19 -23
  9. claude_mpm/cli/commands/__init__.py +3 -1
  10. claude_mpm/cli/commands/agents.py +7 -18
  11. claude_mpm/cli/commands/info.py +5 -10
  12. claude_mpm/cli/commands/memory.py +232 -0
  13. claude_mpm/cli/commands/run.py +501 -28
  14. claude_mpm/cli/commands/tickets.py +10 -17
  15. claude_mpm/cli/commands/ui.py +15 -37
  16. claude_mpm/cli/parser.py +91 -1
  17. claude_mpm/cli/utils.py +9 -28
  18. claude_mpm/config/socketio_config.py +256 -0
  19. claude_mpm/constants.py +9 -0
  20. claude_mpm/core/__init__.py +2 -2
  21. claude_mpm/core/agent_registry.py +4 -4
  22. claude_mpm/core/claude_runner.py +919 -0
  23. claude_mpm/core/config.py +21 -1
  24. claude_mpm/core/factories.py +1 -1
  25. claude_mpm/core/hook_manager.py +196 -0
  26. claude_mpm/core/pm_hook_interceptor.py +205 -0
  27. claude_mpm/core/service_registry.py +1 -1
  28. claude_mpm/core/simple_runner.py +323 -33
  29. claude_mpm/core/socketio_pool.py +582 -0
  30. claude_mpm/core/websocket_handler.py +233 -0
  31. claude_mpm/deployment_paths.py +261 -0
  32. claude_mpm/hooks/builtin/memory_hooks_example.py +67 -0
  33. claude_mpm/hooks/claude_hooks/hook_handler.py +667 -679
  34. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -4
  35. claude_mpm/hooks/memory_integration_hook.py +312 -0
  36. claude_mpm/models/__init__.py +9 -91
  37. claude_mpm/orchestration/__init__.py +1 -1
  38. claude_mpm/scripts/claude-mpm-socketio +32 -0
  39. claude_mpm/scripts/claude_mpm_monitor.html +567 -0
  40. claude_mpm/scripts/install_socketio_server.py +407 -0
  41. claude_mpm/scripts/launch_monitor.py +132 -0
  42. claude_mpm/scripts/manage_version.py +479 -0
  43. claude_mpm/scripts/socketio_daemon.py +181 -0
  44. claude_mpm/scripts/socketio_server_manager.py +428 -0
  45. claude_mpm/services/__init__.py +5 -0
  46. claude_mpm/services/agent_lifecycle_manager.py +76 -25
  47. claude_mpm/services/agent_memory_manager.py +684 -0
  48. claude_mpm/services/agent_modification_tracker.py +98 -17
  49. claude_mpm/services/agent_persistence_service.py +33 -13
  50. claude_mpm/services/agent_registry.py +82 -43
  51. claude_mpm/services/hook_service.py +362 -0
  52. claude_mpm/services/socketio_client_manager.py +474 -0
  53. claude_mpm/services/socketio_server.py +698 -0
  54. claude_mpm/services/standalone_socketio_server.py +631 -0
  55. claude_mpm/services/ticket_manager.py +4 -5
  56. claude_mpm/services/{ticket_manager_dependency_injection.py → ticket_manager_di.py} +12 -39
  57. claude_mpm/services/{legacy_ticketing_service.py → ticketing_service_original.py} +9 -16
  58. claude_mpm/services/version_control/semantic_versioning.py +9 -10
  59. claude_mpm/services/websocket_server.py +376 -0
  60. claude_mpm/utils/dependency_manager.py +211 -0
  61. claude_mpm/utils/import_migration_example.py +80 -0
  62. claude_mpm/utils/path_operations.py +0 -20
  63. claude_mpm/web/open_dashboard.py +34 -0
  64. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/METADATA +20 -9
  65. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/RECORD +70 -50
  66. claude_mpm-3.2.1.dist-info/entry_points.txt +7 -0
  67. claude_mpm/cli_old.py +0 -728
  68. claude_mpm/models/common.py +0 -41
  69. claude_mpm/models/lifecycle.py +0 -97
  70. claude_mpm/models/modification.py +0 -126
  71. claude_mpm/models/persistence.py +0 -57
  72. claude_mpm/models/registry.py +0 -91
  73. claude_mpm/security/__init__.py +0 -8
  74. claude_mpm/security/bash_validator.py +0 -393
  75. claude_mpm-3.1.3.dist-info/entry_points.txt +0 -4
  76. /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
  77. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/WHEEL +0 -0
  78. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/licenses/LICENSE +0 -0
  79. {claude_mpm-3.1.3.dist-info → claude_mpm-3.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,474 @@
1
+ """Socket.IO client manager for connecting to standalone servers.
2
+
3
+ This module provides intelligent client management that:
4
+ 1. Detects existing standalone servers
5
+ 2. Performs version compatibility checks
6
+ 3. Handles graceful fallback to embedded servers
7
+ 4. Manages connection lifecycle and reconnection
8
+
9
+ WHY this approach:
10
+ - Enables seamless integration with standalone servers
11
+ - Provides compatibility checking before connection
12
+ - Handles both local and remote server scenarios
13
+ - Maintains backward compatibility with embedded servers
14
+ """
15
+
16
+ import asyncio
17
+ import json
18
+ import logging
19
+ import socket
20
+ import sys
21
+ import threading
22
+ import time
23
+ from datetime import datetime
24
+ from typing import Dict, Any, Optional, List, Tuple
25
+ import importlib.metadata
26
+
27
+ try:
28
+ import requests
29
+ import socketio
30
+ DEPENDENCIES_AVAILABLE = True
31
+ except ImportError:
32
+ DEPENDENCIES_AVAILABLE = False
33
+ requests = None
34
+ socketio = None
35
+
36
+ from ..core.logger import get_logger
37
+
38
+ # Get claude-mpm version for compatibility checking
39
+ try:
40
+ CLAUDE_MPM_VERSION = importlib.metadata.version('claude-mpm')
41
+ except Exception:
42
+ # Fallback for development
43
+ CLAUDE_MPM_VERSION = "0.7.0-dev"
44
+
45
+
46
+ class ServerInfo:
47
+ """Information about a detected Socket.IO server."""
48
+
49
+ def __init__(self, host: str, port: int, response_data: Dict[str, Any]):
50
+ self.host = host
51
+ self.port = port
52
+ self.server_version = response_data.get("server_version", "unknown")
53
+ self.server_id = response_data.get("server_id", "unknown")
54
+ self.socketio_version = response_data.get("socketio_version", "unknown")
55
+ self.features = response_data.get("features", [])
56
+ self.supported_client_versions = response_data.get("supported_client_versions", [])
57
+ self.compatibility_matrix = response_data.get("compatibility_matrix", {})
58
+ self.detected_at = datetime.utcnow()
59
+
60
+ @property
61
+ def url(self) -> str:
62
+ return f"http://{self.host}:{self.port}"
63
+
64
+ def is_compatible(self, client_version: str = CLAUDE_MPM_VERSION) -> Tuple[bool, List[str]]:
65
+ """Check if this server is compatible with the client version."""
66
+ warnings = []
67
+
68
+ try:
69
+ # Simple version comparison - in production use proper semver
70
+ if client_version >= "0.7.0":
71
+ return True, warnings
72
+ else:
73
+ warnings.append(f"Client version {client_version} may not be fully supported")
74
+ return False, warnings
75
+ except Exception as e:
76
+ warnings.append(f"Could not parse version: {e}")
77
+ return False, warnings
78
+
79
+
80
+ class SocketIOClientManager:
81
+ """Manages Socket.IO client connections with server discovery and compatibility checking."""
82
+
83
+ def __init__(self, client_version: str = CLAUDE_MPM_VERSION):
84
+ self.client_version = client_version
85
+ self.logger = get_logger("socketio_client_manager")
86
+
87
+ # Connection state
88
+ self.current_server: Optional[ServerInfo] = None
89
+ self.client: Optional[socketio.AsyncClient] = None
90
+ self.connected = False
91
+ self.connection_thread: Optional[threading.Thread] = None
92
+ self.running = False
93
+
94
+ # Server discovery
95
+ self.known_servers: Dict[str, ServerInfo] = {}
96
+ self.last_discovery = None
97
+
98
+ if not DEPENDENCIES_AVAILABLE:
99
+ self.logger.warning("Socket.IO client dependencies not available")
100
+
101
+ def discover_servers(self, ports: List[int] = None, hosts: List[str] = None) -> List[ServerInfo]:
102
+ """Discover available Socket.IO servers.
103
+
104
+ Args:
105
+ ports: List of ports to check (default: [8765, 8766, 8767])
106
+ hosts: List of hosts to check (default: ['localhost', '127.0.0.1'])
107
+
108
+ Returns:
109
+ List of discovered server info objects
110
+ """
111
+ if not DEPENDENCIES_AVAILABLE:
112
+ return []
113
+
114
+ ports = ports or [8765, 8766, 8767]
115
+ hosts = hosts or ['localhost', '127.0.0.1']
116
+
117
+ discovered = []
118
+
119
+ for host in hosts:
120
+ for port in ports:
121
+ try:
122
+ # Quick port check first
123
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
124
+ s.settimeout(0.5)
125
+ if s.connect_ex((host, port)) != 0:
126
+ continue
127
+
128
+ # Try to get server version info
129
+ try:
130
+ response = requests.get(
131
+ f"http://{host}:{port}/version",
132
+ timeout=2.0
133
+ )
134
+ if response.status_code == 200:
135
+ data = response.json()
136
+ server_info = ServerInfo(host, port, data)
137
+ discovered.append(server_info)
138
+
139
+ server_key = f"{host}:{port}"
140
+ self.known_servers[server_key] = server_info
141
+
142
+ self.logger.info(f"🔍 Discovered server: {server_info.server_id} "
143
+ f"v{server_info.server_version} at {server_info.url}")
144
+
145
+ except requests.RequestException:
146
+ # Not a standalone server, might be another service
147
+ continue
148
+
149
+ except Exception as e:
150
+ self.logger.debug(f"Error checking {host}:{port}: {e}")
151
+ continue
152
+
153
+ self.last_discovery = datetime.utcnow()
154
+ return discovered
155
+
156
+ def find_best_server(self, discovered_servers: List[ServerInfo] = None) -> Optional[ServerInfo]:
157
+ """Find the best compatible server from discovered servers."""
158
+ if discovered_servers is None:
159
+ discovered_servers = self.discover_servers()
160
+
161
+ if not discovered_servers:
162
+ return None
163
+
164
+ # Score servers based on compatibility and features
165
+ scored_servers = []
166
+
167
+ for server in discovered_servers:
168
+ compatible, warnings = server.is_compatible(self.client_version)
169
+
170
+ if not compatible:
171
+ self.logger.debug(f"Server {server.server_id} not compatible: {warnings}")
172
+ continue
173
+
174
+ # Simple scoring: newer versions and more features get higher scores
175
+ score = 0
176
+
177
+ # Version scoring (basic semantic versioning)
178
+ try:
179
+ version_parts = server.server_version.split('.')
180
+ score += int(version_parts[0]) * 1000 # Major version
181
+ score += int(version_parts[1]) * 100 # Minor version
182
+ score += int(version_parts[2]) * 10 # Patch version
183
+ except (ValueError, IndexError):
184
+ pass
185
+
186
+ # Feature scoring
187
+ score += len(server.features) * 5
188
+
189
+ scored_servers.append((score, server))
190
+
191
+ if not scored_servers:
192
+ self.logger.warning("No compatible servers found")
193
+ return None
194
+
195
+ # Return highest scored server
196
+ scored_servers.sort(key=lambda x: x[0], reverse=True)
197
+ best_server = scored_servers[0][1]
198
+
199
+ self.logger.info(f"🎯 Selected best server: {best_server.server_id} "
200
+ f"v{best_server.server_version} at {best_server.url}")
201
+
202
+ return best_server
203
+
204
+ async def connect_to_server(self, server_info: ServerInfo) -> bool:
205
+ """Connect to a specific server with compatibility verification."""
206
+ if not DEPENDENCIES_AVAILABLE:
207
+ return False
208
+
209
+ try:
210
+ # Perform compatibility check via HTTP first
211
+ compat_response = requests.post(
212
+ f"{server_info.url}/compatibility",
213
+ json={"client_version": self.client_version},
214
+ timeout=5.0
215
+ )
216
+
217
+ if compat_response.status_code == 200:
218
+ compatibility = compat_response.json()
219
+ if not compatibility.get("compatible", False):
220
+ self.logger.error(f"Server {server_info.server_id} rejected client version {self.client_version}")
221
+ return False
222
+
223
+ # Create Socket.IO client
224
+ self.client = socketio.AsyncClient(
225
+ reconnection=True,
226
+ reconnection_attempts=0, # Infinite
227
+ reconnection_delay=1,
228
+ reconnection_delay_max=5,
229
+ randomization_factor=0.5,
230
+ logger=False,
231
+ engineio_logger=False
232
+ )
233
+
234
+ # Setup event handlers
235
+ self._setup_client_event_handlers()
236
+
237
+ # Connect with authentication
238
+ auth = {
239
+ "claude_mpm_version": self.client_version,
240
+ "client_id": f"claude-mpm-{time.time()}"
241
+ }
242
+
243
+ await self.client.connect(server_info.url, auth=auth)
244
+
245
+ self.current_server = server_info
246
+ self.connected = True
247
+
248
+ self.logger.info(f"✅ Connected to server {server_info.server_id} at {server_info.url}")
249
+ return True
250
+
251
+ except Exception as e:
252
+ self.logger.error(f"❌ Failed to connect to server {server_info.url}: {e}")
253
+ if self.client:
254
+ try:
255
+ await self.client.disconnect()
256
+ except:
257
+ pass
258
+ self.client = None
259
+ return False
260
+
261
+ def _setup_client_event_handlers(self):
262
+ """Setup event handlers for the Socket.IO client."""
263
+
264
+ @self.client.event
265
+ async def connect():
266
+ self.logger.info("🔗 Socket.IO client connected")
267
+ self.connected = True
268
+
269
+ @self.client.event
270
+ async def disconnect():
271
+ self.logger.info("🔌 Socket.IO client disconnected")
272
+ self.connected = False
273
+
274
+ @self.client.event
275
+ async def connection_ack(data):
276
+ self.logger.info("🤝 Received connection acknowledgment")
277
+ compatibility = data.get("compatibility", {})
278
+ if not compatibility.get("compatible", True):
279
+ self.logger.warning(f"⚠️ Server compatibility warning: {compatibility.get('warnings', [])}")
280
+
281
+ @self.client.event
282
+ async def compatibility_warning(data):
283
+ self.logger.warning(f"⚠️ Server compatibility warning: {data}")
284
+
285
+ @self.client.event
286
+ async def server_status(data):
287
+ self.logger.debug(f"📊 Server status: {data.get('clients_connected', 0)} clients")
288
+
289
+ @self.client.event
290
+ async def claude_event(data):
291
+ """Handle events broadcasted from other clients."""
292
+ event_type = data.get("type", "unknown")
293
+ self.logger.debug(f"📥 Received claude_event: {event_type}")
294
+
295
+ async def emit_event(self, event_type: str, data: Dict[str, Any]) -> bool:
296
+ """Emit an event to the connected server."""
297
+ if not self.connected or not self.client:
298
+ return False
299
+
300
+ try:
301
+ event_data = {
302
+ "type": event_type,
303
+ "timestamp": datetime.utcnow().isoformat() + "Z",
304
+ "data": data,
305
+ "client_version": self.client_version
306
+ }
307
+
308
+ await self.client.emit('claude_event', event_data)
309
+ self.logger.debug(f"📤 Emitted event: {event_type}")
310
+ return True
311
+
312
+ except Exception as e:
313
+ self.logger.error(f"❌ Failed to emit event {event_type}: {e}")
314
+ return False
315
+
316
+ def start_connection_manager(self):
317
+ """Start the connection manager in a background thread."""
318
+ if self.running:
319
+ return
320
+
321
+ self.running = True
322
+ self.connection_thread = threading.Thread(target=self._run_connection_manager, daemon=True)
323
+ self.connection_thread.start()
324
+ self.logger.info("🚀 Connection manager started")
325
+
326
+ def stop_connection_manager(self):
327
+ """Stop the connection manager."""
328
+ self.running = False
329
+
330
+ if self.connection_thread:
331
+ self.connection_thread.join(timeout=5)
332
+
333
+ if self.client and self.connected:
334
+ try:
335
+ # Disconnect from server
336
+ loop = asyncio.new_event_loop()
337
+ loop.run_until_complete(self.client.disconnect())
338
+ loop.close()
339
+ except Exception as e:
340
+ self.logger.error(f"Error disconnecting client: {e}")
341
+
342
+ self.logger.info("🛑 Connection manager stopped")
343
+
344
+ def _run_connection_manager(self):
345
+ """Run the connection manager loop."""
346
+ loop = asyncio.new_event_loop()
347
+ asyncio.set_event_loop(loop)
348
+
349
+ try:
350
+ loop.run_until_complete(self._connection_manager_loop())
351
+ except Exception as e:
352
+ self.logger.error(f"Connection manager error: {e}")
353
+ finally:
354
+ loop.close()
355
+
356
+ async def _connection_manager_loop(self):
357
+ """Main connection manager loop."""
358
+ connection_attempts = 0
359
+ max_connection_attempts = 3
360
+
361
+ while self.running:
362
+ try:
363
+ if not self.connected:
364
+ if connection_attempts < max_connection_attempts:
365
+ # Try to find and connect to a server
366
+ best_server = self.find_best_server()
367
+
368
+ if best_server:
369
+ success = await self.connect_to_server(best_server)
370
+ if success:
371
+ connection_attempts = 0 # Reset on successful connection
372
+ else:
373
+ connection_attempts += 1
374
+ else:
375
+ self.logger.info("📡 No Socket.IO servers found, will retry...")
376
+ connection_attempts += 1
377
+ else:
378
+ # Too many failed attempts, wait longer
379
+ self.logger.warning(f"⏳ Max connection attempts reached, waiting 30s...")
380
+ await asyncio.sleep(30)
381
+ connection_attempts = 0 # Reset after longer wait
382
+
383
+ # Periodic health check
384
+ if self.connected and self.client:
385
+ try:
386
+ await self.client.emit('ping')
387
+ except Exception as e:
388
+ self.logger.warning(f"Health check failed: {e}")
389
+ self.connected = False
390
+
391
+ await asyncio.sleep(5) # Check every 5 seconds
392
+
393
+ except Exception as e:
394
+ self.logger.error(f"Error in connection manager loop: {e}")
395
+ await asyncio.sleep(5)
396
+
397
+ # Compatibility methods for existing WebSocket server interface
398
+
399
+ def broadcast_event(self, event_type: str, data: Dict[str, Any]):
400
+ """Legacy compatibility method for broadcasting events."""
401
+ if self.connected:
402
+ # Schedule emit in the connection thread
403
+ try:
404
+ loop = asyncio.new_event_loop()
405
+ loop.run_until_complete(self.emit_event(event_type, data))
406
+ loop.close()
407
+ except Exception as e:
408
+ self.logger.error(f"Error broadcasting event {event_type}: {e}")
409
+
410
+ def session_started(self, session_id: str, launch_method: str, working_dir: str):
411
+ """Compatibility method for session start events."""
412
+ self.broadcast_event("session.start", {
413
+ "session_id": session_id,
414
+ "launch_method": launch_method,
415
+ "working_directory": working_dir
416
+ })
417
+
418
+ def session_ended(self):
419
+ """Compatibility method for session end events."""
420
+ self.broadcast_event("session.end", {})
421
+
422
+ def claude_status_changed(self, status: str, pid: Optional[int] = None, message: str = ""):
423
+ """Compatibility method for Claude status events."""
424
+ self.broadcast_event("claude.status", {
425
+ "status": status,
426
+ "pid": pid,
427
+ "message": message
428
+ })
429
+
430
+ def agent_delegated(self, agent: str, task: str, status: str = "started"):
431
+ """Compatibility method for agent delegation events."""
432
+ self.broadcast_event("agent.delegation", {
433
+ "agent": agent,
434
+ "task": task,
435
+ "status": status
436
+ })
437
+
438
+ def todo_updated(self, todos: List[Dict[str, Any]]):
439
+ """Compatibility method for todo update events."""
440
+ self.broadcast_event("todo.update", {
441
+ "todos": todos
442
+ })
443
+
444
+ @property
445
+ def running_status(self) -> bool:
446
+ """Compatibility property."""
447
+ return self.running
448
+
449
+
450
+ # Global instance for easy access
451
+ _client_manager: Optional[SocketIOClientManager] = None
452
+
453
+
454
+ def get_client_manager() -> SocketIOClientManager:
455
+ """Get or create the global client manager instance."""
456
+ global _client_manager
457
+ if _client_manager is None:
458
+ _client_manager = SocketIOClientManager()
459
+ return _client_manager
460
+
461
+
462
+ def start_client_manager():
463
+ """Start the global client manager."""
464
+ manager = get_client_manager()
465
+ manager.start_connection_manager()
466
+ return manager
467
+
468
+
469
+ def stop_client_manager():
470
+ """Stop the global client manager."""
471
+ global _client_manager
472
+ if _client_manager:
473
+ _client_manager.stop_connection_manager()
474
+ _client_manager = None