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