claude-mpm 4.0.23__py3-none-any.whl → 4.0.28__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 (60) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +4 -1
  4. claude_mpm/agents/BASE_PM.md +3 -0
  5. claude_mpm/agents/templates/code_analyzer.json +2 -2
  6. claude_mpm/cli/commands/agents.py +453 -113
  7. claude_mpm/cli/commands/aggregate.py +107 -15
  8. claude_mpm/cli/commands/cleanup.py +142 -10
  9. claude_mpm/cli/commands/config.py +358 -224
  10. claude_mpm/cli/commands/info.py +184 -75
  11. claude_mpm/cli/commands/mcp_command_router.py +5 -76
  12. claude_mpm/cli/commands/mcp_install_commands.py +68 -36
  13. claude_mpm/cli/commands/mcp_server_commands.py +30 -37
  14. claude_mpm/cli/commands/memory.py +331 -61
  15. claude_mpm/cli/commands/monitor.py +101 -7
  16. claude_mpm/cli/commands/run.py +368 -8
  17. claude_mpm/cli/commands/tickets.py +206 -24
  18. claude_mpm/cli/parsers/mcp_parser.py +3 -0
  19. claude_mpm/cli/shared/__init__.py +40 -0
  20. claude_mpm/cli/shared/argument_patterns.py +212 -0
  21. claude_mpm/cli/shared/command_base.py +234 -0
  22. claude_mpm/cli/shared/error_handling.py +238 -0
  23. claude_mpm/cli/shared/output_formatters.py +231 -0
  24. claude_mpm/config/agent_config.py +29 -8
  25. claude_mpm/core/container.py +6 -4
  26. claude_mpm/core/service_registry.py +4 -2
  27. claude_mpm/core/shared/__init__.py +17 -0
  28. claude_mpm/core/shared/config_loader.py +320 -0
  29. claude_mpm/core/shared/path_resolver.py +277 -0
  30. claude_mpm/core/shared/singleton_manager.py +208 -0
  31. claude_mpm/hooks/claude_hooks/memory_integration.py +4 -2
  32. claude_mpm/hooks/claude_hooks/response_tracking.py +14 -3
  33. claude_mpm/hooks/memory_integration_hook.py +11 -2
  34. claude_mpm/services/agents/deployment/agent_deployment.py +49 -23
  35. claude_mpm/services/agents/deployment/deployment_wrapper.py +71 -0
  36. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +1 -0
  37. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +43 -0
  38. claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +4 -0
  39. claude_mpm/services/agents/deployment/processors/agent_processor.py +1 -1
  40. claude_mpm/services/agents/loading/base_agent_manager.py +11 -3
  41. claude_mpm/services/agents/registry/deployed_agent_discovery.py +14 -5
  42. claude_mpm/services/event_aggregator.py +4 -2
  43. claude_mpm/services/mcp_gateway/config/config_loader.py +89 -28
  44. claude_mpm/services/mcp_gateway/config/configuration.py +29 -0
  45. claude_mpm/services/mcp_gateway/registry/service_registry.py +22 -5
  46. claude_mpm/services/memory/builder.py +6 -1
  47. claude_mpm/services/response_tracker.py +3 -1
  48. claude_mpm/services/runner_configuration_service.py +15 -6
  49. claude_mpm/services/shared/__init__.py +20 -0
  50. claude_mpm/services/shared/async_service_base.py +219 -0
  51. claude_mpm/services/shared/config_service_base.py +292 -0
  52. claude_mpm/services/shared/lifecycle_service_base.py +317 -0
  53. claude_mpm/services/shared/manager_base.py +303 -0
  54. claude_mpm/services/shared/service_factory.py +308 -0
  55. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/METADATA +19 -13
  56. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/RECORD +60 -44
  57. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/WHEEL +0 -0
  58. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/entry_points.txt +0 -0
  59. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/licenses/LICENSE +0 -0
  60. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,277 @@
1
+ """
2
+ Shared path resolution utilities to reduce duplication.
3
+ """
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import List, Optional, Union
8
+
9
+ from ..logger import get_logger
10
+
11
+
12
+ class PathResolver:
13
+ """
14
+ Centralized path resolution utility.
15
+
16
+ Reduces duplication by providing standard patterns for:
17
+ - Working directory resolution
18
+ - Configuration directory discovery
19
+ - Agent directory resolution
20
+ - Memory directory resolution
21
+ """
22
+
23
+ def __init__(self, base_dir: Union[str, Path] = None):
24
+ """
25
+ Initialize path resolver.
26
+
27
+ Args:
28
+ base_dir: Base directory for relative paths
29
+ """
30
+ self.base_dir = Path(base_dir) if base_dir else self._get_working_dir()
31
+ self.logger = get_logger("path_resolver")
32
+
33
+ def _get_working_dir(self) -> Path:
34
+ """Get working directory respecting CLAUDE_MPM_USER_PWD."""
35
+ # Use CLAUDE_MPM_USER_PWD if available (when called via shell script)
36
+ user_pwd = os.environ.get("CLAUDE_MPM_USER_PWD")
37
+ if user_pwd:
38
+ return Path(user_pwd)
39
+ return Path.cwd()
40
+
41
+ def resolve_config_dir(self, create: bool = False) -> Path:
42
+ """
43
+ Resolve configuration directory.
44
+
45
+ Args:
46
+ create: Whether to create directory if it doesn't exist
47
+
48
+ Returns:
49
+ Path to configuration directory
50
+ """
51
+ config_dir = self.base_dir / ".claude-mpm"
52
+
53
+ if create and not config_dir.exists():
54
+ config_dir.mkdir(parents=True, exist_ok=True)
55
+ self.logger.debug(f"Created config directory: {config_dir}")
56
+
57
+ return config_dir
58
+
59
+ def resolve_agents_dir(self, create: bool = False) -> Path:
60
+ """
61
+ Resolve agents directory.
62
+
63
+ Args:
64
+ create: Whether to create directory if it doesn't exist
65
+
66
+ Returns:
67
+ Path to agents directory
68
+ """
69
+ agents_dir = self.resolve_config_dir(create) / "agents"
70
+
71
+ if create and not agents_dir.exists():
72
+ agents_dir.mkdir(parents=True, exist_ok=True)
73
+ self.logger.debug(f"Created agents directory: {agents_dir}")
74
+
75
+ return agents_dir
76
+
77
+ def resolve_memories_dir(self, create: bool = False) -> Path:
78
+ """
79
+ Resolve memories directory.
80
+
81
+ Args:
82
+ create: Whether to create directory if it doesn't exist
83
+
84
+ Returns:
85
+ Path to memories directory
86
+ """
87
+ memories_dir = self.resolve_config_dir(create) / "memories"
88
+
89
+ if create and not memories_dir.exists():
90
+ memories_dir.mkdir(parents=True, exist_ok=True)
91
+ self.logger.debug(f"Created memories directory: {memories_dir}")
92
+
93
+ return memories_dir
94
+
95
+ def resolve_logs_dir(self, create: bool = False) -> Path:
96
+ """
97
+ Resolve logs directory.
98
+
99
+ Args:
100
+ create: Whether to create directory if it doesn't exist
101
+
102
+ Returns:
103
+ Path to logs directory
104
+ """
105
+ logs_dir = self.resolve_config_dir(create) / "logs"
106
+
107
+ if create and not logs_dir.exists():
108
+ logs_dir.mkdir(parents=True, exist_ok=True)
109
+ self.logger.debug(f"Created logs directory: {logs_dir}")
110
+
111
+ return logs_dir
112
+
113
+ def resolve_temp_dir(self, create: bool = False) -> Path:
114
+ """
115
+ Resolve temporary directory.
116
+
117
+ Args:
118
+ create: Whether to create directory if it doesn't exist
119
+
120
+ Returns:
121
+ Path to temporary directory
122
+ """
123
+ temp_dir = self.resolve_config_dir(create) / "tmp"
124
+
125
+ if create and not temp_dir.exists():
126
+ temp_dir.mkdir(parents=True, exist_ok=True)
127
+ self.logger.debug(f"Created temp directory: {temp_dir}")
128
+
129
+ return temp_dir
130
+
131
+ def find_agent_file(self, agent_name: str, filename: str = None) -> Optional[Path]:
132
+ """
133
+ Find agent file in standard locations.
134
+
135
+ Args:
136
+ agent_name: Name of the agent
137
+ filename: Specific filename to look for (defaults to agent_name.md)
138
+
139
+ Returns:
140
+ Path to agent file if found
141
+ """
142
+ if filename is None:
143
+ filename = f"{agent_name}.md"
144
+
145
+ # Search locations in order of preference
146
+ search_paths = [
147
+ self.resolve_agents_dir(), # Project agents
148
+ Path.home() / ".claude" / "agents", # User agents
149
+ self.base_dir / "agents", # Local agents directory
150
+ self.base_dir, # Current directory
151
+ ]
152
+
153
+ for search_path in search_paths:
154
+ if not search_path.exists():
155
+ continue
156
+
157
+ agent_file = search_path / filename
158
+ if agent_file.exists() and agent_file.is_file():
159
+ self.logger.debug(f"Found agent file: {agent_file}")
160
+ return agent_file
161
+
162
+ return None
163
+
164
+ def find_memory_file(self, agent_name: str, filename: str = None) -> Optional[Path]:
165
+ """
166
+ Find memory file for an agent.
167
+
168
+ Args:
169
+ agent_name: Name of the agent
170
+ filename: Specific filename to look for (defaults to agent_name.md)
171
+
172
+ Returns:
173
+ Path to memory file if found
174
+ """
175
+ if filename is None:
176
+ filename = f"{agent_name}.md"
177
+
178
+ memories_dir = self.resolve_memories_dir()
179
+ memory_file = memories_dir / filename
180
+
181
+ if memory_file.exists() and memory_file.is_file():
182
+ return memory_file
183
+
184
+ return None
185
+
186
+ def find_config_file(self,
187
+ filename: str,
188
+ search_paths: List[Union[str, Path]] = None) -> Optional[Path]:
189
+ """
190
+ Find configuration file in standard locations.
191
+
192
+ Args:
193
+ filename: Configuration filename
194
+ search_paths: Additional search paths
195
+
196
+ Returns:
197
+ Path to configuration file if found
198
+ """
199
+ default_paths = [
200
+ self.resolve_config_dir(),
201
+ self.base_dir,
202
+ Path.home() / ".claude-mpm",
203
+ ]
204
+
205
+ if search_paths:
206
+ all_paths = [Path(p) for p in search_paths] + default_paths
207
+ else:
208
+ all_paths = default_paths
209
+
210
+ for search_path in all_paths:
211
+ if not search_path.exists():
212
+ continue
213
+
214
+ config_file = search_path / filename
215
+ if config_file.exists() and config_file.is_file():
216
+ self.logger.debug(f"Found config file: {config_file}")
217
+ return config_file
218
+
219
+ return None
220
+
221
+ def ensure_directory(self, path: Union[str, Path]) -> Path:
222
+ """
223
+ Ensure directory exists.
224
+
225
+ Args:
226
+ path: Directory path
227
+
228
+ Returns:
229
+ Path object for the directory
230
+ """
231
+ dir_path = Path(path)
232
+
233
+ if not dir_path.exists():
234
+ dir_path.mkdir(parents=True, exist_ok=True)
235
+ self.logger.debug(f"Created directory: {dir_path}")
236
+ elif not dir_path.is_dir():
237
+ raise ValueError(f"Path exists but is not a directory: {dir_path}")
238
+
239
+ return dir_path
240
+
241
+ def resolve_relative_path(self, path: Union[str, Path]) -> Path:
242
+ """
243
+ Resolve path relative to base directory.
244
+
245
+ Args:
246
+ path: Path to resolve
247
+
248
+ Returns:
249
+ Resolved absolute path
250
+ """
251
+ path_obj = Path(path)
252
+
253
+ if path_obj.is_absolute():
254
+ return path_obj
255
+
256
+ return (self.base_dir / path_obj).resolve()
257
+
258
+ def get_path_info(self) -> dict:
259
+ """
260
+ Get information about resolved paths.
261
+
262
+ Returns:
263
+ Dictionary with path information
264
+ """
265
+ return {
266
+ "base_dir": str(self.base_dir),
267
+ "config_dir": str(self.resolve_config_dir()),
268
+ "agents_dir": str(self.resolve_agents_dir()),
269
+ "memories_dir": str(self.resolve_memories_dir()),
270
+ "logs_dir": str(self.resolve_logs_dir()),
271
+ "temp_dir": str(self.resolve_temp_dir()),
272
+ "working_dir_from_env": os.environ.get("CLAUDE_MPM_USER_PWD"),
273
+ }
274
+
275
+ def __repr__(self) -> str:
276
+ """String representation."""
277
+ return f"PathResolver(base_dir={self.base_dir})"
@@ -0,0 +1,208 @@
1
+ """
2
+ Shared singleton management utilities to reduce duplication.
3
+ """
4
+
5
+ import threading
6
+ from typing import Any, Dict, Optional, Type, TypeVar
7
+
8
+ from ..logger import get_logger
9
+
10
+ T = TypeVar('T')
11
+
12
+
13
+ class SingletonManager:
14
+ """
15
+ Centralized singleton management utility.
16
+
17
+ Reduces duplication by providing thread-safe singleton patterns
18
+ that can be used across different classes.
19
+ """
20
+
21
+ _instances: Dict[Type, Any] = {}
22
+ _locks: Dict[Type, threading.Lock] = {}
23
+ _global_lock = threading.Lock()
24
+
25
+ @classmethod
26
+ def get_instance(cls,
27
+ singleton_class: Type[T],
28
+ *args,
29
+ force_new: bool = False,
30
+ **kwargs) -> T:
31
+ """
32
+ Get singleton instance of a class.
33
+
34
+ Args:
35
+ singleton_class: Class to get singleton instance of
36
+ *args: Arguments for class constructor
37
+ force_new: Force creation of new instance
38
+ **kwargs: Keyword arguments for class constructor
39
+
40
+ Returns:
41
+ Singleton instance
42
+ """
43
+ # Get or create lock for this class
44
+ if singleton_class not in cls._locks:
45
+ with cls._global_lock:
46
+ if singleton_class not in cls._locks:
47
+ cls._locks[singleton_class] = threading.Lock()
48
+
49
+ # Get instance with class-specific lock
50
+ with cls._locks[singleton_class]:
51
+ if force_new or singleton_class not in cls._instances:
52
+ logger = get_logger("singleton_manager")
53
+ logger.debug(f"Creating singleton instance: {singleton_class.__name__}")
54
+
55
+ instance = singleton_class(*args, **kwargs)
56
+ cls._instances[singleton_class] = instance
57
+
58
+ return instance
59
+
60
+ return cls._instances[singleton_class]
61
+
62
+ @classmethod
63
+ def has_instance(cls, singleton_class: Type) -> bool:
64
+ """
65
+ Check if singleton instance exists.
66
+
67
+ Args:
68
+ singleton_class: Class to check
69
+
70
+ Returns:
71
+ True if instance exists
72
+ """
73
+ return singleton_class in cls._instances
74
+
75
+ @classmethod
76
+ def clear_instance(cls, singleton_class: Type) -> None:
77
+ """
78
+ Clear singleton instance.
79
+
80
+ Args:
81
+ singleton_class: Class to clear instance for
82
+ """
83
+ if singleton_class in cls._locks:
84
+ with cls._locks[singleton_class]:
85
+ if singleton_class in cls._instances:
86
+ logger = get_logger("singleton_manager")
87
+ logger.debug(f"Clearing singleton instance: {singleton_class.__name__}")
88
+ del cls._instances[singleton_class]
89
+
90
+ @classmethod
91
+ def clear_all_instances(cls) -> None:
92
+ """Clear all singleton instances."""
93
+ with cls._global_lock:
94
+ logger = get_logger("singleton_manager")
95
+ logger.debug(f"Clearing {len(cls._instances)} singleton instances")
96
+ cls._instances.clear()
97
+
98
+ @classmethod
99
+ def get_instance_info(cls) -> Dict[str, Any]:
100
+ """
101
+ Get information about managed instances.
102
+
103
+ Returns:
104
+ Dictionary with instance information
105
+ """
106
+ return {
107
+ "instance_count": len(cls._instances),
108
+ "instance_types": [cls_type.__name__ for cls_type in cls._instances.keys()],
109
+ "lock_count": len(cls._locks)
110
+ }
111
+
112
+
113
+ class SingletonMixin:
114
+ """
115
+ Mixin class to add singleton behavior to any class.
116
+
117
+ Usage:
118
+ class MyService(SingletonMixin):
119
+ def __init__(self):
120
+ super().__init__()
121
+ # Your initialization code
122
+ """
123
+
124
+ def __new__(cls, *args, **kwargs):
125
+ """Override __new__ to implement singleton pattern."""
126
+ return SingletonManager.get_instance(cls, *args, **kwargs)
127
+
128
+ @classmethod
129
+ def get_instance(cls, *args, **kwargs):
130
+ """Get singleton instance."""
131
+ return SingletonManager.get_instance(cls, *args, **kwargs)
132
+
133
+ @classmethod
134
+ def clear_instance(cls):
135
+ """Clear singleton instance."""
136
+ SingletonManager.clear_instance(cls)
137
+
138
+ @classmethod
139
+ def has_instance(cls) -> bool:
140
+ """Check if instance exists."""
141
+ return SingletonManager.has_instance(cls)
142
+
143
+
144
+ def singleton(cls: Type[T]) -> Type[T]:
145
+ """
146
+ Decorator to make a class a singleton.
147
+
148
+ Usage:
149
+ @singleton
150
+ class MyService:
151
+ def __init__(self):
152
+ # Your initialization code
153
+ pass
154
+ """
155
+ original_new = cls.__new__
156
+
157
+ def new_new(cls_inner, *args, **kwargs):
158
+ return SingletonManager.get_instance(cls_inner, *args, **kwargs)
159
+
160
+ cls.__new__ = new_new
161
+
162
+ # Add convenience methods
163
+ cls.get_instance = classmethod(lambda cls_inner, *args, **kwargs:
164
+ SingletonManager.get_instance(cls_inner, *args, **kwargs))
165
+ cls.clear_instance = classmethod(lambda cls_inner:
166
+ SingletonManager.clear_instance(cls_inner))
167
+ cls.has_instance = classmethod(lambda cls_inner:
168
+ SingletonManager.has_instance(cls_inner))
169
+
170
+ return cls
171
+
172
+
173
+ # Example usage patterns
174
+ if __name__ == "__main__":
175
+ # Example 1: Using SingletonMixin
176
+ class ConfigService(SingletonMixin):
177
+ def __init__(self, config_path: str = "default.yaml"):
178
+ self.config_path = config_path
179
+ self.loaded = True
180
+
181
+ # Example 2: Using @singleton decorator
182
+ @singleton
183
+ class LoggerService:
184
+ def __init__(self, log_level: str = "INFO"):
185
+ self.log_level = log_level
186
+ self.initialized = True
187
+
188
+ # Example 3: Using SingletonManager directly
189
+ class DatabaseService:
190
+ def __init__(self, connection_string: str):
191
+ self.connection_string = connection_string
192
+ self.connected = True
193
+
194
+ # Test the patterns
195
+ config1 = ConfigService("config1.yaml")
196
+ config2 = ConfigService("config2.yaml") # Same instance as config1
197
+ assert config1 is config2
198
+ assert config1.config_path == "config1.yaml" # First initialization wins
199
+
200
+ logger1 = LoggerService("DEBUG")
201
+ logger2 = LoggerService("ERROR") # Same instance as logger1
202
+ assert logger1 is logger2
203
+ assert logger1.log_level == "DEBUG" # First initialization wins
204
+
205
+ db1 = SingletonManager.get_instance(DatabaseService, "postgres://localhost")
206
+ db2 = SingletonManager.get_instance(DatabaseService, "mysql://localhost") # Same instance
207
+ assert db1 is db2
208
+ assert db1.connection_string == "postgres://localhost" # First initialization wins
@@ -22,6 +22,7 @@ try:
22
22
  paths.ensure_in_path()
23
23
 
24
24
  from claude_mpm.core.config import Config
25
+ from claude_mpm.core.shared.config_loader import ConfigLoader
25
26
  from claude_mpm.hooks.base_hook import HookContext, HookType
26
27
  from claude_mpm.hooks.memory_integration_hook import (
27
28
  MemoryPostDelegationHook,
@@ -60,8 +61,9 @@ class MemoryHookManager:
60
61
  are triggered at the right times during agent delegation.
61
62
  """
62
63
  try:
63
- # Create configuration
64
- config = Config()
64
+ # Create configuration using ConfigLoader
65
+ config_loader = ConfigLoader()
66
+ config = config_loader.load_main_config()
65
67
 
66
68
  # Only initialize if memory system is enabled
67
69
  if not config.get("memory.enabled", True):
@@ -51,11 +51,22 @@ class ResponseTrackingManager:
51
51
  response tracking without code changes.
52
52
  """
53
53
  try:
54
- # Create configuration with optional config file
54
+ # Create configuration with optional config file using ConfigLoader
55
55
  config_file = os.environ.get("CLAUDE_PM_CONFIG_FILE")
56
56
  from claude_mpm.core.config import Config
57
-
58
- config = Config(config_file=config_file) if config_file else Config()
57
+ from claude_mpm.core.shared.config_loader import ConfigLoader, ConfigPattern
58
+
59
+ config_loader = ConfigLoader()
60
+ if config_file:
61
+ # Use specific config file with ConfigLoader
62
+ pattern = ConfigPattern(
63
+ filenames=[os.path.basename(config_file)],
64
+ search_paths=[os.path.dirname(config_file)],
65
+ env_prefix="CLAUDE_MPM_"
66
+ )
67
+ config = config_loader.load_config(pattern, cache_key=f"response_tracking_{config_file}")
68
+ else:
69
+ config = config_loader.load_main_config()
59
70
 
60
71
  # Check if response tracking is enabled (check both sections for compatibility)
61
72
  response_tracking_enabled = config.get("response_tracking.enabled", False)
@@ -17,6 +17,7 @@ from typing import Any, Dict, List
17
17
 
18
18
  from claude_mpm.core.config import Config
19
19
  from claude_mpm.core.logger import get_logger
20
+ from claude_mpm.core.shared.config_loader import ConfigLoader
20
21
  from claude_mpm.hooks.base_hook import (
21
22
  HookContext,
22
23
  HookResult,
@@ -65,7 +66,11 @@ class MemoryPreDelegationHook(PreDelegationHook):
65
66
  config: Optional Config object. If not provided, will create default Config.
66
67
  """
67
68
  super().__init__(name="memory_pre_delegation", priority=20)
68
- self.config = config or Config()
69
+ if config:
70
+ self.config = config
71
+ else:
72
+ config_loader = ConfigLoader()
73
+ self.config = config_loader.load_main_config()
69
74
 
70
75
  # Initialize memory manager only if available
71
76
  if MEMORY_MANAGER_AVAILABLE and AgentMemoryManager:
@@ -196,7 +201,11 @@ class MemoryPostDelegationHook(PostDelegationHook):
196
201
  config: Optional Config object. If not provided, will create default Config.
197
202
  """
198
203
  super().__init__(name="memory_post_delegation", priority=80)
199
- self.config = config or Config()
204
+ if config:
205
+ self.config = config
206
+ else:
207
+ config_loader = ConfigLoader()
208
+ self.config = config_loader.load_main_config()
200
209
 
201
210
  # Initialize memory manager only if available
202
211
  if MEMORY_MANAGER_AVAILABLE and AgentMemoryManager: