claude-mpm 5.0.2__py3-none-any.whl → 5.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +2002 -0
- claude_mpm/agents/PM_INSTRUCTIONS.md +1218 -905
- claude_mpm/agents/agent_loader.py +10 -17
- claude_mpm/agents/base_agent_loader.py +10 -35
- claude_mpm/agents/frontmatter_validator.py +68 -0
- claude_mpm/agents/templates/circuit-breakers.md +431 -45
- claude_mpm/cli/__init__.py +0 -1
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/agent_state_manager.py +67 -23
- claude_mpm/cli/commands/agents.py +446 -25
- claude_mpm/cli/commands/auto_configure.py +535 -233
- claude_mpm/cli/commands/configure.py +1500 -147
- claude_mpm/cli/commands/configure_agent_display.py +13 -6
- claude_mpm/cli/commands/mpm_init/core.py +158 -1
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/postmortem.py +401 -0
- claude_mpm/cli/commands/run.py +1 -39
- claude_mpm/cli/commands/skills.py +322 -19
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +8 -0
- claude_mpm/cli/interactive/agent_wizard.py +302 -195
- claude_mpm/cli/parsers/agents_parser.py +137 -0
- claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
- claude_mpm/cli/parsers/base_parser.py +9 -0
- claude_mpm/cli/parsers/skills_parser.py +7 -0
- claude_mpm/cli/startup.py +133 -85
- claude_mpm/commands/mpm-agents-auto-configure.md +2 -2
- claude_mpm/commands/mpm-agents-list.md +2 -2
- claude_mpm/commands/mpm-config-view.md +2 -2
- claude_mpm/commands/mpm-help.md +3 -0
- claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
- claude_mpm/commands/mpm-postmortem.md +123 -0
- claude_mpm/commands/mpm-session-resume.md +2 -2
- claude_mpm/commands/mpm-ticket-view.md +2 -2
- claude_mpm/config/agent_presets.py +312 -82
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/config/skill_presets.py +392 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +2 -25
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework/loaders/file_loader.py +54 -101
- claude_mpm/core/interactive_session.py +19 -5
- claude_mpm/core/oneshot_session.py +16 -4
- claude_mpm/core/output_style_manager.py +173 -43
- claude_mpm/core/protocols/__init__.py +23 -0
- claude_mpm/core/protocols/runner_protocol.py +103 -0
- claude_mpm/core/protocols/session_protocol.py +131 -0
- claude_mpm/core/shared/singleton_manager.py +11 -4
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/system_context.py +38 -0
- claude_mpm/core/unified_agent_registry.py +134 -16
- claude_mpm/core/unified_config.py +22 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
- claude_mpm/models/agent_definition.py +7 -0
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/services/agents/agent_recommendation_service.py +279 -0
- claude_mpm/services/agents/cache_git_manager.py +621 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +110 -3
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +518 -55
- claude_mpm/services/agents/git_source_manager.py +20 -0
- claude_mpm/services/agents/sources/git_source_sync_service.py +45 -6
- claude_mpm/services/agents/toolchain_detector.py +6 -5
- claude_mpm/services/analysis/__init__.py +35 -0
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/analysis/postmortem_reporter.py +474 -0
- claude_mpm/services/analysis/postmortem_service.py +765 -0
- claude_mpm/services/command_deployment_service.py +106 -5
- claude_mpm/services/core/base.py +7 -2
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +8 -8
- claude_mpm/services/mcp_config_manager.py +75 -145
- claude_mpm/services/mcp_service_verifier.py +6 -3
- claude_mpm/services/monitor/daemon.py +37 -10
- claude_mpm/services/monitor/daemon_manager.py +134 -21
- claude_mpm/services/monitor/server.py +225 -19
- claude_mpm/services/project/project_organizer.py +4 -0
- claude_mpm/services/runner_configuration_service.py +16 -3
- claude_mpm/services/session_management_service.py +16 -4
- claude_mpm/services/socketio/event_normalizer.py +15 -1
- claude_mpm/services/socketio/server/core.py +160 -21
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/utils/agent_filters.py +261 -0
- claude_mpm/utils/gitignore.py +3 -0
- claude_mpm/utils/migration.py +372 -0
- claude_mpm/utils/progress.py +5 -1
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +69 -84
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +112 -153
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- claude_mpm/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -971
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
|
@@ -12,17 +12,23 @@ DESIGN DECISIONS:
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
|
+
import shutil
|
|
16
|
+
from collections import defaultdict
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
from typing import Dict, List, Optional
|
|
17
19
|
|
|
18
20
|
import questionary
|
|
19
|
-
|
|
21
|
+
import questionary.constants
|
|
22
|
+
import questionary.prompts.common # For checkbox symbol customization
|
|
23
|
+
from questionary import Choice, Separator, Style
|
|
20
24
|
from rich.console import Console
|
|
21
25
|
from rich.prompt import Confirm, Prompt
|
|
22
26
|
from rich.text import Text
|
|
23
27
|
|
|
24
28
|
from ...core.config import Config
|
|
29
|
+
from ...services.agents.agent_recommendation_service import AgentRecommendationService
|
|
25
30
|
from ...services.version_service import VersionService
|
|
31
|
+
from ...utils.agent_filters import apply_all_filters, get_deployed_agent_ids
|
|
26
32
|
from ...utils.console import console as default_console
|
|
27
33
|
from ..shared import BaseCommand, CommandResult
|
|
28
34
|
from .agent_state_manager import SimpleAgentManager
|
|
@@ -43,13 +49,18 @@ from .configure_validators import (
|
|
|
43
49
|
class ConfigureCommand(BaseCommand):
|
|
44
50
|
"""Interactive configuration management command."""
|
|
45
51
|
|
|
46
|
-
# Questionary style
|
|
52
|
+
# Questionary style optimized for dark terminals (WCAG AAA compliant)
|
|
47
53
|
QUESTIONARY_STYLE = Style(
|
|
48
54
|
[
|
|
49
|
-
("selected", "fg
|
|
50
|
-
("pointer", "fg
|
|
51
|
-
("highlighted", "fg
|
|
52
|
-
("question", "fg
|
|
55
|
+
("selected", "fg:#e0e0e0 bold"), # Light gray - excellent readability
|
|
56
|
+
("pointer", "fg:#ffd700 bold"), # Gold/yellow - highly visible pointer
|
|
57
|
+
("highlighted", "fg:#e0e0e0"), # Light gray - clear hover state
|
|
58
|
+
("question", "fg:#e0e0e0 bold"), # Light gray bold - prominent questions
|
|
59
|
+
("checkbox", "fg:#00ff00"), # Green - for checked boxes
|
|
60
|
+
(
|
|
61
|
+
"checkbox-selected",
|
|
62
|
+
"fg:#00ff00 bold",
|
|
63
|
+
), # Green bold - for checked selected boxes
|
|
53
64
|
]
|
|
54
65
|
)
|
|
55
66
|
|
|
@@ -67,6 +78,7 @@ class ConfigureCommand(BaseCommand):
|
|
|
67
78
|
self._navigation = None # Lazy-initialized
|
|
68
79
|
self._template_editor = None # Lazy-initialized
|
|
69
80
|
self._startup_manager = None # Lazy-initialized
|
|
81
|
+
self._recommendation_service = None # Lazy-initialized
|
|
70
82
|
|
|
71
83
|
def validate_args(self, args) -> Optional[str]:
|
|
72
84
|
"""Validate command arguments."""
|
|
@@ -143,6 +155,13 @@ class ConfigureCommand(BaseCommand):
|
|
|
143
155
|
)
|
|
144
156
|
return self._startup_manager
|
|
145
157
|
|
|
158
|
+
@property
|
|
159
|
+
def recommendation_service(self) -> AgentRecommendationService:
|
|
160
|
+
"""Lazy-initialize recommendation service."""
|
|
161
|
+
if self._recommendation_service is None:
|
|
162
|
+
self._recommendation_service = AgentRecommendationService()
|
|
163
|
+
return self._recommendation_service
|
|
164
|
+
|
|
146
165
|
def run(self, args) -> CommandResult:
|
|
147
166
|
"""Execute the configure command."""
|
|
148
167
|
# Set configuration scope
|
|
@@ -296,70 +315,34 @@ class ConfigureCommand(BaseCommand):
|
|
|
296
315
|
return self.navigation.show_main_menu()
|
|
297
316
|
|
|
298
317
|
def _manage_agents(self) -> None:
|
|
299
|
-
"""Enhanced agent management with remote agent discovery and
|
|
318
|
+
"""Enhanced agent management with remote agent discovery and installation."""
|
|
300
319
|
while True:
|
|
301
320
|
self.console.clear()
|
|
302
321
|
self.navigation.display_header()
|
|
303
322
|
self.console.print("\n[bold blue]═══ Agent Management ═══[/bold blue]\n")
|
|
304
323
|
|
|
305
|
-
#
|
|
306
|
-
self.
|
|
307
|
-
|
|
308
|
-
sources = self._get_configured_sources()
|
|
309
|
-
if sources:
|
|
310
|
-
from rich.table import Table
|
|
311
|
-
|
|
312
|
-
sources_table = Table(show_header=True, header_style="bold cyan")
|
|
313
|
-
sources_table.add_column("Source", style="cyan", width=40)
|
|
314
|
-
sources_table.add_column("Status", style="green", width=15)
|
|
315
|
-
sources_table.add_column("Agents", style="yellow", width=10)
|
|
324
|
+
# Load all agents with spinner (don't show partial state)
|
|
325
|
+
agents = self._load_agents_with_spinner()
|
|
316
326
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
agent_count = source.get("agent_count", "?")
|
|
320
|
-
sources_table.add_row(
|
|
321
|
-
source["identifier"], status, str(agent_count)
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
self.console.print(sources_table)
|
|
325
|
-
else:
|
|
326
|
-
self.console.print("[yellow]No agent sources configured[/yellow]")
|
|
327
|
+
if not agents:
|
|
328
|
+
self.console.print("[yellow]No agents found[/yellow]")
|
|
327
329
|
self.console.print(
|
|
328
|
-
"[dim]
|
|
330
|
+
"[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
|
|
329
331
|
)
|
|
332
|
+
Prompt.ask("\nPress Enter to continue")
|
|
333
|
+
break
|
|
330
334
|
|
|
331
|
-
#
|
|
332
|
-
self.
|
|
333
|
-
|
|
334
|
-
try:
|
|
335
|
-
# Discover agents (includes both local and remote)
|
|
336
|
-
agents = self.agent_manager.discover_agents(include_remote=True)
|
|
337
|
-
|
|
338
|
-
if not agents:
|
|
339
|
-
self.console.print("[yellow]No agents found[/yellow]")
|
|
340
|
-
self.console.print(
|
|
341
|
-
"[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
|
|
342
|
-
)
|
|
343
|
-
else:
|
|
344
|
-
# Display agents in a table
|
|
345
|
-
self._display_agents_with_source_info(agents)
|
|
346
|
-
|
|
347
|
-
except Exception as e:
|
|
348
|
-
self.console.print(f"[red]Error discovering agents: {e}[/red]")
|
|
349
|
-
self.logger.error(f"Agent discovery failed: {e}", exc_info=True)
|
|
335
|
+
# Now display everything at once (after all data loaded)
|
|
336
|
+
self._display_agent_sources_and_list(agents)
|
|
350
337
|
|
|
351
|
-
# Step 3:
|
|
338
|
+
# Step 3: Simplified menu - only "Select Agents" option
|
|
352
339
|
self.console.print()
|
|
340
|
+
self.logger.debug("About to show agent management menu")
|
|
353
341
|
try:
|
|
354
342
|
choice = questionary.select(
|
|
355
343
|
"Agent Management:",
|
|
356
344
|
choices=[
|
|
357
|
-
"
|
|
358
|
-
"Deploy agents (individual selection)",
|
|
359
|
-
"Deploy preset (predefined sets)",
|
|
360
|
-
"Remove agents",
|
|
361
|
-
"View agent details",
|
|
362
|
-
"Toggle agents (legacy enable/disable)",
|
|
345
|
+
"Select Agents",
|
|
363
346
|
questionary.Separator(),
|
|
364
347
|
"← Back to main menu",
|
|
365
348
|
],
|
|
@@ -369,25 +352,115 @@ class ConfigureCommand(BaseCommand):
|
|
|
369
352
|
if choice is None or choice == "← Back to main menu":
|
|
370
353
|
break
|
|
371
354
|
|
|
372
|
-
agents_var = agents if "agents" in locals() else []
|
|
373
|
-
|
|
374
355
|
# Map selection to action
|
|
375
|
-
if choice == "
|
|
376
|
-
self.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
elif choice == "Deploy preset (predefined sets)":
|
|
380
|
-
self._deploy_agents_preset()
|
|
381
|
-
elif choice == "Remove agents":
|
|
382
|
-
self._remove_agents(agents_var)
|
|
383
|
-
elif choice == "View agent details":
|
|
384
|
-
self._view_agent_details_enhanced(agents_var)
|
|
385
|
-
elif choice == "Toggle agents (legacy enable/disable)":
|
|
386
|
-
self._toggle_agents_interactive(agents_var)
|
|
356
|
+
if choice == "Select Agents":
|
|
357
|
+
self.logger.debug("User selected 'Select Agents' from menu")
|
|
358
|
+
self._deploy_agents_unified(agents)
|
|
359
|
+
# Loop back to show updated state after deployment
|
|
387
360
|
|
|
388
361
|
except KeyboardInterrupt:
|
|
389
362
|
self.console.print("\n[yellow]Operation cancelled[/yellow]")
|
|
390
363
|
break
|
|
364
|
+
except Exception as e:
|
|
365
|
+
# Handle questionary menu failure
|
|
366
|
+
import sys
|
|
367
|
+
|
|
368
|
+
self.logger.error(f"Agent management menu failed: {e}", exc_info=True)
|
|
369
|
+
self.console.print("[red]Error: Interactive menu failed[/red]")
|
|
370
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
371
|
+
if not sys.stdin.isatty():
|
|
372
|
+
self.console.print(
|
|
373
|
+
"[dim]Interactive terminal required for this operation[/dim]"
|
|
374
|
+
)
|
|
375
|
+
self.console.print("[dim]Use command-line options instead:[/dim]")
|
|
376
|
+
self.console.print(
|
|
377
|
+
"[dim] claude-mpm configure --list-agents[/dim]"
|
|
378
|
+
)
|
|
379
|
+
self.console.print(
|
|
380
|
+
"[dim] claude-mpm configure --enable-agent <id>[/dim]"
|
|
381
|
+
)
|
|
382
|
+
Prompt.ask("\nPress Enter to continue")
|
|
383
|
+
break
|
|
384
|
+
|
|
385
|
+
def _load_agents_with_spinner(self) -> List[AgentConfig]:
|
|
386
|
+
"""Load agents with loading indicator, don't show partial state.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
List of discovered agents with deployment status set.
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
agents = []
|
|
393
|
+
with self.console.status(
|
|
394
|
+
"[bold blue]Loading agents...[/bold blue]", spinner="dots"
|
|
395
|
+
):
|
|
396
|
+
try:
|
|
397
|
+
# Discover agents (includes both local and remote)
|
|
398
|
+
agents = self.agent_manager.discover_agents(include_remote=True)
|
|
399
|
+
|
|
400
|
+
# Set deployment status on each agent for display
|
|
401
|
+
deployed_ids = get_deployed_agent_ids()
|
|
402
|
+
for agent in agents:
|
|
403
|
+
# Extract leaf name for comparison
|
|
404
|
+
agent_leaf_name = agent.name.split("/")[-1]
|
|
405
|
+
agent.is_deployed = agent_leaf_name in deployed_ids
|
|
406
|
+
|
|
407
|
+
# Filter BASE_AGENT from display (1M-502 Phase 1)
|
|
408
|
+
agents = self._filter_agent_configs(agents, filter_deployed=False)
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
self.console.print(f"[red]Error discovering agents: {e}[/red]")
|
|
412
|
+
self.logger.error(f"Agent discovery failed: {e}", exc_info=True)
|
|
413
|
+
agents = []
|
|
414
|
+
|
|
415
|
+
return agents
|
|
416
|
+
|
|
417
|
+
def _display_agent_sources_and_list(self, agents: List[AgentConfig]) -> None:
|
|
418
|
+
"""Display agent sources and agent list (only after all data loaded).
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
agents: List of discovered agents with deployment status.
|
|
422
|
+
"""
|
|
423
|
+
from rich.table import Table
|
|
424
|
+
|
|
425
|
+
# Step 1: Show configured sources
|
|
426
|
+
self.console.print("[bold white]═══ Agent Sources ═══[/bold white]\n")
|
|
427
|
+
|
|
428
|
+
sources = self._get_configured_sources()
|
|
429
|
+
if sources:
|
|
430
|
+
sources_table = Table(show_header=True, header_style="bold white")
|
|
431
|
+
sources_table.add_column(
|
|
432
|
+
"Source",
|
|
433
|
+
style="bright_yellow",
|
|
434
|
+
width=40,
|
|
435
|
+
no_wrap=True,
|
|
436
|
+
overflow="ellipsis",
|
|
437
|
+
)
|
|
438
|
+
sources_table.add_column("Status", style="green", width=15, no_wrap=True)
|
|
439
|
+
sources_table.add_column("Agents", style="yellow", width=10, no_wrap=True)
|
|
440
|
+
|
|
441
|
+
for source in sources:
|
|
442
|
+
status = "✓ Active" if source.get("enabled", True) else "Disabled"
|
|
443
|
+
agent_count = source.get("agent_count", "?")
|
|
444
|
+
sources_table.add_row(source["identifier"], status, str(agent_count))
|
|
445
|
+
|
|
446
|
+
self.console.print(sources_table)
|
|
447
|
+
else:
|
|
448
|
+
self.console.print("[yellow]No agent sources configured[/yellow]")
|
|
449
|
+
self.console.print(
|
|
450
|
+
"[dim]Default source 'bobmatnyc/claude-mpm-agents' will be used[/dim]\n"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Step 2: Display available agents
|
|
454
|
+
self.console.print("\n[bold white]═══ Available Agents ═══[/bold white]\n")
|
|
455
|
+
|
|
456
|
+
if agents:
|
|
457
|
+
# Show progress spinner while recommendation service processes agents
|
|
458
|
+
with self.console.status(
|
|
459
|
+
"[bold blue]Preparing agent list...[/bold blue]", spinner="dots"
|
|
460
|
+
):
|
|
461
|
+
self._display_agents_with_source_info(agents)
|
|
462
|
+
else:
|
|
463
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
391
464
|
|
|
392
465
|
def _display_agents_table(self, agents: List[AgentConfig]) -> None:
|
|
393
466
|
"""Display a table of available agents."""
|
|
@@ -446,6 +519,9 @@ class ConfigureCommand(BaseCommand):
|
|
|
446
519
|
if self.agent_manager.has_pending_changes():
|
|
447
520
|
self.agent_manager.commit_deferred_changes()
|
|
448
521
|
self.console.print("[green]✓ Changes saved successfully![/green]")
|
|
522
|
+
|
|
523
|
+
# Auto-deploy enabled agents to .claude/agents/
|
|
524
|
+
self._auto_deploy_enabled_agents(agents)
|
|
449
525
|
else:
|
|
450
526
|
self.console.print("[yellow]No changes to save.[/yellow]")
|
|
451
527
|
Prompt.ask("Press Enter to continue")
|
|
@@ -473,6 +549,60 @@ class ConfigureCommand(BaseCommand):
|
|
|
473
549
|
agent.name, not current
|
|
474
550
|
)
|
|
475
551
|
|
|
552
|
+
def _auto_deploy_enabled_agents(self, agents: List[AgentConfig]) -> None:
|
|
553
|
+
"""Auto-deploy enabled agents after saving configuration.
|
|
554
|
+
|
|
555
|
+
WHY: When users enable agents, they expect them to be deployed
|
|
556
|
+
automatically to .claude/agents/ so they're available for use.
|
|
557
|
+
"""
|
|
558
|
+
try:
|
|
559
|
+
# Get list of enabled agents from states
|
|
560
|
+
enabled_agents = [
|
|
561
|
+
agent
|
|
562
|
+
for agent in agents
|
|
563
|
+
if self.agent_manager.is_agent_enabled(agent.name)
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
if not enabled_agents:
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
# Show deployment progress
|
|
570
|
+
self.console.print(
|
|
571
|
+
f"\n[bold blue]Deploying {len(enabled_agents)} enabled agent(s)...[/bold blue]"
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# Deploy each enabled agent
|
|
575
|
+
success_count = 0
|
|
576
|
+
failed_count = 0
|
|
577
|
+
|
|
578
|
+
for agent in enabled_agents:
|
|
579
|
+
# Deploy to .claude/agents/ (project-level)
|
|
580
|
+
try:
|
|
581
|
+
if self._deploy_single_agent(agent, show_feedback=False):
|
|
582
|
+
success_count += 1
|
|
583
|
+
self.console.print(f"[green]✓ Deployed: {agent.name}[/green]")
|
|
584
|
+
else:
|
|
585
|
+
failed_count += 1
|
|
586
|
+
self.console.print(f"[yellow]⚠ Skipped: {agent.name}[/yellow]")
|
|
587
|
+
except Exception as e:
|
|
588
|
+
failed_count += 1
|
|
589
|
+
self.logger.error(f"Failed to deploy {agent.name}: {e}")
|
|
590
|
+
self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
|
|
591
|
+
|
|
592
|
+
# Show summary
|
|
593
|
+
if success_count > 0:
|
|
594
|
+
self.console.print(
|
|
595
|
+
f"\n[green]✓ Successfully deployed {success_count} agent(s) to .claude/agents/[/green]"
|
|
596
|
+
)
|
|
597
|
+
if failed_count > 0:
|
|
598
|
+
self.console.print(
|
|
599
|
+
f"[yellow]⚠ {failed_count} agent(s) failed or were skipped[/yellow]"
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
except Exception as e:
|
|
603
|
+
self.logger.error(f"Auto-deployment failed: {e}", exc_info=True)
|
|
604
|
+
self.console.print(f"[red]✗ Auto-deployment error: {e}[/red]")
|
|
605
|
+
|
|
476
606
|
def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
|
|
477
607
|
"""Customize agent JSON template."""
|
|
478
608
|
self.template_editor.customize_agent_template(agents)
|
|
@@ -560,6 +690,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
560
690
|
|
|
561
691
|
# Get list of enabled agents
|
|
562
692
|
agents = self.agent_manager.discover_agents()
|
|
693
|
+
# Filter BASE_AGENT from all agent operations (1M-502 Phase 1)
|
|
694
|
+
agents = self._filter_agent_configs(agents, filter_deployed=False)
|
|
563
695
|
enabled_agents = [
|
|
564
696
|
a.name
|
|
565
697
|
for a in agents
|
|
@@ -603,9 +735,9 @@ class ConfigureCommand(BaseCommand):
|
|
|
603
735
|
else:
|
|
604
736
|
from rich.table import Table
|
|
605
737
|
|
|
606
|
-
table = Table(show_header=True, header_style="bold
|
|
607
|
-
table.add_column("Agent", style="
|
|
608
|
-
table.add_column("Skills", style="green")
|
|
738
|
+
table = Table(show_header=True, header_style="bold white")
|
|
739
|
+
table.add_column("Agent", style="white", no_wrap=True)
|
|
740
|
+
table.add_column("Skills", style="green", no_wrap=True)
|
|
609
741
|
|
|
610
742
|
for agent_id, skills in mappings.items():
|
|
611
743
|
skills_str = (
|
|
@@ -626,6 +758,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
626
758
|
|
|
627
759
|
# Get enabled agents
|
|
628
760
|
agents = self.agent_manager.discover_agents()
|
|
761
|
+
# Filter BASE_AGENT from all agent operations (1M-502 Phase 1)
|
|
762
|
+
agents = self._filter_agent_configs(agents, filter_deployed=False)
|
|
629
763
|
enabled_agents = [
|
|
630
764
|
a.name
|
|
631
765
|
for a in agents
|
|
@@ -757,6 +891,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
757
891
|
def _list_agents_non_interactive(self) -> CommandResult:
|
|
758
892
|
"""List agents in non-interactive mode."""
|
|
759
893
|
agents = self.agent_manager.discover_agents()
|
|
894
|
+
# Filter BASE_AGENT from all agent lists (1M-502 Phase 1)
|
|
895
|
+
agents = self._filter_agent_configs(agents, filter_deployed=False)
|
|
760
896
|
|
|
761
897
|
data = []
|
|
762
898
|
for agent in agents:
|
|
@@ -876,14 +1012,14 @@ class ConfigureCommand(BaseCommand):
|
|
|
876
1012
|
identifier = repo.identifier
|
|
877
1013
|
|
|
878
1014
|
# Count agents in cache
|
|
1015
|
+
# Note: identifier already includes subdirectory path (e.g., "bobmatnyc/claude-mpm-agents/agents")
|
|
879
1016
|
cache_dir = (
|
|
880
1017
|
Path.home() / ".claude-mpm" / "cache" / "remote-agents" / identifier
|
|
881
1018
|
)
|
|
882
1019
|
agent_count = 0
|
|
883
1020
|
if cache_dir.exists():
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
agent_count = len(list(agents_dir.rglob("*.md")))
|
|
1021
|
+
# cache_dir IS the agents directory - no need to append /agents
|
|
1022
|
+
agent_count = len(list(cache_dir.rglob("*.md")))
|
|
887
1023
|
|
|
888
1024
|
sources.append(
|
|
889
1025
|
{
|
|
@@ -900,41 +1036,263 @@ class ConfigureCommand(BaseCommand):
|
|
|
900
1036
|
self.logger.warning(f"Failed to get configured sources: {e}")
|
|
901
1037
|
return []
|
|
902
1038
|
|
|
1039
|
+
def _filter_agent_configs(
|
|
1040
|
+
self, agents: List[AgentConfig], filter_deployed: bool = False
|
|
1041
|
+
) -> List[AgentConfig]:
|
|
1042
|
+
"""Filter AgentConfig objects using agent_filters utilities.
|
|
1043
|
+
|
|
1044
|
+
Converts AgentConfig objects to dictionaries for filtering,
|
|
1045
|
+
then back to AgentConfig. Always filters BASE_AGENT.
|
|
1046
|
+
Optionally filters deployed agents.
|
|
1047
|
+
|
|
1048
|
+
Args:
|
|
1049
|
+
agents: List of AgentConfig objects
|
|
1050
|
+
filter_deployed: Whether to filter out deployed agents (default: False)
|
|
1051
|
+
|
|
1052
|
+
Returns:
|
|
1053
|
+
Filtered list of AgentConfig objects
|
|
1054
|
+
"""
|
|
1055
|
+
# Convert AgentConfig to dict format for filtering
|
|
1056
|
+
agent_dicts = []
|
|
1057
|
+
for agent in agents:
|
|
1058
|
+
agent_dicts.append(
|
|
1059
|
+
{
|
|
1060
|
+
"agent_id": agent.name,
|
|
1061
|
+
"name": agent.name,
|
|
1062
|
+
"description": agent.description,
|
|
1063
|
+
"deployed": getattr(agent, "is_deployed", False),
|
|
1064
|
+
}
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
# Apply filters (always filter BASE_AGENT)
|
|
1068
|
+
filtered_dicts = apply_all_filters(
|
|
1069
|
+
agent_dicts, filter_base=True, filter_deployed=filter_deployed
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
# Convert back to AgentConfig objects
|
|
1073
|
+
filtered_names = {d["agent_id"] for d in filtered_dicts}
|
|
1074
|
+
return [a for a in agents if a.name in filtered_names]
|
|
1075
|
+
|
|
1076
|
+
@staticmethod
|
|
1077
|
+
def _calculate_column_widths(
|
|
1078
|
+
terminal_width: int, columns: Dict[str, int]
|
|
1079
|
+
) -> Dict[str, int]:
|
|
1080
|
+
"""Calculate dynamic column widths based on terminal size.
|
|
1081
|
+
|
|
1082
|
+
Args:
|
|
1083
|
+
terminal_width: Current terminal width in characters
|
|
1084
|
+
columns: Dict mapping column names to minimum widths
|
|
1085
|
+
|
|
1086
|
+
Returns:
|
|
1087
|
+
Dict mapping column names to calculated widths
|
|
1088
|
+
|
|
1089
|
+
Design:
|
|
1090
|
+
- Ensures minimum widths are respected
|
|
1091
|
+
- Distributes extra space proportionally
|
|
1092
|
+
- Handles narrow terminals gracefully (minimum 80 chars)
|
|
1093
|
+
"""
|
|
1094
|
+
# Ensure minimum terminal width
|
|
1095
|
+
min_terminal_width = 80
|
|
1096
|
+
terminal_width = max(terminal_width, min_terminal_width)
|
|
1097
|
+
|
|
1098
|
+
# Calculate total minimum width needed
|
|
1099
|
+
total_min_width = sum(columns.values())
|
|
1100
|
+
|
|
1101
|
+
# Account for table borders and padding (2 chars per column + 2 for edges)
|
|
1102
|
+
overhead = (len(columns) * 2) + 2
|
|
1103
|
+
available_width = terminal_width - overhead
|
|
1104
|
+
|
|
1105
|
+
# If we have extra space, distribute proportionally
|
|
1106
|
+
if available_width > total_min_width:
|
|
1107
|
+
extra_space = available_width - total_min_width
|
|
1108
|
+
total_weight = sum(columns.values())
|
|
1109
|
+
|
|
1110
|
+
result = {}
|
|
1111
|
+
for col_name, min_width in columns.items():
|
|
1112
|
+
# Distribute extra space based on minimum width proportion
|
|
1113
|
+
proportion = min_width / total_weight
|
|
1114
|
+
extra = int(extra_space * proportion)
|
|
1115
|
+
result[col_name] = min_width + extra
|
|
1116
|
+
return result
|
|
1117
|
+
# Terminal too narrow, use minimum widths
|
|
1118
|
+
return columns.copy()
|
|
1119
|
+
|
|
1120
|
+
def _format_display_name(self, name: str) -> str:
|
|
1121
|
+
"""Format internal agent name to human-readable display name.
|
|
1122
|
+
|
|
1123
|
+
Converts underscores/hyphens to spaces and title-cases.
|
|
1124
|
+
Examples:
|
|
1125
|
+
agentic_coder_optimizer -> Agentic Coder Optimizer
|
|
1126
|
+
python-engineer -> Python Engineer
|
|
1127
|
+
api_qa_agent -> Api Qa Agent
|
|
1128
|
+
|
|
1129
|
+
Args:
|
|
1130
|
+
name: Internal agent name (may contain underscores, hyphens)
|
|
1131
|
+
|
|
1132
|
+
Returns:
|
|
1133
|
+
Human-readable display name
|
|
1134
|
+
"""
|
|
1135
|
+
return name.replace("_", " ").replace("-", " ").title()
|
|
1136
|
+
|
|
903
1137
|
def _display_agents_with_source_info(self, agents: List[AgentConfig]) -> None:
|
|
904
|
-
"""Display agents table with source information and
|
|
1138
|
+
"""Display agents table with source information and installation status."""
|
|
905
1139
|
from rich.table import Table
|
|
906
1140
|
|
|
1141
|
+
# Get recommended agents for this project
|
|
1142
|
+
try:
|
|
1143
|
+
recommended_agents = self.recommendation_service.get_recommended_agents(
|
|
1144
|
+
str(self.project_dir)
|
|
1145
|
+
)
|
|
1146
|
+
except Exception as e:
|
|
1147
|
+
self.logger.warning(f"Failed to get recommended agents: {e}")
|
|
1148
|
+
recommended_agents = set()
|
|
1149
|
+
|
|
1150
|
+
# Get terminal width and calculate dynamic column widths
|
|
1151
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
1152
|
+
min_widths = {
|
|
1153
|
+
"#": 4,
|
|
1154
|
+
"Agent ID": 30,
|
|
1155
|
+
"Name": 20,
|
|
1156
|
+
"Source": 15,
|
|
1157
|
+
"Status": 10,
|
|
1158
|
+
}
|
|
1159
|
+
widths = self._calculate_column_widths(terminal_width, min_widths)
|
|
1160
|
+
|
|
907
1161
|
agents_table = Table(show_header=True, header_style="bold cyan")
|
|
908
|
-
agents_table.add_column(
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
agents_table.add_column(
|
|
912
|
-
|
|
1162
|
+
agents_table.add_column(
|
|
1163
|
+
"#", style="bright_black", width=widths["#"], no_wrap=True
|
|
1164
|
+
)
|
|
1165
|
+
agents_table.add_column(
|
|
1166
|
+
"Agent ID",
|
|
1167
|
+
style="bright_black",
|
|
1168
|
+
width=widths["Agent ID"],
|
|
1169
|
+
no_wrap=True,
|
|
1170
|
+
overflow="ellipsis",
|
|
1171
|
+
)
|
|
1172
|
+
agents_table.add_column(
|
|
1173
|
+
"Name",
|
|
1174
|
+
style="bright_cyan",
|
|
1175
|
+
width=widths["Name"],
|
|
1176
|
+
no_wrap=True,
|
|
1177
|
+
overflow="ellipsis",
|
|
1178
|
+
)
|
|
1179
|
+
agents_table.add_column(
|
|
1180
|
+
"Source",
|
|
1181
|
+
style="bright_yellow",
|
|
1182
|
+
width=widths["Source"],
|
|
1183
|
+
no_wrap=True,
|
|
1184
|
+
)
|
|
1185
|
+
agents_table.add_column(
|
|
1186
|
+
"Status", style="bright_black", width=widths["Status"], no_wrap=True
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
# FIX 3: Get deployed agent IDs once, before the loop (efficiency)
|
|
1190
|
+
deployed_ids = get_deployed_agent_ids()
|
|
913
1191
|
|
|
1192
|
+
recommended_count = 0
|
|
914
1193
|
for idx, agent in enumerate(agents, 1):
|
|
915
|
-
# Determine source
|
|
1194
|
+
# Determine source with repo name
|
|
916
1195
|
source_type = getattr(agent, "source_type", "local")
|
|
917
|
-
source_label = "Remote" if source_type == "remote" else "Local"
|
|
918
1196
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1197
|
+
if source_type == "remote":
|
|
1198
|
+
# Get repo name from agent metadata
|
|
1199
|
+
source_dict = getattr(agent, "source_dict", {})
|
|
1200
|
+
repo_url = source_dict.get("source", "")
|
|
1201
|
+
|
|
1202
|
+
# Extract repo name from URL
|
|
1203
|
+
if (
|
|
1204
|
+
"bobmatnyc/claude-mpm" in repo_url
|
|
1205
|
+
or "claude-mpm" in repo_url.lower()
|
|
1206
|
+
):
|
|
1207
|
+
source_label = "MPM Agents"
|
|
1208
|
+
elif "/" in repo_url:
|
|
1209
|
+
# Extract last part of org/repo
|
|
1210
|
+
parts = repo_url.rstrip("/").split("/")
|
|
1211
|
+
if len(parts) >= 2:
|
|
1212
|
+
source_label = f"{parts[-2]}/{parts[-1]}"
|
|
1213
|
+
else:
|
|
1214
|
+
source_label = "Community"
|
|
1215
|
+
else:
|
|
1216
|
+
source_label = "Community"
|
|
1217
|
+
else:
|
|
1218
|
+
source_label = "Local"
|
|
1219
|
+
|
|
1220
|
+
# FIX 2: Check actual deployment status from .claude/agents/ directory
|
|
1221
|
+
is_installed = agent.name in deployed_ids
|
|
1222
|
+
if is_installed:
|
|
1223
|
+
status = "[green]Installed[/green]"
|
|
1224
|
+
else:
|
|
1225
|
+
status = "Available"
|
|
1226
|
+
|
|
1227
|
+
# Check if agent is recommended
|
|
1228
|
+
# Handle both hierarchical paths (e.g., "engineer/backend/python-engineer")
|
|
1229
|
+
# and leaf names (e.g., "python-engineer")
|
|
1230
|
+
agent_full_path = agent.name
|
|
1231
|
+
agent_leaf_name = (
|
|
1232
|
+
agent_full_path.split("/")[-1]
|
|
1233
|
+
if "/" in agent_full_path
|
|
1234
|
+
else agent_full_path
|
|
1235
|
+
)
|
|
922
1236
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1237
|
+
for recommended_id in recommended_agents:
|
|
1238
|
+
# Check if the recommended_id matches either the full path or just the leaf name
|
|
1239
|
+
recommended_leaf = (
|
|
1240
|
+
recommended_id.split("/")[-1]
|
|
1241
|
+
if "/" in recommended_id
|
|
1242
|
+
else recommended_id
|
|
1243
|
+
)
|
|
1244
|
+
if (
|
|
1245
|
+
agent_full_path == recommended_id
|
|
1246
|
+
or agent_leaf_name == recommended_leaf
|
|
1247
|
+
):
|
|
1248
|
+
recommended_count += 1
|
|
1249
|
+
break
|
|
1250
|
+
|
|
1251
|
+
# FIX 1: Removed asterisk - using Status column instead
|
|
1252
|
+
agent_id_display = agent.name
|
|
1253
|
+
|
|
1254
|
+
# Get display name and format it properly
|
|
1255
|
+
# Raw display_name from YAML may contain underscores (e.g., "agentic_coder_optimizer")
|
|
1256
|
+
raw_display_name = getattr(agent, "display_name", agent.name)
|
|
1257
|
+
display_name = self._format_display_name(raw_display_name)
|
|
927
1258
|
|
|
928
1259
|
agents_table.add_row(
|
|
929
|
-
str(idx),
|
|
1260
|
+
str(idx), agent_id_display, display_name, source_label, status
|
|
930
1261
|
)
|
|
931
1262
|
|
|
932
1263
|
self.console.print(agents_table)
|
|
933
|
-
|
|
1264
|
+
|
|
1265
|
+
# Show legend if there are recommended agents
|
|
1266
|
+
if recommended_count > 0:
|
|
1267
|
+
# Get detection summary for context
|
|
1268
|
+
try:
|
|
1269
|
+
summary = self.recommendation_service.get_detection_summary(
|
|
1270
|
+
str(self.project_dir)
|
|
1271
|
+
)
|
|
1272
|
+
detected_langs = (
|
|
1273
|
+
", ".join(summary.get("detected_languages", [])) or "None"
|
|
1274
|
+
)
|
|
1275
|
+
", ".join(summary.get("detected_frameworks", [])) or "None"
|
|
1276
|
+
self.console.print(
|
|
1277
|
+
f"\n[dim]* = recommended for this project "
|
|
1278
|
+
f"(detected: {detected_langs})[/dim]"
|
|
1279
|
+
)
|
|
1280
|
+
except Exception:
|
|
1281
|
+
self.console.print("\n[dim]* = recommended for this project[/dim]")
|
|
1282
|
+
|
|
1283
|
+
# Show installed vs available count (use deployed_ids for accuracy)
|
|
1284
|
+
installed_count = sum(1 for a in agents if a.name in deployed_ids)
|
|
1285
|
+
available_count = len(agents) - installed_count
|
|
1286
|
+
self.console.print(
|
|
1287
|
+
f"\n[green]✓ {installed_count} installed[/green] | "
|
|
1288
|
+
f"[dim]{available_count} available[/dim] | "
|
|
1289
|
+
f"[yellow]{recommended_count} recommended[/yellow] | "
|
|
1290
|
+
f"[dim]Total: {len(agents)}[/dim]"
|
|
1291
|
+
)
|
|
934
1292
|
|
|
935
1293
|
def _manage_sources(self) -> None:
|
|
936
1294
|
"""Interactive source management."""
|
|
937
|
-
self.console.print("\n[bold
|
|
1295
|
+
self.console.print("\n[bold white]═══ Manage Agent Sources ═══[/bold white]\n")
|
|
938
1296
|
self.console.print(
|
|
939
1297
|
"[dim]Use 'claude-mpm agent-source' command to add/remove sources[/dim]"
|
|
940
1298
|
)
|
|
@@ -944,44 +1302,874 @@ class ConfigureCommand(BaseCommand):
|
|
|
944
1302
|
self.console.print(" claude-mpm agent-source list")
|
|
945
1303
|
Prompt.ask("\nPress Enter to continue")
|
|
946
1304
|
|
|
947
|
-
def
|
|
948
|
-
"""
|
|
1305
|
+
def _deploy_agents_unified(self, agents: List[AgentConfig]) -> None:
|
|
1306
|
+
"""Unified agent selection with inline controls for recommended, presets, and collections.
|
|
1307
|
+
|
|
1308
|
+
Design:
|
|
1309
|
+
- Single nested checkbox list with grouped agents by source/category
|
|
1310
|
+
- Inline controls at top: Select all, Select recommended, Select presets
|
|
1311
|
+
- Asterisk (*) marks recommended agents
|
|
1312
|
+
- Visual hierarchy: Source → Category → Individual agents
|
|
1313
|
+
- Loop with visual feedback: Controls update checkmarks immediately
|
|
1314
|
+
"""
|
|
949
1315
|
if not agents:
|
|
950
|
-
self.console.print("[yellow]No agents available
|
|
1316
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
951
1317
|
Prompt.ask("\nPress Enter to continue")
|
|
952
1318
|
return
|
|
953
1319
|
|
|
954
|
-
|
|
955
|
-
|
|
1320
|
+
from claude_mpm.utils.agent_filters import (
|
|
1321
|
+
filter_base_agents,
|
|
1322
|
+
get_deployed_agent_ids,
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
# Filter BASE_AGENT but keep deployed agents visible
|
|
1326
|
+
all_agents = filter_base_agents(
|
|
1327
|
+
[
|
|
1328
|
+
{
|
|
1329
|
+
"agent_id": a.name,
|
|
1330
|
+
"name": a.name,
|
|
1331
|
+
"description": a.description,
|
|
1332
|
+
"deployed": getattr(a, "is_deployed", False),
|
|
1333
|
+
}
|
|
1334
|
+
for a in agents
|
|
1335
|
+
]
|
|
1336
|
+
)
|
|
956
1337
|
|
|
957
|
-
if not
|
|
958
|
-
self.console.print("[yellow]
|
|
1338
|
+
if not all_agents:
|
|
1339
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
959
1340
|
Prompt.ask("\nPress Enter to continue")
|
|
960
1341
|
return
|
|
961
1342
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
display_name = getattr(agent, "display_name", agent.name)
|
|
965
|
-
self.console.print(f" {idx}. {agent.name} - {display_name}")
|
|
1343
|
+
# Get deployed agent IDs and recommended agents
|
|
1344
|
+
deployed_ids = get_deployed_agent_ids()
|
|
966
1345
|
|
|
967
|
-
|
|
968
|
-
|
|
1346
|
+
try:
|
|
1347
|
+
recommended_agent_ids = self.recommendation_service.get_recommended_agents(
|
|
1348
|
+
str(self.project_dir)
|
|
1349
|
+
)
|
|
1350
|
+
except Exception as e:
|
|
1351
|
+
self.logger.warning(f"Failed to get recommended agents: {e}")
|
|
1352
|
+
recommended_agent_ids = set()
|
|
1353
|
+
|
|
1354
|
+
# Build mapping: leaf name -> full path for deployed agents
|
|
1355
|
+
deployed_full_paths = set()
|
|
1356
|
+
for agent in agents:
|
|
1357
|
+
agent_leaf_name = agent.name.split("/")[-1]
|
|
1358
|
+
if agent_leaf_name in deployed_ids:
|
|
1359
|
+
deployed_full_paths.add(agent.name)
|
|
1360
|
+
|
|
1361
|
+
# Track current selection state (starts with deployed, updated in loop)
|
|
1362
|
+
current_selection = deployed_full_paths.copy()
|
|
1363
|
+
|
|
1364
|
+
# Group agents by source/collection
|
|
1365
|
+
agent_map = {}
|
|
1366
|
+
collections = defaultdict(list)
|
|
1367
|
+
|
|
1368
|
+
for agent in agents:
|
|
1369
|
+
if agent.name in {a["agent_id"] for a in all_agents}:
|
|
1370
|
+
# Determine collection ID
|
|
1371
|
+
source_type = getattr(agent, "source_type", "local")
|
|
1372
|
+
if source_type == "remote":
|
|
1373
|
+
source_dict = getattr(agent, "source_dict", {})
|
|
1374
|
+
repo_url = source_dict.get("source", "")
|
|
1375
|
+
if "/" in repo_url:
|
|
1376
|
+
parts = repo_url.rstrip("/").split("/")
|
|
1377
|
+
if len(parts) >= 2:
|
|
1378
|
+
# Use more readable collection name
|
|
1379
|
+
if (
|
|
1380
|
+
"bobmatnyc/claude-mpm" in repo_url
|
|
1381
|
+
or "claude-mpm" in repo_url.lower()
|
|
1382
|
+
):
|
|
1383
|
+
collection_id = "MPM Agents"
|
|
1384
|
+
else:
|
|
1385
|
+
collection_id = f"{parts[-2]}/{parts[-1]}"
|
|
1386
|
+
else:
|
|
1387
|
+
collection_id = "Community Agents"
|
|
1388
|
+
else:
|
|
1389
|
+
collection_id = "Community Agents"
|
|
1390
|
+
else:
|
|
1391
|
+
collection_id = "Local Agents"
|
|
1392
|
+
|
|
1393
|
+
collections[collection_id].append(agent)
|
|
1394
|
+
agent_map[agent.name] = agent
|
|
1395
|
+
|
|
1396
|
+
# Monkey-patch questionary symbols for better visibility
|
|
1397
|
+
questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
|
|
1398
|
+
questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
|
|
1399
|
+
|
|
1400
|
+
# MAIN LOOP: Re-display UI when controls are used
|
|
1401
|
+
while True:
|
|
1402
|
+
# Build unified checkbox choices with inline controls
|
|
1403
|
+
choices = []
|
|
1404
|
+
|
|
1405
|
+
for collection_id in sorted(collections.keys()):
|
|
1406
|
+
agents_in_collection = collections[collection_id]
|
|
1407
|
+
|
|
1408
|
+
# Count selected/total agents in collection
|
|
1409
|
+
selected_count = sum(
|
|
1410
|
+
1
|
|
1411
|
+
for agent in agents_in_collection
|
|
1412
|
+
if agent.name in current_selection
|
|
1413
|
+
)
|
|
1414
|
+
total_count = len(agents_in_collection)
|
|
1415
|
+
|
|
1416
|
+
# Add collection header
|
|
1417
|
+
choices.append(
|
|
1418
|
+
Separator(
|
|
1419
|
+
f"\n── {collection_id} ({selected_count}/{total_count} selected) ──"
|
|
1420
|
+
)
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
# Determine if all agents in collection are selected
|
|
1424
|
+
all_selected = selected_count == total_count
|
|
1425
|
+
|
|
1426
|
+
# Add inline control: Select/Deselect all from this collection
|
|
1427
|
+
if all_selected:
|
|
1428
|
+
choices.append(
|
|
1429
|
+
Choice(
|
|
1430
|
+
f" [Deselect all from {collection_id}]",
|
|
1431
|
+
value=f"__DESELECT_ALL_{collection_id}__",
|
|
1432
|
+
checked=False,
|
|
1433
|
+
)
|
|
1434
|
+
)
|
|
1435
|
+
else:
|
|
1436
|
+
choices.append(
|
|
1437
|
+
Choice(
|
|
1438
|
+
f" [Select all from {collection_id}]",
|
|
1439
|
+
value=f"__SELECT_ALL_{collection_id}__",
|
|
1440
|
+
checked=False,
|
|
1441
|
+
)
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
# Add inline control: Select recommended from this collection
|
|
1445
|
+
recommended_in_collection = [
|
|
1446
|
+
a
|
|
1447
|
+
for a in agents_in_collection
|
|
1448
|
+
if any(
|
|
1449
|
+
a.name == rec_id
|
|
1450
|
+
or a.name.split("/")[-1] == rec_id.split("/")[-1]
|
|
1451
|
+
for rec_id in recommended_agent_ids
|
|
1452
|
+
)
|
|
1453
|
+
]
|
|
1454
|
+
if recommended_in_collection:
|
|
1455
|
+
recommended_selected = sum(
|
|
1456
|
+
1
|
|
1457
|
+
for a in recommended_in_collection
|
|
1458
|
+
if a.name in current_selection
|
|
1459
|
+
)
|
|
1460
|
+
if recommended_selected == len(recommended_in_collection):
|
|
1461
|
+
choices.append(
|
|
1462
|
+
Choice(
|
|
1463
|
+
f" [Deselect recommended ({len(recommended_in_collection)} agents)]",
|
|
1464
|
+
value=f"__DESELECT_REC_{collection_id}__",
|
|
1465
|
+
checked=False,
|
|
1466
|
+
)
|
|
1467
|
+
)
|
|
1468
|
+
else:
|
|
1469
|
+
choices.append(
|
|
1470
|
+
Choice(
|
|
1471
|
+
f" [Select recommended ({len(recommended_in_collection)} agents)]",
|
|
1472
|
+
value=f"__SELECT_REC_{collection_id}__",
|
|
1473
|
+
checked=False,
|
|
1474
|
+
)
|
|
1475
|
+
)
|
|
1476
|
+
|
|
1477
|
+
# Add separator before individual agents
|
|
1478
|
+
choices.append(Separator())
|
|
1479
|
+
|
|
1480
|
+
# Group agents by category within collection (if hierarchical)
|
|
1481
|
+
category_groups = defaultdict(list)
|
|
1482
|
+
for agent in sorted(agents_in_collection, key=lambda a: a.name):
|
|
1483
|
+
# Extract category from hierarchical path (e.g., "engineer/backend/python-engineer")
|
|
1484
|
+
parts = agent.name.split("/")
|
|
1485
|
+
if len(parts) > 1:
|
|
1486
|
+
category = "/".join(parts[:-1]) # e.g., "engineer/backend"
|
|
1487
|
+
else:
|
|
1488
|
+
category = "" # No category
|
|
1489
|
+
category_groups[category].append(agent)
|
|
1490
|
+
|
|
1491
|
+
# Display agents grouped by category
|
|
1492
|
+
for category in sorted(category_groups.keys()):
|
|
1493
|
+
agents_in_category = category_groups[category]
|
|
1494
|
+
|
|
1495
|
+
# Add category separator if hierarchical
|
|
1496
|
+
if category:
|
|
1497
|
+
choices.append(Separator(f" {category}/"))
|
|
1498
|
+
|
|
1499
|
+
# Add individual agents
|
|
1500
|
+
for agent in agents_in_category:
|
|
1501
|
+
agent_leaf_name = agent.name.split("/")[-1]
|
|
1502
|
+
display_name = getattr(agent, "display_name", agent_leaf_name)
|
|
1503
|
+
|
|
1504
|
+
# Check if agent is deployed (exists in .claude/agents/)
|
|
1505
|
+
|
|
1506
|
+
# Format choice text (no asterisk needed)
|
|
1507
|
+
choice_text = f" {display_name}"
|
|
1508
|
+
|
|
1509
|
+
is_selected = agent.name in current_selection
|
|
1510
|
+
|
|
1511
|
+
choices.append(
|
|
1512
|
+
Choice(
|
|
1513
|
+
title=choice_text,
|
|
1514
|
+
value=agent.name,
|
|
1515
|
+
checked=is_selected,
|
|
1516
|
+
)
|
|
1517
|
+
)
|
|
1518
|
+
|
|
1519
|
+
self.console.print("\n[bold cyan]Select Agents to Install[/bold cyan]")
|
|
1520
|
+
self.console.print("[dim][✓] Checked = Installed (uncheck to remove)[/dim]")
|
|
1521
|
+
self.console.print(
|
|
1522
|
+
"[dim][ ] Unchecked = Available (check to install)[/dim]"
|
|
1523
|
+
)
|
|
1524
|
+
self.console.print(
|
|
1525
|
+
"[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
try:
|
|
1529
|
+
selected_values = questionary.checkbox(
|
|
1530
|
+
"Select agents:",
|
|
1531
|
+
choices=choices,
|
|
1532
|
+
instruction="(Space to toggle, Enter to continue)",
|
|
1533
|
+
style=self.QUESTIONARY_STYLE,
|
|
1534
|
+
).ask()
|
|
1535
|
+
except Exception as e:
|
|
1536
|
+
import sys
|
|
1537
|
+
|
|
1538
|
+
self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
|
|
1539
|
+
self.console.print(
|
|
1540
|
+
"[red]Error: Could not display interactive menu[/red]"
|
|
1541
|
+
)
|
|
1542
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
1543
|
+
if not sys.stdin.isatty():
|
|
1544
|
+
self.console.print("[dim]Interactive terminal required. Use:[/dim]")
|
|
1545
|
+
self.console.print(
|
|
1546
|
+
"[dim] --list-agents to see available agents[/dim]"
|
|
1547
|
+
)
|
|
1548
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1549
|
+
return
|
|
1550
|
+
|
|
1551
|
+
if selected_values is None:
|
|
1552
|
+
self.console.print("[yellow]No changes made[/yellow]")
|
|
1553
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1554
|
+
return
|
|
1555
|
+
|
|
1556
|
+
# Check for inline control selections
|
|
1557
|
+
controls_selected = [v for v in selected_values if v.startswith("__")]
|
|
1558
|
+
|
|
1559
|
+
if controls_selected:
|
|
1560
|
+
# Process controls and update current_selection
|
|
1561
|
+
for control in controls_selected:
|
|
1562
|
+
if control.startswith("__SELECT_ALL_"):
|
|
1563
|
+
collection_id = control.replace("__SELECT_ALL_", "").replace(
|
|
1564
|
+
"__", ""
|
|
1565
|
+
)
|
|
1566
|
+
# Add all agents from this collection to current_selection
|
|
1567
|
+
for agent in collections[collection_id]:
|
|
1568
|
+
current_selection.add(agent.name)
|
|
1569
|
+
elif control.startswith("__DESELECT_ALL_"):
|
|
1570
|
+
collection_id = control.replace("__DESELECT_ALL_", "").replace(
|
|
1571
|
+
"__", ""
|
|
1572
|
+
)
|
|
1573
|
+
# Remove all agents from this collection
|
|
1574
|
+
for agent in collections[collection_id]:
|
|
1575
|
+
current_selection.discard(agent.name)
|
|
1576
|
+
elif control.startswith("__SELECT_REC_"):
|
|
1577
|
+
collection_id = control.replace("__SELECT_REC_", "").replace(
|
|
1578
|
+
"__", ""
|
|
1579
|
+
)
|
|
1580
|
+
# Add all recommended agents from this collection
|
|
1581
|
+
for agent in collections[collection_id]:
|
|
1582
|
+
if any(
|
|
1583
|
+
agent.name == rec_id
|
|
1584
|
+
or agent.name.split("/")[-1] == rec_id.split("/")[-1]
|
|
1585
|
+
for rec_id in recommended_agent_ids
|
|
1586
|
+
):
|
|
1587
|
+
current_selection.add(agent.name)
|
|
1588
|
+
elif control.startswith("__DESELECT_REC_"):
|
|
1589
|
+
collection_id = control.replace("__DESELECT_REC_", "").replace(
|
|
1590
|
+
"__", ""
|
|
1591
|
+
)
|
|
1592
|
+
# Remove all recommended agents from this collection
|
|
1593
|
+
for agent in collections[collection_id]:
|
|
1594
|
+
if any(
|
|
1595
|
+
agent.name == rec_id
|
|
1596
|
+
or agent.name.split("/")[-1] == rec_id.split("/")[-1]
|
|
1597
|
+
for rec_id in recommended_agent_ids
|
|
1598
|
+
):
|
|
1599
|
+
current_selection.discard(agent.name)
|
|
1600
|
+
|
|
1601
|
+
# Loop back to re-display with updated selections
|
|
1602
|
+
continue
|
|
1603
|
+
|
|
1604
|
+
# No controls selected - use the individual selections as final
|
|
1605
|
+
final_selection = set(selected_values)
|
|
1606
|
+
break
|
|
1607
|
+
|
|
1608
|
+
# Determine changes
|
|
1609
|
+
to_deploy = final_selection - deployed_full_paths
|
|
1610
|
+
to_remove = deployed_full_paths - final_selection
|
|
1611
|
+
|
|
1612
|
+
if not to_deploy and not to_remove:
|
|
1613
|
+
self.console.print("[yellow]No changes needed[/yellow]")
|
|
1614
|
+
Prompt.ask("\nPress Enter to continue")
|
|
969
1615
|
return
|
|
970
1616
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1617
|
+
# Show what will happen
|
|
1618
|
+
self.console.print("\n[bold]Changes to apply:[/bold]")
|
|
1619
|
+
if to_deploy:
|
|
1620
|
+
self.console.print(f"[green]Install {len(to_deploy)} agent(s)[/green]")
|
|
1621
|
+
for agent_id in to_deploy:
|
|
1622
|
+
self.console.print(f" + {agent_id}")
|
|
1623
|
+
if to_remove:
|
|
1624
|
+
self.console.print(f"[red]Remove {len(to_remove)} agent(s)[/red]")
|
|
1625
|
+
for agent_id in to_remove:
|
|
1626
|
+
self.console.print(f" - {agent_id}")
|
|
1627
|
+
|
|
1628
|
+
# Confirm
|
|
1629
|
+
if not Confirm.ask("\nApply these changes?", default=True):
|
|
1630
|
+
self.console.print("[yellow]Changes cancelled[/yellow]")
|
|
1631
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1632
|
+
return
|
|
1633
|
+
|
|
1634
|
+
# Execute changes
|
|
1635
|
+
deploy_success = 0
|
|
1636
|
+
deploy_fail = 0
|
|
1637
|
+
remove_success = 0
|
|
1638
|
+
remove_fail = 0
|
|
1639
|
+
|
|
1640
|
+
# Install new agents
|
|
1641
|
+
for agent_id in to_deploy:
|
|
1642
|
+
agent = agent_map.get(agent_id)
|
|
1643
|
+
if agent and self._deploy_single_agent(agent, show_feedback=False):
|
|
1644
|
+
deploy_success += 1
|
|
1645
|
+
self.console.print(f"[green]✓ Installed: {agent_id}[/green]")
|
|
976
1646
|
else:
|
|
977
|
-
|
|
1647
|
+
deploy_fail += 1
|
|
1648
|
+
self.console.print(f"[red]✗ Failed to install: {agent_id}[/red]")
|
|
1649
|
+
|
|
1650
|
+
# Remove agents
|
|
1651
|
+
for agent_id in to_remove:
|
|
1652
|
+
try:
|
|
1653
|
+
import json
|
|
1654
|
+
|
|
1655
|
+
# Extract leaf name to match deployed filename
|
|
1656
|
+
leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
|
|
1657
|
+
|
|
1658
|
+
# Remove from all possible locations
|
|
1659
|
+
paths_to_check = [
|
|
1660
|
+
Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
|
|
1661
|
+
Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
1662
|
+
Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
1663
|
+
]
|
|
1664
|
+
|
|
1665
|
+
removed = False
|
|
1666
|
+
for path in paths_to_check:
|
|
1667
|
+
if path.exists():
|
|
1668
|
+
path.unlink()
|
|
1669
|
+
removed = True
|
|
1670
|
+
|
|
1671
|
+
# Also remove from virtual deployment state
|
|
1672
|
+
deployment_state_paths = [
|
|
1673
|
+
Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
1674
|
+
Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
1675
|
+
]
|
|
1676
|
+
|
|
1677
|
+
for state_path in deployment_state_paths:
|
|
1678
|
+
if state_path.exists():
|
|
1679
|
+
try:
|
|
1680
|
+
with state_path.open() as f:
|
|
1681
|
+
state = json.load(f)
|
|
1682
|
+
agents_in_state = state.get("last_check_results", {}).get(
|
|
1683
|
+
"agents", {}
|
|
1684
|
+
)
|
|
1685
|
+
if leaf_name in agents_in_state:
|
|
1686
|
+
del agents_in_state[leaf_name]
|
|
1687
|
+
removed = True
|
|
1688
|
+
with state_path.open("w") as f:
|
|
1689
|
+
json.dump(state, f, indent=2)
|
|
1690
|
+
except (json.JSONDecodeError, KeyError):
|
|
1691
|
+
pass
|
|
1692
|
+
|
|
1693
|
+
if removed:
|
|
1694
|
+
remove_success += 1
|
|
1695
|
+
self.console.print(f"[green]✓ Removed: {agent_id}[/green]")
|
|
1696
|
+
else:
|
|
1697
|
+
remove_fail += 1
|
|
1698
|
+
self.console.print(f"[yellow]⚠ Not found: {agent_id}[/yellow]")
|
|
1699
|
+
except Exception as e:
|
|
1700
|
+
remove_fail += 1
|
|
1701
|
+
self.console.print(f"[red]✗ Failed to remove {agent_id}: {e}[/red]")
|
|
1702
|
+
|
|
1703
|
+
# Show summary
|
|
1704
|
+
self.console.print()
|
|
1705
|
+
if deploy_success > 0:
|
|
1706
|
+
self.console.print(f"[green]✓ Installed {deploy_success} agent(s)[/green]")
|
|
1707
|
+
if deploy_fail > 0:
|
|
1708
|
+
self.console.print(f"[red]✗ Failed to install {deploy_fail} agent(s)[/red]")
|
|
1709
|
+
if remove_success > 0:
|
|
1710
|
+
self.console.print(f"[green]✓ Removed {remove_success} agent(s)[/green]")
|
|
1711
|
+
if remove_fail > 0:
|
|
1712
|
+
self.console.print(f"[red]✗ Failed to remove {remove_fail} agent(s)[/red]")
|
|
1713
|
+
|
|
1714
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1715
|
+
|
|
1716
|
+
def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
|
|
1717
|
+
"""Manage agent installation state (unified install/remove interface).
|
|
1718
|
+
|
|
1719
|
+
DEPRECATED: Use _deploy_agents_unified instead.
|
|
1720
|
+
This method is kept for backward compatibility but should not be used.
|
|
1721
|
+
"""
|
|
1722
|
+
if not agents:
|
|
1723
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
1724
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1725
|
+
return
|
|
1726
|
+
|
|
1727
|
+
# Get ALL agents (filter BASE_AGENT but keep deployed agents visible)
|
|
1728
|
+
from claude_mpm.utils.agent_filters import (
|
|
1729
|
+
filter_base_agents,
|
|
1730
|
+
get_deployed_agent_ids,
|
|
1731
|
+
)
|
|
1732
|
+
|
|
1733
|
+
# Filter BASE_AGENT but keep deployed agents visible
|
|
1734
|
+
all_agents = filter_base_agents(
|
|
1735
|
+
[
|
|
1736
|
+
{
|
|
1737
|
+
"agent_id": a.name,
|
|
1738
|
+
"name": a.name,
|
|
1739
|
+
"description": a.description,
|
|
1740
|
+
"deployed": getattr(a, "is_deployed", False),
|
|
1741
|
+
}
|
|
1742
|
+
for a in agents
|
|
1743
|
+
]
|
|
1744
|
+
)
|
|
1745
|
+
|
|
1746
|
+
# Get deployed agent IDs (original state - for calculating final changes)
|
|
1747
|
+
# NOTE: deployed_ids contains LEAF NAMES (e.g., "python-engineer")
|
|
1748
|
+
deployed_ids = get_deployed_agent_ids()
|
|
1749
|
+
|
|
1750
|
+
if not all_agents:
|
|
1751
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
1752
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1753
|
+
return
|
|
1754
|
+
|
|
1755
|
+
# Build mapping: leaf name -> full path for deployed agents
|
|
1756
|
+
# This allows comparing deployed_ids (leaf names) with agent.name (full paths)
|
|
1757
|
+
deployed_full_paths = set()
|
|
1758
|
+
for agent in agents:
|
|
1759
|
+
agent_leaf_name = agent.name.split("/")[-1]
|
|
1760
|
+
if agent_leaf_name in deployed_ids:
|
|
1761
|
+
deployed_full_paths.add(agent.name)
|
|
1762
|
+
|
|
1763
|
+
# Track current selection state (starts with deployed full paths, updated after each iteration)
|
|
1764
|
+
current_selection = deployed_full_paths.copy()
|
|
1765
|
+
|
|
1766
|
+
# Loop to allow adjusting selection
|
|
1767
|
+
while True:
|
|
1768
|
+
# Build agent mapping and collections
|
|
1769
|
+
agent_map = {} # For lookup after selection
|
|
1770
|
+
collections = defaultdict(list)
|
|
1771
|
+
|
|
1772
|
+
for agent in agents:
|
|
1773
|
+
if agent.name in {a["agent_id"] for a in all_agents}:
|
|
1774
|
+
# Determine collection ID
|
|
1775
|
+
source_type = getattr(agent, "source_type", "local")
|
|
1776
|
+
if source_type == "remote":
|
|
1777
|
+
source_dict = getattr(agent, "source_dict", {})
|
|
1778
|
+
repo_url = source_dict.get("source", "")
|
|
1779
|
+
# Extract repository name from URL
|
|
1780
|
+
if "/" in repo_url:
|
|
1781
|
+
parts = repo_url.rstrip("/").split("/")
|
|
1782
|
+
if len(parts) >= 2:
|
|
1783
|
+
collection_id = f"{parts[-2]}/{parts[-1]}"
|
|
1784
|
+
else:
|
|
1785
|
+
collection_id = "remote"
|
|
1786
|
+
else:
|
|
1787
|
+
collection_id = "remote"
|
|
1788
|
+
else:
|
|
1789
|
+
collection_id = "local"
|
|
1790
|
+
|
|
1791
|
+
collections[collection_id].append(agent)
|
|
1792
|
+
agent_map[agent.name] = agent
|
|
1793
|
+
|
|
1794
|
+
# STEP 1: Collection-level selection
|
|
1795
|
+
self.console.print("\n[bold cyan]Select Agent Collections[/bold cyan]")
|
|
1796
|
+
self.console.print(
|
|
1797
|
+
"[dim]Checking a collection installs ALL agents in that collection[/dim]"
|
|
1798
|
+
)
|
|
1799
|
+
self.console.print(
|
|
1800
|
+
"[dim]Unchecking a collection removes ALL agents in that collection[/dim]"
|
|
1801
|
+
)
|
|
1802
|
+
self.console.print(
|
|
1803
|
+
"[dim]For partial deployment, use 'Fine-tune individual agents'[/dim]\n"
|
|
1804
|
+
)
|
|
1805
|
+
|
|
1806
|
+
collection_choices = []
|
|
1807
|
+
for collection_id in sorted(collections.keys()):
|
|
1808
|
+
agents_in_collection = collections[collection_id]
|
|
1809
|
+
|
|
1810
|
+
# Check if ANY agent in this collection is currently deployed
|
|
1811
|
+
# This reflects actual deployment state, not just selection
|
|
1812
|
+
any_deployed = any(
|
|
1813
|
+
agent.name in current_selection for agent in agents_in_collection
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
# Count deployed agents for display
|
|
1817
|
+
deployed_count = sum(
|
|
1818
|
+
1
|
|
1819
|
+
for agent in agents_in_collection
|
|
1820
|
+
if agent.name in current_selection
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
collection_choices.append(
|
|
1824
|
+
Choice(
|
|
1825
|
+
f"{collection_id} ({deployed_count}/{len(agents_in_collection)} deployed)",
|
|
1826
|
+
value=collection_id,
|
|
1827
|
+
checked=any_deployed,
|
|
1828
|
+
)
|
|
1829
|
+
)
|
|
1830
|
+
|
|
1831
|
+
# Add option to fine-tune individual agents
|
|
1832
|
+
collection_choices.append(Separator())
|
|
1833
|
+
collection_choices.append(
|
|
1834
|
+
Choice(
|
|
1835
|
+
"→ Fine-tune individual agents...",
|
|
1836
|
+
value="__INDIVIDUAL__",
|
|
1837
|
+
checked=False,
|
|
1838
|
+
)
|
|
1839
|
+
)
|
|
1840
|
+
|
|
1841
|
+
# Monkey-patch questionary symbols for better visibility
|
|
1842
|
+
questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
|
|
1843
|
+
questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
|
|
1844
|
+
|
|
1845
|
+
try:
|
|
1846
|
+
selected_collections = questionary.checkbox(
|
|
1847
|
+
"Select agent collections to deploy:",
|
|
1848
|
+
choices=collection_choices,
|
|
1849
|
+
instruction="(Space to toggle, Enter to continue)",
|
|
1850
|
+
style=self.QUESTIONARY_STYLE,
|
|
1851
|
+
).ask()
|
|
1852
|
+
except Exception as e:
|
|
1853
|
+
import sys
|
|
1854
|
+
|
|
1855
|
+
self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
|
|
1856
|
+
self.console.print(
|
|
1857
|
+
"[red]Error: Could not display interactive menu[/red]"
|
|
1858
|
+
)
|
|
1859
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
1860
|
+
if not sys.stdin.isatty():
|
|
1861
|
+
self.console.print("[dim]Interactive terminal required. Use:[/dim]")
|
|
1862
|
+
self.console.print(
|
|
1863
|
+
"[dim] --list-agents to see available agents[/dim]"
|
|
1864
|
+
)
|
|
1865
|
+
self.console.print(
|
|
1866
|
+
"[dim] --enable-agent/--disable-agent for scripting[/dim]"
|
|
1867
|
+
)
|
|
1868
|
+
else:
|
|
1869
|
+
self.console.print(
|
|
1870
|
+
"[dim]This might be a terminal compatibility issue.[/dim]"
|
|
1871
|
+
)
|
|
978
1872
|
Prompt.ask("\nPress Enter to continue")
|
|
979
|
-
|
|
980
|
-
|
|
1873
|
+
return
|
|
1874
|
+
|
|
1875
|
+
# Handle cancellation
|
|
1876
|
+
if selected_collections is None:
|
|
1877
|
+
import sys
|
|
1878
|
+
|
|
1879
|
+
if not sys.stdin.isatty():
|
|
1880
|
+
self.console.print(
|
|
1881
|
+
"[red]Error: Interactive terminal required for agent selection[/red]"
|
|
1882
|
+
)
|
|
1883
|
+
self.console.print(
|
|
1884
|
+
"[dim]Use --list-agents to see available agents[/dim]"
|
|
1885
|
+
)
|
|
1886
|
+
self.console.print(
|
|
1887
|
+
"[dim]Use --enable-agent/--disable-agent for non-interactive mode[/dim]"
|
|
1888
|
+
)
|
|
1889
|
+
else:
|
|
1890
|
+
self.console.print("[yellow]No changes made[/yellow]")
|
|
1891
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1892
|
+
return
|
|
1893
|
+
|
|
1894
|
+
# STEP 2: Check if user wants individual selection
|
|
1895
|
+
if "__INDIVIDUAL__" in selected_collections:
|
|
1896
|
+
# Remove the __INDIVIDUAL__ marker
|
|
1897
|
+
selected_collections = [
|
|
1898
|
+
c for c in selected_collections if c != "__INDIVIDUAL__"
|
|
1899
|
+
]
|
|
1900
|
+
|
|
1901
|
+
# Build individual agent choices with grouping
|
|
1902
|
+
agent_choices = []
|
|
1903
|
+
for collection_id in sorted(collections.keys()):
|
|
1904
|
+
agents_in_collection = collections[collection_id]
|
|
1905
|
+
|
|
1906
|
+
# Add collection header separator
|
|
1907
|
+
agent_choices.append(
|
|
1908
|
+
Separator(
|
|
1909
|
+
f"\n── {collection_id} ({len(agents_in_collection)} agents) ──"
|
|
1910
|
+
)
|
|
1911
|
+
)
|
|
1912
|
+
|
|
1913
|
+
# Add individual agents from this collection
|
|
1914
|
+
for agent in sorted(agents_in_collection, key=lambda a: a.name):
|
|
1915
|
+
display_name = getattr(agent, "display_name", agent.name)
|
|
1916
|
+
is_selected = agent.name in deployed_full_paths
|
|
1917
|
+
|
|
1918
|
+
choice_text = f"{agent.name}"
|
|
1919
|
+
if display_name and display_name != agent.name:
|
|
1920
|
+
choice_text += f" - {display_name}"
|
|
1921
|
+
|
|
1922
|
+
agent_choices.append(
|
|
1923
|
+
Choice(
|
|
1924
|
+
title=choice_text, value=agent.name, checked=is_selected
|
|
1925
|
+
)
|
|
1926
|
+
)
|
|
1927
|
+
|
|
1928
|
+
self.console.print(
|
|
1929
|
+
"\n[bold cyan]Fine-tune Individual Agents[/bold cyan]"
|
|
1930
|
+
)
|
|
1931
|
+
self.console.print(
|
|
1932
|
+
"[dim][✓] Checked = Installed (uncheck to remove)[/dim]"
|
|
1933
|
+
)
|
|
1934
|
+
self.console.print(
|
|
1935
|
+
"[dim][ ] Unchecked = Available (check to install)[/dim]"
|
|
1936
|
+
)
|
|
1937
|
+
self.console.print(
|
|
1938
|
+
"[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
|
|
1939
|
+
)
|
|
1940
|
+
|
|
1941
|
+
try:
|
|
1942
|
+
selected_agent_ids = questionary.checkbox(
|
|
1943
|
+
"Select individual agents:",
|
|
1944
|
+
choices=agent_choices,
|
|
1945
|
+
style=self.QUESTIONARY_STYLE,
|
|
1946
|
+
).ask()
|
|
1947
|
+
except Exception as e:
|
|
1948
|
+
import sys
|
|
1949
|
+
|
|
1950
|
+
self.logger.error(
|
|
1951
|
+
f"Questionary checkbox failed: {e}", exc_info=True
|
|
1952
|
+
)
|
|
1953
|
+
self.console.print(
|
|
1954
|
+
"[red]Error: Could not display interactive menu[/red]"
|
|
1955
|
+
)
|
|
1956
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
1957
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1958
|
+
return
|
|
1959
|
+
|
|
1960
|
+
if selected_agent_ids is None:
|
|
1961
|
+
self.console.print("[yellow]No changes made[/yellow]")
|
|
1962
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1963
|
+
return
|
|
1964
|
+
|
|
1965
|
+
# Update current_selection with individual selections
|
|
1966
|
+
current_selection = set(selected_agent_ids)
|
|
1967
|
+
else:
|
|
1968
|
+
# Apply collection-level selections
|
|
1969
|
+
# For each collection, if it's selected, include ALL its agents
|
|
1970
|
+
# If it's not selected, exclude ALL its agents
|
|
1971
|
+
final_selections = set()
|
|
1972
|
+
for collection_id in selected_collections:
|
|
1973
|
+
for agent in collections[collection_id]:
|
|
1974
|
+
final_selections.add(agent.name)
|
|
1975
|
+
|
|
1976
|
+
# Update current_selection
|
|
1977
|
+
# This replaces the previous selection entirely with the new collection selections
|
|
1978
|
+
current_selection = final_selections
|
|
1979
|
+
|
|
1980
|
+
# Determine actions based on ORIGINAL deployed state
|
|
1981
|
+
# Compare full paths to full paths (deployed_full_paths was built from deployed_ids)
|
|
1982
|
+
to_deploy = (
|
|
1983
|
+
current_selection - deployed_full_paths
|
|
1984
|
+
) # Selected but not originally deployed
|
|
1985
|
+
|
|
1986
|
+
# For removal, verify files actually exist before adding to the set
|
|
1987
|
+
# This prevents "Not found" warnings when multiple agents share leaf names
|
|
1988
|
+
to_remove = set()
|
|
1989
|
+
for agent_id in deployed_full_paths - current_selection:
|
|
1990
|
+
# Extract leaf name to check file existence
|
|
1991
|
+
leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
|
|
1992
|
+
|
|
1993
|
+
# Check all possible locations
|
|
1994
|
+
paths_to_check = [
|
|
1995
|
+
Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
|
|
1996
|
+
Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
1997
|
+
Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
1998
|
+
]
|
|
1999
|
+
|
|
2000
|
+
# Also check virtual deployment state
|
|
2001
|
+
state_exists = False
|
|
2002
|
+
deployment_state_paths = [
|
|
2003
|
+
Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2004
|
+
Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2005
|
+
]
|
|
2006
|
+
|
|
2007
|
+
for state_path in deployment_state_paths:
|
|
2008
|
+
if state_path.exists():
|
|
2009
|
+
try:
|
|
2010
|
+
import json
|
|
2011
|
+
|
|
2012
|
+
with state_path.open() as f:
|
|
2013
|
+
state = json.load(f)
|
|
2014
|
+
agents_in_state = state.get("last_check_results", {}).get(
|
|
2015
|
+
"agents", {}
|
|
2016
|
+
)
|
|
2017
|
+
if leaf_name in agents_in_state:
|
|
2018
|
+
state_exists = True
|
|
2019
|
+
break
|
|
2020
|
+
except (json.JSONDecodeError, KeyError):
|
|
2021
|
+
continue
|
|
2022
|
+
|
|
2023
|
+
# Only add to removal set if file or state entry actually exists
|
|
2024
|
+
if any(p.exists() for p in paths_to_check) or state_exists:
|
|
2025
|
+
to_remove.add(agent_id)
|
|
2026
|
+
|
|
2027
|
+
if not to_deploy and not to_remove:
|
|
2028
|
+
self.console.print(
|
|
2029
|
+
"[yellow]No changes needed - all selected agents are already installed[/yellow]"
|
|
2030
|
+
)
|
|
2031
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2032
|
+
return
|
|
2033
|
+
|
|
2034
|
+
# Show what will happen
|
|
2035
|
+
self.console.print("\n[bold]Changes to apply:[/bold]")
|
|
2036
|
+
if to_deploy:
|
|
2037
|
+
self.console.print(f"[green]Install {len(to_deploy)} agent(s)[/green]")
|
|
2038
|
+
for agent_id in to_deploy:
|
|
2039
|
+
self.console.print(f" + {agent_id}")
|
|
2040
|
+
if to_remove:
|
|
2041
|
+
self.console.print(f"[red]Remove {len(to_remove)} agent(s)[/red]")
|
|
2042
|
+
for agent_id in to_remove:
|
|
2043
|
+
self.console.print(f" - {agent_id}")
|
|
2044
|
+
|
|
2045
|
+
# Ask user to confirm, adjust, or cancel
|
|
2046
|
+
action = questionary.select(
|
|
2047
|
+
"\nWhat would you like to do?",
|
|
2048
|
+
choices=[
|
|
2049
|
+
questionary.Choice("Apply these changes", value="apply"),
|
|
2050
|
+
questionary.Choice("Adjust selection", value="adjust"),
|
|
2051
|
+
questionary.Choice("Cancel", value="cancel"),
|
|
2052
|
+
],
|
|
2053
|
+
default="apply",
|
|
2054
|
+
style=self.QUESTIONARY_STYLE,
|
|
2055
|
+
).ask()
|
|
2056
|
+
|
|
2057
|
+
if action == "cancel":
|
|
2058
|
+
self.console.print("[yellow]Changes cancelled[/yellow]")
|
|
2059
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2060
|
+
return
|
|
2061
|
+
if action == "adjust":
|
|
2062
|
+
# current_selection is already updated, loop will use it
|
|
2063
|
+
continue
|
|
2064
|
+
|
|
2065
|
+
# Execute changes
|
|
2066
|
+
deploy_success = 0
|
|
2067
|
+
deploy_fail = 0
|
|
2068
|
+
remove_success = 0
|
|
2069
|
+
remove_fail = 0
|
|
2070
|
+
|
|
2071
|
+
# Install new agents
|
|
2072
|
+
for agent_id in to_deploy:
|
|
2073
|
+
agent = agent_map.get(agent_id)
|
|
2074
|
+
if agent and self._deploy_single_agent(agent, show_feedback=False):
|
|
2075
|
+
deploy_success += 1
|
|
2076
|
+
self.console.print(f"[green]✓ Installed: {agent_id}[/green]")
|
|
2077
|
+
else:
|
|
2078
|
+
deploy_fail += 1
|
|
2079
|
+
self.console.print(f"[red]✗ Failed to install: {agent_id}[/red]")
|
|
2080
|
+
|
|
2081
|
+
# Remove agents
|
|
2082
|
+
for agent_id in to_remove:
|
|
2083
|
+
try:
|
|
2084
|
+
import json
|
|
2085
|
+
# Note: Path is already imported at module level (line 17)
|
|
2086
|
+
|
|
2087
|
+
# Extract leaf name to match deployed filename
|
|
2088
|
+
# agent_id may be hierarchical (e.g., "engineer/mobile/tauri-engineer")
|
|
2089
|
+
# but deployed files use flattened leaf names (e.g., "tauri-engineer.md")
|
|
2090
|
+
if "/" in agent_id:
|
|
2091
|
+
leaf_name = agent_id.split("/")[-1]
|
|
2092
|
+
else:
|
|
2093
|
+
leaf_name = agent_id
|
|
2094
|
+
|
|
2095
|
+
# Remove from project, legacy, and user locations
|
|
2096
|
+
project_path = (
|
|
2097
|
+
Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md"
|
|
2098
|
+
)
|
|
2099
|
+
legacy_path = Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md"
|
|
2100
|
+
user_path = Path.home() / ".claude" / "agents" / f"{leaf_name}.md"
|
|
2101
|
+
|
|
2102
|
+
removed = False
|
|
2103
|
+
for path in [project_path, legacy_path, user_path]:
|
|
2104
|
+
if path.exists():
|
|
2105
|
+
path.unlink()
|
|
2106
|
+
removed = True
|
|
2107
|
+
|
|
2108
|
+
# Also remove from virtual deployment state
|
|
2109
|
+
deployment_state_paths = [
|
|
2110
|
+
Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2111
|
+
Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2112
|
+
]
|
|
2113
|
+
|
|
2114
|
+
for state_path in deployment_state_paths:
|
|
2115
|
+
if state_path.exists():
|
|
2116
|
+
try:
|
|
2117
|
+
with state_path.open() as f:
|
|
2118
|
+
state = json.load(f)
|
|
2119
|
+
|
|
2120
|
+
# Remove agent from deployment state
|
|
2121
|
+
# Deployment state uses leaf names, not full hierarchical paths
|
|
2122
|
+
agents = state.get("last_check_results", {}).get(
|
|
2123
|
+
"agents", {}
|
|
2124
|
+
)
|
|
2125
|
+
if leaf_name in agents:
|
|
2126
|
+
del agents[leaf_name]
|
|
2127
|
+
removed = True
|
|
2128
|
+
|
|
2129
|
+
# Save updated state
|
|
2130
|
+
with state_path.open("w") as f:
|
|
2131
|
+
json.dump(state, f, indent=2)
|
|
2132
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
2133
|
+
# Log but don't fail - physical removal still counts
|
|
2134
|
+
self.logger.debug(
|
|
2135
|
+
f"Failed to update deployment state at {state_path}: {e}"
|
|
2136
|
+
)
|
|
2137
|
+
|
|
2138
|
+
if removed:
|
|
2139
|
+
remove_success += 1
|
|
2140
|
+
self.console.print(f"[green]✓ Removed: {agent_id}[/green]")
|
|
2141
|
+
else:
|
|
2142
|
+
remove_fail += 1
|
|
2143
|
+
self.console.print(f"[yellow]⚠ Not found: {agent_id}[/yellow]")
|
|
2144
|
+
except Exception as e:
|
|
2145
|
+
remove_fail += 1
|
|
2146
|
+
self.console.print(f"[red]✗ Failed to remove {agent_id}: {e}[/red]")
|
|
2147
|
+
|
|
2148
|
+
# Show summary
|
|
2149
|
+
self.console.print()
|
|
2150
|
+
if deploy_success > 0:
|
|
2151
|
+
self.console.print(
|
|
2152
|
+
f"[green]✓ Installed {deploy_success} agent(s)[/green]"
|
|
2153
|
+
)
|
|
2154
|
+
if deploy_fail > 0:
|
|
2155
|
+
self.console.print(
|
|
2156
|
+
f"[red]✗ Failed to install {deploy_fail} agent(s)[/red]"
|
|
2157
|
+
)
|
|
2158
|
+
if remove_success > 0:
|
|
2159
|
+
self.console.print(
|
|
2160
|
+
f"[green]✓ Removed {remove_success} agent(s)[/green]"
|
|
2161
|
+
)
|
|
2162
|
+
if remove_fail > 0:
|
|
2163
|
+
self.console.print(
|
|
2164
|
+
f"[red]✗ Failed to remove {remove_fail} agent(s)[/red]"
|
|
2165
|
+
)
|
|
2166
|
+
|
|
981
2167
|
Prompt.ask("\nPress Enter to continue")
|
|
2168
|
+
# Exit the loop after successful execution
|
|
2169
|
+
break
|
|
982
2170
|
|
|
983
2171
|
def _deploy_agents_preset(self) -> None:
|
|
984
|
-
"""
|
|
2172
|
+
"""Install agents using preset configuration."""
|
|
985
2173
|
try:
|
|
986
2174
|
from claude_mpm.services.agents.agent_preset_service import (
|
|
987
2175
|
AgentPresetService,
|
|
@@ -998,9 +2186,9 @@ class ConfigureCommand(BaseCommand):
|
|
|
998
2186
|
Prompt.ask("\nPress Enter to continue")
|
|
999
2187
|
return
|
|
1000
2188
|
|
|
1001
|
-
self.console.print("\n[bold
|
|
2189
|
+
self.console.print("\n[bold white]═══ Available Presets ═══[/bold white]\n")
|
|
1002
2190
|
for idx, preset in enumerate(presets, 1):
|
|
1003
|
-
self.console.print(f" {idx}. [
|
|
2191
|
+
self.console.print(f" {idx}. [white]{preset['name']}[/white]")
|
|
1004
2192
|
self.console.print(f" {preset['description']}")
|
|
1005
2193
|
self.console.print(f" [dim]Agents: {len(preset['agents'])}[/dim]\n")
|
|
1006
2194
|
|
|
@@ -1024,14 +2212,14 @@ class ConfigureCommand(BaseCommand):
|
|
|
1024
2212
|
Prompt.ask("\nPress Enter to continue")
|
|
1025
2213
|
return
|
|
1026
2214
|
|
|
1027
|
-
# Confirm
|
|
2215
|
+
# Confirm installation
|
|
1028
2216
|
self.console.print(
|
|
1029
2217
|
f"\n[bold]Preset '{preset_name}' includes {len(resolution['agents'])} agents[/bold]"
|
|
1030
2218
|
)
|
|
1031
|
-
if Confirm.ask("
|
|
1032
|
-
|
|
2219
|
+
if Confirm.ask("Install all agents?", default=True):
|
|
2220
|
+
installed = 0
|
|
1033
2221
|
for agent in resolution["agents"]:
|
|
1034
|
-
# Convert dict to AgentConfig-like object for
|
|
2222
|
+
# Convert dict to AgentConfig-like object for installation
|
|
1035
2223
|
agent_config = AgentConfig(
|
|
1036
2224
|
name=agent.get("agent_id", "unknown"),
|
|
1037
2225
|
description=agent.get("metadata", {}).get(
|
|
@@ -1043,10 +2231,10 @@ class ConfigureCommand(BaseCommand):
|
|
|
1043
2231
|
agent_config.full_agent_id = agent.get("agent_id", "unknown")
|
|
1044
2232
|
|
|
1045
2233
|
if self._deploy_single_agent(agent_config, show_feedback=False):
|
|
1046
|
-
|
|
2234
|
+
installed += 1
|
|
1047
2235
|
|
|
1048
2236
|
self.console.print(
|
|
1049
|
-
f"\n[green]✓
|
|
2237
|
+
f"\n[green]✓ Installed {installed}/{len(resolution['agents'])} agents[/green]"
|
|
1050
2238
|
)
|
|
1051
2239
|
|
|
1052
2240
|
Prompt.ask("\nPress Enter to continue")
|
|
@@ -1055,14 +2243,177 @@ class ConfigureCommand(BaseCommand):
|
|
|
1055
2243
|
Prompt.ask("\nPress Enter to continue")
|
|
1056
2244
|
|
|
1057
2245
|
except Exception as e:
|
|
1058
|
-
self.console.print(f"[red]Error
|
|
1059
|
-
self.logger.error(f"Preset
|
|
2246
|
+
self.console.print(f"[red]Error installing preset: {e}[/red]")
|
|
2247
|
+
self.logger.error(f"Preset installation failed: {e}", exc_info=True)
|
|
2248
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2249
|
+
|
|
2250
|
+
def _select_recommended_agents(self, agents: List[AgentConfig]) -> None:
|
|
2251
|
+
"""Select and install recommended agents based on toolchain detection."""
|
|
2252
|
+
if not agents:
|
|
2253
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
2254
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2255
|
+
return
|
|
2256
|
+
|
|
2257
|
+
self.console.clear()
|
|
2258
|
+
self.console.print(
|
|
2259
|
+
"\n[bold white]═══ Recommended Agents for This Project ═══[/bold white]\n"
|
|
2260
|
+
)
|
|
2261
|
+
|
|
2262
|
+
# Get recommended agent IDs
|
|
2263
|
+
try:
|
|
2264
|
+
recommended_agent_ids = self.recommendation_service.get_recommended_agents(
|
|
2265
|
+
str(self.project_dir)
|
|
2266
|
+
)
|
|
2267
|
+
except Exception as e:
|
|
2268
|
+
self.console.print(f"[red]Error detecting toolchain: {e}[/red]")
|
|
2269
|
+
self.logger.error(f"Toolchain detection failed: {e}", exc_info=True)
|
|
1060
2270
|
Prompt.ask("\nPress Enter to continue")
|
|
2271
|
+
return
|
|
2272
|
+
|
|
2273
|
+
if not recommended_agent_ids:
|
|
2274
|
+
self.console.print("[yellow]No recommended agents found[/yellow]")
|
|
2275
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2276
|
+
return
|
|
2277
|
+
|
|
2278
|
+
# Get detection summary
|
|
2279
|
+
try:
|
|
2280
|
+
summary = self.recommendation_service.get_detection_summary(
|
|
2281
|
+
str(self.project_dir)
|
|
2282
|
+
)
|
|
2283
|
+
|
|
2284
|
+
self.console.print("[bold]Detected Project Stack:[/bold]")
|
|
2285
|
+
if summary.get("detected_languages"):
|
|
2286
|
+
self.console.print(
|
|
2287
|
+
f" Languages: [cyan]{', '.join(summary['detected_languages'])}[/cyan]"
|
|
2288
|
+
)
|
|
2289
|
+
if summary.get("detected_frameworks"):
|
|
2290
|
+
self.console.print(
|
|
2291
|
+
f" Frameworks: [cyan]{', '.join(summary['detected_frameworks'])}[/cyan]"
|
|
2292
|
+
)
|
|
2293
|
+
self.console.print(
|
|
2294
|
+
f" Detection Quality: [{'green' if summary.get('detection_quality') == 'high' else 'yellow'}]{summary.get('detection_quality', 'unknown')}[/]"
|
|
2295
|
+
)
|
|
2296
|
+
self.console.print()
|
|
2297
|
+
except Exception:
|
|
2298
|
+
pass
|
|
2299
|
+
|
|
2300
|
+
# Build mapping: agent_id -> AgentConfig
|
|
2301
|
+
agent_map = {agent.name: agent for agent in agents}
|
|
2302
|
+
|
|
2303
|
+
# Also check leaf names for matching
|
|
2304
|
+
for agent in agents:
|
|
2305
|
+
leaf_name = agent.name.split("/")[-1] if "/" in agent.name else agent.name
|
|
2306
|
+
if leaf_name not in agent_map:
|
|
2307
|
+
agent_map[leaf_name] = agent
|
|
2308
|
+
|
|
2309
|
+
# Find matching agents from available agents
|
|
2310
|
+
matched_agents = []
|
|
2311
|
+
for recommended_id in recommended_agent_ids:
|
|
2312
|
+
# Try full path match first
|
|
2313
|
+
if recommended_id in agent_map:
|
|
2314
|
+
matched_agents.append(agent_map[recommended_id])
|
|
2315
|
+
else:
|
|
2316
|
+
# Try leaf name match
|
|
2317
|
+
recommended_leaf = (
|
|
2318
|
+
recommended_id.split("/")[-1]
|
|
2319
|
+
if "/" in recommended_id
|
|
2320
|
+
else recommended_id
|
|
2321
|
+
)
|
|
2322
|
+
if recommended_leaf in agent_map:
|
|
2323
|
+
matched_agents.append(agent_map[recommended_leaf])
|
|
2324
|
+
|
|
2325
|
+
if not matched_agents:
|
|
2326
|
+
self.console.print(
|
|
2327
|
+
"[yellow]No matching agents found in available sources[/yellow]"
|
|
2328
|
+
)
|
|
2329
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2330
|
+
return
|
|
2331
|
+
|
|
2332
|
+
# Display recommended agents
|
|
2333
|
+
self.console.print(
|
|
2334
|
+
f"[bold]Recommended Agents ({len(matched_agents)}):[/bold]\n"
|
|
2335
|
+
)
|
|
2336
|
+
|
|
2337
|
+
from rich.table import Table
|
|
2338
|
+
|
|
2339
|
+
rec_table = Table(show_header=True, header_style="bold white")
|
|
2340
|
+
rec_table.add_column("#", style="dim", width=4)
|
|
2341
|
+
rec_table.add_column("Agent ID", style="cyan", width=40)
|
|
2342
|
+
rec_table.add_column("Status", style="white", width=15)
|
|
2343
|
+
|
|
2344
|
+
for idx, agent in enumerate(matched_agents, 1):
|
|
2345
|
+
is_installed = getattr(agent, "is_deployed", False)
|
|
2346
|
+
status = (
|
|
2347
|
+
"[green]Already Installed[/green]"
|
|
2348
|
+
if is_installed
|
|
2349
|
+
else "[yellow]Not Installed[/yellow]"
|
|
2350
|
+
)
|
|
2351
|
+
rec_table.add_row(str(idx), agent.name, status)
|
|
2352
|
+
|
|
2353
|
+
self.console.print(rec_table)
|
|
2354
|
+
|
|
2355
|
+
# Count how many need installation
|
|
2356
|
+
to_install = [a for a in matched_agents if not getattr(a, "is_deployed", False)]
|
|
2357
|
+
already_installed = len(matched_agents) - len(to_install)
|
|
2358
|
+
|
|
2359
|
+
self.console.print()
|
|
2360
|
+
if already_installed > 0:
|
|
2361
|
+
self.console.print(
|
|
2362
|
+
f"[green]✓ {already_installed} already installed[/green]"
|
|
2363
|
+
)
|
|
2364
|
+
if to_install:
|
|
2365
|
+
self.console.print(
|
|
2366
|
+
f"[yellow]⚠ {len(to_install)} need installation[/yellow]"
|
|
2367
|
+
)
|
|
2368
|
+
else:
|
|
2369
|
+
self.console.print(
|
|
2370
|
+
"[green]✓ All recommended agents are already installed![/green]"
|
|
2371
|
+
)
|
|
2372
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2373
|
+
return
|
|
2374
|
+
|
|
2375
|
+
# Ask for confirmation
|
|
2376
|
+
self.console.print()
|
|
2377
|
+
if not Confirm.ask(
|
|
2378
|
+
f"Install {len(to_install)} recommended agent(s)?", default=True
|
|
2379
|
+
):
|
|
2380
|
+
self.console.print("[yellow]Installation cancelled[/yellow]")
|
|
2381
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2382
|
+
return
|
|
2383
|
+
|
|
2384
|
+
# Install agents
|
|
2385
|
+
self.console.print("\n[bold]Installing recommended agents...[/bold]\n")
|
|
2386
|
+
|
|
2387
|
+
success_count = 0
|
|
2388
|
+
fail_count = 0
|
|
2389
|
+
|
|
2390
|
+
for agent in to_install:
|
|
2391
|
+
try:
|
|
2392
|
+
if self._deploy_single_agent(agent, show_feedback=False):
|
|
2393
|
+
success_count += 1
|
|
2394
|
+
self.console.print(f"[green]✓ Installed: {agent.name}[/green]")
|
|
2395
|
+
else:
|
|
2396
|
+
fail_count += 1
|
|
2397
|
+
self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
|
|
2398
|
+
except Exception as e:
|
|
2399
|
+
fail_count += 1
|
|
2400
|
+
self.console.print(f"[red]✗ Failed: {agent.name} - {e}[/red]")
|
|
2401
|
+
|
|
2402
|
+
# Show summary
|
|
2403
|
+
self.console.print()
|
|
2404
|
+
if success_count > 0:
|
|
2405
|
+
self.console.print(
|
|
2406
|
+
f"[green]✓ Successfully installed {success_count} agent(s)[/green]"
|
|
2407
|
+
)
|
|
2408
|
+
if fail_count > 0:
|
|
2409
|
+
self.console.print(f"[red]✗ Failed to install {fail_count} agent(s)[/red]")
|
|
2410
|
+
|
|
2411
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1061
2412
|
|
|
1062
2413
|
def _deploy_single_agent(
|
|
1063
2414
|
self, agent: AgentConfig, show_feedback: bool = True
|
|
1064
2415
|
) -> bool:
|
|
1065
|
-
"""
|
|
2416
|
+
"""Install a single agent to the appropriate location."""
|
|
1066
2417
|
try:
|
|
1067
2418
|
# Check if this is a remote agent with source_dict
|
|
1068
2419
|
source_dict = getattr(agent, "source_dict", None)
|
|
@@ -1084,13 +2435,15 @@ class ConfigureCommand(BaseCommand):
|
|
|
1084
2435
|
else:
|
|
1085
2436
|
target_name = full_agent_id + ".md"
|
|
1086
2437
|
|
|
1087
|
-
# Deploy to
|
|
1088
|
-
target_dir =
|
|
2438
|
+
# Deploy to project-level agents directory
|
|
2439
|
+
target_dir = self.project_dir / ".claude" / "agents"
|
|
1089
2440
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
1090
2441
|
target_file = target_dir / target_name
|
|
1091
2442
|
|
|
1092
2443
|
if show_feedback:
|
|
1093
|
-
self.console.print(
|
|
2444
|
+
self.console.print(
|
|
2445
|
+
f"\n[white]Installing {full_agent_id}...[/white]"
|
|
2446
|
+
)
|
|
1094
2447
|
|
|
1095
2448
|
# Copy the agent file
|
|
1096
2449
|
import shutil
|
|
@@ -1099,38 +2452,38 @@ class ConfigureCommand(BaseCommand):
|
|
|
1099
2452
|
|
|
1100
2453
|
if show_feedback:
|
|
1101
2454
|
self.console.print(
|
|
1102
|
-
f"[green]✓ Successfully
|
|
2455
|
+
f"[green]✓ Successfully installed {full_agent_id} to {target_file}[/green]"
|
|
1103
2456
|
)
|
|
1104
2457
|
Prompt.ask("\nPress Enter to continue")
|
|
1105
2458
|
|
|
1106
2459
|
return True
|
|
1107
|
-
# Legacy local template
|
|
2460
|
+
# Legacy local template installation (not implemented here)
|
|
1108
2461
|
if show_feedback:
|
|
1109
2462
|
self.console.print(
|
|
1110
|
-
"[yellow]Local template
|
|
2463
|
+
"[yellow]Local template installation not yet implemented[/yellow]"
|
|
1111
2464
|
)
|
|
1112
2465
|
Prompt.ask("\nPress Enter to continue")
|
|
1113
2466
|
return False
|
|
1114
2467
|
|
|
1115
2468
|
except Exception as e:
|
|
1116
2469
|
if show_feedback:
|
|
1117
|
-
self.console.print(f"[red]Error
|
|
1118
|
-
self.logger.error(f"Agent
|
|
2470
|
+
self.console.print(f"[red]Error installing agent: {e}[/red]")
|
|
2471
|
+
self.logger.error(f"Agent installation failed: {e}", exc_info=True)
|
|
1119
2472
|
Prompt.ask("\nPress Enter to continue")
|
|
1120
2473
|
return False
|
|
1121
2474
|
|
|
1122
2475
|
def _remove_agents(self, agents: List[AgentConfig]) -> None:
|
|
1123
|
-
"""Remove
|
|
1124
|
-
# Filter to
|
|
1125
|
-
|
|
2476
|
+
"""Remove installed agents."""
|
|
2477
|
+
# Filter to installed agents only
|
|
2478
|
+
installed = [a for a in agents if getattr(a, "is_deployed", False)]
|
|
1126
2479
|
|
|
1127
|
-
if not
|
|
1128
|
-
self.console.print("[yellow]No agents are currently
|
|
2480
|
+
if not installed:
|
|
2481
|
+
self.console.print("[yellow]No agents are currently installed[/yellow]")
|
|
1129
2482
|
Prompt.ask("\nPress Enter to continue")
|
|
1130
2483
|
return
|
|
1131
2484
|
|
|
1132
|
-
self.console.print(f"\n[bold]
|
|
1133
|
-
for idx, agent in enumerate(
|
|
2485
|
+
self.console.print(f"\n[bold]Installed agents ({len(installed)}):[/bold]")
|
|
2486
|
+
for idx, agent in enumerate(installed, 1):
|
|
1134
2487
|
display_name = getattr(agent, "display_name", agent.name)
|
|
1135
2488
|
self.console.print(f" {idx}. {agent.name} - {display_name}")
|
|
1136
2489
|
|
|
@@ -1140,8 +2493,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
1140
2493
|
|
|
1141
2494
|
try:
|
|
1142
2495
|
idx = int(selection) - 1
|
|
1143
|
-
if 0 <= idx < len(
|
|
1144
|
-
agent =
|
|
2496
|
+
if 0 <= idx < len(installed):
|
|
2497
|
+
agent = installed[idx]
|
|
1145
2498
|
full_agent_id = getattr(agent, "full_agent_id", agent.name)
|
|
1146
2499
|
|
|
1147
2500
|
# Determine possible file names (hierarchical and leaf)
|
|
@@ -1207,7 +2560,7 @@ class ConfigureCommand(BaseCommand):
|
|
|
1207
2560
|
agent = agents[idx]
|
|
1208
2561
|
|
|
1209
2562
|
self.console.clear()
|
|
1210
|
-
self.console.print("\n[bold
|
|
2563
|
+
self.console.print("\n[bold white]═══ Agent Details ═══[/bold white]\n")
|
|
1211
2564
|
|
|
1212
2565
|
# Basic info
|
|
1213
2566
|
self.console.print(f"[bold]ID:[/bold] {agent.name}")
|
|
@@ -1229,9 +2582,9 @@ class ConfigureCommand(BaseCommand):
|
|
|
1229
2582
|
self.console.print(f"[bold]Source:[/bold] {source}")
|
|
1230
2583
|
self.console.print(f"[bold]Version:[/bold] {version[:16]}...")
|
|
1231
2584
|
|
|
1232
|
-
#
|
|
1233
|
-
|
|
1234
|
-
status = "
|
|
2585
|
+
# Installation status
|
|
2586
|
+
is_installed = getattr(agent, "is_deployed", False)
|
|
2587
|
+
status = "Installed" if is_installed else "Available"
|
|
1235
2588
|
self.console.print(f"[bold]Status:[/bold] {status}")
|
|
1236
2589
|
|
|
1237
2590
|
Prompt.ask("\nPress Enter to continue")
|