claude-mpm 4.14.7__py3-none-any.whl → 4.14.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/agent_loader.py +13 -1
- claude_mpm/agents/frontmatter_validator.py +284 -253
- claude_mpm/cli/__init__.py +34 -740
- claude_mpm/cli/commands/agent_manager.py +25 -12
- claude_mpm/cli/commands/agent_state_manager.py +186 -0
- claude_mpm/cli/commands/agents.py +204 -148
- claude_mpm/cli/commands/aggregate.py +7 -3
- claude_mpm/cli/commands/analyze.py +9 -4
- claude_mpm/cli/commands/analyze_code.py +7 -2
- claude_mpm/cli/commands/config.py +47 -13
- claude_mpm/cli/commands/configure.py +159 -1801
- claude_mpm/cli/commands/configure_agent_display.py +261 -0
- claude_mpm/cli/commands/configure_behavior_manager.py +204 -0
- claude_mpm/cli/commands/configure_hook_manager.py +225 -0
- claude_mpm/cli/commands/configure_models.py +18 -0
- claude_mpm/cli/commands/configure_navigation.py +165 -0
- claude_mpm/cli/commands/configure_paths.py +104 -0
- claude_mpm/cli/commands/configure_persistence.py +254 -0
- claude_mpm/cli/commands/configure_startup_manager.py +646 -0
- claude_mpm/cli/commands/configure_template_editor.py +497 -0
- claude_mpm/cli/commands/configure_validators.py +73 -0
- claude_mpm/cli/commands/memory.py +54 -20
- claude_mpm/cli/commands/mpm_init.py +35 -21
- claude_mpm/cli/executor.py +202 -0
- claude_mpm/cli/helpers.py +105 -0
- claude_mpm/cli/shared/output_formatters.py +28 -19
- claude_mpm/cli/startup.py +455 -0
- claude_mpm/core/enums.py +399 -0
- claude_mpm/core/instruction_reinforcement_hook.py +2 -1
- claude_mpm/core/interactive_session.py +6 -3
- claude_mpm/core/logging_config.py +6 -2
- claude_mpm/core/oneshot_session.py +8 -4
- claude_mpm/core/service_registry.py +5 -1
- claude_mpm/core/typing_utils.py +7 -6
- claude_mpm/hooks/instruction_reinforcement.py +7 -2
- claude_mpm/services/agents/deployment/interface_adapter.py +3 -2
- claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +3 -2
- claude_mpm/services/agents/memory/agent_memory_manager.py +5 -2
- claude_mpm/services/diagnostics/checks/installation_check.py +3 -2
- claude_mpm/services/diagnostics/checks/mcp_check.py +20 -6
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +8 -7
- claude_mpm/services/memory_hook_service.py +4 -1
- claude_mpm/services/monitor/daemon_manager.py +3 -2
- claude_mpm/services/monitor/handlers/dashboard.py +2 -1
- claude_mpm/services/monitor/handlers/hooks.py +2 -1
- claude_mpm/services/monitor/management/lifecycle.py +3 -2
- claude_mpm/services/monitor/server.py +2 -1
- claude_mpm/services/session_management_service.py +3 -2
- claude_mpm/services/socketio/handlers/hook.py +3 -2
- claude_mpm/services/socketio/server/main.py +3 -1
- claude_mpm/services/subprocess_launcher_service.py +14 -5
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +6 -5
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +6 -5
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +8 -7
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +5 -4
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +5 -4
- claude_mpm/services/unified/config_strategies/validation_strategy.py +13 -9
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +10 -3
- claude_mpm/services/unified/deployment_strategies/local.py +3 -2
- claude_mpm/services/unified/deployment_strategies/utils.py +2 -1
- claude_mpm/services/unified/deployment_strategies/vercel.py +2 -1
- claude_mpm/services/unified/interfaces.py +3 -1
- claude_mpm/services/unified/unified_analyzer.py +7 -6
- claude_mpm/services/unified/unified_config.py +2 -1
- claude_mpm/services/unified/unified_deployment.py +7 -2
- claude_mpm/tools/code_tree_analyzer.py +177 -141
- claude_mpm/tools/code_tree_events.py +4 -2
- {claude_mpm-4.14.7.dist-info → claude_mpm-4.14.9.dist-info}/METADATA +1 -1
- {claude_mpm-4.14.7.dist-info → claude_mpm-4.14.9.dist-info}/RECORD +74 -64
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +0 -425
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +0 -1041
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +0 -347
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +0 -575
- claude_mpm/services/project/analyzer_refactored.py +0 -450
- {claude_mpm-4.14.7.dist-info → claude_mpm-4.14.9.dist-info}/WHEEL +0 -0
- {claude_mpm-4.14.7.dist-info → claude_mpm-4.14.9.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.14.7.dist-info → claude_mpm-4.14.9.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.14.7.dist-info → claude_mpm-4.14.9.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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1897
|
-
|
|
1898
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2096
|
-
|
|
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
|
-
|
|
2169
|
-
|
|
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
|
-
|
|
2236
|
-
|
|
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
|
-
|
|
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."""
|