claude-mpm 4.0.22__py3-none-any.whl → 4.0.25__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/BUILD_NUMBER +1 -1
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +4 -1
- claude_mpm/agents/BASE_PM.md +3 -0
- claude_mpm/agents/templates/code_analyzer.json +3 -3
- claude_mpm/agents/templates/data_engineer.json +2 -2
- claude_mpm/agents/templates/documentation.json +36 -9
- claude_mpm/agents/templates/engineer.json +2 -2
- claude_mpm/agents/templates/ops.json +2 -2
- claude_mpm/agents/templates/qa.json +2 -2
- claude_mpm/agents/templates/refactoring_engineer.json +65 -43
- claude_mpm/agents/templates/security.json +2 -2
- claude_mpm/agents/templates/version_control.json +2 -2
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/commands/agents.py +453 -113
- claude_mpm/cli/commands/aggregate.py +107 -15
- claude_mpm/cli/commands/cleanup.py +142 -10
- claude_mpm/cli/commands/config.py +358 -224
- claude_mpm/cli/commands/info.py +184 -75
- claude_mpm/cli/commands/mcp_command_router.py +5 -76
- claude_mpm/cli/commands/mcp_install_commands.py +68 -36
- claude_mpm/cli/commands/mcp_server_commands.py +30 -37
- claude_mpm/cli/commands/memory.py +331 -61
- claude_mpm/cli/commands/monitor.py +101 -7
- claude_mpm/cli/commands/run.py +368 -8
- claude_mpm/cli/commands/tickets.py +206 -24
- claude_mpm/cli/parsers/mcp_parser.py +3 -0
- claude_mpm/cli/shared/__init__.py +40 -0
- claude_mpm/cli/shared/argument_patterns.py +212 -0
- claude_mpm/cli/shared/command_base.py +234 -0
- claude_mpm/cli/shared/error_handling.py +238 -0
- claude_mpm/cli/shared/output_formatters.py +231 -0
- claude_mpm/config/agent_config.py +29 -8
- claude_mpm/core/container.py +6 -4
- claude_mpm/core/framework_loader.py +32 -9
- claude_mpm/core/service_registry.py +4 -2
- claude_mpm/core/shared/__init__.py +17 -0
- claude_mpm/core/shared/config_loader.py +320 -0
- claude_mpm/core/shared/path_resolver.py +277 -0
- claude_mpm/core/shared/singleton_manager.py +208 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +4 -2
- claude_mpm/hooks/claude_hooks/response_tracking.py +14 -3
- claude_mpm/hooks/memory_integration_hook.py +11 -2
- claude_mpm/services/agents/deployment/agent_deployment.py +43 -23
- claude_mpm/services/agents/deployment/deployment_wrapper.py +71 -0
- claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +1 -0
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +43 -0
- claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +4 -0
- claude_mpm/services/agents/deployment/processors/agent_processor.py +1 -1
- claude_mpm/services/agents/loading/base_agent_manager.py +11 -3
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +14 -5
- claude_mpm/services/event_aggregator.py +4 -2
- claude_mpm/services/mcp_gateway/config/config_loader.py +89 -28
- claude_mpm/services/mcp_gateway/config/configuration.py +29 -0
- claude_mpm/services/mcp_gateway/registry/service_registry.py +22 -5
- claude_mpm/services/memory/builder.py +6 -1
- claude_mpm/services/response_tracker.py +3 -1
- claude_mpm/services/runner_configuration_service.py +15 -6
- claude_mpm/services/shared/__init__.py +20 -0
- claude_mpm/services/shared/async_service_base.py +219 -0
- claude_mpm/services/shared/config_service_base.py +292 -0
- claude_mpm/services/shared/lifecycle_service_base.py +317 -0
- claude_mpm/services/shared/manager_base.py +303 -0
- claude_mpm/services/shared/service_factory.py +308 -0
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/METADATA +19 -13
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/RECORD +70 -54
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/top_level.txt +0 -0
|
@@ -10,6 +10,8 @@ This module handles:
|
|
|
10
10
|
- Environment variable support for agent paths
|
|
11
11
|
- Project-specific agent overrides
|
|
12
12
|
- Tier precedence configuration
|
|
13
|
+
|
|
14
|
+
UPDATED: Migrated to use shared ConfigLoader pattern (TSK-0141)
|
|
13
15
|
"""
|
|
14
16
|
|
|
15
17
|
import json
|
|
@@ -21,6 +23,7 @@ from pathlib import Path
|
|
|
21
23
|
from typing import Any, Dict, List, Optional
|
|
22
24
|
|
|
23
25
|
from claude_mpm.core.unified_paths import get_path_manager
|
|
26
|
+
from claude_mpm.core.shared.config_loader import ConfigLoader, ConfigPattern
|
|
24
27
|
|
|
25
28
|
logger = logging.getLogger(__name__)
|
|
26
29
|
|
|
@@ -62,6 +65,9 @@ class AgentConfig:
|
|
|
62
65
|
validate_on_load: bool = True
|
|
63
66
|
strict_validation: bool = False
|
|
64
67
|
|
|
68
|
+
# ConfigLoader instance for consistent configuration loading
|
|
69
|
+
_config_loader: ConfigLoader = field(default_factory=ConfigLoader, init=False)
|
|
70
|
+
|
|
65
71
|
@classmethod
|
|
66
72
|
def from_environment(cls) -> "AgentConfig":
|
|
67
73
|
"""
|
|
@@ -147,14 +153,29 @@ class AgentConfig:
|
|
|
147
153
|
return cls()
|
|
148
154
|
|
|
149
155
|
try:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
# Use ConfigLoader for consistent file loading
|
|
157
|
+
config_loader = ConfigLoader()
|
|
158
|
+
|
|
159
|
+
# Create a pattern for agent configuration
|
|
160
|
+
pattern = ConfigPattern(
|
|
161
|
+
filenames=[config_file.name],
|
|
162
|
+
search_paths=[str(config_file.parent)],
|
|
163
|
+
env_prefix="CLAUDE_MPM_AGENT_",
|
|
164
|
+
defaults={
|
|
165
|
+
"enable_project_agents": True,
|
|
166
|
+
"enable_user_agents": True,
|
|
167
|
+
"enable_system_agents": True,
|
|
168
|
+
"enable_hot_reload": True,
|
|
169
|
+
"cache_ttl_seconds": 3600,
|
|
170
|
+
"enable_caching": True,
|
|
171
|
+
"validate_on_load": True,
|
|
172
|
+
"strict_validation": False,
|
|
173
|
+
"precedence_mode": "override"
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
loaded_config = config_loader.load_config(pattern, cache_key=f"agent_{config_file}")
|
|
178
|
+
data = loaded_config.to_dict()
|
|
158
179
|
|
|
159
180
|
config = cls()
|
|
160
181
|
|
claude_mpm/core/container.py
CHANGED
|
@@ -250,8 +250,9 @@ class DIContainer(IServiceContainer):
|
|
|
250
250
|
instance: Pre-created instance to register as singleton
|
|
251
251
|
|
|
252
252
|
Examples:
|
|
253
|
-
# Register a pre-created instance
|
|
254
|
-
|
|
253
|
+
# Register a pre-created instance using ConfigLoader
|
|
254
|
+
config_loader = ConfigLoader()
|
|
255
|
+
config = config_loader.load_main_config()
|
|
255
256
|
container.register_instance(IConfig, config)
|
|
256
257
|
"""
|
|
257
258
|
self._register_internal(
|
|
@@ -307,8 +308,9 @@ class DIContainer(IServiceContainer):
|
|
|
307
308
|
# Register with implementation class
|
|
308
309
|
container.register_singleton(ILogger, ConsoleLogger)
|
|
309
310
|
|
|
310
|
-
# Register with instance
|
|
311
|
-
|
|
311
|
+
# Register with instance using ConfigLoader
|
|
312
|
+
config_loader = ConfigLoader()
|
|
313
|
+
container.register_singleton(IConfig, instance=config_loader.load_main_config())
|
|
312
314
|
|
|
313
315
|
# Register with disposal handler
|
|
314
316
|
container.register_singleton(
|
|
@@ -640,9 +640,10 @@ class FrameworkLoader:
|
|
|
640
640
|
Aggregate multiple memory entries into a single memory string.
|
|
641
641
|
|
|
642
642
|
Strategy:
|
|
643
|
-
-
|
|
644
|
-
-
|
|
645
|
-
-
|
|
643
|
+
- Support both sectioned and non-sectioned memories
|
|
644
|
+
- Preserve all bullet-point items (lines starting with -)
|
|
645
|
+
- Merge sections when present, with project-level taking precedence
|
|
646
|
+
- Remove exact duplicates within sections and unsectioned items
|
|
646
647
|
- Preserve unique entries from both sources
|
|
647
648
|
|
|
648
649
|
Args:
|
|
@@ -658,15 +659,16 @@ class FrameworkLoader:
|
|
|
658
659
|
if len(memory_entries) == 1:
|
|
659
660
|
return memory_entries[0]["content"]
|
|
660
661
|
|
|
661
|
-
# Parse all memories into sections
|
|
662
|
+
# Parse all memories into sections and unsectioned items
|
|
662
663
|
all_sections = {}
|
|
664
|
+
unsectioned_items = {} # Items without a section header
|
|
663
665
|
metadata_lines = []
|
|
664
666
|
|
|
665
667
|
for entry in memory_entries:
|
|
666
668
|
content = entry["content"]
|
|
667
669
|
source = entry["source"]
|
|
668
670
|
|
|
669
|
-
# Parse content into sections
|
|
671
|
+
# Parse content into sections and unsectioned items
|
|
670
672
|
current_section = None
|
|
671
673
|
current_items = []
|
|
672
674
|
|
|
@@ -690,11 +692,25 @@ class FrameworkLoader:
|
|
|
690
692
|
# Start new section
|
|
691
693
|
current_section = line
|
|
692
694
|
current_items = []
|
|
693
|
-
# Check for content lines (
|
|
694
|
-
elif line.strip()
|
|
695
|
-
|
|
695
|
+
# Check for content lines (including unsectioned bullet points)
|
|
696
|
+
elif line.strip():
|
|
697
|
+
# If it's a bullet point or regular content
|
|
698
|
+
if current_section:
|
|
699
|
+
# Add to current section
|
|
700
|
+
current_items.append(line)
|
|
701
|
+
elif line.strip().startswith('-'):
|
|
702
|
+
# It's an unsectioned bullet point - preserve it
|
|
703
|
+
# Use content as key to detect duplicates
|
|
704
|
+
# Project source overrides user source
|
|
705
|
+
if line not in unsectioned_items or source == "project":
|
|
706
|
+
unsectioned_items[line] = source
|
|
707
|
+
# Skip other non-bullet unsectioned content (like headers)
|
|
708
|
+
elif not line.strip().startswith('#'):
|
|
709
|
+
# Include non-header orphaned content in unsectioned items
|
|
710
|
+
if line not in unsectioned_items or source == "project":
|
|
711
|
+
unsectioned_items[line] = source
|
|
696
712
|
|
|
697
|
-
# Save last section
|
|
713
|
+
# Save last section if exists
|
|
698
714
|
if current_section and current_items:
|
|
699
715
|
if current_section not in all_sections:
|
|
700
716
|
all_sections[current_section] = {}
|
|
@@ -717,6 +733,13 @@ class FrameworkLoader:
|
|
|
717
733
|
lines.append("*This memory combines user-level and project-level memories.*")
|
|
718
734
|
lines.append("")
|
|
719
735
|
|
|
736
|
+
# Add unsectioned items first (if any)
|
|
737
|
+
if unsectioned_items:
|
|
738
|
+
# Sort items to ensure consistent output
|
|
739
|
+
for item in sorted(unsectioned_items.keys()):
|
|
740
|
+
lines.append(item)
|
|
741
|
+
lines.append("") # Empty line after unsectioned items
|
|
742
|
+
|
|
720
743
|
# Add sections
|
|
721
744
|
for section_header in sorted(all_sections.keys()):
|
|
722
745
|
lines.append(section_header)
|
|
@@ -11,6 +11,7 @@ from .base_service import BaseService
|
|
|
11
11
|
from .config import Config
|
|
12
12
|
from .container import DIContainer, ServiceLifetime, get_container
|
|
13
13
|
from .logger import get_logger
|
|
14
|
+
from .shared.config_loader import ConfigLoader
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
from claude_mpm.services.agents.deployment import AgentDeploymentService
|
|
@@ -46,8 +47,9 @@ class ServiceRegistry:
|
|
|
46
47
|
from .agent_session_manager import AgentSessionManager
|
|
47
48
|
from .session_manager import SessionManager
|
|
48
49
|
|
|
49
|
-
# Register configuration as singleton with name
|
|
50
|
-
|
|
50
|
+
# Register configuration as singleton with name using ConfigLoader
|
|
51
|
+
config_loader = ConfigLoader()
|
|
52
|
+
config = config_loader.load_main_config()
|
|
51
53
|
self.container.register_singleton(Config, instance=config, name="main_config")
|
|
52
54
|
|
|
53
55
|
# Register core services with proper lifetime management
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared core utilities to reduce code duplication.
|
|
3
|
+
|
|
4
|
+
This module provides common utilities that can be used across
|
|
5
|
+
different parts of the application.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .config_loader import ConfigLoader, ConfigPattern
|
|
9
|
+
from .path_resolver import PathResolver
|
|
10
|
+
from .singleton_manager import SingletonManager
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ConfigLoader",
|
|
14
|
+
"ConfigPattern",
|
|
15
|
+
"PathResolver",
|
|
16
|
+
"SingletonManager",
|
|
17
|
+
]
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared configuration loading utilities to reduce duplication.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from ..config import Config
|
|
11
|
+
from ..logger import get_logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ConfigPattern:
|
|
16
|
+
"""Configuration loading pattern definition."""
|
|
17
|
+
|
|
18
|
+
# File patterns to search for
|
|
19
|
+
filenames: List[str]
|
|
20
|
+
|
|
21
|
+
# Search paths (relative to working directory)
|
|
22
|
+
search_paths: List[str]
|
|
23
|
+
|
|
24
|
+
# Environment variable prefix
|
|
25
|
+
env_prefix: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
# Default values
|
|
28
|
+
defaults: Optional[Dict[str, Any]] = None
|
|
29
|
+
|
|
30
|
+
# Required configuration keys
|
|
31
|
+
required_keys: Optional[List[str]] = None
|
|
32
|
+
|
|
33
|
+
# Configuration section name
|
|
34
|
+
section: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConfigLoader:
|
|
38
|
+
"""
|
|
39
|
+
Centralized configuration loading utility.
|
|
40
|
+
|
|
41
|
+
Reduces duplication by providing standard patterns for:
|
|
42
|
+
- Configuration file discovery
|
|
43
|
+
- Environment variable loading
|
|
44
|
+
- Default value management
|
|
45
|
+
- Configuration validation
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# Standard configuration patterns
|
|
49
|
+
AGENT_CONFIG = ConfigPattern(
|
|
50
|
+
filenames=[".agent.yaml", ".agent.yml", "agent.yaml", "agent.yml"],
|
|
51
|
+
search_paths=[".", ".claude-mpm", "agents"],
|
|
52
|
+
env_prefix="CLAUDE_MPM_AGENT_",
|
|
53
|
+
defaults={
|
|
54
|
+
"timeout": 30,
|
|
55
|
+
"max_retries": 3,
|
|
56
|
+
"log_level": "INFO"
|
|
57
|
+
},
|
|
58
|
+
required_keys=["name", "model"]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
MEMORY_CONFIG = ConfigPattern(
|
|
62
|
+
filenames=[".memory.yaml", ".memory.yml", "memory.yaml", "memory.yml"],
|
|
63
|
+
search_paths=[".", ".claude-mpm", "memories"],
|
|
64
|
+
env_prefix="CLAUDE_MPM_MEMORY_",
|
|
65
|
+
defaults={
|
|
66
|
+
"max_entries": 1000,
|
|
67
|
+
"cleanup_interval": 3600,
|
|
68
|
+
"compression": True
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
SERVICE_CONFIG = ConfigPattern(
|
|
73
|
+
filenames=[".service.yaml", ".service.yml", "service.yaml", "service.yml"],
|
|
74
|
+
search_paths=[".", ".claude-mpm", "config"],
|
|
75
|
+
env_prefix="CLAUDE_MPM_SERVICE_",
|
|
76
|
+
defaults={
|
|
77
|
+
"enabled": True,
|
|
78
|
+
"auto_start": False,
|
|
79
|
+
"health_check_interval": 60
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def __init__(self, working_dir: Union[str, Path] = None):
|
|
84
|
+
"""
|
|
85
|
+
Initialize config loader.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
working_dir: Working directory for relative paths
|
|
89
|
+
"""
|
|
90
|
+
self.working_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
91
|
+
self.logger = get_logger("config_loader")
|
|
92
|
+
self._cache: Dict[str, Config] = {}
|
|
93
|
+
|
|
94
|
+
def load_config(self,
|
|
95
|
+
pattern: ConfigPattern,
|
|
96
|
+
cache_key: str = None,
|
|
97
|
+
force_reload: bool = False) -> Config:
|
|
98
|
+
"""
|
|
99
|
+
Load configuration using a pattern.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
pattern: Configuration pattern to use
|
|
103
|
+
cache_key: Optional cache key (defaults to pattern hash)
|
|
104
|
+
force_reload: Force reload even if cached
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Loaded configuration
|
|
108
|
+
"""
|
|
109
|
+
# Generate cache key if not provided
|
|
110
|
+
if cache_key is None:
|
|
111
|
+
cache_key = self._generate_cache_key(pattern)
|
|
112
|
+
|
|
113
|
+
# Check cache
|
|
114
|
+
if not force_reload and cache_key in self._cache:
|
|
115
|
+
self.logger.debug(f"Using cached config: {cache_key}")
|
|
116
|
+
return self._cache[cache_key]
|
|
117
|
+
|
|
118
|
+
# Start with defaults
|
|
119
|
+
config_data = pattern.defaults.copy() if pattern.defaults else {}
|
|
120
|
+
|
|
121
|
+
# Load from files
|
|
122
|
+
config_file = self._find_config_file(pattern)
|
|
123
|
+
if config_file:
|
|
124
|
+
file_config = self._load_config_file(config_file)
|
|
125
|
+
if pattern.section:
|
|
126
|
+
file_config = file_config.get(pattern.section, {})
|
|
127
|
+
config_data.update(file_config)
|
|
128
|
+
self.logger.info(f"Loaded config from: {config_file}")
|
|
129
|
+
|
|
130
|
+
# Load from environment
|
|
131
|
+
if pattern.env_prefix:
|
|
132
|
+
env_config = self._load_env_config(pattern.env_prefix)
|
|
133
|
+
config_data.update(env_config)
|
|
134
|
+
if env_config:
|
|
135
|
+
self.logger.debug(f"Loaded {len(env_config)} env vars with prefix {pattern.env_prefix}")
|
|
136
|
+
|
|
137
|
+
# Create config instance
|
|
138
|
+
config = Config(config_data)
|
|
139
|
+
|
|
140
|
+
# Validate required keys
|
|
141
|
+
if pattern.required_keys:
|
|
142
|
+
self._validate_required_keys(config, pattern.required_keys)
|
|
143
|
+
|
|
144
|
+
# Cache the result
|
|
145
|
+
self._cache[cache_key] = config
|
|
146
|
+
|
|
147
|
+
return config
|
|
148
|
+
|
|
149
|
+
def load_agent_config(self, agent_dir: Union[str, Path] = None) -> Config:
|
|
150
|
+
"""Load agent configuration."""
|
|
151
|
+
pattern = self.AGENT_CONFIG
|
|
152
|
+
if agent_dir:
|
|
153
|
+
# Override search paths for specific agent directory
|
|
154
|
+
pattern = ConfigPattern(
|
|
155
|
+
filenames=pattern.filenames,
|
|
156
|
+
search_paths=[str(agent_dir)],
|
|
157
|
+
env_prefix=pattern.env_prefix,
|
|
158
|
+
defaults=pattern.defaults,
|
|
159
|
+
required_keys=pattern.required_keys
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return self.load_config(pattern, cache_key=f"agent_{agent_dir}")
|
|
163
|
+
|
|
164
|
+
def load_main_config(self) -> Config:
|
|
165
|
+
"""Load main application configuration."""
|
|
166
|
+
pattern = ConfigPattern(
|
|
167
|
+
filenames=["claude-mpm.yaml", "claude-mpm.yml", ".claude-mpm.yaml", ".claude-mpm.yml", "config.yaml", "config.yml"],
|
|
168
|
+
search_paths=["~/.config/claude-mpm", ".", "./config", "/etc/claude-mpm"],
|
|
169
|
+
env_prefix="CLAUDE_MPM_",
|
|
170
|
+
defaults={}
|
|
171
|
+
)
|
|
172
|
+
return self.load_config(pattern, cache_key="main_config")
|
|
173
|
+
|
|
174
|
+
def load_memory_config(self, memory_dir: Union[str, Path] = None) -> Config:
|
|
175
|
+
"""Load memory configuration."""
|
|
176
|
+
pattern = self.MEMORY_CONFIG
|
|
177
|
+
if memory_dir:
|
|
178
|
+
pattern = ConfigPattern(
|
|
179
|
+
filenames=pattern.filenames,
|
|
180
|
+
search_paths=[str(memory_dir)],
|
|
181
|
+
env_prefix=pattern.env_prefix,
|
|
182
|
+
defaults=pattern.defaults
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return self.load_config(pattern, cache_key=f"memory_{memory_dir}")
|
|
186
|
+
|
|
187
|
+
def load_service_config(self, service_name: str, config_dir: Union[str, Path] = None) -> Config:
|
|
188
|
+
"""Load service configuration."""
|
|
189
|
+
pattern = self.SERVICE_CONFIG
|
|
190
|
+
|
|
191
|
+
# Add service-specific filenames
|
|
192
|
+
service_filenames = [
|
|
193
|
+
f".{service_name}.yaml",
|
|
194
|
+
f".{service_name}.yml",
|
|
195
|
+
f"{service_name}.yaml",
|
|
196
|
+
f"{service_name}.yml"
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
pattern = ConfigPattern(
|
|
200
|
+
filenames=service_filenames + pattern.filenames,
|
|
201
|
+
search_paths=[str(config_dir)] if config_dir else pattern.search_paths,
|
|
202
|
+
env_prefix=f"CLAUDE_MPM_{service_name.upper()}_",
|
|
203
|
+
defaults=pattern.defaults,
|
|
204
|
+
section=service_name
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return self.load_config(pattern, cache_key=f"service_{service_name}")
|
|
208
|
+
|
|
209
|
+
def _find_config_file(self, pattern: ConfigPattern) -> Optional[Path]:
|
|
210
|
+
"""Find configuration file using pattern."""
|
|
211
|
+
for search_path in pattern.search_paths:
|
|
212
|
+
search_dir = self.working_dir / search_path
|
|
213
|
+
if not search_dir.exists():
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
for filename in pattern.filenames:
|
|
217
|
+
config_file = search_dir / filename
|
|
218
|
+
if config_file.exists() and config_file.is_file():
|
|
219
|
+
return config_file
|
|
220
|
+
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
def _load_config_file(self, config_file: Path) -> Dict[str, Any]:
|
|
224
|
+
"""Load configuration from file."""
|
|
225
|
+
try:
|
|
226
|
+
import yaml
|
|
227
|
+
|
|
228
|
+
with open(config_file, 'r') as f:
|
|
229
|
+
if config_file.suffix.lower() in ('.yaml', '.yml'):
|
|
230
|
+
return yaml.safe_load(f) or {}
|
|
231
|
+
else:
|
|
232
|
+
# Try JSON as fallback
|
|
233
|
+
import json
|
|
234
|
+
f.seek(0)
|
|
235
|
+
return json.load(f)
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
self.logger.error(f"Failed to load config file {config_file}: {e}")
|
|
239
|
+
return {}
|
|
240
|
+
|
|
241
|
+
def _load_env_config(self, prefix: str) -> Dict[str, Any]:
|
|
242
|
+
"""Load configuration from environment variables."""
|
|
243
|
+
config = {}
|
|
244
|
+
|
|
245
|
+
for key, value in os.environ.items():
|
|
246
|
+
if key.startswith(prefix):
|
|
247
|
+
# Convert env var name to config key
|
|
248
|
+
config_key = key[len(prefix):].lower().replace('_', '.')
|
|
249
|
+
|
|
250
|
+
# Try to parse value
|
|
251
|
+
parsed_value = self._parse_env_value(value)
|
|
252
|
+
config[config_key] = parsed_value
|
|
253
|
+
|
|
254
|
+
return config
|
|
255
|
+
|
|
256
|
+
def _parse_env_value(self, value: str) -> Any:
|
|
257
|
+
"""Parse environment variable value."""
|
|
258
|
+
# Try boolean
|
|
259
|
+
if value.lower() in ('true', 'false'):
|
|
260
|
+
return value.lower() == 'true'
|
|
261
|
+
|
|
262
|
+
# Try integer
|
|
263
|
+
try:
|
|
264
|
+
return int(value)
|
|
265
|
+
except ValueError:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
# Try float
|
|
269
|
+
try:
|
|
270
|
+
return float(value)
|
|
271
|
+
except ValueError:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
# Try JSON
|
|
275
|
+
try:
|
|
276
|
+
import json
|
|
277
|
+
return json.loads(value)
|
|
278
|
+
except (json.JSONDecodeError, ValueError):
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
# Return as string
|
|
282
|
+
return value
|
|
283
|
+
|
|
284
|
+
def _validate_required_keys(self, config: Config, required_keys: List[str]) -> None:
|
|
285
|
+
"""Validate that required keys are present."""
|
|
286
|
+
missing_keys = []
|
|
287
|
+
|
|
288
|
+
for key in required_keys:
|
|
289
|
+
if config.get(key) is None:
|
|
290
|
+
missing_keys.append(key)
|
|
291
|
+
|
|
292
|
+
if missing_keys:
|
|
293
|
+
raise ValueError(f"Missing required configuration keys: {missing_keys}")
|
|
294
|
+
|
|
295
|
+
def _generate_cache_key(self, pattern: ConfigPattern) -> str:
|
|
296
|
+
"""Generate cache key for pattern."""
|
|
297
|
+
import hashlib
|
|
298
|
+
|
|
299
|
+
# Create a string representation of the pattern
|
|
300
|
+
pattern_str = f"{pattern.filenames}_{pattern.search_paths}_{pattern.env_prefix}"
|
|
301
|
+
|
|
302
|
+
# Generate hash
|
|
303
|
+
return hashlib.md5(pattern_str.encode()).hexdigest()[:8]
|
|
304
|
+
|
|
305
|
+
def clear_cache(self, cache_key: str = None) -> None:
|
|
306
|
+
"""Clear configuration cache."""
|
|
307
|
+
if cache_key:
|
|
308
|
+
self._cache.pop(cache_key, None)
|
|
309
|
+
self.logger.debug(f"Cleared cache for: {cache_key}")
|
|
310
|
+
else:
|
|
311
|
+
self._cache.clear()
|
|
312
|
+
self.logger.debug("Cleared all config cache")
|
|
313
|
+
|
|
314
|
+
def get_cache_info(self) -> Dict[str, Any]:
|
|
315
|
+
"""Get cache information."""
|
|
316
|
+
return {
|
|
317
|
+
"cached_configs": len(self._cache),
|
|
318
|
+
"cache_keys": list(self._cache.keys()),
|
|
319
|
+
"working_dir": str(self.working_dir)
|
|
320
|
+
}
|