claude-mpm 0.3.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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/__init__.py +17 -0
- claude_mpm/__main__.py +14 -0
- claude_mpm/_version.py +32 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
- claude_mpm/agents/INSTRUCTIONS.md +375 -0
- claude_mpm/agents/__init__.py +118 -0
- claude_mpm/agents/agent_loader.py +621 -0
- claude_mpm/agents/agent_loader_integration.py +229 -0
- claude_mpm/agents/agents_metadata.py +204 -0
- claude_mpm/agents/base_agent.json +27 -0
- claude_mpm/agents/base_agent_loader.py +519 -0
- claude_mpm/agents/schema/agent_schema.json +160 -0
- claude_mpm/agents/system_agent_config.py +587 -0
- claude_mpm/agents/templates/__init__.py +101 -0
- claude_mpm/agents/templates/data_engineer_agent.json +46 -0
- claude_mpm/agents/templates/documentation_agent.json +45 -0
- claude_mpm/agents/templates/engineer_agent.json +49 -0
- claude_mpm/agents/templates/ops_agent.json +46 -0
- claude_mpm/agents/templates/qa_agent.json +45 -0
- claude_mpm/agents/templates/research_agent.json +49 -0
- claude_mpm/agents/templates/security_agent.json +46 -0
- claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
- claude_mpm/agents/templates/version_control_agent.json +46 -0
- claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
- claude_mpm/cli.py +655 -0
- claude_mpm/cli_main.py +13 -0
- claude_mpm/cli_module/__init__.py +15 -0
- claude_mpm/cli_module/args.py +222 -0
- claude_mpm/cli_module/commands.py +203 -0
- claude_mpm/cli_module/migration_example.py +183 -0
- claude_mpm/cli_module/refactoring_guide.md +253 -0
- claude_mpm/cli_old/__init__.py +1 -0
- claude_mpm/cli_old/ticket_cli.py +102 -0
- claude_mpm/config/__init__.py +5 -0
- claude_mpm/config/hook_config.py +42 -0
- claude_mpm/constants.py +150 -0
- claude_mpm/core/__init__.py +45 -0
- claude_mpm/core/agent_name_normalizer.py +248 -0
- claude_mpm/core/agent_registry.py +627 -0
- claude_mpm/core/agent_registry.py.bak +312 -0
- claude_mpm/core/agent_session_manager.py +273 -0
- claude_mpm/core/base_service.py +747 -0
- claude_mpm/core/base_service.py.bak +406 -0
- claude_mpm/core/config.py +334 -0
- claude_mpm/core/config_aliases.py +292 -0
- claude_mpm/core/container.py +347 -0
- claude_mpm/core/factories.py +281 -0
- claude_mpm/core/framework_loader.py +472 -0
- claude_mpm/core/injectable_service.py +206 -0
- claude_mpm/core/interfaces.py +539 -0
- claude_mpm/core/logger.py +468 -0
- claude_mpm/core/minimal_framework_loader.py +107 -0
- claude_mpm/core/mixins.py +150 -0
- claude_mpm/core/service_registry.py +299 -0
- claude_mpm/core/session_manager.py +190 -0
- claude_mpm/core/simple_runner.py +511 -0
- claude_mpm/core/tool_access_control.py +173 -0
- claude_mpm/hooks/README.md +243 -0
- claude_mpm/hooks/__init__.py +5 -0
- claude_mpm/hooks/base_hook.py +154 -0
- claude_mpm/hooks/builtin/__init__.py +1 -0
- claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
- claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
- claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
- claude_mpm/hooks/hook_client.py +264 -0
- claude_mpm/hooks/hook_runner.py +370 -0
- claude_mpm/hooks/json_rpc_executor.py +259 -0
- claude_mpm/hooks/json_rpc_hook_client.py +319 -0
- claude_mpm/hooks/tool_call_interceptor.py +204 -0
- claude_mpm/init.py +246 -0
- claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
- claude_mpm/orchestration/__init__.py +6 -0
- claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
- claude_mpm/orchestration/archive/factory.py +215 -0
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
- claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
- claude_mpm/orchestration/archive/orchestrator.py +501 -0
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
- claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
- claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
- claude_mpm/scripts/__init__.py +1 -0
- claude_mpm/scripts/ticket.py +269 -0
- claude_mpm/services/__init__.py +10 -0
- claude_mpm/services/agent_deployment.py +955 -0
- claude_mpm/services/agent_lifecycle_manager.py +948 -0
- claude_mpm/services/agent_management_service.py +596 -0
- claude_mpm/services/agent_modification_tracker.py +841 -0
- claude_mpm/services/agent_profile_loader.py +606 -0
- claude_mpm/services/agent_registry.py +677 -0
- claude_mpm/services/base_agent_manager.py +380 -0
- claude_mpm/services/framework_agent_loader.py +337 -0
- claude_mpm/services/framework_claude_md_generator/README.md +92 -0
- claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
- claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
- claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
- claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
- claude_mpm/services/framework_claude_md_generator.py +621 -0
- claude_mpm/services/hook_service.py +388 -0
- claude_mpm/services/hook_service_manager.py +223 -0
- claude_mpm/services/json_rpc_hook_manager.py +92 -0
- claude_mpm/services/parent_directory_manager/README.md +83 -0
- claude_mpm/services/parent_directory_manager/__init__.py +577 -0
- claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
- claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
- claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
- claude_mpm/services/parent_directory_manager/operations.py +186 -0
- claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
- claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
- claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
- claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
- claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
- claude_mpm/services/shared_prompt_cache.py +819 -0
- claude_mpm/services/ticket_manager.py +213 -0
- claude_mpm/services/ticket_manager_di.py +318 -0
- claude_mpm/services/ticketing_service_original.py +508 -0
- claude_mpm/services/version_control/VERSION +1 -0
- claude_mpm/services/version_control/__init__.py +70 -0
- claude_mpm/services/version_control/branch_strategy.py +670 -0
- claude_mpm/services/version_control/conflict_resolution.py +744 -0
- claude_mpm/services/version_control/git_operations.py +784 -0
- claude_mpm/services/version_control/semantic_versioning.py +703 -0
- claude_mpm/ui/__init__.py +1 -0
- claude_mpm/ui/rich_terminal_ui.py +295 -0
- claude_mpm/ui/terminal_ui.py +328 -0
- claude_mpm/utils/__init__.py +16 -0
- claude_mpm/utils/config_manager.py +468 -0
- claude_mpm/utils/import_migration_example.py +80 -0
- claude_mpm/utils/imports.py +182 -0
- claude_mpm/utils/path_operations.py +357 -0
- claude_mpm/utils/paths.py +289 -0
- claude_mpm-0.3.0.dist-info/METADATA +290 -0
- claude_mpm-0.3.0.dist-info/RECORD +159 -0
- claude_mpm-0.3.0.dist-info/WHEEL +5 -0
- claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
- claude_mpm-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for Claude PM Framework.
|
|
3
|
+
|
|
4
|
+
Handles loading configuration from files, environment variables,
|
|
5
|
+
and default values with proper validation and type conversion.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, Optional, Union
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from ..utils.config_manager import ConfigurationManager
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Config:
|
|
19
|
+
"""
|
|
20
|
+
Configuration manager for Claude PM services.
|
|
21
|
+
|
|
22
|
+
Supports loading from:
|
|
23
|
+
- Python dictionaries
|
|
24
|
+
- JSON files
|
|
25
|
+
- YAML files
|
|
26
|
+
- Environment variables
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
config: Optional[Dict[str, Any]] = None,
|
|
32
|
+
config_file: Optional[Union[str, Path]] = None,
|
|
33
|
+
env_prefix: str = "CLAUDE_PM_",
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Initialize configuration.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
config: Base configuration dictionary
|
|
40
|
+
config_file: Path to configuration file (JSON or YAML)
|
|
41
|
+
env_prefix: Prefix for environment variables
|
|
42
|
+
"""
|
|
43
|
+
self._config: Dict[str, Any] = {}
|
|
44
|
+
self._env_prefix = env_prefix
|
|
45
|
+
self._config_mgr = ConfigurationManager(cache_enabled=True)
|
|
46
|
+
|
|
47
|
+
# Load base configuration
|
|
48
|
+
if config:
|
|
49
|
+
self._config.update(config)
|
|
50
|
+
|
|
51
|
+
# Load from file if provided
|
|
52
|
+
if config_file:
|
|
53
|
+
self.load_file(config_file)
|
|
54
|
+
|
|
55
|
+
# Load from environment variables (new and legacy prefixes)
|
|
56
|
+
self._load_env_vars()
|
|
57
|
+
self._load_legacy_env_vars()
|
|
58
|
+
|
|
59
|
+
# Apply defaults
|
|
60
|
+
self._apply_defaults()
|
|
61
|
+
|
|
62
|
+
def load_file(self, file_path: Union[str, Path]) -> None:
|
|
63
|
+
"""Load configuration from file."""
|
|
64
|
+
file_path = Path(file_path)
|
|
65
|
+
|
|
66
|
+
if not file_path.exists():
|
|
67
|
+
logger.warning(f"Configuration file not found: {file_path}")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
file_config = self._config_mgr.load_auto(file_path)
|
|
72
|
+
if file_config:
|
|
73
|
+
self._config = self._config_mgr.merge_configs(self._config, file_config)
|
|
74
|
+
logger.info(f"Loaded configuration from {file_path}")
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Failed to load configuration from {file_path}: {e}")
|
|
78
|
+
|
|
79
|
+
def _load_env_vars(self) -> None:
|
|
80
|
+
"""Load configuration from environment variables."""
|
|
81
|
+
for key, value in os.environ.items():
|
|
82
|
+
if key.startswith(self._env_prefix):
|
|
83
|
+
config_key = key[len(self._env_prefix) :].lower()
|
|
84
|
+
|
|
85
|
+
# Convert environment variable value to appropriate type
|
|
86
|
+
converted_value = self._convert_env_value(value)
|
|
87
|
+
self._config[config_key] = converted_value
|
|
88
|
+
|
|
89
|
+
logger.debug(f"Loaded env var: {key} -> {config_key}")
|
|
90
|
+
|
|
91
|
+
def _load_legacy_env_vars(self) -> None:
|
|
92
|
+
"""Load configuration from legacy CLAUDE_PM_ environment variables for backward compatibility."""
|
|
93
|
+
legacy_prefix = "CLAUDE_PM_"
|
|
94
|
+
loaded_legacy_vars = []
|
|
95
|
+
|
|
96
|
+
for key, value in os.environ.items():
|
|
97
|
+
if key.startswith(legacy_prefix):
|
|
98
|
+
config_key = key[len(legacy_prefix) :].lower()
|
|
99
|
+
|
|
100
|
+
# Only load if not already set by new environment variables
|
|
101
|
+
if config_key not in self._config:
|
|
102
|
+
converted_value = self._convert_env_value(value)
|
|
103
|
+
self._config[config_key] = converted_value
|
|
104
|
+
loaded_legacy_vars.append(key)
|
|
105
|
+
logger.debug(f"Loaded legacy env var: {key} -> {config_key}")
|
|
106
|
+
|
|
107
|
+
# Warn about legacy variables in use
|
|
108
|
+
if loaded_legacy_vars:
|
|
109
|
+
logger.warning(
|
|
110
|
+
f"Using legacy CLAUDE_PM_ environment variables: {', '.join(loaded_legacy_vars)}. "
|
|
111
|
+
"Please migrate to CLAUDE_MULTIAGENT_PM_ prefix for future compatibility."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _convert_env_value(self, value: str) -> Union[str, int, float, bool]:
|
|
115
|
+
"""Convert environment variable string to appropriate type."""
|
|
116
|
+
# Boolean conversion
|
|
117
|
+
if value.lower() in ("true", "yes", "1", "on"):
|
|
118
|
+
return True
|
|
119
|
+
elif value.lower() in ("false", "no", "0", "off"):
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
# Numeric conversion
|
|
123
|
+
try:
|
|
124
|
+
if "." in value:
|
|
125
|
+
return float(value)
|
|
126
|
+
else:
|
|
127
|
+
return int(value)
|
|
128
|
+
except ValueError:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
# Return as string
|
|
132
|
+
return value
|
|
133
|
+
|
|
134
|
+
def _apply_defaults(self) -> None:
|
|
135
|
+
"""Apply default configuration values."""
|
|
136
|
+
# Get CLAUDE_MULTIAGENT_PM_ROOT (new) or CLAUDE_PM_ROOT (backward compatibility)
|
|
137
|
+
claude_multiagent_pm_root = os.getenv("CLAUDE_MULTIAGENT_PM_ROOT")
|
|
138
|
+
claude_pm_root = os.getenv("CLAUDE_PM_ROOT") # Backward compatibility
|
|
139
|
+
|
|
140
|
+
# Prioritize new variable name, fall back to old for compatibility
|
|
141
|
+
project_root = claude_multiagent_pm_root or claude_pm_root
|
|
142
|
+
|
|
143
|
+
if project_root:
|
|
144
|
+
# Use custom root directory
|
|
145
|
+
claude_pm_path = project_root
|
|
146
|
+
base_path = str(Path(project_root).parent)
|
|
147
|
+
managed_path = str(Path(project_root).parent / "managed")
|
|
148
|
+
|
|
149
|
+
# Log which environment variable was used
|
|
150
|
+
if claude_multiagent_pm_root:
|
|
151
|
+
logger.debug("Using CLAUDE_MULTIAGENT_PM_ROOT environment variable")
|
|
152
|
+
else:
|
|
153
|
+
logger.warning(
|
|
154
|
+
"Using deprecated CLAUDE_PM_ROOT environment variable. Please migrate to CLAUDE_MULTIAGENT_PM_ROOT"
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
# Use default paths
|
|
158
|
+
base_path = str(Path.home() / "Projects")
|
|
159
|
+
claude_pm_path = str(Path.home() / "Projects" / "claude-pm")
|
|
160
|
+
managed_path = str(Path.home() / "Projects" / "managed")
|
|
161
|
+
|
|
162
|
+
defaults = {
|
|
163
|
+
# Logging
|
|
164
|
+
"log_level": "INFO",
|
|
165
|
+
"log_format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
166
|
+
# Health monitoring
|
|
167
|
+
"enable_health_monitoring": True,
|
|
168
|
+
"health_check_interval": 30,
|
|
169
|
+
# Metrics
|
|
170
|
+
"enable_metrics": True,
|
|
171
|
+
"metrics_interval": 60,
|
|
172
|
+
# Service management
|
|
173
|
+
"graceful_shutdown_timeout": 30,
|
|
174
|
+
"startup_timeout": 60,
|
|
175
|
+
# ai-trackdown-tools integration
|
|
176
|
+
"use_ai_trackdown_tools": False,
|
|
177
|
+
"ai_trackdown_tools_timeout": 30,
|
|
178
|
+
"ai_trackdown_tools_fallback_logging": True,
|
|
179
|
+
# Claude PM specific - dynamic path resolution
|
|
180
|
+
"base_path": base_path,
|
|
181
|
+
"claude_pm_path": claude_pm_path,
|
|
182
|
+
"managed_path": managed_path,
|
|
183
|
+
# Alerting
|
|
184
|
+
"enable_alerting": True,
|
|
185
|
+
"alert_threshold": 60,
|
|
186
|
+
# Development
|
|
187
|
+
"debug": False,
|
|
188
|
+
"verbose": False,
|
|
189
|
+
# Task and issue tracking
|
|
190
|
+
"enable_persistent_tracking": True,
|
|
191
|
+
"fallback_tracking_method": "logging", # Options: "logging", "file", "disabled"
|
|
192
|
+
# Evaluation system - Phase 2 Mirascope integration
|
|
193
|
+
"enable_evaluation": True,
|
|
194
|
+
"evaluation_storage_path": str(Path.home() / ".claude-pm" / "training"),
|
|
195
|
+
"correction_capture_enabled": True,
|
|
196
|
+
"correction_storage_rotation_days": 30,
|
|
197
|
+
"evaluation_logging_enabled": True,
|
|
198
|
+
"auto_prompt_improvement": False, # Disabled by default for Phase 1
|
|
199
|
+
# Mirascope evaluation settings
|
|
200
|
+
"evaluation_provider": "auto", # auto, openai, anthropic
|
|
201
|
+
"evaluation_criteria": ["correctness", "relevance", "completeness", "clarity", "helpfulness"],
|
|
202
|
+
"evaluation_caching_enabled": True,
|
|
203
|
+
"evaluation_cache_ttl_hours": 24,
|
|
204
|
+
"evaluation_cache_max_size": 1000,
|
|
205
|
+
"evaluation_cache_memory_limit_mb": 100,
|
|
206
|
+
"evaluation_cache_strategy": "hybrid", # lru, ttl, hybrid
|
|
207
|
+
"evaluation_async_enabled": True,
|
|
208
|
+
"evaluation_batch_size": 10,
|
|
209
|
+
"evaluation_max_concurrent": 10,
|
|
210
|
+
"evaluation_timeout_seconds": 30,
|
|
211
|
+
"evaluation_model_config": {},
|
|
212
|
+
# Integration settings
|
|
213
|
+
"auto_evaluate_corrections": True,
|
|
214
|
+
"auto_evaluate_responses": True,
|
|
215
|
+
"batch_evaluation_enabled": True,
|
|
216
|
+
"batch_evaluation_interval_minutes": 5,
|
|
217
|
+
# Performance optimization
|
|
218
|
+
"evaluation_performance_enabled": True,
|
|
219
|
+
"evaluation_batch_wait_ms": 100,
|
|
220
|
+
"evaluation_max_concurrent_batches": 5,
|
|
221
|
+
"evaluation_circuit_breaker_threshold": 5,
|
|
222
|
+
"evaluation_circuit_breaker_timeout": 60,
|
|
223
|
+
"evaluation_circuit_breaker_success_threshold": 3,
|
|
224
|
+
# Metrics and monitoring
|
|
225
|
+
"enable_evaluation_metrics": True,
|
|
226
|
+
"evaluation_monitoring_enabled": True,
|
|
227
|
+
# Additional configuration
|
|
228
|
+
"correction_max_file_size_mb": 10,
|
|
229
|
+
"correction_backup_enabled": True,
|
|
230
|
+
"correction_compression_enabled": True
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# Apply defaults for missing keys
|
|
234
|
+
for key, default_value in defaults.items():
|
|
235
|
+
if key not in self._config:
|
|
236
|
+
self._config[key] = default_value
|
|
237
|
+
|
|
238
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
239
|
+
"""Get configuration value."""
|
|
240
|
+
# Support nested keys with dot notation
|
|
241
|
+
keys = key.split(".")
|
|
242
|
+
value = self._config
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
for k in keys:
|
|
246
|
+
value = value[k]
|
|
247
|
+
return value
|
|
248
|
+
except (KeyError, TypeError):
|
|
249
|
+
return default
|
|
250
|
+
|
|
251
|
+
def set(self, key: str, value: Any) -> None:
|
|
252
|
+
"""Set configuration value."""
|
|
253
|
+
# Support nested keys with dot notation
|
|
254
|
+
keys = key.split(".")
|
|
255
|
+
config = self._config
|
|
256
|
+
|
|
257
|
+
for k in keys[:-1]:
|
|
258
|
+
if k not in config:
|
|
259
|
+
config[k] = {}
|
|
260
|
+
config = config[k]
|
|
261
|
+
|
|
262
|
+
config[keys[-1]] = value
|
|
263
|
+
|
|
264
|
+
def update(self, config: Dict[str, Any]) -> None:
|
|
265
|
+
"""Update configuration with new values."""
|
|
266
|
+
self._config = self._config_mgr.merge_configs(self._config, config)
|
|
267
|
+
|
|
268
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
269
|
+
"""Get configuration as dictionary."""
|
|
270
|
+
return self._config.copy()
|
|
271
|
+
|
|
272
|
+
def save(self, file_path: Union[str, Path], format: str = "json") -> None:
|
|
273
|
+
"""Save configuration to file."""
|
|
274
|
+
file_path = Path(file_path)
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
if format.lower() == "json":
|
|
278
|
+
self._config_mgr.save_json(self._config, file_path)
|
|
279
|
+
elif format.lower() in ["yaml", "yml"]:
|
|
280
|
+
self._config_mgr.save_yaml(self._config, file_path)
|
|
281
|
+
else:
|
|
282
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
283
|
+
|
|
284
|
+
logger.info(f"Configuration saved to {file_path}")
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.error(f"Failed to save configuration to {file_path}: {e}")
|
|
288
|
+
raise
|
|
289
|
+
|
|
290
|
+
def validate(self, schema: Dict[str, Any]) -> bool:
|
|
291
|
+
"""
|
|
292
|
+
Validate configuration against a schema.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
schema: Dictionary defining required keys and types
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
True if valid, False otherwise
|
|
299
|
+
"""
|
|
300
|
+
try:
|
|
301
|
+
for key, expected_type in schema.items():
|
|
302
|
+
if key not in self._config:
|
|
303
|
+
logger.error(f"Missing required configuration key: {key}")
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
value = self.get(key)
|
|
307
|
+
if not isinstance(value, expected_type):
|
|
308
|
+
logger.error(
|
|
309
|
+
f"Configuration key '{key}' has wrong type. "
|
|
310
|
+
f"Expected {expected_type}, got {type(value)}"
|
|
311
|
+
)
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
return True
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.error(f"Configuration validation error: {e}")
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
def __getitem__(self, key: str) -> Any:
|
|
321
|
+
"""Allow dictionary-style access."""
|
|
322
|
+
return self.get(key)
|
|
323
|
+
|
|
324
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
325
|
+
"""Allow dictionary-style assignment."""
|
|
326
|
+
self.set(key, value)
|
|
327
|
+
|
|
328
|
+
def __contains__(self, key: str) -> bool:
|
|
329
|
+
"""Check if configuration contains a key."""
|
|
330
|
+
return self.get(key) is not None
|
|
331
|
+
|
|
332
|
+
def __repr__(self) -> str:
|
|
333
|
+
"""String representation of configuration."""
|
|
334
|
+
return f"<Config({len(self._config)} keys)>"
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration alias management for Claude PM Framework.
|
|
3
|
+
|
|
4
|
+
Manages friendly directory aliases for configuration paths, allowing users to
|
|
5
|
+
reference configurations using memorable names instead of full paths.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
claude-pm --config personal # Resolves to ~/.claude-pm/configs/personal/
|
|
9
|
+
claude-pm --config work # Resolves to ~/work/claude-configs/
|
|
10
|
+
|
|
11
|
+
Aliases are stored in ~/.claude-pm/config_aliases.json
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, Optional, List, Tuple
|
|
19
|
+
|
|
20
|
+
from ..utils.config_manager import ConfigurationManager
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigAliasError(Exception):
|
|
26
|
+
"""Base exception for configuration alias errors."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AliasNotFoundError(ConfigAliasError):
|
|
31
|
+
"""Raised when attempting to resolve a non-existent alias."""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DuplicateAliasError(ConfigAliasError):
|
|
36
|
+
"""Raised when attempting to create an alias that already exists."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class InvalidDirectoryError(ConfigAliasError):
|
|
41
|
+
"""Raised when a directory path is invalid or cannot be created."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConfigAliasManager:
|
|
46
|
+
"""
|
|
47
|
+
Manages configuration directory aliases for the Claude PM Framework.
|
|
48
|
+
|
|
49
|
+
Provides methods to create, delete, list, and resolve aliases that map
|
|
50
|
+
friendly names to actual directory paths.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, aliases_file: Optional[Path] = None):
|
|
54
|
+
"""
|
|
55
|
+
Initialize the configuration alias manager.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
aliases_file: Path to the aliases JSON file. Defaults to
|
|
59
|
+
~/.claude-pm/config_aliases.json
|
|
60
|
+
"""
|
|
61
|
+
if aliases_file is None:
|
|
62
|
+
self.aliases_file = Path.home() / ".claude-pm" / "config_aliases.json"
|
|
63
|
+
else:
|
|
64
|
+
self.aliases_file = Path(aliases_file)
|
|
65
|
+
|
|
66
|
+
self.config_mgr = ConfigurationManager(cache_enabled=True)
|
|
67
|
+
self._ensure_aliases_file()
|
|
68
|
+
self._aliases: Dict[str, str] = self._load_aliases()
|
|
69
|
+
|
|
70
|
+
def _ensure_aliases_file(self) -> None:
|
|
71
|
+
"""Ensure the aliases file and its parent directory exist."""
|
|
72
|
+
self.aliases_file.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
if not self.aliases_file.exists():
|
|
74
|
+
self._save_aliases({})
|
|
75
|
+
logger.info(f"Created new aliases file at {self.aliases_file}")
|
|
76
|
+
|
|
77
|
+
def _load_aliases(self) -> Dict[str, str]:
|
|
78
|
+
"""Load aliases from the JSON file."""
|
|
79
|
+
try:
|
|
80
|
+
return self.config_mgr.load_json(self.aliases_file)
|
|
81
|
+
except (json.JSONDecodeError, Exception) as e:
|
|
82
|
+
logger.error(f"Failed to load aliases: {e}")
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
def _save_aliases(self, aliases: Dict[str, str]) -> None:
|
|
86
|
+
"""Save aliases to the JSON file."""
|
|
87
|
+
try:
|
|
88
|
+
self.config_mgr.save_json(aliases, self.aliases_file, sort_keys=True)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Failed to save aliases: {e}")
|
|
91
|
+
raise ConfigAliasError(f"Failed to save aliases: {e}")
|
|
92
|
+
|
|
93
|
+
def create_alias(self, alias_name: str, directory_path: str) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Create a new configuration alias.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
alias_name: The friendly name for the alias
|
|
99
|
+
directory_path: The directory path this alias should resolve to
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
DuplicateAliasError: If the alias already exists
|
|
103
|
+
InvalidDirectoryError: If the directory path is invalid
|
|
104
|
+
"""
|
|
105
|
+
# Validate alias name
|
|
106
|
+
if not alias_name or not alias_name.strip():
|
|
107
|
+
raise ValueError("Alias name cannot be empty")
|
|
108
|
+
|
|
109
|
+
alias_name = alias_name.strip()
|
|
110
|
+
|
|
111
|
+
# Check for duplicate
|
|
112
|
+
if alias_name in self._aliases:
|
|
113
|
+
raise DuplicateAliasError(
|
|
114
|
+
f"Alias '{alias_name}' already exists, pointing to: {self._aliases[alias_name]}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Validate and normalize the directory path
|
|
118
|
+
validated_path = self.validate_directory(directory_path)
|
|
119
|
+
|
|
120
|
+
# Add the alias
|
|
121
|
+
self._aliases[alias_name] = str(validated_path)
|
|
122
|
+
self._save_aliases(self._aliases)
|
|
123
|
+
|
|
124
|
+
logger.info(f"Created alias '{alias_name}' -> {validated_path}")
|
|
125
|
+
|
|
126
|
+
def resolve_alias(self, alias_name: str) -> Path:
|
|
127
|
+
"""
|
|
128
|
+
Resolve an alias to its directory path.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
alias_name: The alias to resolve
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The Path object for the resolved directory
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
AliasNotFoundError: If the alias does not exist
|
|
138
|
+
"""
|
|
139
|
+
if alias_name not in self._aliases:
|
|
140
|
+
raise AliasNotFoundError(f"Alias '{alias_name}' not found")
|
|
141
|
+
|
|
142
|
+
path = Path(self._aliases[alias_name])
|
|
143
|
+
|
|
144
|
+
# Ensure the directory still exists or can be created
|
|
145
|
+
try:
|
|
146
|
+
path = self.validate_directory(str(path))
|
|
147
|
+
except InvalidDirectoryError:
|
|
148
|
+
logger.warning(f"Directory for alias '{alias_name}' no longer valid: {path}")
|
|
149
|
+
# Still return the path, let the caller handle the missing directory
|
|
150
|
+
|
|
151
|
+
return path
|
|
152
|
+
|
|
153
|
+
def list_aliases(self) -> List[Tuple[str, str]]:
|
|
154
|
+
"""
|
|
155
|
+
List all configured aliases.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
A list of tuples containing (alias_name, directory_path)
|
|
159
|
+
"""
|
|
160
|
+
return sorted(self._aliases.items())
|
|
161
|
+
|
|
162
|
+
def delete_alias(self, alias_name: str) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Delete a configuration alias.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
alias_name: The alias to delete
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
AliasNotFoundError: If the alias does not exist
|
|
171
|
+
"""
|
|
172
|
+
if alias_name not in self._aliases:
|
|
173
|
+
raise AliasNotFoundError(f"Alias '{alias_name}' not found")
|
|
174
|
+
|
|
175
|
+
directory_path = self._aliases[alias_name]
|
|
176
|
+
del self._aliases[alias_name]
|
|
177
|
+
self._save_aliases(self._aliases)
|
|
178
|
+
|
|
179
|
+
logger.info(f"Deleted alias '{alias_name}' (was pointing to: {directory_path})")
|
|
180
|
+
|
|
181
|
+
def validate_directory(self, path: str) -> Path:
|
|
182
|
+
"""
|
|
183
|
+
Validate that a directory exists or can be created.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
path: The directory path to validate
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The normalized Path object
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
InvalidDirectoryError: If the path is invalid or cannot be created
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
# Expand user home directory and environment variables
|
|
196
|
+
expanded_path = os.path.expanduser(os.path.expandvars(path))
|
|
197
|
+
directory_path = Path(expanded_path).resolve()
|
|
198
|
+
|
|
199
|
+
# Check if it's a file
|
|
200
|
+
if directory_path.exists() and directory_path.is_file():
|
|
201
|
+
raise InvalidDirectoryError(
|
|
202
|
+
f"Path exists but is a file, not a directory: {directory_path}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Try to create the directory if it doesn't exist
|
|
206
|
+
if not directory_path.exists():
|
|
207
|
+
try:
|
|
208
|
+
directory_path.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
logger.debug(f"Created directory: {directory_path}")
|
|
210
|
+
except Exception as e:
|
|
211
|
+
raise InvalidDirectoryError(
|
|
212
|
+
f"Cannot create directory '{directory_path}': {e}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Verify we can write to the directory
|
|
216
|
+
test_file = directory_path / ".claude_pm_test"
|
|
217
|
+
try:
|
|
218
|
+
test_file.touch()
|
|
219
|
+
test_file.unlink()
|
|
220
|
+
except Exception as e:
|
|
221
|
+
raise InvalidDirectoryError(
|
|
222
|
+
f"Directory '{directory_path}' is not writable: {e}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return directory_path
|
|
226
|
+
|
|
227
|
+
except InvalidDirectoryError:
|
|
228
|
+
raise
|
|
229
|
+
except Exception as e:
|
|
230
|
+
raise InvalidDirectoryError(f"Invalid directory path '{path}': {e}")
|
|
231
|
+
|
|
232
|
+
def get_alias(self, alias_name: str) -> Optional[str]:
|
|
233
|
+
"""
|
|
234
|
+
Get the directory path for an alias without validation.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
alias_name: The alias to look up
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
The directory path string if the alias exists, None otherwise
|
|
241
|
+
"""
|
|
242
|
+
return self._aliases.get(alias_name)
|
|
243
|
+
|
|
244
|
+
def update_alias(self, alias_name: str, new_directory_path: str) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Update an existing alias to point to a new directory.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
alias_name: The alias to update
|
|
250
|
+
new_directory_path: The new directory path
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
AliasNotFoundError: If the alias does not exist
|
|
254
|
+
InvalidDirectoryError: If the new directory path is invalid
|
|
255
|
+
"""
|
|
256
|
+
if alias_name not in self._aliases:
|
|
257
|
+
raise AliasNotFoundError(f"Alias '{alias_name}' not found")
|
|
258
|
+
|
|
259
|
+
# Validate the new directory path
|
|
260
|
+
validated_path = self.validate_directory(new_directory_path)
|
|
261
|
+
|
|
262
|
+
old_path = self._aliases[alias_name]
|
|
263
|
+
self._aliases[alias_name] = str(validated_path)
|
|
264
|
+
self._save_aliases(self._aliases)
|
|
265
|
+
|
|
266
|
+
logger.info(f"Updated alias '{alias_name}': {old_path} -> {validated_path}")
|
|
267
|
+
|
|
268
|
+
def alias_exists(self, alias_name: str) -> bool:
|
|
269
|
+
"""
|
|
270
|
+
Check if an alias exists.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
alias_name: The alias to check
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
True if the alias exists, False otherwise
|
|
277
|
+
"""
|
|
278
|
+
return alias_name in self._aliases
|
|
279
|
+
|
|
280
|
+
def get_all_aliases(self) -> Dict[str, str]:
|
|
281
|
+
"""
|
|
282
|
+
Get a copy of all aliases.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
A dictionary mapping alias names to directory paths
|
|
286
|
+
"""
|
|
287
|
+
return self._aliases.copy()
|
|
288
|
+
|
|
289
|
+
def reload_aliases(self) -> None:
|
|
290
|
+
"""Reload aliases from the file, discarding any in-memory changes."""
|
|
291
|
+
self._aliases = self._load_aliases()
|
|
292
|
+
logger.debug("Reloaded aliases from file")
|