claude-mpm 4.0.32__py3-none-any.whl → 4.0.34__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 (67) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/documentation.json +51 -34
  3. claude_mpm/agents/templates/research.json +0 -11
  4. claude_mpm/cli/__init__.py +63 -26
  5. claude_mpm/cli/commands/agent_manager.py +10 -8
  6. claude_mpm/core/framework_loader.py +173 -84
  7. claude_mpm/dashboard/static/css/dashboard.css +449 -0
  8. claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
  9. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  10. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
  11. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  12. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  13. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  14. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  15. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
  16. claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
  17. claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
  18. claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
  19. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
  20. claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
  21. claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
  22. claude_mpm/dashboard/static/js/dashboard.js +207 -31
  23. claude_mpm/dashboard/static/js/socket-client.js +85 -6
  24. claude_mpm/dashboard/templates/index.html +1 -0
  25. claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
  26. claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
  28. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
  29. claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
  30. claude_mpm/services/agents/deployment/agent_deployment.py +34 -48
  31. claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
  32. claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
  33. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
  34. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
  35. claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
  36. claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
  37. claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -85
  38. claude_mpm/services/agents/memory/content_manager.py +98 -105
  39. claude_mpm/services/event_bus/__init__.py +18 -0
  40. claude_mpm/services/event_bus/event_bus.py +334 -0
  41. claude_mpm/services/event_bus/relay.py +301 -0
  42. claude_mpm/services/events/__init__.py +44 -0
  43. claude_mpm/services/events/consumers/__init__.py +18 -0
  44. claude_mpm/services/events/consumers/dead_letter.py +296 -0
  45. claude_mpm/services/events/consumers/logging.py +183 -0
  46. claude_mpm/services/events/consumers/metrics.py +242 -0
  47. claude_mpm/services/events/consumers/socketio.py +376 -0
  48. claude_mpm/services/events/core.py +470 -0
  49. claude_mpm/services/events/interfaces.py +230 -0
  50. claude_mpm/services/events/producers/__init__.py +14 -0
  51. claude_mpm/services/events/producers/hook.py +269 -0
  52. claude_mpm/services/events/producers/system.py +327 -0
  53. claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
  54. claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
  55. claude_mpm/services/monitor_build_service.py +345 -0
  56. claude_mpm/services/socketio/event_normalizer.py +667 -0
  57. claude_mpm/services/socketio/handlers/connection.py +78 -20
  58. claude_mpm/services/socketio/handlers/hook.py +14 -5
  59. claude_mpm/services/socketio/migration_utils.py +329 -0
  60. claude_mpm/services/socketio/server/broadcaster.py +26 -33
  61. claude_mpm/services/socketio/server/core.py +4 -3
  62. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
  63. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +67 -46
  64. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
  65. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
  66. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
  67. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/top_level.txt +0 -0
@@ -3,9 +3,10 @@
3
3
  import logging
4
4
  import os
5
5
  import sys
6
+ import time
6
7
  from datetime import datetime
7
8
  from pathlib import Path
8
- from typing import Any, Dict, Optional
9
+ from typing import Any, Dict, Optional, Set, Tuple
9
10
 
10
11
  # Import resource handling for packaged installations
11
12
  try:
@@ -86,6 +87,22 @@ class FrameworkLoader:
86
87
  self.agents_dir = agents_dir
87
88
  self.framework_version = None
88
89
  self.framework_last_modified = None
90
+
91
+ # Performance optimization: Initialize caches
92
+ self._agent_capabilities_cache: Optional[str] = None
93
+ self._agent_capabilities_cache_time: float = 0
94
+ self._deployed_agents_cache: Optional[Set[str]] = None
95
+ self._deployed_agents_cache_time: float = 0
96
+ self._agent_metadata_cache: Dict[str, Tuple[Optional[Dict[str, Any]], float]] = {}
97
+ self._memories_cache: Optional[Dict[str, Any]] = None
98
+ self._memories_cache_time: float = 0
99
+
100
+ # Cache TTL settings (in seconds)
101
+ self.CAPABILITIES_CACHE_TTL = 60 # 60 seconds for capabilities
102
+ self.DEPLOYED_AGENTS_CACHE_TTL = 30 # 30 seconds for deployed agents
103
+ self.METADATA_CACHE_TTL = 60 # 60 seconds for agent metadata
104
+ self.MEMORIES_CACHE_TTL = 60 # 60 seconds for memories
105
+
89
106
  self.framework_content = self._load_framework_content()
90
107
 
91
108
  # Initialize agent registry
@@ -94,6 +111,32 @@ class FrameworkLoader:
94
111
  # Initialize output style manager (must be after content is loaded)
95
112
  self.output_style_manager = None
96
113
  # Defer initialization until first use to ensure content is loaded
114
+
115
+ def clear_all_caches(self) -> None:
116
+ """Clear all caches to force reload on next access."""
117
+ self.logger.info("Clearing all framework loader caches")
118
+ self._agent_capabilities_cache = None
119
+ self._agent_capabilities_cache_time = 0
120
+ self._deployed_agents_cache = None
121
+ self._deployed_agents_cache_time = 0
122
+ self._agent_metadata_cache.clear()
123
+ self._memories_cache = None
124
+ self._memories_cache_time = 0
125
+
126
+ def clear_agent_caches(self) -> None:
127
+ """Clear agent-related caches (capabilities, deployed agents, metadata)."""
128
+ self.logger.info("Clearing agent-related caches")
129
+ self._agent_capabilities_cache = None
130
+ self._agent_capabilities_cache_time = 0
131
+ self._deployed_agents_cache = None
132
+ self._deployed_agents_cache_time = 0
133
+ self._agent_metadata_cache.clear()
134
+
135
+ def clear_memory_caches(self) -> None:
136
+ """Clear memory-related caches."""
137
+ self.logger.info("Clearing memory caches")
138
+ self._memories_cache = None
139
+ self._memories_cache_time = 0
97
140
 
98
141
  def _initialize_output_style(self) -> None:
99
142
  """Initialize output style management and deploy if applicable."""
@@ -486,10 +529,20 @@ class FrameworkLoader:
486
529
  def _get_deployed_agents(self) -> set:
487
530
  """
488
531
  Get a set of deployed agent names from .claude/agents/ directories.
532
+ Uses caching to avoid repeated filesystem scans.
489
533
 
490
534
  Returns:
491
535
  Set of agent names (file stems) that are deployed
492
536
  """
537
+ # Check if cache is valid
538
+ current_time = time.time()
539
+ if (self._deployed_agents_cache is not None and
540
+ current_time - self._deployed_agents_cache_time < self.DEPLOYED_AGENTS_CACHE_TTL):
541
+ self.logger.debug(f"Using cached deployed agents (age: {current_time - self._deployed_agents_cache_time:.1f}s)")
542
+ return self._deployed_agents_cache
543
+
544
+ # Cache miss or expired - perform actual scan
545
+ self.logger.debug("Scanning for deployed agents (cache miss or expired)")
493
546
  deployed = set()
494
547
 
495
548
  # Check multiple locations for deployed agents
@@ -507,11 +560,17 @@ class FrameworkLoader:
507
560
  self.logger.debug(f"Found deployed agent: {agent_file.stem} in {agents_dir}")
508
561
 
509
562
  self.logger.debug(f"Total deployed agents found: {len(deployed)}")
563
+
564
+ # Update cache
565
+ self._deployed_agents_cache = deployed
566
+ self._deployed_agents_cache_time = current_time
567
+
510
568
  return deployed
511
569
 
512
570
  def _load_actual_memories(self, content: Dict[str, Any]) -> None:
513
571
  """
514
572
  Load actual memories from both user and project directories.
573
+ Uses caching to avoid repeated file I/O operations.
515
574
 
516
575
  Loading order:
517
576
  1. User-level memories from ~/.claude-mpm/memories/ (global defaults)
@@ -524,6 +583,23 @@ class FrameworkLoader:
524
583
  Args:
525
584
  content: Dictionary to update with actual memories
526
585
  """
586
+ # Check if cache is valid
587
+ current_time = time.time()
588
+ if (self._memories_cache is not None and
589
+ current_time - self._memories_cache_time < self.MEMORIES_CACHE_TTL):
590
+ cache_age = current_time - self._memories_cache_time
591
+ self.logger.debug(f"Using cached memories (age: {cache_age:.1f}s)")
592
+
593
+ # Apply cached memories to content
594
+ if "actual_memories" in self._memories_cache:
595
+ content["actual_memories"] = self._memories_cache["actual_memories"]
596
+ if "agent_memories" in self._memories_cache:
597
+ content["agent_memories"] = self._memories_cache["agent_memories"]
598
+ return
599
+
600
+ # Cache miss or expired - perform actual loading
601
+ self.logger.debug("Loading memories from disk (cache miss or expired)")
602
+
527
603
  # Define memory directories in priority order (user first, then project)
528
604
  user_memories_dir = Path.home() / ".claude-mpm" / "memories"
529
605
  project_memories_dir = Path.cwd() / ".claude-mpm" / "memories"
@@ -575,6 +651,14 @@ class FrameworkLoader:
575
651
  memory_size = len(memory_content.encode('utf-8'))
576
652
  self.logger.debug(f"Aggregated {agent_name} memory: {memory_size:,} bytes")
577
653
 
654
+ # Update cache with loaded memories
655
+ self._memories_cache = {}
656
+ if "actual_memories" in content:
657
+ self._memories_cache["actual_memories"] = content["actual_memories"]
658
+ if "agent_memories" in content:
659
+ self._memories_cache["agent_memories"] = content["agent_memories"]
660
+ self._memories_cache_time = current_time
661
+
578
662
  # Log detailed summary
579
663
  if loaded_count > 0 or skipped_count > 0:
580
664
  # Count unique agents with memories
@@ -726,11 +810,10 @@ class FrameworkLoader:
726
810
  Aggregate multiple memory entries into a single memory string.
727
811
 
728
812
  Strategy:
729
- - Support both sectioned and non-sectioned memories
730
- - Preserve all bullet-point items (lines starting with -)
731
- - Merge sections when present, with project-level taking precedence
732
- - Remove exact duplicates within sections and unsectioned items
733
- - Preserve unique entries from both sources
813
+ - Simplified to support list-based memories only
814
+ - Preserve all unique bullet-point items (lines starting with -)
815
+ - Remove exact duplicates
816
+ - Project-level memories take precedence over user-level
734
817
 
735
818
  Args:
736
819
  memory_entries: List of memory entries with source, content, and path
@@ -745,97 +828,52 @@ class FrameworkLoader:
745
828
  if len(memory_entries) == 1:
746
829
  return memory_entries[0]["content"]
747
830
 
748
- # Parse all memories into sections and unsectioned items
749
- all_sections = {}
750
- unsectioned_items = {} # Items without a section header
831
+ # Parse all memories into a simple list
832
+ all_items = {} # Dict to track items and their source
751
833
  metadata_lines = []
834
+ agent_id = None
752
835
 
753
836
  for entry in memory_entries:
754
837
  content = entry["content"]
755
838
  source = entry["source"]
756
839
 
757
- # Parse content into sections and unsectioned items
758
- current_section = None
759
- current_items = []
760
-
761
840
  for line in content.split('\n'):
841
+ # Check for header to extract agent_id
842
+ if line.startswith('# Agent Memory:'):
843
+ agent_id = line.replace('# Agent Memory:', '').strip()
762
844
  # Check for metadata lines
763
- if line.startswith('<!-- ') and line.endswith(' -->'):
845
+ elif line.startswith('<!-- ') and line.endswith(' -->'):
764
846
  # Only keep metadata from project source or if not already present
765
847
  if source == "project" or line not in metadata_lines:
766
848
  metadata_lines.append(line)
767
- # Check for section headers (## Level 2 headers)
768
- elif line.startswith('## '):
769
- # Save previous section if exists
770
- if current_section and current_items:
771
- if current_section not in all_sections:
772
- all_sections[current_section] = {}
773
- # Store items with their source
774
- for item in current_items:
775
- # Use content as key to detect duplicates
776
- all_sections[current_section][item] = source
849
+ # Check for list items
850
+ elif line.strip().startswith('-'):
851
+ # Normalize the item for comparison
852
+ item_text = line.strip()
853
+ normalized = item_text.lstrip('- ').strip().lower()
777
854
 
778
- # Start new section
779
- current_section = line
780
- current_items = []
781
- # Check for content lines (including unsectioned bullet points)
782
- elif line.strip():
783
- # If it's a bullet point or regular content
784
- if current_section:
785
- # Add to current section
786
- current_items.append(line)
787
- elif line.strip().startswith('-'):
788
- # It's an unsectioned bullet point - preserve it
789
- # Use content as key to detect duplicates
790
- # Project source overrides user source
791
- if line not in unsectioned_items or source == "project":
792
- unsectioned_items[line] = source
793
- # Skip other non-bullet unsectioned content (like headers)
794
- elif not line.strip().startswith('#'):
795
- # Include non-header orphaned content in unsectioned items
796
- if line not in unsectioned_items or source == "project":
797
- unsectioned_items[line] = source
798
-
799
- # Save last section if exists
800
- if current_section and current_items:
801
- if current_section not in all_sections:
802
- all_sections[current_section] = {}
803
- for item in current_items:
804
- # Project source overrides user source
805
- if item not in all_sections[current_section] or source == "project":
806
- all_sections[current_section][item] = source
855
+ # Add item if new or if project source overrides user source
856
+ if normalized not in all_items or source == "project":
857
+ all_items[normalized] = (item_text, source)
807
858
 
808
- # Build aggregated content
859
+ # Build aggregated content as simple list
809
860
  lines = []
810
861
 
811
- # Add metadata
812
- if metadata_lines:
813
- lines.extend(metadata_lines)
814
- lines.append("")
815
-
816
862
  # Add header
817
- lines.append("# Aggregated Memory")
818
- lines.append("")
819
- lines.append("*This memory combines user-level and project-level memories.*")
820
- lines.append("")
863
+ if agent_id:
864
+ lines.append(f"# Agent Memory: {agent_id}")
865
+ else:
866
+ lines.append("# Agent Memory")
821
867
 
822
- # Add unsectioned items first (if any)
823
- if unsectioned_items:
824
- # Sort items to ensure consistent output
825
- for item in sorted(unsectioned_items.keys()):
826
- lines.append(item)
827
- lines.append("") # Empty line after unsectioned items
868
+ # Add latest timestamp from metadata
869
+ from datetime import datetime
870
+ lines.append(f"<!-- Last Updated: {datetime.now().isoformat()}Z -->")
871
+ lines.append("")
828
872
 
829
- # Add sections
830
- for section_header in sorted(all_sections.keys()):
831
- lines.append(section_header)
832
- section_items = all_sections[section_header]
833
-
834
- # Sort items to ensure consistent output
835
- for item in sorted(section_items.keys()):
836
- lines.append(item)
837
-
838
- lines.append("") # Empty line after section
873
+ # Add all unique items (sorted for consistency)
874
+ for normalized_key in sorted(all_items.keys()):
875
+ item_text, source = all_items[normalized_key]
876
+ lines.append(item_text)
839
877
 
840
878
  return '\n'.join(lines)
841
879
 
@@ -1392,7 +1430,20 @@ Extract tickets from these patterns:
1392
1430
  return instructions
1393
1431
 
1394
1432
  def _generate_agent_capabilities_section(self) -> str:
1395
- """Generate dynamic agent capabilities section from deployed agents."""
1433
+ """Generate dynamic agent capabilities section from deployed agents.
1434
+ Uses caching to avoid repeated file I/O and parsing operations."""
1435
+
1436
+ # Check if cache is valid
1437
+ current_time = time.time()
1438
+ if (self._agent_capabilities_cache is not None and
1439
+ current_time - self._agent_capabilities_cache_time < self.CAPABILITIES_CACHE_TTL):
1440
+ cache_age = current_time - self._agent_capabilities_cache_time
1441
+ self.logger.debug(f"Using cached agent capabilities (age: {cache_age:.1f}s)")
1442
+ return self._agent_capabilities_cache
1443
+
1444
+ # Cache miss or expired - generate capabilities
1445
+ self.logger.debug("Generating agent capabilities (cache miss or expired)")
1446
+
1396
1447
  try:
1397
1448
  from pathlib import Path
1398
1449
 
@@ -1419,7 +1470,7 @@ Extract tickets from these patterns:
1419
1470
  if agent_file.name.startswith("."):
1420
1471
  continue
1421
1472
 
1422
- # Parse agent metadata
1473
+ # Parse agent metadata (with caching)
1423
1474
  agent_data = self._parse_agent_metadata(agent_file)
1424
1475
  if agent_data:
1425
1476
  agent_id = agent_data["id"]
@@ -1431,7 +1482,11 @@ Extract tickets from these patterns:
1431
1482
 
1432
1483
  if not all_agents:
1433
1484
  self.logger.warning(f"No agents found in any location: {agents_dirs}")
1434
- return self._get_fallback_capabilities()
1485
+ result = self._get_fallback_capabilities()
1486
+ # Cache the fallback result too
1487
+ self._agent_capabilities_cache = result
1488
+ self._agent_capabilities_cache_time = current_time
1489
+ return result
1435
1490
 
1436
1491
  # Log agent collection summary
1437
1492
  project_agents = [aid for aid, (_, pri) in all_agents.items() if pri == 0]
@@ -1449,7 +1504,11 @@ Extract tickets from these patterns:
1449
1504
  deployed_agents = [agent_data for agent_data, _ in all_agents.values()]
1450
1505
 
1451
1506
  if not deployed_agents:
1452
- return self._get_fallback_capabilities()
1507
+ result = self._get_fallback_capabilities()
1508
+ # Cache the fallback result
1509
+ self._agent_capabilities_cache = result
1510
+ self._agent_capabilities_cache_time = current_time
1511
+ return result
1453
1512
 
1454
1513
  # Sort agents alphabetically by ID
1455
1514
  deployed_agents.sort(key=lambda x: x["id"])
@@ -1496,19 +1555,46 @@ Extract tickets from these patterns:
1496
1555
  # Add summary
1497
1556
  section += f"\n**Total Available Agents**: {len(deployed_agents)}\n"
1498
1557
 
1558
+ # Cache the generated capabilities
1559
+ self._agent_capabilities_cache = section
1560
+ self._agent_capabilities_cache_time = current_time
1561
+ self.logger.debug(f"Cached agent capabilities section ({len(section)} chars)")
1562
+
1499
1563
  return section
1500
1564
 
1501
1565
  except Exception as e:
1502
1566
  self.logger.warning(f"Could not generate dynamic agent capabilities: {e}")
1503
- return self._get_fallback_capabilities()
1567
+ result = self._get_fallback_capabilities()
1568
+ # Cache even the fallback result
1569
+ self._agent_capabilities_cache = result
1570
+ self._agent_capabilities_cache_time = current_time
1571
+ return result
1504
1572
 
1505
1573
  def _parse_agent_metadata(self, agent_file: Path) -> Optional[Dict[str, Any]]:
1506
1574
  """Parse agent metadata from deployed agent file.
1575
+ Uses caching based on file path and modification time.
1507
1576
 
1508
1577
  Returns:
1509
1578
  Dictionary with agent metadata directly from YAML frontmatter.
1510
1579
  """
1511
1580
  try:
1581
+ # Check cache based on file path and modification time
1582
+ cache_key = str(agent_file)
1583
+ file_mtime = agent_file.stat().st_mtime
1584
+ current_time = time.time()
1585
+
1586
+ # Check if we have cached data for this file
1587
+ if cache_key in self._agent_metadata_cache:
1588
+ cached_data, cached_mtime = self._agent_metadata_cache[cache_key]
1589
+ # Use cache if file hasn't been modified and cache isn't too old
1590
+ if (cached_mtime == file_mtime and
1591
+ current_time - cached_mtime < self.METADATA_CACHE_TTL):
1592
+ self.logger.debug(f"Using cached metadata for {agent_file.name}")
1593
+ return cached_data
1594
+
1595
+ # Cache miss or expired - parse the file
1596
+ self.logger.debug(f"Parsing metadata for {agent_file.name} (cache miss or expired)")
1597
+
1512
1598
  import yaml
1513
1599
 
1514
1600
  with open(agent_file, "r") as f:
@@ -1546,6 +1632,9 @@ Extract tickets from these patterns:
1546
1632
  # IMPORTANT: Do NOT add spaces to tools field - it breaks deployment!
1547
1633
  # Tools must remain as comma-separated without spaces: "Read,Write,Edit"
1548
1634
 
1635
+ # Cache the parsed metadata
1636
+ self._agent_metadata_cache[cache_key] = (agent_data, file_mtime)
1637
+
1549
1638
  return agent_data
1550
1639
 
1551
1640
  except Exception as e: