claude-mpm 5.0.9__py3-none-any.whl → 5.4.41__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/__init__.py +4 -0
- claude_mpm/agents/BASE_AGENT.md +164 -0
- claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
- claude_mpm/agents/MEMORY.md +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +468 -468
- claude_mpm/agents/WORKFLOW.md +5 -254
- claude_mpm/agents/agent_loader.py +13 -44
- claude_mpm/agents/base_agent.json +1 -1
- claude_mpm/agents/frontmatter_validator.py +70 -2
- claude_mpm/agents/templates/circuit-breakers.md +431 -45
- claude_mpm/cli/__init__.py +0 -1
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/chrome_devtools_installer.py +175 -0
- claude_mpm/cli/commands/agent_state_manager.py +18 -27
- claude_mpm/cli/commands/agents.py +175 -37
- claude_mpm/cli/commands/auto_configure.py +723 -236
- claude_mpm/cli/commands/config.py +88 -2
- claude_mpm/cli/commands/configure.py +1262 -157
- claude_mpm/cli/commands/configure_agent_display.py +25 -6
- claude_mpm/cli/commands/mpm_init/core.py +225 -46
- 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 +1 -1
- claude_mpm/cli/commands/profile.py +277 -0
- claude_mpm/cli/commands/skills.py +214 -189
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +21 -3
- claude_mpm/cli/interactive/agent_wizard.py +85 -10
- claude_mpm/cli/parsers/agents_parser.py +54 -9
- claude_mpm/cli/parsers/auto_configure_parser.py +13 -138
- claude_mpm/cli/parsers/base_parser.py +12 -0
- claude_mpm/cli/parsers/config_parser.py +153 -83
- claude_mpm/cli/parsers/profile_parser.py +148 -0
- claude_mpm/cli/parsers/skills_parser.py +3 -2
- claude_mpm/cli/startup.py +879 -149
- claude_mpm/commands/mpm-config.md +28 -0
- claude_mpm/commands/mpm-doctor.md +9 -22
- claude_mpm/commands/mpm-help.md +5 -287
- claude_mpm/commands/mpm-init.md +81 -507
- claude_mpm/commands/mpm-monitor.md +15 -402
- claude_mpm/commands/mpm-organize.md +120 -0
- claude_mpm/commands/mpm-postmortem.md +6 -108
- claude_mpm/commands/mpm-session-resume.md +12 -363
- claude_mpm/commands/mpm-status.md +5 -69
- claude_mpm/commands/mpm-ticket-view.md +52 -495
- claude_mpm/commands/mpm-version.md +5 -107
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/core/config.py +2 -4
- claude_mpm/core/framework/formatters/content_formatter.py +3 -13
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
- claude_mpm/core/framework_loader.py +4 -2
- claude_mpm/core/logger.py +13 -0
- claude_mpm/core/optimized_startup.py +59 -0
- claude_mpm/core/output_style_manager.py +173 -43
- claude_mpm/core/shared/config_loader.py +1 -1
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/unified_agent_registry.py +134 -16
- claude_mpm/core/unified_config.py +22 -0
- claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
- claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
- claude_mpm/dashboard/static/svelte-build/index.html +36 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
- claude_mpm/hooks/claude_hooks/hook_handler.py +155 -1
- claude_mpm/hooks/claude_hooks/installer.py +33 -10
- claude_mpm/hooks/claude_hooks/memory_integration.py +28 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +30 -6
- claude_mpm/hooks/memory_integration_hook.py +46 -1
- claude_mpm/init.py +63 -19
- claude_mpm/models/agent_definition.py +7 -0
- claude_mpm/models/git_repository.py +3 -3
- claude_mpm/scripts/claude-hook-handler.sh +58 -18
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_builder.py +3 -3
- claude_mpm/services/agents/agent_recommendation_service.py +278 -0
- claude_mpm/services/agents/agent_review_service.py +280 -0
- claude_mpm/services/agents/cache_git_manager.py +6 -6
- claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -5
- claude_mpm/services/agents/deployment/agent_template_builder.py +5 -3
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +320 -29
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +546 -68
- claude_mpm/services/agents/git_source_manager.py +36 -2
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
- claude_mpm/services/agents/recommender.py +5 -3
- claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
- claude_mpm/services/agents/sources/git_source_sync_service.py +13 -6
- claude_mpm/services/agents/startup_sync.py +22 -2
- claude_mpm/services/agents/toolchain_detector.py +10 -6
- claude_mpm/services/analysis/__init__.py +11 -1
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/command_deployment_service.py +81 -10
- claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +101 -16
- claude_mpm/services/monitor/daemon.py +9 -2
- claude_mpm/services/monitor/daemon_manager.py +39 -3
- claude_mpm/services/monitor/management/lifecycle.py +8 -1
- claude_mpm/services/monitor/server.py +698 -22
- claude_mpm/services/pm_skills_deployer.py +676 -0
- claude_mpm/services/profile_manager.py +331 -0
- claude_mpm/services/project/project_organizer.py +4 -0
- claude_mpm/services/self_upgrade_service.py +120 -12
- claude_mpm/services/skills/__init__.py +3 -0
- claude_mpm/services/skills/git_skill_source_manager.py +130 -2
- claude_mpm/services/skills/selective_skill_deployer.py +704 -0
- claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
- claude_mpm/services/skills_deployer.py +126 -9
- claude_mpm/services/socketio/dashboard_server.py +1 -0
- claude_mpm/services/socketio/event_normalizer.py +51 -6
- claude_mpm/services/socketio/server/core.py +386 -108
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/skills/skill_manager.py +92 -3
- claude_mpm/utils/agent_dependency_loader.py +14 -2
- claude_mpm/utils/agent_filters.py +17 -44
- claude_mpm/utils/gitignore.py +3 -0
- claude_mpm/utils/migration.py +4 -4
- claude_mpm/utils/robust_installer.py +47 -3
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/METADATA +57 -87
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/RECORD +160 -211
- claude_mpm-5.4.41.dist-info/entry_points.txt +5 -0
- claude_mpm-5.4.41.dist-info/licenses/LICENSE +94 -0
- claude_mpm-5.4.41.dist-info/licenses/LICENSE-FAQ.md +153 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
- claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
- claude_mpm/agents/BASE_OPS.md +0 -219
- claude_mpm/agents/BASE_PM.md +0 -480
- claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
- claude_mpm/agents/BASE_QA.md +0 -167
- claude_mpm/agents/BASE_RESEARCH.md +0 -53
- claude_mpm/agents/base_agent_loader.py +0 -601
- claude_mpm/cli/commands/agents_detect.py +0 -380
- claude_mpm/cli/commands/agents_recommend.py +0 -309
- claude_mpm/cli/ticket_cli.py +0 -35
- claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
- claude_mpm/commands/mpm-agents-detect.md +0 -177
- claude_mpm/commands/mpm-agents-list.md +0 -131
- claude_mpm/commands/mpm-agents-recommend.md +0 -223
- claude_mpm/commands/mpm-config-view.md +0 -150
- claude_mpm/commands/mpm-ticket-organize.md +0 -304
- 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/hooks/claude_hooks/__pycache__/__init__.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/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/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.0.9.dist-info/entry_points.txt +0 -10
- claude_mpm-5.0.9.dist-info/licenses/LICENSE +0 -21
- /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/WHEEL +0 -0
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/top_level.txt +0 -0
|
@@ -12,16 +12,21 @@ 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
|
|
26
31
|
from ...utils.agent_filters import apply_all_filters, get_deployed_agent_ids
|
|
27
32
|
from ...utils.console import console as default_console
|
|
@@ -73,6 +78,7 @@ class ConfigureCommand(BaseCommand):
|
|
|
73
78
|
self._navigation = None # Lazy-initialized
|
|
74
79
|
self._template_editor = None # Lazy-initialized
|
|
75
80
|
self._startup_manager = None # Lazy-initialized
|
|
81
|
+
self._recommendation_service = None # Lazy-initialized
|
|
76
82
|
|
|
77
83
|
def validate_args(self, args) -> Optional[str]:
|
|
78
84
|
"""Validate command arguments."""
|
|
@@ -149,6 +155,13 @@ class ConfigureCommand(BaseCommand):
|
|
|
149
155
|
)
|
|
150
156
|
return self._startup_manager
|
|
151
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
|
+
|
|
152
165
|
def run(self, args) -> CommandResult:
|
|
153
166
|
"""Execute the configure command."""
|
|
154
167
|
# Set configuration scope
|
|
@@ -308,45 +321,78 @@ class ConfigureCommand(BaseCommand):
|
|
|
308
321
|
self.navigation.display_header()
|
|
309
322
|
self.console.print("\n[bold blue]═══ Agent Management ═══[/bold blue]\n")
|
|
310
323
|
|
|
311
|
-
#
|
|
312
|
-
self.
|
|
324
|
+
# Load all agents with spinner (don't show partial state)
|
|
325
|
+
agents = self._load_agents_with_spinner()
|
|
313
326
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
sources_table = Table(show_header=True, header_style="bold white")
|
|
319
|
-
sources_table.add_column(
|
|
320
|
-
"Source",
|
|
321
|
-
style="bright_yellow",
|
|
322
|
-
width=40,
|
|
323
|
-
no_wrap=True,
|
|
324
|
-
overflow="ellipsis",
|
|
325
|
-
)
|
|
326
|
-
sources_table.add_column(
|
|
327
|
-
"Status", style="green", width=15, no_wrap=True
|
|
328
|
-
)
|
|
329
|
-
sources_table.add_column(
|
|
330
|
-
"Agents", style="yellow", width=10, no_wrap=True
|
|
327
|
+
if not agents:
|
|
328
|
+
self.console.print("[yellow]No agents found[/yellow]")
|
|
329
|
+
self.console.print(
|
|
330
|
+
"[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
|
|
331
331
|
)
|
|
332
|
+
Prompt.ask("\nPress Enter to continue")
|
|
333
|
+
break
|
|
332
334
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
335
|
+
# Now display everything at once (after all data loaded)
|
|
336
|
+
self._display_agent_sources_and_list(agents)
|
|
337
|
+
|
|
338
|
+
# Step 3: Simplified menu - only "Select Agents" option
|
|
339
|
+
self.console.print()
|
|
340
|
+
self.logger.debug("About to show agent management menu")
|
|
341
|
+
try:
|
|
342
|
+
choice = questionary.select(
|
|
343
|
+
"Agent Management:",
|
|
344
|
+
choices=[
|
|
345
|
+
"Select Agents",
|
|
346
|
+
questionary.Separator(),
|
|
347
|
+
"← Back to main menu",
|
|
348
|
+
],
|
|
349
|
+
style=self.QUESTIONARY_STYLE,
|
|
350
|
+
).ask()
|
|
351
|
+
|
|
352
|
+
if choice is None or choice == "← Back to main menu":
|
|
353
|
+
break
|
|
354
|
+
|
|
355
|
+
# Map selection to action
|
|
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
|
|
360
|
+
|
|
361
|
+
except KeyboardInterrupt:
|
|
362
|
+
self.console.print("\n[yellow]Operation cancelled[/yellow]")
|
|
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]"
|
|
338
381
|
)
|
|
382
|
+
Prompt.ask("\nPress Enter to continue")
|
|
383
|
+
break
|
|
339
384
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
self.console.print("[yellow]No agent sources configured[/yellow]")
|
|
343
|
-
self.console.print(
|
|
344
|
-
"[dim]Default source 'bobmatnyc/claude-mpm-agents' will be used[/dim]\n"
|
|
345
|
-
)
|
|
385
|
+
def _load_agents_with_spinner(self) -> List[AgentConfig]:
|
|
386
|
+
"""Load agents with loading indicator, don't show partial state.
|
|
346
387
|
|
|
347
|
-
|
|
348
|
-
|
|
388
|
+
Returns:
|
|
389
|
+
List of discovered agents with deployment status set.
|
|
390
|
+
"""
|
|
349
391
|
|
|
392
|
+
agents = []
|
|
393
|
+
with self.console.status(
|
|
394
|
+
"[bold blue]Loading agents...[/bold blue]", spinner="dots"
|
|
395
|
+
):
|
|
350
396
|
try:
|
|
351
397
|
# Discover agents (includes both local and remote)
|
|
352
398
|
agents = self.agent_manager.discover_agents(include_remote=True)
|
|
@@ -354,66 +400,68 @@ class ConfigureCommand(BaseCommand):
|
|
|
354
400
|
# Set deployment status on each agent for display
|
|
355
401
|
deployed_ids = get_deployed_agent_ids()
|
|
356
402
|
for agent in agents:
|
|
357
|
-
#
|
|
358
|
-
|
|
403
|
+
# Use agent_id (technical ID) for comparison, not display name
|
|
404
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
405
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
359
406
|
agent.is_deployed = agent_leaf_name in deployed_ids
|
|
360
407
|
|
|
361
408
|
# Filter BASE_AGENT from display (1M-502 Phase 1)
|
|
362
409
|
agents = self._filter_agent_configs(agents, filter_deployed=False)
|
|
363
410
|
|
|
364
|
-
if not agents:
|
|
365
|
-
self.console.print("[yellow]No agents found[/yellow]")
|
|
366
|
-
self.console.print(
|
|
367
|
-
"[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
|
|
368
|
-
)
|
|
369
|
-
else:
|
|
370
|
-
# Display agents in a table (already filtered at line 339)
|
|
371
|
-
self._display_agents_with_source_info(agents)
|
|
372
|
-
|
|
373
411
|
except Exception as e:
|
|
374
412
|
self.console.print(f"[red]Error discovering agents: {e}[/red]")
|
|
375
413
|
self.logger.error(f"Agent discovery failed: {e}", exc_info=True)
|
|
414
|
+
agents = []
|
|
376
415
|
|
|
377
|
-
|
|
378
|
-
self.console.print()
|
|
379
|
-
try:
|
|
380
|
-
choice = questionary.select(
|
|
381
|
-
"Agent Management:",
|
|
382
|
-
choices=[
|
|
383
|
-
"Manage sources (add/remove repositories)",
|
|
384
|
-
"Select Agents",
|
|
385
|
-
"Install preset (predefined sets)",
|
|
386
|
-
"Remove agents",
|
|
387
|
-
"View agent details",
|
|
388
|
-
"Toggle agents (legacy enable/disable)",
|
|
389
|
-
questionary.Separator(),
|
|
390
|
-
"← Back to main menu",
|
|
391
|
-
],
|
|
392
|
-
style=self.QUESTIONARY_STYLE,
|
|
393
|
-
).ask()
|
|
416
|
+
return agents
|
|
394
417
|
|
|
395
|
-
|
|
396
|
-
|
|
418
|
+
def _display_agent_sources_and_list(self, agents: List[AgentConfig]) -> None:
|
|
419
|
+
"""Display agent sources and agent list (only after all data loaded).
|
|
397
420
|
|
|
398
|
-
|
|
421
|
+
Args:
|
|
422
|
+
agents: List of discovered agents with deployment status.
|
|
423
|
+
"""
|
|
424
|
+
from rich.table import Table
|
|
399
425
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
426
|
+
# Step 1: Show configured sources
|
|
427
|
+
self.console.print("[bold white]═══ Agent Sources ═══[/bold white]\n")
|
|
428
|
+
|
|
429
|
+
sources = self._get_configured_sources()
|
|
430
|
+
if sources:
|
|
431
|
+
sources_table = Table(show_header=True, header_style="bold white")
|
|
432
|
+
sources_table.add_column(
|
|
433
|
+
"Source",
|
|
434
|
+
style="bright_yellow",
|
|
435
|
+
width=40,
|
|
436
|
+
no_wrap=True,
|
|
437
|
+
overflow="ellipsis",
|
|
438
|
+
)
|
|
439
|
+
sources_table.add_column("Status", style="green", width=15, no_wrap=True)
|
|
440
|
+
sources_table.add_column("Agents", style="yellow", width=10, no_wrap=True)
|
|
413
441
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
442
|
+
for source in sources:
|
|
443
|
+
status = "✓ Active" if source.get("enabled", True) else "Disabled"
|
|
444
|
+
agent_count = source.get("agent_count", "?")
|
|
445
|
+
sources_table.add_row(source["identifier"], status, str(agent_count))
|
|
446
|
+
|
|
447
|
+
self.console.print(sources_table)
|
|
448
|
+
else:
|
|
449
|
+
self.console.print("[yellow]No agent sources configured[/yellow]")
|
|
450
|
+
self.console.print(
|
|
451
|
+
"[dim]Default source 'bobmatnyc/claude-mpm-agents' will be used[/dim]\n"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Step 2: Display available agents
|
|
455
|
+
self.console.print("\n[bold white]═══ Available Agents ═══[/bold white]\n")
|
|
456
|
+
|
|
457
|
+
if agents:
|
|
458
|
+
# Show progress spinner while recommendation service processes agents
|
|
459
|
+
with self.console.status(
|
|
460
|
+
"[bold blue]Preparing agent list...[/bold blue]", spinner="dots"
|
|
461
|
+
):
|
|
462
|
+
self._display_agents_with_source_info(agents)
|
|
463
|
+
else:
|
|
464
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
417
465
|
|
|
418
466
|
def _display_agents_table(self, agents: List[AgentConfig]) -> None:
|
|
419
467
|
"""Display a table of available agents."""
|
|
@@ -472,6 +520,9 @@ class ConfigureCommand(BaseCommand):
|
|
|
472
520
|
if self.agent_manager.has_pending_changes():
|
|
473
521
|
self.agent_manager.commit_deferred_changes()
|
|
474
522
|
self.console.print("[green]✓ Changes saved successfully![/green]")
|
|
523
|
+
|
|
524
|
+
# Auto-deploy enabled agents to .claude/agents/
|
|
525
|
+
self._auto_deploy_enabled_agents(agents)
|
|
475
526
|
else:
|
|
476
527
|
self.console.print("[yellow]No changes to save.[/yellow]")
|
|
477
528
|
Prompt.ask("Press Enter to continue")
|
|
@@ -499,6 +550,60 @@ class ConfigureCommand(BaseCommand):
|
|
|
499
550
|
agent.name, not current
|
|
500
551
|
)
|
|
501
552
|
|
|
553
|
+
def _auto_deploy_enabled_agents(self, agents: List[AgentConfig]) -> None:
|
|
554
|
+
"""Auto-deploy enabled agents after saving configuration.
|
|
555
|
+
|
|
556
|
+
WHY: When users enable agents, they expect them to be deployed
|
|
557
|
+
automatically to .claude/agents/ so they're available for use.
|
|
558
|
+
"""
|
|
559
|
+
try:
|
|
560
|
+
# Get list of enabled agents from states
|
|
561
|
+
enabled_agents = [
|
|
562
|
+
agent
|
|
563
|
+
for agent in agents
|
|
564
|
+
if self.agent_manager.is_agent_enabled(agent.name)
|
|
565
|
+
]
|
|
566
|
+
|
|
567
|
+
if not enabled_agents:
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
# Show deployment progress
|
|
571
|
+
self.console.print(
|
|
572
|
+
f"\n[bold blue]Deploying {len(enabled_agents)} enabled agent(s)...[/bold blue]"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Deploy each enabled agent
|
|
576
|
+
success_count = 0
|
|
577
|
+
failed_count = 0
|
|
578
|
+
|
|
579
|
+
for agent in enabled_agents:
|
|
580
|
+
# Deploy to .claude/agents/ (project-level)
|
|
581
|
+
try:
|
|
582
|
+
if self._deploy_single_agent(agent, show_feedback=False):
|
|
583
|
+
success_count += 1
|
|
584
|
+
self.console.print(f"[green]✓ Deployed: {agent.name}[/green]")
|
|
585
|
+
else:
|
|
586
|
+
failed_count += 1
|
|
587
|
+
self.console.print(f"[yellow]⚠ Skipped: {agent.name}[/yellow]")
|
|
588
|
+
except Exception as e:
|
|
589
|
+
failed_count += 1
|
|
590
|
+
self.logger.error(f"Failed to deploy {agent.name}: {e}")
|
|
591
|
+
self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
|
|
592
|
+
|
|
593
|
+
# Show summary
|
|
594
|
+
if success_count > 0:
|
|
595
|
+
self.console.print(
|
|
596
|
+
f"\n[green]✓ Successfully deployed {success_count} agent(s) to .claude/agents/[/green]"
|
|
597
|
+
)
|
|
598
|
+
if failed_count > 0:
|
|
599
|
+
self.console.print(
|
|
600
|
+
f"[yellow]⚠ {failed_count} agent(s) failed or were skipped[/yellow]"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
except Exception as e:
|
|
604
|
+
self.logger.error(f"Auto-deployment failed: {e}", exc_info=True)
|
|
605
|
+
self.console.print(f"[red]✗ Auto-deployment error: {e}[/red]")
|
|
606
|
+
|
|
502
607
|
def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
|
|
503
608
|
"""Customize agent JSON template."""
|
|
504
609
|
self.template_editor.customize_agent_template(agents)
|
|
@@ -908,14 +1013,14 @@ class ConfigureCommand(BaseCommand):
|
|
|
908
1013
|
identifier = repo.identifier
|
|
909
1014
|
|
|
910
1015
|
# Count agents in cache
|
|
1016
|
+
# Note: identifier already includes subdirectory path (e.g., "bobmatnyc/claude-mpm-agents/agents")
|
|
911
1017
|
cache_dir = (
|
|
912
|
-
Path.home() / ".claude-mpm" / "cache" / "
|
|
1018
|
+
Path.home() / ".claude-mpm" / "cache" / "agents" / identifier
|
|
913
1019
|
)
|
|
914
1020
|
agent_count = 0
|
|
915
1021
|
if cache_dir.exists():
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
agent_count = len(list(agents_dir.rglob("*.md")))
|
|
1022
|
+
# cache_dir IS the agents directory - no need to append /agents
|
|
1023
|
+
agent_count = len(list(cache_dir.rglob("*.md")))
|
|
919
1024
|
|
|
920
1025
|
sources.append(
|
|
921
1026
|
{
|
|
@@ -969,21 +1074,123 @@ class ConfigureCommand(BaseCommand):
|
|
|
969
1074
|
filtered_names = {d["agent_id"] for d in filtered_dicts}
|
|
970
1075
|
return [a for a in agents if a.name in filtered_names]
|
|
971
1076
|
|
|
1077
|
+
@staticmethod
|
|
1078
|
+
def _calculate_column_widths(
|
|
1079
|
+
terminal_width: int, columns: Dict[str, int]
|
|
1080
|
+
) -> Dict[str, int]:
|
|
1081
|
+
"""Calculate dynamic column widths based on terminal size.
|
|
1082
|
+
|
|
1083
|
+
Args:
|
|
1084
|
+
terminal_width: Current terminal width in characters
|
|
1085
|
+
columns: Dict mapping column names to minimum widths
|
|
1086
|
+
|
|
1087
|
+
Returns:
|
|
1088
|
+
Dict mapping column names to calculated widths
|
|
1089
|
+
|
|
1090
|
+
Design:
|
|
1091
|
+
- Ensures minimum widths are respected
|
|
1092
|
+
- Distributes extra space proportionally
|
|
1093
|
+
- Handles narrow terminals gracefully (minimum 80 chars)
|
|
1094
|
+
"""
|
|
1095
|
+
# Ensure minimum terminal width
|
|
1096
|
+
min_terminal_width = 80
|
|
1097
|
+
terminal_width = max(terminal_width, min_terminal_width)
|
|
1098
|
+
|
|
1099
|
+
# Calculate total minimum width needed
|
|
1100
|
+
total_min_width = sum(columns.values())
|
|
1101
|
+
|
|
1102
|
+
# Account for table borders and padding (2 chars per column + 2 for edges)
|
|
1103
|
+
overhead = (len(columns) * 2) + 2
|
|
1104
|
+
available_width = terminal_width - overhead
|
|
1105
|
+
|
|
1106
|
+
# If we have extra space, distribute proportionally
|
|
1107
|
+
if available_width > total_min_width:
|
|
1108
|
+
extra_space = available_width - total_min_width
|
|
1109
|
+
total_weight = sum(columns.values())
|
|
1110
|
+
|
|
1111
|
+
result = {}
|
|
1112
|
+
for col_name, min_width in columns.items():
|
|
1113
|
+
# Distribute extra space based on minimum width proportion
|
|
1114
|
+
proportion = min_width / total_weight
|
|
1115
|
+
extra = int(extra_space * proportion)
|
|
1116
|
+
result[col_name] = min_width + extra
|
|
1117
|
+
return result
|
|
1118
|
+
# Terminal too narrow, use minimum widths
|
|
1119
|
+
return columns.copy()
|
|
1120
|
+
|
|
1121
|
+
def _format_display_name(self, name: str) -> str:
|
|
1122
|
+
"""Format internal agent name to human-readable display name.
|
|
1123
|
+
|
|
1124
|
+
Converts underscores/hyphens to spaces and title-cases.
|
|
1125
|
+
Examples:
|
|
1126
|
+
agentic_coder_optimizer -> Agentic Coder Optimizer
|
|
1127
|
+
python-engineer -> Python Engineer
|
|
1128
|
+
api_qa_agent -> Api Qa Agent
|
|
1129
|
+
|
|
1130
|
+
Args:
|
|
1131
|
+
name: Internal agent name (may contain underscores, hyphens)
|
|
1132
|
+
|
|
1133
|
+
Returns:
|
|
1134
|
+
Human-readable display name
|
|
1135
|
+
"""
|
|
1136
|
+
return name.replace("_", " ").replace("-", " ").title()
|
|
1137
|
+
|
|
972
1138
|
def _display_agents_with_source_info(self, agents: List[AgentConfig]) -> None:
|
|
973
1139
|
"""Display agents table with source information and installation status."""
|
|
974
1140
|
from rich.table import Table
|
|
975
1141
|
|
|
976
|
-
|
|
977
|
-
|
|
1142
|
+
# Get recommended agents for this project
|
|
1143
|
+
try:
|
|
1144
|
+
recommended_agents = self.recommendation_service.get_recommended_agents(
|
|
1145
|
+
str(self.project_dir)
|
|
1146
|
+
)
|
|
1147
|
+
except Exception as e:
|
|
1148
|
+
self.logger.warning(f"Failed to get recommended agents: {e}")
|
|
1149
|
+
recommended_agents = set()
|
|
1150
|
+
|
|
1151
|
+
# Get terminal width and calculate dynamic column widths
|
|
1152
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
1153
|
+
min_widths = {
|
|
1154
|
+
"#": 4,
|
|
1155
|
+
"Agent ID": 30,
|
|
1156
|
+
"Name": 20,
|
|
1157
|
+
"Source": 15,
|
|
1158
|
+
"Status": 10,
|
|
1159
|
+
}
|
|
1160
|
+
widths = self._calculate_column_widths(terminal_width, min_widths)
|
|
1161
|
+
|
|
1162
|
+
agents_table = Table(show_header=True, header_style="bold cyan")
|
|
1163
|
+
agents_table.add_column(
|
|
1164
|
+
"#", style="bright_black", width=widths["#"], no_wrap=True
|
|
1165
|
+
)
|
|
1166
|
+
agents_table.add_column(
|
|
1167
|
+
"Agent ID",
|
|
1168
|
+
style="bright_black",
|
|
1169
|
+
width=widths["Agent ID"],
|
|
1170
|
+
no_wrap=True,
|
|
1171
|
+
overflow="ellipsis",
|
|
1172
|
+
)
|
|
1173
|
+
agents_table.add_column(
|
|
1174
|
+
"Name",
|
|
1175
|
+
style="bright_cyan",
|
|
1176
|
+
width=widths["Name"],
|
|
1177
|
+
no_wrap=True,
|
|
1178
|
+
overflow="ellipsis",
|
|
1179
|
+
)
|
|
978
1180
|
agents_table.add_column(
|
|
979
|
-
"
|
|
1181
|
+
"Source",
|
|
1182
|
+
style="bright_yellow",
|
|
1183
|
+
width=widths["Source"],
|
|
1184
|
+
no_wrap=True,
|
|
980
1185
|
)
|
|
981
1186
|
agents_table.add_column(
|
|
982
|
-
"
|
|
1187
|
+
"Status", style="bright_black", width=widths["Status"], no_wrap=True
|
|
983
1188
|
)
|
|
984
|
-
agents_table.add_column("Source", style="bright_yellow", width=20, no_wrap=True)
|
|
985
|
-
agents_table.add_column("Status", style="white", width=12, no_wrap=True)
|
|
986
1189
|
|
|
1190
|
+
# FIX 3: Get deployed agent IDs once, before the loop (efficiency)
|
|
1191
|
+
deployed_ids = get_deployed_agent_ids()
|
|
1192
|
+
|
|
1193
|
+
recommended_count = 0
|
|
987
1194
|
for idx, agent in enumerate(agents, 1):
|
|
988
1195
|
# Determine source with repo name
|
|
989
1196
|
source_type = getattr(agent, "source_type", "local")
|
|
@@ -1011,23 +1218,83 @@ class ConfigureCommand(BaseCommand):
|
|
|
1011
1218
|
else:
|
|
1012
1219
|
source_label = "Local"
|
|
1013
1220
|
|
|
1014
|
-
#
|
|
1015
|
-
|
|
1221
|
+
# FIX 2: Check actual deployment status from .claude/agents/ directory
|
|
1222
|
+
# Use agent_id (technical ID like "python-engineer") not display name
|
|
1223
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1224
|
+
is_installed = agent_id in deployed_ids
|
|
1016
1225
|
if is_installed:
|
|
1017
1226
|
status = "[green]Installed[/green]"
|
|
1018
1227
|
else:
|
|
1019
1228
|
status = "Available"
|
|
1020
1229
|
|
|
1021
|
-
#
|
|
1022
|
-
|
|
1023
|
-
#
|
|
1230
|
+
# Check if agent is recommended
|
|
1231
|
+
# Handle both hierarchical paths (e.g., "engineer/backend/python-engineer")
|
|
1232
|
+
# and leaf names (e.g., "python-engineer")
|
|
1233
|
+
agent_full_path = agent.name
|
|
1234
|
+
agent_leaf_name = (
|
|
1235
|
+
agent_full_path.split("/")[-1]
|
|
1236
|
+
if "/" in agent_full_path
|
|
1237
|
+
else agent_full_path
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
for recommended_id in recommended_agents:
|
|
1241
|
+
# Check if the recommended_id matches either the full path or just the leaf name
|
|
1242
|
+
recommended_leaf = (
|
|
1243
|
+
recommended_id.split("/")[-1]
|
|
1244
|
+
if "/" in recommended_id
|
|
1245
|
+
else recommended_id
|
|
1246
|
+
)
|
|
1247
|
+
if (
|
|
1248
|
+
agent_full_path == recommended_id
|
|
1249
|
+
or agent_leaf_name == recommended_leaf
|
|
1250
|
+
):
|
|
1251
|
+
recommended_count += 1
|
|
1252
|
+
break
|
|
1253
|
+
|
|
1254
|
+
# FIX 1: Show agent_id (technical ID) in first column, not display name
|
|
1255
|
+
agent_id_display = getattr(agent, "agent_id", agent.name)
|
|
1256
|
+
|
|
1257
|
+
# Get display name and format it properly
|
|
1258
|
+
# Raw display_name from YAML may contain underscores (e.g., "agentic_coder_optimizer")
|
|
1259
|
+
raw_display_name = getattr(agent, "display_name", agent.name)
|
|
1260
|
+
display_name = self._format_display_name(raw_display_name)
|
|
1024
1261
|
|
|
1025
1262
|
agents_table.add_row(
|
|
1026
|
-
str(idx),
|
|
1263
|
+
str(idx), agent_id_display, display_name, source_label, status
|
|
1027
1264
|
)
|
|
1028
1265
|
|
|
1029
1266
|
self.console.print(agents_table)
|
|
1030
|
-
|
|
1267
|
+
|
|
1268
|
+
# Show legend if there are recommended agents
|
|
1269
|
+
if recommended_count > 0:
|
|
1270
|
+
# Get detection summary for context
|
|
1271
|
+
try:
|
|
1272
|
+
summary = self.recommendation_service.get_detection_summary(
|
|
1273
|
+
str(self.project_dir)
|
|
1274
|
+
)
|
|
1275
|
+
detected_langs = (
|
|
1276
|
+
", ".join(summary.get("detected_languages", [])) or "None"
|
|
1277
|
+
)
|
|
1278
|
+
", ".join(summary.get("detected_frameworks", [])) or "None"
|
|
1279
|
+
self.console.print(
|
|
1280
|
+
f"\n[dim]* = recommended for this project "
|
|
1281
|
+
f"(detected: {detected_langs})[/dim]"
|
|
1282
|
+
)
|
|
1283
|
+
except Exception:
|
|
1284
|
+
self.console.print("\n[dim]* = recommended for this project[/dim]")
|
|
1285
|
+
|
|
1286
|
+
# Show installed vs available count (use deployed_ids for accuracy)
|
|
1287
|
+
# Use agent_id (technical ID) for comparison, not display name
|
|
1288
|
+
installed_count = sum(
|
|
1289
|
+
1 for a in agents if getattr(a, "agent_id", a.name) in deployed_ids
|
|
1290
|
+
)
|
|
1291
|
+
available_count = len(agents) - installed_count
|
|
1292
|
+
self.console.print(
|
|
1293
|
+
f"\n[green]✓ {installed_count} installed[/green] | "
|
|
1294
|
+
f"[dim]{available_count} available[/dim] | "
|
|
1295
|
+
f"[yellow]{recommended_count} recommended[/yellow] | "
|
|
1296
|
+
f"[dim]Total: {len(agents)}[/dim]"
|
|
1297
|
+
)
|
|
1031
1298
|
|
|
1032
1299
|
def _manage_sources(self) -> None:
|
|
1033
1300
|
"""Interactive source management."""
|
|
@@ -1041,8 +1308,438 @@ class ConfigureCommand(BaseCommand):
|
|
|
1041
1308
|
self.console.print(" claude-mpm agent-source list")
|
|
1042
1309
|
Prompt.ask("\nPress Enter to continue")
|
|
1043
1310
|
|
|
1311
|
+
def _deploy_agents_unified(self, agents: List[AgentConfig]) -> None:
|
|
1312
|
+
"""Unified agent selection with inline controls for recommended, presets, and collections.
|
|
1313
|
+
|
|
1314
|
+
Design:
|
|
1315
|
+
- Single nested checkbox list with grouped agents by source/category
|
|
1316
|
+
- Inline controls at top: Select all, Select recommended, Select presets
|
|
1317
|
+
- Asterisk (*) marks recommended agents
|
|
1318
|
+
- Visual hierarchy: Source → Category → Individual agents
|
|
1319
|
+
- Loop with visual feedback: Controls update checkmarks immediately
|
|
1320
|
+
"""
|
|
1321
|
+
if not agents:
|
|
1322
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
1323
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1324
|
+
return
|
|
1325
|
+
|
|
1326
|
+
from claude_mpm.utils.agent_filters import (
|
|
1327
|
+
filter_base_agents,
|
|
1328
|
+
get_deployed_agent_ids,
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
# Filter BASE_AGENT but keep deployed agents visible
|
|
1332
|
+
all_agents = filter_base_agents(
|
|
1333
|
+
[
|
|
1334
|
+
{
|
|
1335
|
+
"agent_id": getattr(a, "agent_id", a.name),
|
|
1336
|
+
"name": a.name,
|
|
1337
|
+
"description": a.description,
|
|
1338
|
+
"deployed": getattr(a, "is_deployed", False),
|
|
1339
|
+
}
|
|
1340
|
+
for a in agents
|
|
1341
|
+
]
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
if not all_agents:
|
|
1345
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
1346
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1347
|
+
return
|
|
1348
|
+
|
|
1349
|
+
# Get deployed agent IDs and recommended agents
|
|
1350
|
+
deployed_ids = get_deployed_agent_ids()
|
|
1351
|
+
|
|
1352
|
+
try:
|
|
1353
|
+
recommended_agent_ids = self.recommendation_service.get_recommended_agents(
|
|
1354
|
+
str(self.project_dir)
|
|
1355
|
+
)
|
|
1356
|
+
except Exception as e:
|
|
1357
|
+
self.logger.warning(f"Failed to get recommended agents: {e}")
|
|
1358
|
+
recommended_agent_ids = set()
|
|
1359
|
+
|
|
1360
|
+
# Build mapping: leaf name -> full path for deployed agents
|
|
1361
|
+
# Use agent_id (technical ID) for comparison, not display name
|
|
1362
|
+
deployed_full_paths = set()
|
|
1363
|
+
for agent in agents:
|
|
1364
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1365
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
1366
|
+
if agent_leaf_name in deployed_ids:
|
|
1367
|
+
# Store agent_id for selection tracking (not display name)
|
|
1368
|
+
deployed_full_paths.add(agent_id)
|
|
1369
|
+
|
|
1370
|
+
# Track current selection state (starts with deployed, updated in loop)
|
|
1371
|
+
current_selection = deployed_full_paths.copy()
|
|
1372
|
+
|
|
1373
|
+
# Group agents by source/collection
|
|
1374
|
+
agent_map = {}
|
|
1375
|
+
collections = defaultdict(list)
|
|
1376
|
+
|
|
1377
|
+
for agent in agents:
|
|
1378
|
+
# Use agent_id (technical ID) for comparison, not display name
|
|
1379
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1380
|
+
if agent_id in {a["agent_id"] for a in all_agents}:
|
|
1381
|
+
# Determine collection ID
|
|
1382
|
+
source_type = getattr(agent, "source_type", "local")
|
|
1383
|
+
if source_type == "remote":
|
|
1384
|
+
source_dict = getattr(agent, "source_dict", {})
|
|
1385
|
+
repo_url = source_dict.get("source", "")
|
|
1386
|
+
if "/" in repo_url:
|
|
1387
|
+
parts = repo_url.rstrip("/").split("/")
|
|
1388
|
+
if len(parts) >= 2:
|
|
1389
|
+
# Use more readable collection name
|
|
1390
|
+
if (
|
|
1391
|
+
"bobmatnyc/claude-mpm" in repo_url
|
|
1392
|
+
or "claude-mpm" in repo_url.lower()
|
|
1393
|
+
):
|
|
1394
|
+
collection_id = "MPM Agents"
|
|
1395
|
+
else:
|
|
1396
|
+
collection_id = f"{parts[-2]}/{parts[-1]}"
|
|
1397
|
+
else:
|
|
1398
|
+
collection_id = "Community Agents"
|
|
1399
|
+
else:
|
|
1400
|
+
collection_id = "Community Agents"
|
|
1401
|
+
else:
|
|
1402
|
+
collection_id = "Local Agents"
|
|
1403
|
+
|
|
1404
|
+
collections[collection_id].append(agent)
|
|
1405
|
+
agent_map[agent_id] = agent
|
|
1406
|
+
|
|
1407
|
+
# Monkey-patch questionary symbols for better visibility
|
|
1408
|
+
questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
|
|
1409
|
+
questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
|
|
1410
|
+
|
|
1411
|
+
# MAIN LOOP: Re-display UI when controls are used
|
|
1412
|
+
while True:
|
|
1413
|
+
# Build unified checkbox choices with inline controls
|
|
1414
|
+
choices = []
|
|
1415
|
+
|
|
1416
|
+
for collection_id in sorted(collections.keys()):
|
|
1417
|
+
agents_in_collection = collections[collection_id]
|
|
1418
|
+
|
|
1419
|
+
# Count selected/total agents in collection
|
|
1420
|
+
# Use agent_id for selection tracking, not display name
|
|
1421
|
+
selected_count = sum(
|
|
1422
|
+
1
|
|
1423
|
+
for agent in agents_in_collection
|
|
1424
|
+
if getattr(agent, "agent_id", agent.name) in current_selection
|
|
1425
|
+
)
|
|
1426
|
+
total_count = len(agents_in_collection)
|
|
1427
|
+
|
|
1428
|
+
# Add collection header
|
|
1429
|
+
choices.append(
|
|
1430
|
+
Separator(
|
|
1431
|
+
f"\n── {collection_id} ({selected_count}/{total_count} selected) ──"
|
|
1432
|
+
)
|
|
1433
|
+
)
|
|
1434
|
+
|
|
1435
|
+
# Determine if all agents in collection are selected
|
|
1436
|
+
all_selected = selected_count == total_count
|
|
1437
|
+
|
|
1438
|
+
# Add inline control: Select/Deselect all from this collection
|
|
1439
|
+
if all_selected:
|
|
1440
|
+
choices.append(
|
|
1441
|
+
Choice(
|
|
1442
|
+
f" [Deselect all from {collection_id}]",
|
|
1443
|
+
value=f"__DESELECT_ALL_{collection_id}__",
|
|
1444
|
+
checked=False,
|
|
1445
|
+
)
|
|
1446
|
+
)
|
|
1447
|
+
else:
|
|
1448
|
+
choices.append(
|
|
1449
|
+
Choice(
|
|
1450
|
+
f" [Select all from {collection_id}]",
|
|
1451
|
+
value=f"__SELECT_ALL_{collection_id}__",
|
|
1452
|
+
checked=False,
|
|
1453
|
+
)
|
|
1454
|
+
)
|
|
1455
|
+
|
|
1456
|
+
# Add inline control: Select recommended from this collection
|
|
1457
|
+
recommended_in_collection = [
|
|
1458
|
+
a
|
|
1459
|
+
for a in agents_in_collection
|
|
1460
|
+
if any(
|
|
1461
|
+
a.name == rec_id
|
|
1462
|
+
or a.name.split("/")[-1] == rec_id.split("/")[-1]
|
|
1463
|
+
for rec_id in recommended_agent_ids
|
|
1464
|
+
)
|
|
1465
|
+
]
|
|
1466
|
+
if recommended_in_collection:
|
|
1467
|
+
recommended_selected = sum(
|
|
1468
|
+
1
|
|
1469
|
+
for a in recommended_in_collection
|
|
1470
|
+
if a.name in current_selection
|
|
1471
|
+
)
|
|
1472
|
+
if recommended_selected == len(recommended_in_collection):
|
|
1473
|
+
choices.append(
|
|
1474
|
+
Choice(
|
|
1475
|
+
f" [Deselect recommended ({len(recommended_in_collection)} agents)]",
|
|
1476
|
+
value=f"__DESELECT_REC_{collection_id}__",
|
|
1477
|
+
checked=False,
|
|
1478
|
+
)
|
|
1479
|
+
)
|
|
1480
|
+
else:
|
|
1481
|
+
choices.append(
|
|
1482
|
+
Choice(
|
|
1483
|
+
f" [Select recommended ({len(recommended_in_collection)} agents)]",
|
|
1484
|
+
value=f"__SELECT_REC_{collection_id}__",
|
|
1485
|
+
checked=False,
|
|
1486
|
+
)
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
# Add separator before individual agents
|
|
1490
|
+
choices.append(Separator())
|
|
1491
|
+
|
|
1492
|
+
# Group agents by category within collection (if hierarchical)
|
|
1493
|
+
category_groups = defaultdict(list)
|
|
1494
|
+
for agent in sorted(agents_in_collection, key=lambda a: a.name):
|
|
1495
|
+
# Extract category from hierarchical path (e.g., "engineer/backend/python-engineer")
|
|
1496
|
+
parts = agent.name.split("/")
|
|
1497
|
+
if len(parts) > 1:
|
|
1498
|
+
category = "/".join(parts[:-1]) # e.g., "engineer/backend"
|
|
1499
|
+
else:
|
|
1500
|
+
category = "" # No category
|
|
1501
|
+
category_groups[category].append(agent)
|
|
1502
|
+
|
|
1503
|
+
# Display agents grouped by category
|
|
1504
|
+
for category in sorted(category_groups.keys()):
|
|
1505
|
+
agents_in_category = category_groups[category]
|
|
1506
|
+
|
|
1507
|
+
# Add category separator if hierarchical
|
|
1508
|
+
if category:
|
|
1509
|
+
choices.append(Separator(f" {category}/"))
|
|
1510
|
+
|
|
1511
|
+
# Add individual agents
|
|
1512
|
+
for agent in agents_in_category:
|
|
1513
|
+
# Use agent_id (technical ID) for all tracking/selection
|
|
1514
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1515
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
1516
|
+
raw_display_name = getattr(
|
|
1517
|
+
agent, "display_name", agent_leaf_name
|
|
1518
|
+
)
|
|
1519
|
+
display_name = self._format_display_name(raw_display_name)
|
|
1520
|
+
|
|
1521
|
+
# Check if agent is deployed (exists in .claude/agents/)
|
|
1522
|
+
|
|
1523
|
+
# Format choice text (no asterisk needed)
|
|
1524
|
+
choice_text = f" {display_name}"
|
|
1525
|
+
|
|
1526
|
+
is_selected = agent_id in current_selection
|
|
1527
|
+
|
|
1528
|
+
choices.append(
|
|
1529
|
+
Choice(
|
|
1530
|
+
title=choice_text,
|
|
1531
|
+
value=agent_id, # Use agent_id for value
|
|
1532
|
+
checked=is_selected,
|
|
1533
|
+
)
|
|
1534
|
+
)
|
|
1535
|
+
|
|
1536
|
+
self.console.print("\n[bold cyan]Select Agents to Install[/bold cyan]")
|
|
1537
|
+
self.console.print("[dim][✓] Checked = Installed (uncheck to remove)[/dim]")
|
|
1538
|
+
self.console.print(
|
|
1539
|
+
"[dim][ ] Unchecked = Available (check to install)[/dim]"
|
|
1540
|
+
)
|
|
1541
|
+
self.console.print(
|
|
1542
|
+
"[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
try:
|
|
1546
|
+
selected_values = questionary.checkbox(
|
|
1547
|
+
"Select agents:",
|
|
1548
|
+
choices=choices,
|
|
1549
|
+
instruction="(Space to toggle, Enter to continue)",
|
|
1550
|
+
style=self.QUESTIONARY_STYLE,
|
|
1551
|
+
).ask()
|
|
1552
|
+
except Exception as e:
|
|
1553
|
+
import sys
|
|
1554
|
+
|
|
1555
|
+
self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
|
|
1556
|
+
self.console.print(
|
|
1557
|
+
"[red]Error: Could not display interactive menu[/red]"
|
|
1558
|
+
)
|
|
1559
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
1560
|
+
if not sys.stdin.isatty():
|
|
1561
|
+
self.console.print("[dim]Interactive terminal required. Use:[/dim]")
|
|
1562
|
+
self.console.print(
|
|
1563
|
+
"[dim] --list-agents to see available agents[/dim]"
|
|
1564
|
+
)
|
|
1565
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1566
|
+
return
|
|
1567
|
+
|
|
1568
|
+
if selected_values is None:
|
|
1569
|
+
self.console.print("[yellow]No changes made[/yellow]")
|
|
1570
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1571
|
+
return
|
|
1572
|
+
|
|
1573
|
+
# Check for inline control selections
|
|
1574
|
+
controls_selected = [v for v in selected_values if v.startswith("__")]
|
|
1575
|
+
|
|
1576
|
+
if controls_selected:
|
|
1577
|
+
# Process controls and update current_selection
|
|
1578
|
+
for control in controls_selected:
|
|
1579
|
+
if control.startswith("__SELECT_ALL_"):
|
|
1580
|
+
collection_id = control.replace("__SELECT_ALL_", "").replace(
|
|
1581
|
+
"__", ""
|
|
1582
|
+
)
|
|
1583
|
+
# Add all agents from this collection to current_selection
|
|
1584
|
+
for agent in collections[collection_id]:
|
|
1585
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1586
|
+
current_selection.add(agent_id)
|
|
1587
|
+
elif control.startswith("__DESELECT_ALL_"):
|
|
1588
|
+
collection_id = control.replace("__DESELECT_ALL_", "").replace(
|
|
1589
|
+
"__", ""
|
|
1590
|
+
)
|
|
1591
|
+
# Remove all agents from this collection
|
|
1592
|
+
for agent in collections[collection_id]:
|
|
1593
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1594
|
+
current_selection.discard(agent_id)
|
|
1595
|
+
elif control.startswith("__SELECT_REC_"):
|
|
1596
|
+
collection_id = control.replace("__SELECT_REC_", "").replace(
|
|
1597
|
+
"__", ""
|
|
1598
|
+
)
|
|
1599
|
+
# Add all recommended agents from this collection
|
|
1600
|
+
for agent in collections[collection_id]:
|
|
1601
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1602
|
+
if any(
|
|
1603
|
+
agent_id == rec_id
|
|
1604
|
+
or agent_id.split("/")[-1] == rec_id.split("/")[-1]
|
|
1605
|
+
for rec_id in recommended_agent_ids
|
|
1606
|
+
):
|
|
1607
|
+
current_selection.add(agent_id)
|
|
1608
|
+
elif control.startswith("__DESELECT_REC_"):
|
|
1609
|
+
collection_id = control.replace("__DESELECT_REC_", "").replace(
|
|
1610
|
+
"__", ""
|
|
1611
|
+
)
|
|
1612
|
+
# Remove all recommended agents from this collection
|
|
1613
|
+
for agent in collections[collection_id]:
|
|
1614
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1615
|
+
if any(
|
|
1616
|
+
agent_id == rec_id
|
|
1617
|
+
or agent_id.split("/")[-1] == rec_id.split("/")[-1]
|
|
1618
|
+
for rec_id in recommended_agent_ids
|
|
1619
|
+
):
|
|
1620
|
+
current_selection.discard(agent_id)
|
|
1621
|
+
|
|
1622
|
+
# Loop back to re-display with updated selections
|
|
1623
|
+
continue
|
|
1624
|
+
|
|
1625
|
+
# No controls selected - use the individual selections as final
|
|
1626
|
+
final_selection = set(selected_values)
|
|
1627
|
+
break
|
|
1628
|
+
|
|
1629
|
+
# Determine changes
|
|
1630
|
+
to_deploy = final_selection - deployed_full_paths
|
|
1631
|
+
to_remove = deployed_full_paths - final_selection
|
|
1632
|
+
|
|
1633
|
+
if not to_deploy and not to_remove:
|
|
1634
|
+
self.console.print("[yellow]No changes needed[/yellow]")
|
|
1635
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1636
|
+
return
|
|
1637
|
+
|
|
1638
|
+
# Show what will happen
|
|
1639
|
+
self.console.print("\n[bold]Changes to apply:[/bold]")
|
|
1640
|
+
if to_deploy:
|
|
1641
|
+
self.console.print(f"[green]Install {len(to_deploy)} agent(s)[/green]")
|
|
1642
|
+
for agent_id in to_deploy:
|
|
1643
|
+
self.console.print(f" + {agent_id}")
|
|
1644
|
+
if to_remove:
|
|
1645
|
+
self.console.print(f"[red]Remove {len(to_remove)} agent(s)[/red]")
|
|
1646
|
+
for agent_id in to_remove:
|
|
1647
|
+
self.console.print(f" - {agent_id}")
|
|
1648
|
+
|
|
1649
|
+
# Confirm
|
|
1650
|
+
if not Confirm.ask("\nApply these changes?", default=True):
|
|
1651
|
+
self.console.print("[yellow]Changes cancelled[/yellow]")
|
|
1652
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1653
|
+
return
|
|
1654
|
+
|
|
1655
|
+
# Execute changes
|
|
1656
|
+
deploy_success = 0
|
|
1657
|
+
deploy_fail = 0
|
|
1658
|
+
remove_success = 0
|
|
1659
|
+
remove_fail = 0
|
|
1660
|
+
|
|
1661
|
+
# Install new agents
|
|
1662
|
+
for agent_id in to_deploy:
|
|
1663
|
+
agent = agent_map.get(agent_id)
|
|
1664
|
+
if agent and self._deploy_single_agent(agent, show_feedback=False):
|
|
1665
|
+
deploy_success += 1
|
|
1666
|
+
self.console.print(f"[green]✓ Installed: {agent_id}[/green]")
|
|
1667
|
+
else:
|
|
1668
|
+
deploy_fail += 1
|
|
1669
|
+
self.console.print(f"[red]✗ Failed to install: {agent_id}[/red]")
|
|
1670
|
+
|
|
1671
|
+
# Remove agents
|
|
1672
|
+
for agent_id in to_remove:
|
|
1673
|
+
try:
|
|
1674
|
+
import json
|
|
1675
|
+
|
|
1676
|
+
# Extract leaf name to match deployed filename
|
|
1677
|
+
leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
|
|
1678
|
+
|
|
1679
|
+
# Remove from all possible locations
|
|
1680
|
+
paths_to_check = [
|
|
1681
|
+
Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
|
|
1682
|
+
Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
1683
|
+
Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
1684
|
+
]
|
|
1685
|
+
|
|
1686
|
+
removed = False
|
|
1687
|
+
for path in paths_to_check:
|
|
1688
|
+
if path.exists():
|
|
1689
|
+
path.unlink()
|
|
1690
|
+
removed = True
|
|
1691
|
+
|
|
1692
|
+
# Also remove from virtual deployment state
|
|
1693
|
+
deployment_state_paths = [
|
|
1694
|
+
Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
1695
|
+
Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
1696
|
+
]
|
|
1697
|
+
|
|
1698
|
+
for state_path in deployment_state_paths:
|
|
1699
|
+
if state_path.exists():
|
|
1700
|
+
try:
|
|
1701
|
+
with state_path.open() as f:
|
|
1702
|
+
state = json.load(f)
|
|
1703
|
+
agents_in_state = state.get("last_check_results", {}).get(
|
|
1704
|
+
"agents", {}
|
|
1705
|
+
)
|
|
1706
|
+
if leaf_name in agents_in_state:
|
|
1707
|
+
del agents_in_state[leaf_name]
|
|
1708
|
+
removed = True
|
|
1709
|
+
with state_path.open("w") as f:
|
|
1710
|
+
json.dump(state, f, indent=2)
|
|
1711
|
+
except (json.JSONDecodeError, KeyError):
|
|
1712
|
+
pass
|
|
1713
|
+
|
|
1714
|
+
if removed:
|
|
1715
|
+
remove_success += 1
|
|
1716
|
+
self.console.print(f"[green]✓ Removed: {agent_id}[/green]")
|
|
1717
|
+
else:
|
|
1718
|
+
remove_fail += 1
|
|
1719
|
+
self.console.print(f"[yellow]⚠ Not found: {agent_id}[/yellow]")
|
|
1720
|
+
except Exception as e:
|
|
1721
|
+
remove_fail += 1
|
|
1722
|
+
self.console.print(f"[red]✗ Failed to remove {agent_id}: {e}[/red]")
|
|
1723
|
+
|
|
1724
|
+
# Show summary
|
|
1725
|
+
self.console.print()
|
|
1726
|
+
if deploy_success > 0:
|
|
1727
|
+
self.console.print(f"[green]✓ Installed {deploy_success} agent(s)[/green]")
|
|
1728
|
+
if deploy_fail > 0:
|
|
1729
|
+
self.console.print(f"[red]✗ Failed to install {deploy_fail} agent(s)[/red]")
|
|
1730
|
+
if remove_success > 0:
|
|
1731
|
+
self.console.print(f"[green]✓ Removed {remove_success} agent(s)[/green]")
|
|
1732
|
+
if remove_fail > 0:
|
|
1733
|
+
self.console.print(f"[red]✗ Failed to remove {remove_fail} agent(s)[/red]")
|
|
1734
|
+
|
|
1735
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1736
|
+
|
|
1044
1737
|
def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
|
|
1045
|
-
"""Manage agent installation state (unified install/remove interface).
|
|
1738
|
+
"""Manage agent installation state (unified install/remove interface).
|
|
1739
|
+
|
|
1740
|
+
DEPRECATED: Use _deploy_agents_unified instead.
|
|
1741
|
+
This method is kept for backward compatibility but should not be used.
|
|
1742
|
+
"""
|
|
1046
1743
|
if not agents:
|
|
1047
1744
|
self.console.print("[yellow]No agents available[/yellow]")
|
|
1048
1745
|
Prompt.ask("\nPress Enter to continue")
|
|
@@ -1058,7 +1755,7 @@ class ConfigureCommand(BaseCommand):
|
|
|
1058
1755
|
all_agents = filter_base_agents(
|
|
1059
1756
|
[
|
|
1060
1757
|
{
|
|
1061
|
-
"agent_id": a.name,
|
|
1758
|
+
"agent_id": getattr(a, "agent_id", a.name),
|
|
1062
1759
|
"name": a.name,
|
|
1063
1760
|
"description": a.description,
|
|
1064
1761
|
"deployed": getattr(a, "is_deployed", False),
|
|
@@ -1067,7 +1764,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
1067
1764
|
]
|
|
1068
1765
|
)
|
|
1069
1766
|
|
|
1070
|
-
# Get deployed agent IDs
|
|
1767
|
+
# Get deployed agent IDs (original state - for calculating final changes)
|
|
1768
|
+
# NOTE: deployed_ids contains LEAF NAMES (e.g., "python-engineer")
|
|
1071
1769
|
deployed_ids = get_deployed_agent_ids()
|
|
1072
1770
|
|
|
1073
1771
|
if not all_agents:
|
|
@@ -1075,68 +1773,296 @@ class ConfigureCommand(BaseCommand):
|
|
|
1075
1773
|
Prompt.ask("\nPress Enter to continue")
|
|
1076
1774
|
return
|
|
1077
1775
|
|
|
1776
|
+
# Build mapping: leaf name -> full path for deployed agents
|
|
1777
|
+
# This allows comparing deployed_ids (leaf names) with agent.agent_id (full paths)
|
|
1778
|
+
deployed_full_paths = set()
|
|
1779
|
+
for agent in agents:
|
|
1780
|
+
# FIX: Use agent_id (technical ID) instead of display name
|
|
1781
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1782
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
1783
|
+
if agent_leaf_name in deployed_ids:
|
|
1784
|
+
deployed_full_paths.add(agent_id)
|
|
1785
|
+
|
|
1786
|
+
# Track current selection state (starts with deployed full paths, updated after each iteration)
|
|
1787
|
+
current_selection = deployed_full_paths.copy()
|
|
1788
|
+
|
|
1078
1789
|
# Loop to allow adjusting selection
|
|
1079
1790
|
while True:
|
|
1080
|
-
# Build
|
|
1081
|
-
agent_choices = []
|
|
1791
|
+
# Build agent mapping and collections
|
|
1082
1792
|
agent_map = {} # For lookup after selection
|
|
1793
|
+
collections = defaultdict(list)
|
|
1083
1794
|
|
|
1084
1795
|
for agent in agents:
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
#
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
)
|
|
1796
|
+
# FIX: Use agent_id (technical ID) for comparison
|
|
1797
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1798
|
+
if agent_id in {a["agent_id"] for a in all_agents}:
|
|
1799
|
+
# Determine collection ID
|
|
1800
|
+
source_type = getattr(agent, "source_type", "local")
|
|
1801
|
+
if source_type == "remote":
|
|
1802
|
+
source_dict = getattr(agent, "source_dict", {})
|
|
1803
|
+
repo_url = source_dict.get("source", "")
|
|
1804
|
+
# Extract repository name from URL
|
|
1805
|
+
if "/" in repo_url:
|
|
1806
|
+
parts = repo_url.rstrip("/").split("/")
|
|
1807
|
+
if len(parts) >= 2:
|
|
1808
|
+
collection_id = f"{parts[-2]}/{parts[-1]}"
|
|
1809
|
+
else:
|
|
1810
|
+
collection_id = "remote"
|
|
1811
|
+
else:
|
|
1812
|
+
collection_id = "remote"
|
|
1813
|
+
else:
|
|
1814
|
+
collection_id = "local"
|
|
1105
1815
|
|
|
1106
|
-
|
|
1107
|
-
agent_map[
|
|
1816
|
+
collections[collection_id].append(agent)
|
|
1817
|
+
agent_map[agent_id] = agent # FIX: Use agent_id as key
|
|
1108
1818
|
|
|
1109
|
-
#
|
|
1110
|
-
self.console.print("\n[bold cyan]
|
|
1111
|
-
self.console.print(
|
|
1112
|
-
|
|
1819
|
+
# STEP 1: Collection-level selection
|
|
1820
|
+
self.console.print("\n[bold cyan]Select Agent Collections[/bold cyan]")
|
|
1821
|
+
self.console.print(
|
|
1822
|
+
"[dim]Checking a collection installs ALL agents in that collection[/dim]"
|
|
1823
|
+
)
|
|
1824
|
+
self.console.print(
|
|
1825
|
+
"[dim]Unchecking a collection removes ALL agents in that collection[/dim]"
|
|
1826
|
+
)
|
|
1113
1827
|
self.console.print(
|
|
1114
|
-
"[dim]
|
|
1115
|
-
"Enter to apply changes[/dim]\n"
|
|
1828
|
+
"[dim]For partial deployment, use 'Fine-tune individual agents'[/dim]\n"
|
|
1116
1829
|
)
|
|
1117
1830
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
"Agents:", choices=agent_choices, style=self.QUESTIONARY_STYLE
|
|
1122
|
-
).ask()
|
|
1831
|
+
collection_choices = []
|
|
1832
|
+
for collection_id in sorted(collections.keys()):
|
|
1833
|
+
agents_in_collection = collections[collection_id]
|
|
1123
1834
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1835
|
+
# Check if ANY agent in this collection is currently deployed
|
|
1836
|
+
# This reflects actual deployment state, not just selection
|
|
1837
|
+
# FIX: Use agent_id for comparison with current_selection
|
|
1838
|
+
any_deployed = any(
|
|
1839
|
+
getattr(agent, "agent_id", agent.name) in current_selection
|
|
1840
|
+
for agent in agents_in_collection
|
|
1841
|
+
)
|
|
1842
|
+
|
|
1843
|
+
# Count deployed agents for display
|
|
1844
|
+
# FIX: Use agent_id for comparison with current_selection
|
|
1845
|
+
deployed_count = sum(
|
|
1846
|
+
1
|
|
1847
|
+
for agent in agents_in_collection
|
|
1848
|
+
if getattr(agent, "agent_id", agent.name) in current_selection
|
|
1849
|
+
)
|
|
1850
|
+
|
|
1851
|
+
collection_choices.append(
|
|
1852
|
+
Choice(
|
|
1853
|
+
f"{collection_id} ({deployed_count}/{len(agents_in_collection)} deployed)",
|
|
1854
|
+
value=collection_id,
|
|
1855
|
+
checked=any_deployed,
|
|
1856
|
+
)
|
|
1857
|
+
)
|
|
1858
|
+
|
|
1859
|
+
# Add option to fine-tune individual agents
|
|
1860
|
+
collection_choices.append(Separator())
|
|
1861
|
+
collection_choices.append(
|
|
1862
|
+
Choice(
|
|
1863
|
+
"→ Fine-tune individual agents...",
|
|
1864
|
+
value="__INDIVIDUAL__",
|
|
1865
|
+
checked=False,
|
|
1866
|
+
)
|
|
1867
|
+
)
|
|
1868
|
+
|
|
1869
|
+
# Monkey-patch questionary symbols for better visibility
|
|
1870
|
+
questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
|
|
1871
|
+
questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
|
|
1872
|
+
|
|
1873
|
+
try:
|
|
1874
|
+
selected_collections = questionary.checkbox(
|
|
1875
|
+
"Select agent collections to deploy:",
|
|
1876
|
+
choices=collection_choices,
|
|
1877
|
+
instruction="(Space to toggle, Enter to continue)",
|
|
1878
|
+
style=self.QUESTIONARY_STYLE,
|
|
1879
|
+
).ask()
|
|
1880
|
+
except Exception as e:
|
|
1881
|
+
import sys
|
|
1882
|
+
|
|
1883
|
+
self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
|
|
1884
|
+
self.console.print(
|
|
1885
|
+
"[red]Error: Could not display interactive menu[/red]"
|
|
1886
|
+
)
|
|
1887
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
1888
|
+
if not sys.stdin.isatty():
|
|
1889
|
+
self.console.print("[dim]Interactive terminal required. Use:[/dim]")
|
|
1890
|
+
self.console.print(
|
|
1891
|
+
"[dim] --list-agents to see available agents[/dim]"
|
|
1892
|
+
)
|
|
1893
|
+
self.console.print(
|
|
1894
|
+
"[dim] --enable-agent/--disable-agent for scripting[/dim]"
|
|
1895
|
+
)
|
|
1896
|
+
else:
|
|
1897
|
+
self.console.print(
|
|
1898
|
+
"[dim]This might be a terminal compatibility issue.[/dim]"
|
|
1899
|
+
)
|
|
1900
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1901
|
+
return
|
|
1902
|
+
|
|
1903
|
+
# Handle cancellation
|
|
1904
|
+
if selected_collections is None:
|
|
1905
|
+
import sys
|
|
1906
|
+
|
|
1907
|
+
if not sys.stdin.isatty():
|
|
1908
|
+
self.console.print(
|
|
1909
|
+
"[red]Error: Interactive terminal required for agent selection[/red]"
|
|
1910
|
+
)
|
|
1911
|
+
self.console.print(
|
|
1912
|
+
"[dim]Use --list-agents to see available agents[/dim]"
|
|
1913
|
+
)
|
|
1914
|
+
self.console.print(
|
|
1915
|
+
"[dim]Use --enable-agent/--disable-agent for non-interactive mode[/dim]"
|
|
1916
|
+
)
|
|
1917
|
+
else:
|
|
1918
|
+
self.console.print("[yellow]No changes made[/yellow]")
|
|
1127
1919
|
Prompt.ask("\nPress Enter to continue")
|
|
1128
1920
|
return
|
|
1129
1921
|
|
|
1130
|
-
#
|
|
1131
|
-
|
|
1132
|
-
|
|
1922
|
+
# STEP 2: Check if user wants individual selection
|
|
1923
|
+
if "__INDIVIDUAL__" in selected_collections:
|
|
1924
|
+
# Remove the __INDIVIDUAL__ marker
|
|
1925
|
+
selected_collections = [
|
|
1926
|
+
c for c in selected_collections if c != "__INDIVIDUAL__"
|
|
1927
|
+
]
|
|
1133
1928
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1929
|
+
# Build individual agent choices with grouping
|
|
1930
|
+
agent_choices = []
|
|
1931
|
+
for collection_id in sorted(collections.keys()):
|
|
1932
|
+
agents_in_collection = collections[collection_id]
|
|
1933
|
+
|
|
1934
|
+
# Add collection header separator
|
|
1935
|
+
agent_choices.append(
|
|
1936
|
+
Separator(
|
|
1937
|
+
f"\n── {collection_id} ({len(agents_in_collection)} agents) ──"
|
|
1938
|
+
)
|
|
1939
|
+
)
|
|
1940
|
+
|
|
1941
|
+
# Add individual agents from this collection
|
|
1942
|
+
# FIX: Use agent_id for sorting, comparison, and values
|
|
1943
|
+
for agent in sorted(
|
|
1944
|
+
agents_in_collection,
|
|
1945
|
+
key=lambda a: getattr(a, "agent_id", a.name),
|
|
1946
|
+
):
|
|
1947
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1948
|
+
raw_display_name = getattr(agent, "display_name", agent.name)
|
|
1949
|
+
display_name = self._format_display_name(raw_display_name)
|
|
1950
|
+
is_selected = agent_id in deployed_full_paths
|
|
1951
|
+
|
|
1952
|
+
choice_text = f"{agent_id}"
|
|
1953
|
+
if display_name and display_name != agent_id:
|
|
1954
|
+
choice_text += f" - {display_name}"
|
|
1955
|
+
|
|
1956
|
+
agent_choices.append(
|
|
1957
|
+
Choice(
|
|
1958
|
+
title=choice_text, value=agent_id, checked=is_selected
|
|
1959
|
+
)
|
|
1960
|
+
)
|
|
1961
|
+
|
|
1962
|
+
self.console.print(
|
|
1963
|
+
"\n[bold cyan]Fine-tune Individual Agents[/bold cyan]"
|
|
1964
|
+
)
|
|
1965
|
+
self.console.print(
|
|
1966
|
+
"[dim][✓] Checked = Installed (uncheck to remove)[/dim]"
|
|
1967
|
+
)
|
|
1968
|
+
self.console.print(
|
|
1969
|
+
"[dim][ ] Unchecked = Available (check to install)[/dim]"
|
|
1970
|
+
)
|
|
1971
|
+
self.console.print(
|
|
1972
|
+
"[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
|
|
1973
|
+
)
|
|
1974
|
+
|
|
1975
|
+
try:
|
|
1976
|
+
selected_agent_ids = questionary.checkbox(
|
|
1977
|
+
"Select individual agents:",
|
|
1978
|
+
choices=agent_choices,
|
|
1979
|
+
style=self.QUESTIONARY_STYLE,
|
|
1980
|
+
).ask()
|
|
1981
|
+
except Exception as e:
|
|
1982
|
+
import sys
|
|
1983
|
+
|
|
1984
|
+
self.logger.error(
|
|
1985
|
+
f"Questionary checkbox failed: {e}", exc_info=True
|
|
1986
|
+
)
|
|
1987
|
+
self.console.print(
|
|
1988
|
+
"[red]Error: Could not display interactive menu[/red]"
|
|
1989
|
+
)
|
|
1990
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
1991
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1992
|
+
return
|
|
1993
|
+
|
|
1994
|
+
if selected_agent_ids is None:
|
|
1995
|
+
self.console.print("[yellow]No changes made[/yellow]")
|
|
1996
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1997
|
+
return
|
|
1998
|
+
|
|
1999
|
+
# Update current_selection with individual selections
|
|
2000
|
+
current_selection = set(selected_agent_ids)
|
|
2001
|
+
else:
|
|
2002
|
+
# Apply collection-level selections
|
|
2003
|
+
# For each collection, if it's selected, include ALL its agents
|
|
2004
|
+
# If it's not selected, exclude ALL its agents
|
|
2005
|
+
final_selections = set()
|
|
2006
|
+
for collection_id in selected_collections:
|
|
2007
|
+
for agent in collections[collection_id]:
|
|
2008
|
+
# FIX: Use agent_id for selection tracking
|
|
2009
|
+
final_selections.add(getattr(agent, "agent_id", agent.name))
|
|
2010
|
+
|
|
2011
|
+
# Update current_selection
|
|
2012
|
+
# This replaces the previous selection entirely with the new collection selections
|
|
2013
|
+
current_selection = final_selections
|
|
2014
|
+
|
|
2015
|
+
# Determine actions based on ORIGINAL deployed state
|
|
2016
|
+
# Compare full paths to full paths (deployed_full_paths was built from deployed_ids)
|
|
2017
|
+
to_deploy = (
|
|
2018
|
+
current_selection - deployed_full_paths
|
|
2019
|
+
) # Selected but not originally deployed
|
|
2020
|
+
|
|
2021
|
+
# For removal, verify files actually exist before adding to the set
|
|
2022
|
+
# This prevents "Not found" warnings when multiple agents share leaf names
|
|
2023
|
+
to_remove = set()
|
|
2024
|
+
for agent_id in deployed_full_paths - current_selection:
|
|
2025
|
+
# Extract leaf name to check file existence
|
|
2026
|
+
leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
|
|
2027
|
+
|
|
2028
|
+
# Check all possible locations
|
|
2029
|
+
paths_to_check = [
|
|
2030
|
+
Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
|
|
2031
|
+
Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
2032
|
+
Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
2033
|
+
]
|
|
2034
|
+
|
|
2035
|
+
# Also check virtual deployment state
|
|
2036
|
+
state_exists = False
|
|
2037
|
+
deployment_state_paths = [
|
|
2038
|
+
Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2039
|
+
Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2040
|
+
]
|
|
2041
|
+
|
|
2042
|
+
for state_path in deployment_state_paths:
|
|
2043
|
+
if state_path.exists():
|
|
2044
|
+
try:
|
|
2045
|
+
import json
|
|
2046
|
+
|
|
2047
|
+
with state_path.open() as f:
|
|
2048
|
+
state = json.load(f)
|
|
2049
|
+
agents_in_state = state.get("last_check_results", {}).get(
|
|
2050
|
+
"agents", {}
|
|
2051
|
+
)
|
|
2052
|
+
if leaf_name in agents_in_state:
|
|
2053
|
+
state_exists = True
|
|
2054
|
+
break
|
|
2055
|
+
except (json.JSONDecodeError, KeyError):
|
|
2056
|
+
continue
|
|
2057
|
+
|
|
2058
|
+
# Only add to removal set if file or state entry actually exists
|
|
2059
|
+
if any(p.exists() for p in paths_to_check) or state_exists:
|
|
2060
|
+
to_remove.add(agent_id)
|
|
1137
2061
|
|
|
1138
2062
|
if not to_deploy and not to_remove:
|
|
1139
|
-
self.console.print(
|
|
2063
|
+
self.console.print(
|
|
2064
|
+
"[yellow]No changes needed - all selected agents are already installed[/yellow]"
|
|
2065
|
+
)
|
|
1140
2066
|
Prompt.ask("\nPress Enter to continue")
|
|
1141
2067
|
return
|
|
1142
2068
|
|
|
@@ -1168,7 +2094,7 @@ class ConfigureCommand(BaseCommand):
|
|
|
1168
2094
|
Prompt.ask("\nPress Enter to continue")
|
|
1169
2095
|
return
|
|
1170
2096
|
if action == "adjust":
|
|
1171
|
-
#
|
|
2097
|
+
# current_selection is already updated, loop will use it
|
|
1172
2098
|
continue
|
|
1173
2099
|
|
|
1174
2100
|
# Execute changes
|
|
@@ -1191,14 +2117,22 @@ class ConfigureCommand(BaseCommand):
|
|
|
1191
2117
|
for agent_id in to_remove:
|
|
1192
2118
|
try:
|
|
1193
2119
|
import json
|
|
1194
|
-
|
|
2120
|
+
# Note: Path is already imported at module level (line 17)
|
|
2121
|
+
|
|
2122
|
+
# Extract leaf name to match deployed filename
|
|
2123
|
+
# agent_id may be hierarchical (e.g., "engineer/mobile/tauri-engineer")
|
|
2124
|
+
# but deployed files use flattened leaf names (e.g., "tauri-engineer.md")
|
|
2125
|
+
if "/" in agent_id:
|
|
2126
|
+
leaf_name = agent_id.split("/")[-1]
|
|
2127
|
+
else:
|
|
2128
|
+
leaf_name = agent_id
|
|
1195
2129
|
|
|
1196
2130
|
# Remove from project, legacy, and user locations
|
|
1197
2131
|
project_path = (
|
|
1198
|
-
Path.cwd() / ".claude-mpm" / "agents" / f"{
|
|
2132
|
+
Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md"
|
|
1199
2133
|
)
|
|
1200
|
-
legacy_path = Path.cwd() / ".claude" / "agents" / f"{
|
|
1201
|
-
user_path = Path.home() / ".claude" / "agents" / f"{
|
|
2134
|
+
legacy_path = Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md"
|
|
2135
|
+
user_path = Path.home() / ".claude" / "agents" / f"{leaf_name}.md"
|
|
1202
2136
|
|
|
1203
2137
|
removed = False
|
|
1204
2138
|
for path in [project_path, legacy_path, user_path]:
|
|
@@ -1219,11 +2153,12 @@ class ConfigureCommand(BaseCommand):
|
|
|
1219
2153
|
state = json.load(f)
|
|
1220
2154
|
|
|
1221
2155
|
# Remove agent from deployment state
|
|
2156
|
+
# Deployment state uses leaf names, not full hierarchical paths
|
|
1222
2157
|
agents = state.get("last_check_results", {}).get(
|
|
1223
2158
|
"agents", {}
|
|
1224
2159
|
)
|
|
1225
|
-
if
|
|
1226
|
-
del agents[
|
|
2160
|
+
if leaf_name in agents:
|
|
2161
|
+
del agents[leaf_name]
|
|
1227
2162
|
removed = True
|
|
1228
2163
|
|
|
1229
2164
|
# Save updated state
|
|
@@ -1347,6 +2282,169 @@ class ConfigureCommand(BaseCommand):
|
|
|
1347
2282
|
self.logger.error(f"Preset installation failed: {e}", exc_info=True)
|
|
1348
2283
|
Prompt.ask("\nPress Enter to continue")
|
|
1349
2284
|
|
|
2285
|
+
def _select_recommended_agents(self, agents: List[AgentConfig]) -> None:
|
|
2286
|
+
"""Select and install recommended agents based on toolchain detection."""
|
|
2287
|
+
if not agents:
|
|
2288
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
2289
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2290
|
+
return
|
|
2291
|
+
|
|
2292
|
+
self.console.clear()
|
|
2293
|
+
self.console.print(
|
|
2294
|
+
"\n[bold white]═══ Recommended Agents for This Project ═══[/bold white]\n"
|
|
2295
|
+
)
|
|
2296
|
+
|
|
2297
|
+
# Get recommended agent IDs
|
|
2298
|
+
try:
|
|
2299
|
+
recommended_agent_ids = self.recommendation_service.get_recommended_agents(
|
|
2300
|
+
str(self.project_dir)
|
|
2301
|
+
)
|
|
2302
|
+
except Exception as e:
|
|
2303
|
+
self.console.print(f"[red]Error detecting toolchain: {e}[/red]")
|
|
2304
|
+
self.logger.error(f"Toolchain detection failed: {e}", exc_info=True)
|
|
2305
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2306
|
+
return
|
|
2307
|
+
|
|
2308
|
+
if not recommended_agent_ids:
|
|
2309
|
+
self.console.print("[yellow]No recommended agents found[/yellow]")
|
|
2310
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2311
|
+
return
|
|
2312
|
+
|
|
2313
|
+
# Get detection summary
|
|
2314
|
+
try:
|
|
2315
|
+
summary = self.recommendation_service.get_detection_summary(
|
|
2316
|
+
str(self.project_dir)
|
|
2317
|
+
)
|
|
2318
|
+
|
|
2319
|
+
self.console.print("[bold]Detected Project Stack:[/bold]")
|
|
2320
|
+
if summary.get("detected_languages"):
|
|
2321
|
+
self.console.print(
|
|
2322
|
+
f" Languages: [cyan]{', '.join(summary['detected_languages'])}[/cyan]"
|
|
2323
|
+
)
|
|
2324
|
+
if summary.get("detected_frameworks"):
|
|
2325
|
+
self.console.print(
|
|
2326
|
+
f" Frameworks: [cyan]{', '.join(summary['detected_frameworks'])}[/cyan]"
|
|
2327
|
+
)
|
|
2328
|
+
self.console.print(
|
|
2329
|
+
f" Detection Quality: [{'green' if summary.get('detection_quality') == 'high' else 'yellow'}]{summary.get('detection_quality', 'unknown')}[/]"
|
|
2330
|
+
)
|
|
2331
|
+
self.console.print()
|
|
2332
|
+
except Exception:
|
|
2333
|
+
pass
|
|
2334
|
+
|
|
2335
|
+
# Build mapping: agent_id -> AgentConfig
|
|
2336
|
+
agent_map = {agent.name: agent for agent in agents}
|
|
2337
|
+
|
|
2338
|
+
# Also check leaf names for matching
|
|
2339
|
+
for agent in agents:
|
|
2340
|
+
leaf_name = agent.name.split("/")[-1] if "/" in agent.name else agent.name
|
|
2341
|
+
if leaf_name not in agent_map:
|
|
2342
|
+
agent_map[leaf_name] = agent
|
|
2343
|
+
|
|
2344
|
+
# Find matching agents from available agents
|
|
2345
|
+
matched_agents = []
|
|
2346
|
+
for recommended_id in recommended_agent_ids:
|
|
2347
|
+
# Try full path match first
|
|
2348
|
+
if recommended_id in agent_map:
|
|
2349
|
+
matched_agents.append(agent_map[recommended_id])
|
|
2350
|
+
else:
|
|
2351
|
+
# Try leaf name match
|
|
2352
|
+
recommended_leaf = (
|
|
2353
|
+
recommended_id.split("/")[-1]
|
|
2354
|
+
if "/" in recommended_id
|
|
2355
|
+
else recommended_id
|
|
2356
|
+
)
|
|
2357
|
+
if recommended_leaf in agent_map:
|
|
2358
|
+
matched_agents.append(agent_map[recommended_leaf])
|
|
2359
|
+
|
|
2360
|
+
if not matched_agents:
|
|
2361
|
+
self.console.print(
|
|
2362
|
+
"[yellow]No matching agents found in available sources[/yellow]"
|
|
2363
|
+
)
|
|
2364
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2365
|
+
return
|
|
2366
|
+
|
|
2367
|
+
# Display recommended agents
|
|
2368
|
+
self.console.print(
|
|
2369
|
+
f"[bold]Recommended Agents ({len(matched_agents)}):[/bold]\n"
|
|
2370
|
+
)
|
|
2371
|
+
|
|
2372
|
+
from rich.table import Table
|
|
2373
|
+
|
|
2374
|
+
rec_table = Table(show_header=True, header_style="bold white")
|
|
2375
|
+
rec_table.add_column("#", style="dim", width=4)
|
|
2376
|
+
rec_table.add_column("Agent ID", style="cyan", width=40)
|
|
2377
|
+
rec_table.add_column("Status", style="white", width=15)
|
|
2378
|
+
|
|
2379
|
+
for idx, agent in enumerate(matched_agents, 1):
|
|
2380
|
+
is_installed = getattr(agent, "is_deployed", False)
|
|
2381
|
+
status = (
|
|
2382
|
+
"[green]Already Installed[/green]"
|
|
2383
|
+
if is_installed
|
|
2384
|
+
else "[yellow]Not Installed[/yellow]"
|
|
2385
|
+
)
|
|
2386
|
+
rec_table.add_row(str(idx), agent.name, status)
|
|
2387
|
+
|
|
2388
|
+
self.console.print(rec_table)
|
|
2389
|
+
|
|
2390
|
+
# Count how many need installation
|
|
2391
|
+
to_install = [a for a in matched_agents if not getattr(a, "is_deployed", False)]
|
|
2392
|
+
already_installed = len(matched_agents) - len(to_install)
|
|
2393
|
+
|
|
2394
|
+
self.console.print()
|
|
2395
|
+
if already_installed > 0:
|
|
2396
|
+
self.console.print(
|
|
2397
|
+
f"[green]✓ {already_installed} already installed[/green]"
|
|
2398
|
+
)
|
|
2399
|
+
if to_install:
|
|
2400
|
+
self.console.print(
|
|
2401
|
+
f"[yellow]⚠ {len(to_install)} need installation[/yellow]"
|
|
2402
|
+
)
|
|
2403
|
+
else:
|
|
2404
|
+
self.console.print(
|
|
2405
|
+
"[green]✓ All recommended agents are already installed![/green]"
|
|
2406
|
+
)
|
|
2407
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2408
|
+
return
|
|
2409
|
+
|
|
2410
|
+
# Ask for confirmation
|
|
2411
|
+
self.console.print()
|
|
2412
|
+
if not Confirm.ask(
|
|
2413
|
+
f"Install {len(to_install)} recommended agent(s)?", default=True
|
|
2414
|
+
):
|
|
2415
|
+
self.console.print("[yellow]Installation cancelled[/yellow]")
|
|
2416
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2417
|
+
return
|
|
2418
|
+
|
|
2419
|
+
# Install agents
|
|
2420
|
+
self.console.print("\n[bold]Installing recommended agents...[/bold]\n")
|
|
2421
|
+
|
|
2422
|
+
success_count = 0
|
|
2423
|
+
fail_count = 0
|
|
2424
|
+
|
|
2425
|
+
for agent in to_install:
|
|
2426
|
+
try:
|
|
2427
|
+
if self._deploy_single_agent(agent, show_feedback=False):
|
|
2428
|
+
success_count += 1
|
|
2429
|
+
self.console.print(f"[green]✓ Installed: {agent.name}[/green]")
|
|
2430
|
+
else:
|
|
2431
|
+
fail_count += 1
|
|
2432
|
+
self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
|
|
2433
|
+
except Exception as e:
|
|
2434
|
+
fail_count += 1
|
|
2435
|
+
self.console.print(f"[red]✗ Failed: {agent.name} - {e}[/red]")
|
|
2436
|
+
|
|
2437
|
+
# Show summary
|
|
2438
|
+
self.console.print()
|
|
2439
|
+
if success_count > 0:
|
|
2440
|
+
self.console.print(
|
|
2441
|
+
f"[green]✓ Successfully installed {success_count} agent(s)[/green]"
|
|
2442
|
+
)
|
|
2443
|
+
if fail_count > 0:
|
|
2444
|
+
self.console.print(f"[red]✗ Failed to install {fail_count} agent(s)[/red]")
|
|
2445
|
+
|
|
2446
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2447
|
+
|
|
1350
2448
|
def _deploy_single_agent(
|
|
1351
2449
|
self, agent: AgentConfig, show_feedback: bool = True
|
|
1352
2450
|
) -> bool:
|
|
@@ -1372,8 +2470,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
1372
2470
|
else:
|
|
1373
2471
|
target_name = full_agent_id + ".md"
|
|
1374
2472
|
|
|
1375
|
-
# Deploy to
|
|
1376
|
-
target_dir =
|
|
2473
|
+
# Deploy to project-level agents directory
|
|
2474
|
+
target_dir = self.project_dir / ".claude" / "agents"
|
|
1377
2475
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
1378
2476
|
target_file = target_dir / target_name
|
|
1379
2477
|
|
|
@@ -1421,7 +2519,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
1421
2519
|
|
|
1422
2520
|
self.console.print(f"\n[bold]Installed agents ({len(installed)}):[/bold]")
|
|
1423
2521
|
for idx, agent in enumerate(installed, 1):
|
|
1424
|
-
|
|
2522
|
+
raw_display_name = getattr(agent, "display_name", agent.name)
|
|
2523
|
+
display_name = self._format_display_name(raw_display_name)
|
|
1425
2524
|
self.console.print(f" {idx}. {agent.name} - {display_name}")
|
|
1426
2525
|
|
|
1427
2526
|
selection = Prompt.ask("\nEnter agent number to remove (or 'c' to cancel)")
|
|
@@ -1484,7 +2583,8 @@ class ConfigureCommand(BaseCommand):
|
|
|
1484
2583
|
|
|
1485
2584
|
self.console.print(f"\n[bold]Available agents ({len(agents)}):[/bold]")
|
|
1486
2585
|
for idx, agent in enumerate(agents, 1):
|
|
1487
|
-
|
|
2586
|
+
raw_display_name = getattr(agent, "display_name", agent.name)
|
|
2587
|
+
display_name = self._format_display_name(raw_display_name)
|
|
1488
2588
|
self.console.print(f" {idx}. {agent.name} - {display_name}")
|
|
1489
2589
|
|
|
1490
2590
|
selection = Prompt.ask("\nEnter agent number to view (or 'c' to cancel)")
|
|
@@ -1501,7 +2601,12 @@ class ConfigureCommand(BaseCommand):
|
|
|
1501
2601
|
|
|
1502
2602
|
# Basic info
|
|
1503
2603
|
self.console.print(f"[bold]ID:[/bold] {agent.name}")
|
|
1504
|
-
|
|
2604
|
+
raw_display_name = getattr(agent, "display_name", "N/A")
|
|
2605
|
+
display_name = (
|
|
2606
|
+
self._format_display_name(raw_display_name)
|
|
2607
|
+
if raw_display_name != "N/A"
|
|
2608
|
+
else "N/A"
|
|
2609
|
+
)
|
|
1505
2610
|
self.console.print(f"[bold]Name:[/bold] {display_name}")
|
|
1506
2611
|
self.console.print(f"[bold]Description:[/bold] {agent.description}")
|
|
1507
2612
|
|