claude-mpm 5.1.9__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/PM_INSTRUCTIONS.md +46 -0
- claude_mpm/agents/agent_loader.py +10 -17
- claude_mpm/agents/templates/circuit-breakers.md +138 -1
- claude_mpm/cli/commands/agent_state_manager.py +8 -17
- claude_mpm/cli/commands/configure.py +1046 -149
- 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/summarize.py +413 -0
- claude_mpm/cli/executor.py +8 -0
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/cli/startup.py +60 -53
- claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/unified_agent_registry.py +5 -15
- 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/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/scripts/launch_monitor.py +93 -13
- claude_mpm/services/agents/agent_recommendation_service.py +279 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +322 -53
- claude_mpm/services/agents/git_source_manager.py +20 -0
- claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
- claude_mpm/services/agents/toolchain_detector.py +6 -5
- claude_mpm/services/analysis/__init__.py +11 -1
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/command_deployment_service.py +0 -2
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/monitor/daemon.py +9 -2
- claude_mpm/services/monitor/daemon_manager.py +39 -3
- claude_mpm/services/monitor/server.py +225 -19
- 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 +17 -44
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +1 -77
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +59 -114
- {claude_mpm-5.1.9.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 -977
- 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-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
|
@@ -13,18 +13,20 @@ DESIGN DECISIONS:
|
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
15
|
import shutil
|
|
16
|
+
from collections import defaultdict
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Dict, List, Optional
|
|
18
19
|
|
|
19
20
|
import questionary
|
|
20
21
|
import questionary.constants
|
|
21
22
|
import questionary.prompts.common # For checkbox symbol customization
|
|
22
|
-
from questionary import Style
|
|
23
|
+
from questionary import Choice, Separator, Style
|
|
23
24
|
from rich.console import Console
|
|
24
25
|
from rich.prompt import Confirm, Prompt
|
|
25
26
|
from rich.text import Text
|
|
26
27
|
|
|
27
28
|
from ...core.config import Config
|
|
29
|
+
from ...services.agents.agent_recommendation_service import AgentRecommendationService
|
|
28
30
|
from ...services.version_service import VersionService
|
|
29
31
|
from ...utils.agent_filters import apply_all_filters, get_deployed_agent_ids
|
|
30
32
|
from ...utils.console import console as default_console
|
|
@@ -76,6 +78,7 @@ class ConfigureCommand(BaseCommand):
|
|
|
76
78
|
self._navigation = None # Lazy-initialized
|
|
77
79
|
self._template_editor = None # Lazy-initialized
|
|
78
80
|
self._startup_manager = None # Lazy-initialized
|
|
81
|
+
self._recommendation_service = None # Lazy-initialized
|
|
79
82
|
|
|
80
83
|
def validate_args(self, args) -> Optional[str]:
|
|
81
84
|
"""Validate command arguments."""
|
|
@@ -152,6 +155,13 @@ class ConfigureCommand(BaseCommand):
|
|
|
152
155
|
)
|
|
153
156
|
return self._startup_manager
|
|
154
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
|
+
|
|
155
165
|
def run(self, args) -> CommandResult:
|
|
156
166
|
"""Execute the configure command."""
|
|
157
167
|
# Set configuration scope
|
|
@@ -311,85 +321,28 @@ class ConfigureCommand(BaseCommand):
|
|
|
311
321
|
self.navigation.display_header()
|
|
312
322
|
self.console.print("\n[bold blue]═══ Agent Management ═══[/bold blue]\n")
|
|
313
323
|
|
|
314
|
-
#
|
|
315
|
-
self.
|
|
316
|
-
|
|
317
|
-
sources = self._get_configured_sources()
|
|
318
|
-
if sources:
|
|
319
|
-
from rich.table import Table
|
|
320
|
-
|
|
321
|
-
sources_table = Table(show_header=True, header_style="bold white")
|
|
322
|
-
sources_table.add_column(
|
|
323
|
-
"Source",
|
|
324
|
-
style="bright_yellow",
|
|
325
|
-
width=40,
|
|
326
|
-
no_wrap=True,
|
|
327
|
-
overflow="ellipsis",
|
|
328
|
-
)
|
|
329
|
-
sources_table.add_column(
|
|
330
|
-
"Status", style="green", width=15, no_wrap=True
|
|
331
|
-
)
|
|
332
|
-
sources_table.add_column(
|
|
333
|
-
"Agents", style="yellow", width=10, no_wrap=True
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
for source in sources:
|
|
337
|
-
status = "✓ Active" if source.get("enabled", True) else "Disabled"
|
|
338
|
-
agent_count = source.get("agent_count", "?")
|
|
339
|
-
sources_table.add_row(
|
|
340
|
-
source["identifier"], status, str(agent_count)
|
|
341
|
-
)
|
|
324
|
+
# Load all agents with spinner (don't show partial state)
|
|
325
|
+
agents = self._load_agents_with_spinner()
|
|
342
326
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
self.console.print("[yellow]No agent sources configured[/yellow]")
|
|
327
|
+
if not agents:
|
|
328
|
+
self.console.print("[yellow]No agents found[/yellow]")
|
|
346
329
|
self.console.print(
|
|
347
|
-
"[dim]
|
|
330
|
+
"[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
|
|
348
331
|
)
|
|
332
|
+
Prompt.ask("\nPress Enter to continue")
|
|
333
|
+
break
|
|
349
334
|
|
|
350
|
-
#
|
|
351
|
-
self.
|
|
352
|
-
|
|
353
|
-
try:
|
|
354
|
-
# Discover agents (includes both local and remote)
|
|
355
|
-
agents = self.agent_manager.discover_agents(include_remote=True)
|
|
356
|
-
|
|
357
|
-
# Set deployment status on each agent for display
|
|
358
|
-
deployed_ids = get_deployed_agent_ids()
|
|
359
|
-
for agent in agents:
|
|
360
|
-
# Extract leaf name for comparison
|
|
361
|
-
agent_leaf_name = agent.name.split("/")[-1]
|
|
362
|
-
agent.is_deployed = agent_leaf_name in deployed_ids
|
|
363
|
-
|
|
364
|
-
# Filter BASE_AGENT from display (1M-502 Phase 1)
|
|
365
|
-
agents = self._filter_agent_configs(agents, filter_deployed=False)
|
|
366
|
-
|
|
367
|
-
if not agents:
|
|
368
|
-
self.console.print("[yellow]No agents found[/yellow]")
|
|
369
|
-
self.console.print(
|
|
370
|
-
"[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
|
|
371
|
-
)
|
|
372
|
-
else:
|
|
373
|
-
# Display agents in a table (already filtered at line 339)
|
|
374
|
-
self._display_agents_with_source_info(agents)
|
|
375
|
-
|
|
376
|
-
except Exception as e:
|
|
377
|
-
self.console.print(f"[red]Error discovering agents: {e}[/red]")
|
|
378
|
-
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)
|
|
379
337
|
|
|
380
|
-
# Step 3:
|
|
338
|
+
# Step 3: Simplified menu - only "Select Agents" option
|
|
381
339
|
self.console.print()
|
|
382
340
|
self.logger.debug("About to show agent management menu")
|
|
383
341
|
try:
|
|
384
342
|
choice = questionary.select(
|
|
385
343
|
"Agent Management:",
|
|
386
344
|
choices=[
|
|
387
|
-
"Manage sources (add/remove repositories)",
|
|
388
345
|
"Select Agents",
|
|
389
|
-
"Install preset (predefined sets)",
|
|
390
|
-
"Remove agents",
|
|
391
|
-
"View agent details",
|
|
392
|
-
"Toggle agents (legacy enable/disable)",
|
|
393
346
|
questionary.Separator(),
|
|
394
347
|
"← Back to main menu",
|
|
395
348
|
],
|
|
@@ -399,22 +352,11 @@ class ConfigureCommand(BaseCommand):
|
|
|
399
352
|
if choice is None or choice == "← Back to main menu":
|
|
400
353
|
break
|
|
401
354
|
|
|
402
|
-
agents_var = agents if "agents" in locals() else []
|
|
403
|
-
|
|
404
355
|
# Map selection to action
|
|
405
|
-
if choice == "
|
|
406
|
-
self._manage_sources()
|
|
407
|
-
elif choice == "Select Agents":
|
|
356
|
+
if choice == "Select Agents":
|
|
408
357
|
self.logger.debug("User selected 'Select Agents' from menu")
|
|
409
|
-
self.
|
|
410
|
-
|
|
411
|
-
self._deploy_agents_preset()
|
|
412
|
-
elif choice == "Remove agents":
|
|
413
|
-
self._remove_agents(agents_var)
|
|
414
|
-
elif choice == "View agent details":
|
|
415
|
-
self._view_agent_details_enhanced(agents_var)
|
|
416
|
-
elif choice == "Toggle agents (legacy enable/disable)":
|
|
417
|
-
self._toggle_agents_interactive(agents_var)
|
|
358
|
+
self._deploy_agents_unified(agents)
|
|
359
|
+
# Loop back to show updated state after deployment
|
|
418
360
|
|
|
419
361
|
except KeyboardInterrupt:
|
|
420
362
|
self.console.print("\n[yellow]Operation cancelled[/yellow]")
|
|
@@ -440,6 +382,86 @@ class ConfigureCommand(BaseCommand):
|
|
|
440
382
|
Prompt.ask("\nPress Enter to continue")
|
|
441
383
|
break
|
|
442
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]")
|
|
464
|
+
|
|
443
465
|
def _display_agents_table(self, agents: List[AgentConfig]) -> None:
|
|
444
466
|
"""Display a table of available agents."""
|
|
445
467
|
self.agent_display.display_agents_table(agents)
|
|
@@ -497,6 +519,9 @@ class ConfigureCommand(BaseCommand):
|
|
|
497
519
|
if self.agent_manager.has_pending_changes():
|
|
498
520
|
self.agent_manager.commit_deferred_changes()
|
|
499
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)
|
|
500
525
|
else:
|
|
501
526
|
self.console.print("[yellow]No changes to save.[/yellow]")
|
|
502
527
|
Prompt.ask("Press Enter to continue")
|
|
@@ -524,6 +549,60 @@ class ConfigureCommand(BaseCommand):
|
|
|
524
549
|
agent.name, not current
|
|
525
550
|
)
|
|
526
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
|
+
|
|
527
606
|
def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
|
|
528
607
|
"""Customize agent JSON template."""
|
|
529
608
|
self.template_editor.customize_agent_template(agents)
|
|
@@ -933,14 +1012,14 @@ class ConfigureCommand(BaseCommand):
|
|
|
933
1012
|
identifier = repo.identifier
|
|
934
1013
|
|
|
935
1014
|
# Count agents in cache
|
|
1015
|
+
# Note: identifier already includes subdirectory path (e.g., "bobmatnyc/claude-mpm-agents/agents")
|
|
936
1016
|
cache_dir = (
|
|
937
1017
|
Path.home() / ".claude-mpm" / "cache" / "remote-agents" / identifier
|
|
938
1018
|
)
|
|
939
1019
|
agent_count = 0
|
|
940
1020
|
if cache_dir.exists():
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
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")))
|
|
944
1023
|
|
|
945
1024
|
sources.append(
|
|
946
1025
|
{
|
|
@@ -1038,10 +1117,36 @@ class ConfigureCommand(BaseCommand):
|
|
|
1038
1117
|
# Terminal too narrow, use minimum widths
|
|
1039
1118
|
return columns.copy()
|
|
1040
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
|
+
|
|
1041
1137
|
def _display_agents_with_source_info(self, agents: List[AgentConfig]) -> None:
|
|
1042
1138
|
"""Display agents table with source information and installation status."""
|
|
1043
1139
|
from rich.table import Table
|
|
1044
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
|
+
|
|
1045
1150
|
# Get terminal width and calculate dynamic column widths
|
|
1046
1151
|
terminal_width = shutil.get_terminal_size().columns
|
|
1047
1152
|
min_widths = {
|
|
@@ -1053,18 +1158,20 @@ class ConfigureCommand(BaseCommand):
|
|
|
1053
1158
|
}
|
|
1054
1159
|
widths = self._calculate_column_widths(terminal_width, min_widths)
|
|
1055
1160
|
|
|
1056
|
-
agents_table = Table(show_header=True, header_style="bold
|
|
1057
|
-
agents_table.add_column(
|
|
1161
|
+
agents_table = Table(show_header=True, header_style="bold cyan")
|
|
1162
|
+
agents_table.add_column(
|
|
1163
|
+
"#", style="bright_black", width=widths["#"], no_wrap=True
|
|
1164
|
+
)
|
|
1058
1165
|
agents_table.add_column(
|
|
1059
1166
|
"Agent ID",
|
|
1060
|
-
style="
|
|
1167
|
+
style="bright_black",
|
|
1061
1168
|
width=widths["Agent ID"],
|
|
1062
1169
|
no_wrap=True,
|
|
1063
1170
|
overflow="ellipsis",
|
|
1064
1171
|
)
|
|
1065
1172
|
agents_table.add_column(
|
|
1066
1173
|
"Name",
|
|
1067
|
-
style="
|
|
1174
|
+
style="bright_cyan",
|
|
1068
1175
|
width=widths["Name"],
|
|
1069
1176
|
no_wrap=True,
|
|
1070
1177
|
overflow="ellipsis",
|
|
@@ -1076,9 +1183,13 @@ class ConfigureCommand(BaseCommand):
|
|
|
1076
1183
|
no_wrap=True,
|
|
1077
1184
|
)
|
|
1078
1185
|
agents_table.add_column(
|
|
1079
|
-
"Status", style="
|
|
1186
|
+
"Status", style="bright_black", width=widths["Status"], no_wrap=True
|
|
1080
1187
|
)
|
|
1081
1188
|
|
|
1189
|
+
# FIX 3: Get deployed agent IDs once, before the loop (efficiency)
|
|
1190
|
+
deployed_ids = get_deployed_agent_ids()
|
|
1191
|
+
|
|
1192
|
+
recommended_count = 0
|
|
1082
1193
|
for idx, agent in enumerate(agents, 1):
|
|
1083
1194
|
# Determine source with repo name
|
|
1084
1195
|
source_type = getattr(agent, "source_type", "local")
|
|
@@ -1106,29 +1217,76 @@ class ConfigureCommand(BaseCommand):
|
|
|
1106
1217
|
else:
|
|
1107
1218
|
source_label = "Local"
|
|
1108
1219
|
|
|
1109
|
-
#
|
|
1110
|
-
is_installed =
|
|
1220
|
+
# FIX 2: Check actual deployment status from .claude/agents/ directory
|
|
1221
|
+
is_installed = agent.name in deployed_ids
|
|
1111
1222
|
if is_installed:
|
|
1112
1223
|
status = "[green]Installed[/green]"
|
|
1113
1224
|
else:
|
|
1114
1225
|
status = "Available"
|
|
1115
1226
|
|
|
1116
|
-
#
|
|
1117
|
-
|
|
1118
|
-
#
|
|
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
|
+
)
|
|
1236
|
+
|
|
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)
|
|
1119
1258
|
|
|
1120
1259
|
agents_table.add_row(
|
|
1121
|
-
str(idx),
|
|
1260
|
+
str(idx), agent_id_display, display_name, source_label, status
|
|
1122
1261
|
)
|
|
1123
1262
|
|
|
1124
1263
|
self.console.print(agents_table)
|
|
1125
1264
|
|
|
1126
|
-
# Show
|
|
1127
|
-
|
|
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)
|
|
1128
1285
|
available_count = len(agents) - installed_count
|
|
1129
1286
|
self.console.print(
|
|
1130
1287
|
f"\n[green]✓ {installed_count} installed[/green] | "
|
|
1131
1288
|
f"[dim]{available_count} available[/dim] | "
|
|
1289
|
+
f"[yellow]{recommended_count} recommended[/yellow] | "
|
|
1132
1290
|
f"[dim]Total: {len(agents)}[/dim]"
|
|
1133
1291
|
)
|
|
1134
1292
|
|
|
@@ -1144,8 +1302,423 @@ class ConfigureCommand(BaseCommand):
|
|
|
1144
1302
|
self.console.print(" claude-mpm agent-source list")
|
|
1145
1303
|
Prompt.ask("\nPress Enter to continue")
|
|
1146
1304
|
|
|
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
|
+
"""
|
|
1315
|
+
if not agents:
|
|
1316
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
1317
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1318
|
+
return
|
|
1319
|
+
|
|
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
|
+
)
|
|
1337
|
+
|
|
1338
|
+
if not all_agents:
|
|
1339
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
1340
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1341
|
+
return
|
|
1342
|
+
|
|
1343
|
+
# Get deployed agent IDs and recommended agents
|
|
1344
|
+
deployed_ids = get_deployed_agent_ids()
|
|
1345
|
+
|
|
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")
|
|
1615
|
+
return
|
|
1616
|
+
|
|
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]")
|
|
1646
|
+
else:
|
|
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
|
+
|
|
1147
1716
|
def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
|
|
1148
|
-
"""Manage agent installation state (unified install/remove interface).
|
|
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
|
+
"""
|
|
1149
1722
|
if not agents:
|
|
1150
1723
|
self.console.print("[yellow]No agents available[/yellow]")
|
|
1151
1724
|
Prompt.ask("\nPress Enter to continue")
|
|
@@ -1192,59 +1765,91 @@ class ConfigureCommand(BaseCommand):
|
|
|
1192
1765
|
|
|
1193
1766
|
# Loop to allow adjusting selection
|
|
1194
1767
|
while True:
|
|
1195
|
-
# Build
|
|
1196
|
-
agent_choices = []
|
|
1768
|
+
# Build agent mapping and collections
|
|
1197
1769
|
agent_map = {} # For lookup after selection
|
|
1770
|
+
collections = defaultdict(list)
|
|
1198
1771
|
|
|
1199
1772
|
for agent in agents:
|
|
1200
1773
|
if agent.name in {a["agent_id"] for a in all_agents}:
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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"
|
|
1217
1790
|
|
|
1218
|
-
|
|
1791
|
+
collections[collection_id].append(agent)
|
|
1219
1792
|
agent_map[agent.name] = agent
|
|
1220
1793
|
|
|
1221
|
-
#
|
|
1222
|
-
self.console.print("\n[bold cyan]
|
|
1223
|
-
self.console.print("[dim][✓] Checked = Installed (uncheck to remove)[/dim]")
|
|
1794
|
+
# STEP 1: Collection-level selection
|
|
1795
|
+
self.console.print("\n[bold cyan]Select Agent Collections[/bold cyan]")
|
|
1224
1796
|
self.console.print(
|
|
1225
|
-
"[dim]
|
|
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]"
|
|
1226
1801
|
)
|
|
1227
1802
|
self.console.print(
|
|
1228
|
-
"[dim]
|
|
1229
|
-
|
|
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
|
+
)
|
|
1230
1839
|
)
|
|
1231
1840
|
|
|
1232
1841
|
# Monkey-patch questionary symbols for better visibility
|
|
1233
|
-
# Must patch common module directly since it imports constants at load time
|
|
1234
1842
|
questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
|
|
1235
1843
|
questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
|
|
1236
1844
|
|
|
1237
|
-
# Pre-selection via checked=True on Choice objects
|
|
1238
|
-
self.logger.debug(
|
|
1239
|
-
"About to show checkbox selection with %d agents", len(agent_choices)
|
|
1240
|
-
)
|
|
1241
|
-
|
|
1242
1845
|
try:
|
|
1243
|
-
|
|
1244
|
-
"
|
|
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,
|
|
1245
1851
|
).ask()
|
|
1246
1852
|
except Exception as e:
|
|
1247
|
-
# Handle questionary failure (non-TTY, broken pipe, keyboard interrupt, etc.)
|
|
1248
1853
|
import sys
|
|
1249
1854
|
|
|
1250
1855
|
self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
|
|
@@ -1267,9 +1872,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
1267
1872
|
Prompt.ask("\nPress Enter to continue")
|
|
1268
1873
|
return
|
|
1269
1874
|
|
|
1270
|
-
# Handle
|
|
1271
|
-
if
|
|
1272
|
-
# Check if we're in a non-interactive environment
|
|
1875
|
+
# Handle cancellation
|
|
1876
|
+
if selected_collections is None:
|
|
1273
1877
|
import sys
|
|
1274
1878
|
|
|
1275
1879
|
if not sys.stdin.isatty():
|
|
@@ -1287,17 +1891,138 @@ class ConfigureCommand(BaseCommand):
|
|
|
1287
1891
|
Prompt.ask("\nPress Enter to continue")
|
|
1288
1892
|
return
|
|
1289
1893
|
|
|
1290
|
-
#
|
|
1291
|
-
|
|
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
|
|
1292
1979
|
|
|
1293
1980
|
# Determine actions based on ORIGINAL deployed state
|
|
1294
1981
|
# Compare full paths to full paths (deployed_full_paths was built from deployed_ids)
|
|
1295
1982
|
to_deploy = (
|
|
1296
1983
|
current_selection - deployed_full_paths
|
|
1297
1984
|
) # Selected but not originally deployed
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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)
|
|
1301
2026
|
|
|
1302
2027
|
if not to_deploy and not to_remove:
|
|
1303
2028
|
self.console.print(
|
|
@@ -1357,14 +2082,22 @@ class ConfigureCommand(BaseCommand):
|
|
|
1357
2082
|
for agent_id in to_remove:
|
|
1358
2083
|
try:
|
|
1359
2084
|
import json
|
|
1360
|
-
|
|
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
|
|
1361
2094
|
|
|
1362
2095
|
# Remove from project, legacy, and user locations
|
|
1363
2096
|
project_path = (
|
|
1364
|
-
Path.cwd() / ".claude-mpm" / "agents" / f"{
|
|
2097
|
+
Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md"
|
|
1365
2098
|
)
|
|
1366
|
-
legacy_path = Path.cwd() / ".claude" / "agents" / f"{
|
|
1367
|
-
user_path = Path.home() / ".claude" / "agents" / f"{
|
|
2099
|
+
legacy_path = Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md"
|
|
2100
|
+
user_path = Path.home() / ".claude" / "agents" / f"{leaf_name}.md"
|
|
1368
2101
|
|
|
1369
2102
|
removed = False
|
|
1370
2103
|
for path in [project_path, legacy_path, user_path]:
|
|
@@ -1385,11 +2118,12 @@ class ConfigureCommand(BaseCommand):
|
|
|
1385
2118
|
state = json.load(f)
|
|
1386
2119
|
|
|
1387
2120
|
# Remove agent from deployment state
|
|
2121
|
+
# Deployment state uses leaf names, not full hierarchical paths
|
|
1388
2122
|
agents = state.get("last_check_results", {}).get(
|
|
1389
2123
|
"agents", {}
|
|
1390
2124
|
)
|
|
1391
|
-
if
|
|
1392
|
-
del agents[
|
|
2125
|
+
if leaf_name in agents:
|
|
2126
|
+
del agents[leaf_name]
|
|
1393
2127
|
removed = True
|
|
1394
2128
|
|
|
1395
2129
|
# Save updated state
|
|
@@ -1513,6 +2247,169 @@ class ConfigureCommand(BaseCommand):
|
|
|
1513
2247
|
self.logger.error(f"Preset installation failed: {e}", exc_info=True)
|
|
1514
2248
|
Prompt.ask("\nPress Enter to continue")
|
|
1515
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)
|
|
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")
|
|
2412
|
+
|
|
1516
2413
|
def _deploy_single_agent(
|
|
1517
2414
|
self, agent: AgentConfig, show_feedback: bool = True
|
|
1518
2415
|
) -> bool:
|
|
@@ -1538,8 +2435,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
1538
2435
|
else:
|
|
1539
2436
|
target_name = full_agent_id + ".md"
|
|
1540
2437
|
|
|
1541
|
-
# Deploy to
|
|
1542
|
-
target_dir =
|
|
2438
|
+
# Deploy to project-level agents directory
|
|
2439
|
+
target_dir = self.project_dir / ".claude" / "agents"
|
|
1543
2440
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
1544
2441
|
target_file = target_dir / target_name
|
|
1545
2442
|
|