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.

Files changed (159) hide show
  1. claude_mpm/__init__.py +17 -0
  2. claude_mpm/__main__.py +14 -0
  3. claude_mpm/_version.py +32 -0
  4. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
  5. claude_mpm/agents/INSTRUCTIONS.md +375 -0
  6. claude_mpm/agents/__init__.py +118 -0
  7. claude_mpm/agents/agent_loader.py +621 -0
  8. claude_mpm/agents/agent_loader_integration.py +229 -0
  9. claude_mpm/agents/agents_metadata.py +204 -0
  10. claude_mpm/agents/base_agent.json +27 -0
  11. claude_mpm/agents/base_agent_loader.py +519 -0
  12. claude_mpm/agents/schema/agent_schema.json +160 -0
  13. claude_mpm/agents/system_agent_config.py +587 -0
  14. claude_mpm/agents/templates/__init__.py +101 -0
  15. claude_mpm/agents/templates/data_engineer_agent.json +46 -0
  16. claude_mpm/agents/templates/documentation_agent.json +45 -0
  17. claude_mpm/agents/templates/engineer_agent.json +49 -0
  18. claude_mpm/agents/templates/ops_agent.json +46 -0
  19. claude_mpm/agents/templates/qa_agent.json +45 -0
  20. claude_mpm/agents/templates/research_agent.json +49 -0
  21. claude_mpm/agents/templates/security_agent.json +46 -0
  22. claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
  23. claude_mpm/agents/templates/version_control_agent.json +46 -0
  24. claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
  25. claude_mpm/cli.py +655 -0
  26. claude_mpm/cli_main.py +13 -0
  27. claude_mpm/cli_module/__init__.py +15 -0
  28. claude_mpm/cli_module/args.py +222 -0
  29. claude_mpm/cli_module/commands.py +203 -0
  30. claude_mpm/cli_module/migration_example.py +183 -0
  31. claude_mpm/cli_module/refactoring_guide.md +253 -0
  32. claude_mpm/cli_old/__init__.py +1 -0
  33. claude_mpm/cli_old/ticket_cli.py +102 -0
  34. claude_mpm/config/__init__.py +5 -0
  35. claude_mpm/config/hook_config.py +42 -0
  36. claude_mpm/constants.py +150 -0
  37. claude_mpm/core/__init__.py +45 -0
  38. claude_mpm/core/agent_name_normalizer.py +248 -0
  39. claude_mpm/core/agent_registry.py +627 -0
  40. claude_mpm/core/agent_registry.py.bak +312 -0
  41. claude_mpm/core/agent_session_manager.py +273 -0
  42. claude_mpm/core/base_service.py +747 -0
  43. claude_mpm/core/base_service.py.bak +406 -0
  44. claude_mpm/core/config.py +334 -0
  45. claude_mpm/core/config_aliases.py +292 -0
  46. claude_mpm/core/container.py +347 -0
  47. claude_mpm/core/factories.py +281 -0
  48. claude_mpm/core/framework_loader.py +472 -0
  49. claude_mpm/core/injectable_service.py +206 -0
  50. claude_mpm/core/interfaces.py +539 -0
  51. claude_mpm/core/logger.py +468 -0
  52. claude_mpm/core/minimal_framework_loader.py +107 -0
  53. claude_mpm/core/mixins.py +150 -0
  54. claude_mpm/core/service_registry.py +299 -0
  55. claude_mpm/core/session_manager.py +190 -0
  56. claude_mpm/core/simple_runner.py +511 -0
  57. claude_mpm/core/tool_access_control.py +173 -0
  58. claude_mpm/hooks/README.md +243 -0
  59. claude_mpm/hooks/__init__.py +5 -0
  60. claude_mpm/hooks/base_hook.py +154 -0
  61. claude_mpm/hooks/builtin/__init__.py +1 -0
  62. claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
  63. claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
  64. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
  65. claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
  66. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
  67. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
  68. claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
  69. claude_mpm/hooks/hook_client.py +264 -0
  70. claude_mpm/hooks/hook_runner.py +370 -0
  71. claude_mpm/hooks/json_rpc_executor.py +259 -0
  72. claude_mpm/hooks/json_rpc_hook_client.py +319 -0
  73. claude_mpm/hooks/tool_call_interceptor.py +204 -0
  74. claude_mpm/init.py +246 -0
  75. claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
  76. claude_mpm/orchestration/__init__.py +6 -0
  77. claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
  78. claude_mpm/orchestration/archive/factory.py +215 -0
  79. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
  80. claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
  81. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
  82. claude_mpm/orchestration/archive/orchestrator.py +501 -0
  83. claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
  84. claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
  85. claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
  86. claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
  87. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
  88. claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
  89. claude_mpm/scripts/__init__.py +1 -0
  90. claude_mpm/scripts/ticket.py +269 -0
  91. claude_mpm/services/__init__.py +10 -0
  92. claude_mpm/services/agent_deployment.py +955 -0
  93. claude_mpm/services/agent_lifecycle_manager.py +948 -0
  94. claude_mpm/services/agent_management_service.py +596 -0
  95. claude_mpm/services/agent_modification_tracker.py +841 -0
  96. claude_mpm/services/agent_profile_loader.py +606 -0
  97. claude_mpm/services/agent_registry.py +677 -0
  98. claude_mpm/services/base_agent_manager.py +380 -0
  99. claude_mpm/services/framework_agent_loader.py +337 -0
  100. claude_mpm/services/framework_claude_md_generator/README.md +92 -0
  101. claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
  102. claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
  103. claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
  104. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
  105. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
  106. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
  107. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
  108. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
  109. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
  110. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
  111. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
  112. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
  113. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
  114. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
  115. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
  116. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
  117. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
  118. claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
  119. claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
  120. claude_mpm/services/framework_claude_md_generator.py +621 -0
  121. claude_mpm/services/hook_service.py +388 -0
  122. claude_mpm/services/hook_service_manager.py +223 -0
  123. claude_mpm/services/json_rpc_hook_manager.py +92 -0
  124. claude_mpm/services/parent_directory_manager/README.md +83 -0
  125. claude_mpm/services/parent_directory_manager/__init__.py +577 -0
  126. claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
  127. claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
  128. claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
  129. claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
  130. claude_mpm/services/parent_directory_manager/operations.py +186 -0
  131. claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
  132. claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
  133. claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
  134. claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
  135. claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
  136. claude_mpm/services/shared_prompt_cache.py +819 -0
  137. claude_mpm/services/ticket_manager.py +213 -0
  138. claude_mpm/services/ticket_manager_di.py +318 -0
  139. claude_mpm/services/ticketing_service_original.py +508 -0
  140. claude_mpm/services/version_control/VERSION +1 -0
  141. claude_mpm/services/version_control/__init__.py +70 -0
  142. claude_mpm/services/version_control/branch_strategy.py +670 -0
  143. claude_mpm/services/version_control/conflict_resolution.py +744 -0
  144. claude_mpm/services/version_control/git_operations.py +784 -0
  145. claude_mpm/services/version_control/semantic_versioning.py +703 -0
  146. claude_mpm/ui/__init__.py +1 -0
  147. claude_mpm/ui/rich_terminal_ui.py +295 -0
  148. claude_mpm/ui/terminal_ui.py +328 -0
  149. claude_mpm/utils/__init__.py +16 -0
  150. claude_mpm/utils/config_manager.py +468 -0
  151. claude_mpm/utils/import_migration_example.py +80 -0
  152. claude_mpm/utils/imports.py +182 -0
  153. claude_mpm/utils/path_operations.py +357 -0
  154. claude_mpm/utils/paths.py +289 -0
  155. claude_mpm-0.3.0.dist-info/METADATA +290 -0
  156. claude_mpm-0.3.0.dist-info/RECORD +159 -0
  157. claude_mpm-0.3.0.dist-info/WHEEL +5 -0
  158. claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
  159. 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")