claude-mpm 4.13.2__py3-none-any.whl → 4.18.2__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 (250) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_ENGINEER.md +286 -0
  3. claude_mpm/agents/BASE_PM.md +48 -17
  4. claude_mpm/agents/OUTPUT_STYLE.md +329 -11
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +227 -8
  6. claude_mpm/agents/agent_loader.py +17 -5
  7. claude_mpm/agents/frontmatter_validator.py +284 -253
  8. claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
  9. claude_mpm/agents/templates/api_qa.json +7 -1
  10. claude_mpm/agents/templates/clerk-ops.json +8 -1
  11. claude_mpm/agents/templates/code_analyzer.json +4 -1
  12. claude_mpm/agents/templates/dart_engineer.json +11 -1
  13. claude_mpm/agents/templates/data_engineer.json +11 -1
  14. claude_mpm/agents/templates/documentation.json +6 -1
  15. claude_mpm/agents/templates/engineer.json +18 -1
  16. claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
  17. claude_mpm/agents/templates/golang_engineer.json +11 -1
  18. claude_mpm/agents/templates/java_engineer.json +12 -2
  19. claude_mpm/agents/templates/local_ops_agent.json +1217 -6
  20. claude_mpm/agents/templates/nextjs_engineer.json +11 -1
  21. claude_mpm/agents/templates/ops.json +8 -1
  22. claude_mpm/agents/templates/php-engineer.json +11 -1
  23. claude_mpm/agents/templates/project_organizer.json +10 -3
  24. claude_mpm/agents/templates/prompt-engineer.json +5 -1
  25. claude_mpm/agents/templates/python_engineer.json +11 -1
  26. claude_mpm/agents/templates/qa.json +7 -1
  27. claude_mpm/agents/templates/react_engineer.json +11 -1
  28. claude_mpm/agents/templates/refactoring_engineer.json +8 -1
  29. claude_mpm/agents/templates/research.json +4 -1
  30. claude_mpm/agents/templates/ruby-engineer.json +11 -1
  31. claude_mpm/agents/templates/rust_engineer.json +11 -1
  32. claude_mpm/agents/templates/security.json +6 -1
  33. claude_mpm/agents/templates/svelte-engineer.json +225 -0
  34. claude_mpm/agents/templates/ticketing.json +6 -1
  35. claude_mpm/agents/templates/typescript_engineer.json +11 -1
  36. claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
  37. claude_mpm/agents/templates/version_control.json +8 -1
  38. claude_mpm/agents/templates/web_qa.json +7 -1
  39. claude_mpm/agents/templates/web_ui.json +11 -1
  40. claude_mpm/cli/__init__.py +34 -706
  41. claude_mpm/cli/commands/agent_manager.py +25 -12
  42. claude_mpm/cli/commands/agent_state_manager.py +186 -0
  43. claude_mpm/cli/commands/agents.py +204 -148
  44. claude_mpm/cli/commands/aggregate.py +7 -3
  45. claude_mpm/cli/commands/analyze.py +9 -4
  46. claude_mpm/cli/commands/analyze_code.py +7 -2
  47. claude_mpm/cli/commands/auto_configure.py +7 -9
  48. claude_mpm/cli/commands/config.py +47 -13
  49. claude_mpm/cli/commands/configure.py +294 -1788
  50. claude_mpm/cli/commands/configure_agent_display.py +261 -0
  51. claude_mpm/cli/commands/configure_behavior_manager.py +204 -0
  52. claude_mpm/cli/commands/configure_hook_manager.py +225 -0
  53. claude_mpm/cli/commands/configure_models.py +18 -0
  54. claude_mpm/cli/commands/configure_navigation.py +167 -0
  55. claude_mpm/cli/commands/configure_paths.py +104 -0
  56. claude_mpm/cli/commands/configure_persistence.py +254 -0
  57. claude_mpm/cli/commands/configure_startup_manager.py +646 -0
  58. claude_mpm/cli/commands/configure_template_editor.py +497 -0
  59. claude_mpm/cli/commands/configure_validators.py +73 -0
  60. claude_mpm/cli/commands/local_deploy.py +537 -0
  61. claude_mpm/cli/commands/memory.py +54 -20
  62. claude_mpm/cli/commands/mpm_init.py +39 -25
  63. claude_mpm/cli/commands/mpm_init_handler.py +8 -3
  64. claude_mpm/cli/executor.py +202 -0
  65. claude_mpm/cli/helpers.py +105 -0
  66. claude_mpm/cli/interactive/__init__.py +3 -0
  67. claude_mpm/cli/interactive/skills_wizard.py +491 -0
  68. claude_mpm/cli/parsers/__init__.py +7 -1
  69. claude_mpm/cli/parsers/base_parser.py +98 -3
  70. claude_mpm/cli/parsers/local_deploy_parser.py +227 -0
  71. claude_mpm/cli/shared/output_formatters.py +28 -19
  72. claude_mpm/cli/startup.py +481 -0
  73. claude_mpm/cli/utils.py +52 -1
  74. claude_mpm/commands/mpm-help.md +3 -0
  75. claude_mpm/commands/mpm-version.md +113 -0
  76. claude_mpm/commands/mpm.md +1 -0
  77. claude_mpm/config/agent_config.py +2 -2
  78. claude_mpm/config/model_config.py +428 -0
  79. claude_mpm/core/base_service.py +13 -12
  80. claude_mpm/core/enums.py +452 -0
  81. claude_mpm/core/factories.py +1 -1
  82. claude_mpm/core/instruction_reinforcement_hook.py +2 -1
  83. claude_mpm/core/interactive_session.py +9 -3
  84. claude_mpm/core/logging_config.py +6 -2
  85. claude_mpm/core/oneshot_session.py +8 -4
  86. claude_mpm/core/optimized_agent_loader.py +3 -3
  87. claude_mpm/core/output_style_manager.py +12 -192
  88. claude_mpm/core/service_registry.py +5 -1
  89. claude_mpm/core/types.py +2 -9
  90. claude_mpm/core/typing_utils.py +7 -6
  91. claude_mpm/dashboard/static/js/dashboard.js +0 -14
  92. claude_mpm/dashboard/templates/index.html +3 -41
  93. claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
  94. claude_mpm/hooks/instruction_reinforcement.py +7 -2
  95. claude_mpm/models/resume_log.py +340 -0
  96. claude_mpm/services/agents/auto_config_manager.py +10 -11
  97. claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
  98. claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
  99. claude_mpm/services/agents/deployment/agent_validator.py +17 -1
  100. claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
  101. claude_mpm/services/agents/deployment/interface_adapter.py +3 -2
  102. claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
  103. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +7 -6
  104. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +7 -16
  105. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +4 -3
  106. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +5 -3
  107. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +6 -5
  108. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +9 -6
  109. claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
  110. claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
  111. claude_mpm/services/agents/local_template_manager.py +1 -1
  112. claude_mpm/services/agents/memory/agent_memory_manager.py +5 -2
  113. claude_mpm/services/agents/registry/modification_tracker.py +5 -2
  114. claude_mpm/services/command_handler_service.py +11 -5
  115. claude_mpm/services/core/interfaces/__init__.py +74 -2
  116. claude_mpm/services/core/interfaces/health.py +172 -0
  117. claude_mpm/services/core/interfaces/model.py +281 -0
  118. claude_mpm/services/core/interfaces/process.py +372 -0
  119. claude_mpm/services/core/interfaces/restart.py +307 -0
  120. claude_mpm/services/core/interfaces/stability.py +260 -0
  121. claude_mpm/services/core/models/__init__.py +33 -0
  122. claude_mpm/services/core/models/agent_config.py +12 -28
  123. claude_mpm/services/core/models/health.py +162 -0
  124. claude_mpm/services/core/models/process.py +235 -0
  125. claude_mpm/services/core/models/restart.py +302 -0
  126. claude_mpm/services/core/models/stability.py +264 -0
  127. claude_mpm/services/core/path_resolver.py +23 -7
  128. claude_mpm/services/diagnostics/__init__.py +2 -2
  129. claude_mpm/services/diagnostics/checks/agent_check.py +25 -24
  130. claude_mpm/services/diagnostics/checks/claude_code_check.py +24 -23
  131. claude_mpm/services/diagnostics/checks/common_issues_check.py +25 -24
  132. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -23
  133. claude_mpm/services/diagnostics/checks/filesystem_check.py +18 -17
  134. claude_mpm/services/diagnostics/checks/installation_check.py +30 -29
  135. claude_mpm/services/diagnostics/checks/instructions_check.py +20 -19
  136. claude_mpm/services/diagnostics/checks/mcp_check.py +50 -36
  137. claude_mpm/services/diagnostics/checks/mcp_services_check.py +36 -31
  138. claude_mpm/services/diagnostics/checks/monitor_check.py +23 -22
  139. claude_mpm/services/diagnostics/checks/startup_log_check.py +9 -8
  140. claude_mpm/services/diagnostics/diagnostic_runner.py +6 -5
  141. claude_mpm/services/diagnostics/doctor_reporter.py +28 -25
  142. claude_mpm/services/diagnostics/models.py +19 -24
  143. claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
  144. claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
  145. claude_mpm/services/infrastructure/monitoring/base.py +5 -13
  146. claude_mpm/services/infrastructure/monitoring/network.py +7 -6
  147. claude_mpm/services/infrastructure/monitoring/process.py +13 -12
  148. claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
  149. claude_mpm/services/infrastructure/monitoring/service.py +16 -15
  150. claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
  151. claude_mpm/services/local_ops/__init__.py +163 -0
  152. claude_mpm/services/local_ops/crash_detector.py +257 -0
  153. claude_mpm/services/local_ops/health_checks/__init__.py +28 -0
  154. claude_mpm/services/local_ops/health_checks/http_check.py +224 -0
  155. claude_mpm/services/local_ops/health_checks/process_check.py +236 -0
  156. claude_mpm/services/local_ops/health_checks/resource_check.py +255 -0
  157. claude_mpm/services/local_ops/health_manager.py +430 -0
  158. claude_mpm/services/local_ops/log_monitor.py +396 -0
  159. claude_mpm/services/local_ops/memory_leak_detector.py +294 -0
  160. claude_mpm/services/local_ops/process_manager.py +595 -0
  161. claude_mpm/services/local_ops/resource_monitor.py +331 -0
  162. claude_mpm/services/local_ops/restart_manager.py +401 -0
  163. claude_mpm/services/local_ops/restart_policy.py +387 -0
  164. claude_mpm/services/local_ops/state_manager.py +372 -0
  165. claude_mpm/services/local_ops/unified_manager.py +600 -0
  166. claude_mpm/services/mcp_config_manager.py +9 -4
  167. claude_mpm/services/mcp_gateway/core/__init__.py +1 -2
  168. claude_mpm/services/mcp_gateway/core/base.py +18 -31
  169. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +71 -24
  170. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +30 -28
  171. claude_mpm/services/memory_hook_service.py +4 -1
  172. claude_mpm/services/model/__init__.py +147 -0
  173. claude_mpm/services/model/base_provider.py +365 -0
  174. claude_mpm/services/model/claude_provider.py +412 -0
  175. claude_mpm/services/model/model_router.py +453 -0
  176. claude_mpm/services/model/ollama_provider.py +415 -0
  177. claude_mpm/services/monitor/daemon_manager.py +3 -2
  178. claude_mpm/services/monitor/handlers/dashboard.py +2 -1
  179. claude_mpm/services/monitor/handlers/hooks.py +2 -1
  180. claude_mpm/services/monitor/management/lifecycle.py +3 -2
  181. claude_mpm/services/monitor/server.py +2 -1
  182. claude_mpm/services/session_management_service.py +3 -2
  183. claude_mpm/services/session_manager.py +205 -1
  184. claude_mpm/services/shared/async_service_base.py +16 -27
  185. claude_mpm/services/shared/lifecycle_service_base.py +1 -14
  186. claude_mpm/services/socketio/handlers/__init__.py +5 -2
  187. claude_mpm/services/socketio/handlers/hook.py +13 -2
  188. claude_mpm/services/socketio/handlers/registry.py +4 -2
  189. claude_mpm/services/socketio/server/main.py +10 -8
  190. claude_mpm/services/subprocess_launcher_service.py +14 -5
  191. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +8 -7
  192. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +6 -5
  193. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +8 -7
  194. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +7 -6
  195. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +5 -4
  196. claude_mpm/services/unified/config_strategies/validation_strategy.py +13 -9
  197. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +10 -3
  198. claude_mpm/services/unified/deployment_strategies/local.py +6 -5
  199. claude_mpm/services/unified/deployment_strategies/utils.py +6 -5
  200. claude_mpm/services/unified/deployment_strategies/vercel.py +7 -6
  201. claude_mpm/services/unified/interfaces.py +3 -1
  202. claude_mpm/services/unified/unified_analyzer.py +14 -10
  203. claude_mpm/services/unified/unified_config.py +2 -1
  204. claude_mpm/services/unified/unified_deployment.py +9 -4
  205. claude_mpm/services/version_service.py +104 -1
  206. claude_mpm/skills/__init__.py +21 -0
  207. claude_mpm/skills/bundled/__init__.py +6 -0
  208. claude_mpm/skills/bundled/api-documentation.md +393 -0
  209. claude_mpm/skills/bundled/async-testing.md +571 -0
  210. claude_mpm/skills/bundled/code-review.md +143 -0
  211. claude_mpm/skills/bundled/database-migration.md +199 -0
  212. claude_mpm/skills/bundled/docker-containerization.md +194 -0
  213. claude_mpm/skills/bundled/express-local-dev.md +1429 -0
  214. claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
  215. claude_mpm/skills/bundled/git-workflow.md +414 -0
  216. claude_mpm/skills/bundled/imagemagick.md +204 -0
  217. claude_mpm/skills/bundled/json-data-handling.md +223 -0
  218. claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
  219. claude_mpm/skills/bundled/pdf.md +141 -0
  220. claude_mpm/skills/bundled/performance-profiling.md +567 -0
  221. claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
  222. claude_mpm/skills/bundled/security-scanning.md +327 -0
  223. claude_mpm/skills/bundled/systematic-debugging.md +473 -0
  224. claude_mpm/skills/bundled/test-driven-development.md +378 -0
  225. claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
  226. claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
  227. claude_mpm/skills/bundled/xlsx.md +157 -0
  228. claude_mpm/skills/registry.py +286 -0
  229. claude_mpm/skills/skill_manager.py +310 -0
  230. claude_mpm/tools/code_tree_analyzer.py +177 -141
  231. claude_mpm/tools/code_tree_events.py +4 -2
  232. claude_mpm/utils/agent_dependency_loader.py +2 -2
  233. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/METADATA +117 -8
  234. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/RECORD +238 -174
  235. claude_mpm/dashboard/static/css/code-tree.css +0 -1639
  236. claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
  237. claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
  238. claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
  239. claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
  240. claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
  241. claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
  242. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +0 -425
  243. claude_mpm/hooks/claude_hooks/hook_handler_original.py +0 -1041
  244. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +0 -347
  245. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +0 -575
  246. claude_mpm/services/project/analyzer_refactored.py +0 -450
  247. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/WHEEL +0 -0
  248. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/entry_points.txt +0 -0
  249. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/licenses/LICENSE +0 -0
  250. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/top_level.txt +0 -0
@@ -12,196 +12,30 @@ DESIGN DECISIONS:
12
12
  """
13
13
 
14
14
  import json
15
- import os
16
- import sys
17
15
  from pathlib import Path
18
16
  from typing import Dict, List, Optional
19
17
 
20
- from rich.box import ROUNDED
21
- from rich.columns import Columns
22
18
  from rich.console import Console
23
- from rich.panel import Panel
24
19
  from rich.prompt import Confirm, Prompt
25
- from rich.syntax import Syntax
26
- from rich.table import Table
27
20
  from rich.text import Text
28
21
 
29
22
  from ...core.config import Config
30
- from ...services.mcp_config_manager import MCPConfigManager
31
23
  from ...services.version_service import VersionService
32
24
  from ...utils.console import console as default_console
33
25
  from ..shared import BaseCommand, CommandResult
34
-
35
-
36
- class AgentConfig:
37
- """Simple agent configuration model."""
38
-
39
- def __init__(
40
- self, name: str, description: str = "", dependencies: Optional[List[str]] = None
41
- ):
42
- self.name = name
43
- self.description = description
44
- self.dependencies = dependencies or []
45
-
46
-
47
- class SimpleAgentManager:
48
- """Simple agent state management that discovers real agents from templates."""
49
-
50
- def __init__(self, config_dir: Path):
51
- self.config_dir = config_dir
52
- self.config_file = config_dir / "agent_states.json"
53
- self.config_dir.mkdir(parents=True, exist_ok=True)
54
- self._load_states()
55
- # Path to agent templates directory
56
- self.templates_dir = (
57
- Path(__file__).parent.parent.parent / "agents" / "templates"
58
- )
59
- # Add logger for error reporting
60
- import logging
61
-
62
- self.logger = logging.getLogger(__name__)
63
- # Track pending changes for batch operations
64
- self.deferred_changes: Dict[str, bool] = {}
65
-
66
- def _load_states(self):
67
- """Load agent states from file."""
68
- if self.config_file.exists():
69
- with self.config_file.open() as f:
70
- self.states = json.load(f)
71
- else:
72
- self.states = {}
73
-
74
- def _save_states(self):
75
- """Save agent states to file."""
76
- with self.config_file.open("w") as f:
77
- json.dump(self.states, f, indent=2)
78
-
79
- def is_agent_enabled(self, agent_name: str) -> bool:
80
- """Check if an agent is enabled."""
81
- return self.states.get(agent_name, {}).get("enabled", True)
82
-
83
- def set_agent_enabled(self, agent_name: str, enabled: bool):
84
- """Set agent enabled state."""
85
- if agent_name not in self.states:
86
- self.states[agent_name] = {}
87
- self.states[agent_name]["enabled"] = enabled
88
- self._save_states()
89
-
90
- def set_agent_enabled_deferred(self, agent_name: str, enabled: bool) -> None:
91
- """Queue agent state change without saving."""
92
- self.deferred_changes[agent_name] = enabled
93
-
94
- def commit_deferred_changes(self) -> None:
95
- """Save all deferred changes at once."""
96
- for agent_name, enabled in self.deferred_changes.items():
97
- if agent_name not in self.states:
98
- self.states[agent_name] = {}
99
- self.states[agent_name]["enabled"] = enabled
100
- self._save_states()
101
- self.deferred_changes.clear()
102
-
103
- def discard_deferred_changes(self) -> None:
104
- """Discard all pending changes."""
105
- self.deferred_changes.clear()
106
-
107
- def get_pending_state(self, agent_name: str) -> bool:
108
- """Get agent state including pending changes."""
109
- if agent_name in self.deferred_changes:
110
- return self.deferred_changes[agent_name]
111
- return self.states.get(agent_name, {}).get("enabled", True)
112
-
113
- def has_pending_changes(self) -> bool:
114
- """Check if there are unsaved changes."""
115
- return len(self.deferred_changes) > 0
116
-
117
- def discover_agents(self) -> List[AgentConfig]:
118
- """Discover available agents from template JSON files."""
119
- agents = []
120
-
121
- # Scan templates directory for JSON files
122
- if not self.templates_dir.exists():
123
- # Fallback to a minimal set if templates dir doesn't exist
124
- return [
125
- AgentConfig("engineer", "Engineering agent (templates not found)", []),
126
- AgentConfig("research", "Research agent (templates not found)", []),
127
- ]
128
-
129
- try:
130
- # Read all JSON template files
131
- for template_file in sorted(self.templates_dir.glob("*.json")):
132
- # Skip backup files
133
- if "backup" in template_file.name.lower():
134
- continue
135
-
136
- try:
137
- with template_file.open() as f:
138
- template_data = json.load(f)
139
-
140
- # Extract agent information from template
141
- agent_id = template_data.get("agent_id", template_file.stem)
142
-
143
- # Get metadata for display info
144
- metadata = template_data.get("metadata", {})
145
- metadata.get("name", agent_id)
146
- description = metadata.get(
147
- "description", "No description available"
148
- )
149
-
150
- # Extract capabilities/tools as dependencies for display
151
- capabilities = template_data.get("capabilities", {})
152
- tools = capabilities.get("tools", [])
153
- # Ensure tools is a list before slicing
154
- if not isinstance(tools, list):
155
- tools = []
156
- # Show first few tools as "dependencies" for UI purposes
157
- display_tools = tools[:3] if len(tools) > 3 else tools
158
-
159
- # Normalize agent ID (remove -agent suffix if present, replace underscores)
160
- normalized_id = agent_id.replace("-agent", "").replace("_", "-")
161
-
162
- agents.append(
163
- AgentConfig(
164
- name=normalized_id,
165
- description=(
166
- description[:80] + "..."
167
- if len(description) > 80
168
- else description
169
- ),
170
- dependencies=display_tools,
171
- )
172
- )
173
-
174
- except (json.JSONDecodeError, KeyError) as e:
175
- # Log malformed templates but continue
176
- self.logger.debug(
177
- f"Skipping malformed template {template_file.name}: {e}"
178
- )
179
- continue
180
- except Exception as e:
181
- # Log unexpected errors but continue processing other templates
182
- self.logger.debug(
183
- f"Error processing template {template_file.name}: {e}"
184
- )
185
- continue
186
-
187
- except Exception as e:
188
- # If there's a catastrophic error reading templates directory
189
- self.logger.error(f"Failed to read templates directory: {e}")
190
- return [
191
- AgentConfig("engineer", f"Error accessing templates: {e!s}", []),
192
- AgentConfig("research", "Research agent", []),
193
- ]
194
-
195
- # Sort agents by name for consistent display
196
- agents.sort(key=lambda a: a.name)
197
-
198
- return (
199
- agents
200
- if agents
201
- else [
202
- AgentConfig("engineer", "No agents found in templates", []),
203
- ]
204
- )
26
+ from .agent_state_manager import SimpleAgentManager
27
+ from .configure_agent_display import AgentDisplay
28
+ from .configure_behavior_manager import BehaviorManager
29
+ from .configure_hook_manager import HookManager
30
+ from .configure_models import AgentConfig
31
+ from .configure_navigation import ConfigNavigation
32
+ from .configure_persistence import ConfigPersistence
33
+ from .configure_startup_manager import StartupManager
34
+ from .configure_template_editor import TemplateEditor
35
+ from .configure_validators import (
36
+ parse_id_selection,
37
+ validate_args as validate_configure_args,
38
+ )
205
39
 
206
40
 
207
41
  class ConfigureCommand(BaseCommand):
@@ -214,25 +48,88 @@ class ConfigureCommand(BaseCommand):
214
48
  self.current_scope = "project"
215
49
  self.project_dir = Path.cwd()
216
50
  self.agent_manager = None
51
+ self.hook_manager = HookManager(self.console)
52
+ self.behavior_manager = None # Initialized when scope is set
53
+ self._agent_display = None # Lazy-initialized
54
+ self._persistence = None # Lazy-initialized
55
+ self._navigation = None # Lazy-initialized
56
+ self._template_editor = None # Lazy-initialized
57
+ self._startup_manager = None # Lazy-initialized
217
58
 
218
59
  def validate_args(self, args) -> Optional[str]:
219
60
  """Validate command arguments."""
220
- # Check for conflicting direct navigation options
221
- nav_options = [
222
- getattr(args, "agents", False),
223
- getattr(args, "templates", False),
224
- getattr(args, "behaviors", False),
225
- getattr(args, "startup", False),
226
- getattr(args, "version_info", False),
227
- ]
228
- if sum(nav_options) > 1:
229
- return "Only one direct navigation option can be specified at a time"
230
-
231
- # Check for conflicting non-interactive options
232
- if getattr(args, "enable_agent", None) and getattr(args, "disable_agent", None):
233
- return "Cannot enable and disable agents at the same time"
234
-
235
- return None
61
+ return validate_configure_args(args)
62
+
63
+ @property
64
+ def agent_display(self) -> AgentDisplay:
65
+ """Lazy-initialize agent display handler."""
66
+ if self._agent_display is None:
67
+ if self.agent_manager is None:
68
+ raise RuntimeError(
69
+ "agent_manager must be initialized before agent_display"
70
+ )
71
+ self._agent_display = AgentDisplay(
72
+ self.console,
73
+ self.agent_manager,
74
+ self._get_agent_template_path,
75
+ self._display_header,
76
+ )
77
+ return self._agent_display
78
+
79
+ @property
80
+ def persistence(self) -> ConfigPersistence:
81
+ """Lazy-initialize persistence handler."""
82
+ if self._persistence is None:
83
+ # Note: agent_manager might be None for version_info calls
84
+ self._persistence = ConfigPersistence(
85
+ self.console,
86
+ self.version_service,
87
+ self.agent_manager, # Can be None for version operations
88
+ self._get_agent_template_path,
89
+ self._display_header,
90
+ self.current_scope,
91
+ self.project_dir,
92
+ )
93
+ return self._persistence
94
+
95
+ @property
96
+ def navigation(self) -> ConfigNavigation:
97
+ """Lazy-initialize navigation handler."""
98
+ if self._navigation is None:
99
+ self._navigation = ConfigNavigation(self.console, self.project_dir)
100
+ # Sync scope from main command
101
+ self._navigation.current_scope = self.current_scope
102
+ return self._navigation
103
+
104
+ @property
105
+ def template_editor(self) -> TemplateEditor:
106
+ """Lazy-initialize template editor."""
107
+ if self._template_editor is None:
108
+ if self.agent_manager is None:
109
+ raise RuntimeError(
110
+ "agent_manager must be initialized before template_editor"
111
+ )
112
+ self._template_editor = TemplateEditor(
113
+ self.console, self.agent_manager, self.current_scope, self.project_dir
114
+ )
115
+ return self._template_editor
116
+
117
+ @property
118
+ def startup_manager(self) -> StartupManager:
119
+ """Lazy-initialize startup manager."""
120
+ if self._startup_manager is None:
121
+ if self.agent_manager is None:
122
+ raise RuntimeError(
123
+ "agent_manager must be initialized before startup_manager"
124
+ )
125
+ self._startup_manager = StartupManager(
126
+ self.agent_manager,
127
+ self.console,
128
+ self.current_scope,
129
+ self.project_dir,
130
+ self._display_header,
131
+ )
132
+ return self._startup_manager
236
133
 
237
134
  def run(self, args) -> CommandResult:
238
135
  """Execute the configure command."""
@@ -241,12 +138,15 @@ class ConfigureCommand(BaseCommand):
241
138
  if getattr(args, "project_dir", None):
242
139
  self.project_dir = Path(args.project_dir)
243
140
 
244
- # Initialize agent manager with appropriate config directory
141
+ # Initialize agent manager and behavior manager with appropriate config directory
245
142
  if self.current_scope == "project":
246
143
  config_dir = self.project_dir / ".claude-mpm"
247
144
  else:
248
145
  config_dir = Path.home() / ".claude-mpm"
249
146
  self.agent_manager = SimpleAgentManager(config_dir)
147
+ self.behavior_manager = BehaviorManager(
148
+ config_dir, self.current_scope, self.console
149
+ )
250
150
 
251
151
  # Disable colors if requested
252
152
  if getattr(args, "no_colors", False):
@@ -311,19 +211,21 @@ class ConfigureCommand(BaseCommand):
311
211
  if choice == "1":
312
212
  self._manage_agents()
313
213
  elif choice == "2":
314
- self._edit_templates()
214
+ self._manage_skills()
315
215
  elif choice == "3":
316
- self._manage_behaviors()
216
+ self._edit_templates()
317
217
  elif choice == "4":
218
+ self._manage_behaviors()
219
+ elif choice == "5":
318
220
  # If user saves and wants to proceed to startup, exit the configurator
319
221
  if self._manage_startup_configuration():
320
222
  self.console.print(
321
223
  "\n[green]Configuration saved. Exiting configurator...[/green]"
322
224
  )
323
225
  break
324
- elif choice == "5":
325
- self._switch_scope()
326
226
  elif choice == "6":
227
+ self._switch_scope()
228
+ elif choice == "7":
327
229
  self._show_version_info_interactive()
328
230
  elif choice == "l":
329
231
  # Check for pending agent changes
@@ -371,69 +273,15 @@ class ConfigureCommand(BaseCommand):
371
273
 
372
274
  def _display_header(self) -> None:
373
275
  """Display the TUI header."""
374
- self.console.clear()
375
-
376
- # Get version for display
377
- from claude_mpm import __version__
378
-
379
- # Create header panel
380
- header_text = Text()
381
- header_text.append("Claude MPM ", style="bold cyan")
382
- header_text.append("Configuration Interface", style="bold white")
383
- header_text.append(f"\nv{__version__}", style="dim cyan")
384
-
385
- scope_text = Text(f"Scope: {self.current_scope.upper()}", style="yellow")
386
- dir_text = Text(f"Directory: {self.project_dir}", style="dim")
387
-
388
- header_content = Columns([header_text], align="center")
389
- subtitle_content = f"{scope_text} | {dir_text}"
390
-
391
- header_panel = Panel(
392
- header_content,
393
- subtitle=subtitle_content,
394
- box=ROUNDED,
395
- style="blue",
396
- padding=(1, 2),
397
- )
398
-
399
- self.console.print(header_panel)
400
- self.console.print()
276
+ # Sync scope to navigation before display
277
+ self.navigation.current_scope = self.current_scope
278
+ self.navigation.display_header()
401
279
 
402
280
  def _show_main_menu(self) -> str:
403
281
  """Show the main menu and get user choice."""
404
- menu_items = [
405
- ("1", "Agent Management", "Enable/disable agents and customize settings"),
406
- ("2", "Template Editing", "Edit agent JSON templates"),
407
- ("3", "Behavior Files", "Manage identity and workflow configurations"),
408
- (
409
- "4",
410
- "Startup Configuration",
411
- "Configure MCP services and agents to start",
412
- ),
413
- ("5", "Switch Scope", f"Current: {self.current_scope}"),
414
- ("6", "Version Info", "Display MPM and Claude versions"),
415
- ("l", "Save & Launch", "Save all changes and start Claude MPM"),
416
- ("q", "Quit", "Exit without launching"),
417
- ]
418
-
419
- table = Table(show_header=False, box=None, padding=(0, 2))
420
- table.add_column("Key", style="cyan bold", width=4) # Bolder shortcuts
421
- table.add_column("Option", style="bold white", width=24) # Wider for titles
422
- table.add_column("Description", style="white") # Better contrast
423
-
424
- for key, option, desc in menu_items:
425
- table.add_row(f"\\[{key}]", option, desc)
426
-
427
- menu_panel = Panel(
428
- table, title="[bold]Main Menu[/bold]", box=ROUNDED, style="green"
429
- )
430
-
431
- self.console.print(menu_panel)
432
- self.console.print()
433
-
434
- choice = Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="q")
435
- # Strip whitespace to handle leading/trailing spaces
436
- return choice.strip().lower()
282
+ # Sync scope to navigation before display
283
+ self.navigation.current_scope = self.current_scope
284
+ return self.navigation.show_main_menu()
437
285
 
438
286
  def _manage_agents(self) -> None:
439
287
  """Agent management interface."""
@@ -450,33 +298,33 @@ class ConfigureCommand(BaseCommand):
450
298
 
451
299
  # Use Text objects to properly display shortcuts with styling
452
300
  text_t = Text(" ")
453
- text_t.append("[t]", style="cyan bold")
301
+ text_t.append("[t]", style="bold blue")
454
302
  text_t.append(" Toggle agents (enable/disable multiple)")
455
303
  self.console.print(text_t)
456
304
 
457
305
  text_c = Text(" ")
458
- text_c.append("[c]", style="cyan bold")
306
+ text_c.append("[c]", style="bold blue")
459
307
  text_c.append(" Customize agent template")
460
308
  self.console.print(text_c)
461
309
 
462
310
  text_v = Text(" ")
463
- text_v.append("[v]", style="cyan bold")
311
+ text_v.append("[v]", style="bold blue")
464
312
  text_v.append(" View agent details")
465
313
  self.console.print(text_v)
466
314
 
467
315
  text_r = Text(" ")
468
- text_r.append("[r]", style="cyan bold")
316
+ text_r.append("[r]", style="bold blue")
469
317
  text_r.append(" Reset agent to defaults")
470
318
  self.console.print(text_r)
471
319
 
472
320
  text_b = Text(" ")
473
- text_b.append("[b]", style="cyan bold")
321
+ text_b.append("[b]", style="bold blue")
474
322
  text_b.append(" Back to main menu")
475
323
  self.console.print(text_b)
476
324
 
477
325
  self.console.print()
478
326
 
479
- choice = Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="b")
327
+ choice = Prompt.ask("[bold blue]Select an option[/bold blue]", default="b")
480
328
 
481
329
  if choice == "b":
482
330
  break
@@ -494,101 +342,11 @@ class ConfigureCommand(BaseCommand):
494
342
 
495
343
  def _display_agents_table(self, agents: List[AgentConfig]) -> None:
496
344
  """Display a table of available agents."""
497
- table = Table(
498
- title=f"Available Agents ({len(agents)} total)",
499
- box=ROUNDED,
500
- show_lines=True,
501
- )
502
-
503
- table.add_column("ID", style="dim", width=3)
504
- table.add_column("Name", style="cyan", width=22)
505
- table.add_column("Status", width=12)
506
- table.add_column("Description", style="bold cyan", width=45)
507
- table.add_column("Model/Tools", style="dim", width=20)
508
-
509
- for idx, agent in enumerate(agents, 1):
510
- # Check if agent is enabled
511
- is_enabled = self.agent_manager.is_agent_enabled(agent.name)
512
- status = (
513
- "[green]✓ Enabled[/green]" if is_enabled else "[red]✗ Disabled[/red]"
514
- )
515
-
516
- # Format tools/dependencies - show first 2 tools
517
- tools_display = ""
518
- if agent.dependencies:
519
- if len(agent.dependencies) > 2:
520
- tools_display = f"{', '.join(agent.dependencies[:2])}..."
521
- else:
522
- tools_display = ", ".join(agent.dependencies)
523
- else:
524
- # Try to get model from template
525
- try:
526
- template_path = self._get_agent_template_path(agent.name)
527
- if template_path.exists():
528
- with template_path.open() as f:
529
- template = json.load(f)
530
- model = template.get("capabilities", {}).get("model", "default")
531
- tools_display = f"Model: {model}"
532
- else:
533
- tools_display = "Default"
534
- except Exception:
535
- tools_display = "Default"
536
-
537
- # Truncate description for table display with bright styling
538
- if len(agent.description) > 42:
539
- desc_display = f"[cyan]{agent.description[:42]}[/cyan][dim]...[/dim]"
540
- else:
541
- desc_display = f"[cyan]{agent.description}[/cyan]"
542
-
543
- table.add_row(str(idx), agent.name, status, desc_display, tools_display)
544
-
545
- self.console.print(table)
345
+ self.agent_display.display_agents_table(agents)
546
346
 
547
347
  def _display_agents_with_pending_states(self, agents: List[AgentConfig]) -> None:
548
348
  """Display agents table with pending state indicators."""
549
- has_pending = self.agent_manager.has_pending_changes()
550
- pending_count = len(self.agent_manager.deferred_changes) if has_pending else 0
551
-
552
- title = f"Available Agents ({len(agents)} total)"
553
- if has_pending:
554
- title += f" [yellow]({pending_count} change{'s' if pending_count != 1 else ''} pending)[/yellow]"
555
-
556
- table = Table(title=title, box=ROUNDED, show_lines=True, expand=True)
557
- table.add_column("ID", justify="right", style="cyan", width=5)
558
- table.add_column("Name", style="bold", width=22)
559
- table.add_column("Status", width=20)
560
- table.add_column("Description", style="bold cyan", width=45)
561
-
562
- for idx, agent in enumerate(agents, 1):
563
- current_state = self.agent_manager.is_agent_enabled(agent.name)
564
- pending_state = self.agent_manager.get_pending_state(agent.name)
565
-
566
- # Show pending status with arrow
567
- if current_state != pending_state:
568
- if pending_state:
569
- status = "[yellow]✗ Disabled → ✓ Enabled[/yellow]"
570
- else:
571
- status = "[yellow]✓ Enabled → ✗ Disabled[/yellow]"
572
- else:
573
- status = (
574
- "[green]✓ Enabled[/green]"
575
- if current_state
576
- else "[dim]✗ Disabled[/dim]"
577
- )
578
-
579
- desc_display = Text()
580
- desc_display.append(
581
- (
582
- agent.description[:42] + "..."
583
- if len(agent.description) > 42
584
- else agent.description
585
- ),
586
- style="cyan",
587
- )
588
-
589
- table.add_row(str(idx), agent.name, status, desc_display)
590
-
591
- self.console.print(table)
349
+ self.agent_display.display_agents_with_pending_states(agents)
592
350
 
593
351
  def _toggle_agents_interactive(self, agents: List[AgentConfig]) -> None:
594
352
  """Interactive multi-agent enable/disable with batch save."""
@@ -605,32 +363,32 @@ class ConfigureCommand(BaseCommand):
605
363
  # Show menu
606
364
  self.console.print("\n[bold]Toggle Agent Status:[/bold]")
607
365
  text_toggle = Text(" ")
608
- text_toggle.append("[t]", style="cyan bold")
366
+ text_toggle.append("[t]", style="bold blue")
609
367
  text_toggle.append(" Enter agent IDs to toggle (e.g., '1,3,5' or '1-4')")
610
368
  self.console.print(text_toggle)
611
369
 
612
370
  text_all = Text(" ")
613
- text_all.append("[a]", style="cyan bold")
371
+ text_all.append("[a]", style="bold blue")
614
372
  text_all.append(" Enable all agents")
615
373
  self.console.print(text_all)
616
374
 
617
375
  text_none = Text(" ")
618
- text_none.append("[n]", style="cyan bold")
376
+ text_none.append("[n]", style="bold blue")
619
377
  text_none.append(" Disable all agents")
620
378
  self.console.print(text_none)
621
379
 
622
380
  text_save = Text(" ")
623
- text_save.append("[s]", style="green bold")
381
+ text_save.append("[s]", style="bold green")
624
382
  text_save.append(" Save changes and return")
625
383
  self.console.print(text_save)
626
384
 
627
385
  text_cancel = Text(" ")
628
- text_cancel.append("[c]", style="yellow bold")
386
+ text_cancel.append("[c]", style="bold magenta")
629
387
  text_cancel.append(" Cancel (discard changes)")
630
388
  self.console.print(text_cancel)
631
389
 
632
390
  choice = (
633
- Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="s")
391
+ Prompt.ask("[bold blue]Select an option[/bold blue]", default="s")
634
392
  .strip()
635
393
  .lower()
636
394
  )
@@ -668,1300 +426,282 @@ class ConfigureCommand(BaseCommand):
668
426
 
669
427
  def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
670
428
  """Customize agent JSON template."""
671
- agent_id = Prompt.ask("Enter agent ID to customize")
672
-
673
- try:
674
- idx = int(agent_id) - 1
675
- if 0 <= idx < len(agents):
676
- agent = agents[idx]
677
- self._edit_agent_template(agent)
678
- else:
679
- self.console.print("[red]Invalid agent ID.[/red]")
680
- Prompt.ask("Press Enter to continue")
681
- except ValueError:
682
- self.console.print("[red]Invalid input. Please enter a number.[/red]")
683
- Prompt.ask("Press Enter to continue")
429
+ self.template_editor.customize_agent_template(agents)
684
430
 
685
431
  def _edit_agent_template(self, agent: AgentConfig) -> None:
686
432
  """Edit an agent's JSON template."""
687
- self.console.clear()
688
- self.console.print(f"[bold]Editing template for: {agent.name}[/bold]\n")
689
-
690
- # Get current template
691
- template_path = self._get_agent_template_path(agent.name)
692
-
693
- if template_path.exists():
694
- with template_path.open() as f:
695
- template = json.load(f)
696
- is_system = str(template_path).startswith(
697
- str(self.agent_manager.templates_dir)
698
- )
699
- else:
700
- # Create a minimal template structure based on system templates
701
- template = {
702
- "schema_version": "1.2.0",
703
- "agent_id": agent.name,
704
- "agent_version": "1.0.0",
705
- "agent_type": agent.name.replace("-", "_"),
706
- "metadata": {
707
- "name": agent.name.replace("-", " ").title() + " Agent",
708
- "description": agent.description,
709
- "tags": [agent.name],
710
- "author": "Custom",
711
- "created_at": "",
712
- "updated_at": "",
713
- },
714
- "capabilities": {
715
- "model": "opus",
716
- "tools": (
717
- agent.dependencies
718
- if agent.dependencies
719
- else ["Read", "Write", "Edit", "Bash"]
720
- ),
721
- },
722
- "instructions": {
723
- "base_template": "BASE_AGENT_TEMPLATE.md",
724
- "custom_instructions": "",
725
- },
726
- }
727
- is_system = False
728
-
729
- # Display current template
730
- if is_system:
731
- self.console.print(
732
- "[yellow]Viewing SYSTEM template (read-only). Customization will create a local copy.[/yellow]\n"
733
- )
734
-
735
- self.console.print("[bold]Current Template:[/bold]")
736
- # Truncate for display if too large
737
- display_template = template.copy()
738
- if (
739
- "instructions" in display_template
740
- and isinstance(display_template["instructions"], dict)
741
- and (
742
- "custom_instructions" in display_template["instructions"]
743
- and len(str(display_template["instructions"]["custom_instructions"]))
744
- > 200
745
- )
746
- ):
747
- display_template["instructions"]["custom_instructions"] = (
748
- display_template["instructions"]["custom_instructions"][:200] + "..."
749
- )
750
-
751
- json_str = json.dumps(display_template, indent=2)
752
- # Limit display to first 50 lines for readability
753
- lines = json_str.split("\n")
754
- if len(lines) > 50:
755
- json_str = "\n".join(lines[:50]) + "\n... (truncated for display)"
756
-
757
- syntax = Syntax(json_str, "json", theme="monokai", line_numbers=True)
758
- self.console.print(syntax)
759
- self.console.print()
760
-
761
- # Editing options
762
- self.console.print("[bold]Editing Options:[/bold]")
763
- if not is_system:
764
- text_1 = Text(" ")
765
- text_1.append("[1]", style="cyan bold")
766
- text_1.append(" Edit in external editor")
767
- self.console.print(text_1)
768
-
769
- text_2 = Text(" ")
770
- text_2.append("[2]", style="cyan bold")
771
- text_2.append(" Add/modify a field")
772
- self.console.print(text_2)
773
-
774
- text_3 = Text(" ")
775
- text_3.append("[3]", style="cyan bold")
776
- text_3.append(" Remove a field")
777
- self.console.print(text_3)
778
-
779
- text_4 = Text(" ")
780
- text_4.append("[4]", style="cyan bold")
781
- text_4.append(" Reset to defaults")
782
- self.console.print(text_4)
783
- else:
784
- text_1 = Text(" ")
785
- text_1.append("[1]", style="cyan bold")
786
- text_1.append(" Create customized copy")
787
- self.console.print(text_1)
788
-
789
- text_2 = Text(" ")
790
- text_2.append("[2]", style="cyan bold")
791
- text_2.append(" View full template")
792
- self.console.print(text_2)
793
-
794
- text_b = Text(" ")
795
- text_b.append("[b]", style="cyan bold")
796
- text_b.append(" Back")
797
- self.console.print(text_b)
798
-
799
- self.console.print()
800
-
801
- choice = Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="b")
802
-
803
- if is_system:
804
- if choice == "1":
805
- # Create a customized copy
806
- self._create_custom_template_copy(agent, template)
807
- elif choice == "2":
808
- # View full template
809
- self._view_full_template(template)
810
- elif choice == "1":
811
- self._edit_in_external_editor(template_path, template)
812
- elif choice == "2":
813
- self._modify_template_field(template, template_path)
814
- elif choice == "3":
815
- self._remove_template_field(template, template_path)
816
- elif choice == "4":
817
- self._reset_template(agent, template_path)
818
-
819
- if choice != "b":
820
- Prompt.ask("Press Enter to continue")
433
+ self.template_editor.edit_agent_template(agent)
821
434
 
822
435
  def _get_agent_template_path(self, agent_name: str) -> Path:
823
436
  """Get the path to an agent's template file."""
824
- # First check for custom template in project/user config
825
- if self.current_scope == "project":
826
- config_dir = self.project_dir / ".claude-mpm" / "agents"
827
- else:
828
- config_dir = Path.home() / ".claude-mpm" / "agents"
829
-
830
- config_dir.mkdir(parents=True, exist_ok=True)
831
- custom_template = config_dir / f"{agent_name}.json"
832
-
833
- # If custom template exists, return it
834
- if custom_template.exists():
835
- return custom_template
836
-
837
- # Otherwise, look for the system template
838
- # Handle various naming conventions
839
- possible_names = [
840
- f"{agent_name}.json",
841
- f"{agent_name.replace('-', '_')}.json",
842
- f"{agent_name}-agent.json",
843
- f"{agent_name.replace('-', '_')}_agent.json",
844
- ]
845
-
846
- for name in possible_names:
847
- system_template = self.agent_manager.templates_dir / name
848
- if system_template.exists():
849
- return system_template
850
-
851
- # Return the custom template path for new templates
852
- return custom_template
437
+ return self.template_editor.get_agent_template_path(agent_name)
853
438
 
854
439
  def _edit_in_external_editor(self, template_path: Path, template: Dict) -> None:
855
440
  """Open template in external editor."""
856
- import subprocess
857
- import tempfile
858
-
859
- # Write current template to temp file
860
- with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
861
- json.dump(template, f, indent=2)
862
- temp_path = f.name
863
-
864
- # Get editor from environment
865
- editor = os.environ.get("EDITOR", "nano")
866
-
867
- try:
868
- # Open in editor
869
- subprocess.call([editor, temp_path])
870
-
871
- # Read back the edited content
872
- with temp_path.open() as f:
873
- new_template = json.load(f)
874
-
875
- # Save to actual template path
876
- with template_path.open("w") as f:
877
- json.dump(new_template, f, indent=2)
878
-
879
- self.console.print("[green]Template updated successfully![/green]")
880
-
881
- except Exception as e:
882
- self.console.print(f"[red]Error editing template: {e}[/red]")
883
- finally:
884
- # Clean up temp file
885
- Path(temp_path).unlink(missing_ok=True)
441
+ self.template_editor.edit_in_external_editor(template_path, template)
886
442
 
887
443
  def _modify_template_field(self, template: Dict, template_path: Path) -> None:
888
444
  """Add or modify a field in the template."""
889
- field_name = Prompt.ask(
890
- "Enter field name (use dot notation for nested, e.g., 'config.timeout')"
891
- )
892
- field_value = Prompt.ask("Enter field value (JSON format)")
893
-
894
- try:
895
- # Parse the value as JSON
896
- value = json.loads(field_value)
897
-
898
- # Navigate to the field location
899
- parts = field_name.split(".")
900
- current = template
901
-
902
- for part in parts[:-1]:
903
- if part not in current:
904
- current[part] = {}
905
- current = current[part]
906
-
907
- # Set the value
908
- current[parts[-1]] = value
909
-
910
- # Save the template
911
- with template_path.open("w") as f:
912
- json.dump(template, f, indent=2)
913
-
914
- self.console.print(
915
- f"[green]Field '{field_name}' updated successfully![/green]"
916
- )
917
-
918
- except json.JSONDecodeError:
919
- self.console.print("[red]Invalid JSON value. Please try again.[/red]")
920
- except Exception as e:
921
- self.console.print(f"[red]Error updating field: {e}[/red]")
445
+ self.template_editor.modify_template_field(template, template_path)
922
446
 
923
447
  def _remove_template_field(self, template: Dict, template_path: Path) -> None:
924
448
  """Remove a field from the template."""
925
- field_name = Prompt.ask(
926
- "Enter field name to remove (use dot notation for nested)"
927
- )
928
-
929
- try:
930
- # Navigate to the field location
931
- parts = field_name.split(".")
932
- current = template
933
-
934
- for part in parts[:-1]:
935
- if part not in current:
936
- raise KeyError(f"Field '{field_name}' not found")
937
- current = current[part]
938
-
939
- # Remove the field
940
- if parts[-1] in current:
941
- del current[parts[-1]]
942
-
943
- # Save the template
944
- with template_path.open("w") as f:
945
- json.dump(template, f, indent=2)
946
-
947
- self.console.print(
948
- f"[green]Field '{field_name}' removed successfully![/green]"
949
- )
950
- else:
951
- self.console.print(f"[red]Field '{field_name}' not found.[/red]")
952
-
953
- except Exception as e:
954
- self.console.print(f"[red]Error removing field: {e}[/red]")
449
+ self.template_editor.remove_template_field(template, template_path)
955
450
 
956
451
  def _reset_template(self, agent: AgentConfig, template_path: Path) -> None:
957
452
  """Reset template to defaults."""
958
- if Confirm.ask(f"[yellow]Reset '{agent.name}' template to defaults?[/yellow]"):
959
- # Remove custom template file
960
- template_path.unlink(missing_ok=True)
961
- self.console.print(
962
- f"[green]Template for '{agent.name}' reset to defaults![/green]"
963
- )
453
+ self.template_editor.reset_template(agent, template_path)
964
454
 
965
455
  def _create_custom_template_copy(self, agent: AgentConfig, template: Dict) -> None:
966
456
  """Create a customized copy of a system template."""
967
- if self.current_scope == "project":
968
- config_dir = self.project_dir / ".claude-mpm" / "agents"
969
- else:
970
- config_dir = Path.home() / ".claude-mpm" / "agents"
971
-
972
- config_dir.mkdir(parents=True, exist_ok=True)
973
- custom_path = config_dir / f"{agent.name}.json"
974
-
975
- if custom_path.exists() and not Confirm.ask(
976
- "[yellow]Custom template already exists. Overwrite?[/yellow]"
977
- ):
978
- return
979
-
980
- # Save the template copy
981
- with custom_path.open("w") as f:
982
- json.dump(template, f, indent=2)
983
-
984
- self.console.print(f"[green]Created custom template at: {custom_path}[/green]")
985
- self.console.print("[green]You can now edit this template.[/green]")
457
+ self.template_editor.create_custom_template_copy(agent, template)
986
458
 
987
459
  def _view_full_template(self, template: Dict) -> None:
988
460
  """View the full template without truncation."""
989
- self.console.clear()
990
- self.console.print("[bold]Full Template View:[/bold]\n")
991
-
992
- json_str = json.dumps(template, indent=2)
993
- syntax = Syntax(json_str, "json", theme="monokai", line_numbers=True)
994
-
995
- # Use pager for long content
996
-
997
- with self.console.pager():
998
- self.console.print(syntax)
461
+ self.template_editor.view_full_template(template)
999
462
 
1000
463
  def _reset_agent_defaults(self, agents: List[AgentConfig]) -> None:
1001
- """Reset an agent to default enabled state and remove custom template.
464
+ """Reset an agent to default enabled state and remove custom template."""
465
+ self.template_editor.reset_agent_defaults(agents)
1002
466
 
1003
- This method:
1004
- - Prompts for agent ID
1005
- - Resets agent to enabled state
1006
- - Removes any custom template overrides
1007
- - Shows success/error messages
1008
- """
1009
- agent_id = Prompt.ask("Enter agent ID to reset to defaults")
467
+ def _edit_templates(self) -> None:
468
+ """Template editing interface."""
469
+ self.template_editor.edit_templates_interface()
1010
470
 
1011
- try:
1012
- idx = int(agent_id) - 1
1013
- if 0 <= idx < len(agents):
1014
- agent = agents[idx]
1015
-
1016
- # Confirm the reset action
1017
- if not Confirm.ask(
1018
- f"[yellow]Reset '{agent.name}' to defaults? This will:[/yellow]\n"
1019
- " - Enable the agent\n"
1020
- " - Remove custom template (if any)\n"
1021
- "[yellow]Continue?[/yellow]"
1022
- ):
1023
- self.console.print("[yellow]Reset cancelled.[/yellow]")
1024
- Prompt.ask("Press Enter to continue")
1025
- return
1026
-
1027
- # Enable the agent
1028
- self.agent_manager.set_agent_enabled(agent.name, True)
1029
-
1030
- # Remove custom template if exists
1031
- template_path = self._get_agent_template_path(agent.name)
1032
- if template_path.exists() and not str(template_path).startswith(
1033
- str(self.agent_manager.templates_dir)
1034
- ):
1035
- # This is a custom template, remove it
1036
- template_path.unlink(missing_ok=True)
1037
- self.console.print(
1038
- f"[green]✓ Removed custom template for '{agent.name}'[/green]"
1039
- )
471
+ def _manage_behaviors(self) -> None:
472
+ """Behavior file management interface."""
473
+ # Note: BehaviorManager handles its own loop and clears screen
474
+ # but doesn't display our header. We'll need to update BehaviorManager
475
+ # to accept a header callback in the future. For now, just delegate.
476
+ self.behavior_manager.manage_behaviors()
1040
477
 
1041
- self.console.print(
1042
- f"[green]✓ Agent '{agent.name}' reset to defaults![/green]"
1043
- )
1044
- self.console.print(
1045
- "[dim]Agent is now enabled with system template.[/dim]"
1046
- )
1047
- else:
1048
- self.console.print("[red]Invalid agent ID.[/red]")
478
+ def _manage_skills(self) -> None:
479
+ """Skills management interface."""
480
+ from ...cli.interactive.skills_wizard import SkillsWizard
481
+ from ...skills.skill_manager import get_manager
1049
482
 
1050
- except ValueError:
1051
- self.console.print("[red]Invalid input. Please enter a number.[/red]")
483
+ wizard = SkillsWizard()
484
+ manager = get_manager()
1052
485
 
1053
- Prompt.ask("Press Enter to continue")
486
+ while True:
487
+ self.console.clear()
488
+ self._display_header()
1054
489
 
1055
- def _view_agent_details(self, agents: List[AgentConfig]) -> None:
1056
- """View detailed information about an agent."""
1057
- agent_id = Prompt.ask("Enter agent ID to view")
490
+ self.console.print("\n[bold]Skills Management Options:[/bold]\n")
491
+ self.console.print(" [1] View Available Skills")
492
+ self.console.print(" [2] Configure Skills for Agents")
493
+ self.console.print(" [3] View Current Skill Mappings")
494
+ self.console.print(" [4] Auto-Link Skills to Agents")
495
+ self.console.print(" [b] Back to Main Menu")
496
+ self.console.print()
1058
497
 
1059
- try:
1060
- idx = int(agent_id) - 1
1061
- if 0 <= idx < len(agents):
1062
- agent = agents[idx]
498
+ choice = Prompt.ask("[bold blue]Select an option[/bold blue]", default="b")
1063
499
 
500
+ if choice == "1":
501
+ # View available skills
1064
502
  self.console.clear()
1065
503
  self._display_header()
504
+ wizard.list_available_skills()
505
+ Prompt.ask("\nPress Enter to continue")
1066
506
 
1067
- # Try to load full template for more details
1068
- template_path = self._get_agent_template_path(agent.name)
1069
- extra_info = ""
1070
-
1071
- if template_path.exists():
1072
- try:
1073
- with template_path.open() as f:
1074
- template = json.load(f)
1075
-
1076
- # Extract additional information
1077
- metadata = template.get("metadata", {})
1078
- capabilities = template.get("capabilities", {})
1079
-
1080
- # Get full description if available
1081
- full_desc = metadata.get("description", agent.description)
507
+ elif choice == "2":
508
+ # Configure skills interactively
509
+ self.console.clear()
510
+ self._display_header()
1082
511
 
1083
- # Get model and tools
1084
- model = capabilities.get("model", "default")
1085
- tools = capabilities.get("tools", [])
512
+ # Get list of enabled agents
513
+ agents = self.agent_manager.discover_agents()
514
+ enabled_agents = [
515
+ a.name
516
+ for a in agents
517
+ if self.agent_manager.get_pending_state(a.name)
518
+ ]
1086
519
 
1087
- # Get tags
1088
- tags = metadata.get("tags", [])
520
+ if not enabled_agents:
521
+ self.console.print(
522
+ "[yellow]No agents are currently enabled.[/yellow]"
523
+ )
524
+ self.console.print(
525
+ "Please enable agents first in Agent Management."
526
+ )
527
+ Prompt.ask("\nPress Enter to continue")
528
+ continue
1089
529
 
1090
- # Get version info
1091
- agent_version = template.get("agent_version", "N/A")
1092
- schema_version = template.get("schema_version", "N/A")
530
+ # Run skills wizard
531
+ success, mapping = wizard.run_interactive_selection(enabled_agents)
1093
532
 
1094
- extra_info = f"""
1095
- [bold]Full Description:[/bold]
1096
- {full_desc}
533
+ if success:
534
+ # Save the configuration
535
+ manager.save_mappings_to_config()
536
+ self.console.print("\n[green]✓ Skills configuration saved![/green]")
537
+ else:
538
+ self.console.print(
539
+ "\n[yellow]Skills configuration cancelled.[/yellow]"
540
+ )
1097
541
 
1098
- [bold]Model:[/bold] {model}
1099
- [bold]Agent Version:[/bold] {agent_version}
1100
- [bold]Schema Version:[/bold] {schema_version}
1101
- [bold]Tags:[/bold] {', '.join(tags) if tags else 'None'}
1102
- [bold]Tools:[/bold] {', '.join(tools[:5]) if tools else 'None'}{'...' if len(tools) > 5 else ''}
1103
- """
1104
- except Exception:
1105
- pass
1106
-
1107
- # Create detail panel
1108
- detail_text = f"""
1109
- [bold]Name:[/bold] {agent.name}
1110
- [bold]Status:[/bold] {'[green]Enabled[/green]' if self.agent_manager.is_agent_enabled(agent.name) else '[red]Disabled[/red]'}
1111
- [bold]Template Path:[/bold] {template_path}
1112
- [bold]Is System Template:[/bold] {'Yes' if str(template_path).startswith(str(self.agent_manager.templates_dir)) else 'No (Custom)'}
1113
- {extra_info}
1114
- """
1115
-
1116
- panel = Panel(
1117
- detail_text.strip(),
1118
- title=f"[bold]{agent.name} Details[/bold]",
1119
- box=ROUNDED,
1120
- style="cyan",
1121
- )
542
+ Prompt.ask("\nPress Enter to continue")
1122
543
 
1123
- self.console.print(panel)
544
+ elif choice == "3":
545
+ # View current mappings
546
+ self.console.clear()
547
+ self._display_header()
1124
548
 
1125
- else:
1126
- self.console.print("[red]Invalid agent ID.[/red]")
549
+ self.console.print("\n[bold]Current Skill Mappings:[/bold]\n")
1127
550
 
1128
- except ValueError:
1129
- self.console.print("[red]Invalid input. Please enter a number.[/red]")
551
+ mappings = manager.list_agent_skill_mappings()
552
+ if not mappings:
553
+ self.console.print("[dim]No skill mappings configured yet.[/dim]")
554
+ else:
555
+ from rich.table import Table
1130
556
 
1131
- Prompt.ask("\nPress Enter to continue")
557
+ table = Table(show_header=True, header_style="bold cyan")
558
+ table.add_column("Agent", style="yellow")
559
+ table.add_column("Skills", style="green")
1132
560
 
1133
- def _edit_templates(self) -> None:
1134
- """Template editing interface."""
1135
- self.console.print("[yellow]Template editing interface - Coming soon![/yellow]")
1136
- Prompt.ask("Press Enter to continue")
561
+ for agent_id, skills in mappings.items():
562
+ skills_str = (
563
+ ", ".join(skills) if skills else "[dim](none)[/dim]"
564
+ )
565
+ table.add_row(agent_id, skills_str)
1137
566
 
1138
- def _manage_behaviors(self) -> None:
1139
- """Behavior file management interface."""
1140
- while True:
1141
- self.console.clear()
1142
- self._display_header()
567
+ self.console.print(table)
1143
568
 
1144
- self.console.print("[bold]Behavior File Management[/bold]\n")
569
+ Prompt.ask("\nPress Enter to continue")
1145
570
 
1146
- # Display current behavior files
1147
- self._display_behavior_files()
571
+ elif choice == "4":
572
+ # Auto-link skills
573
+ self.console.clear()
574
+ self._display_header()
1148
575
 
1149
- # Show behavior menu
1150
- self.console.print("\n[bold]Options:[/bold]")
576
+ self.console.print("\n[bold]Auto-Linking Skills to Agents...[/bold]\n")
1151
577
 
1152
- text_1 = Text(" ")
1153
- text_1.append("[1]", style="cyan bold")
1154
- text_1.append(" Edit identity configuration")
1155
- self.console.print(text_1)
578
+ # Get enabled agents
579
+ agents = self.agent_manager.discover_agents()
580
+ enabled_agents = [
581
+ a.name
582
+ for a in agents
583
+ if self.agent_manager.get_pending_state(a.name)
584
+ ]
1156
585
 
1157
- text_2 = Text(" ")
1158
- text_2.append("[2]", style="cyan bold")
1159
- text_2.append(" Edit workflow configuration")
1160
- self.console.print(text_2)
586
+ if not enabled_agents:
587
+ self.console.print(
588
+ "[yellow]No agents are currently enabled.[/yellow]"
589
+ )
590
+ self.console.print(
591
+ "Please enable agents first in Agent Management."
592
+ )
593
+ Prompt.ask("\nPress Enter to continue")
594
+ continue
1161
595
 
1162
- text_3 = Text(" ")
1163
- text_3.append("[3]", style="cyan bold")
1164
- text_3.append(" Import behavior file")
1165
- self.console.print(text_3)
596
+ # Auto-link
597
+ mapping = wizard._auto_link_skills(enabled_agents)
1166
598
 
1167
- text_4 = Text(" ")
1168
- text_4.append("[4]", style="cyan bold")
1169
- text_4.append(" Export behavior file")
1170
- self.console.print(text_4)
599
+ # Display preview
600
+ self.console.print("Auto-linked skills:\n")
601
+ for agent_id, skills in mapping.items():
602
+ self.console.print(f" [yellow]{agent_id}[/yellow]:")
603
+ for skill in skills:
604
+ self.console.print(f" - {skill}")
1171
605
 
1172
- text_b = Text(" ")
1173
- text_b.append("[b]", style="cyan bold")
1174
- text_b.append(" Back to main menu")
1175
- self.console.print(text_b)
606
+ # Confirm
607
+ confirm = Confirm.ask("\nApply this configuration?", default=True)
1176
608
 
1177
- self.console.print()
609
+ if confirm:
610
+ wizard._apply_skills_configuration(mapping)
611
+ manager.save_mappings_to_config()
612
+ self.console.print("\n[green]✓ Auto-linking complete![/green]")
613
+ else:
614
+ self.console.print("\n[yellow]Auto-linking cancelled.[/yellow]")
1178
615
 
1179
- choice = Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="b")
616
+ Prompt.ask("\nPress Enter to continue")
1180
617
 
1181
- if choice == "b":
618
+ elif choice == "b":
1182
619
  break
1183
- if choice == "1":
1184
- self._edit_identity_config()
1185
- elif choice == "2":
1186
- self._edit_workflow_config()
1187
- elif choice == "3":
1188
- self._import_behavior_file()
1189
- elif choice == "4":
1190
- self._export_behavior_file()
1191
620
  else:
1192
- self.console.print("[red]Invalid choice.[/red]")
1193
- Prompt.ask("Press Enter to continue")
621
+ self.console.print("[red]Invalid choice. Please try again.[/red]")
622
+ Prompt.ask("\nPress Enter to continue")
1194
623
 
1195
624
  def _display_behavior_files(self) -> None:
1196
625
  """Display current behavior files."""
1197
- if self.current_scope == "project":
1198
- config_dir = self.project_dir / ".claude-mpm" / "behaviors"
1199
- else:
1200
- config_dir = Path.home() / ".claude-mpm" / "behaviors"
1201
-
1202
- config_dir.mkdir(parents=True, exist_ok=True)
1203
-
1204
- table = Table(title="Behavior Files", box=ROUNDED)
1205
- table.add_column("File", style="cyan", width=30)
1206
- table.add_column("Size", style="dim", width=10)
1207
- table.add_column("Modified", style="white", width=20)
1208
-
1209
- identity_file = config_dir / "identity.yaml"
1210
- workflow_file = config_dir / "workflow.yaml"
1211
-
1212
- for file_path in [identity_file, workflow_file]:
1213
- if file_path.exists():
1214
- stat = file_path.stat()
1215
- size = f"{stat.st_size} bytes"
1216
- modified = f"{stat.st_mtime:.0f}" # Simplified timestamp
1217
- table.add_row(file_path.name, size, modified)
1218
- else:
1219
- table.add_row(file_path.name, "[dim]Not found[/dim]", "-")
1220
-
1221
- self.console.print(table)
626
+ self.behavior_manager.display_behavior_files()
1222
627
 
1223
628
  def _edit_identity_config(self) -> None:
1224
629
  """Edit identity configuration."""
1225
- self.console.print(
1226
- "[yellow]Identity configuration editor - Coming soon![/yellow]"
1227
- )
1228
- Prompt.ask("Press Enter to continue")
630
+ self.behavior_manager.edit_identity_config()
1229
631
 
1230
632
  def _edit_workflow_config(self) -> None:
1231
633
  """Edit workflow configuration."""
1232
- self.console.print(
1233
- "[yellow]Workflow configuration editor - Coming soon![/yellow]"
1234
- )
1235
- Prompt.ask("Press Enter to continue")
634
+ self.behavior_manager.edit_workflow_config()
1236
635
 
1237
636
  def _import_behavior_file(self) -> None:
1238
637
  """Import a behavior file."""
1239
- file_path = Prompt.ask("Enter path to behavior file to import")
1240
-
1241
- try:
1242
- source = Path(file_path)
1243
- if not source.exists():
1244
- self.console.print(f"[red]File not found: {file_path}[/red]")
1245
- return
1246
-
1247
- # Determine target directory
1248
- if self.current_scope == "project":
1249
- config_dir = self.project_dir / ".claude-mpm" / "behaviors"
1250
- else:
1251
- config_dir = Path.home() / ".claude-mpm" / "behaviors"
1252
-
1253
- config_dir.mkdir(parents=True, exist_ok=True)
1254
-
1255
- # Copy file
1256
- import shutil
1257
-
1258
- target = config_dir / source.name
1259
- shutil.copy2(source, target)
1260
-
1261
- self.console.print(f"[green]Successfully imported {source.name}![/green]")
1262
-
1263
- except Exception as e:
1264
- self.console.print(f"[red]Error importing file: {e}[/red]")
1265
-
1266
- Prompt.ask("Press Enter to continue")
638
+ self.behavior_manager.import_behavior_file()
1267
639
 
1268
640
  def _export_behavior_file(self) -> None:
1269
641
  """Export a behavior file."""
1270
- self.console.print("[yellow]Behavior file export - Coming soon![/yellow]")
1271
- Prompt.ask("Press Enter to continue")
642
+ self.behavior_manager.export_behavior_file()
1272
643
 
1273
644
  def _manage_startup_configuration(self) -> bool:
1274
- """Manage startup configuration for MCP services and agents.
1275
-
1276
- Returns:
1277
- bool: True if user saved and wants to proceed to startup, False otherwise
1278
- """
1279
- # Temporarily suppress INFO logging during Config initialization
1280
- import logging
1281
-
1282
- root_logger = logging.getLogger("claude_mpm")
1283
- original_level = root_logger.level
1284
- root_logger.setLevel(logging.WARNING)
1285
-
1286
- try:
1287
- # Load current configuration ONCE at the start
1288
- config = Config()
1289
- startup_config = self._load_startup_configuration(config)
1290
- finally:
1291
- # Restore original logging level
1292
- root_logger.setLevel(original_level)
1293
-
1294
- proceed_to_startup = False
1295
- while True:
1296
- self.console.clear()
1297
- self._display_header()
1298
-
1299
- self.console.print("[bold]Startup Configuration Management[/bold]\n")
1300
- self.console.print(
1301
- "[dim]Configure which MCP services, hook services, and system agents "
1302
- "are enabled when Claude MPM starts.[/dim]\n"
1303
- )
1304
-
1305
- # Display current configuration (using in-memory state)
1306
- self._display_startup_configuration(startup_config)
1307
-
1308
- # Show menu options
1309
- self.console.print("\n[bold]Options:[/bold]")
1310
- self.console.print(" [cyan]1[/cyan] - Configure MCP Services")
1311
- self.console.print(" [cyan]2[/cyan] - Configure Hook Services")
1312
- self.console.print(" [cyan]3[/cyan] - Configure System Agents")
1313
- self.console.print(" [cyan]4[/cyan] - Enable All")
1314
- self.console.print(" [cyan]5[/cyan] - Disable All")
1315
- self.console.print(" [cyan]6[/cyan] - Reset to Defaults")
1316
- self.console.print(
1317
- " [cyan]s[/cyan] - Save configuration and start claude-mpm"
1318
- )
1319
- self.console.print(" [cyan]b[/cyan] - Cancel and return without saving")
1320
- self.console.print()
1321
-
1322
- choice = Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="s")
1323
-
1324
- if choice == "b":
1325
- break
1326
- if choice == "1":
1327
- self._configure_mcp_services(startup_config, config)
1328
- elif choice == "2":
1329
- self._configure_hook_services(startup_config, config)
1330
- elif choice == "3":
1331
- self._configure_system_agents(startup_config, config)
1332
- elif choice == "4":
1333
- self._enable_all_services(startup_config, config)
1334
- elif choice == "5":
1335
- self._disable_all_services(startup_config, config)
1336
- elif choice == "6":
1337
- self._reset_to_defaults(startup_config, config)
1338
- elif choice == "s":
1339
- # Save and exit if successful
1340
- if self._save_startup_configuration(startup_config, config):
1341
- proceed_to_startup = True
1342
- break
1343
- else:
1344
- self.console.print("[red]Invalid choice.[/red]")
1345
- Prompt.ask("Press Enter to continue")
1346
-
1347
- return proceed_to_startup
645
+ """Manage startup configuration for MCP services and agents."""
646
+ return self.startup_manager.manage_startup_configuration()
1348
647
 
1349
648
  def _load_startup_configuration(self, config: Config) -> Dict:
1350
649
  """Load current startup configuration from config."""
1351
- startup_config = config.get("startup", {})
1352
-
1353
- # Ensure all required sections exist
1354
- if "enabled_mcp_services" not in startup_config:
1355
- # Get available MCP services from MCPConfigManager
1356
- mcp_manager = MCPConfigManager()
1357
- available_services = list(mcp_manager.STATIC_MCP_CONFIGS.keys())
1358
- startup_config["enabled_mcp_services"] = available_services.copy()
1359
-
1360
- if "enabled_hook_services" not in startup_config:
1361
- # Default hook services (health-monitor enabled by default)
1362
- startup_config["enabled_hook_services"] = [
1363
- "monitor",
1364
- "dashboard",
1365
- "response-logger",
1366
- "health-monitor",
1367
- ]
1368
-
1369
- if "disabled_agents" not in startup_config:
1370
- # NEW LOGIC: Track DISABLED agents instead of enabled
1371
- # By default, NO agents are disabled (all agents enabled)
1372
- startup_config["disabled_agents"] = []
1373
-
1374
- return startup_config
650
+ return self.startup_manager.load_startup_configuration(config)
1375
651
 
1376
652
  def _display_startup_configuration(self, startup_config: Dict) -> None:
1377
653
  """Display current startup configuration in a table."""
1378
- table = Table(
1379
- title="Current Startup Configuration", box=ROUNDED, show_lines=True
1380
- )
1381
-
1382
- table.add_column("Category", style="cyan", width=20)
1383
- table.add_column("Enabled Services", style="white", width=50)
1384
- table.add_column("Count", style="dim", width=10)
1385
-
1386
- # MCP Services
1387
- mcp_services = startup_config.get("enabled_mcp_services", [])
1388
- mcp_display = ", ".join(mcp_services[:3]) + (
1389
- "..." if len(mcp_services) > 3 else ""
1390
- )
1391
- table.add_row(
1392
- "MCP Services",
1393
- mcp_display if mcp_services else "[dim]None[/dim]",
1394
- str(len(mcp_services)),
1395
- )
1396
-
1397
- # Hook Services
1398
- hook_services = startup_config.get("enabled_hook_services", [])
1399
- hook_display = ", ".join(hook_services[:3]) + (
1400
- "..." if len(hook_services) > 3 else ""
1401
- )
1402
- table.add_row(
1403
- "Hook Services",
1404
- hook_display if hook_services else "[dim]None[/dim]",
1405
- str(len(hook_services)),
1406
- )
1407
-
1408
- # System Agents - show count of ENABLED agents (total - disabled)
1409
- all_agents = self.agent_manager.discover_agents() if self.agent_manager else []
1410
- disabled_agents = startup_config.get("disabled_agents", [])
1411
- enabled_count = len(all_agents) - len(disabled_agents)
1412
-
1413
- # Show first few enabled agent names
1414
- enabled_names = [a.name for a in all_agents if a.name not in disabled_agents]
1415
- agent_display = ", ".join(enabled_names[:3]) + (
1416
- "..." if len(enabled_names) > 3 else ""
1417
- )
1418
- table.add_row(
1419
- "System Agents",
1420
- agent_display if enabled_names else "[dim]All Disabled[/dim]",
1421
- f"{enabled_count}/{len(all_agents)}",
1422
- )
1423
-
1424
- self.console.print(table)
654
+ self.startup_manager.display_startup_configuration(startup_config)
1425
655
 
1426
656
  def _configure_mcp_services(self, startup_config: Dict, config: Config) -> None:
1427
657
  """Configure which MCP services to enable at startup."""
1428
- self.console.clear()
1429
- self._display_header()
1430
- self.console.print("[bold]Configure MCP Services[/bold]\n")
1431
-
1432
- # Get available MCP services
1433
- mcp_manager = MCPConfigManager()
1434
- available_services = list(mcp_manager.STATIC_MCP_CONFIGS.keys())
1435
- enabled_services = set(startup_config.get("enabled_mcp_services", []))
1436
-
1437
- # Display services with checkboxes
1438
- table = Table(box=ROUNDED, show_lines=True)
1439
- table.add_column("ID", style="dim", width=5)
1440
- table.add_column("Service", style="cyan", width=25)
1441
- table.add_column("Status", width=15)
1442
- table.add_column("Description", style="white", width=45)
1443
-
1444
- service_descriptions = {
1445
- "kuzu-memory": "Graph-based memory system for agents",
1446
- "mcp-ticketer": "Ticket and issue tracking integration",
1447
- "mcp-browser": "Browser automation and web scraping",
1448
- "mcp-vector-search": "Semantic code search capabilities",
1449
- }
1450
-
1451
- for idx, service in enumerate(available_services, 1):
1452
- status = (
1453
- "[green]✓ Enabled[/green]"
1454
- if service in enabled_services
1455
- else "[red]✗ Disabled[/red]"
1456
- )
1457
- description = service_descriptions.get(service, "MCP service")
1458
- table.add_row(str(idx), service, status, description)
1459
-
1460
- self.console.print(table)
1461
- self.console.print("\n[bold]Commands:[/bold]")
1462
- self.console.print(" Enter service IDs to toggle (e.g., '1,3' or '1-4')")
1463
-
1464
- text_a = Text(" ")
1465
- text_a.append("[a]", style="cyan bold")
1466
- text_a.append(" Enable all")
1467
- self.console.print(text_a)
1468
-
1469
- text_n = Text(" ")
1470
- text_n.append("[n]", style="cyan bold")
1471
- text_n.append(" Disable all")
1472
- self.console.print(text_n)
1473
-
1474
- text_b = Text(" ")
1475
- text_b.append("[b]", style="cyan bold")
1476
- text_b.append(" Back to previous menu")
1477
- self.console.print(text_b)
1478
-
1479
- self.console.print()
1480
-
1481
- choice = Prompt.ask("[bold cyan]Toggle services[/bold cyan]", default="b")
1482
-
1483
- if choice == "b":
1484
- return
1485
- if choice == "a":
1486
- startup_config["enabled_mcp_services"] = available_services.copy()
1487
- self.console.print("[green]All MCP services enabled![/green]")
1488
- elif choice == "n":
1489
- startup_config["enabled_mcp_services"] = []
1490
- self.console.print("[green]All MCP services disabled![/green]")
1491
- else:
1492
- # Parse service IDs
1493
- try:
1494
- selected_ids = self._parse_id_selection(choice, len(available_services))
1495
- for idx in selected_ids:
1496
- service = available_services[idx - 1]
1497
- if service in enabled_services:
1498
- enabled_services.remove(service)
1499
- self.console.print(f"[red]Disabled {service}[/red]")
1500
- else:
1501
- enabled_services.add(service)
1502
- self.console.print(f"[green]Enabled {service}[/green]")
1503
- startup_config["enabled_mcp_services"] = list(enabled_services)
1504
- except (ValueError, IndexError) as e:
1505
- self.console.print(f"[red]Invalid selection: {e}[/red]")
1506
-
1507
- Prompt.ask("Press Enter to continue")
658
+ self.startup_manager.configure_mcp_services(startup_config, config)
1508
659
 
1509
660
  def _configure_hook_services(self, startup_config: Dict, config: Config) -> None:
1510
661
  """Configure which hook services to enable at startup."""
1511
- self.console.clear()
1512
- self._display_header()
1513
- self.console.print("[bold]Configure Hook Services[/bold]\n")
1514
-
1515
- # Available hook services
1516
- available_services = [
1517
- ("monitor", "Real-time event monitoring server (SocketIO)"),
1518
- ("dashboard", "Web-based dashboard interface"),
1519
- ("response-logger", "Agent response logging"),
1520
- ("health-monitor", "Service health and recovery monitoring"),
1521
- ]
1522
-
1523
- enabled_services = set(startup_config.get("enabled_hook_services", []))
1524
-
1525
- # Display services with checkboxes
1526
- table = Table(box=ROUNDED, show_lines=True)
1527
- table.add_column("ID", style="dim", width=5)
1528
- table.add_column("Service", style="cyan", width=25)
1529
- table.add_column("Status", width=15)
1530
- table.add_column("Description", style="white", width=45)
1531
-
1532
- for idx, (service, description) in enumerate(available_services, 1):
1533
- status = (
1534
- "[green]✓ Enabled[/green]"
1535
- if service in enabled_services
1536
- else "[red]✗ Disabled[/red]"
1537
- )
1538
- table.add_row(str(idx), service, status, description)
1539
-
1540
- self.console.print(table)
1541
- self.console.print("\n[bold]Commands:[/bold]")
1542
- self.console.print(" Enter service IDs to toggle (e.g., '1,3' or '1-4')")
1543
-
1544
- text_a = Text(" ")
1545
- text_a.append("[a]", style="cyan bold")
1546
- text_a.append(" Enable all")
1547
- self.console.print(text_a)
1548
-
1549
- text_n = Text(" ")
1550
- text_n.append("[n]", style="cyan bold")
1551
- text_n.append(" Disable all")
1552
- self.console.print(text_n)
1553
-
1554
- text_b = Text(" ")
1555
- text_b.append("[b]", style="cyan bold")
1556
- text_b.append(" Back to previous menu")
1557
- self.console.print(text_b)
1558
-
1559
- self.console.print()
1560
-
1561
- choice = Prompt.ask("[bold cyan]Toggle services[/bold cyan]", default="b")
1562
-
1563
- if choice == "b":
1564
- return
1565
- if choice == "a":
1566
- startup_config["enabled_hook_services"] = [s[0] for s in available_services]
1567
- self.console.print("[green]All hook services enabled![/green]")
1568
- elif choice == "n":
1569
- startup_config["enabled_hook_services"] = []
1570
- self.console.print("[green]All hook services disabled![/green]")
1571
- else:
1572
- # Parse service IDs
1573
- try:
1574
- selected_ids = self._parse_id_selection(choice, len(available_services))
1575
- for idx in selected_ids:
1576
- service = available_services[idx - 1][0]
1577
- if service in enabled_services:
1578
- enabled_services.remove(service)
1579
- self.console.print(f"[red]Disabled {service}[/red]")
1580
- else:
1581
- enabled_services.add(service)
1582
- self.console.print(f"[green]Enabled {service}[/green]")
1583
- startup_config["enabled_hook_services"] = list(enabled_services)
1584
- except (ValueError, IndexError) as e:
1585
- self.console.print(f"[red]Invalid selection: {e}[/red]")
1586
-
1587
- Prompt.ask("Press Enter to continue")
662
+ self.startup_manager.configure_hook_services(startup_config, config)
1588
663
 
1589
664
  def _configure_system_agents(self, startup_config: Dict, config: Config) -> None:
1590
- """Configure which system agents to deploy at startup.
1591
-
1592
- NEW LOGIC: Uses disabled_agents list. All agents from templates are enabled by default.
1593
- """
1594
- while True:
1595
- self.console.clear()
1596
- self._display_header()
1597
- self.console.print("[bold]Configure System Agents[/bold]\n")
1598
- self.console.print(
1599
- "[dim]All agents discovered from templates are enabled by default. "
1600
- "Mark agents as disabled to prevent deployment.[/dim]\n"
1601
- )
1602
-
1603
- # Discover available agents from template files
1604
- agents = self.agent_manager.discover_agents()
1605
- disabled_agents = set(startup_config.get("disabled_agents", []))
1606
-
1607
- # Display agents with checkboxes
1608
- table = Table(box=ROUNDED, show_lines=True)
1609
- table.add_column("ID", style="dim", width=5)
1610
- table.add_column("Agent", style="cyan", width=25)
1611
- table.add_column("Status", width=15)
1612
- table.add_column("Description", style="bold cyan", width=45)
1613
-
1614
- for idx, agent in enumerate(agents, 1):
1615
- # Agent is ENABLED if NOT in disabled list
1616
- is_enabled = agent.name not in disabled_agents
1617
- status = (
1618
- "[green]✓ Enabled[/green]"
1619
- if is_enabled
1620
- else "[red]✗ Disabled[/red]"
1621
- )
1622
- # Format description with bright styling
1623
- if len(agent.description) > 42:
1624
- desc_display = (
1625
- f"[cyan]{agent.description[:42]}[/cyan][dim]...[/dim]"
1626
- )
1627
- else:
1628
- desc_display = f"[cyan]{agent.description}[/cyan]"
1629
- table.add_row(str(idx), agent.name, status, desc_display)
1630
-
1631
- self.console.print(table)
1632
- self.console.print("\n[bold]Commands:[/bold]")
1633
- self.console.print(" Enter agent IDs to toggle (e.g., '1,3' or '1-4')")
1634
- self.console.print(" [cyan]a[/cyan] - Enable all (clear disabled list)")
1635
- self.console.print(" [cyan]n[/cyan] - Disable all")
1636
- self.console.print(" [cyan]b[/cyan] - Back to previous menu")
1637
- self.console.print()
1638
-
1639
- choice = Prompt.ask("[bold cyan]Select option[/bold cyan]", default="b")
1640
-
1641
- if choice == "b":
1642
- return
1643
- if choice == "a":
1644
- # Enable all = empty disabled list
1645
- startup_config["disabled_agents"] = []
1646
- self.console.print("[green]All agents enabled![/green]")
1647
- Prompt.ask("Press Enter to continue")
1648
- elif choice == "n":
1649
- # Disable all = all agents in disabled list
1650
- startup_config["disabled_agents"] = [agent.name for agent in agents]
1651
- self.console.print("[green]All agents disabled![/green]")
1652
- Prompt.ask("Press Enter to continue")
1653
- else:
1654
- # Parse agent IDs
1655
- try:
1656
- selected_ids = self._parse_id_selection(choice, len(agents))
1657
- for idx in selected_ids:
1658
- agent = agents[idx - 1]
1659
- if agent.name in disabled_agents:
1660
- # Currently disabled, enable it (remove from disabled list)
1661
- disabled_agents.remove(agent.name)
1662
- self.console.print(f"[green]Enabled {agent.name}[/green]")
1663
- else:
1664
- # Currently enabled, disable it (add to disabled list)
1665
- disabled_agents.add(agent.name)
1666
- self.console.print(f"[red]Disabled {agent.name}[/red]")
1667
- startup_config["disabled_agents"] = list(disabled_agents)
1668
- # Refresh the display to show updated status immediately
1669
- except (ValueError, IndexError) as e:
1670
- self.console.print(f"[red]Invalid selection: {e}[/red]")
1671
- Prompt.ask("Press Enter to continue")
665
+ """Configure which system agents to deploy at startup."""
666
+ self.startup_manager.configure_system_agents(startup_config, config)
1672
667
 
1673
668
  def _parse_id_selection(self, selection: str, max_id: int) -> List[int]:
1674
669
  """Parse ID selection string (e.g., '1,3,5' or '1-4')."""
1675
- ids = set()
1676
- parts = selection.split(",")
1677
-
1678
- for part in parts:
1679
- part = part.strip()
1680
- if "-" in part:
1681
- # Range selection
1682
- start, end = part.split("-")
1683
- start_id = int(start.strip())
1684
- end_id = int(end.strip())
1685
- if start_id < 1 or end_id > max_id or start_id > end_id:
1686
- raise ValueError(f"Invalid range: {part}")
1687
- ids.update(range(start_id, end_id + 1))
1688
- else:
1689
- # Single ID
1690
- id_num = int(part)
1691
- if id_num < 1 or id_num > max_id:
1692
- raise ValueError(f"Invalid ID: {id_num}")
1693
- ids.add(id_num)
1694
-
1695
- return sorted(ids)
670
+ return parse_id_selection(selection, max_id)
1696
671
 
1697
672
  def _enable_all_services(self, startup_config: Dict, config: Config) -> None:
1698
673
  """Enable all services and agents."""
1699
- if Confirm.ask("[yellow]Enable ALL services and agents?[/yellow]"):
1700
- # Enable all MCP services
1701
- mcp_manager = MCPConfigManager()
1702
- startup_config["enabled_mcp_services"] = list(
1703
- mcp_manager.STATIC_MCP_CONFIGS.keys()
1704
- )
1705
-
1706
- # Enable all hook services
1707
- startup_config["enabled_hook_services"] = [
1708
- "monitor",
1709
- "dashboard",
1710
- "response-logger",
1711
- "health-monitor",
1712
- ]
1713
-
1714
- # Enable all agents (empty disabled list)
1715
- startup_config["disabled_agents"] = []
1716
-
1717
- self.console.print("[green]All services and agents enabled![/green]")
1718
- Prompt.ask("Press Enter to continue")
674
+ self.startup_manager.enable_all_services(startup_config, config)
1719
675
 
1720
676
  def _disable_all_services(self, startup_config: Dict, config: Config) -> None:
1721
677
  """Disable all services and agents."""
1722
- if Confirm.ask("[yellow]Disable ALL services and agents?[/yellow]"):
1723
- startup_config["enabled_mcp_services"] = []
1724
- startup_config["enabled_hook_services"] = []
1725
- # Disable all agents = add all to disabled list
1726
- agents = self.agent_manager.discover_agents()
1727
- startup_config["disabled_agents"] = [agent.name for agent in agents]
1728
-
1729
- self.console.print("[green]All services and agents disabled![/green]")
1730
- self.console.print(
1731
- "[yellow]Note: You may need to enable at least some services for Claude MPM to function properly.[/yellow]"
1732
- )
1733
- Prompt.ask("Press Enter to continue")
678
+ self.startup_manager.disable_all_services(startup_config, config)
1734
679
 
1735
680
  def _reset_to_defaults(self, startup_config: Dict, config: Config) -> None:
1736
681
  """Reset startup configuration to defaults."""
1737
- if Confirm.ask("[yellow]Reset startup configuration to defaults?[/yellow]"):
1738
- # Reset to default values
1739
- mcp_manager = MCPConfigManager()
1740
- startup_config["enabled_mcp_services"] = list(
1741
- mcp_manager.STATIC_MCP_CONFIGS.keys()
1742
- )
1743
- startup_config["enabled_hook_services"] = [
1744
- "monitor",
1745
- "dashboard",
1746
- "response-logger",
1747
- "health-monitor",
1748
- ]
1749
- # Default: All agents enabled (empty disabled list)
1750
- startup_config["disabled_agents"] = []
1751
-
1752
- self.console.print(
1753
- "[green]Startup configuration reset to defaults![/green]"
1754
- )
1755
- Prompt.ask("Press Enter to continue")
682
+ self.startup_manager.reset_to_defaults(startup_config, config)
1756
683
 
1757
684
  def _save_startup_configuration(self, startup_config: Dict, config: Config) -> bool:
1758
- """Save startup configuration to config file and return whether to proceed to startup.
1759
-
1760
- Returns:
1761
- bool: True if should proceed to startup, False to continue in menu
1762
- """
1763
- try:
1764
- # Update the startup configuration
1765
- config.set("startup", startup_config)
1766
-
1767
- # IMPORTANT: Also update agent_deployment.disabled_agents so the deployment
1768
- # system actually uses the configured disabled agents list
1769
- config.set(
1770
- "agent_deployment.disabled_agents",
1771
- startup_config.get("disabled_agents", []),
1772
- )
1773
-
1774
- # Determine config file path
1775
- if self.current_scope == "project":
1776
- config_file = self.project_dir / ".claude-mpm" / "configuration.yaml"
1777
- else:
1778
- config_file = Path.home() / ".claude-mpm" / "configuration.yaml"
1779
-
1780
- # Ensure directory exists
1781
- config_file.parent.mkdir(parents=True, exist_ok=True)
1782
-
1783
- # Temporarily suppress INFO logging to avoid duplicate save messages
1784
- import logging
1785
-
1786
- root_logger = logging.getLogger("claude_mpm")
1787
- original_level = root_logger.level
1788
- root_logger.setLevel(logging.WARNING)
1789
-
1790
- try:
1791
- # Save configuration (this will log at INFO level which we've suppressed)
1792
- config.save(config_file, format="yaml")
1793
- finally:
1794
- # Restore original logging level
1795
- root_logger.setLevel(original_level)
1796
-
1797
- self.console.print(
1798
- f"[green]✓ Startup configuration saved to {config_file}[/green]"
1799
- )
1800
- self.console.print(
1801
- "\n[cyan]Applying configuration and launching Claude MPM...[/cyan]\n"
1802
- )
1803
-
1804
- # Launch claude-mpm run command to get full startup cycle
1805
- # This ensures:
1806
- # 1. Configuration is loaded
1807
- # 2. Enabled agents are deployed
1808
- # 3. Disabled agents are removed from .claude/agents/
1809
- # 4. MCP services and hooks are started
1810
- try:
1811
- # Use execvp to replace the current process with claude-mpm run
1812
- # This ensures a clean transition from configurator to Claude MPM
1813
- os.execvp("claude-mpm", ["claude-mpm", "run"])
1814
- except Exception as e:
1815
- self.console.print(
1816
- f"[yellow]Could not launch Claude MPM automatically: {e}[/yellow]"
1817
- )
1818
- self.console.print(
1819
- "[cyan]Please run 'claude-mpm' manually to start.[/cyan]"
1820
- )
1821
- Prompt.ask("Press Enter to continue")
1822
- return True
1823
-
1824
- # This line will never be reached if execvp succeeds
1825
- return True
1826
-
1827
- except Exception as e:
1828
- self.console.print(f"[red]Error saving configuration: {e}[/red]")
1829
- Prompt.ask("Press Enter to continue")
1830
- return False
685
+ """Save startup configuration to config file and return whether to proceed to startup."""
686
+ return self.startup_manager.save_startup_configuration(startup_config, config)
1831
687
 
1832
688
  def _save_all_configuration(self) -> bool:
1833
- """Save all configuration changes across all contexts.
1834
-
1835
- Returns:
1836
- bool: True if all saves successful, False otherwise
1837
- """
1838
- try:
1839
- # 1. Save any pending agent changes
1840
- if self.agent_manager and self.agent_manager.has_pending_changes():
1841
- self.agent_manager.commit_deferred_changes()
1842
- self.console.print("[green]✓ Agent changes saved[/green]")
1843
-
1844
- # 2. Save configuration file
1845
- config = Config()
1846
-
1847
- # Determine config file path based on scope
1848
- if self.current_scope == "project":
1849
- config_file = self.project_dir / ".claude-mpm" / "configuration.yaml"
1850
- else:
1851
- config_file = Path.home() / ".claude-mpm" / "configuration.yaml"
1852
-
1853
- config_file.parent.mkdir(parents=True, exist_ok=True)
1854
-
1855
- # Save with suppressed logging to avoid duplicate messages
1856
- import logging
1857
-
1858
- root_logger = logging.getLogger("claude_mpm")
1859
- original_level = root_logger.level
1860
- root_logger.setLevel(logging.WARNING)
1861
-
1862
- try:
1863
- config.save(config_file, format="yaml")
1864
- finally:
1865
- root_logger.setLevel(original_level)
1866
-
1867
- self.console.print(f"[green]✓ Configuration saved to {config_file}[/green]")
1868
- return True
1869
-
1870
- except Exception as e:
1871
- self.console.print(f"[red]✗ Error saving configuration: {e}[/red]")
1872
- import traceback
1873
-
1874
- traceback.print_exc()
1875
- return False
689
+ """Save all configuration changes across all contexts."""
690
+ return self.startup_manager.save_all_configuration()
1876
691
 
1877
692
  def _launch_claude_mpm(self) -> None:
1878
693
  """Launch Claude MPM run command, replacing current process."""
1879
- self.console.print("\n[bold cyan]═══ Launching Claude MPM ═══[/bold cyan]\n")
1880
-
1881
- try:
1882
- # Use execvp to replace the current process with claude-mpm run
1883
- # This ensures a clean transition from configurator to Claude MPM
1884
- os.execvp("claude-mpm", ["claude-mpm", "run"])
1885
- except Exception as e:
1886
- self.console.print(
1887
- f"[yellow]⚠ Could not launch Claude MPM automatically: {e}[/yellow]"
1888
- )
1889
- self.console.print(
1890
- "[cyan]→ Please run 'claude-mpm run' manually to start.[/cyan]"
1891
- )
1892
- Prompt.ask("\nPress Enter to exit")
694
+ self.navigation.launch_claude_mpm()
1893
695
 
1894
696
  def _switch_scope(self) -> None:
1895
697
  """Switch between project and user scope."""
1896
- self.current_scope = "user" if self.current_scope == "project" else "project"
1897
- self.console.print(f"[green]Switched to {self.current_scope} scope[/green]")
1898
- Prompt.ask("Press Enter to continue")
698
+ self.navigation.switch_scope()
699
+ # Sync scope back from navigation
700
+ self.current_scope = self.navigation.current_scope
1899
701
 
1900
702
  def _show_version_info_interactive(self) -> None:
1901
703
  """Show version information in interactive mode."""
1902
- self.console.clear()
1903
- self._display_header()
1904
-
1905
- # Get version information
1906
- mpm_version = self.version_service.get_version()
1907
- build_number = self.version_service.get_build_number()
1908
-
1909
- # Try to get Claude Code version using the installer's method
1910
- claude_version = "Unknown"
1911
- try:
1912
- from ...hooks.claude_hooks.installer import HookInstaller
1913
-
1914
- installer = HookInstaller()
1915
- detected_version = installer.get_claude_version()
1916
- if detected_version:
1917
- is_compatible, _ = installer.is_version_compatible()
1918
- claude_version = f"{detected_version} (Claude Code)"
1919
- if not is_compatible:
1920
- claude_version += (
1921
- f" - Monitoring requires {installer.MIN_CLAUDE_VERSION}+"
1922
- )
1923
- else:
1924
- # Fallback to direct subprocess call
1925
- import subprocess
1926
-
1927
- result = subprocess.run(
1928
- ["claude", "--version"],
1929
- capture_output=True,
1930
- text=True,
1931
- timeout=5,
1932
- check=False,
1933
- )
1934
- if result.returncode == 0:
1935
- claude_version = result.stdout.strip()
1936
- except Exception:
1937
- pass
1938
-
1939
- # Create version panel
1940
- version_text = f"""
1941
- [bold cyan]Claude MPM[/bold cyan]
1942
- Version: {mpm_version}
1943
- Build: {build_number}
1944
-
1945
- [bold cyan]Claude Code[/bold cyan]
1946
- Version: {claude_version}
1947
-
1948
- [bold cyan]Python[/bold cyan]
1949
- Version: {sys.version.split()[0]}
1950
-
1951
- [bold cyan]Configuration[/bold cyan]
1952
- Scope: {self.current_scope}
1953
- Directory: {self.project_dir}
1954
- """
1955
-
1956
- panel = Panel(
1957
- version_text.strip(),
1958
- title="[bold]Version Information[/bold]",
1959
- box=ROUNDED,
1960
- style="green",
1961
- )
1962
-
1963
- self.console.print(panel)
1964
- Prompt.ask("\nPress Enter to continue")
704
+ self.persistence.show_version_info_interactive()
1965
705
 
1966
706
  # Non-interactive command methods
1967
707
 
@@ -2003,261 +743,33 @@ Directory: {self.project_dir}
2003
743
 
2004
744
  def _export_config(self, file_path: str) -> CommandResult:
2005
745
  """Export configuration to a file."""
2006
- try:
2007
- # Gather all configuration
2008
- config_data = {"scope": self.current_scope, "agents": {}, "behaviors": {}}
2009
-
2010
- # Get agent states
2011
- agents = self.agent_manager.discover_agents()
2012
- for agent in agents:
2013
- config_data["agents"][agent.name] = {
2014
- "enabled": self.agent_manager.is_agent_enabled(agent.name),
2015
- "template_path": str(self._get_agent_template_path(agent.name)),
2016
- }
2017
-
2018
- # Write to file
2019
- output_path = Path(file_path)
2020
- with output_path.open("w") as f:
2021
- json.dump(config_data, f, indent=2)
2022
-
2023
- return CommandResult.success_result(
2024
- f"Configuration exported to {output_path}"
2025
- )
2026
-
2027
- except Exception as e:
2028
- return CommandResult.error_result(f"Failed to export configuration: {e}")
746
+ return self.persistence.export_config(file_path)
2029
747
 
2030
748
  def _import_config(self, file_path: str) -> CommandResult:
2031
749
  """Import configuration from a file."""
2032
- try:
2033
- input_path = Path(file_path)
2034
- if not input_path.exists():
2035
- return CommandResult.error_result(f"File not found: {file_path}")
2036
-
2037
- with input_path.open() as f:
2038
- config_data = json.load(f)
2039
-
2040
- # Apply agent states
2041
- if "agents" in config_data:
2042
- for agent_name, agent_config in config_data["agents"].items():
2043
- if "enabled" in agent_config:
2044
- self.agent_manager.set_agent_enabled(
2045
- agent_name, agent_config["enabled"]
2046
- )
2047
-
2048
- return CommandResult.success_result(
2049
- f"Configuration imported from {input_path}"
2050
- )
2051
-
2052
- except Exception as e:
2053
- return CommandResult.error_result(f"Failed to import configuration: {e}")
750
+ return self.persistence.import_config(file_path)
2054
751
 
2055
752
  def _show_version_info(self) -> CommandResult:
2056
753
  """Show version information in non-interactive mode."""
2057
- mpm_version = self.version_service.get_version()
2058
- build_number = self.version_service.get_build_number()
2059
-
2060
- data = {
2061
- "mpm_version": mpm_version,
2062
- "build_number": build_number,
2063
- "python_version": sys.version.split()[0],
2064
- }
2065
-
2066
- # Try to get Claude version
2067
- try:
2068
- import subprocess
2069
-
2070
- result = subprocess.run(
2071
- ["claude", "--version"],
2072
- capture_output=True,
2073
- text=True,
2074
- timeout=5,
2075
- check=False,
2076
- )
2077
- if result.returncode == 0:
2078
- data["claude_version"] = result.stdout.strip()
2079
- except Exception:
2080
- data["claude_version"] = "Unknown"
2081
-
2082
- # Print formatted output
2083
- self.console.print(
2084
- f"[bold]Claude MPM:[/bold] {mpm_version} (build {build_number})"
2085
- )
2086
- self.console.print(
2087
- f"[bold]Claude Code:[/bold] {data.get('claude_version', 'Unknown')}"
2088
- )
2089
- self.console.print(f"[bold]Python:[/bold] {data['python_version']}")
2090
-
2091
- return CommandResult.success_result("Version information displayed", data=data)
754
+ return self.persistence.show_version_info()
2092
755
 
2093
756
  def _install_hooks(self, force: bool = False) -> CommandResult:
2094
757
  """Install Claude MPM hooks for Claude Code integration."""
2095
- try:
2096
- from ...hooks.claude_hooks.installer import HookInstaller
2097
-
2098
- installer = HookInstaller()
2099
-
2100
- # Check Claude Code version compatibility first
2101
- is_compatible, version_message = installer.is_version_compatible()
2102
- self.console.print("[cyan]Checking Claude Code version...[/cyan]")
2103
- self.console.print(version_message)
2104
-
2105
- if not is_compatible:
2106
- self.console.print(
2107
- "\n[yellow]⚠ Hook monitoring is not available for your Claude Code version.[/yellow]"
2108
- )
2109
- self.console.print(
2110
- "The dashboard and other features will work without real-time monitoring."
2111
- )
2112
- self.console.print(
2113
- f"\n[dim]To enable monitoring, upgrade Claude Code to version {installer.MIN_CLAUDE_VERSION} or higher.[/dim]"
2114
- )
2115
- return CommandResult.success_result(
2116
- "Version incompatible with hook monitoring",
2117
- data={"compatible": False, "message": version_message},
2118
- )
2119
-
2120
- # Check current status
2121
- status = installer.get_status()
2122
- if status["installed"] and not force:
2123
- self.console.print("[yellow]Hooks are already installed.[/yellow]")
2124
- self.console.print("Use --force to reinstall.")
2125
-
2126
- if not status["valid"]:
2127
- self.console.print("\n[red]However, there are issues:[/red]")
2128
- for issue in status["issues"]:
2129
- self.console.print(f" - {issue}")
2130
-
2131
- return CommandResult.success_result(
2132
- "Hooks already installed", data=status
2133
- )
2134
-
2135
- # Install hooks
2136
- self.console.print("[cyan]Installing Claude MPM hooks...[/cyan]")
2137
- success = installer.install_hooks(force=force)
2138
-
2139
- if success:
2140
- self.console.print("[green]✓ Hooks installed successfully![/green]")
2141
- self.console.print("\nYou can now use /mpm commands in Claude Code:")
2142
- self.console.print(" /mpm - Show help")
2143
- self.console.print(" /mpm status - Show claude-mpm status")
2144
-
2145
- # Verify installation
2146
- is_valid, issues = installer.verify_hooks()
2147
- if not is_valid:
2148
- self.console.print(
2149
- "\n[yellow]Warning: Installation completed but verification found issues:[/yellow]"
2150
- )
2151
- for issue in issues:
2152
- self.console.print(f" - {issue}")
2153
-
2154
- return CommandResult.success_result("Hooks installed successfully")
2155
- self.console.print("[red]✗ Hook installation failed[/red]")
2156
- return CommandResult.error_result("Hook installation failed")
2157
-
2158
- except ImportError:
2159
- self.console.print("[red]Error: HookInstaller module not found[/red]")
2160
- self.console.print("Please ensure claude-mpm is properly installed.")
2161
- return CommandResult.error_result("HookInstaller module not found")
2162
- except Exception as e:
2163
- self.logger.error(f"Hook installation error: {e}", exc_info=True)
2164
- return CommandResult.error_result(f"Hook installation failed: {e}")
758
+ # Share logger with hook manager for consistent error logging
759
+ self.hook_manager.logger = self.logger
760
+ return self.hook_manager.install_hooks(force=force)
2165
761
 
2166
762
  def _verify_hooks(self) -> CommandResult:
2167
763
  """Verify that Claude MPM hooks are properly installed."""
2168
- try:
2169
- from ...hooks.claude_hooks.installer import HookInstaller
2170
-
2171
- installer = HookInstaller()
2172
- status = installer.get_status()
2173
-
2174
- self.console.print("[bold]Hook Installation Status[/bold]\n")
2175
-
2176
- # Show Claude Code version and compatibility
2177
- if status.get("claude_version"):
2178
- self.console.print(f"Claude Code Version: {status['claude_version']}")
2179
- if status.get("version_compatible"):
2180
- self.console.print(
2181
- "[green]✓[/green] Version compatible with hook monitoring"
2182
- )
2183
- else:
2184
- self.console.print(
2185
- f"[yellow]⚠[/yellow] {status.get('version_message', 'Version incompatible')}"
2186
- )
2187
- self.console.print()
2188
- else:
2189
- self.console.print(
2190
- "[yellow]Claude Code version could not be detected[/yellow]"
2191
- )
2192
- self.console.print()
2193
-
2194
- if status["installed"]:
2195
- self.console.print(
2196
- f"[green]✓[/green] Hooks installed at: {status['hook_script']}"
2197
- )
2198
- else:
2199
- self.console.print("[red]✗[/red] Hooks not installed")
2200
-
2201
- if status["settings_file"]:
2202
- self.console.print(
2203
- f"[green]✓[/green] Settings file: {status['settings_file']}"
2204
- )
2205
- else:
2206
- self.console.print("[red]✗[/red] Settings file not found")
2207
-
2208
- if status.get("configured_events"):
2209
- self.console.print(
2210
- f"[green]✓[/green] Configured events: {', '.join(status['configured_events'])}"
2211
- )
2212
- else:
2213
- self.console.print("[red]✗[/red] No events configured")
2214
-
2215
- if status["valid"]:
2216
- self.console.print("\n[green]All checks passed![/green]")
2217
- else:
2218
- self.console.print("\n[red]Issues found:[/red]")
2219
- for issue in status["issues"]:
2220
- self.console.print(f" - {issue}")
2221
-
2222
- return CommandResult.success_result(
2223
- "Hook verification complete", data=status
2224
- )
2225
-
2226
- except ImportError:
2227
- self.console.print("[red]Error: HookInstaller module not found[/red]")
2228
- return CommandResult.error_result("HookInstaller module not found")
2229
- except Exception as e:
2230
- self.logger.error(f"Hook verification error: {e}", exc_info=True)
2231
- return CommandResult.error_result(f"Hook verification failed: {e}")
764
+ # Share logger with hook manager for consistent error logging
765
+ self.hook_manager.logger = self.logger
766
+ return self.hook_manager.verify_hooks()
2232
767
 
2233
768
  def _uninstall_hooks(self) -> CommandResult:
2234
769
  """Uninstall Claude MPM hooks."""
2235
- try:
2236
- from ...hooks.claude_hooks.installer import HookInstaller
2237
-
2238
- installer = HookInstaller()
2239
-
2240
- # Confirm uninstallation
2241
- if not Confirm.ask(
2242
- "[yellow]Are you sure you want to uninstall Claude MPM hooks?[/yellow]"
2243
- ):
2244
- return CommandResult.success_result("Uninstallation cancelled")
2245
-
2246
- self.console.print("[cyan]Uninstalling Claude MPM hooks...[/cyan]")
2247
- success = installer.uninstall_hooks()
2248
-
2249
- if success:
2250
- self.console.print("[green]✓ Hooks uninstalled successfully![/green]")
2251
- return CommandResult.success_result("Hooks uninstalled successfully")
2252
- self.console.print("[red]✗ Hook uninstallation failed[/red]")
2253
- return CommandResult.error_result("Hook uninstallation failed")
2254
-
2255
- except ImportError:
2256
- self.console.print("[red]Error: HookInstaller module not found[/red]")
2257
- return CommandResult.error_result("HookInstaller module not found")
2258
- except Exception as e:
2259
- self.logger.error(f"Hook uninstallation error: {e}", exc_info=True)
2260
- return CommandResult.error_result(f"Hook uninstallation failed: {e}")
770
+ # Share logger with hook manager for consistent error logging
771
+ self.hook_manager.logger = self.logger
772
+ return self.hook_manager.uninstall_hooks()
2261
773
 
2262
774
  def _run_agent_management(self) -> CommandResult:
2263
775
  """Jump directly to agent management."""
@@ -2281,13 +793,7 @@ Directory: {self.project_dir}
2281
793
 
2282
794
  def _run_behavior_management(self) -> CommandResult:
2283
795
  """Jump directly to behavior management."""
2284
- try:
2285
- self._manage_behaviors()
2286
- return CommandResult.success_result("Behavior management completed")
2287
- except KeyboardInterrupt:
2288
- return CommandResult.success_result("Behavior management cancelled")
2289
- except Exception as e:
2290
- return CommandResult.error_result(f"Behavior management failed: {e}")
796
+ return self.behavior_manager.run_behavior_management()
2291
797
 
2292
798
  def _run_startup_configuration(self) -> CommandResult:
2293
799
  """Jump directly to startup configuration."""