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