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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +70 -2
- claude_mpm/agents/OUTPUT_STYLE.md +0 -11
- claude_mpm/agents/WORKFLOW.md +14 -2
- claude_mpm/cli/__init__.py +48 -7
- claude_mpm/cli/commands/agents.py +82 -0
- claude_mpm/cli/commands/cleanup_orphaned_agents.py +150 -0
- claude_mpm/cli/commands/mcp_pipx_config.py +199 -0
- claude_mpm/cli/parsers/agents_parser.py +27 -0
- claude_mpm/cli/parsers/base_parser.py +6 -0
- claude_mpm/cli/startup_logging.py +75 -0
- claude_mpm/dashboard/static/js/components/build-tracker.js +35 -1
- claude_mpm/dashboard/static/js/socket-client.js +7 -5
- claude_mpm/hooks/claude_hooks/connection_pool.py +13 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +67 -167
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -1
- claude_mpm/services/agents/deployment/agent_template_builder.py +2 -1
- claude_mpm/services/agents/deployment/agent_version_manager.py +4 -1
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +207 -10
- claude_mpm/services/event_bus/config.py +165 -0
- claude_mpm/services/event_bus/event_bus.py +35 -20
- claude_mpm/services/event_bus/relay.py +8 -12
- claude_mpm/services/mcp_gateway/auto_configure.py +372 -0
- claude_mpm/services/socketio/handlers/connection.py +3 -3
- claude_mpm/services/socketio/server/core.py +25 -2
- claude_mpm/services/socketio/server/eventbus_integration.py +189 -0
- claude_mpm/services/socketio/server/main.py +25 -0
- {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/METADATA +25 -7
- {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/RECORD +33 -28
- {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.34.dist-info → claude_mpm-4.1.0.dist-info}/licenses/LICENSE +0 -0
- {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"
|
|
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
|
-
|
|
602
|
-
f"
|
|
603
|
-
f"{len(comparison_results['
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
#
|
|
221
|
-
|
|
244
|
+
# Store wildcard handlers separately
|
|
245
|
+
if not hasattr(self, '_wildcard_handlers'):
|
|
246
|
+
self._wildcard_handlers = {}
|
|
222
247
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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=
|
|
113
|
-
reconnection_delay=
|
|
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=
|
|
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:
|