claude-mpm 4.14.7__py3-none-any.whl → 4.14.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

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