claude-mpm 4.0.32__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/agents/templates/documentation.json +51 -34
- claude_mpm/agents/templates/research.json +0 -11
- claude_mpm/cli/__init__.py +111 -33
- claude_mpm/cli/commands/agent_manager.py +10 -8
- 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/core/framework_loader.py +173 -84
- claude_mpm/dashboard/static/css/dashboard.css +449 -0
- claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
- claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
- claude_mpm/dashboard/static/js/components/build-tracker.js +323 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
- claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
- claude_mpm/dashboard/static/js/dashboard.js +207 -31
- claude_mpm/dashboard/static/js/socket-client.js +92 -11
- claude_mpm/dashboard/templates/index.html +1 -0
- claude_mpm/hooks/claude_hooks/connection_pool.py +25 -4
- claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
- claude_mpm/hooks/claude_hooks/hook_handler.py +125 -163
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +34 -48
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -1
- claude_mpm/services/agents/deployment/agent_template_builder.py +20 -11
- claude_mpm/services/agents/deployment/agent_version_manager.py +4 -1
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +396 -13
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
- claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -85
- claude_mpm/services/agents/memory/content_manager.py +98 -105
- claude_mpm/services/event_bus/__init__.py +18 -0
- claude_mpm/services/event_bus/config.py +165 -0
- claude_mpm/services/event_bus/event_bus.py +349 -0
- claude_mpm/services/event_bus/relay.py +297 -0
- claude_mpm/services/events/__init__.py +44 -0
- claude_mpm/services/events/consumers/__init__.py +18 -0
- claude_mpm/services/events/consumers/dead_letter.py +296 -0
- claude_mpm/services/events/consumers/logging.py +183 -0
- claude_mpm/services/events/consumers/metrics.py +242 -0
- claude_mpm/services/events/consumers/socketio.py +376 -0
- claude_mpm/services/events/core.py +470 -0
- claude_mpm/services/events/interfaces.py +230 -0
- claude_mpm/services/events/producers/__init__.py +14 -0
- claude_mpm/services/events/producers/hook.py +269 -0
- claude_mpm/services/events/producers/system.py +327 -0
- claude_mpm/services/mcp_gateway/auto_configure.py +372 -0
- claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
- claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
- claude_mpm/services/monitor_build_service.py +345 -0
- claude_mpm/services/socketio/event_normalizer.py +667 -0
- claude_mpm/services/socketio/handlers/connection.py +81 -23
- claude_mpm/services/socketio/handlers/hook.py +14 -5
- claude_mpm/services/socketio/migration_utils.py +329 -0
- claude_mpm/services/socketio/server/broadcaster.py +26 -33
- claude_mpm/services/socketio/server/core.py +29 -5
- claude_mpm/services/socketio/server/eventbus_integration.py +189 -0
- claude_mpm/services/socketio/server/main.py +25 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/METADATA +28 -9
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/RECORD +82 -56
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/top_level.txt +0 -0
|
@@ -17,106 +17,29 @@ class SystemInstructionsDeployer:
|
|
|
17
17
|
self.logger = logger
|
|
18
18
|
self.working_directory = working_directory
|
|
19
19
|
|
|
20
|
-
def deploy_system_instructions_to_claude_mpm(
|
|
21
|
-
self,
|
|
22
|
-
target_dir: Path,
|
|
23
|
-
force_rebuild: bool,
|
|
24
|
-
results: Dict[str, Any],
|
|
25
|
-
is_project_specific: bool,
|
|
26
|
-
) -> None:
|
|
27
|
-
"""
|
|
28
|
-
Deploy system instructions to .claude-mpm/ directory (not .claude/).
|
|
29
|
-
|
|
30
|
-
This method should ONLY be called when explicitly requested by the user.
|
|
31
|
-
It deploys INSTRUCTIONS.md, WORKFLOW.md, and MEMORY.md files to .claude-mpm/
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
target_dir: Target directory (should be .claude-mpm/)
|
|
35
|
-
force_rebuild: Force rebuild even if exists
|
|
36
|
-
results: Results dictionary to update
|
|
37
|
-
is_project_specific: Whether this is a project-specific deployment
|
|
38
|
-
"""
|
|
39
|
-
try:
|
|
40
|
-
# Framework files to deploy
|
|
41
|
-
framework_files = [
|
|
42
|
-
("INSTRUCTIONS.md", "INSTRUCTIONS.md"),
|
|
43
|
-
("WORKFLOW.md", "WORKFLOW.md"),
|
|
44
|
-
("MEMORY.md", "MEMORY.md"),
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
# Find the agents directory with framework files
|
|
48
|
-
from claude_mpm.config.paths import paths
|
|
49
|
-
agents_path = paths.agents_dir
|
|
50
|
-
|
|
51
|
-
for source_name, target_name in framework_files:
|
|
52
|
-
source_path = agents_path / source_name
|
|
53
|
-
|
|
54
|
-
if not source_path.exists():
|
|
55
|
-
self.logger.warning(f"Framework file not found: {source_path}")
|
|
56
|
-
continue
|
|
57
|
-
|
|
58
|
-
target_file = target_dir / target_name
|
|
59
|
-
|
|
60
|
-
# Check if update needed
|
|
61
|
-
if not force_rebuild and target_file.exists():
|
|
62
|
-
# Compare modification times
|
|
63
|
-
if target_file.stat().st_mtime >= source_path.stat().st_mtime:
|
|
64
|
-
results["skipped"].append(target_name)
|
|
65
|
-
self.logger.debug(f"Framework file {target_name} up to date")
|
|
66
|
-
continue
|
|
67
|
-
|
|
68
|
-
# Read and deploy framework file
|
|
69
|
-
file_content = source_path.read_text()
|
|
70
|
-
target_file.write_text(file_content)
|
|
71
|
-
|
|
72
|
-
# Track deployment
|
|
73
|
-
file_existed = target_file.exists()
|
|
74
|
-
deployment_info = {
|
|
75
|
-
"name": target_name,
|
|
76
|
-
"template": str(source_path),
|
|
77
|
-
"target": str(target_file),
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if file_existed:
|
|
81
|
-
results["updated"].append(deployment_info)
|
|
82
|
-
self.logger.info(f"Updated framework file in .claude-mpm: {target_name}")
|
|
83
|
-
else:
|
|
84
|
-
results["deployed"].append(deployment_info)
|
|
85
|
-
self.logger.info(f"Deployed framework file to .claude-mpm: {target_name}")
|
|
86
|
-
|
|
87
|
-
except Exception as e:
|
|
88
|
-
error_msg = f"Failed to deploy system instructions to .claude-mpm: {e}"
|
|
89
|
-
self.logger.error(error_msg)
|
|
90
|
-
results["errors"].append(error_msg)
|
|
91
|
-
|
|
92
20
|
def deploy_system_instructions(
|
|
93
21
|
self,
|
|
94
22
|
target_dir: Path,
|
|
95
23
|
force_rebuild: bool,
|
|
96
24
|
results: Dict[str, Any],
|
|
97
|
-
is_project_specific: bool,
|
|
98
25
|
) -> None:
|
|
99
26
|
"""
|
|
100
27
|
Deploy system instructions and framework files for PM framework.
|
|
101
28
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
29
|
+
Always deploys to project .claude directory regardless of agent source
|
|
30
|
+
(system, user, or project). This ensures consistent project-level
|
|
31
|
+
deployment while maintaining discovery from both user (~/.claude-mpm)
|
|
32
|
+
and project (.claude-mpm) directories.
|
|
105
33
|
|
|
106
34
|
Args:
|
|
107
|
-
target_dir: Target directory for deployment
|
|
35
|
+
target_dir: Target directory for deployment (not used - always uses project .claude)
|
|
108
36
|
force_rebuild: Force rebuild even if exists
|
|
109
37
|
results: Results dictionary to update
|
|
110
|
-
is_project_specific: Whether this is a project-specific deployment
|
|
111
38
|
"""
|
|
112
39
|
try:
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
claude_dir = self.working_directory / ".claude"
|
|
117
|
-
else:
|
|
118
|
-
# System and user files go to home ~/.claude directory
|
|
119
|
-
claude_dir = Path.home() / ".claude"
|
|
40
|
+
# Always use project's .claude directory
|
|
41
|
+
# This is the key change - all system instructions go to project .claude
|
|
42
|
+
claude_dir = self.working_directory / ".claude"
|
|
120
43
|
|
|
121
44
|
# Ensure .claude directory exists
|
|
122
45
|
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -222,7 +222,7 @@ class MemoryContentManager:
|
|
|
222
222
|
"""Validate memory file and repair if needed.
|
|
223
223
|
|
|
224
224
|
WHY: Memory files might be manually edited by developers or corrupted.
|
|
225
|
-
This method ensures the file maintains
|
|
225
|
+
This method ensures the file maintains proper simple list structure.
|
|
226
226
|
|
|
227
227
|
Args:
|
|
228
228
|
content: Content to validate
|
|
@@ -232,88 +232,94 @@ class MemoryContentManager:
|
|
|
232
232
|
str: Validated and repaired content
|
|
233
233
|
"""
|
|
234
234
|
lines = content.split("\n")
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
235
|
+
|
|
236
|
+
# Ensure proper header format
|
|
237
|
+
has_header = False
|
|
238
|
+
has_timestamp = False
|
|
239
|
+
|
|
240
|
+
for i, line in enumerate(lines[:5]): # Check first 5 lines
|
|
241
|
+
if line.startswith("# Agent Memory:"):
|
|
242
|
+
has_header = True
|
|
243
|
+
elif line.startswith("<!-- Last Updated:"):
|
|
244
|
+
has_timestamp = True
|
|
245
|
+
|
|
246
|
+
# Add missing header or timestamp
|
|
247
|
+
if not has_header or not has_timestamp:
|
|
248
|
+
from datetime import datetime
|
|
249
|
+
new_lines = []
|
|
250
|
+
|
|
251
|
+
if not has_header:
|
|
252
|
+
new_lines.append(f"# Agent Memory: {agent_id}")
|
|
253
|
+
else:
|
|
254
|
+
# Keep existing header
|
|
255
|
+
for line in lines:
|
|
256
|
+
if line.startswith("# "):
|
|
257
|
+
new_lines.append(line)
|
|
258
|
+
lines.remove(line)
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
if not has_timestamp:
|
|
262
|
+
new_lines.append(f"<!-- Last Updated: {datetime.now().isoformat()}Z -->")
|
|
263
|
+
new_lines.append("")
|
|
264
|
+
else:
|
|
265
|
+
# Keep existing timestamp
|
|
266
|
+
for line in lines:
|
|
267
|
+
if line.startswith("<!-- Last Updated:"):
|
|
268
|
+
new_lines.append(line)
|
|
269
|
+
lines.remove(line)
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
# Add remaining content
|
|
273
|
+
for line in lines:
|
|
274
|
+
if not line.startswith("# ") and not line.startswith("<!-- Last Updated:"):
|
|
275
|
+
new_lines.append(line)
|
|
276
|
+
|
|
277
|
+
return "\n".join(new_lines)
|
|
278
|
+
|
|
272
279
|
return "\n".join(lines)
|
|
273
280
|
|
|
274
|
-
def
|
|
275
|
-
"""Parse memory content into
|
|
281
|
+
def parse_memory_content_to_list(self, content: str) -> List[str]:
|
|
282
|
+
"""Parse memory content into a simple list format.
|
|
276
283
|
|
|
277
|
-
WHY: Provides consistent parsing of memory content
|
|
278
|
-
for both display and programmatic access.
|
|
279
|
-
logic is used across the system.
|
|
284
|
+
WHY: Provides consistent parsing of memory content as a simple list
|
|
285
|
+
for both display and programmatic access.
|
|
280
286
|
|
|
281
287
|
Args:
|
|
282
288
|
content: Raw memory file content
|
|
283
289
|
|
|
284
290
|
Returns:
|
|
285
|
-
|
|
291
|
+
List of memory items
|
|
286
292
|
"""
|
|
287
|
-
|
|
288
|
-
current_section = None
|
|
289
|
-
current_items = []
|
|
293
|
+
items = []
|
|
290
294
|
|
|
291
295
|
for line in content.split("\n"):
|
|
292
296
|
line = line.strip()
|
|
293
297
|
|
|
294
|
-
# Skip empty lines and
|
|
295
|
-
if not line or line.startswith("#")
|
|
298
|
+
# Skip empty lines, headers, and metadata
|
|
299
|
+
if not line or line.startswith("#") or line.startswith("<!--"):
|
|
296
300
|
continue
|
|
297
301
|
|
|
298
|
-
if line.startswith("
|
|
299
|
-
#
|
|
300
|
-
if current_section and current_items:
|
|
301
|
-
sections[current_section] = current_items.copy()
|
|
302
|
-
|
|
303
|
-
current_section = line[3:].strip()
|
|
304
|
-
current_items = []
|
|
305
|
-
|
|
306
|
-
elif line.startswith("- ") and current_section:
|
|
307
|
-
# Item in current section
|
|
302
|
+
if line.startswith("- "):
|
|
303
|
+
# Item in list
|
|
308
304
|
item = line[2:].strip()
|
|
309
305
|
if item and len(item) > 3: # Filter out very short items
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
# Add final section
|
|
313
|
-
if current_section and current_items:
|
|
314
|
-
sections[current_section] = current_items
|
|
306
|
+
items.append(item)
|
|
315
307
|
|
|
316
|
-
return
|
|
308
|
+
return items
|
|
309
|
+
|
|
310
|
+
def parse_memory_content_to_dict(self, content: str) -> Dict[str, List[str]]:
|
|
311
|
+
"""Legacy method for backward compatibility.
|
|
312
|
+
|
|
313
|
+
Returns a dict with single key 'memories' containing all items.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
content: Raw memory file content
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Dict with 'memories' key mapping to list of items
|
|
320
|
+
"""
|
|
321
|
+
items = self.parse_memory_content_to_list(content)
|
|
322
|
+
return {"memories": items}
|
|
317
323
|
|
|
318
324
|
def _calculate_similarity(self, str1: str, str2: str) -> float:
|
|
319
325
|
"""Calculate similarity between two strings using fuzzy matching.
|
|
@@ -357,44 +363,27 @@ class MemoryContentManager:
|
|
|
357
363
|
|
|
358
364
|
return similarity
|
|
359
365
|
|
|
360
|
-
def
|
|
361
|
-
"""Deduplicate items
|
|
366
|
+
def deduplicate_list(self, content: str) -> Tuple[str, int]:
|
|
367
|
+
"""Deduplicate items in the memory list using NLP similarity.
|
|
362
368
|
|
|
363
|
-
WHY: Over time,
|
|
364
|
-
different sessions. This method cleans up
|
|
365
|
-
|
|
369
|
+
WHY: Over time, memory lists can accumulate similar or duplicate items from
|
|
370
|
+
different sessions. This method cleans up by removing similar items while
|
|
371
|
+
preserving the most recent ones.
|
|
366
372
|
|
|
367
373
|
Args:
|
|
368
374
|
content: Current memory file content
|
|
369
|
-
section: Section name to deduplicate
|
|
370
375
|
|
|
371
376
|
Returns:
|
|
372
377
|
Tuple of (updated content, number of items removed)
|
|
373
378
|
"""
|
|
374
379
|
lines = content.split("\n")
|
|
375
|
-
section_start = None
|
|
376
|
-
section_end = None
|
|
377
|
-
|
|
378
|
-
# Find section boundaries
|
|
379
|
-
for i, line in enumerate(lines):
|
|
380
|
-
if line.startswith(f"## {section}"):
|
|
381
|
-
section_start = i
|
|
382
|
-
elif section_start is not None and line.startswith("## "):
|
|
383
|
-
section_end = i
|
|
384
|
-
break
|
|
385
|
-
|
|
386
|
-
if section_start is None:
|
|
387
|
-
return content, 0 # Section not found
|
|
388
|
-
|
|
389
|
-
if section_end is None:
|
|
390
|
-
section_end = len(lines)
|
|
391
380
|
|
|
392
|
-
# Collect all items in the
|
|
381
|
+
# Collect all items in the list
|
|
393
382
|
items = []
|
|
394
383
|
item_indices = []
|
|
395
|
-
for i in
|
|
396
|
-
if
|
|
397
|
-
items.append(
|
|
384
|
+
for i, line in enumerate(lines):
|
|
385
|
+
if line.strip().startswith("- "):
|
|
386
|
+
items.append(line.strip()[2:]) # Remove "- " prefix
|
|
398
387
|
item_indices.append(i)
|
|
399
388
|
|
|
400
389
|
# Find duplicates using pairwise comparison
|
|
@@ -421,6 +410,18 @@ class MemoryContentManager:
|
|
|
421
410
|
lines.pop(item_indices[idx])
|
|
422
411
|
|
|
423
412
|
return "\n".join(lines), removed_count
|
|
413
|
+
|
|
414
|
+
def deduplicate_section(self, content: str, section: str) -> Tuple[str, int]:
|
|
415
|
+
"""Legacy method for backward compatibility - delegates to deduplicate_list.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
content: Current memory file content
|
|
419
|
+
section: Section name (ignored in simple list format)
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Tuple of (updated content, number of items removed)
|
|
423
|
+
"""
|
|
424
|
+
return self.deduplicate_list(content)
|
|
424
425
|
|
|
425
426
|
def validate_memory_size(self, content: str) -> tuple[bool, Optional[str]]:
|
|
426
427
|
"""Validate memory content size and structure.
|
|
@@ -434,7 +435,7 @@ class MemoryContentManager:
|
|
|
434
435
|
try:
|
|
435
436
|
# Check file size
|
|
436
437
|
size_kb = len(content.encode("utf-8")) / 1024
|
|
437
|
-
max_size_kb = self.memory_limits.get("max_file_size_kb",
|
|
438
|
+
max_size_kb = self.memory_limits.get("max_file_size_kb", 80)
|
|
438
439
|
|
|
439
440
|
if size_kb > max_size_kb:
|
|
440
441
|
return (
|
|
@@ -442,20 +443,12 @@ class MemoryContentManager:
|
|
|
442
443
|
f"Memory size {size_kb:.1f}KB exceeds limit of {max_size_kb}KB",
|
|
443
444
|
)
|
|
444
445
|
|
|
445
|
-
# Check
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if len(sections) > max_sections:
|
|
450
|
-
return False, f"Too many sections: {len(sections)} (max {max_sections})"
|
|
451
|
-
|
|
452
|
-
# Check for required sections
|
|
453
|
-
required = set(self.REQUIRED_SECTIONS)
|
|
454
|
-
found = set(sections)
|
|
455
|
-
missing = required - found
|
|
446
|
+
# Check item count
|
|
447
|
+
items = sum(1 for line in content.split("\n") if line.strip().startswith("- "))
|
|
448
|
+
max_items = self.memory_limits.get("max_items", 100)
|
|
456
449
|
|
|
457
|
-
if
|
|
458
|
-
return False, f"
|
|
450
|
+
if items > max_items:
|
|
451
|
+
return False, f"Too many items: {items} (max {max_items})"
|
|
459
452
|
|
|
460
453
|
return True, None
|
|
461
454
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Event Bus Service for decoupled event handling.
|
|
2
|
+
|
|
3
|
+
This module provides a centralized event bus that decouples event producers
|
|
4
|
+
(like hooks) from consumers (like Socket.IO). It uses pyee's AsyncIOEventEmitter
|
|
5
|
+
to support both synchronous publishing and asynchronous consumption.
|
|
6
|
+
|
|
7
|
+
WHY event bus architecture:
|
|
8
|
+
- Decouples hooks from Socket.IO implementation details
|
|
9
|
+
- Allows multiple consumers for the same events
|
|
10
|
+
- Enables easy testing without Socket.IO dependencies
|
|
11
|
+
- Provides event filtering and routing capabilities
|
|
12
|
+
- Supports both sync (hooks) and async (Socket.IO) contexts
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .event_bus import EventBus
|
|
16
|
+
from .relay import SocketIORelay
|
|
17
|
+
|
|
18
|
+
__all__ = ["EventBus", "SocketIORelay"]
|
|
@@ -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()
|