claude-mpm 4.0.34__py3-none-any.whl → 4.1.0__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 (33) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +70 -2
  3. claude_mpm/agents/OUTPUT_STYLE.md +0 -11
  4. claude_mpm/agents/WORKFLOW.md +14 -2
  5. claude_mpm/cli/__init__.py +48 -7
  6. claude_mpm/cli/commands/agents.py +82 -0
  7. claude_mpm/cli/commands/cleanup_orphaned_agents.py +150 -0
  8. claude_mpm/cli/commands/mcp_pipx_config.py +199 -0
  9. claude_mpm/cli/parsers/agents_parser.py +27 -0
  10. claude_mpm/cli/parsers/base_parser.py +6 -0
  11. claude_mpm/cli/startup_logging.py +75 -0
  12. claude_mpm/dashboard/static/js/components/build-tracker.js +35 -1
  13. claude_mpm/dashboard/static/js/socket-client.js +7 -5
  14. claude_mpm/hooks/claude_hooks/connection_pool.py +13 -2
  15. claude_mpm/hooks/claude_hooks/hook_handler.py +67 -167
  16. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -1
  17. claude_mpm/services/agents/deployment/agent_template_builder.py +2 -1
  18. claude_mpm/services/agents/deployment/agent_version_manager.py +4 -1
  19. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +207 -10
  20. claude_mpm/services/event_bus/config.py +165 -0
  21. claude_mpm/services/event_bus/event_bus.py +35 -20
  22. claude_mpm/services/event_bus/relay.py +8 -12
  23. claude_mpm/services/mcp_gateway/auto_configure.py +372 -0
  24. claude_mpm/services/socketio/handlers/connection.py +3 -3
  25. claude_mpm/services/socketio/server/core.py +25 -2
  26. claude_mpm/services/socketio/server/eventbus_integration.py +189 -0
  27. claude_mpm/services/socketio/server/main.py +25 -0
  28. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/METADATA +25 -7
  29. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/RECORD +33 -28
  30. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/WHEEL +0 -0
  31. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/entry_points.txt +0 -0
  32. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/licenses/LICENSE +0 -0
  33. {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/top_level.txt +0 -0
@@ -338,6 +338,12 @@ def create_parser(
338
338
  from ..commands.cleanup import add_cleanup_parser
339
339
 
340
340
  add_cleanup_parser(subparsers)
341
+
342
+ # MCP pipx configuration command
343
+ if hasattr(CLICommands, "MCP_PIPX_CONFIG") or True: # Always add for now
344
+ from ..commands.mcp_pipx_config import add_parser as add_mcp_pipx_parser
345
+
346
+ add_mcp_pipx_parser(subparsers)
341
347
 
342
348
  from ..commands.doctor import add_doctor_parser
343
349
 
@@ -66,8 +66,10 @@ class StartupStatusLogger:
66
66
  self.logger.info(f"MCP Server: {config_status['servers_count']} server(s) configured")
67
67
  else:
68
68
  self.logger.info("MCP Server: No servers configured")
69
+ self._log_mcp_setup_hint()
69
70
  else:
70
71
  self.logger.info("MCP Server: No configuration found in ~/.claude.json")
72
+ self._log_mcp_setup_hint()
71
73
 
72
74
  # Check for claude-mpm MCP gateway status
73
75
  gateway_status = self._check_mcp_gateway_status()
@@ -75,6 +77,9 @@ class StartupStatusLogger:
75
77
  self.logger.info("MCP Gateway: Claude MPM gateway configured")
76
78
  else:
77
79
  self.logger.info("MCP Gateway: Claude MPM gateway not configured")
80
+ # Check if this is a pipx installation that could benefit from auto-config
81
+ if self._is_pipx_installation() and not self._has_auto_config_preference():
82
+ self.logger.info("MCP Gateway: Auto-configuration available for pipx users")
78
83
 
79
84
  except Exception as e:
80
85
  self.logger.warning(f"MCP Server: Status check failed - {e}")
@@ -293,6 +298,76 @@ class StartupStatusLogger:
293
298
  result["error"] = str(e)
294
299
 
295
300
  return result
301
+
302
+ def _is_pipx_installation(self) -> bool:
303
+ """Check if this is a pipx installation."""
304
+ try:
305
+ # Check if running from pipx
306
+ if "pipx" in sys.executable.lower():
307
+ return True
308
+
309
+ # Check module path
310
+ import claude_mpm
311
+ module_path = Path(claude_mpm.__file__).parent
312
+ if "pipx" in str(module_path):
313
+ return True
314
+ except Exception:
315
+ pass
316
+
317
+ return False
318
+
319
+ def _has_auto_config_preference(self) -> bool:
320
+ """Check if user has already been asked about auto-configuration."""
321
+ try:
322
+ from ..config.paths import paths
323
+ preference_file = paths.claude_mpm_dir_hidden / "mcp_auto_config_preference.json"
324
+ return preference_file.exists()
325
+ except Exception:
326
+ return False
327
+
328
+ def _log_mcp_setup_hint(self) -> None:
329
+ """Log helpful hints for MCP setup."""
330
+ # Check if installed via pipx
331
+ is_pipx = self._check_pipx_installation()
332
+
333
+ if is_pipx:
334
+ self.logger.info("💡 TIP: It looks like you installed claude-mpm via pipx")
335
+ self.logger.info(" To configure MCP for Claude Code with pipx:")
336
+ self.logger.info(" 1. Run: python3 scripts/configure_mcp_pipx.py")
337
+ self.logger.info(" 2. Or see: docs/MCP_PIPX_SETUP.md for manual setup")
338
+ self.logger.info(" 3. Restart Claude Code after configuration")
339
+ else:
340
+ self.logger.info("💡 TIP: To enable MCP integration with Claude Code:")
341
+ self.logger.info(" 1. See docs/MCP_SETUP.md for setup instructions")
342
+ self.logger.info(" 2. Run: claude-mpm doctor --check mcp to verify")
343
+ self.logger.info(" 3. Restart Claude Code after configuration")
344
+
345
+ def _check_pipx_installation(self) -> bool:
346
+ """Check if claude-mpm was installed via pipx."""
347
+ try:
348
+ # Check if running from a pipx venv
349
+ if "pipx" in sys.executable.lower():
350
+ return True
351
+
352
+ # Check if claude-mpm-mcp command exists and is from pipx
353
+ mcp_cmd = shutil.which("claude-mpm-mcp")
354
+ if mcp_cmd and "pipx" in mcp_cmd.lower():
355
+ return True
356
+
357
+ # Try to check pipx list
358
+ result = subprocess.run(
359
+ ["pipx", "list"],
360
+ capture_output=True,
361
+ text=True,
362
+ timeout=2
363
+ )
364
+ if result.returncode == 0 and "claude-mpm" in result.stdout:
365
+ return True
366
+
367
+ except Exception:
368
+ pass
369
+
370
+ return False
296
371
 
297
372
 
298
373
  def setup_startup_logging(project_root: Optional[Path] = None) -> Path:
@@ -35,12 +35,46 @@ export class BuildTracker {
35
35
  /**
36
36
  * Initialize the build tracker component
37
37
  */
38
- init() {
38
+ async init() {
39
39
  console.log('Initializing BuildTracker component');
40
+
41
+ // Try to load version.json for dashboard version
42
+ await this.loadDashboardVersion();
43
+
40
44
  this.createElements();
41
45
  this.setupEventListeners();
42
46
  }
43
47
 
48
+ /**
49
+ * Load dashboard version from version.json if available
50
+ *
51
+ * WHY: Attempts to load the actual dashboard version from the
52
+ * version.json file created by the version management script.
53
+ * Falls back to defaults if file is not available.
54
+ */
55
+ async loadDashboardVersion() {
56
+ try {
57
+ // Try to fetch version.json from the dashboard root
58
+ const response = await fetch('/version.json');
59
+ if (response.ok) {
60
+ const versionData = await response.json();
61
+
62
+ // Update monitor build info with loaded data
63
+ this.buildInfo.monitor = {
64
+ version: versionData.version || "1.0.0",
65
+ build: versionData.build || 1,
66
+ formatted_build: versionData.formatted_build || "0001",
67
+ full_version: versionData.full_version || "v1.0.0-0001"
68
+ };
69
+
70
+ console.log('Loaded dashboard version:', this.buildInfo.monitor);
71
+ }
72
+ } catch (error) {
73
+ // Silently fall back to defaults if version.json not available
74
+ console.debug('Dashboard version.json not available, using defaults');
75
+ }
76
+ }
77
+
44
78
  /**
45
79
  * Create the DOM elements for version display
46
80
  *
@@ -47,7 +47,7 @@ class SocketClient {
47
47
  // Health monitoring
48
48
  this.lastPingTime = null;
49
49
  this.lastPongTime = null;
50
- this.pingTimeout = 40000; // 40 seconds (server sends every 30s)
50
+ this.pingTimeout = 90000; // 90 seconds for health check (more lenient than Socket.IO timeout)
51
51
  this.healthCheckInterval = null;
52
52
 
53
53
  // Start periodic status check as fallback mechanism
@@ -97,11 +97,13 @@ class SocketClient {
97
97
  autoConnect: true,
98
98
  reconnection: true,
99
99
  reconnectionDelay: 1000,
100
- reconnectionDelayMax: 10000,
101
- maxReconnectionAttempts: 10,
102
- timeout: 10000,
100
+ reconnectionDelayMax: 5000,
101
+ reconnectionAttempts: Infinity, // Keep trying indefinitely
102
+ timeout: 20000, // Increase connection timeout
103
103
  forceNew: true,
104
- transports: ['websocket', 'polling']
104
+ transports: ['websocket', 'polling'],
105
+ pingInterval: 25000, // Match server setting
106
+ pingTimeout: 60000 // Match server setting
105
107
  });
106
108
 
107
109
  this.setupSocketHandlers();
@@ -1,7 +1,18 @@
1
1
  #!/usr/bin/env python3
2
- """Socket.IO connection pool for Claude Code hook handler.
2
+ """[DEPRECATED] Socket.IO connection pool for Claude Code hook handler.
3
3
 
4
- This module provides connection pooling for Socket.IO clients to reduce
4
+ DEPRECATION NOTICE: As of v4.0.35, this module is deprecated.
5
+ All event emission now goes through the EventBus, which handles
6
+ Socket.IO connections via its relay component. This provides:
7
+ - Single event path (no duplicates)
8
+ - Better separation of concerns
9
+ - Centralized connection management
10
+ - More resilient architecture
11
+
12
+ This module is kept for backward compatibility but will be removed in v5.0.0.
13
+ Please use EventBus.publish() instead of direct Socket.IO emission.
14
+
15
+ Original purpose: Provided connection pooling for Socket.IO clients to reduce
5
16
  connection overhead and implement circuit breaker patterns.
6
17
  """
7
18
 
@@ -60,6 +60,14 @@ except ImportError:
60
60
  }
61
61
  })
62
62
 
63
+ # Import EventBus for decoupled event distribution
64
+ try:
65
+ from claude_mpm.services.event_bus import EventBus
66
+ EVENTBUS_AVAILABLE = True
67
+ except ImportError:
68
+ EVENTBUS_AVAILABLE = False
69
+ EventBus = None
70
+
63
71
  # Import constants for configuration
64
72
  try:
65
73
  from claude_mpm.core.constants import NetworkConfig, RetryConfig, TimeoutConfig
@@ -111,13 +119,23 @@ class ClaudeHookHandler:
111
119
  """
112
120
 
113
121
  def __init__(self):
114
- # Socket.IO client (persistent if possible)
115
- self.connection_pool = SocketIOConnectionPool(max_connections=3)
116
122
  # Track events for periodic cleanup
117
123
  self.events_processed = 0
118
124
  self.last_cleanup = time.time()
119
125
  # Event normalizer for consistent event schema
120
126
  self.event_normalizer = EventNormalizer()
127
+
128
+ # Initialize EventBus for decoupled event distribution
129
+ self.event_bus = None
130
+ if EVENTBUS_AVAILABLE:
131
+ try:
132
+ self.event_bus = EventBus.get_instance()
133
+ if DEBUG:
134
+ print("✅ EventBus initialized for hook handler", file=sys.stderr)
135
+ except Exception as e:
136
+ if DEBUG:
137
+ print(f"⚠️ Failed to initialize EventBus: {e}", file=sys.stderr)
138
+ self.event_bus = None
121
139
 
122
140
  # Maximum sizes for tracking
123
141
  self.MAX_DELEGATION_TRACKING = 200
@@ -511,176 +529,61 @@ class ClaudeHookHandler:
511
529
  """
512
530
  print(json.dumps({"action": "continue"}))
513
531
 
514
- def _discover_socketio_port(self) -> int:
515
- """Discover the port of the running SocketIO server."""
516
- try:
517
- # Try to import port manager
518
- from claude_mpm.services.port_manager import PortManager
519
-
520
- port_manager = PortManager()
521
- instances = port_manager.list_active_instances()
522
-
523
- if instances:
524
- # Prefer port 8765 if available
525
- for instance in instances:
526
- if instance.get("port") == 8765:
527
- return 8765
528
- # Otherwise use the first active instance
529
- return instances[0].get("port", 8765)
530
- else:
531
- # No active instances, use default
532
- return 8765
533
- except Exception:
534
- # Fallback to environment variable or default
535
- return int(os.environ.get("CLAUDE_MPM_SOCKETIO_PORT", "8765"))
536
532
 
537
533
  def _emit_socketio_event(self, namespace: str, event: str, data: dict):
538
- """Emit Socket.IO event with improved reliability and event normalization.
539
-
540
- WHY improved approach:
541
- - Uses EventNormalizer for consistent event schema
542
- - Maintains persistent connections throughout handler lifecycle
543
- - Better error handling and automatic recovery
544
- - Connection health monitoring before emission
545
- - Automatic reconnection for critical events
546
- - All events normalized to standard schema before emission
534
+ """Emit event through EventBus for Socket.IO relay.
535
+
536
+ WHY EventBus-only approach:
537
+ - Single event path prevents duplicates
538
+ - EventBus relay handles Socket.IO connection management
539
+ - Better separation of concerns
540
+ - More resilient with centralized failure handling
541
+ - Cleaner architecture and easier testing
547
542
  """
548
- # Always try to emit Socket.IO events if available
549
- # The daemon should be running when manager is active
550
-
551
- # Get Socket.IO client with dynamic port discovery
552
- port = self._discover_socketio_port()
553
- client = self.connection_pool.get_connection(port)
543
+ # Create event data for normalization
544
+ raw_event = {
545
+ "type": "hook",
546
+ "subtype": event, # e.g., "user_prompt", "pre_tool", "subagent_stop"
547
+ "timestamp": datetime.now().isoformat(),
548
+ "data": data,
549
+ "source": "claude_hooks", # Identify the source
550
+ "session_id": data.get("sessionId"), # Include session if available
551
+ }
554
552
 
555
- # If no client available, try to create one
556
- if not client:
557
- if DEBUG:
553
+ # Normalize the event using EventNormalizer for consistent schema
554
+ normalized_event = self.event_normalizer.normalize(raw_event, source="hook")
555
+ claude_event_data = normalized_event.to_dict()
556
+
557
+ # Log important events for debugging
558
+ if DEBUG and event in ["subagent_stop", "pre_tool"]:
559
+ if event == "subagent_stop":
560
+ agent_type = data.get("agent_type", "unknown")
558
561
  print(
559
- f"Hook handler: No Socket.IO client available, attempting to create connection for event: hook.{event}",
562
+ f"Hook handler: Publishing SubagentStop for agent '{agent_type}'",
560
563
  file=sys.stderr,
561
564
  )
562
- # Force creation of a new connection
563
- client = self.connection_pool._create_connection(port)
564
- if client:
565
- # Add to pool for future use
566
- self.connection_pool.connections.append(
567
- {"port": port, "client": client, "created": time.time()}
565
+ elif event == "pre_tool" and data.get("tool_name") == "Task":
566
+ delegation = data.get("delegation_details", {})
567
+ agent_type = delegation.get("agent_type", "unknown")
568
+ print(
569
+ f"Hook handler: Publishing Task delegation to agent '{agent_type}'",
570
+ file=sys.stderr,
568
571
  )
569
- else:
572
+
573
+ # Publish to EventBus for distribution through relay
574
+ if self.event_bus and EVENTBUS_AVAILABLE:
575
+ try:
576
+ # Publish to EventBus with topic format: hook.{event}
577
+ topic = f"hook.{event}"
578
+ self.event_bus.publish(topic, claude_event_data)
570
579
  if DEBUG:
571
- print(
572
- f"Hook handler: Failed to create Socket.IO connection for event: hook.{event}",
573
- file=sys.stderr,
574
- )
575
- return
576
-
577
- try:
578
- # Verify connection is alive before emitting
579
- if not client.connected:
580
+ print(f"✅ Published to EventBus: {topic}", file=sys.stderr)
581
+ except Exception as e:
580
582
  if DEBUG:
581
- print(
582
- f"Hook handler: Client not connected, attempting reconnection for event: hook.{event}",
583
- file=sys.stderr,
584
- )
585
- # Try to reconnect
586
- try:
587
- client.connect(
588
- f"http://localhost:{port}",
589
- wait=True,
590
- wait_timeout=1.0,
591
- transports=['websocket', 'polling'],
592
- )
593
- except:
594
- # If reconnection fails, get a fresh client
595
- client = self.connection_pool._create_connection(port)
596
- if not client:
597
- if DEBUG:
598
- print(
599
- f"Hook handler: Reconnection failed for event: hook.{event}",
600
- file=sys.stderr,
601
- )
602
- return
603
-
604
- # Create event data for normalization
605
- raw_event = {
606
- "type": "hook",
607
- "subtype": event, # e.g., "user_prompt", "pre_tool", "subagent_stop"
608
- "timestamp": datetime.now().isoformat(),
609
- "data": data,
610
- "source": "claude_hooks", # Identify the source
611
- "session_id": data.get("sessionId"), # Include session if available
612
- }
613
-
614
- # Normalize the event using EventNormalizer for consistent schema
615
- # Pass source explicitly to ensure it's set correctly
616
- normalized_event = self.event_normalizer.normalize(raw_event, source="hook")
617
- claude_event_data = normalized_event.to_dict()
618
-
619
- # Log important events for debugging
620
- if DEBUG and event in ["subagent_stop", "pre_tool"]:
621
- if event == "subagent_stop":
622
- agent_type = data.get("agent_type", "unknown")
623
- print(
624
- f"Hook handler: Emitting SubagentStop for agent '{agent_type}'",
625
- file=sys.stderr,
626
- )
627
- elif event == "pre_tool" and data.get("tool_name") == "Task":
628
- delegation = data.get("delegation_details", {})
629
- agent_type = delegation.get("agent_type", "unknown")
630
- print(
631
- f"Hook handler: Emitting Task delegation to agent '{agent_type}'",
632
- file=sys.stderr,
633
- )
634
-
635
- # Emit synchronously
636
- client.emit("claude_event", claude_event_data)
637
-
638
- # For critical events, wait a moment to ensure delivery
639
- if event in ["subagent_stop", "pre_tool"]:
640
- time.sleep(0.01) # Small delay to ensure event is sent
641
-
642
- # Verify emission for critical events
643
- if event in ["subagent_stop", "pre_tool"] and DEBUG:
644
- if client.connected:
645
- print(
646
- f"✅ Successfully emitted Socket.IO event: hook.{event} (connection still active)",
647
- file=sys.stderr,
648
- )
649
- else:
650
- print(
651
- f"⚠️ Event emitted but connection closed after: hook.{event}",
652
- file=sys.stderr,
653
- )
654
-
655
- except Exception as e:
583
+ print(f"⚠️ Failed to publish to EventBus: {e}", file=sys.stderr)
584
+ else:
656
585
  if DEBUG:
657
- print(f" Socket.IO emit failed for hook.{event}: {e}", file=sys.stderr)
658
-
659
- # Try to reconnect immediately for critical events
660
- if event in ["subagent_stop", "pre_tool"]:
661
- if DEBUG:
662
- print(
663
- f"Hook handler: Attempting immediate reconnection for critical event: hook.{event}",
664
- file=sys.stderr,
665
- )
666
- # Force get a new client and emit again
667
- self.connection_pool._cleanup_dead_connections()
668
- retry_client = self.connection_pool._create_connection(port)
669
- if retry_client:
670
- try:
671
- retry_client.emit("claude_event", claude_event_data)
672
- # Add to pool for future use
673
- self.connection_pool.connections.append(
674
- {"port": port, "client": retry_client, "created": time.time()}
675
- )
676
- if DEBUG:
677
- print(
678
- f"✅ Successfully re-emitted event after reconnection: hook.{event}",
679
- file=sys.stderr,
680
- )
681
- except Exception as retry_e:
682
- if DEBUG:
683
- print(f"❌ Re-emission failed: {retry_e}", file=sys.stderr)
586
+ print(f"⚠️ EventBus not available for event: hook.{event}", file=sys.stderr)
684
587
 
685
588
  def handle_subagent_stop(self, event: dict):
686
589
  """Handle subagent stop events with improved agent type detection.
@@ -1012,12 +915,9 @@ class ClaudeHookHandler:
1012
915
  self._emit_socketio_event("/hook", "subagent_stop", subagent_stop_data)
1013
916
 
1014
917
  def __del__(self):
1015
- """Cleanup Socket.IO connections on handler destruction."""
1016
- if hasattr(self, "connection_pool") and self.connection_pool:
1017
- try:
1018
- self.connection_pool.close_all()
1019
- except:
1020
- pass
918
+ """Cleanup on handler destruction."""
919
+ # Connection pool no longer used - EventBus handles cleanup
920
+ pass
1021
921
 
1022
922
 
1023
923
  def main():
@@ -198,7 +198,10 @@ class AgentDiscoveryService:
198
198
  "name": metadata.get("name", template_file.stem),
199
199
  "description": metadata.get("description", "No description available"),
200
200
  "version": template_data.get(
201
- "agent_version", template_data.get("version", "1.0.0")
201
+ "agent_version",
202
+ template_data.get("version",
203
+ metadata.get("version", "1.0.0")
204
+ )
202
205
  ),
203
206
  "tools": capabilities.get("tools", []),
204
207
  "specializations": metadata.get(
@@ -136,7 +136,8 @@ class AgentTemplateBuilder:
136
136
  )
137
137
 
138
138
  # Extract custom metadata fields
139
- agent_version = template_data.get("agent_version", "1.0.0")
139
+ metadata = template_data.get("metadata", {})
140
+ agent_version = template_data.get("agent_version") or template_data.get("version") or metadata.get("version", "1.0.0")
140
141
  agent_type = template_data.get("agent_type", "general")
141
142
  # Use the capabilities_model we already extracted earlier
142
143
  model_type = capabilities_model or "sonnet"
@@ -254,8 +254,11 @@ class AgentVersionManager:
254
254
  template_data = json.loads(template_file.read_text())
255
255
 
256
256
  # Extract agent version from template
257
+ metadata = template_data.get("metadata", {})
257
258
  current_agent_version = self.parse_version(
258
- template_data.get("agent_version") or template_data.get("version", 0)
259
+ template_data.get("agent_version") or
260
+ template_data.get("version") or
261
+ metadata.get("version", 0)
259
262
  )
260
263
 
261
264
  # If old format detected, always trigger update for migration