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
@@ -507,6 +507,7 @@ class MultiSourceAgentDeploymentService:
507
507
  "needs_update": [],
508
508
  "up_to_date": [],
509
509
  "new_agents": [],
510
+ "orphaned_agents": [], # Agents without templates
510
511
  "version_upgrades": [],
511
512
  "version_downgrades": [],
512
513
  "source_changes": []
@@ -527,9 +528,11 @@ class MultiSourceAgentDeploymentService:
527
528
  # Read template version
528
529
  try:
529
530
  template_data = json.loads(template_path.read_text())
531
+ metadata = template_data.get("metadata", {})
530
532
  template_version = self.version_manager.parse_version(
531
533
  template_data.get("agent_version") or
532
- template_data.get("version", "0.0.0")
534
+ template_data.get("version") or
535
+ metadata.get("version", "0.0.0")
533
536
  )
534
537
  except Exception as e:
535
538
  self.logger.warning(f"Error reading template for '{agent_name}': {e}")
@@ -597,13 +600,20 @@ class MultiSourceAgentDeploymentService:
597
600
  "source": agent_sources[agent_name]
598
601
  })
599
602
 
603
+ # Check for orphaned agents (deployed but no template)
604
+ orphaned = self._detect_orphaned_agents_simple(deployed_agents_dir, agents_to_deploy)
605
+ comparison_results["orphaned_agents"] = orphaned
606
+
600
607
  # Log summary
601
- self.logger.info(
602
- f"Version comparison complete: "
603
- f"{len(comparison_results['needs_update'])} need updates, "
604
- f"{len(comparison_results['up_to_date'])} up to date, "
608
+ summary_parts = [
609
+ f"{len(comparison_results['needs_update'])} need updates",
610
+ f"{len(comparison_results['up_to_date'])} up to date",
605
611
  f"{len(comparison_results['new_agents'])} new agents"
606
- )
612
+ ]
613
+ if comparison_results["orphaned_agents"]:
614
+ summary_parts.append(f"{len(comparison_results['orphaned_agents'])} orphaned")
615
+
616
+ self.logger.info(f"Version comparison complete: {', '.join(summary_parts)}")
607
617
 
608
618
  if comparison_results["version_upgrades"]:
609
619
  for upgrade in comparison_results["version_upgrades"]:
@@ -622,10 +632,24 @@ class MultiSourceAgentDeploymentService:
622
632
 
623
633
  if comparison_results["version_downgrades"]:
624
634
  for downgrade in comparison_results["version_downgrades"]:
625
- self.logger.warning(
626
- f" Warning: {downgrade['name']} deployed version "
635
+ # Changed from warning to debug - deployed versions higher than templates
636
+ # are not errors, just informational
637
+ self.logger.debug(
638
+ f" Note: {downgrade['name']} deployed version "
627
639
  f"{downgrade['deployed_version']} is higher than template "
628
- f"{downgrade['template_version']}"
640
+ f"{downgrade['template_version']} (keeping deployed version)"
641
+ )
642
+
643
+ # Log orphaned agents if found
644
+ if comparison_results["orphaned_agents"]:
645
+ self.logger.info(
646
+ f"Found {len(comparison_results['orphaned_agents'])} orphaned agent(s) "
647
+ f"(deployed without templates):"
648
+ )
649
+ for orphan in comparison_results["orphaned_agents"]:
650
+ self.logger.info(
651
+ f" - {orphan['name']} v{orphan['version']} "
652
+ f"(consider removing or creating a template)"
629
653
  )
630
654
 
631
655
  return comparison_results
@@ -692,4 +716,177 @@ class MultiSourceAgentDeploymentService:
692
716
  return "system"
693
717
 
694
718
  # Complex names are more likely to be user/project agents
695
- return "user"
719
+ return "user"
720
+
721
+ def detect_orphaned_agents(
722
+ self,
723
+ deployed_agents_dir: Path,
724
+ available_agents: Dict[str, Any]
725
+ ) -> List[Dict[str, Any]]:
726
+ """Detect deployed agents that don't have corresponding templates.
727
+
728
+ WHY: Orphaned agents can cause confusion with version warnings.
729
+ This method identifies them so they can be handled appropriately.
730
+
731
+ Args:
732
+ deployed_agents_dir: Directory containing deployed agents
733
+ available_agents: Dictionary of available agents from all sources
734
+
735
+ Returns:
736
+ List of orphaned agent information
737
+ """
738
+ orphaned = []
739
+
740
+ if not deployed_agents_dir.exists():
741
+ return orphaned
742
+
743
+ # Build a mapping of file stems to agent names for comparison
744
+ # Since available_agents uses display names like "Code Analysis Agent"
745
+ # but deployed files use stems like "code_analyzer"
746
+ available_stems = set()
747
+ stem_to_name = {}
748
+
749
+ for agent_name, agent_sources in available_agents.items():
750
+ # Get the file path from the first source to extract the stem
751
+ if agent_sources and isinstance(agent_sources, list) and len(agent_sources) > 0:
752
+ first_source = agent_sources[0]
753
+ if 'file_path' in first_source:
754
+ file_path = Path(first_source['file_path'])
755
+ stem = file_path.stem
756
+ available_stems.add(stem)
757
+ stem_to_name[stem] = agent_name
758
+
759
+ for deployed_file in deployed_agents_dir.glob("*.md"):
760
+ agent_stem = deployed_file.stem
761
+
762
+ # Skip if this agent has a template (check by stem, not display name)
763
+ if agent_stem in available_stems:
764
+ continue
765
+
766
+ # This is an orphaned agent
767
+ try:
768
+ deployed_content = deployed_file.read_text()
769
+ deployed_version, _, _ = self.version_manager.extract_version_from_frontmatter(
770
+ deployed_content
771
+ )
772
+ version_str = self.version_manager.format_version_display(deployed_version)
773
+ except Exception:
774
+ version_str = "unknown"
775
+
776
+ orphaned.append({
777
+ "name": agent_stem,
778
+ "file": str(deployed_file),
779
+ "version": version_str
780
+ })
781
+
782
+ return orphaned
783
+
784
+ def _detect_orphaned_agents_simple(
785
+ self,
786
+ deployed_agents_dir: Path,
787
+ agents_to_deploy: Dict[str, Path]
788
+ ) -> List[Dict[str, Any]]:
789
+ """Simple orphan detection that works with agents_to_deploy structure.
790
+
791
+ Args:
792
+ deployed_agents_dir: Directory containing deployed agents
793
+ agents_to_deploy: Dictionary mapping file stems to template paths
794
+
795
+ Returns:
796
+ List of orphaned agent information
797
+ """
798
+ orphaned = []
799
+
800
+ if not deployed_agents_dir.exists():
801
+ return orphaned
802
+
803
+ # agents_to_deploy already contains file stems as keys
804
+ available_stems = set(agents_to_deploy.keys())
805
+
806
+ for deployed_file in deployed_agents_dir.glob("*.md"):
807
+ agent_stem = deployed_file.stem
808
+
809
+ # Skip if this agent has a template (check by stem)
810
+ if agent_stem in available_stems:
811
+ continue
812
+
813
+ # This is an orphaned agent
814
+ try:
815
+ deployed_content = deployed_file.read_text()
816
+ deployed_version, _, _ = self.version_manager.extract_version_from_frontmatter(
817
+ deployed_content
818
+ )
819
+ version_str = self.version_manager.format_version_display(deployed_version)
820
+ except Exception:
821
+ version_str = "unknown"
822
+
823
+ orphaned.append({
824
+ "name": agent_stem,
825
+ "file": str(deployed_file),
826
+ "version": version_str
827
+ })
828
+
829
+ return orphaned
830
+
831
+ def cleanup_orphaned_agents(
832
+ self,
833
+ deployed_agents_dir: Path,
834
+ dry_run: bool = True
835
+ ) -> Dict[str, Any]:
836
+ """Clean up orphaned agents that don't have templates.
837
+
838
+ WHY: Orphaned agents can accumulate over time and cause confusion.
839
+ This method provides a way to clean them up systematically.
840
+
841
+ Args:
842
+ deployed_agents_dir: Directory containing deployed agents
843
+ dry_run: If True, only report what would be removed
844
+
845
+ Returns:
846
+ Dictionary with cleanup results
847
+ """
848
+ results = {
849
+ "orphaned": [],
850
+ "removed": [],
851
+ "errors": []
852
+ }
853
+
854
+ # First, discover all available agents from all sources
855
+ all_agents = self.discover_agents_from_all_sources()
856
+ available_names = set(all_agents.keys())
857
+
858
+ # Detect orphaned agents
859
+ orphaned = self.detect_orphaned_agents(deployed_agents_dir, all_agents)
860
+ results["orphaned"] = orphaned
861
+
862
+ if not orphaned:
863
+ self.logger.info("No orphaned agents found")
864
+ return results
865
+
866
+ self.logger.info(f"Found {len(orphaned)} orphaned agent(s)")
867
+
868
+ for orphan in orphaned:
869
+ agent_file = Path(orphan["file"])
870
+
871
+ if dry_run:
872
+ self.logger.info(
873
+ f" Would remove: {orphan['name']} v{orphan['version']}"
874
+ )
875
+ else:
876
+ try:
877
+ agent_file.unlink()
878
+ results["removed"].append(orphan["name"])
879
+ self.logger.info(
880
+ f" Removed: {orphan['name']} v{orphan['version']}"
881
+ )
882
+ except Exception as e:
883
+ error_msg = f"Failed to remove {orphan['name']}: {e}"
884
+ results["errors"].append(error_msg)
885
+ self.logger.error(f" {error_msg}")
886
+
887
+ if dry_run and orphaned:
888
+ self.logger.info(
889
+ "Run with dry_run=False to actually remove orphaned agents"
890
+ )
891
+
892
+ return results
@@ -0,0 +1,165 @@
1
+ """Configuration for EventBus service.
2
+
3
+ WHY configuration module:
4
+ - Centralized configuration management
5
+ - Environment variable support
6
+ - Easy testing with different configurations
7
+ - Runtime configuration changes
8
+ """
9
+
10
+ import os
11
+ from typing import List, Optional
12
+ from dataclasses import dataclass, field
13
+
14
+
15
+ @dataclass
16
+ class EventBusConfig:
17
+ """Configuration for EventBus service.
18
+
19
+ All settings can be overridden via environment variables.
20
+ """
21
+
22
+ # Enable/disable the EventBus
23
+ enabled: bool = field(
24
+ default_factory=lambda: os.environ.get("CLAUDE_MPM_EVENTBUS_ENABLED", "true").lower() == "true"
25
+ )
26
+
27
+ # Debug logging
28
+ debug: bool = field(
29
+ default_factory=lambda: os.environ.get("CLAUDE_MPM_EVENTBUS_DEBUG", "false").lower() == "true"
30
+ )
31
+
32
+ # Event history settings
33
+ max_history_size: int = field(
34
+ default_factory=lambda: int(os.environ.get("CLAUDE_MPM_EVENTBUS_HISTORY_SIZE", "100"))
35
+ )
36
+
37
+ # Event filters (comma-separated list)
38
+ event_filters: List[str] = field(
39
+ default_factory=lambda: [
40
+ f.strip() for f in os.environ.get("CLAUDE_MPM_EVENTBUS_FILTERS", "").split(",")
41
+ if f.strip()
42
+ ]
43
+ )
44
+
45
+ # Relay configuration
46
+ relay_enabled: bool = field(
47
+ default_factory=lambda: os.environ.get("CLAUDE_MPM_RELAY_ENABLED", "true").lower() == "true"
48
+ )
49
+
50
+ relay_port: int = field(
51
+ default_factory=lambda: int(os.environ.get("CLAUDE_MPM_SOCKETIO_PORT", "8765"))
52
+ )
53
+
54
+ relay_debug: bool = field(
55
+ default_factory=lambda: os.environ.get("CLAUDE_MPM_RELAY_DEBUG", "false").lower() == "true"
56
+ )
57
+
58
+ # Connection settings
59
+ relay_max_retries: int = field(
60
+ default_factory=lambda: int(os.environ.get("CLAUDE_MPM_RELAY_MAX_RETRIES", "3"))
61
+ )
62
+
63
+ relay_retry_delay: float = field(
64
+ default_factory=lambda: float(os.environ.get("CLAUDE_MPM_RELAY_RETRY_DELAY", "0.5"))
65
+ )
66
+
67
+ relay_connection_cooldown: float = field(
68
+ default_factory=lambda: float(os.environ.get("CLAUDE_MPM_RELAY_CONNECTION_COOLDOWN", "5.0"))
69
+ )
70
+
71
+ @classmethod
72
+ def from_env(cls) -> "EventBusConfig":
73
+ """Create configuration from environment variables.
74
+
75
+ Returns:
76
+ EventBusConfig: Configuration instance
77
+ """
78
+ return cls()
79
+
80
+ def to_dict(self) -> dict:
81
+ """Convert configuration to dictionary.
82
+
83
+ Returns:
84
+ dict: Configuration as dictionary
85
+ """
86
+ return {
87
+ "enabled": self.enabled,
88
+ "debug": self.debug,
89
+ "max_history_size": self.max_history_size,
90
+ "event_filters": self.event_filters,
91
+ "relay_enabled": self.relay_enabled,
92
+ "relay_port": self.relay_port,
93
+ "relay_debug": self.relay_debug,
94
+ "relay_max_retries": self.relay_max_retries,
95
+ "relay_retry_delay": self.relay_retry_delay,
96
+ "relay_connection_cooldown": self.relay_connection_cooldown
97
+ }
98
+
99
+ def apply_to_eventbus(self, event_bus) -> None:
100
+ """Apply configuration to an EventBus instance.
101
+
102
+ Args:
103
+ event_bus: EventBus instance to configure
104
+ """
105
+ if not self.enabled:
106
+ event_bus.disable()
107
+ else:
108
+ event_bus.enable()
109
+
110
+ event_bus.set_debug(self.debug)
111
+ event_bus._max_history_size = self.max_history_size
112
+
113
+ # Apply filters
114
+ event_bus.clear_filters()
115
+ for filter_pattern in self.event_filters:
116
+ event_bus.add_filter(filter_pattern)
117
+
118
+ def apply_to_relay(self, relay) -> None:
119
+ """Apply configuration to a SocketIORelay instance.
120
+
121
+ Args:
122
+ relay: SocketIORelay instance to configure
123
+ """
124
+ if not self.relay_enabled:
125
+ relay.disable()
126
+ else:
127
+ relay.enable()
128
+
129
+ relay.port = self.relay_port
130
+ relay.debug = self.relay_debug
131
+ relay.max_retries = self.relay_max_retries
132
+ relay.retry_delay = self.relay_retry_delay
133
+ relay.connection_cooldown = self.relay_connection_cooldown
134
+
135
+
136
+ # Global configuration instance
137
+ _config: Optional[EventBusConfig] = None
138
+
139
+
140
+ def get_config() -> EventBusConfig:
141
+ """Get the global EventBus configuration.
142
+
143
+ Returns:
144
+ EventBusConfig: Configuration instance
145
+ """
146
+ global _config
147
+ if _config is None:
148
+ _config = EventBusConfig.from_env()
149
+ return _config
150
+
151
+
152
+ def set_config(config: EventBusConfig) -> None:
153
+ """Set the global EventBus configuration.
154
+
155
+ Args:
156
+ config: Configuration to set
157
+ """
158
+ global _config
159
+ _config = config
160
+
161
+
162
+ def reset_config() -> None:
163
+ """Reset configuration to defaults from environment."""
164
+ global _config
165
+ _config = EventBusConfig.from_env()
@@ -13,7 +13,7 @@ import logging
13
13
  import threading
14
14
  from datetime import datetime
15
15
  from typing import Any, Callable, Dict, List, Optional, Set
16
- from pyee import AsyncIOEventEmitter
16
+ from pyee.asyncio import AsyncIOEventEmitter
17
17
 
18
18
  # Configure logger
19
19
  logger = logging.getLogger(__name__)
@@ -176,9 +176,33 @@ class EventBus:
176
176
  # Record event in history
177
177
  self._record_event(event_type, data)
178
178
 
179
- # Emit event (pyee handles thread safety)
179
+ # Emit event to regular handlers (pyee handles thread safety)
180
180
  self._emitter.emit(event_type, data)
181
181
 
182
+ # Also emit to wildcard handlers
183
+ if hasattr(self, '_wildcard_handlers'):
184
+ for prefix, handlers in self._wildcard_handlers.items():
185
+ if event_type.startswith(prefix):
186
+ for handler in handlers:
187
+ try:
188
+ # Call with event_type and data for wildcard handlers
189
+ if asyncio.iscoroutinefunction(handler):
190
+ # Schedule async handlers
191
+ try:
192
+ loop = asyncio.get_event_loop()
193
+ if loop.is_running():
194
+ asyncio.create_task(handler(event_type, data))
195
+ else:
196
+ loop.run_until_complete(handler(event_type, data))
197
+ except RuntimeError:
198
+ # No event loop, skip async handler
199
+ pass
200
+ else:
201
+ handler(event_type, data)
202
+ except Exception as e:
203
+ if self._debug:
204
+ logger.debug(f"Wildcard handler error: {e}")
205
+
182
206
  # Update stats
183
207
  self._stats["events_published"] += 1
184
208
  self._stats["last_event_time"] = datetime.now().isoformat()
@@ -217,29 +241,20 @@ class EventBus:
217
241
  handler: The handler function
218
242
  """
219
243
  if event_type.endswith("*"):
220
- # Register for wildcard pattern
221
- prefix = event_type[:-1]
244
+ # Store wildcard handlers separately
245
+ if not hasattr(self, '_wildcard_handlers'):
246
+ self._wildcard_handlers = {}
222
247
 
223
- # Create a wrapper that checks event names
224
- def wildcard_wrapper(actual_event_type: str):
225
- def wrapper(data):
226
- if actual_event_type.startswith(prefix):
227
- if asyncio.iscoroutinefunction(handler):
228
- return handler(actual_event_type, data)
229
- else:
230
- handler(actual_event_type, data)
231
- return wrapper
248
+ prefix = event_type[:-1]
249
+ if prefix not in self._wildcard_handlers:
250
+ self._wildcard_handlers[prefix] = []
251
+ self._wildcard_handlers[prefix].append(handler)
232
252
 
233
- # Register for all possible events (we'll filter in the wrapper)
234
- # For now, register common prefixes
235
- for common_event in ["hook", "socketio", "system", "agent"]:
236
- if common_event.startswith(prefix) or prefix.startswith(common_event):
237
- self._emitter.on(f"{common_event}.*", wildcard_wrapper(f"{common_event}.*"))
253
+ logger.debug(f"Registered wildcard handler for: {event_type}")
238
254
  else:
239
255
  # Regular event registration
240
256
  self._emitter.on(event_type, handler)
241
-
242
- logger.debug(f"Registered handler for: {event_type}")
257
+ logger.debug(f"Registered handler for: {event_type}")
243
258
 
244
259
  def once(self, event_type: str, handler: Callable) -> None:
245
260
  """Register a one-time event handler.
@@ -106,20 +106,21 @@ class SocketIORelay:
106
106
  self.last_connection_attempt = current_time
107
107
 
108
108
  try:
109
- # Create new client
109
+ # Create new client with better connection settings
110
110
  self.client = socketio.Client(
111
111
  reconnection=True,
112
- reconnection_attempts=3,
113
- reconnection_delay=1,
112
+ reconnection_attempts=5,
113
+ reconnection_delay=2,
114
+ reconnection_delay_max=10,
114
115
  logger=False,
115
116
  engineio_logger=False
116
117
  )
117
118
 
118
- # Connect to server
119
+ # Connect to server with longer timeout
119
120
  self.client.connect(
120
121
  f"http://localhost:{self.port}",
121
122
  wait=True,
122
- wait_timeout=2.0,
123
+ wait_timeout=10.0, # Increase timeout for stability
123
124
  transports=['websocket', 'polling']
124
125
  )
125
126
 
@@ -219,15 +220,10 @@ class SocketIORelay:
219
220
  if event_type.startswith("hook."):
220
221
  await self.relay_event(event_type, data)
221
222
 
222
- # Subscribe to all hook events
223
+ # Subscribe to all hook events via wildcard
224
+ # This will catch ALL hook.* events
223
225
  self.event_bus.on("hook.*", handle_hook_event)
224
226
 
225
- # Also subscribe to specific high-priority events
226
- for event in ["hook.pre_tool", "hook.post_tool", "hook.subagent_stop",
227
- "hook.user_prompt", "hook.assistant_response"]:
228
- self.event_bus.on(event, lambda data, evt=event:
229
- asyncio.create_task(self.relay_event(evt, data)))
230
-
231
227
  logger.info("SocketIO relay started and subscribed to events")
232
228
 
233
229
  def stop(self) -> None: