claude-mpm 4.1.4__py3-none-any.whl → 4.1.6__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/templates/research.json +39 -13
- claude_mpm/cli/__init__.py +2 -0
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/configure.py +1221 -0
- claude_mpm/cli/commands/configure_tui.py +1921 -0
- claude_mpm/cli/commands/tickets.py +365 -784
- claude_mpm/cli/parsers/base_parser.py +7 -0
- claude_mpm/cli/parsers/configure_parser.py +119 -0
- claude_mpm/cli/startup_logging.py +39 -12
- claude_mpm/constants.py +1 -0
- claude_mpm/core/output_style_manager.py +24 -0
- claude_mpm/core/socketio_pool.py +35 -3
- claude_mpm/core/unified_agent_registry.py +46 -15
- claude_mpm/dashboard/static/css/connection-status.css +370 -0
- claude_mpm/dashboard/static/js/components/connection-debug.js +654 -0
- claude_mpm/dashboard/static/js/connection-manager.js +536 -0
- claude_mpm/dashboard/templates/index.html +11 -0
- claude_mpm/hooks/claude_hooks/services/__init__.py +3 -1
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +190 -0
- claude_mpm/services/agents/deployment/agent_discovery_service.py +12 -3
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +172 -233
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +575 -0
- claude_mpm/services/agents/deployment/agent_operation_service.py +573 -0
- claude_mpm/services/agents/deployment/agent_record_service.py +419 -0
- claude_mpm/services/agents/deployment/agent_state_service.py +381 -0
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +4 -2
- claude_mpm/services/diagnostics/checks/__init__.py +2 -0
- claude_mpm/services/diagnostics/checks/instructions_check.py +418 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +15 -2
- claude_mpm/services/event_bus/direct_relay.py +173 -0
- claude_mpm/services/infrastructure/__init__.py +31 -5
- claude_mpm/services/infrastructure/monitoring/__init__.py +43 -0
- claude_mpm/services/infrastructure/monitoring/aggregator.py +437 -0
- claude_mpm/services/infrastructure/monitoring/base.py +130 -0
- claude_mpm/services/infrastructure/monitoring/legacy.py +203 -0
- claude_mpm/services/infrastructure/monitoring/network.py +218 -0
- claude_mpm/services/infrastructure/monitoring/process.py +342 -0
- claude_mpm/services/infrastructure/monitoring/resources.py +243 -0
- claude_mpm/services/infrastructure/monitoring/service.py +367 -0
- claude_mpm/services/infrastructure/monitoring.py +67 -1030
- claude_mpm/services/project/analyzer.py +13 -4
- claude_mpm/services/project/analyzer_refactored.py +450 -0
- claude_mpm/services/project/analyzer_v2.py +566 -0
- claude_mpm/services/project/architecture_analyzer.py +461 -0
- claude_mpm/services/project/dependency_analyzer.py +462 -0
- claude_mpm/services/project/language_analyzer.py +265 -0
- claude_mpm/services/project/metrics_collector.py +410 -0
- claude_mpm/services/socketio/handlers/connection_handler.py +345 -0
- claude_mpm/services/socketio/server/broadcaster.py +32 -1
- claude_mpm/services/socketio/server/connection_manager.py +516 -0
- claude_mpm/services/socketio/server/core.py +63 -0
- claude_mpm/services/socketio/server/eventbus_integration.py +20 -9
- claude_mpm/services/socketio/server/main.py +27 -1
- claude_mpm/services/ticket_manager.py +5 -1
- claude_mpm/services/ticket_services/__init__.py +26 -0
- claude_mpm/services/ticket_services/crud_service.py +328 -0
- claude_mpm/services/ticket_services/formatter_service.py +290 -0
- claude_mpm/services/ticket_services/search_service.py +324 -0
- claude_mpm/services/ticket_services/validation_service.py +303 -0
- claude_mpm/services/ticket_services/workflow_service.py +244 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/METADATA +3 -1
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/RECORD +67 -46
- claude_mpm/agents/OUTPUT_STYLE.md +0 -73
- claude_mpm/agents/backups/INSTRUCTIONS.md +0 -352
- claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +0 -156
- claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +0 -79
- claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +0 -68
- claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +0 -77
- claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +0 -78
- claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +0 -67
- claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +0 -88
- claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +0 -72
- claude_mpm/agents/templates/backup/research_memory_efficient.json +0 -88
- claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +0 -78
- claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +0 -62
- claude_mpm/agents/templates/vercel_ops_instructions.md +0 -582
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive configuration management command for claude-mpm CLI.
|
|
3
|
+
|
|
4
|
+
WHY: Users need an intuitive, interactive way to manage agent configurations,
|
|
5
|
+
edit templates, and configure behavior files without manually editing JSON/YAML files.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Use Rich for modern TUI with menus, tables, and panels
|
|
9
|
+
- Support both project-level and user-level configurations
|
|
10
|
+
- Provide non-interactive options for scripting
|
|
11
|
+
- Allow direct navigation to specific sections
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from rich.box import ROUNDED
|
|
21
|
+
from rich.columns import Columns
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.prompt import Confirm, Prompt
|
|
25
|
+
from rich.syntax import Syntax
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
from rich.text import Text
|
|
28
|
+
|
|
29
|
+
from ...services.version_service import VersionService
|
|
30
|
+
from ...utils.console import console as default_console
|
|
31
|
+
from ..shared import BaseCommand, CommandResult
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AgentConfig:
|
|
35
|
+
"""Simple agent configuration model."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self, name: str, description: str = "", dependencies: List[str] = None
|
|
39
|
+
):
|
|
40
|
+
self.name = name
|
|
41
|
+
self.description = description
|
|
42
|
+
self.dependencies = dependencies or []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SimpleAgentManager:
|
|
46
|
+
"""Simple agent state management that discovers real agents from templates."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, config_dir: Path):
|
|
49
|
+
self.config_dir = config_dir
|
|
50
|
+
self.config_file = config_dir / "agent_states.json"
|
|
51
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
self._load_states()
|
|
53
|
+
# Path to agent templates directory
|
|
54
|
+
self.templates_dir = (
|
|
55
|
+
Path(__file__).parent.parent.parent / "agents" / "templates"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _load_states(self):
|
|
59
|
+
"""Load agent states from file."""
|
|
60
|
+
if self.config_file.exists():
|
|
61
|
+
with open(self.config_file) as f:
|
|
62
|
+
self.states = json.load(f)
|
|
63
|
+
else:
|
|
64
|
+
self.states = {}
|
|
65
|
+
|
|
66
|
+
def _save_states(self):
|
|
67
|
+
"""Save agent states to file."""
|
|
68
|
+
with open(self.config_file, "w") as f:
|
|
69
|
+
json.dump(self.states, f, indent=2)
|
|
70
|
+
|
|
71
|
+
def is_agent_enabled(self, agent_name: str) -> bool:
|
|
72
|
+
"""Check if an agent is enabled."""
|
|
73
|
+
return self.states.get(agent_name, {}).get("enabled", True)
|
|
74
|
+
|
|
75
|
+
def set_agent_enabled(self, agent_name: str, enabled: bool):
|
|
76
|
+
"""Set agent enabled state."""
|
|
77
|
+
if agent_name not in self.states:
|
|
78
|
+
self.states[agent_name] = {}
|
|
79
|
+
self.states[agent_name]["enabled"] = enabled
|
|
80
|
+
self._save_states()
|
|
81
|
+
|
|
82
|
+
def discover_agents(self) -> List[AgentConfig]:
|
|
83
|
+
"""Discover available agents from template JSON files."""
|
|
84
|
+
agents = []
|
|
85
|
+
|
|
86
|
+
# Scan templates directory for JSON files
|
|
87
|
+
if not self.templates_dir.exists():
|
|
88
|
+
# Fallback to a minimal set if templates dir doesn't exist
|
|
89
|
+
return [
|
|
90
|
+
AgentConfig("engineer", "Engineering agent (templates not found)", []),
|
|
91
|
+
AgentConfig("research", "Research agent (templates not found)", []),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Read all JSON template files
|
|
96
|
+
for template_file in sorted(self.templates_dir.glob("*.json")):
|
|
97
|
+
# Skip backup files
|
|
98
|
+
if "backup" in template_file.name.lower():
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
with open(template_file) as f:
|
|
103
|
+
template_data = json.load(f)
|
|
104
|
+
|
|
105
|
+
# Extract agent information from template
|
|
106
|
+
agent_id = template_data.get("agent_id", template_file.stem)
|
|
107
|
+
|
|
108
|
+
# Get metadata for display info
|
|
109
|
+
metadata = template_data.get("metadata", {})
|
|
110
|
+
name = metadata.get("name", agent_id)
|
|
111
|
+
description = metadata.get(
|
|
112
|
+
"description", "No description available"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Extract capabilities/tools as dependencies for display
|
|
116
|
+
capabilities = template_data.get("capabilities", {})
|
|
117
|
+
tools = capabilities.get("tools", [])
|
|
118
|
+
# Show first few tools as "dependencies" for UI purposes
|
|
119
|
+
display_tools = tools[:3] if len(tools) > 3 else tools
|
|
120
|
+
|
|
121
|
+
# Normalize agent ID (remove -agent suffix if present, replace underscores)
|
|
122
|
+
normalized_id = agent_id.replace("-agent", "").replace("_", "-")
|
|
123
|
+
|
|
124
|
+
agents.append(
|
|
125
|
+
AgentConfig(
|
|
126
|
+
name=normalized_id,
|
|
127
|
+
description=(
|
|
128
|
+
description[:80] + "..."
|
|
129
|
+
if len(description) > 80
|
|
130
|
+
else description
|
|
131
|
+
),
|
|
132
|
+
dependencies=display_tools,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
except (json.JSONDecodeError, KeyError):
|
|
137
|
+
# Skip malformed templates
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
# If there's an error reading templates, return a minimal set
|
|
142
|
+
return [
|
|
143
|
+
AgentConfig("engineer", f"Error loading templates: {e!s}", []),
|
|
144
|
+
AgentConfig("research", "Research agent", []),
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# Sort agents by name for consistent display
|
|
148
|
+
agents.sort(key=lambda a: a.name)
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
agents
|
|
152
|
+
if agents
|
|
153
|
+
else [
|
|
154
|
+
AgentConfig("engineer", "No agents found in templates", []),
|
|
155
|
+
]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ConfigureCommand(BaseCommand):
|
|
160
|
+
"""Interactive configuration management command."""
|
|
161
|
+
|
|
162
|
+
def __init__(self):
|
|
163
|
+
super().__init__("configure")
|
|
164
|
+
self.console = default_console
|
|
165
|
+
self.version_service = VersionService()
|
|
166
|
+
self.current_scope = "project"
|
|
167
|
+
self.project_dir = Path.cwd()
|
|
168
|
+
self.agent_manager = None
|
|
169
|
+
|
|
170
|
+
def validate_args(self, args) -> Optional[str]:
|
|
171
|
+
"""Validate command arguments."""
|
|
172
|
+
# Check for conflicting direct navigation options
|
|
173
|
+
nav_options = [
|
|
174
|
+
getattr(args, "agents", False),
|
|
175
|
+
getattr(args, "templates", False),
|
|
176
|
+
getattr(args, "behaviors", False),
|
|
177
|
+
getattr(args, "version_info", False),
|
|
178
|
+
]
|
|
179
|
+
if sum(nav_options) > 1:
|
|
180
|
+
return "Only one direct navigation option can be specified at a time"
|
|
181
|
+
|
|
182
|
+
# Check for conflicting non-interactive options
|
|
183
|
+
if getattr(args, "enable_agent", None) and getattr(args, "disable_agent", None):
|
|
184
|
+
return "Cannot enable and disable agents at the same time"
|
|
185
|
+
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
def run(self, args) -> CommandResult:
|
|
189
|
+
"""Execute the configure command."""
|
|
190
|
+
# Set configuration scope
|
|
191
|
+
self.current_scope = getattr(args, "scope", "project")
|
|
192
|
+
if getattr(args, "project_dir", None):
|
|
193
|
+
self.project_dir = Path(args.project_dir)
|
|
194
|
+
|
|
195
|
+
# Initialize agent manager with appropriate config directory
|
|
196
|
+
if self.current_scope == "project":
|
|
197
|
+
config_dir = self.project_dir / ".claude-mpm"
|
|
198
|
+
else:
|
|
199
|
+
config_dir = Path.home() / ".claude-mpm"
|
|
200
|
+
self.agent_manager = SimpleAgentManager(config_dir)
|
|
201
|
+
|
|
202
|
+
# Disable colors if requested
|
|
203
|
+
if getattr(args, "no_colors", False):
|
|
204
|
+
self.console = Console(color_system=None)
|
|
205
|
+
|
|
206
|
+
# Handle non-interactive options first
|
|
207
|
+
if getattr(args, "list_agents", False):
|
|
208
|
+
return self._list_agents_non_interactive()
|
|
209
|
+
|
|
210
|
+
if getattr(args, "enable_agent", None):
|
|
211
|
+
return self._enable_agent_non_interactive(args.enable_agent)
|
|
212
|
+
|
|
213
|
+
if getattr(args, "disable_agent", None):
|
|
214
|
+
return self._disable_agent_non_interactive(args.disable_agent)
|
|
215
|
+
|
|
216
|
+
if getattr(args, "export_config", None):
|
|
217
|
+
return self._export_config(args.export_config)
|
|
218
|
+
|
|
219
|
+
if getattr(args, "import_config", None):
|
|
220
|
+
return self._import_config(args.import_config)
|
|
221
|
+
|
|
222
|
+
if getattr(args, "version_info", False):
|
|
223
|
+
return self._show_version_info()
|
|
224
|
+
|
|
225
|
+
# Handle direct navigation options
|
|
226
|
+
if getattr(args, "agents", False):
|
|
227
|
+
return self._run_agent_management()
|
|
228
|
+
|
|
229
|
+
if getattr(args, "templates", False):
|
|
230
|
+
return self._run_template_editing()
|
|
231
|
+
|
|
232
|
+
if getattr(args, "behaviors", False):
|
|
233
|
+
return self._run_behavior_management()
|
|
234
|
+
|
|
235
|
+
# Launch interactive TUI
|
|
236
|
+
return self._run_interactive_tui(args)
|
|
237
|
+
|
|
238
|
+
def _run_interactive_tui(self, args) -> CommandResult:
|
|
239
|
+
"""Run the main interactive TUI."""
|
|
240
|
+
# Check if we can use the modern Textual TUI
|
|
241
|
+
use_textual = getattr(args, "use_textual", True)
|
|
242
|
+
force_rich = getattr(args, "force_rich", False)
|
|
243
|
+
|
|
244
|
+
if use_textual and not force_rich:
|
|
245
|
+
try:
|
|
246
|
+
# Try to import and use Textual TUI
|
|
247
|
+
from .configure_tui import can_use_tui, launch_tui
|
|
248
|
+
|
|
249
|
+
if can_use_tui():
|
|
250
|
+
self.console.print(
|
|
251
|
+
"[cyan]Launching full-screen configuration interface...[/cyan]"
|
|
252
|
+
)
|
|
253
|
+
return launch_tui(self.current_scope, self.project_dir)
|
|
254
|
+
# Fall back to Rich TUI if terminal doesn't support full-screen
|
|
255
|
+
self.console.print(
|
|
256
|
+
"[yellow]Terminal doesn't support full-screen mode. Using menu interface.[/yellow]"
|
|
257
|
+
)
|
|
258
|
+
except ImportError:
|
|
259
|
+
# Textual not available, fall back to Rich
|
|
260
|
+
self.console.print(
|
|
261
|
+
"[yellow]Textual not installed. Using menu interface.[/yellow]"
|
|
262
|
+
)
|
|
263
|
+
self.console.print(
|
|
264
|
+
"[dim]Install textual for a better experience: pip install textual[/dim]"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Original Rich-based TUI
|
|
268
|
+
try:
|
|
269
|
+
self.console.clear()
|
|
270
|
+
|
|
271
|
+
while True:
|
|
272
|
+
# Display main menu
|
|
273
|
+
self._display_header()
|
|
274
|
+
choice = self._show_main_menu()
|
|
275
|
+
|
|
276
|
+
if choice == "1":
|
|
277
|
+
self._manage_agents()
|
|
278
|
+
elif choice == "2":
|
|
279
|
+
self._edit_templates()
|
|
280
|
+
elif choice == "3":
|
|
281
|
+
self._manage_behaviors()
|
|
282
|
+
elif choice == "4":
|
|
283
|
+
self._switch_scope()
|
|
284
|
+
elif choice == "5":
|
|
285
|
+
self._show_version_info_interactive()
|
|
286
|
+
elif choice == "q":
|
|
287
|
+
self.console.print(
|
|
288
|
+
"\n[green]Configuration complete. Goodbye![/green]"
|
|
289
|
+
)
|
|
290
|
+
break
|
|
291
|
+
else:
|
|
292
|
+
self.console.print("[red]Invalid choice. Please try again.[/red]")
|
|
293
|
+
|
|
294
|
+
return CommandResult.success_result("Configuration completed")
|
|
295
|
+
|
|
296
|
+
except KeyboardInterrupt:
|
|
297
|
+
self.console.print("\n[yellow]Configuration cancelled.[/yellow]")
|
|
298
|
+
return CommandResult.success_result("Configuration cancelled")
|
|
299
|
+
except Exception as e:
|
|
300
|
+
self.logger.error(f"Configuration error: {e}", exc_info=True)
|
|
301
|
+
return CommandResult.error_result(f"Configuration failed: {e}")
|
|
302
|
+
|
|
303
|
+
def _display_header(self) -> None:
|
|
304
|
+
"""Display the TUI header."""
|
|
305
|
+
self.console.clear()
|
|
306
|
+
|
|
307
|
+
# Create header panel
|
|
308
|
+
header_text = Text()
|
|
309
|
+
header_text.append("Claude MPM ", style="bold cyan")
|
|
310
|
+
header_text.append("Configuration Interface", style="bold white")
|
|
311
|
+
|
|
312
|
+
scope_text = Text(f"Scope: {self.current_scope.upper()}", style="yellow")
|
|
313
|
+
dir_text = Text(f"Directory: {self.project_dir}", style="dim")
|
|
314
|
+
|
|
315
|
+
header_content = Columns([header_text], align="center")
|
|
316
|
+
subtitle_content = f"{scope_text} | {dir_text}"
|
|
317
|
+
|
|
318
|
+
header_panel = Panel(
|
|
319
|
+
header_content,
|
|
320
|
+
subtitle=subtitle_content,
|
|
321
|
+
box=ROUNDED,
|
|
322
|
+
style="blue",
|
|
323
|
+
padding=(1, 2),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
self.console.print(header_panel)
|
|
327
|
+
self.console.print()
|
|
328
|
+
|
|
329
|
+
def _show_main_menu(self) -> str:
|
|
330
|
+
"""Show the main menu and get user choice."""
|
|
331
|
+
menu_items = [
|
|
332
|
+
("1", "Agent Management", "Enable/disable agents and customize settings"),
|
|
333
|
+
("2", "Template Editing", "Edit agent JSON templates"),
|
|
334
|
+
("3", "Behavior Files", "Manage identity and workflow configurations"),
|
|
335
|
+
("4", "Switch Scope", f"Current: {self.current_scope}"),
|
|
336
|
+
("5", "Version Info", "Display MPM and Claude versions"),
|
|
337
|
+
("q", "Quit", "Exit configuration interface"),
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
341
|
+
table.add_column("Key", style="cyan", width=3)
|
|
342
|
+
table.add_column("Option", style="bold white", width=20)
|
|
343
|
+
table.add_column("Description", style="dim")
|
|
344
|
+
|
|
345
|
+
for key, option, desc in menu_items:
|
|
346
|
+
table.add_row(f"[{key}]", option, desc)
|
|
347
|
+
|
|
348
|
+
menu_panel = Panel(
|
|
349
|
+
table, title="[bold]Main Menu[/bold]", box=ROUNDED, style="green"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
self.console.print(menu_panel)
|
|
353
|
+
self.console.print()
|
|
354
|
+
|
|
355
|
+
return Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="q")
|
|
356
|
+
|
|
357
|
+
def _manage_agents(self) -> None:
|
|
358
|
+
"""Agent management interface."""
|
|
359
|
+
while True:
|
|
360
|
+
self.console.clear()
|
|
361
|
+
self._display_header()
|
|
362
|
+
|
|
363
|
+
# Display available agents
|
|
364
|
+
agents = self.agent_manager.discover_agents()
|
|
365
|
+
self._display_agents_table(agents)
|
|
366
|
+
|
|
367
|
+
# Show agent menu
|
|
368
|
+
self.console.print("\n[bold]Agent Management Options:[/bold]")
|
|
369
|
+
self.console.print(" [cyan][e][/cyan] Enable an agent")
|
|
370
|
+
self.console.print(" [cyan][d][/cyan] Disable an agent")
|
|
371
|
+
self.console.print(" [cyan][c][/cyan] Customize agent template")
|
|
372
|
+
self.console.print(" [cyan][v][/cyan] View agent details")
|
|
373
|
+
self.console.print(" [cyan][r][/cyan] Reset agent to defaults")
|
|
374
|
+
self.console.print(" [cyan][b][/cyan] Back to main menu")
|
|
375
|
+
self.console.print()
|
|
376
|
+
|
|
377
|
+
choice = Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="b")
|
|
378
|
+
|
|
379
|
+
if choice == "b":
|
|
380
|
+
break
|
|
381
|
+
if choice == "e":
|
|
382
|
+
self._enable_agent_interactive(agents)
|
|
383
|
+
elif choice == "d":
|
|
384
|
+
self._disable_agent_interactive(agents)
|
|
385
|
+
elif choice == "c":
|
|
386
|
+
self._customize_agent_template(agents)
|
|
387
|
+
elif choice == "v":
|
|
388
|
+
self._view_agent_details(agents)
|
|
389
|
+
elif choice == "r":
|
|
390
|
+
self._reset_agent_defaults(agents)
|
|
391
|
+
else:
|
|
392
|
+
self.console.print("[red]Invalid choice.[/red]")
|
|
393
|
+
Prompt.ask("Press Enter to continue")
|
|
394
|
+
|
|
395
|
+
def _display_agents_table(self, agents: List[AgentConfig]) -> None:
|
|
396
|
+
"""Display a table of available agents."""
|
|
397
|
+
table = Table(
|
|
398
|
+
title=f"Available Agents ({len(agents)} total)",
|
|
399
|
+
box=ROUNDED,
|
|
400
|
+
show_lines=True,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
table.add_column("ID", style="dim", width=3)
|
|
404
|
+
table.add_column("Name", style="cyan", width=22)
|
|
405
|
+
table.add_column("Status", width=12)
|
|
406
|
+
table.add_column("Description", style="white", width=45)
|
|
407
|
+
table.add_column("Model/Tools", style="dim", width=20)
|
|
408
|
+
|
|
409
|
+
for idx, agent in enumerate(agents, 1):
|
|
410
|
+
# Check if agent is enabled
|
|
411
|
+
is_enabled = self.agent_manager.is_agent_enabled(agent.name)
|
|
412
|
+
status = (
|
|
413
|
+
"[green]✓ Enabled[/green]" if is_enabled else "[red]✗ Disabled[/red]"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Format tools/dependencies - show first 2 tools
|
|
417
|
+
tools_display = ""
|
|
418
|
+
if agent.dependencies:
|
|
419
|
+
if len(agent.dependencies) > 2:
|
|
420
|
+
tools_display = f"{', '.join(agent.dependencies[:2])}..."
|
|
421
|
+
else:
|
|
422
|
+
tools_display = ", ".join(agent.dependencies)
|
|
423
|
+
else:
|
|
424
|
+
# Try to get model from template
|
|
425
|
+
try:
|
|
426
|
+
template_path = self._get_agent_template_path(agent.name)
|
|
427
|
+
if template_path.exists():
|
|
428
|
+
with open(template_path) as f:
|
|
429
|
+
template = json.load(f)
|
|
430
|
+
model = template.get("capabilities", {}).get("model", "default")
|
|
431
|
+
tools_display = f"Model: {model}"
|
|
432
|
+
else:
|
|
433
|
+
tools_display = "Default"
|
|
434
|
+
except:
|
|
435
|
+
tools_display = "Default"
|
|
436
|
+
|
|
437
|
+
# Truncate description for table display
|
|
438
|
+
desc_display = (
|
|
439
|
+
agent.description[:42] + "..."
|
|
440
|
+
if len(agent.description) > 42
|
|
441
|
+
else agent.description
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
table.add_row(str(idx), agent.name, status, desc_display, tools_display)
|
|
445
|
+
|
|
446
|
+
self.console.print(table)
|
|
447
|
+
|
|
448
|
+
def _enable_agent_interactive(self, agents: List[AgentConfig]) -> None:
|
|
449
|
+
"""Interactive agent enabling."""
|
|
450
|
+
agent_id = Prompt.ask("Enter agent ID to enable (or 'all' for all agents)")
|
|
451
|
+
|
|
452
|
+
if agent_id.lower() == "all":
|
|
453
|
+
if Confirm.ask("[yellow]Enable ALL agents?[/yellow]"):
|
|
454
|
+
for agent in agents:
|
|
455
|
+
self.agent_manager.set_agent_enabled(agent.name, True)
|
|
456
|
+
self.console.print("[green]All agents enabled successfully![/green]")
|
|
457
|
+
else:
|
|
458
|
+
try:
|
|
459
|
+
idx = int(agent_id) - 1
|
|
460
|
+
if 0 <= idx < len(agents):
|
|
461
|
+
agent = agents[idx]
|
|
462
|
+
self.agent_manager.set_agent_enabled(agent.name, True)
|
|
463
|
+
self.console.print(
|
|
464
|
+
f"[green]Agent '{agent.name}' enabled successfully![/green]"
|
|
465
|
+
)
|
|
466
|
+
else:
|
|
467
|
+
self.console.print("[red]Invalid agent ID.[/red]")
|
|
468
|
+
except ValueError:
|
|
469
|
+
self.console.print("[red]Invalid input. Please enter a number.[/red]")
|
|
470
|
+
|
|
471
|
+
Prompt.ask("Press Enter to continue")
|
|
472
|
+
|
|
473
|
+
def _disable_agent_interactive(self, agents: List[AgentConfig]) -> None:
|
|
474
|
+
"""Interactive agent disabling."""
|
|
475
|
+
agent_id = Prompt.ask("Enter agent ID to disable (or 'all' for all agents)")
|
|
476
|
+
|
|
477
|
+
if agent_id.lower() == "all":
|
|
478
|
+
if Confirm.ask("[yellow]Disable ALL agents?[/yellow]"):
|
|
479
|
+
for agent in agents:
|
|
480
|
+
self.agent_manager.set_agent_enabled(agent.name, False)
|
|
481
|
+
self.console.print("[green]All agents disabled successfully![/green]")
|
|
482
|
+
else:
|
|
483
|
+
try:
|
|
484
|
+
idx = int(agent_id) - 1
|
|
485
|
+
if 0 <= idx < len(agents):
|
|
486
|
+
agent = agents[idx]
|
|
487
|
+
self.agent_manager.set_agent_enabled(agent.name, False)
|
|
488
|
+
self.console.print(
|
|
489
|
+
f"[green]Agent '{agent.name}' disabled successfully![/green]"
|
|
490
|
+
)
|
|
491
|
+
else:
|
|
492
|
+
self.console.print("[red]Invalid agent ID.[/red]")
|
|
493
|
+
except ValueError:
|
|
494
|
+
self.console.print("[red]Invalid input. Please enter a number.[/red]")
|
|
495
|
+
|
|
496
|
+
Prompt.ask("Press Enter to continue")
|
|
497
|
+
|
|
498
|
+
def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
|
|
499
|
+
"""Customize agent JSON template."""
|
|
500
|
+
agent_id = Prompt.ask("Enter agent ID to customize")
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
idx = int(agent_id) - 1
|
|
504
|
+
if 0 <= idx < len(agents):
|
|
505
|
+
agent = agents[idx]
|
|
506
|
+
self._edit_agent_template(agent)
|
|
507
|
+
else:
|
|
508
|
+
self.console.print("[red]Invalid agent ID.[/red]")
|
|
509
|
+
Prompt.ask("Press Enter to continue")
|
|
510
|
+
except ValueError:
|
|
511
|
+
self.console.print("[red]Invalid input. Please enter a number.[/red]")
|
|
512
|
+
Prompt.ask("Press Enter to continue")
|
|
513
|
+
|
|
514
|
+
def _edit_agent_template(self, agent: AgentConfig) -> None:
|
|
515
|
+
"""Edit an agent's JSON template."""
|
|
516
|
+
self.console.clear()
|
|
517
|
+
self.console.print(f"[bold]Editing template for: {agent.name}[/bold]\n")
|
|
518
|
+
|
|
519
|
+
# Get current template
|
|
520
|
+
template_path = self._get_agent_template_path(agent.name)
|
|
521
|
+
|
|
522
|
+
if template_path.exists():
|
|
523
|
+
with open(template_path) as f:
|
|
524
|
+
template = json.load(f)
|
|
525
|
+
is_system = str(template_path).startswith(
|
|
526
|
+
str(self.agent_manager.templates_dir)
|
|
527
|
+
)
|
|
528
|
+
else:
|
|
529
|
+
# Create a minimal template structure based on system templates
|
|
530
|
+
template = {
|
|
531
|
+
"schema_version": "1.2.0",
|
|
532
|
+
"agent_id": agent.name,
|
|
533
|
+
"agent_version": "1.0.0",
|
|
534
|
+
"agent_type": agent.name.replace("-", "_"),
|
|
535
|
+
"metadata": {
|
|
536
|
+
"name": agent.name.replace("-", " ").title() + " Agent",
|
|
537
|
+
"description": agent.description,
|
|
538
|
+
"tags": [agent.name],
|
|
539
|
+
"author": "Custom",
|
|
540
|
+
"created_at": "",
|
|
541
|
+
"updated_at": "",
|
|
542
|
+
},
|
|
543
|
+
"capabilities": {
|
|
544
|
+
"model": "opus",
|
|
545
|
+
"tools": (
|
|
546
|
+
agent.dependencies
|
|
547
|
+
if agent.dependencies
|
|
548
|
+
else ["Read", "Write", "Edit", "Bash"]
|
|
549
|
+
),
|
|
550
|
+
},
|
|
551
|
+
"instructions": {
|
|
552
|
+
"base_template": "BASE_AGENT_TEMPLATE.md",
|
|
553
|
+
"custom_instructions": "",
|
|
554
|
+
},
|
|
555
|
+
}
|
|
556
|
+
is_system = False
|
|
557
|
+
|
|
558
|
+
# Display current template
|
|
559
|
+
if is_system:
|
|
560
|
+
self.console.print(
|
|
561
|
+
"[yellow]Viewing SYSTEM template (read-only). Customization will create a local copy.[/yellow]\n"
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
self.console.print("[bold]Current Template:[/bold]")
|
|
565
|
+
# Truncate for display if too large
|
|
566
|
+
display_template = template.copy()
|
|
567
|
+
if "instructions" in display_template and isinstance(
|
|
568
|
+
display_template["instructions"], dict
|
|
569
|
+
):
|
|
570
|
+
if (
|
|
571
|
+
"custom_instructions" in display_template["instructions"]
|
|
572
|
+
and len(str(display_template["instructions"]["custom_instructions"]))
|
|
573
|
+
> 200
|
|
574
|
+
):
|
|
575
|
+
display_template["instructions"]["custom_instructions"] = (
|
|
576
|
+
display_template["instructions"]["custom_instructions"][:200]
|
|
577
|
+
+ "..."
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
json_str = json.dumps(display_template, indent=2)
|
|
581
|
+
# Limit display to first 50 lines for readability
|
|
582
|
+
lines = json_str.split("\n")
|
|
583
|
+
if len(lines) > 50:
|
|
584
|
+
json_str = "\n".join(lines[:50]) + "\n... (truncated for display)"
|
|
585
|
+
|
|
586
|
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=True)
|
|
587
|
+
self.console.print(syntax)
|
|
588
|
+
self.console.print()
|
|
589
|
+
|
|
590
|
+
# Editing options
|
|
591
|
+
self.console.print("[bold]Editing Options:[/bold]")
|
|
592
|
+
if not is_system:
|
|
593
|
+
self.console.print(" [cyan][1][/cyan] Edit in external editor")
|
|
594
|
+
self.console.print(" [cyan][2][/cyan] Add/modify a field")
|
|
595
|
+
self.console.print(" [cyan][3][/cyan] Remove a field")
|
|
596
|
+
self.console.print(" [cyan][4][/cyan] Reset to defaults")
|
|
597
|
+
else:
|
|
598
|
+
self.console.print(" [cyan][1][/cyan] Create customized copy")
|
|
599
|
+
self.console.print(" [cyan][2][/cyan] View full template")
|
|
600
|
+
self.console.print(" [cyan][b][/cyan] Back")
|
|
601
|
+
self.console.print()
|
|
602
|
+
|
|
603
|
+
choice = Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="b")
|
|
604
|
+
|
|
605
|
+
if is_system:
|
|
606
|
+
if choice == "1":
|
|
607
|
+
# Create a customized copy
|
|
608
|
+
self._create_custom_template_copy(agent, template)
|
|
609
|
+
elif choice == "2":
|
|
610
|
+
# View full template
|
|
611
|
+
self._view_full_template(template)
|
|
612
|
+
elif choice == "1":
|
|
613
|
+
self._edit_in_external_editor(template_path, template)
|
|
614
|
+
elif choice == "2":
|
|
615
|
+
self._modify_template_field(template, template_path)
|
|
616
|
+
elif choice == "3":
|
|
617
|
+
self._remove_template_field(template, template_path)
|
|
618
|
+
elif choice == "4":
|
|
619
|
+
self._reset_template(agent, template_path)
|
|
620
|
+
|
|
621
|
+
if choice != "b":
|
|
622
|
+
Prompt.ask("Press Enter to continue")
|
|
623
|
+
|
|
624
|
+
def _get_agent_template_path(self, agent_name: str) -> Path:
|
|
625
|
+
"""Get the path to an agent's template file."""
|
|
626
|
+
# First check for custom template in project/user config
|
|
627
|
+
if self.current_scope == "project":
|
|
628
|
+
config_dir = self.project_dir / ".claude-mpm" / "agents"
|
|
629
|
+
else:
|
|
630
|
+
config_dir = Path.home() / ".claude-mpm" / "agents"
|
|
631
|
+
|
|
632
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
633
|
+
custom_template = config_dir / f"{agent_name}.json"
|
|
634
|
+
|
|
635
|
+
# If custom template exists, return it
|
|
636
|
+
if custom_template.exists():
|
|
637
|
+
return custom_template
|
|
638
|
+
|
|
639
|
+
# Otherwise, look for the system template
|
|
640
|
+
# Handle various naming conventions
|
|
641
|
+
possible_names = [
|
|
642
|
+
f"{agent_name}.json",
|
|
643
|
+
f"{agent_name.replace('-', '_')}.json",
|
|
644
|
+
f"{agent_name}-agent.json",
|
|
645
|
+
f"{agent_name.replace('-', '_')}_agent.json",
|
|
646
|
+
]
|
|
647
|
+
|
|
648
|
+
for name in possible_names:
|
|
649
|
+
system_template = self.agent_manager.templates_dir / name
|
|
650
|
+
if system_template.exists():
|
|
651
|
+
return system_template
|
|
652
|
+
|
|
653
|
+
# Return the custom template path for new templates
|
|
654
|
+
return custom_template
|
|
655
|
+
|
|
656
|
+
def _edit_in_external_editor(self, template_path: Path, template: Dict) -> None:
|
|
657
|
+
"""Open template in external editor."""
|
|
658
|
+
import subprocess
|
|
659
|
+
import tempfile
|
|
660
|
+
|
|
661
|
+
# Write current template to temp file
|
|
662
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
|
663
|
+
json.dump(template, f, indent=2)
|
|
664
|
+
temp_path = f.name
|
|
665
|
+
|
|
666
|
+
# Get editor from environment
|
|
667
|
+
editor = os.environ.get("EDITOR", "nano")
|
|
668
|
+
|
|
669
|
+
try:
|
|
670
|
+
# Open in editor
|
|
671
|
+
subprocess.call([editor, temp_path])
|
|
672
|
+
|
|
673
|
+
# Read back the edited content
|
|
674
|
+
with open(temp_path) as f:
|
|
675
|
+
new_template = json.load(f)
|
|
676
|
+
|
|
677
|
+
# Save to actual template path
|
|
678
|
+
with open(template_path, "w") as f:
|
|
679
|
+
json.dump(new_template, f, indent=2)
|
|
680
|
+
|
|
681
|
+
self.console.print("[green]Template updated successfully![/green]")
|
|
682
|
+
|
|
683
|
+
except Exception as e:
|
|
684
|
+
self.console.print(f"[red]Error editing template: {e}[/red]")
|
|
685
|
+
finally:
|
|
686
|
+
# Clean up temp file
|
|
687
|
+
Path(temp_path).unlink(missing_ok=True)
|
|
688
|
+
|
|
689
|
+
def _modify_template_field(self, template: Dict, template_path: Path) -> None:
|
|
690
|
+
"""Add or modify a field in the template."""
|
|
691
|
+
field_name = Prompt.ask(
|
|
692
|
+
"Enter field name (use dot notation for nested, e.g., 'config.timeout')"
|
|
693
|
+
)
|
|
694
|
+
field_value = Prompt.ask("Enter field value (JSON format)")
|
|
695
|
+
|
|
696
|
+
try:
|
|
697
|
+
# Parse the value as JSON
|
|
698
|
+
value = json.loads(field_value)
|
|
699
|
+
|
|
700
|
+
# Navigate to the field location
|
|
701
|
+
parts = field_name.split(".")
|
|
702
|
+
current = template
|
|
703
|
+
|
|
704
|
+
for part in parts[:-1]:
|
|
705
|
+
if part not in current:
|
|
706
|
+
current[part] = {}
|
|
707
|
+
current = current[part]
|
|
708
|
+
|
|
709
|
+
# Set the value
|
|
710
|
+
current[parts[-1]] = value
|
|
711
|
+
|
|
712
|
+
# Save the template
|
|
713
|
+
with open(template_path, "w") as f:
|
|
714
|
+
json.dump(template, f, indent=2)
|
|
715
|
+
|
|
716
|
+
self.console.print(
|
|
717
|
+
f"[green]Field '{field_name}' updated successfully![/green]"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
except json.JSONDecodeError:
|
|
721
|
+
self.console.print("[red]Invalid JSON value. Please try again.[/red]")
|
|
722
|
+
except Exception as e:
|
|
723
|
+
self.console.print(f"[red]Error updating field: {e}[/red]")
|
|
724
|
+
|
|
725
|
+
def _remove_template_field(self, template: Dict, template_path: Path) -> None:
|
|
726
|
+
"""Remove a field from the template."""
|
|
727
|
+
field_name = Prompt.ask(
|
|
728
|
+
"Enter field name to remove (use dot notation for nested)"
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
try:
|
|
732
|
+
# Navigate to the field location
|
|
733
|
+
parts = field_name.split(".")
|
|
734
|
+
current = template
|
|
735
|
+
|
|
736
|
+
for part in parts[:-1]:
|
|
737
|
+
if part not in current:
|
|
738
|
+
raise KeyError(f"Field '{field_name}' not found")
|
|
739
|
+
current = current[part]
|
|
740
|
+
|
|
741
|
+
# Remove the field
|
|
742
|
+
if parts[-1] in current:
|
|
743
|
+
del current[parts[-1]]
|
|
744
|
+
|
|
745
|
+
# Save the template
|
|
746
|
+
with open(template_path, "w") as f:
|
|
747
|
+
json.dump(template, f, indent=2)
|
|
748
|
+
|
|
749
|
+
self.console.print(
|
|
750
|
+
f"[green]Field '{field_name}' removed successfully![/green]"
|
|
751
|
+
)
|
|
752
|
+
else:
|
|
753
|
+
self.console.print(f"[red]Field '{field_name}' not found.[/red]")
|
|
754
|
+
|
|
755
|
+
except Exception as e:
|
|
756
|
+
self.console.print(f"[red]Error removing field: {e}[/red]")
|
|
757
|
+
|
|
758
|
+
def _reset_template(self, agent: AgentConfig, template_path: Path) -> None:
|
|
759
|
+
"""Reset template to defaults."""
|
|
760
|
+
if Confirm.ask(f"[yellow]Reset '{agent.name}' template to defaults?[/yellow]"):
|
|
761
|
+
# Remove custom template file
|
|
762
|
+
template_path.unlink(missing_ok=True)
|
|
763
|
+
self.console.print(
|
|
764
|
+
f"[green]Template for '{agent.name}' reset to defaults![/green]"
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
def _create_custom_template_copy(self, agent: AgentConfig, template: Dict) -> None:
|
|
768
|
+
"""Create a customized copy of a system template."""
|
|
769
|
+
if self.current_scope == "project":
|
|
770
|
+
config_dir = self.project_dir / ".claude-mpm" / "agents"
|
|
771
|
+
else:
|
|
772
|
+
config_dir = Path.home() / ".claude-mpm" / "agents"
|
|
773
|
+
|
|
774
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
775
|
+
custom_path = config_dir / f"{agent.name}.json"
|
|
776
|
+
|
|
777
|
+
if custom_path.exists():
|
|
778
|
+
if not Confirm.ask(
|
|
779
|
+
"[yellow]Custom template already exists. Overwrite?[/yellow]"
|
|
780
|
+
):
|
|
781
|
+
return
|
|
782
|
+
|
|
783
|
+
# Save the template copy
|
|
784
|
+
with open(custom_path, "w") as f:
|
|
785
|
+
json.dump(template, f, indent=2)
|
|
786
|
+
|
|
787
|
+
self.console.print(f"[green]Created custom template at: {custom_path}[/green]")
|
|
788
|
+
self.console.print("[green]You can now edit this template.[/green]")
|
|
789
|
+
|
|
790
|
+
def _view_full_template(self, template: Dict) -> None:
|
|
791
|
+
"""View the full template without truncation."""
|
|
792
|
+
self.console.clear()
|
|
793
|
+
self.console.print("[bold]Full Template View:[/bold]\n")
|
|
794
|
+
|
|
795
|
+
json_str = json.dumps(template, indent=2)
|
|
796
|
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=True)
|
|
797
|
+
|
|
798
|
+
# Use pager for long content
|
|
799
|
+
|
|
800
|
+
with self.console.pager():
|
|
801
|
+
self.console.print(syntax)
|
|
802
|
+
|
|
803
|
+
def _view_agent_details(self, agents: List[AgentConfig]) -> None:
|
|
804
|
+
"""View detailed information about an agent."""
|
|
805
|
+
agent_id = Prompt.ask("Enter agent ID to view")
|
|
806
|
+
|
|
807
|
+
try:
|
|
808
|
+
idx = int(agent_id) - 1
|
|
809
|
+
if 0 <= idx < len(agents):
|
|
810
|
+
agent = agents[idx]
|
|
811
|
+
|
|
812
|
+
self.console.clear()
|
|
813
|
+
self._display_header()
|
|
814
|
+
|
|
815
|
+
# Try to load full template for more details
|
|
816
|
+
template_path = self._get_agent_template_path(agent.name)
|
|
817
|
+
extra_info = ""
|
|
818
|
+
|
|
819
|
+
if template_path.exists():
|
|
820
|
+
try:
|
|
821
|
+
with open(template_path) as f:
|
|
822
|
+
template = json.load(f)
|
|
823
|
+
|
|
824
|
+
# Extract additional information
|
|
825
|
+
metadata = template.get("metadata", {})
|
|
826
|
+
capabilities = template.get("capabilities", {})
|
|
827
|
+
|
|
828
|
+
# Get full description if available
|
|
829
|
+
full_desc = metadata.get("description", agent.description)
|
|
830
|
+
|
|
831
|
+
# Get model and tools
|
|
832
|
+
model = capabilities.get("model", "default")
|
|
833
|
+
tools = capabilities.get("tools", [])
|
|
834
|
+
|
|
835
|
+
# Get tags
|
|
836
|
+
tags = metadata.get("tags", [])
|
|
837
|
+
|
|
838
|
+
# Get version info
|
|
839
|
+
agent_version = template.get("agent_version", "N/A")
|
|
840
|
+
schema_version = template.get("schema_version", "N/A")
|
|
841
|
+
|
|
842
|
+
extra_info = f"""
|
|
843
|
+
[bold]Full Description:[/bold]
|
|
844
|
+
{full_desc}
|
|
845
|
+
|
|
846
|
+
[bold]Model:[/bold] {model}
|
|
847
|
+
[bold]Agent Version:[/bold] {agent_version}
|
|
848
|
+
[bold]Schema Version:[/bold] {schema_version}
|
|
849
|
+
[bold]Tags:[/bold] {', '.join(tags) if tags else 'None'}
|
|
850
|
+
[bold]Tools:[/bold] {', '.join(tools[:5]) if tools else 'None'}{'...' if len(tools) > 5 else ''}
|
|
851
|
+
"""
|
|
852
|
+
except:
|
|
853
|
+
pass
|
|
854
|
+
|
|
855
|
+
# Create detail panel
|
|
856
|
+
detail_text = f"""
|
|
857
|
+
[bold]Name:[/bold] {agent.name}
|
|
858
|
+
[bold]Status:[/bold] {'[green]Enabled[/green]' if self.agent_manager.is_agent_enabled(agent.name) else '[red]Disabled[/red]'}
|
|
859
|
+
[bold]Template Path:[/bold] {template_path}
|
|
860
|
+
[bold]Is System Template:[/bold] {'Yes' if str(template_path).startswith(str(self.agent_manager.templates_dir)) else 'No (Custom)'}
|
|
861
|
+
{extra_info}
|
|
862
|
+
"""
|
|
863
|
+
|
|
864
|
+
panel = Panel(
|
|
865
|
+
detail_text.strip(),
|
|
866
|
+
title=f"[bold]{agent.name} Details[/bold]",
|
|
867
|
+
box=ROUNDED,
|
|
868
|
+
style="cyan",
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
self.console.print(panel)
|
|
872
|
+
|
|
873
|
+
else:
|
|
874
|
+
self.console.print("[red]Invalid agent ID.[/red]")
|
|
875
|
+
|
|
876
|
+
except ValueError:
|
|
877
|
+
self.console.print("[red]Invalid input. Please enter a number.[/red]")
|
|
878
|
+
|
|
879
|
+
Prompt.ask("\nPress Enter to continue")
|
|
880
|
+
|
|
881
|
+
def _edit_templates(self) -> None:
|
|
882
|
+
"""Template editing interface."""
|
|
883
|
+
self.console.print("[yellow]Template editing interface - Coming soon![/yellow]")
|
|
884
|
+
Prompt.ask("Press Enter to continue")
|
|
885
|
+
|
|
886
|
+
def _manage_behaviors(self) -> None:
|
|
887
|
+
"""Behavior file management interface."""
|
|
888
|
+
while True:
|
|
889
|
+
self.console.clear()
|
|
890
|
+
self._display_header()
|
|
891
|
+
|
|
892
|
+
self.console.print("[bold]Behavior File Management[/bold]\n")
|
|
893
|
+
|
|
894
|
+
# Display current behavior files
|
|
895
|
+
self._display_behavior_files()
|
|
896
|
+
|
|
897
|
+
# Show behavior menu
|
|
898
|
+
self.console.print("\n[bold]Options:[/bold]")
|
|
899
|
+
self.console.print(" [cyan][1][/cyan] Edit identity configuration")
|
|
900
|
+
self.console.print(" [cyan][2][/cyan] Edit workflow configuration")
|
|
901
|
+
self.console.print(" [cyan][3][/cyan] Import behavior file")
|
|
902
|
+
self.console.print(" [cyan][4][/cyan] Export behavior file")
|
|
903
|
+
self.console.print(" [cyan][b][/cyan] Back to main menu")
|
|
904
|
+
self.console.print()
|
|
905
|
+
|
|
906
|
+
choice = Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="b")
|
|
907
|
+
|
|
908
|
+
if choice == "b":
|
|
909
|
+
break
|
|
910
|
+
if choice == "1":
|
|
911
|
+
self._edit_identity_config()
|
|
912
|
+
elif choice == "2":
|
|
913
|
+
self._edit_workflow_config()
|
|
914
|
+
elif choice == "3":
|
|
915
|
+
self._import_behavior_file()
|
|
916
|
+
elif choice == "4":
|
|
917
|
+
self._export_behavior_file()
|
|
918
|
+
else:
|
|
919
|
+
self.console.print("[red]Invalid choice.[/red]")
|
|
920
|
+
Prompt.ask("Press Enter to continue")
|
|
921
|
+
|
|
922
|
+
def _display_behavior_files(self) -> None:
|
|
923
|
+
"""Display current behavior files."""
|
|
924
|
+
if self.current_scope == "project":
|
|
925
|
+
config_dir = self.project_dir / ".claude-mpm" / "behaviors"
|
|
926
|
+
else:
|
|
927
|
+
config_dir = Path.home() / ".claude-mpm" / "behaviors"
|
|
928
|
+
|
|
929
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
930
|
+
|
|
931
|
+
table = Table(title="Behavior Files", box=ROUNDED)
|
|
932
|
+
table.add_column("File", style="cyan", width=30)
|
|
933
|
+
table.add_column("Size", style="dim", width=10)
|
|
934
|
+
table.add_column("Modified", style="white", width=20)
|
|
935
|
+
|
|
936
|
+
identity_file = config_dir / "identity.yaml"
|
|
937
|
+
workflow_file = config_dir / "workflow.yaml"
|
|
938
|
+
|
|
939
|
+
for file_path in [identity_file, workflow_file]:
|
|
940
|
+
if file_path.exists():
|
|
941
|
+
stat = file_path.stat()
|
|
942
|
+
size = f"{stat.st_size} bytes"
|
|
943
|
+
modified = f"{stat.st_mtime:.0f}" # Simplified timestamp
|
|
944
|
+
table.add_row(file_path.name, size, modified)
|
|
945
|
+
else:
|
|
946
|
+
table.add_row(file_path.name, "[dim]Not found[/dim]", "-")
|
|
947
|
+
|
|
948
|
+
self.console.print(table)
|
|
949
|
+
|
|
950
|
+
def _edit_identity_config(self) -> None:
|
|
951
|
+
"""Edit identity configuration."""
|
|
952
|
+
self.console.print(
|
|
953
|
+
"[yellow]Identity configuration editor - Coming soon![/yellow]"
|
|
954
|
+
)
|
|
955
|
+
Prompt.ask("Press Enter to continue")
|
|
956
|
+
|
|
957
|
+
def _edit_workflow_config(self) -> None:
|
|
958
|
+
"""Edit workflow configuration."""
|
|
959
|
+
self.console.print(
|
|
960
|
+
"[yellow]Workflow configuration editor - Coming soon![/yellow]"
|
|
961
|
+
)
|
|
962
|
+
Prompt.ask("Press Enter to continue")
|
|
963
|
+
|
|
964
|
+
def _import_behavior_file(self) -> None:
|
|
965
|
+
"""Import a behavior file."""
|
|
966
|
+
file_path = Prompt.ask("Enter path to behavior file to import")
|
|
967
|
+
|
|
968
|
+
try:
|
|
969
|
+
source = Path(file_path)
|
|
970
|
+
if not source.exists():
|
|
971
|
+
self.console.print(f"[red]File not found: {file_path}[/red]")
|
|
972
|
+
return
|
|
973
|
+
|
|
974
|
+
# Determine target directory
|
|
975
|
+
if self.current_scope == "project":
|
|
976
|
+
config_dir = self.project_dir / ".claude-mpm" / "behaviors"
|
|
977
|
+
else:
|
|
978
|
+
config_dir = Path.home() / ".claude-mpm" / "behaviors"
|
|
979
|
+
|
|
980
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
981
|
+
|
|
982
|
+
# Copy file
|
|
983
|
+
import shutil
|
|
984
|
+
|
|
985
|
+
target = config_dir / source.name
|
|
986
|
+
shutil.copy2(source, target)
|
|
987
|
+
|
|
988
|
+
self.console.print(f"[green]Successfully imported {source.name}![/green]")
|
|
989
|
+
|
|
990
|
+
except Exception as e:
|
|
991
|
+
self.console.print(f"[red]Error importing file: {e}[/red]")
|
|
992
|
+
|
|
993
|
+
Prompt.ask("Press Enter to continue")
|
|
994
|
+
|
|
995
|
+
def _export_behavior_file(self) -> None:
|
|
996
|
+
"""Export a behavior file."""
|
|
997
|
+
self.console.print("[yellow]Behavior file export - Coming soon![/yellow]")
|
|
998
|
+
Prompt.ask("Press Enter to continue")
|
|
999
|
+
|
|
1000
|
+
def _switch_scope(self) -> None:
|
|
1001
|
+
"""Switch between project and user scope."""
|
|
1002
|
+
self.current_scope = "user" if self.current_scope == "project" else "project"
|
|
1003
|
+
self.console.print(f"[green]Switched to {self.current_scope} scope[/green]")
|
|
1004
|
+
Prompt.ask("Press Enter to continue")
|
|
1005
|
+
|
|
1006
|
+
def _show_version_info_interactive(self) -> None:
|
|
1007
|
+
"""Show version information in interactive mode."""
|
|
1008
|
+
self.console.clear()
|
|
1009
|
+
self._display_header()
|
|
1010
|
+
|
|
1011
|
+
# Get version information
|
|
1012
|
+
mpm_version = self.version_service.get_version()
|
|
1013
|
+
build_number = self.version_service.get_build_number()
|
|
1014
|
+
|
|
1015
|
+
# Try to get Claude Code version
|
|
1016
|
+
claude_version = "Unknown"
|
|
1017
|
+
try:
|
|
1018
|
+
import subprocess
|
|
1019
|
+
|
|
1020
|
+
result = subprocess.run(
|
|
1021
|
+
["claude", "--version"], capture_output=True, text=True, timeout=5, check=False
|
|
1022
|
+
)
|
|
1023
|
+
if result.returncode == 0:
|
|
1024
|
+
claude_version = result.stdout.strip()
|
|
1025
|
+
except:
|
|
1026
|
+
pass
|
|
1027
|
+
|
|
1028
|
+
# Create version panel
|
|
1029
|
+
version_text = f"""
|
|
1030
|
+
[bold cyan]Claude MPM[/bold cyan]
|
|
1031
|
+
Version: {mpm_version}
|
|
1032
|
+
Build: {build_number}
|
|
1033
|
+
|
|
1034
|
+
[bold cyan]Claude Code[/bold cyan]
|
|
1035
|
+
Version: {claude_version}
|
|
1036
|
+
|
|
1037
|
+
[bold cyan]Python[/bold cyan]
|
|
1038
|
+
Version: {sys.version.split()[0]}
|
|
1039
|
+
|
|
1040
|
+
[bold cyan]Configuration[/bold cyan]
|
|
1041
|
+
Scope: {self.current_scope}
|
|
1042
|
+
Directory: {self.project_dir}
|
|
1043
|
+
"""
|
|
1044
|
+
|
|
1045
|
+
panel = Panel(
|
|
1046
|
+
version_text.strip(),
|
|
1047
|
+
title="[bold]Version Information[/bold]",
|
|
1048
|
+
box=ROUNDED,
|
|
1049
|
+
style="green",
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
self.console.print(panel)
|
|
1053
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1054
|
+
|
|
1055
|
+
# Non-interactive command methods
|
|
1056
|
+
|
|
1057
|
+
def _list_agents_non_interactive(self) -> CommandResult:
|
|
1058
|
+
"""List agents in non-interactive mode."""
|
|
1059
|
+
agents = self.agent_manager.discover_agents()
|
|
1060
|
+
|
|
1061
|
+
data = []
|
|
1062
|
+
for agent in agents:
|
|
1063
|
+
data.append(
|
|
1064
|
+
{
|
|
1065
|
+
"name": agent.name,
|
|
1066
|
+
"enabled": self.agent_manager.is_agent_enabled(agent.name),
|
|
1067
|
+
"description": agent.description,
|
|
1068
|
+
"dependencies": agent.dependencies,
|
|
1069
|
+
}
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
# Print as JSON for scripting
|
|
1073
|
+
print(json.dumps(data, indent=2))
|
|
1074
|
+
|
|
1075
|
+
return CommandResult.success_result("Agents listed", data={"agents": data})
|
|
1076
|
+
|
|
1077
|
+
def _enable_agent_non_interactive(self, agent_name: str) -> CommandResult:
|
|
1078
|
+
"""Enable an agent in non-interactive mode."""
|
|
1079
|
+
try:
|
|
1080
|
+
self.agent_manager.set_agent_enabled(agent_name, True)
|
|
1081
|
+
return CommandResult.success_result(f"Agent '{agent_name}' enabled")
|
|
1082
|
+
except Exception as e:
|
|
1083
|
+
return CommandResult.error_result(f"Failed to enable agent: {e}")
|
|
1084
|
+
|
|
1085
|
+
def _disable_agent_non_interactive(self, agent_name: str) -> CommandResult:
|
|
1086
|
+
"""Disable an agent in non-interactive mode."""
|
|
1087
|
+
try:
|
|
1088
|
+
self.agent_manager.set_agent_enabled(agent_name, False)
|
|
1089
|
+
return CommandResult.success_result(f"Agent '{agent_name}' disabled")
|
|
1090
|
+
except Exception as e:
|
|
1091
|
+
return CommandResult.error_result(f"Failed to disable agent: {e}")
|
|
1092
|
+
|
|
1093
|
+
def _export_config(self, file_path: str) -> CommandResult:
|
|
1094
|
+
"""Export configuration to a file."""
|
|
1095
|
+
try:
|
|
1096
|
+
# Gather all configuration
|
|
1097
|
+
config_data = {"scope": self.current_scope, "agents": {}, "behaviors": {}}
|
|
1098
|
+
|
|
1099
|
+
# Get agent states
|
|
1100
|
+
agents = self.agent_manager.discover_agents()
|
|
1101
|
+
for agent in agents:
|
|
1102
|
+
config_data["agents"][agent.name] = {
|
|
1103
|
+
"enabled": self.agent_manager.is_agent_enabled(agent.name),
|
|
1104
|
+
"template_path": str(self._get_agent_template_path(agent.name)),
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
# Write to file
|
|
1108
|
+
output_path = Path(file_path)
|
|
1109
|
+
with open(output_path, "w") as f:
|
|
1110
|
+
json.dump(config_data, f, indent=2)
|
|
1111
|
+
|
|
1112
|
+
return CommandResult.success_result(
|
|
1113
|
+
f"Configuration exported to {output_path}"
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
except Exception as e:
|
|
1117
|
+
return CommandResult.error_result(f"Failed to export configuration: {e}")
|
|
1118
|
+
|
|
1119
|
+
def _import_config(self, file_path: str) -> CommandResult:
|
|
1120
|
+
"""Import configuration from a file."""
|
|
1121
|
+
try:
|
|
1122
|
+
input_path = Path(file_path)
|
|
1123
|
+
if not input_path.exists():
|
|
1124
|
+
return CommandResult.error_result(f"File not found: {file_path}")
|
|
1125
|
+
|
|
1126
|
+
with open(input_path) as f:
|
|
1127
|
+
config_data = json.load(f)
|
|
1128
|
+
|
|
1129
|
+
# Apply agent states
|
|
1130
|
+
if "agents" in config_data:
|
|
1131
|
+
for agent_name, agent_config in config_data["agents"].items():
|
|
1132
|
+
if "enabled" in agent_config:
|
|
1133
|
+
self.agent_manager.set_agent_enabled(
|
|
1134
|
+
agent_name, agent_config["enabled"]
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
return CommandResult.success_result(
|
|
1138
|
+
f"Configuration imported from {input_path}"
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
except Exception as e:
|
|
1142
|
+
return CommandResult.error_result(f"Failed to import configuration: {e}")
|
|
1143
|
+
|
|
1144
|
+
def _show_version_info(self) -> CommandResult:
|
|
1145
|
+
"""Show version information in non-interactive mode."""
|
|
1146
|
+
mpm_version = self.version_service.get_version()
|
|
1147
|
+
build_number = self.version_service.get_build_number()
|
|
1148
|
+
|
|
1149
|
+
data = {
|
|
1150
|
+
"mpm_version": mpm_version,
|
|
1151
|
+
"build_number": build_number,
|
|
1152
|
+
"python_version": sys.version.split()[0],
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
# Try to get Claude version
|
|
1156
|
+
try:
|
|
1157
|
+
import subprocess
|
|
1158
|
+
|
|
1159
|
+
result = subprocess.run(
|
|
1160
|
+
["claude", "--version"], capture_output=True, text=True, timeout=5, check=False
|
|
1161
|
+
)
|
|
1162
|
+
if result.returncode == 0:
|
|
1163
|
+
data["claude_version"] = result.stdout.strip()
|
|
1164
|
+
except:
|
|
1165
|
+
data["claude_version"] = "Unknown"
|
|
1166
|
+
|
|
1167
|
+
# Print formatted output
|
|
1168
|
+
self.console.print(
|
|
1169
|
+
f"[bold]Claude MPM:[/bold] {mpm_version} (build {build_number})"
|
|
1170
|
+
)
|
|
1171
|
+
self.console.print(
|
|
1172
|
+
f"[bold]Claude Code:[/bold] {data.get('claude_version', 'Unknown')}"
|
|
1173
|
+
)
|
|
1174
|
+
self.console.print(f"[bold]Python:[/bold] {data['python_version']}")
|
|
1175
|
+
|
|
1176
|
+
return CommandResult.success_result("Version information displayed", data=data)
|
|
1177
|
+
|
|
1178
|
+
def _run_agent_management(self) -> CommandResult:
|
|
1179
|
+
"""Jump directly to agent management."""
|
|
1180
|
+
try:
|
|
1181
|
+
self._manage_agents()
|
|
1182
|
+
return CommandResult.success_result("Agent management completed")
|
|
1183
|
+
except KeyboardInterrupt:
|
|
1184
|
+
return CommandResult.success_result("Agent management cancelled")
|
|
1185
|
+
except Exception as e:
|
|
1186
|
+
return CommandResult.error_result(f"Agent management failed: {e}")
|
|
1187
|
+
|
|
1188
|
+
def _run_template_editing(self) -> CommandResult:
|
|
1189
|
+
"""Jump directly to template editing."""
|
|
1190
|
+
try:
|
|
1191
|
+
self._edit_templates()
|
|
1192
|
+
return CommandResult.success_result("Template editing completed")
|
|
1193
|
+
except KeyboardInterrupt:
|
|
1194
|
+
return CommandResult.success_result("Template editing cancelled")
|
|
1195
|
+
except Exception as e:
|
|
1196
|
+
return CommandResult.error_result(f"Template editing failed: {e}")
|
|
1197
|
+
|
|
1198
|
+
def _run_behavior_management(self) -> CommandResult:
|
|
1199
|
+
"""Jump directly to behavior management."""
|
|
1200
|
+
try:
|
|
1201
|
+
self._manage_behaviors()
|
|
1202
|
+
return CommandResult.success_result("Behavior management completed")
|
|
1203
|
+
except KeyboardInterrupt:
|
|
1204
|
+
return CommandResult.success_result("Behavior management cancelled")
|
|
1205
|
+
except Exception as e:
|
|
1206
|
+
return CommandResult.error_result(f"Behavior management failed: {e}")
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def manage_configure(args) -> int:
|
|
1210
|
+
"""Main entry point for configuration management command.
|
|
1211
|
+
|
|
1212
|
+
This function maintains backward compatibility while using the new BaseCommand pattern.
|
|
1213
|
+
"""
|
|
1214
|
+
command = ConfigureCommand()
|
|
1215
|
+
result = command.execute(args)
|
|
1216
|
+
|
|
1217
|
+
# Print result if needed
|
|
1218
|
+
if hasattr(args, "format") and args.format in ["json", "yaml"]:
|
|
1219
|
+
command.print_result(result, args)
|
|
1220
|
+
|
|
1221
|
+
return result.exit_code
|