claude-mpm 3.4.10__py3-none-any.whl → 5.4.85__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/BUILD_NUMBER +1 -0
- claude_mpm/VERSION +1 -0
- claude_mpm/__init__.py +50 -12
- claude_mpm/__main__.py +7 -2
- claude_mpm/agents/BASE_AGENT.md +164 -0
- claude_mpm/agents/BASE_ENGINEER.md +658 -0
- claude_mpm/agents/CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md +405 -0
- claude_mpm/agents/CLAUDE_MPM_OUTPUT_STYLE.md +112 -0
- claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +186 -0
- claude_mpm/agents/MEMORY.md +72 -0
- claude_mpm/agents/PM_INSTRUCTIONS.md +1429 -0
- claude_mpm/agents/WORKFLOW.md +111 -0
- claude_mpm/agents/__init__.py +92 -80
- claude_mpm/agents/agent-template.yaml +83 -0
- claude_mpm/agents/agent_loader.py +560 -745
- claude_mpm/agents/agent_loader_integration.py +53 -55
- claude_mpm/agents/agents_metadata.py +186 -27
- claude_mpm/agents/async_agent_loader.py +436 -0
- claude_mpm/agents/base_agent.json +8 -4
- claude_mpm/agents/frontmatter_validator.py +754 -0
- claude_mpm/agents/system_agent_config.py +222 -155
- claude_mpm/agents/templates/README.md +465 -0
- claude_mpm/agents/templates/__init__.py +17 -13
- claude_mpm/agents/templates/circuit-breakers.md +1391 -0
- claude_mpm/agents/templates/context-management-examples.md +544 -0
- claude_mpm/agents/templates/git-file-tracking.md +584 -0
- claude_mpm/agents/templates/pm-examples.md +474 -0
- claude_mpm/agents/templates/pm-red-flags.md +310 -0
- claude_mpm/agents/templates/pr-workflow-examples.md +427 -0
- claude_mpm/agents/templates/research-gate-examples.md +669 -0
- claude_mpm/agents/templates/response-format.md +583 -0
- claude_mpm/agents/templates/structured-questions-examples.md +615 -0
- claude_mpm/agents/templates/ticket-completeness-examples.md +139 -0
- claude_mpm/agents/templates/ticketing-examples.md +277 -0
- claude_mpm/agents/templates/validation-templates.md +312 -0
- claude_mpm/cli/__init__.py +94 -128
- claude_mpm/cli/__main__.py +33 -0
- claude_mpm/cli/chrome_devtools_installer.py +175 -0
- claude_mpm/cli/commands/__init__.py +36 -12
- claude_mpm/cli/commands/agent_manager.py +1403 -0
- claude_mpm/cli/commands/agent_source.py +774 -0
- claude_mpm/cli/commands/agent_state_manager.py +335 -0
- claude_mpm/cli/commands/agents.py +2501 -168
- claude_mpm/cli/commands/agents_cleanup.py +210 -0
- claude_mpm/cli/commands/agents_discover.py +338 -0
- claude_mpm/cli/commands/agents_reconcile.py +197 -0
- claude_mpm/cli/commands/aggregate.py +540 -0
- claude_mpm/cli/commands/analyze.py +553 -0
- claude_mpm/cli/commands/analyze_code.py +528 -0
- claude_mpm/cli/commands/auto_configure.py +1053 -0
- claude_mpm/cli/commands/cleanup.py +588 -0
- claude_mpm/cli/commands/cleanup_orphaned_agents.py +150 -0
- claude_mpm/cli/commands/config.py +586 -0
- claude_mpm/cli/commands/configure.py +3253 -0
- claude_mpm/cli/commands/configure_agent_display.py +282 -0
- claude_mpm/cli/commands/configure_behavior_manager.py +204 -0
- claude_mpm/cli/commands/configure_hook_manager.py +225 -0
- claude_mpm/cli/commands/configure_models.py +18 -0
- claude_mpm/cli/commands/configure_navigation.py +184 -0
- claude_mpm/cli/commands/configure_paths.py +104 -0
- claude_mpm/cli/commands/configure_persistence.py +254 -0
- claude_mpm/cli/commands/configure_startup_manager.py +646 -0
- claude_mpm/cli/commands/configure_template_editor.py +497 -0
- claude_mpm/cli/commands/configure_validators.py +73 -0
- claude_mpm/cli/commands/dashboard.py +286 -0
- claude_mpm/cli/commands/debug.py +1386 -0
- claude_mpm/cli/commands/doctor.py +243 -0
- claude_mpm/cli/commands/hook_errors.py +277 -0
- claude_mpm/cli/commands/info.py +195 -74
- claude_mpm/cli/commands/local_deploy.py +534 -0
- claude_mpm/cli/commands/mcp.py +205 -0
- claude_mpm/cli/commands/mcp_command_router.py +161 -0
- claude_mpm/cli/commands/mcp_config.py +154 -0
- claude_mpm/cli/commands/mcp_config_commands.py +20 -0
- claude_mpm/cli/commands/mcp_external_commands.py +249 -0
- claude_mpm/cli/commands/mcp_install_commands.py +346 -0
- claude_mpm/cli/commands/mcp_pipx_config.py +208 -0
- claude_mpm/cli/commands/mcp_server_commands.py +155 -0
- claude_mpm/cli/commands/mcp_setup_external.py +868 -0
- claude_mpm/cli/commands/mcp_tool_commands.py +34 -0
- claude_mpm/cli/commands/memory.py +585 -846
- claude_mpm/cli/commands/monitor.py +228 -310
- claude_mpm/cli/commands/mpm_init/__init__.py +73 -0
- claude_mpm/cli/commands/mpm_init/core.py +759 -0
- claude_mpm/cli/commands/mpm_init/display.py +341 -0
- claude_mpm/cli/commands/mpm_init/git_activity.py +427 -0
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/modes.py +397 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +722 -0
- claude_mpm/cli/commands/mpm_init_cli.py +396 -0
- claude_mpm/cli/commands/mpm_init_handler.py +195 -0
- claude_mpm/cli/commands/postmortem.py +401 -0
- claude_mpm/cli/commands/profile.py +276 -0
- claude_mpm/cli/commands/run.py +910 -488
- claude_mpm/cli/commands/search.py +458 -0
- claude_mpm/cli/commands/skill_source.py +694 -0
- claude_mpm/cli/commands/skills.py +1398 -0
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/commands/tickets.py +536 -53
- claude_mpm/cli/commands/uninstall.py +176 -0
- claude_mpm/cli/commands/upgrade.py +152 -0
- claude_mpm/cli/commands/verify.py +119 -0
- claude_mpm/cli/executor.py +298 -0
- claude_mpm/cli/helpers.py +105 -0
- claude_mpm/cli/interactive/__init__.py +31 -0
- claude_mpm/cli/interactive/agent_wizard.py +1927 -0
- claude_mpm/cli/interactive/questionary_styles.py +65 -0
- claude_mpm/cli/interactive/skill_selector.py +481 -0
- claude_mpm/cli/interactive/skills_wizard.py +491 -0
- claude_mpm/cli/parser.py +87 -563
- claude_mpm/cli/parsers/__init__.py +35 -0
- claude_mpm/cli/parsers/agent_manager_parser.py +393 -0
- claude_mpm/cli/parsers/agent_source_parser.py +171 -0
- claude_mpm/cli/parsers/agents_parser.py +575 -0
- claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
- claude_mpm/cli/parsers/analyze_parser.py +135 -0
- claude_mpm/cli/parsers/auto_configure_parser.py +120 -0
- claude_mpm/cli/parsers/base_parser.py +649 -0
- claude_mpm/cli/parsers/config_parser.py +208 -0
- claude_mpm/cli/parsers/configure_parser.py +138 -0
- claude_mpm/cli/parsers/dashboard_parser.py +113 -0
- claude_mpm/cli/parsers/debug_parser.py +319 -0
- claude_mpm/cli/parsers/local_deploy_parser.py +227 -0
- claude_mpm/cli/parsers/mcp_parser.py +195 -0
- claude_mpm/cli/parsers/memory_parser.py +138 -0
- claude_mpm/cli/parsers/monitor_parser.py +142 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +311 -0
- claude_mpm/cli/parsers/profile_parser.py +147 -0
- claude_mpm/cli/parsers/run_parser.py +157 -0
- claude_mpm/cli/parsers/search_parser.py +245 -0
- claude_mpm/cli/parsers/skill_source_parser.py +169 -0
- claude_mpm/cli/parsers/skills_parser.py +277 -0
- claude_mpm/cli/parsers/source_parser.py +138 -0
- claude_mpm/cli/parsers/tickets_parser.py +203 -0
- claude_mpm/cli/shared/__init__.py +40 -0
- claude_mpm/cli/shared/argument_patterns.py +205 -0
- claude_mpm/cli/shared/base_command.py +242 -0
- claude_mpm/cli/shared/error_handling.py +242 -0
- claude_mpm/cli/shared/output_formatters.py +241 -0
- claude_mpm/cli/startup.py +1578 -0
- claude_mpm/cli/startup_display.py +480 -0
- claude_mpm/cli/startup_logging.py +839 -0
- claude_mpm/cli/utils.py +136 -47
- claude_mpm/cli_module/__init__.py +6 -6
- claude_mpm/cli_module/args.py +188 -140
- claude_mpm/cli_module/commands.py +79 -70
- claude_mpm/cli_module/migration_example.py +42 -64
- claude_mpm/commands/__init__.py +14 -0
- claude_mpm/commands/mpm-config.md +28 -0
- claude_mpm/commands/mpm-doctor.md +20 -0
- claude_mpm/commands/mpm-help.md +20 -0
- claude_mpm/commands/mpm-init.md +120 -0
- claude_mpm/commands/mpm-monitor.md +31 -0
- claude_mpm/commands/mpm-organize.md +120 -0
- claude_mpm/commands/mpm-postmortem.md +21 -0
- claude_mpm/commands/mpm-session-resume.md +30 -0
- claude_mpm/commands/mpm-status.md +20 -0
- claude_mpm/commands/mpm-ticket-view.md +109 -0
- claude_mpm/commands/mpm-version.md +20 -0
- claude_mpm/commands/mpm.md +31 -0
- claude_mpm/config/__init__.py +42 -2
- claude_mpm/config/agent_config.py +402 -0
- claude_mpm/config/agent_presets.py +488 -0
- claude_mpm/config/agent_sources.py +352 -0
- claude_mpm/config/experimental_features.py +217 -0
- claude_mpm/config/model_config.py +428 -0
- claude_mpm/config/paths.py +258 -0
- claude_mpm/config/skill_presets.py +392 -0
- claude_mpm/config/skill_sources.py +590 -0
- claude_mpm/config/socketio_config.py +125 -83
- claude_mpm/constants.py +133 -22
- claude_mpm/core/__init__.py +62 -36
- claude_mpm/core/agent_name_normalizer.py +71 -73
- claude_mpm/core/agent_registry.py +385 -492
- claude_mpm/core/agent_session_manager.py +81 -70
- claude_mpm/core/api_validator.py +330 -0
- claude_mpm/core/base_service.py +159 -122
- claude_mpm/core/cache.py +560 -0
- claude_mpm/core/claude_runner.py +696 -916
- claude_mpm/core/config.py +613 -122
- claude_mpm/core/config_aliases.py +74 -73
- claude_mpm/core/config_constants.py +314 -0
- claude_mpm/core/constants.py +361 -0
- claude_mpm/core/container.py +646 -104
- claude_mpm/core/enums.py +452 -0
- claude_mpm/core/error_handler.py +623 -0
- claude_mpm/core/exceptions.py +536 -0
- claude_mpm/core/factories.py +105 -109
- claude_mpm/core/file_utils.py +764 -0
- claude_mpm/core/framework/__init__.py +25 -0
- claude_mpm/core/framework/formatters/__init__.py +11 -0
- claude_mpm/core/framework/formatters/capability_generator.py +367 -0
- claude_mpm/core/framework/formatters/content_formatter.py +278 -0
- claude_mpm/core/framework/formatters/context_generator.py +185 -0
- claude_mpm/core/framework/loaders/__init__.py +13 -0
- claude_mpm/core/framework/loaders/agent_loader.py +213 -0
- claude_mpm/core/framework/loaders/file_loader.py +176 -0
- claude_mpm/core/framework/loaders/instruction_loader.py +222 -0
- claude_mpm/core/framework/loaders/packaged_loader.py +232 -0
- claude_mpm/core/framework/processors/__init__.py +11 -0
- claude_mpm/core/framework/processors/memory_processor.py +230 -0
- claude_mpm/core/framework/processors/metadata_processor.py +146 -0
- claude_mpm/core/framework/processors/template_processor.py +244 -0
- claude_mpm/core/framework_loader.py +485 -414
- claude_mpm/core/hook_error_memory.py +381 -0
- claude_mpm/core/hook_manager.py +246 -86
- claude_mpm/core/hook_performance_config.py +147 -0
- claude_mpm/core/injectable_service.py +72 -63
- claude_mpm/core/instruction_reinforcement_hook.py +267 -0
- claude_mpm/core/interactive_session.py +670 -0
- claude_mpm/core/interfaces.py +570 -164
- claude_mpm/core/lazy.py +467 -0
- claude_mpm/core/log_manager.py +707 -0
- claude_mpm/core/logger.py +295 -134
- claude_mpm/core/logging_config.py +474 -0
- claude_mpm/core/logging_utils.py +520 -0
- claude_mpm/core/minimal_framework_loader.py +24 -22
- claude_mpm/core/mixins.py +30 -29
- claude_mpm/core/oneshot_session.py +594 -0
- claude_mpm/core/optimized_agent_loader.py +479 -0
- claude_mpm/core/optimized_startup.py +554 -0
- claude_mpm/core/output_style_manager.py +491 -0
- claude_mpm/core/pm_hook_interceptor.py +197 -82
- claude_mpm/core/protocols/__init__.py +23 -0
- claude_mpm/core/protocols/runner_protocol.py +103 -0
- claude_mpm/core/protocols/session_protocol.py +131 -0
- claude_mpm/core/service_registry.py +153 -116
- claude_mpm/core/session_manager.py +179 -64
- claude_mpm/core/shared/__init__.py +17 -0
- claude_mpm/core/shared/config_loader.py +326 -0
- claude_mpm/core/shared/path_resolver.py +281 -0
- claude_mpm/core/shared/singleton_manager.py +221 -0
- claude_mpm/core/socketio_pool.py +400 -137
- claude_mpm/core/system_context.py +38 -0
- claude_mpm/core/tool_access_control.py +64 -57
- claude_mpm/core/types.py +307 -0
- claude_mpm/core/typing_utils.py +553 -0
- claude_mpm/core/unified_agent_registry.py +969 -0
- claude_mpm/core/unified_config.py +612 -0
- claude_mpm/core/unified_paths.py +958 -0
- claude_mpm/dashboard/__init__.py +12 -0
- claude_mpm/dashboard/api/simple_directory.py +261 -0
- claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/4TdZjIqw.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B0uc0UOD.js +36 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7RN905-.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7xVLGWV.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BIF9m_hv.js +61 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BPYeabCQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BSNlmTZj.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Be7GpZd6.js +7 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bh0LDWpI.js +145 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BofRWZRR.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BovzEFCE.js +30 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C30mlcqg.js +165 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4B-KCzX.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4JcI4KD.js +122 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CBBdVcY8.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CDuw-vjf.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C_Usid8X.js +15 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cfqx1Qun.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CiIAseT4.js +128 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CnA0NrzZ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cs_tUR18.js +24 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CyWMqx4W.js +43 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzZX-COe.js +220 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzeYkLYB.js +65 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D3k0OPJN.js +4 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9lljYKQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DGkLK5U1.js +267 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DI7hHRFL.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DLVjFsZ3.js +139 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUrLdbGD.js +89 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DY1XQ8fi.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DZX00Y4g.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DaimHw_p.js +68 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +323 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dhb8PKl3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dle-35c7.js +64 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DmxopI1J.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DwBR2MJi.js +60 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/GYwsonyD.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/NqQ1dWOy.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/RJiighC3.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Vzk33B_K.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ZGh7QtNv.js +7 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bT1r9zLR.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bTOqqlTd.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/eNVUfhuA.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/iEWssX7S.js +162 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/sQeU3Y1z.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uuIeMWc-.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.D6-I5TpK.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.m1gL8KXf.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.CgNOuw-d.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +1 -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/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
- claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
- claude_mpm/experimental/__init__.py +10 -0
- claude_mpm/experimental/cli_enhancements.py +104 -89
- claude_mpm/generators/__init__.py +1 -1
- claude_mpm/generators/agent_profile_generator.py +76 -66
- claude_mpm/hooks/__init__.py +37 -1
- claude_mpm/hooks/base_hook.py +37 -32
- claude_mpm/hooks/claude_hooks/__init__.py +1 -1
- claude_mpm/hooks/claude_hooks/connection_pool.py +250 -0
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +888 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +652 -875
- claude_mpm/hooks/claude_hooks/hook_wrapper.sh +10 -7
- claude_mpm/hooks/claude_hooks/installer.py +806 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +249 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +412 -0
- claude_mpm/hooks/claude_hooks/services/__init__.py +15 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +229 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +254 -0
- claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +284 -0
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
- claude_mpm/hooks/claude_hooks/tool_analysis.py +224 -0
- claude_mpm/hooks/failure_learning/__init__.py +54 -0
- claude_mpm/hooks/failure_learning/failure_detection_hook.py +230 -0
- claude_mpm/hooks/failure_learning/fix_detection_hook.py +212 -0
- claude_mpm/hooks/failure_learning/learning_extraction_hook.py +281 -0
- claude_mpm/hooks/instruction_reinforcement.py +301 -0
- claude_mpm/hooks/kuzu_enrichment_hook.py +263 -0
- claude_mpm/hooks/kuzu_memory_hook.py +386 -0
- claude_mpm/hooks/kuzu_response_hook.py +179 -0
- claude_mpm/hooks/memory_integration_hook.py +201 -107
- claude_mpm/hooks/session_resume_hook.py +121 -0
- claude_mpm/hooks/templates/pre_tool_use_simple.py +78 -0
- claude_mpm/hooks/templates/pre_tool_use_template.py +323 -0
- claude_mpm/hooks/tool_call_interceptor.py +92 -76
- claude_mpm/hooks/validation_hooks.py +62 -54
- claude_mpm/init.py +518 -83
- claude_mpm/models/__init__.py +9 -9
- claude_mpm/models/agent_definition.py +40 -23
- claude_mpm/models/agent_session.py +538 -0
- claude_mpm/models/git_repository.py +198 -0
- claude_mpm/models/resume_log.py +340 -0
- claude_mpm/schemas/__init__.py +12 -0
- claude_mpm/scripts/__init__.py +15 -0
- claude_mpm/scripts/claude-hook-handler.sh +227 -0
- claude_mpm/scripts/launch_monitor.py +165 -0
- claude_mpm/scripts/mpm_doctor.py +322 -0
- claude_mpm/scripts/socketio_daemon.py +189 -200
- claude_mpm/scripts/start_activity_logging.py +91 -0
- claude_mpm/services/__init__.py +208 -39
- claude_mpm/services/agent_capabilities_service.py +266 -0
- claude_mpm/services/agents/__init__.py +89 -0
- claude_mpm/services/agents/agent_builder.py +514 -0
- claude_mpm/services/agents/agent_preset_service.py +238 -0
- claude_mpm/services/agents/agent_recommendation_service.py +278 -0
- claude_mpm/services/agents/agent_review_service.py +280 -0
- claude_mpm/services/agents/agent_selection_service.py +484 -0
- claude_mpm/services/agents/auto_config_manager.py +796 -0
- claude_mpm/services/agents/auto_deploy_index_parser.py +569 -0
- claude_mpm/services/agents/cache_git_manager.py +621 -0
- claude_mpm/services/agents/deployment/__init__.py +21 -0
- claude_mpm/services/agents/deployment/agent_config_provider.py +410 -0
- claude_mpm/services/agents/deployment/agent_configuration_manager.py +358 -0
- claude_mpm/services/agents/deployment/agent_definition_factory.py +80 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +1037 -0
- claude_mpm/services/agents/deployment/agent_discovery_service.py +546 -0
- claude_mpm/services/agents/deployment/agent_environment_manager.py +288 -0
- claude_mpm/services/agents/deployment/agent_filesystem_manager.py +383 -0
- claude_mpm/services/agents/deployment/agent_format_converter.py +505 -0
- claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +160 -0
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +957 -0
- claude_mpm/services/agents/deployment/agent_metrics_collector.py +273 -0
- claude_mpm/services/agents/deployment/agent_operation_service.py +573 -0
- claude_mpm/services/agents/deployment/agent_record_service.py +418 -0
- claude_mpm/services/agents/deployment/agent_restore_handler.py +84 -0
- claude_mpm/services/agents/deployment/agent_state_service.py +381 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +1377 -0
- claude_mpm/services/agents/deployment/agent_validator.py +376 -0
- claude_mpm/services/agents/deployment/agent_version_manager.py +322 -0
- claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +10 -13
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +149 -0
- claude_mpm/services/agents/deployment/async_agent_deployment.py +768 -0
- claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
- claude_mpm/services/agents/deployment/config/__init__.py +13 -0
- claude_mpm/services/agents/deployment/config/deployment_config.py +181 -0
- claude_mpm/services/agents/deployment/config/deployment_config_manager.py +200 -0
- claude_mpm/services/agents/deployment/deployment_config_loader.py +178 -0
- claude_mpm/services/agents/deployment/deployment_reconciler.py +577 -0
- claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
- claude_mpm/services/agents/deployment/deployment_type_detector.py +120 -0
- claude_mpm/services/agents/deployment/deployment_wrapper.py +129 -0
- claude_mpm/services/agents/deployment/facade/__init__.py +18 -0
- claude_mpm/services/agents/deployment/facade/async_deployment_executor.py +159 -0
- claude_mpm/services/agents/deployment/facade/deployment_executor.py +70 -0
- claude_mpm/services/agents/deployment/facade/deployment_facade.py +269 -0
- claude_mpm/services/agents/deployment/facade/sync_deployment_executor.py +178 -0
- claude_mpm/services/agents/deployment/interface_adapter.py +226 -0
- claude_mpm/services/agents/deployment/lifecycle_health_checker.py +85 -0
- claude_mpm/services/agents/deployment/lifecycle_performance_tracker.py +100 -0
- claude_mpm/services/agents/deployment/local_template_deployment.py +362 -0
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +1478 -0
- claude_mpm/services/agents/deployment/pipeline/__init__.py +32 -0
- claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +158 -0
- claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +162 -0
- claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +169 -0
- claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +19 -0
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +240 -0
- claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +110 -0
- claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +80 -0
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +92 -0
- claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +101 -0
- claude_mpm/services/agents/deployment/processors/__init__.py +15 -0
- claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +102 -0
- claude_mpm/services/agents/deployment/processors/agent_deployment_result.py +235 -0
- claude_mpm/services/agents/deployment/processors/agent_processor.py +269 -0
- claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +311 -0
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +862 -0
- claude_mpm/services/agents/deployment/results/__init__.py +13 -0
- claude_mpm/services/agents/deployment/results/deployment_metrics.py +200 -0
- claude_mpm/services/agents/deployment/results/deployment_result_builder.py +249 -0
- claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
- claude_mpm/services/agents/deployment/startup_reconciliation.py +138 -0
- claude_mpm/services/agents/deployment/strategies/__init__.py +25 -0
- claude_mpm/services/agents/deployment/strategies/base_strategy.py +113 -0
- claude_mpm/services/agents/deployment/strategies/project_strategy.py +148 -0
- claude_mpm/services/agents/deployment/strategies/strategy_selector.py +117 -0
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +131 -0
- claude_mpm/services/agents/deployment/strategies/user_strategy.py +130 -0
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +228 -0
- claude_mpm/services/agents/deployment/validation/__init__.py +21 -0
- claude_mpm/services/agents/deployment/validation/agent_validator.py +323 -0
- claude_mpm/services/agents/deployment/validation/deployment_validator.py +238 -0
- claude_mpm/services/agents/deployment/validation/template_validator.py +319 -0
- claude_mpm/services/agents/deployment/validation/validation_result.py +214 -0
- claude_mpm/services/agents/git_source_manager.py +682 -0
- claude_mpm/services/agents/loading/__init__.py +11 -0
- claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +306 -228
- claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +106 -91
- claude_mpm/services/agents/loading/framework_agent_loader.py +433 -0
- claude_mpm/services/agents/local_template_manager.py +784 -0
- claude_mpm/services/agents/management/__init__.py +9 -0
- claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +92 -69
- claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +219 -168
- claude_mpm/services/agents/memory/__init__.py +22 -0
- claude_mpm/services/agents/memory/agent_memory_manager.py +784 -0
- claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +20 -18
- claude_mpm/services/agents/memory/content_manager.py +470 -0
- claude_mpm/services/agents/memory/memory_categorization_service.py +167 -0
- claude_mpm/services/agents/memory/memory_file_service.py +129 -0
- claude_mpm/services/agents/memory/memory_format_service.py +201 -0
- claude_mpm/services/agents/memory/memory_limits_service.py +101 -0
- claude_mpm/services/agents/memory/template_generator.py +83 -0
- claude_mpm/services/agents/observers.py +547 -0
- claude_mpm/services/agents/recommender.py +617 -0
- claude_mpm/services/agents/registry/__init__.py +30 -0
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +273 -0
- claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +370 -295
- claude_mpm/services/agents/single_tier_deployment_service.py +696 -0
- claude_mpm/services/agents/sources/__init__.py +13 -0
- claude_mpm/services/agents/sources/agent_sync_state.py +516 -0
- claude_mpm/services/agents/sources/git_source_sync_service.py +1205 -0
- claude_mpm/services/agents/startup_sync.py +262 -0
- claude_mpm/services/agents/toolchain_detector.py +478 -0
- claude_mpm/services/analysis/__init__.py +35 -0
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/analysis/postmortem_reporter.py +474 -0
- claude_mpm/services/analysis/postmortem_service.py +765 -0
- claude_mpm/services/async_session_logger.py +665 -0
- claude_mpm/services/claude_session_logger.py +321 -0
- claude_mpm/services/cli/__init__.py +18 -0
- claude_mpm/services/cli/agent_cleanup_service.py +408 -0
- claude_mpm/services/cli/agent_dependency_service.py +395 -0
- claude_mpm/services/cli/agent_listing_service.py +463 -0
- claude_mpm/services/cli/agent_output_formatter.py +605 -0
- claude_mpm/services/cli/agent_validation_service.py +590 -0
- claude_mpm/services/cli/memory_crud_service.py +622 -0
- claude_mpm/services/cli/memory_output_formatter.py +604 -0
- claude_mpm/services/cli/resume_service.py +617 -0
- claude_mpm/services/cli/session_manager.py +604 -0
- claude_mpm/services/cli/session_pause_manager.py +504 -0
- claude_mpm/services/cli/session_resume_helper.py +372 -0
- claude_mpm/services/cli/startup_checker.py +362 -0
- claude_mpm/services/cli/unified_dashboard_manager.py +439 -0
- claude_mpm/services/command_deployment_service.py +446 -0
- claude_mpm/services/command_handler_service.py +221 -0
- claude_mpm/services/communication/__init__.py +22 -0
- claude_mpm/services/core/__init__.py +108 -0
- claude_mpm/services/core/base.py +269 -0
- claude_mpm/services/core/cache_manager.py +309 -0
- claude_mpm/services/core/interfaces/__init__.py +273 -0
- claude_mpm/services/core/interfaces/agent.py +514 -0
- claude_mpm/services/core/interfaces/communication.py +316 -0
- claude_mpm/services/core/interfaces/health.py +169 -0
- claude_mpm/services/core/interfaces/infrastructure.py +357 -0
- claude_mpm/services/core/interfaces/model.py +281 -0
- claude_mpm/services/core/interfaces/process.py +372 -0
- claude_mpm/services/core/interfaces/project.py +121 -0
- claude_mpm/services/core/interfaces/restart.py +307 -0
- claude_mpm/services/core/interfaces/service.py +405 -0
- claude_mpm/services/core/interfaces/stability.py +260 -0
- claude_mpm/services/core/interfaces.py +81 -0
- claude_mpm/services/core/memory_manager.py +682 -0
- claude_mpm/services/core/models/__init__.py +70 -0
- claude_mpm/services/core/models/agent_config.py +384 -0
- claude_mpm/services/core/models/health.py +162 -0
- claude_mpm/services/core/models/process.py +239 -0
- claude_mpm/services/core/models/restart.py +302 -0
- claude_mpm/services/core/models/stability.py +264 -0
- claude_mpm/services/core/models/toolchain.py +306 -0
- claude_mpm/services/core/path_resolver.py +517 -0
- claude_mpm/services/core/service_container.py +520 -0
- claude_mpm/services/core/service_interfaces.py +436 -0
- claude_mpm/services/diagnostics/__init__.py +18 -0
- claude_mpm/services/diagnostics/checks/__init__.py +38 -0
- claude_mpm/services/diagnostics/checks/agent_check.py +370 -0
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +577 -0
- claude_mpm/services/diagnostics/checks/base_check.py +60 -0
- claude_mpm/services/diagnostics/checks/claude_code_check.py +270 -0
- claude_mpm/services/diagnostics/checks/common_issues_check.py +363 -0
- claude_mpm/services/diagnostics/checks/configuration_check.py +306 -0
- claude_mpm/services/diagnostics/checks/filesystem_check.py +233 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +520 -0
- claude_mpm/services/diagnostics/checks/instructions_check.py +415 -0
- claude_mpm/services/diagnostics/checks/mcp_check.py +330 -0
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +1058 -0
- claude_mpm/services/diagnostics/checks/monitor_check.py +281 -0
- claude_mpm/services/diagnostics/checks/skill_sources_check.py +587 -0
- claude_mpm/services/diagnostics/checks/startup_log_check.py +319 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +286 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +578 -0
- claude_mpm/services/diagnostics/models.py +138 -0
- claude_mpm/services/event_aggregator.py +582 -0
- claude_mpm/services/event_bus/__init__.py +18 -0
- claude_mpm/services/event_bus/config.py +186 -0
- claude_mpm/services/event_bus/direct_relay.py +312 -0
- claude_mpm/services/event_bus/event_bus.py +396 -0
- claude_mpm/services/event_bus/relay.py +326 -0
- claude_mpm/services/events/__init__.py +44 -0
- claude_mpm/services/events/consumers/__init__.py +18 -0
- claude_mpm/services/events/consumers/dead_letter.py +306 -0
- claude_mpm/services/events/consumers/logging.py +184 -0
- claude_mpm/services/events/consumers/metrics.py +241 -0
- claude_mpm/services/events/consumers/socketio.py +377 -0
- claude_mpm/services/events/core.py +480 -0
- claude_mpm/services/events/interfaces.py +214 -0
- claude_mpm/services/events/producers/__init__.py +14 -0
- claude_mpm/services/events/producers/hook.py +269 -0
- claude_mpm/services/events/producers/system.py +329 -0
- claude_mpm/services/exceptions.py +433 -353
- claude_mpm/services/framework_claude_md_generator/__init__.py +81 -80
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +74 -67
- claude_mpm/services/framework_claude_md_generator/content_validator.py +66 -62
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +82 -60
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +36 -37
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +41 -40
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +15 -15
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +5 -4
- claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +4 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +4 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +6 -5
- claude_mpm/services/framework_claude_md_generator/section_generators/header.py +8 -7
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +5 -4
- claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +6 -5
- claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +9 -8
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +26 -30
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +6 -5
- claude_mpm/services/framework_claude_md_generator/section_manager.py +28 -27
- claude_mpm/services/framework_claude_md_generator/version_manager.py +31 -30
- claude_mpm/services/git/__init__.py +21 -0
- claude_mpm/services/git/git_operations_service.py +579 -0
- claude_mpm/services/github/__init__.py +21 -0
- claude_mpm/services/github/github_cli_service.py +397 -0
- claude_mpm/services/hook_installer_service.py +506 -0
- claude_mpm/services/hook_service.py +159 -111
- claude_mpm/services/infrastructure/__init__.py +52 -0
- claude_mpm/services/infrastructure/context_preservation.py +569 -0
- claude_mpm/services/infrastructure/daemon_manager.py +279 -0
- claude_mpm/services/infrastructure/logging.py +209 -0
- claude_mpm/services/infrastructure/monitoring/__init__.py +39 -0
- claude_mpm/services/infrastructure/monitoring/aggregator.py +432 -0
- claude_mpm/services/infrastructure/monitoring/base.py +122 -0
- claude_mpm/services/infrastructure/monitoring/legacy.py +203 -0
- claude_mpm/services/infrastructure/monitoring/network.py +219 -0
- claude_mpm/services/infrastructure/monitoring/process.py +343 -0
- claude_mpm/services/infrastructure/monitoring/resources.py +244 -0
- claude_mpm/services/infrastructure/monitoring/service.py +368 -0
- claude_mpm/services/infrastructure/monitoring.py +71 -0
- claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
- claude_mpm/services/instructions/__init__.py +9 -0
- claude_mpm/services/instructions/instruction_cache_service.py +374 -0
- claude_mpm/services/local_ops/__init__.py +155 -0
- claude_mpm/services/local_ops/crash_detector.py +257 -0
- claude_mpm/services/local_ops/health_checks/__init__.py +26 -0
- claude_mpm/services/local_ops/health_checks/http_check.py +224 -0
- claude_mpm/services/local_ops/health_checks/process_check.py +236 -0
- claude_mpm/services/local_ops/health_checks/resource_check.py +255 -0
- claude_mpm/services/local_ops/health_manager.py +427 -0
- claude_mpm/services/local_ops/log_monitor.py +396 -0
- claude_mpm/services/local_ops/memory_leak_detector.py +294 -0
- claude_mpm/services/local_ops/process_manager.py +595 -0
- claude_mpm/services/local_ops/resource_monitor.py +331 -0
- claude_mpm/services/local_ops/restart_manager.py +401 -0
- claude_mpm/services/local_ops/restart_policy.py +387 -0
- claude_mpm/services/local_ops/state_manager.py +372 -0
- claude_mpm/services/local_ops/unified_manager.py +600 -0
- claude_mpm/services/mcp_config_manager.py +1542 -0
- claude_mpm/services/mcp_service_verifier.py +732 -0
- claude_mpm/services/memory/__init__.py +19 -0
- claude_mpm/services/{memory_builder.py → memory/builder.py} +465 -373
- claude_mpm/services/memory/cache/__init__.py +14 -0
- claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +237 -200
- claude_mpm/services/memory/cache/simple_cache.py +331 -0
- claude_mpm/services/memory/failure_tracker.py +578 -0
- claude_mpm/services/memory/indexed_memory.py +648 -0
- claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +272 -243
- claude_mpm/services/memory/router.py +951 -0
- claude_mpm/services/memory_hook_service.py +470 -0
- claude_mpm/services/model/__init__.py +147 -0
- claude_mpm/services/model/base_provider.py +365 -0
- claude_mpm/services/model/claude_provider.py +412 -0
- claude_mpm/services/model/model_router.py +452 -0
- claude_mpm/services/model/ollama_provider.py +415 -0
- claude_mpm/services/monitor/__init__.py +20 -0
- claude_mpm/services/monitor/daemon.py +698 -0
- claude_mpm/services/monitor/daemon_manager.py +1076 -0
- claude_mpm/services/monitor/event_emitter.py +350 -0
- claude_mpm/services/monitor/handlers/__init__.py +21 -0
- claude_mpm/services/monitor/handlers/code_analysis.py +332 -0
- claude_mpm/services/monitor/handlers/dashboard.py +299 -0
- claude_mpm/services/monitor/handlers/file.py +264 -0
- claude_mpm/services/monitor/handlers/hooks.py +512 -0
- claude_mpm/services/monitor/management/__init__.py +18 -0
- claude_mpm/services/monitor/management/health.py +124 -0
- claude_mpm/services/monitor/management/lifecycle.py +730 -0
- claude_mpm/services/monitor/server.py +1493 -0
- claude_mpm/services/monitor_build_service.py +349 -0
- claude_mpm/services/native_agent_converter.py +356 -0
- claude_mpm/services/orphan_detection.py +786 -0
- claude_mpm/services/pm_skills_deployer.py +711 -0
- claude_mpm/services/port_manager.py +597 -0
- claude_mpm/services/pr/__init__.py +14 -0
- claude_mpm/services/pr/pr_template_service.py +329 -0
- claude_mpm/services/profile_manager.py +337 -0
- claude_mpm/services/project/__init__.py +44 -0
- claude_mpm/services/{project_analyzer.py → project/analyzer.py} +541 -291
- claude_mpm/services/project/analyzer_v2.py +566 -0
- claude_mpm/services/project/architecture_analyzer.py +461 -0
- claude_mpm/services/project/archive_manager.py +1045 -0
- claude_mpm/services/project/dependency_analyzer.py +462 -0
- claude_mpm/services/project/detection_strategies.py +719 -0
- claude_mpm/services/project/documentation_manager.py +554 -0
- claude_mpm/services/project/enhanced_analyzer.py +572 -0
- claude_mpm/services/project/language_analyzer.py +265 -0
- claude_mpm/services/project/metrics_collector.py +407 -0
- claude_mpm/services/project/project_organizer.py +1009 -0
- claude_mpm/services/project/registry.py +636 -0
- claude_mpm/services/project/toolchain_analyzer.py +583 -0
- claude_mpm/services/project_port_allocator.py +596 -0
- claude_mpm/services/recovery_manager.py +293 -240
- claude_mpm/services/response_tracker.py +267 -0
- claude_mpm/services/runner_configuration_service.py +605 -0
- claude_mpm/services/self_upgrade_service.py +608 -0
- claude_mpm/services/session_management_service.py +314 -0
- claude_mpm/services/session_manager.py +380 -0
- claude_mpm/services/shared/__init__.py +21 -0
- claude_mpm/services/shared/async_service_base.py +216 -0
- claude_mpm/services/shared/config_service_base.py +301 -0
- claude_mpm/services/shared/lifecycle_service_base.py +308 -0
- claude_mpm/services/shared/manager_base.py +315 -0
- claude_mpm/services/shared/service_factory.py +309 -0
- claude_mpm/services/skills/__init__.py +21 -0
- claude_mpm/services/skills/git_skill_source_manager.py +1340 -0
- claude_mpm/services/skills/selective_skill_deployer.py +743 -0
- claude_mpm/services/skills/skill_discovery_service.py +568 -0
- claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
- claude_mpm/services/skills_config.py +547 -0
- claude_mpm/services/skills_deployer.py +1168 -0
- claude_mpm/services/socketio/__init__.py +25 -0
- claude_mpm/services/socketio/client_proxy.py +229 -0
- claude_mpm/services/socketio/dashboard_server.py +362 -0
- claude_mpm/services/socketio/event_normalizer.py +798 -0
- claude_mpm/services/socketio/handlers/__init__.py +30 -0
- claude_mpm/services/socketio/handlers/base.py +136 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +682 -0
- claude_mpm/services/socketio/handlers/connection.py +643 -0
- claude_mpm/services/socketio/handlers/connection_handler.py +333 -0
- claude_mpm/services/socketio/handlers/file.py +263 -0
- claude_mpm/services/socketio/handlers/git.py +962 -0
- claude_mpm/services/socketio/handlers/hook.py +211 -0
- claude_mpm/services/socketio/handlers/memory.py +26 -0
- claude_mpm/services/socketio/handlers/project.py +24 -0
- claude_mpm/services/socketio/handlers/registry.py +214 -0
- claude_mpm/services/socketio/migration_utils.py +343 -0
- claude_mpm/services/socketio/monitor_client.py +364 -0
- claude_mpm/services/socketio/server/__init__.py +18 -0
- claude_mpm/services/socketio/server/broadcaster.py +569 -0
- claude_mpm/services/socketio/server/connection_manager.py +579 -0
- claude_mpm/services/socketio/server/core.py +1079 -0
- claude_mpm/services/socketio/server/eventbus_integration.py +245 -0
- claude_mpm/services/socketio/server/main.py +501 -0
- claude_mpm/services/socketio_client_manager.py +173 -143
- claude_mpm/services/socketio_server.py +38 -1657
- claude_mpm/services/subprocess_launcher_service.py +322 -0
- claude_mpm/services/system_instructions_service.py +270 -0
- claude_mpm/services/ticket_manager.py +25 -209
- claude_mpm/services/ticket_services/__init__.py +26 -0
- claude_mpm/services/ticket_services/crud_service.py +328 -0
- claude_mpm/services/ticket_services/formatter_service.py +290 -0
- claude_mpm/services/ticket_services/search_service.py +324 -0
- claude_mpm/services/ticket_services/validation_service.py +303 -0
- claude_mpm/services/ticket_services/workflow_service.py +244 -0
- claude_mpm/services/unified/__init__.py +65 -0
- claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +518 -0
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +680 -0
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +900 -0
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +745 -0
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +733 -0
- claude_mpm/services/unified/config_strategies/__init__.py +175 -0
- claude_mpm/services/unified/config_strategies/config_schema.py +731 -0
- claude_mpm/services/unified/config_strategies/context_strategy.py +747 -0
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +1005 -0
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +881 -0
- claude_mpm/services/unified/config_strategies/unified_config_service.py +823 -0
- claude_mpm/services/unified/config_strategies/validation_strategy.py +1148 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
- claude_mpm/services/unified/deployment_strategies/base.py +553 -0
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +573 -0
- claude_mpm/services/unified/deployment_strategies/local.py +607 -0
- claude_mpm/services/unified/deployment_strategies/utils.py +667 -0
- claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
- claude_mpm/services/unified/interfaces.py +475 -0
- claude_mpm/services/unified/migration.py +509 -0
- claude_mpm/services/unified/strategies.py +534 -0
- claude_mpm/services/unified/unified_analyzer.py +542 -0
- claude_mpm/services/unified/unified_config.py +691 -0
- claude_mpm/services/unified/unified_deployment.py +466 -0
- claude_mpm/services/utility_service.py +280 -0
- claude_mpm/services/version_control/__init__.py +34 -37
- claude_mpm/services/version_control/branch_strategy.py +26 -17
- claude_mpm/services/version_control/conflict_resolution.py +52 -36
- claude_mpm/services/version_control/git_operations.py +183 -49
- claude_mpm/services/version_control/semantic_versioning.py +172 -61
- claude_mpm/services/version_control/version_parser.py +546 -0
- claude_mpm/services/version_service.py +379 -0
- claude_mpm/services/visualization/__init__.py +15 -0
- claude_mpm/services/visualization/mermaid_generator.py +937 -0
- claude_mpm/skills/__init__.py +42 -0
- claude_mpm/skills/agent_skills_injector.py +324 -0
- claude_mpm/skills/bundled/LICENSE_ATTRIBUTIONS.md +79 -0
- claude_mpm/skills/bundled/__init__.py +6 -0
- claude_mpm/skills/bundled/api-documentation.md +393 -0
- claude_mpm/skills/bundled/async-testing.md +571 -0
- claude_mpm/skills/bundled/code-review.md +143 -0
- claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +79 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +178 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/agent-prompts.md +577 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/coordination-patterns.md +467 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/examples.md +537 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/troubleshooting.md +730 -0
- claude_mpm/skills/bundled/collaboration/git-worktrees.md +317 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +112 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/references/code-reviewer-template.md +146 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/references/review-examples.md +412 -0
- claude_mpm/skills/bundled/collaboration/stacked-prs.md +251 -0
- claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +81 -0
- claude_mpm/skills/bundled/collaboration/writing-plans/references/best-practices.md +362 -0
- claude_mpm/skills/bundled/collaboration/writing-plans/references/plan-structure-templates.md +312 -0
- claude_mpm/skills/bundled/database-migration.md +199 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +152 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/advanced-techniques.md +668 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/examples.md +587 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/integration.md +438 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/references/tracing-techniques.md +391 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +119 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +148 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +483 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +452 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +449 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +411 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +14 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +58 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +68 -0
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +69 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +131 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +325 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/integration-and-workflows.md +490 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/red-flags-and-failures.md +425 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +499 -0
- claude_mpm/skills/bundled/docker-containerization.md +194 -0
- claude_mpm/skills/bundled/express-local-dev.md +1429 -0
- claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
- claude_mpm/skills/bundled/git-workflow.md +414 -0
- claude_mpm/skills/bundled/imagemagick.md +204 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/INTEGRATION.md +611 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/README.md +596 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/SKILL.md +260 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/examples/nextjs-env-structure.md +315 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/references/frameworks.md +436 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/references/security.md +433 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/references/synchronization.md +452 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/references/troubleshooting.md +404 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/references/validation.md +420 -0
- claude_mpm/skills/bundled/infrastructure/env-manager/scripts/validate_env.py +576 -0
- claude_mpm/skills/bundled/json-data-handling.md +223 -0
- claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +86 -0
- claude_mpm/skills/bundled/main/internal-comms/SKILL.md +43 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +47 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +65 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +30 -0
- claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +16 -0
- claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +160 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/design_principles.md +412 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +602 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +915 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +916 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +752 -0
- claude_mpm/skills/bundled/main/mcp-builder/reference/workflow.md +1237 -0
- claude_mpm/skills/bundled/main/mcp-builder/scripts/connections.py +157 -0
- claude_mpm/skills/bundled/main/mcp-builder/scripts/evaluation.py +425 -0
- claude_mpm/skills/bundled/main/skill-creator/SKILL.md +189 -0
- claude_mpm/skills/bundled/main/skill-creator/references/best-practices.md +500 -0
- claude_mpm/skills/bundled/main/skill-creator/references/creation-workflow.md +464 -0
- claude_mpm/skills/bundled/main/skill-creator/references/examples.md +619 -0
- claude_mpm/skills/bundled/main/skill-creator/references/progressive-disclosure.md +437 -0
- claude_mpm/skills/bundled/main/skill-creator/references/skill-structure.md +231 -0
- claude_mpm/skills/bundled/main/skill-creator/scripts/init_skill.py +303 -0
- claude_mpm/skills/bundled/main/skill-creator/scripts/package_skill.py +113 -0
- claude_mpm/skills/bundled/main/skill-creator/scripts/quick_validate.py +72 -0
- claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
- claude_mpm/skills/bundled/pdf.md +141 -0
- claude_mpm/skills/bundled/performance-profiling.md +573 -0
- claude_mpm/skills/bundled/php/espocrm-development/SKILL.md +170 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/architecture.md +602 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/common-tasks.md +821 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/development-workflow.md +742 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/frontend-customization.md +726 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/hooks-and-services.md +764 -0
- claude_mpm/skills/bundled/php/espocrm-development/references/testing-debugging.md +831 -0
- claude_mpm/skills/bundled/pm/pm-bug-reporting/pm-bug-reporting.md +248 -0
- claude_mpm/skills/bundled/pm/pm-delegation-patterns/SKILL.md +167 -0
- claude_mpm/skills/bundled/pm/pm-git-file-tracking/SKILL.md +113 -0
- claude_mpm/skills/bundled/pm/pm-pr-workflow/SKILL.md +124 -0
- claude_mpm/skills/bundled/pm/pm-teaching-mode/SKILL.md +657 -0
- claude_mpm/skills/bundled/pm/pm-ticketing-integration/SKILL.md +154 -0
- claude_mpm/skills/bundled/pm/pm-verification-protocols/SKILL.md +198 -0
- claude_mpm/skills/bundled/react/flexlayout-react.md +742 -0
- claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
- claude_mpm/skills/bundled/rust/desktop-applications/SKILL.md +226 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/architecture-patterns.md +901 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/native-gui-frameworks.md +901 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/platform-integration.md +775 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/state-management.md +937 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/tauri-framework.md +770 -0
- claude_mpm/skills/bundled/rust/desktop-applications/references/testing-deployment.md +961 -0
- claude_mpm/skills/bundled/security-scanning.md +439 -0
- claude_mpm/skills/bundled/systematic-debugging.md +473 -0
- claude_mpm/skills/bundled/tauri/tauri-async-patterns.md +495 -0
- claude_mpm/skills/bundled/tauri/tauri-build-deploy.md +599 -0
- claude_mpm/skills/bundled/tauri/tauri-command-patterns.md +535 -0
- claude_mpm/skills/bundled/tauri/tauri-error-handling.md +613 -0
- claude_mpm/skills/bundled/tauri/tauri-event-system.md +648 -0
- claude_mpm/skills/bundled/tauri/tauri-file-system.md +673 -0
- claude_mpm/skills/bundled/tauri/tauri-frontend-integration.md +767 -0
- claude_mpm/skills/bundled/tauri/tauri-performance.md +669 -0
- claude_mpm/skills/bundled/tauri/tauri-state-management.md +573 -0
- claude_mpm/skills/bundled/tauri/tauri-testing.md +384 -0
- claude_mpm/skills/bundled/tauri/tauri-window-management.md +628 -0
- claude_mpm/skills/bundled/test-driven-development.md +378 -0
- claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +119 -0
- claude_mpm/skills/bundled/testing/condition-based-waiting/references/patterns-and-implementation.md +253 -0
- claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +145 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +543 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +741 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +470 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +458 -0
- claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +639 -0
- claude_mpm/skills/bundled/testing/test-quality-inspector/SKILL.md +458 -0
- claude_mpm/skills/bundled/testing/test-quality-inspector/examples/example-inspection-report.md +411 -0
- claude_mpm/skills/bundled/testing/test-quality-inspector/references/assertion-quality.md +317 -0
- claude_mpm/skills/bundled/testing/test-quality-inspector/references/inspection-checklist.md +270 -0
- claude_mpm/skills/bundled/testing/test-quality-inspector/references/red-flags.md +436 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +140 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/completeness-anti-patterns.md +572 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/core-anti-patterns.md +411 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/detection-guide.md +569 -0
- claude_mpm/skills/bundled/testing/testing-anti-patterns/references/tdd-connection.md +695 -0
- claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +184 -0
- claude_mpm/skills/bundled/testing/webapp-testing/decision-tree.md +459 -0
- claude_mpm/skills/bundled/testing/webapp-testing/examples/console_logging.py +35 -0
- claude_mpm/skills/bundled/testing/webapp-testing/examples/element_discovery.py +44 -0
- claude_mpm/skills/bundled/testing/webapp-testing/examples/static_html_automation.py +34 -0
- claude_mpm/skills/bundled/testing/webapp-testing/playwright-patterns.md +479 -0
- claude_mpm/skills/bundled/testing/webapp-testing/reconnaissance-pattern.md +687 -0
- claude_mpm/skills/bundled/testing/webapp-testing/scripts/with_server.py +129 -0
- claude_mpm/skills/bundled/testing/webapp-testing/server-management.md +758 -0
- claude_mpm/skills/bundled/testing/webapp-testing/troubleshooting.md +868 -0
- claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
- claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
- claude_mpm/skills/bundled/xlsx.md +157 -0
- claude_mpm/skills/registry.py +286 -0
- claude_mpm/skills/skill_manager.py +405 -0
- claude_mpm/skills/skills_registry.py +347 -0
- claude_mpm/skills/skills_service.py +739 -0
- claude_mpm/storage/__init__.py +9 -0
- claude_mpm/storage/state_storage.py +546 -0
- claude_mpm/templates/.pre-commit-config.yaml +112 -0
- claude_mpm/templates/questions/__init__.py +38 -0
- claude_mpm/templates/questions/base.py +193 -0
- claude_mpm/templates/questions/pr_strategy.py +311 -0
- claude_mpm/templates/questions/project_init.py +385 -0
- claude_mpm/templates/questions/ticket_mgmt.py +394 -0
- claude_mpm/ticket_wrapper.py +2 -2
- claude_mpm/tools/__init__.py +10 -0
- claude_mpm/tools/__main__.py +208 -0
- claude_mpm/tools/code_tree_analyzer/__init__.py +45 -0
- claude_mpm/tools/code_tree_analyzer/analysis.py +299 -0
- claude_mpm/tools/code_tree_analyzer/cache.py +131 -0
- claude_mpm/tools/code_tree_analyzer/core.py +380 -0
- claude_mpm/tools/code_tree_analyzer/discovery.py +403 -0
- claude_mpm/tools/code_tree_analyzer/events.py +168 -0
- claude_mpm/tools/code_tree_analyzer/gitignore.py +308 -0
- claude_mpm/tools/code_tree_analyzer/models.py +39 -0
- claude_mpm/tools/code_tree_analyzer/multilang_analyzer.py +224 -0
- claude_mpm/tools/code_tree_analyzer/python_analyzer.py +284 -0
- claude_mpm/tools/code_tree_builder.py +631 -0
- claude_mpm/tools/code_tree_events.py +420 -0
- claude_mpm/tools/socketio_debug.py +671 -0
- claude_mpm/utils/__init__.py +8 -8
- claude_mpm/utils/agent_dependency_loader.py +1189 -0
- claude_mpm/utils/agent_filters.py +261 -0
- claude_mpm/utils/common.py +544 -0
- claude_mpm/utils/config_manager.py +168 -126
- claude_mpm/utils/console.py +11 -0
- claude_mpm/utils/database_connector.py +298 -0
- claude_mpm/utils/dependency_cache.py +373 -0
- claude_mpm/utils/dependency_manager.py +60 -59
- claude_mpm/utils/dependency_strategies.py +381 -0
- claude_mpm/utils/display_helper.py +260 -0
- claude_mpm/utils/environment_context.py +313 -0
- claude_mpm/utils/error_handler.py +78 -66
- claude_mpm/utils/file_utils.py +305 -0
- claude_mpm/utils/framework_detection.py +12 -11
- claude_mpm/utils/git_analyzer.py +407 -0
- claude_mpm/utils/gitignore.py +244 -0
- claude_mpm/utils/import_migration_example.py +12 -60
- claude_mpm/utils/imports.py +48 -45
- claude_mpm/utils/log_cleanup.py +627 -0
- claude_mpm/utils/migration.py +372 -0
- claude_mpm/utils/path_operations.py +110 -104
- claude_mpm/utils/progress.py +387 -0
- claude_mpm/utils/robust_installer.py +844 -0
- claude_mpm/utils/session_logging.py +121 -0
- claude_mpm/utils/structured_questions.py +619 -0
- claude_mpm/utils/subprocess_utils.py +343 -0
- claude_mpm/validation/__init__.py +1 -1
- claude_mpm/validation/agent_validator.py +214 -108
- claude_mpm/validation/frontmatter_validator.py +252 -0
- claude_mpm-5.4.85.dist-info/METADATA +1023 -0
- claude_mpm-5.4.85.dist-info/RECORD +980 -0
- {claude_mpm-3.4.10.dist-info → claude_mpm-5.4.85.dist-info}/entry_points.txt +1 -3
- claude_mpm-5.4.85.dist-info/licenses/LICENSE +94 -0
- claude_mpm-5.4.85.dist-info/licenses/LICENSE-FAQ.md +153 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -88
- claude_mpm/agents/INSTRUCTIONS.md +0 -352
- claude_mpm/agents/backups/INSTRUCTIONS.md +0 -352
- claude_mpm/agents/base_agent_loader.py +0 -529
- claude_mpm/agents/schema/agent_schema.json +0 -314
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -36
- claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +0 -46
- claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +0 -45
- claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +0 -49
- claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +0 -46
- claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +0 -45
- claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +0 -49
- claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +0 -46
- claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +0 -46
- claude_mpm/agents/templates/data_engineer.json +0 -110
- claude_mpm/agents/templates/documentation.json +0 -109
- claude_mpm/agents/templates/engineer.json +0 -113
- claude_mpm/agents/templates/ops.json +0 -109
- claude_mpm/agents/templates/pm.json +0 -25
- claude_mpm/agents/templates/qa.json +0 -111
- claude_mpm/agents/templates/research.json +0 -65
- claude_mpm/agents/templates/security.json +0 -113
- claude_mpm/agents/templates/test_integration.json +0 -112
- claude_mpm/agents/templates/version_control.json +0 -107
- claude_mpm/cli/commands/ui.py +0 -57
- claude_mpm/core/simple_runner.py +0 -1046
- claude_mpm/dashboard/open_dashboard.py +0 -34
- claude_mpm/deployment_paths.py +0 -261
- claude_mpm/hooks/builtin/__init__.py +0 -1
- claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
- claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
- claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
- claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
- claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
- claude_mpm/orchestration/__init__.py +0 -6
- claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
- claude_mpm/orchestration/archive/factory.py +0 -215
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
- claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
- claude_mpm/orchestration/archive/orchestrator.py +0 -501
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
- claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
- claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
- claude_mpm/schemas/workflow_validator.py +0 -411
- claude_mpm/services/agent_deployment.py +0 -1534
- claude_mpm/services/agent_lifecycle_manager.py +0 -1169
- claude_mpm/services/agent_memory_manager.py +0 -1415
- claude_mpm/services/agent_registry.py +0 -676
- claude_mpm/services/deployed_agent_discovery.py +0 -226
- claude_mpm/services/framework_agent_loader.py +0 -337
- claude_mpm/services/framework_claude_md_generator.py +0 -621
- claude_mpm/services/health_monitor.py +0 -892
- claude_mpm/services/memory_router.py +0 -538
- claude_mpm/services/parent_directory_manager/__init__.py +0 -577
- claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
- claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
- claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
- claude_mpm/services/parent_directory_manager/operations.py +0 -186
- claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
- claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
- claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
- claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
- claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
- claude_mpm/services/standalone_socketio_server.py +0 -1300
- claude_mpm/services/ticket_manager_di.py +0 -318
- claude_mpm/services/ticketing_service_original.py +0 -508
- claude_mpm/ui/__init__.py +0 -1
- claude_mpm/ui/rich_terminal_ui.py +0 -295
- claude_mpm/ui/terminal_ui.py +0 -328
- claude_mpm/utils/paths.py +0 -289
- claude_mpm-3.4.10.dist-info/METADATA +0 -183
- claude_mpm-3.4.10.dist-info/RECORD +0 -201
- claude_mpm-3.4.10.dist-info/licenses/LICENSE +0 -21
- {claude_mpm-3.4.10.dist-info → claude_mpm-5.4.85.dist-info}/WHEEL +0 -0
- {claude_mpm-3.4.10.dist-info → claude_mpm-5.4.85.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,3253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive configuration management command for claude-mpm CLI.
|
|
3
|
+
|
|
4
|
+
WHY: Users need an intuitive, interactive way to manage agent configurations,
|
|
5
|
+
edit templates, and configure behavior files without manually editing JSON/YAML files.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Use Rich for modern TUI with menus, tables, and panels
|
|
9
|
+
- Support both project-level and user-level configurations
|
|
10
|
+
- Provide non-interactive options for scripting
|
|
11
|
+
- Allow direct navigation to specific sections
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import shutil
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
import questionary
|
|
21
|
+
import questionary.constants
|
|
22
|
+
import questionary.prompts.common # For checkbox symbol customization
|
|
23
|
+
from questionary import Choice, Separator, Style
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
from rich.prompt import Confirm, Prompt
|
|
26
|
+
from rich.text import Text
|
|
27
|
+
|
|
28
|
+
from ...core.config import Config
|
|
29
|
+
from ...core.unified_config import UnifiedConfig
|
|
30
|
+
from ...services.agents.agent_recommendation_service import AgentRecommendationService
|
|
31
|
+
from ...services.version_service import VersionService
|
|
32
|
+
from ...utils.agent_filters import apply_all_filters, get_deployed_agent_ids
|
|
33
|
+
from ...utils.console import console as default_console
|
|
34
|
+
from ..shared import BaseCommand, CommandResult
|
|
35
|
+
from .agent_state_manager import SimpleAgentManager
|
|
36
|
+
from .configure_agent_display import AgentDisplay
|
|
37
|
+
from .configure_behavior_manager import BehaviorManager
|
|
38
|
+
from .configure_hook_manager import HookManager
|
|
39
|
+
from .configure_models import AgentConfig
|
|
40
|
+
from .configure_navigation import ConfigNavigation
|
|
41
|
+
from .configure_persistence import ConfigPersistence
|
|
42
|
+
from .configure_startup_manager import StartupManager
|
|
43
|
+
from .configure_template_editor import TemplateEditor
|
|
44
|
+
from .configure_validators import (
|
|
45
|
+
parse_id_selection,
|
|
46
|
+
validate_args as validate_configure_args,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ConfigureCommand(BaseCommand):
|
|
51
|
+
"""Interactive configuration management command."""
|
|
52
|
+
|
|
53
|
+
# Questionary style optimized for dark terminals (WCAG AAA compliant)
|
|
54
|
+
QUESTIONARY_STYLE = Style(
|
|
55
|
+
[
|
|
56
|
+
("selected", "fg:#e0e0e0 bold"), # Light gray - excellent readability
|
|
57
|
+
("pointer", "fg:#ffd700 bold"), # Gold/yellow - highly visible pointer
|
|
58
|
+
("highlighted", "fg:#e0e0e0"), # Light gray - clear hover state
|
|
59
|
+
("question", "fg:#e0e0e0 bold"), # Light gray bold - prominent questions
|
|
60
|
+
("checkbox", "fg:#00ff00"), # Green - for checked boxes
|
|
61
|
+
(
|
|
62
|
+
"checkbox-selected",
|
|
63
|
+
"fg:#00ff00 bold",
|
|
64
|
+
), # Green bold - for checked selected boxes
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def __init__(self):
|
|
69
|
+
super().__init__("configure")
|
|
70
|
+
self.console = default_console
|
|
71
|
+
self.version_service = VersionService()
|
|
72
|
+
self.current_scope = "project"
|
|
73
|
+
self.project_dir = Path.cwd()
|
|
74
|
+
self.agent_manager = None
|
|
75
|
+
self.hook_manager = HookManager(self.console)
|
|
76
|
+
self.behavior_manager = None # Initialized when scope is set
|
|
77
|
+
self._agent_display = None # Lazy-initialized
|
|
78
|
+
self._persistence = None # Lazy-initialized
|
|
79
|
+
self._navigation = None # Lazy-initialized
|
|
80
|
+
self._template_editor = None # Lazy-initialized
|
|
81
|
+
self._startup_manager = None # Lazy-initialized
|
|
82
|
+
self._recommendation_service = None # Lazy-initialized
|
|
83
|
+
self._unified_config = None # Lazy-initialized
|
|
84
|
+
|
|
85
|
+
def validate_args(self, args) -> Optional[str]:
|
|
86
|
+
"""Validate command arguments."""
|
|
87
|
+
return validate_configure_args(args)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def agent_display(self) -> AgentDisplay:
|
|
91
|
+
"""Lazy-initialize agent display handler."""
|
|
92
|
+
if self._agent_display is None:
|
|
93
|
+
if self.agent_manager is None:
|
|
94
|
+
raise RuntimeError(
|
|
95
|
+
"agent_manager must be initialized before agent_display"
|
|
96
|
+
)
|
|
97
|
+
self._agent_display = AgentDisplay(
|
|
98
|
+
self.console,
|
|
99
|
+
self.agent_manager,
|
|
100
|
+
self._get_agent_template_path,
|
|
101
|
+
self._display_header,
|
|
102
|
+
)
|
|
103
|
+
return self._agent_display
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def persistence(self) -> ConfigPersistence:
|
|
107
|
+
"""Lazy-initialize persistence handler."""
|
|
108
|
+
if self._persistence is None:
|
|
109
|
+
# Note: agent_manager might be None for version_info calls
|
|
110
|
+
self._persistence = ConfigPersistence(
|
|
111
|
+
self.console,
|
|
112
|
+
self.version_service,
|
|
113
|
+
self.agent_manager, # Can be None for version operations
|
|
114
|
+
self._get_agent_template_path,
|
|
115
|
+
self._display_header,
|
|
116
|
+
self.current_scope,
|
|
117
|
+
self.project_dir,
|
|
118
|
+
)
|
|
119
|
+
return self._persistence
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def navigation(self) -> ConfigNavigation:
|
|
123
|
+
"""Lazy-initialize navigation handler."""
|
|
124
|
+
if self._navigation is None:
|
|
125
|
+
self._navigation = ConfigNavigation(self.console, self.project_dir)
|
|
126
|
+
# Sync scope from main command
|
|
127
|
+
self._navigation.current_scope = self.current_scope
|
|
128
|
+
return self._navigation
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def template_editor(self) -> TemplateEditor:
|
|
132
|
+
"""Lazy-initialize template editor."""
|
|
133
|
+
if self._template_editor is None:
|
|
134
|
+
if self.agent_manager is None:
|
|
135
|
+
raise RuntimeError(
|
|
136
|
+
"agent_manager must be initialized before template_editor"
|
|
137
|
+
)
|
|
138
|
+
self._template_editor = TemplateEditor(
|
|
139
|
+
self.console, self.agent_manager, self.current_scope, self.project_dir
|
|
140
|
+
)
|
|
141
|
+
return self._template_editor
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def startup_manager(self) -> StartupManager:
|
|
145
|
+
"""Lazy-initialize startup manager."""
|
|
146
|
+
if self._startup_manager is None:
|
|
147
|
+
if self.agent_manager is None:
|
|
148
|
+
raise RuntimeError(
|
|
149
|
+
"agent_manager must be initialized before startup_manager"
|
|
150
|
+
)
|
|
151
|
+
self._startup_manager = StartupManager(
|
|
152
|
+
self.agent_manager,
|
|
153
|
+
self.console,
|
|
154
|
+
self.current_scope,
|
|
155
|
+
self.project_dir,
|
|
156
|
+
self._display_header,
|
|
157
|
+
)
|
|
158
|
+
return self._startup_manager
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def recommendation_service(self) -> AgentRecommendationService:
|
|
162
|
+
"""Lazy-initialize recommendation service."""
|
|
163
|
+
if self._recommendation_service is None:
|
|
164
|
+
self._recommendation_service = AgentRecommendationService()
|
|
165
|
+
return self._recommendation_service
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def unified_config(self) -> UnifiedConfig:
|
|
169
|
+
"""Lazy-initialize unified config."""
|
|
170
|
+
if self._unified_config is None:
|
|
171
|
+
try:
|
|
172
|
+
self._unified_config = UnifiedConfig()
|
|
173
|
+
except Exception as e:
|
|
174
|
+
self.logger.warning(f"Failed to load unified config: {e}")
|
|
175
|
+
# Fallback to default config
|
|
176
|
+
self._unified_config = UnifiedConfig()
|
|
177
|
+
return self._unified_config
|
|
178
|
+
|
|
179
|
+
def run(self, args) -> CommandResult:
|
|
180
|
+
"""Execute the configure command."""
|
|
181
|
+
# Set configuration scope
|
|
182
|
+
self.current_scope = getattr(args, "scope", "project")
|
|
183
|
+
if getattr(args, "project_dir", None):
|
|
184
|
+
self.project_dir = Path(args.project_dir)
|
|
185
|
+
|
|
186
|
+
# Initialize agent manager and behavior manager with appropriate config directory
|
|
187
|
+
if self.current_scope == "project":
|
|
188
|
+
config_dir = self.project_dir / ".claude-mpm"
|
|
189
|
+
else:
|
|
190
|
+
config_dir = Path.home() / ".claude-mpm"
|
|
191
|
+
self.agent_manager = SimpleAgentManager(config_dir)
|
|
192
|
+
self.behavior_manager = BehaviorManager(
|
|
193
|
+
config_dir, self.current_scope, self.console
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Disable colors if requested
|
|
197
|
+
if getattr(args, "no_colors", False):
|
|
198
|
+
self.console = Console(color_system=None)
|
|
199
|
+
|
|
200
|
+
# Handle non-interactive options first
|
|
201
|
+
if getattr(args, "list_agents", False):
|
|
202
|
+
return self._list_agents_non_interactive()
|
|
203
|
+
|
|
204
|
+
if getattr(args, "enable_agent", None):
|
|
205
|
+
return self._enable_agent_non_interactive(args.enable_agent)
|
|
206
|
+
|
|
207
|
+
if getattr(args, "disable_agent", None):
|
|
208
|
+
return self._disable_agent_non_interactive(args.disable_agent)
|
|
209
|
+
|
|
210
|
+
if getattr(args, "export_config", None):
|
|
211
|
+
return self._export_config(args.export_config)
|
|
212
|
+
|
|
213
|
+
if getattr(args, "import_config", None):
|
|
214
|
+
return self._import_config(args.import_config)
|
|
215
|
+
|
|
216
|
+
if getattr(args, "version_info", False):
|
|
217
|
+
return self._show_version_info()
|
|
218
|
+
|
|
219
|
+
# Handle hook installation
|
|
220
|
+
if getattr(args, "install_hooks", False):
|
|
221
|
+
return self._install_hooks(force=getattr(args, "force", False))
|
|
222
|
+
|
|
223
|
+
if getattr(args, "verify_hooks", False):
|
|
224
|
+
return self._verify_hooks()
|
|
225
|
+
|
|
226
|
+
if getattr(args, "uninstall_hooks", False):
|
|
227
|
+
return self._uninstall_hooks()
|
|
228
|
+
|
|
229
|
+
# Handle direct navigation options
|
|
230
|
+
if getattr(args, "agents", False):
|
|
231
|
+
return self._run_agent_management()
|
|
232
|
+
|
|
233
|
+
if getattr(args, "templates", False):
|
|
234
|
+
return self._run_template_editing()
|
|
235
|
+
|
|
236
|
+
if getattr(args, "behaviors", False):
|
|
237
|
+
return self._run_behavior_management()
|
|
238
|
+
|
|
239
|
+
if getattr(args, "startup", False):
|
|
240
|
+
return self._run_startup_configuration()
|
|
241
|
+
|
|
242
|
+
# Launch interactive TUI
|
|
243
|
+
return self._run_interactive_tui(args)
|
|
244
|
+
|
|
245
|
+
def _run_interactive_tui(self, args) -> CommandResult:
|
|
246
|
+
"""Run the main interactive menu interface."""
|
|
247
|
+
# Rich-based menu interface
|
|
248
|
+
try:
|
|
249
|
+
self.console.clear()
|
|
250
|
+
|
|
251
|
+
while True:
|
|
252
|
+
# Display main menu
|
|
253
|
+
self._display_header()
|
|
254
|
+
choice = self._show_main_menu()
|
|
255
|
+
|
|
256
|
+
if choice == "1":
|
|
257
|
+
self._manage_agents()
|
|
258
|
+
elif choice == "2":
|
|
259
|
+
self._manage_skills()
|
|
260
|
+
elif choice == "3":
|
|
261
|
+
self._edit_templates()
|
|
262
|
+
elif choice == "4":
|
|
263
|
+
self._manage_behaviors()
|
|
264
|
+
elif choice == "5":
|
|
265
|
+
# If user saves and wants to proceed to startup, exit the configurator
|
|
266
|
+
if self._manage_startup_configuration():
|
|
267
|
+
self.console.print(
|
|
268
|
+
"\n[green]Configuration saved. Exiting configurator...[/green]"
|
|
269
|
+
)
|
|
270
|
+
break
|
|
271
|
+
elif choice == "6":
|
|
272
|
+
self._switch_scope()
|
|
273
|
+
elif choice == "7":
|
|
274
|
+
self._show_version_info_interactive()
|
|
275
|
+
elif choice == "l":
|
|
276
|
+
# Check for pending agent changes
|
|
277
|
+
if self.agent_manager and self.agent_manager.has_pending_changes():
|
|
278
|
+
should_save = Confirm.ask(
|
|
279
|
+
"[yellow]You have unsaved agent changes. Save them before launching?[/yellow]",
|
|
280
|
+
default=True,
|
|
281
|
+
)
|
|
282
|
+
if should_save:
|
|
283
|
+
self.agent_manager.commit_deferred_changes()
|
|
284
|
+
self.console.print("[green]✓ Agent changes saved[/green]")
|
|
285
|
+
else:
|
|
286
|
+
self.agent_manager.discard_deferred_changes()
|
|
287
|
+
self.console.print(
|
|
288
|
+
"[yellow]⚠ Agent changes discarded[/yellow]"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Save all configuration
|
|
292
|
+
self.console.print("\n[cyan]Saving configuration...[/cyan]")
|
|
293
|
+
if self._save_all_configuration():
|
|
294
|
+
# Launch Claude MPM (this will replace the process if successful)
|
|
295
|
+
self._launch_claude_mpm()
|
|
296
|
+
# If execvp fails, we'll return here and break
|
|
297
|
+
break
|
|
298
|
+
self.console.print(
|
|
299
|
+
"[red]✗ Failed to save configuration. Not launching.[/red]"
|
|
300
|
+
)
|
|
301
|
+
Prompt.ask("\nPress Enter to continue")
|
|
302
|
+
elif choice == "q":
|
|
303
|
+
self.console.print(
|
|
304
|
+
"\n[green]Configuration complete. Goodbye![/green]"
|
|
305
|
+
)
|
|
306
|
+
break
|
|
307
|
+
else:
|
|
308
|
+
self.console.print("[red]Invalid choice. Please try again.[/red]")
|
|
309
|
+
|
|
310
|
+
return CommandResult.success_result("Configuration completed")
|
|
311
|
+
|
|
312
|
+
except KeyboardInterrupt:
|
|
313
|
+
self.console.print("\n[yellow]Configuration cancelled.[/yellow]")
|
|
314
|
+
return CommandResult.success_result("Configuration cancelled")
|
|
315
|
+
except Exception as e:
|
|
316
|
+
self.logger.error(f"Configuration error: {e}", exc_info=True)
|
|
317
|
+
return CommandResult.error_result(f"Configuration failed: {e}")
|
|
318
|
+
|
|
319
|
+
def _display_header(self) -> None:
|
|
320
|
+
"""Display the TUI header."""
|
|
321
|
+
# Sync scope to navigation before display
|
|
322
|
+
self.navigation.current_scope = self.current_scope
|
|
323
|
+
self.navigation.display_header()
|
|
324
|
+
|
|
325
|
+
def _show_main_menu(self) -> str:
|
|
326
|
+
"""Show the main menu and get user choice."""
|
|
327
|
+
# Sync scope to navigation before display
|
|
328
|
+
self.navigation.current_scope = self.current_scope
|
|
329
|
+
return self.navigation.show_main_menu()
|
|
330
|
+
|
|
331
|
+
def _manage_agents(self) -> None:
|
|
332
|
+
"""Enhanced agent management with remote agent discovery and installation."""
|
|
333
|
+
while True:
|
|
334
|
+
self.console.clear()
|
|
335
|
+
self.navigation.display_header()
|
|
336
|
+
self.console.print("\n[bold blue]═══ Agent Management ═══[/bold blue]\n")
|
|
337
|
+
|
|
338
|
+
# Load all agents with spinner (don't show partial state)
|
|
339
|
+
agents = self._load_agents_with_spinner()
|
|
340
|
+
|
|
341
|
+
if not agents:
|
|
342
|
+
self.console.print("[yellow]No agents found[/yellow]")
|
|
343
|
+
self.console.print(
|
|
344
|
+
"[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
|
|
345
|
+
)
|
|
346
|
+
Prompt.ask("\nPress Enter to continue")
|
|
347
|
+
break
|
|
348
|
+
|
|
349
|
+
# Now display everything at once (after all data loaded)
|
|
350
|
+
self._display_agent_sources_and_list(agents)
|
|
351
|
+
|
|
352
|
+
# Step 3: Simplified menu - only "Select Agents" option
|
|
353
|
+
self.console.print()
|
|
354
|
+
self.logger.debug("About to show agent management menu")
|
|
355
|
+
try:
|
|
356
|
+
choice = questionary.select(
|
|
357
|
+
"Agent Management:",
|
|
358
|
+
choices=[
|
|
359
|
+
"Select Agents",
|
|
360
|
+
questionary.Separator(),
|
|
361
|
+
"← Back to main menu",
|
|
362
|
+
],
|
|
363
|
+
style=self.QUESTIONARY_STYLE,
|
|
364
|
+
).ask()
|
|
365
|
+
|
|
366
|
+
if choice is None or choice == "← Back to main menu":
|
|
367
|
+
break
|
|
368
|
+
|
|
369
|
+
# Map selection to action
|
|
370
|
+
if choice == "Select Agents":
|
|
371
|
+
self.logger.debug("User selected 'Select Agents' from menu")
|
|
372
|
+
self._deploy_agents_unified(agents)
|
|
373
|
+
# Loop back to show updated state after deployment
|
|
374
|
+
|
|
375
|
+
except KeyboardInterrupt:
|
|
376
|
+
self.console.print("\n[yellow]Operation cancelled[/yellow]")
|
|
377
|
+
break
|
|
378
|
+
except Exception as e:
|
|
379
|
+
# Handle questionary menu failure
|
|
380
|
+
import sys
|
|
381
|
+
|
|
382
|
+
self.logger.error(f"Agent management menu failed: {e}", exc_info=True)
|
|
383
|
+
self.console.print("[red]Error: Interactive menu failed[/red]")
|
|
384
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
385
|
+
if not sys.stdin.isatty():
|
|
386
|
+
self.console.print(
|
|
387
|
+
"[dim]Interactive terminal required for this operation[/dim]"
|
|
388
|
+
)
|
|
389
|
+
self.console.print("[dim]Use command-line options instead:[/dim]")
|
|
390
|
+
self.console.print(
|
|
391
|
+
"[dim] claude-mpm configure --list-agents[/dim]"
|
|
392
|
+
)
|
|
393
|
+
self.console.print(
|
|
394
|
+
"[dim] claude-mpm configure --enable-agent <id>[/dim]"
|
|
395
|
+
)
|
|
396
|
+
Prompt.ask("\nPress Enter to continue")
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
def _load_agents_with_spinner(self) -> List[AgentConfig]:
|
|
400
|
+
"""Load agents with loading indicator, don't show partial state.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
List of discovered agents with deployment status set.
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
agents = []
|
|
407
|
+
with self.console.status(
|
|
408
|
+
"[bold blue]Loading agents...[/bold blue]", spinner="dots"
|
|
409
|
+
):
|
|
410
|
+
try:
|
|
411
|
+
# Discover agents (includes both local and remote)
|
|
412
|
+
agents = self.agent_manager.discover_agents(include_remote=True)
|
|
413
|
+
|
|
414
|
+
# Set deployment status on each agent for display
|
|
415
|
+
deployed_ids = get_deployed_agent_ids()
|
|
416
|
+
for agent in agents:
|
|
417
|
+
# Use agent_id (technical ID) for comparison, not display name
|
|
418
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
419
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
420
|
+
agent.is_deployed = agent_leaf_name in deployed_ids
|
|
421
|
+
|
|
422
|
+
# Filter BASE_AGENT from display (1M-502 Phase 1)
|
|
423
|
+
agents = self._filter_agent_configs(agents, filter_deployed=False)
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
self.console.print(f"[red]Error discovering agents: {e}[/red]")
|
|
427
|
+
self.logger.error(f"Agent discovery failed: {e}", exc_info=True)
|
|
428
|
+
agents = []
|
|
429
|
+
|
|
430
|
+
return agents
|
|
431
|
+
|
|
432
|
+
def _display_agent_sources_and_list(self, agents: List[AgentConfig]) -> None:
|
|
433
|
+
"""Display agent sources and agent list (only after all data loaded).
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
agents: List of discovered agents with deployment status.
|
|
437
|
+
"""
|
|
438
|
+
from rich.table import Table
|
|
439
|
+
|
|
440
|
+
# Step 1: Show configured sources
|
|
441
|
+
self.console.print("[bold white]═══ Agent Sources ═══[/bold white]\n")
|
|
442
|
+
|
|
443
|
+
sources = self._get_configured_sources()
|
|
444
|
+
if sources:
|
|
445
|
+
sources_table = Table(show_header=True, header_style="bold white")
|
|
446
|
+
sources_table.add_column(
|
|
447
|
+
"Source",
|
|
448
|
+
style="bright_yellow",
|
|
449
|
+
width=40,
|
|
450
|
+
no_wrap=True,
|
|
451
|
+
overflow="ellipsis",
|
|
452
|
+
)
|
|
453
|
+
sources_table.add_column("Status", style="green", width=15, no_wrap=True)
|
|
454
|
+
sources_table.add_column("Agents", style="yellow", width=10, no_wrap=True)
|
|
455
|
+
|
|
456
|
+
for source in sources:
|
|
457
|
+
status = "✓ Active" if source.get("enabled", True) else "Disabled"
|
|
458
|
+
agent_count = source.get("agent_count", "?")
|
|
459
|
+
sources_table.add_row(source["identifier"], status, str(agent_count))
|
|
460
|
+
|
|
461
|
+
self.console.print(sources_table)
|
|
462
|
+
else:
|
|
463
|
+
self.console.print("[yellow]No agent sources configured[/yellow]")
|
|
464
|
+
self.console.print(
|
|
465
|
+
"[dim]Default source 'bobmatnyc/claude-mpm-agents' will be used[/dim]\n"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Step 2: Display available agents
|
|
469
|
+
self.console.print("\n[bold white]═══ Available Agents ═══[/bold white]\n")
|
|
470
|
+
|
|
471
|
+
if agents:
|
|
472
|
+
# Show progress spinner while recommendation service processes agents
|
|
473
|
+
with self.console.status(
|
|
474
|
+
"[bold blue]Preparing agent list...[/bold blue]", spinner="dots"
|
|
475
|
+
):
|
|
476
|
+
self._display_agents_with_source_info(agents)
|
|
477
|
+
else:
|
|
478
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
479
|
+
|
|
480
|
+
def _display_agents_table(self, agents: List[AgentConfig]) -> None:
|
|
481
|
+
"""Display a table of available agents."""
|
|
482
|
+
self.agent_display.display_agents_table(agents)
|
|
483
|
+
|
|
484
|
+
def _display_agents_with_pending_states(self, agents: List[AgentConfig]) -> None:
|
|
485
|
+
"""Display agents table with pending state indicators."""
|
|
486
|
+
self.agent_display.display_agents_with_pending_states(agents)
|
|
487
|
+
|
|
488
|
+
def _toggle_agents_interactive(self, agents: List[AgentConfig]) -> None:
|
|
489
|
+
"""Interactive multi-agent enable/disable with batch save."""
|
|
490
|
+
|
|
491
|
+
# Initialize pending states from current states
|
|
492
|
+
for agent in agents:
|
|
493
|
+
current_state = self.agent_manager.is_agent_enabled(agent.name)
|
|
494
|
+
self.agent_manager.set_agent_enabled_deferred(agent.name, current_state)
|
|
495
|
+
|
|
496
|
+
while True:
|
|
497
|
+
# Display table with pending states
|
|
498
|
+
self._display_agents_with_pending_states(agents)
|
|
499
|
+
|
|
500
|
+
# Show menu
|
|
501
|
+
self.console.print("\n[bold]Toggle Agent Status:[/bold]")
|
|
502
|
+
text_toggle = Text(" ")
|
|
503
|
+
text_toggle.append("[t]", style="bold blue")
|
|
504
|
+
text_toggle.append(" Enter agent IDs to toggle (e.g., '1,3,5' or '1-4')")
|
|
505
|
+
self.console.print(text_toggle)
|
|
506
|
+
|
|
507
|
+
text_all = Text(" ")
|
|
508
|
+
text_all.append("[a]", style="bold blue")
|
|
509
|
+
text_all.append(" Enable all agents")
|
|
510
|
+
self.console.print(text_all)
|
|
511
|
+
|
|
512
|
+
text_none = Text(" ")
|
|
513
|
+
text_none.append("[n]", style="bold blue")
|
|
514
|
+
text_none.append(" Disable all agents")
|
|
515
|
+
self.console.print(text_none)
|
|
516
|
+
|
|
517
|
+
text_save = Text(" ")
|
|
518
|
+
text_save.append("[s]", style="bold green")
|
|
519
|
+
text_save.append(" Save changes and return")
|
|
520
|
+
self.console.print(text_save)
|
|
521
|
+
|
|
522
|
+
text_cancel = Text(" ")
|
|
523
|
+
text_cancel.append("[c]", style="bold magenta")
|
|
524
|
+
text_cancel.append(" Cancel (discard changes)")
|
|
525
|
+
self.console.print(text_cancel)
|
|
526
|
+
|
|
527
|
+
choice = (
|
|
528
|
+
Prompt.ask("[bold blue]Select an option[/bold blue]", default="s")
|
|
529
|
+
.strip()
|
|
530
|
+
.lower()
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if choice == "s":
|
|
534
|
+
if self.agent_manager.has_pending_changes():
|
|
535
|
+
self.agent_manager.commit_deferred_changes()
|
|
536
|
+
self.console.print("[green]✓ Changes saved successfully![/green]")
|
|
537
|
+
|
|
538
|
+
# Auto-deploy enabled agents to .claude/agents/
|
|
539
|
+
self._auto_deploy_enabled_agents(agents)
|
|
540
|
+
else:
|
|
541
|
+
self.console.print("[yellow]No changes to save.[/yellow]")
|
|
542
|
+
Prompt.ask("Press Enter to continue")
|
|
543
|
+
break
|
|
544
|
+
if choice == "c":
|
|
545
|
+
self.agent_manager.discard_deferred_changes()
|
|
546
|
+
self.console.print("[yellow]Changes discarded.[/yellow]")
|
|
547
|
+
Prompt.ask("Press Enter to continue")
|
|
548
|
+
break
|
|
549
|
+
if choice == "a":
|
|
550
|
+
for agent in agents:
|
|
551
|
+
self.agent_manager.set_agent_enabled_deferred(agent.name, True)
|
|
552
|
+
elif choice == "n":
|
|
553
|
+
for agent in agents:
|
|
554
|
+
self.agent_manager.set_agent_enabled_deferred(agent.name, False)
|
|
555
|
+
elif choice == "t" or choice.replace(",", "").replace("-", "").isdigit():
|
|
556
|
+
selected_ids = self._parse_id_selection(
|
|
557
|
+
choice if choice != "t" else Prompt.ask("Enter IDs"), len(agents)
|
|
558
|
+
)
|
|
559
|
+
for idx in selected_ids:
|
|
560
|
+
if 1 <= idx <= len(agents):
|
|
561
|
+
agent = agents[idx - 1]
|
|
562
|
+
current = self.agent_manager.get_pending_state(agent.name)
|
|
563
|
+
self.agent_manager.set_agent_enabled_deferred(
|
|
564
|
+
agent.name, not current
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
def _auto_deploy_enabled_agents(self, agents: List[AgentConfig]) -> None:
|
|
568
|
+
"""Auto-deploy enabled agents after saving configuration.
|
|
569
|
+
|
|
570
|
+
WHY: When users enable agents, they expect them to be deployed
|
|
571
|
+
automatically to .claude/agents/ so they're available for use.
|
|
572
|
+
"""
|
|
573
|
+
try:
|
|
574
|
+
# Get list of enabled agents from states
|
|
575
|
+
enabled_agents = [
|
|
576
|
+
agent
|
|
577
|
+
for agent in agents
|
|
578
|
+
if self.agent_manager.is_agent_enabled(agent.name)
|
|
579
|
+
]
|
|
580
|
+
|
|
581
|
+
if not enabled_agents:
|
|
582
|
+
return
|
|
583
|
+
|
|
584
|
+
# Show deployment progress
|
|
585
|
+
self.console.print(
|
|
586
|
+
f"\n[bold blue]Deploying {len(enabled_agents)} enabled agent(s)...[/bold blue]"
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# Deploy each enabled agent
|
|
590
|
+
success_count = 0
|
|
591
|
+
failed_count = 0
|
|
592
|
+
|
|
593
|
+
for agent in enabled_agents:
|
|
594
|
+
# Deploy to .claude/agents/ (project-level)
|
|
595
|
+
try:
|
|
596
|
+
if self._deploy_single_agent(agent, show_feedback=False):
|
|
597
|
+
success_count += 1
|
|
598
|
+
self.console.print(f"[green]✓ Deployed: {agent.name}[/green]")
|
|
599
|
+
else:
|
|
600
|
+
failed_count += 1
|
|
601
|
+
self.console.print(f"[yellow]⚠ Skipped: {agent.name}[/yellow]")
|
|
602
|
+
except Exception as e:
|
|
603
|
+
failed_count += 1
|
|
604
|
+
self.logger.error(f"Failed to deploy {agent.name}: {e}")
|
|
605
|
+
self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
|
|
606
|
+
|
|
607
|
+
# Show summary
|
|
608
|
+
if success_count > 0:
|
|
609
|
+
self.console.print(
|
|
610
|
+
f"\n[green]✓ Successfully deployed {success_count} agent(s) to .claude/agents/[/green]"
|
|
611
|
+
)
|
|
612
|
+
if failed_count > 0:
|
|
613
|
+
self.console.print(
|
|
614
|
+
f"[yellow]⚠ {failed_count} agent(s) failed or were skipped[/yellow]"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
except Exception as e:
|
|
618
|
+
self.logger.error(f"Auto-deployment failed: {e}", exc_info=True)
|
|
619
|
+
self.console.print(f"[red]✗ Auto-deployment error: {e}[/red]")
|
|
620
|
+
|
|
621
|
+
def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
|
|
622
|
+
"""Customize agent JSON template."""
|
|
623
|
+
self.template_editor.customize_agent_template(agents)
|
|
624
|
+
|
|
625
|
+
def _edit_agent_template(self, agent: AgentConfig) -> None:
|
|
626
|
+
"""Edit an agent's JSON template."""
|
|
627
|
+
self.template_editor.edit_agent_template(agent)
|
|
628
|
+
|
|
629
|
+
def _get_agent_template_path(self, agent_name: str) -> Path:
|
|
630
|
+
"""Get the path to an agent's template file."""
|
|
631
|
+
return self.template_editor.get_agent_template_path(agent_name)
|
|
632
|
+
|
|
633
|
+
def _edit_in_external_editor(self, template_path: Path, template: Dict) -> None:
|
|
634
|
+
"""Open template in external editor."""
|
|
635
|
+
self.template_editor.edit_in_external_editor(template_path, template)
|
|
636
|
+
|
|
637
|
+
def _modify_template_field(self, template: Dict, template_path: Path) -> None:
|
|
638
|
+
"""Add or modify a field in the template."""
|
|
639
|
+
self.template_editor.modify_template_field(template, template_path)
|
|
640
|
+
|
|
641
|
+
def _remove_template_field(self, template: Dict, template_path: Path) -> None:
|
|
642
|
+
"""Remove a field from the template."""
|
|
643
|
+
self.template_editor.remove_template_field(template, template_path)
|
|
644
|
+
|
|
645
|
+
def _reset_template(self, agent: AgentConfig, template_path: Path) -> None:
|
|
646
|
+
"""Reset template to defaults."""
|
|
647
|
+
self.template_editor.reset_template(agent, template_path)
|
|
648
|
+
|
|
649
|
+
def _create_custom_template_copy(self, agent: AgentConfig, template: Dict) -> None:
|
|
650
|
+
"""Create a customized copy of a system template."""
|
|
651
|
+
self.template_editor.create_custom_template_copy(agent, template)
|
|
652
|
+
|
|
653
|
+
def _view_full_template(self, template: Dict) -> None:
|
|
654
|
+
"""View the full template without truncation."""
|
|
655
|
+
self.template_editor.view_full_template(template)
|
|
656
|
+
|
|
657
|
+
def _reset_agent_defaults(self, agents: List[AgentConfig]) -> None:
|
|
658
|
+
"""Reset an agent to default enabled state and remove custom template."""
|
|
659
|
+
self.template_editor.reset_agent_defaults(agents)
|
|
660
|
+
|
|
661
|
+
def _edit_templates(self) -> None:
|
|
662
|
+
"""Template editing interface."""
|
|
663
|
+
self.template_editor.edit_templates_interface()
|
|
664
|
+
|
|
665
|
+
def _manage_behaviors(self) -> None:
|
|
666
|
+
"""Behavior file management interface."""
|
|
667
|
+
# Note: BehaviorManager handles its own loop and clears screen
|
|
668
|
+
# but doesn't display our header. We'll need to update BehaviorManager
|
|
669
|
+
# to accept a header callback in the future. For now, just delegate.
|
|
670
|
+
self.behavior_manager.manage_behaviors()
|
|
671
|
+
|
|
672
|
+
def _manage_skills(self) -> None:
|
|
673
|
+
"""Skills management interface with questionary checkbox selection."""
|
|
674
|
+
from ...cli.interactive.skills_wizard import SkillsWizard
|
|
675
|
+
from ...skills.skill_manager import get_manager
|
|
676
|
+
|
|
677
|
+
wizard = SkillsWizard()
|
|
678
|
+
manager = get_manager()
|
|
679
|
+
|
|
680
|
+
while True:
|
|
681
|
+
self.console.clear()
|
|
682
|
+
self._display_header()
|
|
683
|
+
|
|
684
|
+
self.console.print("\n[bold]Skills Management[/bold]")
|
|
685
|
+
|
|
686
|
+
# Show action options
|
|
687
|
+
self.console.print("\n[bold]Actions:[/bold]")
|
|
688
|
+
self.console.print(" [1] Install/Uninstall skills")
|
|
689
|
+
self.console.print(" [2] Configure skills for agents")
|
|
690
|
+
self.console.print(" [3] View current skill mappings")
|
|
691
|
+
self.console.print(" [4] Auto-link skills to agents")
|
|
692
|
+
self.console.print(" [b] Back to main menu")
|
|
693
|
+
self.console.print()
|
|
694
|
+
|
|
695
|
+
choice = Prompt.ask("[bold blue]Select an option[/bold blue]", default="b")
|
|
696
|
+
|
|
697
|
+
if choice == "1":
|
|
698
|
+
# Install/Uninstall skills with category-based selection
|
|
699
|
+
self._manage_skill_installation()
|
|
700
|
+
|
|
701
|
+
elif choice == "2":
|
|
702
|
+
# Configure skills interactively
|
|
703
|
+
self.console.clear()
|
|
704
|
+
self._display_header()
|
|
705
|
+
|
|
706
|
+
# Get list of enabled agents
|
|
707
|
+
agents = self.agent_manager.discover_agents()
|
|
708
|
+
# Filter BASE_AGENT from all agent operations (1M-502 Phase 1)
|
|
709
|
+
agents = self._filter_agent_configs(agents, filter_deployed=False)
|
|
710
|
+
enabled_agents = [
|
|
711
|
+
a.name
|
|
712
|
+
for a in agents
|
|
713
|
+
if self.agent_manager.get_pending_state(a.name)
|
|
714
|
+
]
|
|
715
|
+
|
|
716
|
+
if not enabled_agents:
|
|
717
|
+
self.console.print(
|
|
718
|
+
"[yellow]No agents are currently enabled.[/yellow]"
|
|
719
|
+
)
|
|
720
|
+
self.console.print(
|
|
721
|
+
"Please enable agents first in Agent Management."
|
|
722
|
+
)
|
|
723
|
+
Prompt.ask("\nPress Enter to continue")
|
|
724
|
+
continue
|
|
725
|
+
|
|
726
|
+
# Run skills wizard
|
|
727
|
+
success, mapping = wizard.run_interactive_selection(enabled_agents)
|
|
728
|
+
|
|
729
|
+
if success:
|
|
730
|
+
# Save the configuration
|
|
731
|
+
manager.save_mappings_to_config()
|
|
732
|
+
self.console.print("\n[green]✓ Skills configuration saved![/green]")
|
|
733
|
+
else:
|
|
734
|
+
self.console.print(
|
|
735
|
+
"\n[yellow]Skills configuration cancelled.[/yellow]"
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
Prompt.ask("\nPress Enter to continue")
|
|
739
|
+
|
|
740
|
+
elif choice == "3":
|
|
741
|
+
# View current mappings
|
|
742
|
+
self.console.clear()
|
|
743
|
+
self._display_header()
|
|
744
|
+
|
|
745
|
+
self.console.print("\n[bold]Current Skill Mappings:[/bold]\n")
|
|
746
|
+
|
|
747
|
+
mappings = manager.list_agent_skill_mappings()
|
|
748
|
+
if not mappings:
|
|
749
|
+
self.console.print("[dim]No skill mappings configured yet.[/dim]")
|
|
750
|
+
else:
|
|
751
|
+
from rich.table import Table
|
|
752
|
+
|
|
753
|
+
table = Table(show_header=True, header_style="bold white")
|
|
754
|
+
table.add_column("Agent", style="white", no_wrap=True)
|
|
755
|
+
table.add_column("Skills", style="green", no_wrap=True)
|
|
756
|
+
|
|
757
|
+
for agent_id, skills in mappings.items():
|
|
758
|
+
skills_str = (
|
|
759
|
+
", ".join(skills) if skills else "[dim](none)[/dim]"
|
|
760
|
+
)
|
|
761
|
+
table.add_row(agent_id, skills_str)
|
|
762
|
+
|
|
763
|
+
self.console.print(table)
|
|
764
|
+
|
|
765
|
+
Prompt.ask("\nPress Enter to continue")
|
|
766
|
+
|
|
767
|
+
elif choice == "4":
|
|
768
|
+
# Auto-link skills
|
|
769
|
+
self.console.clear()
|
|
770
|
+
self._display_header()
|
|
771
|
+
|
|
772
|
+
self.console.print("\n[bold]Auto-Linking Skills to Agents...[/bold]\n")
|
|
773
|
+
|
|
774
|
+
# Get enabled agents
|
|
775
|
+
agents = self.agent_manager.discover_agents()
|
|
776
|
+
# Filter BASE_AGENT from all agent operations (1M-502 Phase 1)
|
|
777
|
+
agents = self._filter_agent_configs(agents, filter_deployed=False)
|
|
778
|
+
enabled_agents = [
|
|
779
|
+
a.name
|
|
780
|
+
for a in agents
|
|
781
|
+
if self.agent_manager.get_pending_state(a.name)
|
|
782
|
+
]
|
|
783
|
+
|
|
784
|
+
if not enabled_agents:
|
|
785
|
+
self.console.print(
|
|
786
|
+
"[yellow]No agents are currently enabled.[/yellow]"
|
|
787
|
+
)
|
|
788
|
+
self.console.print(
|
|
789
|
+
"Please enable agents first in Agent Management."
|
|
790
|
+
)
|
|
791
|
+
Prompt.ask("\nPress Enter to continue")
|
|
792
|
+
continue
|
|
793
|
+
|
|
794
|
+
# Auto-link
|
|
795
|
+
mapping = wizard._auto_link_skills(enabled_agents)
|
|
796
|
+
|
|
797
|
+
# Display preview
|
|
798
|
+
self.console.print("Auto-linked skills:\n")
|
|
799
|
+
for agent_id, skills in mapping.items():
|
|
800
|
+
self.console.print(f" [yellow]{agent_id}[/yellow]:")
|
|
801
|
+
for skill in skills:
|
|
802
|
+
self.console.print(f" - {skill}")
|
|
803
|
+
|
|
804
|
+
# Confirm
|
|
805
|
+
confirm = Confirm.ask("\nApply this configuration?", default=True)
|
|
806
|
+
|
|
807
|
+
if confirm:
|
|
808
|
+
wizard._apply_skills_configuration(mapping)
|
|
809
|
+
manager.save_mappings_to_config()
|
|
810
|
+
self.console.print("\n[green]✓ Auto-linking complete![/green]")
|
|
811
|
+
else:
|
|
812
|
+
self.console.print("\n[yellow]Auto-linking cancelled.[/yellow]")
|
|
813
|
+
|
|
814
|
+
Prompt.ask("\nPress Enter to continue")
|
|
815
|
+
|
|
816
|
+
elif choice == "b":
|
|
817
|
+
break
|
|
818
|
+
else:
|
|
819
|
+
self.console.print("[red]Invalid choice. Please try again.[/red]")
|
|
820
|
+
Prompt.ask("\nPress Enter to continue")
|
|
821
|
+
|
|
822
|
+
def _detect_skill_patterns(self, skills: list[dict]) -> dict[str, list[dict]]:
|
|
823
|
+
"""Group skills by detected common prefixes.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
skills: List of skill dictionaries
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
Dict mapping pattern prefix to list of skills.
|
|
830
|
+
Skills without pattern match go under "" (empty string) key.
|
|
831
|
+
"""
|
|
832
|
+
from collections import defaultdict
|
|
833
|
+
|
|
834
|
+
# Count prefix occurrences (try 1-segment and 2-segment prefixes)
|
|
835
|
+
prefix_counts = defaultdict(list)
|
|
836
|
+
|
|
837
|
+
for skill in skills:
|
|
838
|
+
skill_id = skill.get("name", skill.get("skill_id", ""))
|
|
839
|
+
|
|
840
|
+
# Try to extract prefixes (split by hyphen)
|
|
841
|
+
parts = skill_id.split("-")
|
|
842
|
+
|
|
843
|
+
if len(parts) >= 2:
|
|
844
|
+
# Try 2-segment prefix first (e.g., "toolchains-universal")
|
|
845
|
+
two_seg_prefix = f"{parts[0]}-{parts[1]}"
|
|
846
|
+
prefix_counts[two_seg_prefix].append(skill)
|
|
847
|
+
|
|
848
|
+
# Also try 1-segment prefix (e.g., "digitalocean")
|
|
849
|
+
one_seg_prefix = parts[0]
|
|
850
|
+
if one_seg_prefix != two_seg_prefix:
|
|
851
|
+
prefix_counts[one_seg_prefix].append(skill)
|
|
852
|
+
|
|
853
|
+
# Build pattern groups (require at least 2 skills per pattern)
|
|
854
|
+
pattern_groups = defaultdict(list)
|
|
855
|
+
used_skills = set()
|
|
856
|
+
|
|
857
|
+
# Prefer longer (more specific) prefixes
|
|
858
|
+
sorted_prefixes = sorted(prefix_counts.keys(), key=lambda x: (-len(x), x))
|
|
859
|
+
|
|
860
|
+
for prefix in sorted_prefixes:
|
|
861
|
+
matching_skills = prefix_counts[prefix]
|
|
862
|
+
|
|
863
|
+
# Only create a pattern group if we have 2+ skills and they're not already grouped
|
|
864
|
+
available_skills = [s for s in matching_skills if id(s) not in used_skills]
|
|
865
|
+
|
|
866
|
+
if len(available_skills) >= 2:
|
|
867
|
+
pattern_groups[prefix] = available_skills
|
|
868
|
+
used_skills.update(id(s) for s in available_skills)
|
|
869
|
+
|
|
870
|
+
# Add ungrouped skills to "" (Other) group
|
|
871
|
+
for skill in skills:
|
|
872
|
+
if id(skill) not in used_skills:
|
|
873
|
+
pattern_groups[""].append(skill)
|
|
874
|
+
|
|
875
|
+
return dict(pattern_groups)
|
|
876
|
+
|
|
877
|
+
def _get_pattern_icon(self, prefix: str) -> str:
|
|
878
|
+
"""Get icon for a pattern prefix.
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
prefix: Pattern prefix (e.g., "digitalocean", "vercel")
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Emoji icon for the pattern
|
|
885
|
+
"""
|
|
886
|
+
pattern_icons = {
|
|
887
|
+
"digitalocean": "🌊",
|
|
888
|
+
"aws": "☁️",
|
|
889
|
+
"github": "🐙",
|
|
890
|
+
"google": "🔍",
|
|
891
|
+
"vercel": "▲",
|
|
892
|
+
"netlify": "🦋",
|
|
893
|
+
"universal-testing": "🧪",
|
|
894
|
+
"universal-debugging": "🐛",
|
|
895
|
+
"universal-security": "🔒",
|
|
896
|
+
"toolchains-python": "🐍",
|
|
897
|
+
"toolchains-typescript": "📘",
|
|
898
|
+
"toolchains-javascript": "📒",
|
|
899
|
+
}
|
|
900
|
+
return pattern_icons.get(prefix, "📦")
|
|
901
|
+
|
|
902
|
+
def _manage_skill_installation(self) -> None:
|
|
903
|
+
"""Manage skill installation with category-based questionary checkbox selection."""
|
|
904
|
+
import questionary
|
|
905
|
+
|
|
906
|
+
# Get all skills
|
|
907
|
+
all_skills = self._get_all_skills_from_git()
|
|
908
|
+
if not all_skills:
|
|
909
|
+
self.console.print(
|
|
910
|
+
"[yellow]No skills available. Try syncing skills first.[/yellow]"
|
|
911
|
+
)
|
|
912
|
+
Prompt.ask("\nPress Enter to continue")
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
# Get deployed skills
|
|
916
|
+
deployed = self._get_deployed_skill_ids()
|
|
917
|
+
|
|
918
|
+
# Group by category
|
|
919
|
+
grouped = {}
|
|
920
|
+
for skill in all_skills:
|
|
921
|
+
# Try to get category from tags or use toolchain
|
|
922
|
+
category = None
|
|
923
|
+
tags = skill.get("tags", [])
|
|
924
|
+
|
|
925
|
+
# Look for category tag
|
|
926
|
+
for tag in tags:
|
|
927
|
+
if tag in [
|
|
928
|
+
"universal",
|
|
929
|
+
"python",
|
|
930
|
+
"typescript",
|
|
931
|
+
"javascript",
|
|
932
|
+
"go",
|
|
933
|
+
"rust",
|
|
934
|
+
]:
|
|
935
|
+
category = tag
|
|
936
|
+
break
|
|
937
|
+
|
|
938
|
+
# Fallback to toolchain or universal
|
|
939
|
+
if not category:
|
|
940
|
+
category = skill.get("toolchain", "universal")
|
|
941
|
+
|
|
942
|
+
if category not in grouped:
|
|
943
|
+
grouped[category] = []
|
|
944
|
+
grouped[category].append(skill)
|
|
945
|
+
|
|
946
|
+
# Category icons
|
|
947
|
+
icons = {
|
|
948
|
+
"universal": "🌐",
|
|
949
|
+
"python": "🐍",
|
|
950
|
+
"typescript": "📘",
|
|
951
|
+
"javascript": "📒",
|
|
952
|
+
"go": "🔷",
|
|
953
|
+
"rust": "⚙️",
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
# Sort categories: universal first, then alphabetically
|
|
957
|
+
categories = sorted(grouped.keys(), key=lambda x: (x != "universal", x))
|
|
958
|
+
|
|
959
|
+
while True:
|
|
960
|
+
# Show category selection first
|
|
961
|
+
self.console.clear()
|
|
962
|
+
self._display_header()
|
|
963
|
+
self.console.print("\n[bold cyan]Skills Management[/bold cyan]")
|
|
964
|
+
self.console.print(
|
|
965
|
+
f"[dim]{len(all_skills)} skills available, {len(deployed)} installed[/dim]\n"
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
cat_choices = [
|
|
969
|
+
Choice(
|
|
970
|
+
title=f"{icons.get(cat, '📦')} {cat.title()} ({len(grouped[cat])} skills)",
|
|
971
|
+
value=cat,
|
|
972
|
+
)
|
|
973
|
+
for cat in categories
|
|
974
|
+
]
|
|
975
|
+
cat_choices.append(Choice(title="← Back to main menu", value="back"))
|
|
976
|
+
|
|
977
|
+
selected_cat = questionary.select(
|
|
978
|
+
"Select a category:", choices=cat_choices, style=self.QUESTIONARY_STYLE
|
|
979
|
+
).ask()
|
|
980
|
+
|
|
981
|
+
if selected_cat is None or selected_cat == "back":
|
|
982
|
+
return
|
|
983
|
+
|
|
984
|
+
# Show skills in category with checkbox selection
|
|
985
|
+
category_skills = grouped[selected_cat]
|
|
986
|
+
|
|
987
|
+
# Detect pattern groups within category
|
|
988
|
+
pattern_groups = self._detect_skill_patterns(category_skills)
|
|
989
|
+
|
|
990
|
+
# Build choices with pattern grouping and installation status
|
|
991
|
+
skill_choices = []
|
|
992
|
+
|
|
993
|
+
# Track which skills belong to which group for expansion later
|
|
994
|
+
group_to_skills = {}
|
|
995
|
+
|
|
996
|
+
# Sort pattern groups: "" (Other) last, rest alphabetically
|
|
997
|
+
sorted_patterns = sorted(pattern_groups.keys(), key=lambda x: (x == "", x))
|
|
998
|
+
|
|
999
|
+
for pattern in sorted_patterns:
|
|
1000
|
+
pattern_skills = pattern_groups[pattern]
|
|
1001
|
+
|
|
1002
|
+
# Skip empty groups
|
|
1003
|
+
if not pattern_skills:
|
|
1004
|
+
continue
|
|
1005
|
+
|
|
1006
|
+
# Collect skill IDs in this group
|
|
1007
|
+
skill_ids_in_group = []
|
|
1008
|
+
for skill in pattern_skills:
|
|
1009
|
+
skill_id = skill.get("name", skill.get("skill_id", "unknown"))
|
|
1010
|
+
skill_ids_in_group.append(skill_id)
|
|
1011
|
+
|
|
1012
|
+
# Check if all skills in group are installed
|
|
1013
|
+
all_installed = all(
|
|
1014
|
+
skill.get(
|
|
1015
|
+
"deployment_name", skill.get("name", skill.get("skill_id"))
|
|
1016
|
+
)
|
|
1017
|
+
in deployed
|
|
1018
|
+
or skill.get("name", skill.get("skill_id")) in deployed
|
|
1019
|
+
for skill in pattern_skills
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
# Add pattern group header as selectable choice
|
|
1023
|
+
if pattern:
|
|
1024
|
+
# Named pattern group
|
|
1025
|
+
pattern_icon = self._get_pattern_icon(pattern)
|
|
1026
|
+
skill_count = len(pattern_skills)
|
|
1027
|
+
group_key = f"__group__:{pattern}"
|
|
1028
|
+
group_to_skills[group_key] = skill_ids_in_group
|
|
1029
|
+
|
|
1030
|
+
skill_choices.append(
|
|
1031
|
+
Choice(
|
|
1032
|
+
title=f"{pattern_icon} {pattern} ({skill_count} skills) [Select All]",
|
|
1033
|
+
value=group_key,
|
|
1034
|
+
checked=all_installed,
|
|
1035
|
+
)
|
|
1036
|
+
)
|
|
1037
|
+
elif pattern_skills:
|
|
1038
|
+
# "Other" group - only show if there are skills
|
|
1039
|
+
group_key = "__group__:Other"
|
|
1040
|
+
group_to_skills[group_key] = skill_ids_in_group
|
|
1041
|
+
|
|
1042
|
+
skill_choices.append(
|
|
1043
|
+
Choice(
|
|
1044
|
+
title=f"📦 Other ({len(pattern_skills)} skills) [Select All]",
|
|
1045
|
+
value=group_key,
|
|
1046
|
+
checked=all_installed,
|
|
1047
|
+
)
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
# Add skills in this pattern group
|
|
1051
|
+
for skill in sorted(pattern_skills, key=lambda x: x.get("name", "")):
|
|
1052
|
+
skill_id = skill.get("name", skill.get("skill_id", "unknown"))
|
|
1053
|
+
deploy_name = skill.get("deployment_name", skill_id)
|
|
1054
|
+
description = skill.get("description", "")[:50]
|
|
1055
|
+
|
|
1056
|
+
# Check if installed
|
|
1057
|
+
is_installed = deploy_name in deployed or skill_id in deployed
|
|
1058
|
+
|
|
1059
|
+
# Add indentation for pattern-grouped skills (all skills are indented)
|
|
1060
|
+
skill_choices.append(
|
|
1061
|
+
Choice(
|
|
1062
|
+
title=f" {skill_id} - {description}",
|
|
1063
|
+
value=skill_id,
|
|
1064
|
+
checked=is_installed,
|
|
1065
|
+
)
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
# Add spacing between pattern groups (not after last group)
|
|
1069
|
+
if pattern != sorted_patterns[-1]:
|
|
1070
|
+
skill_choices.append(Separator())
|
|
1071
|
+
|
|
1072
|
+
self.console.clear()
|
|
1073
|
+
self._display_header()
|
|
1074
|
+
self.console.print(
|
|
1075
|
+
f"\n{icons.get(selected_cat, '📦')} [bold]{selected_cat.title()}[/bold]"
|
|
1076
|
+
)
|
|
1077
|
+
self.console.print(
|
|
1078
|
+
"[dim]Use spacebar to toggle individual skills or entire groups, enter to confirm[/dim]\n"
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
selected = questionary.checkbox(
|
|
1082
|
+
"Select skills to install:",
|
|
1083
|
+
choices=skill_choices,
|
|
1084
|
+
style=self.QUESTIONARY_STYLE,
|
|
1085
|
+
).ask()
|
|
1086
|
+
|
|
1087
|
+
if selected is None:
|
|
1088
|
+
continue # User cancelled, go back to category selection
|
|
1089
|
+
|
|
1090
|
+
# Process group selections - expand to individual skills
|
|
1091
|
+
selected_set = set()
|
|
1092
|
+
for item in selected:
|
|
1093
|
+
if item.startswith("__group__:"):
|
|
1094
|
+
# Expand group selection to all skills in that group
|
|
1095
|
+
selected_set.update(group_to_skills[item])
|
|
1096
|
+
else:
|
|
1097
|
+
# Individual skill selection
|
|
1098
|
+
selected_set.add(item)
|
|
1099
|
+
|
|
1100
|
+
current_in_cat = set()
|
|
1101
|
+
|
|
1102
|
+
# Find currently installed skills in this category
|
|
1103
|
+
for skill in category_skills:
|
|
1104
|
+
skill_id = skill.get("name", skill.get("skill_id", "unknown"))
|
|
1105
|
+
deploy_name = skill.get("deployment_name", skill_id)
|
|
1106
|
+
if deploy_name in deployed or skill_id in deployed:
|
|
1107
|
+
current_in_cat.add(skill_id)
|
|
1108
|
+
|
|
1109
|
+
# Install newly selected
|
|
1110
|
+
to_install = selected_set - current_in_cat
|
|
1111
|
+
for skill_id in to_install:
|
|
1112
|
+
skill = next(
|
|
1113
|
+
(
|
|
1114
|
+
s
|
|
1115
|
+
for s in category_skills
|
|
1116
|
+
if s.get("name") == skill_id or s.get("skill_id") == skill_id
|
|
1117
|
+
),
|
|
1118
|
+
None,
|
|
1119
|
+
)
|
|
1120
|
+
if skill:
|
|
1121
|
+
self._install_skill_from_dict(skill)
|
|
1122
|
+
self.console.print(f"[green]✓ Installed {skill_id}[/green]")
|
|
1123
|
+
|
|
1124
|
+
# Uninstall deselected
|
|
1125
|
+
to_uninstall = current_in_cat - selected_set
|
|
1126
|
+
for skill_id in to_uninstall:
|
|
1127
|
+
# Find the skill to get deployment_name
|
|
1128
|
+
skill = next(
|
|
1129
|
+
(
|
|
1130
|
+
s
|
|
1131
|
+
for s in category_skills
|
|
1132
|
+
if s.get("name") == skill_id or s.get("skill_id") == skill_id
|
|
1133
|
+
),
|
|
1134
|
+
None,
|
|
1135
|
+
)
|
|
1136
|
+
if skill:
|
|
1137
|
+
deploy_name = skill.get("deployment_name", skill_id)
|
|
1138
|
+
# Use the name that's actually in deployed set
|
|
1139
|
+
name_to_uninstall = (
|
|
1140
|
+
deploy_name if deploy_name in deployed else skill_id
|
|
1141
|
+
)
|
|
1142
|
+
self._uninstall_skill_by_name(name_to_uninstall)
|
|
1143
|
+
self.console.print(f"[yellow]✗ Uninstalled {skill_id}[/yellow]")
|
|
1144
|
+
|
|
1145
|
+
# Update deployed set for next iteration
|
|
1146
|
+
deployed = self._get_deployed_skill_ids()
|
|
1147
|
+
|
|
1148
|
+
# Show completion message
|
|
1149
|
+
if to_install or to_uninstall:
|
|
1150
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1151
|
+
|
|
1152
|
+
def _get_all_skills_from_git(self) -> list:
|
|
1153
|
+
"""Get all skills from Git-based skill manager.
|
|
1154
|
+
|
|
1155
|
+
Returns:
|
|
1156
|
+
List of skill dicts with full metadata from GitSkillSourceManager.
|
|
1157
|
+
"""
|
|
1158
|
+
from ...config.skill_sources import SkillSourceConfiguration
|
|
1159
|
+
from ...services.skills.git_skill_source_manager import GitSkillSourceManager
|
|
1160
|
+
|
|
1161
|
+
try:
|
|
1162
|
+
config = SkillSourceConfiguration()
|
|
1163
|
+
manager = GitSkillSourceManager(config)
|
|
1164
|
+
return manager.get_all_skills()
|
|
1165
|
+
except Exception as e:
|
|
1166
|
+
self.console.print(
|
|
1167
|
+
f"[yellow]Warning: Could not load Git skills: {e}[/yellow]"
|
|
1168
|
+
)
|
|
1169
|
+
return []
|
|
1170
|
+
|
|
1171
|
+
def _display_skills_table_grouped(self) -> None:
|
|
1172
|
+
"""Display skills in a table grouped by category, like agents."""
|
|
1173
|
+
from rich import box
|
|
1174
|
+
from rich.table import Table
|
|
1175
|
+
|
|
1176
|
+
# Get all skills from Git manager
|
|
1177
|
+
all_skills = self._get_all_skills_from_git()
|
|
1178
|
+
deployed_ids = self._get_deployed_skill_ids()
|
|
1179
|
+
|
|
1180
|
+
if not all_skills:
|
|
1181
|
+
self.console.print(
|
|
1182
|
+
"[yellow]No skills available. Try syncing skills first.[/yellow]"
|
|
1183
|
+
)
|
|
1184
|
+
return
|
|
1185
|
+
|
|
1186
|
+
# Group skills by category/toolchain
|
|
1187
|
+
grouped = {}
|
|
1188
|
+
for skill in all_skills:
|
|
1189
|
+
# Try to get category from tags or use toolchain
|
|
1190
|
+
category = None
|
|
1191
|
+
tags = skill.get("tags", [])
|
|
1192
|
+
|
|
1193
|
+
# Look for category tag
|
|
1194
|
+
for tag in tags:
|
|
1195
|
+
if tag in [
|
|
1196
|
+
"universal",
|
|
1197
|
+
"python",
|
|
1198
|
+
"typescript",
|
|
1199
|
+
"javascript",
|
|
1200
|
+
"go",
|
|
1201
|
+
"rust",
|
|
1202
|
+
]:
|
|
1203
|
+
category = tag
|
|
1204
|
+
break
|
|
1205
|
+
|
|
1206
|
+
# Fallback to toolchain or universal
|
|
1207
|
+
if not category:
|
|
1208
|
+
category = skill.get("toolchain", "universal")
|
|
1209
|
+
|
|
1210
|
+
if category not in grouped:
|
|
1211
|
+
grouped[category] = []
|
|
1212
|
+
grouped[category].append(skill)
|
|
1213
|
+
|
|
1214
|
+
# Sort categories: universal first, then alphabetically
|
|
1215
|
+
categories = sorted(grouped.keys(), key=lambda x: (x != "universal", x))
|
|
1216
|
+
|
|
1217
|
+
# Track global skill number across all categories
|
|
1218
|
+
skill_counter = 0
|
|
1219
|
+
|
|
1220
|
+
for category in categories:
|
|
1221
|
+
category_skills = grouped[category]
|
|
1222
|
+
|
|
1223
|
+
# Category header with icon
|
|
1224
|
+
icons = {
|
|
1225
|
+
"universal": "🌐",
|
|
1226
|
+
"python": "🐍",
|
|
1227
|
+
"typescript": "📘",
|
|
1228
|
+
"javascript": "📒",
|
|
1229
|
+
"go": "🔷",
|
|
1230
|
+
"rust": "⚙️",
|
|
1231
|
+
}
|
|
1232
|
+
icon = icons.get(category, "📦")
|
|
1233
|
+
self.console.print(
|
|
1234
|
+
f"\n{icon} [bold cyan]{category.title()}[/bold cyan] ({len(category_skills)} skills)"
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
# Create table for this category
|
|
1238
|
+
table = Table(show_header=True, header_style="bold", box=box.SIMPLE)
|
|
1239
|
+
table.add_column("#", style="dim", width=4)
|
|
1240
|
+
table.add_column("Skill ID", style="cyan", width=35)
|
|
1241
|
+
table.add_column("Description", style="white", width=45)
|
|
1242
|
+
table.add_column("Status", style="green", width=12)
|
|
1243
|
+
|
|
1244
|
+
for skill in sorted(category_skills, key=lambda x: x.get("name", "")):
|
|
1245
|
+
skill_counter += 1
|
|
1246
|
+
skill_id = skill.get("name", skill.get("skill_id", "unknown"))
|
|
1247
|
+
# Use deployment_name for matching if available
|
|
1248
|
+
deploy_name = skill.get("deployment_name", skill_id)
|
|
1249
|
+
description = skill.get("description", "")[:45]
|
|
1250
|
+
|
|
1251
|
+
# Check if installed - handle both deployment_name and skill_id
|
|
1252
|
+
is_installed = deploy_name in deployed_ids or skill_id in deployed_ids
|
|
1253
|
+
status = "[green]✓ Installed[/green]" if is_installed else "Available"
|
|
1254
|
+
|
|
1255
|
+
table.add_row(str(skill_counter), skill_id, description, status)
|
|
1256
|
+
|
|
1257
|
+
self.console.print(table)
|
|
1258
|
+
|
|
1259
|
+
# Summary
|
|
1260
|
+
total = len(all_skills)
|
|
1261
|
+
installed = sum(
|
|
1262
|
+
1
|
|
1263
|
+
for s in all_skills
|
|
1264
|
+
if s.get("deployment_name", s.get("name", "")) in deployed_ids
|
|
1265
|
+
or s.get("name", "") in deployed_ids
|
|
1266
|
+
)
|
|
1267
|
+
self.console.print(
|
|
1268
|
+
f"\n[dim]Showing {total} skills ({installed} installed)[/dim]"
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
def _get_deployed_skill_ids(self) -> set:
|
|
1272
|
+
"""Get set of deployed skill IDs from .claude/skills/ directory.
|
|
1273
|
+
|
|
1274
|
+
Returns:
|
|
1275
|
+
Set of skill directory names and common variations for matching.
|
|
1276
|
+
"""
|
|
1277
|
+
from pathlib import Path
|
|
1278
|
+
|
|
1279
|
+
skills_dir = Path.cwd() / ".claude" / "skills"
|
|
1280
|
+
if not skills_dir.exists():
|
|
1281
|
+
return set()
|
|
1282
|
+
|
|
1283
|
+
# Each deployed skill is a directory in .claude/skills/
|
|
1284
|
+
deployed_ids = set()
|
|
1285
|
+
for skill_dir in skills_dir.iterdir():
|
|
1286
|
+
if skill_dir.is_dir() and not skill_dir.name.startswith("."):
|
|
1287
|
+
# Add both the directory name and common variations
|
|
1288
|
+
deployed_ids.add(skill_dir.name)
|
|
1289
|
+
# Also add without prefix for matching (e.g., universal-testing -> testing)
|
|
1290
|
+
if skill_dir.name.startswith("universal-"):
|
|
1291
|
+
deployed_ids.add(skill_dir.name.replace("universal-", "", 1))
|
|
1292
|
+
|
|
1293
|
+
return deployed_ids
|
|
1294
|
+
|
|
1295
|
+
def _install_skill(self, skill) -> None:
|
|
1296
|
+
"""Install a skill to .claude/skills/ directory."""
|
|
1297
|
+
import shutil
|
|
1298
|
+
from pathlib import Path
|
|
1299
|
+
|
|
1300
|
+
# Target directory
|
|
1301
|
+
target_dir = Path.cwd() / ".claude" / "skills" / skill.skill_id
|
|
1302
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
1303
|
+
|
|
1304
|
+
# Copy skill file(s)
|
|
1305
|
+
if skill.path.is_file():
|
|
1306
|
+
# Single file skill - copy to skill.md in target directory
|
|
1307
|
+
shutil.copy2(skill.path, target_dir / "skill.md")
|
|
1308
|
+
elif skill.path.is_dir():
|
|
1309
|
+
# Directory-based skill - copy all contents
|
|
1310
|
+
for item in skill.path.iterdir():
|
|
1311
|
+
if item.is_file():
|
|
1312
|
+
shutil.copy2(item, target_dir / item.name)
|
|
1313
|
+
elif item.is_dir():
|
|
1314
|
+
shutil.copytree(item, target_dir / item.name, dirs_exist_ok=True)
|
|
1315
|
+
|
|
1316
|
+
def _uninstall_skill(self, skill) -> None:
|
|
1317
|
+
"""Uninstall a skill from .claude/skills/ directory."""
|
|
1318
|
+
import shutil
|
|
1319
|
+
from pathlib import Path
|
|
1320
|
+
|
|
1321
|
+
target_dir = Path.cwd() / ".claude" / "skills" / skill.skill_id
|
|
1322
|
+
if target_dir.exists():
|
|
1323
|
+
shutil.rmtree(target_dir)
|
|
1324
|
+
|
|
1325
|
+
def _install_skill_from_dict(self, skill_dict: dict) -> None:
|
|
1326
|
+
"""Install a skill from Git skill dict to .claude/skills/ directory.
|
|
1327
|
+
|
|
1328
|
+
Args:
|
|
1329
|
+
skill_dict: Skill metadata dict from GitSkillSourceManager.get_all_skills()
|
|
1330
|
+
"""
|
|
1331
|
+
from pathlib import Path
|
|
1332
|
+
|
|
1333
|
+
skill_id = skill_dict.get("name", skill_dict.get("skill_id", "unknown"))
|
|
1334
|
+
content = skill_dict.get("content", "")
|
|
1335
|
+
|
|
1336
|
+
if not content:
|
|
1337
|
+
self.console.print(
|
|
1338
|
+
f"[yellow]Warning: Skill '{skill_id}' has no content[/yellow]"
|
|
1339
|
+
)
|
|
1340
|
+
return
|
|
1341
|
+
|
|
1342
|
+
# Target directory using deployment_name if available
|
|
1343
|
+
deploy_name = skill_dict.get("deployment_name", skill_id)
|
|
1344
|
+
target_dir = Path.cwd() / ".claude" / "skills" / deploy_name
|
|
1345
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
1346
|
+
|
|
1347
|
+
# Write skill content to skill.md
|
|
1348
|
+
skill_file = target_dir / "skill.md"
|
|
1349
|
+
skill_file.write_text(content, encoding="utf-8")
|
|
1350
|
+
|
|
1351
|
+
def _uninstall_skill_by_name(self, skill_name: str) -> None:
|
|
1352
|
+
"""Uninstall a skill by name from .claude/skills/ directory.
|
|
1353
|
+
|
|
1354
|
+
Args:
|
|
1355
|
+
skill_name: Name of skill directory to remove
|
|
1356
|
+
"""
|
|
1357
|
+
import shutil
|
|
1358
|
+
from pathlib import Path
|
|
1359
|
+
|
|
1360
|
+
target_dir = Path.cwd() / ".claude" / "skills" / skill_name
|
|
1361
|
+
if target_dir.exists():
|
|
1362
|
+
shutil.rmtree(target_dir)
|
|
1363
|
+
|
|
1364
|
+
def _display_behavior_files(self) -> None:
|
|
1365
|
+
"""Display current behavior files."""
|
|
1366
|
+
self.behavior_manager.display_behavior_files()
|
|
1367
|
+
|
|
1368
|
+
def _edit_identity_config(self) -> None:
|
|
1369
|
+
"""Edit identity configuration."""
|
|
1370
|
+
self.behavior_manager.edit_identity_config()
|
|
1371
|
+
|
|
1372
|
+
def _edit_workflow_config(self) -> None:
|
|
1373
|
+
"""Edit workflow configuration."""
|
|
1374
|
+
self.behavior_manager.edit_workflow_config()
|
|
1375
|
+
|
|
1376
|
+
def _import_behavior_file(self) -> None:
|
|
1377
|
+
"""Import a behavior file."""
|
|
1378
|
+
self.behavior_manager.import_behavior_file()
|
|
1379
|
+
|
|
1380
|
+
def _export_behavior_file(self) -> None:
|
|
1381
|
+
"""Export a behavior file."""
|
|
1382
|
+
self.behavior_manager.export_behavior_file()
|
|
1383
|
+
|
|
1384
|
+
def _manage_startup_configuration(self) -> bool:
|
|
1385
|
+
"""Manage startup configuration for MCP services and agents."""
|
|
1386
|
+
return self.startup_manager.manage_startup_configuration()
|
|
1387
|
+
|
|
1388
|
+
def _load_startup_configuration(self, config: Config) -> Dict:
|
|
1389
|
+
"""Load current startup configuration from config."""
|
|
1390
|
+
return self.startup_manager.load_startup_configuration(config)
|
|
1391
|
+
|
|
1392
|
+
def _display_startup_configuration(self, startup_config: Dict) -> None:
|
|
1393
|
+
"""Display current startup configuration in a table."""
|
|
1394
|
+
self.startup_manager.display_startup_configuration(startup_config)
|
|
1395
|
+
|
|
1396
|
+
def _configure_mcp_services(self, startup_config: Dict, config: Config) -> None:
|
|
1397
|
+
"""Configure which MCP services to enable at startup."""
|
|
1398
|
+
self.startup_manager.configure_mcp_services(startup_config, config)
|
|
1399
|
+
|
|
1400
|
+
def _configure_hook_services(self, startup_config: Dict, config: Config) -> None:
|
|
1401
|
+
"""Configure which hook services to enable at startup."""
|
|
1402
|
+
self.startup_manager.configure_hook_services(startup_config, config)
|
|
1403
|
+
|
|
1404
|
+
def _configure_system_agents(self, startup_config: Dict, config: Config) -> None:
|
|
1405
|
+
"""Configure which system agents to deploy at startup."""
|
|
1406
|
+
self.startup_manager.configure_system_agents(startup_config, config)
|
|
1407
|
+
|
|
1408
|
+
def _parse_id_selection(self, selection: str, max_id: int) -> List[int]:
|
|
1409
|
+
"""Parse ID selection string (e.g., '1,3,5' or '1-4')."""
|
|
1410
|
+
return parse_id_selection(selection, max_id)
|
|
1411
|
+
|
|
1412
|
+
def _enable_all_services(self, startup_config: Dict, config: Config) -> None:
|
|
1413
|
+
"""Enable all services and agents."""
|
|
1414
|
+
self.startup_manager.enable_all_services(startup_config, config)
|
|
1415
|
+
|
|
1416
|
+
def _disable_all_services(self, startup_config: Dict, config: Config) -> None:
|
|
1417
|
+
"""Disable all services and agents."""
|
|
1418
|
+
self.startup_manager.disable_all_services(startup_config, config)
|
|
1419
|
+
|
|
1420
|
+
def _reset_to_defaults(self, startup_config: Dict, config: Config) -> None:
|
|
1421
|
+
"""Reset startup configuration to defaults."""
|
|
1422
|
+
self.startup_manager.reset_to_defaults(startup_config, config)
|
|
1423
|
+
|
|
1424
|
+
def _save_startup_configuration(self, startup_config: Dict, config: Config) -> bool:
|
|
1425
|
+
"""Save startup configuration to config file and return whether to proceed to startup."""
|
|
1426
|
+
return self.startup_manager.save_startup_configuration(startup_config, config)
|
|
1427
|
+
|
|
1428
|
+
def _save_all_configuration(self) -> bool:
|
|
1429
|
+
"""Save all configuration changes across all contexts."""
|
|
1430
|
+
return self.startup_manager.save_all_configuration()
|
|
1431
|
+
|
|
1432
|
+
def _launch_claude_mpm(self) -> None:
|
|
1433
|
+
"""Launch Claude MPM run command, replacing current process."""
|
|
1434
|
+
self.navigation.launch_claude_mpm()
|
|
1435
|
+
|
|
1436
|
+
def _switch_scope(self) -> None:
|
|
1437
|
+
"""Switch between project and user scope."""
|
|
1438
|
+
self.navigation.switch_scope()
|
|
1439
|
+
# Sync scope back from navigation
|
|
1440
|
+
self.current_scope = self.navigation.current_scope
|
|
1441
|
+
|
|
1442
|
+
def _show_version_info_interactive(self) -> None:
|
|
1443
|
+
"""Show version information in interactive mode."""
|
|
1444
|
+
self.persistence.show_version_info_interactive()
|
|
1445
|
+
|
|
1446
|
+
# Non-interactive command methods
|
|
1447
|
+
|
|
1448
|
+
def _list_agents_non_interactive(self) -> CommandResult:
|
|
1449
|
+
"""List agents in non-interactive mode."""
|
|
1450
|
+
agents = self.agent_manager.discover_agents()
|
|
1451
|
+
# Filter BASE_AGENT from all agent lists (1M-502 Phase 1)
|
|
1452
|
+
agents = self._filter_agent_configs(agents, filter_deployed=False)
|
|
1453
|
+
|
|
1454
|
+
data = []
|
|
1455
|
+
for agent in agents:
|
|
1456
|
+
data.append(
|
|
1457
|
+
{
|
|
1458
|
+
"name": agent.name,
|
|
1459
|
+
"enabled": self.agent_manager.is_agent_enabled(agent.name),
|
|
1460
|
+
"description": agent.description,
|
|
1461
|
+
"dependencies": agent.dependencies,
|
|
1462
|
+
}
|
|
1463
|
+
)
|
|
1464
|
+
|
|
1465
|
+
# Print as JSON for scripting
|
|
1466
|
+
print(json.dumps(data, indent=2))
|
|
1467
|
+
|
|
1468
|
+
return CommandResult.success_result("Agents listed", data={"agents": data})
|
|
1469
|
+
|
|
1470
|
+
def _enable_agent_non_interactive(self, agent_name: str) -> CommandResult:
|
|
1471
|
+
"""Enable an agent in non-interactive mode."""
|
|
1472
|
+
try:
|
|
1473
|
+
self.agent_manager.set_agent_enabled(agent_name, True)
|
|
1474
|
+
return CommandResult.success_result(f"Agent '{agent_name}' enabled")
|
|
1475
|
+
except Exception as e:
|
|
1476
|
+
return CommandResult.error_result(f"Failed to enable agent: {e}")
|
|
1477
|
+
|
|
1478
|
+
def _disable_agent_non_interactive(self, agent_name: str) -> CommandResult:
|
|
1479
|
+
"""Disable an agent in non-interactive mode."""
|
|
1480
|
+
try:
|
|
1481
|
+
self.agent_manager.set_agent_enabled(agent_name, False)
|
|
1482
|
+
return CommandResult.success_result(f"Agent '{agent_name}' disabled")
|
|
1483
|
+
except Exception as e:
|
|
1484
|
+
return CommandResult.error_result(f"Failed to disable agent: {e}")
|
|
1485
|
+
|
|
1486
|
+
def _export_config(self, file_path: str) -> CommandResult:
|
|
1487
|
+
"""Export configuration to a file."""
|
|
1488
|
+
return self.persistence.export_config(file_path)
|
|
1489
|
+
|
|
1490
|
+
def _import_config(self, file_path: str) -> CommandResult:
|
|
1491
|
+
"""Import configuration from a file."""
|
|
1492
|
+
return self.persistence.import_config(file_path)
|
|
1493
|
+
|
|
1494
|
+
def _show_version_info(self) -> CommandResult:
|
|
1495
|
+
"""Show version information in non-interactive mode."""
|
|
1496
|
+
return self.persistence.show_version_info()
|
|
1497
|
+
|
|
1498
|
+
def _install_hooks(self, force: bool = False) -> CommandResult:
|
|
1499
|
+
"""Install Claude MPM hooks for Claude Code integration."""
|
|
1500
|
+
# Share logger with hook manager for consistent error logging
|
|
1501
|
+
self.hook_manager.logger = self.logger
|
|
1502
|
+
return self.hook_manager.install_hooks(force=force)
|
|
1503
|
+
|
|
1504
|
+
def _verify_hooks(self) -> CommandResult:
|
|
1505
|
+
"""Verify that Claude MPM hooks are properly installed."""
|
|
1506
|
+
# Share logger with hook manager for consistent error logging
|
|
1507
|
+
self.hook_manager.logger = self.logger
|
|
1508
|
+
return self.hook_manager.verify_hooks()
|
|
1509
|
+
|
|
1510
|
+
def _uninstall_hooks(self) -> CommandResult:
|
|
1511
|
+
"""Uninstall Claude MPM hooks."""
|
|
1512
|
+
# Share logger with hook manager for consistent error logging
|
|
1513
|
+
self.hook_manager.logger = self.logger
|
|
1514
|
+
return self.hook_manager.uninstall_hooks()
|
|
1515
|
+
|
|
1516
|
+
def _run_agent_management(self) -> CommandResult:
|
|
1517
|
+
"""Jump directly to agent management."""
|
|
1518
|
+
try:
|
|
1519
|
+
self._manage_agents()
|
|
1520
|
+
return CommandResult.success_result("Agent management completed")
|
|
1521
|
+
except KeyboardInterrupt:
|
|
1522
|
+
return CommandResult.success_result("Agent management cancelled")
|
|
1523
|
+
except Exception as e:
|
|
1524
|
+
return CommandResult.error_result(f"Agent management failed: {e}")
|
|
1525
|
+
|
|
1526
|
+
def _run_template_editing(self) -> CommandResult:
|
|
1527
|
+
"""Jump directly to template editing."""
|
|
1528
|
+
try:
|
|
1529
|
+
self._edit_templates()
|
|
1530
|
+
return CommandResult.success_result("Template editing completed")
|
|
1531
|
+
except KeyboardInterrupt:
|
|
1532
|
+
return CommandResult.success_result("Template editing cancelled")
|
|
1533
|
+
except Exception as e:
|
|
1534
|
+
return CommandResult.error_result(f"Template editing failed: {e}")
|
|
1535
|
+
|
|
1536
|
+
def _run_behavior_management(self) -> CommandResult:
|
|
1537
|
+
"""Jump directly to behavior management."""
|
|
1538
|
+
return self.behavior_manager.run_behavior_management()
|
|
1539
|
+
|
|
1540
|
+
def _run_startup_configuration(self) -> CommandResult:
|
|
1541
|
+
"""Jump directly to startup configuration."""
|
|
1542
|
+
try:
|
|
1543
|
+
proceed = self._manage_startup_configuration()
|
|
1544
|
+
if proceed:
|
|
1545
|
+
return CommandResult.success_result(
|
|
1546
|
+
"Startup configuration saved, proceeding to startup"
|
|
1547
|
+
)
|
|
1548
|
+
return CommandResult.success_result("Startup configuration completed")
|
|
1549
|
+
except KeyboardInterrupt:
|
|
1550
|
+
return CommandResult.success_result("Startup configuration cancelled")
|
|
1551
|
+
except Exception as e:
|
|
1552
|
+
return CommandResult.error_result(f"Startup configuration failed: {e}")
|
|
1553
|
+
|
|
1554
|
+
# ========================================================================
|
|
1555
|
+
# Enhanced Agent Management Methods (Remote Agent Discovery Integration)
|
|
1556
|
+
# ========================================================================
|
|
1557
|
+
|
|
1558
|
+
def _get_configured_sources(self) -> List[Dict]:
|
|
1559
|
+
"""Get list of configured agent sources with agent counts."""
|
|
1560
|
+
try:
|
|
1561
|
+
from claude_mpm.config.agent_sources import AgentSourceConfiguration
|
|
1562
|
+
|
|
1563
|
+
config = AgentSourceConfiguration.load()
|
|
1564
|
+
|
|
1565
|
+
# Convert repositories to source dictionaries
|
|
1566
|
+
sources = []
|
|
1567
|
+
for repo in config.repositories:
|
|
1568
|
+
# Extract identifier from repository
|
|
1569
|
+
identifier = repo.identifier
|
|
1570
|
+
|
|
1571
|
+
# Count agents in cache
|
|
1572
|
+
# Note: identifier already includes subdirectory path (e.g., "bobmatnyc/claude-mpm-agents/agents")
|
|
1573
|
+
cache_dir = (
|
|
1574
|
+
Path.home() / ".claude-mpm" / "cache" / "agents" / identifier
|
|
1575
|
+
)
|
|
1576
|
+
agent_count = 0
|
|
1577
|
+
if cache_dir.exists():
|
|
1578
|
+
# cache_dir IS the agents directory - no need to append /agents
|
|
1579
|
+
agent_count = len(list(cache_dir.rglob("*.md")))
|
|
1580
|
+
|
|
1581
|
+
sources.append(
|
|
1582
|
+
{
|
|
1583
|
+
"identifier": identifier,
|
|
1584
|
+
"url": repo.url,
|
|
1585
|
+
"enabled": repo.enabled,
|
|
1586
|
+
"priority": repo.priority,
|
|
1587
|
+
"agent_count": agent_count,
|
|
1588
|
+
}
|
|
1589
|
+
)
|
|
1590
|
+
|
|
1591
|
+
return sources
|
|
1592
|
+
except Exception as e:
|
|
1593
|
+
self.logger.warning(f"Failed to get configured sources: {e}")
|
|
1594
|
+
return []
|
|
1595
|
+
|
|
1596
|
+
def _filter_agent_configs(
|
|
1597
|
+
self, agents: List[AgentConfig], filter_deployed: bool = False
|
|
1598
|
+
) -> List[AgentConfig]:
|
|
1599
|
+
"""Filter AgentConfig objects using agent_filters utilities.
|
|
1600
|
+
|
|
1601
|
+
Converts AgentConfig objects to dictionaries for filtering,
|
|
1602
|
+
then back to AgentConfig. Always filters BASE_AGENT.
|
|
1603
|
+
Optionally filters deployed agents.
|
|
1604
|
+
|
|
1605
|
+
Args:
|
|
1606
|
+
agents: List of AgentConfig objects
|
|
1607
|
+
filter_deployed: Whether to filter out deployed agents (default: False)
|
|
1608
|
+
|
|
1609
|
+
Returns:
|
|
1610
|
+
Filtered list of AgentConfig objects
|
|
1611
|
+
"""
|
|
1612
|
+
# Convert AgentConfig to dict format for filtering
|
|
1613
|
+
agent_dicts = []
|
|
1614
|
+
for agent in agents:
|
|
1615
|
+
agent_dicts.append(
|
|
1616
|
+
{
|
|
1617
|
+
"agent_id": agent.name,
|
|
1618
|
+
"name": agent.name,
|
|
1619
|
+
"description": agent.description,
|
|
1620
|
+
"deployed": getattr(agent, "is_deployed", False),
|
|
1621
|
+
}
|
|
1622
|
+
)
|
|
1623
|
+
|
|
1624
|
+
# Apply filters (always filter BASE_AGENT)
|
|
1625
|
+
filtered_dicts = apply_all_filters(
|
|
1626
|
+
agent_dicts, filter_base=True, filter_deployed=filter_deployed
|
|
1627
|
+
)
|
|
1628
|
+
|
|
1629
|
+
# Convert back to AgentConfig objects
|
|
1630
|
+
filtered_names = {d["agent_id"] for d in filtered_dicts}
|
|
1631
|
+
return [a for a in agents if a.name in filtered_names]
|
|
1632
|
+
|
|
1633
|
+
@staticmethod
|
|
1634
|
+
def _calculate_column_widths(
|
|
1635
|
+
terminal_width: int, columns: Dict[str, int]
|
|
1636
|
+
) -> Dict[str, int]:
|
|
1637
|
+
"""Calculate dynamic column widths based on terminal size.
|
|
1638
|
+
|
|
1639
|
+
Args:
|
|
1640
|
+
terminal_width: Current terminal width in characters
|
|
1641
|
+
columns: Dict mapping column names to minimum widths
|
|
1642
|
+
|
|
1643
|
+
Returns:
|
|
1644
|
+
Dict mapping column names to calculated widths
|
|
1645
|
+
|
|
1646
|
+
Design:
|
|
1647
|
+
- Ensures minimum widths are respected
|
|
1648
|
+
- Distributes extra space proportionally
|
|
1649
|
+
- Handles narrow terminals gracefully (minimum 80 chars)
|
|
1650
|
+
"""
|
|
1651
|
+
# Ensure minimum terminal width
|
|
1652
|
+
min_terminal_width = 80
|
|
1653
|
+
terminal_width = max(terminal_width, min_terminal_width)
|
|
1654
|
+
|
|
1655
|
+
# Calculate total minimum width needed
|
|
1656
|
+
total_min_width = sum(columns.values())
|
|
1657
|
+
|
|
1658
|
+
# Account for table borders and padding (2 chars per column + 2 for edges)
|
|
1659
|
+
overhead = (len(columns) * 2) + 2
|
|
1660
|
+
available_width = terminal_width - overhead
|
|
1661
|
+
|
|
1662
|
+
# If we have extra space, distribute proportionally
|
|
1663
|
+
if available_width > total_min_width:
|
|
1664
|
+
extra_space = available_width - total_min_width
|
|
1665
|
+
total_weight = sum(columns.values())
|
|
1666
|
+
|
|
1667
|
+
result = {}
|
|
1668
|
+
for col_name, min_width in columns.items():
|
|
1669
|
+
# Distribute extra space based on minimum width proportion
|
|
1670
|
+
proportion = min_width / total_weight
|
|
1671
|
+
extra = int(extra_space * proportion)
|
|
1672
|
+
result[col_name] = min_width + extra
|
|
1673
|
+
return result
|
|
1674
|
+
# Terminal too narrow, use minimum widths
|
|
1675
|
+
return columns.copy()
|
|
1676
|
+
|
|
1677
|
+
def _format_display_name(self, name: str) -> str:
|
|
1678
|
+
"""Format internal agent name to human-readable display name.
|
|
1679
|
+
|
|
1680
|
+
Converts underscores/hyphens to spaces and title-cases.
|
|
1681
|
+
Examples:
|
|
1682
|
+
agentic_coder_optimizer -> Agentic Coder Optimizer
|
|
1683
|
+
python-engineer -> Python Engineer
|
|
1684
|
+
api_qa_agent -> Api Qa Agent
|
|
1685
|
+
|
|
1686
|
+
Args:
|
|
1687
|
+
name: Internal agent name (may contain underscores, hyphens)
|
|
1688
|
+
|
|
1689
|
+
Returns:
|
|
1690
|
+
Human-readable display name
|
|
1691
|
+
"""
|
|
1692
|
+
return name.replace("_", " ").replace("-", " ").title()
|
|
1693
|
+
|
|
1694
|
+
def _display_agents_with_source_info(self, agents: List[AgentConfig]) -> None:
|
|
1695
|
+
"""Display agents table with source information and installation status."""
|
|
1696
|
+
from rich.table import Table
|
|
1697
|
+
|
|
1698
|
+
# Get recommended agents for this project
|
|
1699
|
+
try:
|
|
1700
|
+
recommended_agents = self.recommendation_service.get_recommended_agents(
|
|
1701
|
+
str(self.project_dir)
|
|
1702
|
+
)
|
|
1703
|
+
except Exception as e:
|
|
1704
|
+
self.logger.warning(f"Failed to get recommended agents: {e}")
|
|
1705
|
+
recommended_agents = set()
|
|
1706
|
+
|
|
1707
|
+
# Get terminal width and calculate dynamic column widths
|
|
1708
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
1709
|
+
min_widths = {
|
|
1710
|
+
"#": 4,
|
|
1711
|
+
"Agent ID": 30,
|
|
1712
|
+
"Name": 20,
|
|
1713
|
+
"Source": 15,
|
|
1714
|
+
"Status": 10,
|
|
1715
|
+
}
|
|
1716
|
+
widths = self._calculate_column_widths(terminal_width, min_widths)
|
|
1717
|
+
|
|
1718
|
+
agents_table = Table(show_header=True, header_style="bold cyan")
|
|
1719
|
+
agents_table.add_column(
|
|
1720
|
+
"#", style="bright_black", width=widths["#"], no_wrap=True
|
|
1721
|
+
)
|
|
1722
|
+
agents_table.add_column(
|
|
1723
|
+
"Agent ID",
|
|
1724
|
+
style="bright_black",
|
|
1725
|
+
width=widths["Agent ID"],
|
|
1726
|
+
no_wrap=True,
|
|
1727
|
+
overflow="ellipsis",
|
|
1728
|
+
)
|
|
1729
|
+
agents_table.add_column(
|
|
1730
|
+
"Name",
|
|
1731
|
+
style="bright_cyan",
|
|
1732
|
+
width=widths["Name"],
|
|
1733
|
+
no_wrap=True,
|
|
1734
|
+
overflow="ellipsis",
|
|
1735
|
+
)
|
|
1736
|
+
agents_table.add_column(
|
|
1737
|
+
"Source",
|
|
1738
|
+
style="bright_yellow",
|
|
1739
|
+
width=widths["Source"],
|
|
1740
|
+
no_wrap=True,
|
|
1741
|
+
)
|
|
1742
|
+
agents_table.add_column(
|
|
1743
|
+
"Status", style="bright_black", width=widths["Status"], no_wrap=True
|
|
1744
|
+
)
|
|
1745
|
+
|
|
1746
|
+
# FIX 3: Get deployed agent IDs once, before the loop (efficiency)
|
|
1747
|
+
deployed_ids = get_deployed_agent_ids()
|
|
1748
|
+
|
|
1749
|
+
recommended_count = 0
|
|
1750
|
+
for idx, agent in enumerate(agents, 1):
|
|
1751
|
+
# Determine source with repo name
|
|
1752
|
+
source_type = getattr(agent, "source_type", "local")
|
|
1753
|
+
|
|
1754
|
+
if source_type == "remote":
|
|
1755
|
+
# Get repo name from agent metadata
|
|
1756
|
+
source_dict = getattr(agent, "source_dict", {})
|
|
1757
|
+
repo_url = source_dict.get("source", "")
|
|
1758
|
+
|
|
1759
|
+
# Extract repo name from URL
|
|
1760
|
+
if (
|
|
1761
|
+
"bobmatnyc/claude-mpm" in repo_url
|
|
1762
|
+
or "claude-mpm" in repo_url.lower()
|
|
1763
|
+
):
|
|
1764
|
+
source_label = "MPM Agents"
|
|
1765
|
+
elif "/" in repo_url:
|
|
1766
|
+
# Extract last part of org/repo
|
|
1767
|
+
parts = repo_url.rstrip("/").split("/")
|
|
1768
|
+
if len(parts) >= 2:
|
|
1769
|
+
source_label = f"{parts[-2]}/{parts[-1]}"
|
|
1770
|
+
else:
|
|
1771
|
+
source_label = "Community"
|
|
1772
|
+
else:
|
|
1773
|
+
source_label = "Community"
|
|
1774
|
+
else:
|
|
1775
|
+
source_label = "Local"
|
|
1776
|
+
|
|
1777
|
+
# FIX 2: Check actual deployment status from .claude/agents/ directory
|
|
1778
|
+
# Use agent_id (technical ID like "python-engineer") not display name
|
|
1779
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1780
|
+
is_installed = agent_id in deployed_ids
|
|
1781
|
+
if is_installed:
|
|
1782
|
+
status = "[green]Installed[/green]"
|
|
1783
|
+
else:
|
|
1784
|
+
status = "Available"
|
|
1785
|
+
|
|
1786
|
+
# Check if agent is recommended
|
|
1787
|
+
# Handle both hierarchical paths (e.g., "engineer/backend/python-engineer")
|
|
1788
|
+
# and leaf names (e.g., "python-engineer")
|
|
1789
|
+
agent_full_path = agent.name
|
|
1790
|
+
agent_leaf_name = (
|
|
1791
|
+
agent_full_path.split("/")[-1]
|
|
1792
|
+
if "/" in agent_full_path
|
|
1793
|
+
else agent_full_path
|
|
1794
|
+
)
|
|
1795
|
+
|
|
1796
|
+
for recommended_id in recommended_agents:
|
|
1797
|
+
# Check if the recommended_id matches either the full path or just the leaf name
|
|
1798
|
+
recommended_leaf = (
|
|
1799
|
+
recommended_id.split("/")[-1]
|
|
1800
|
+
if "/" in recommended_id
|
|
1801
|
+
else recommended_id
|
|
1802
|
+
)
|
|
1803
|
+
if (
|
|
1804
|
+
agent_full_path == recommended_id
|
|
1805
|
+
or agent_leaf_name == recommended_leaf
|
|
1806
|
+
):
|
|
1807
|
+
recommended_count += 1
|
|
1808
|
+
break
|
|
1809
|
+
|
|
1810
|
+
# FIX 1: Show agent_id (technical ID) in first column, not display name
|
|
1811
|
+
agent_id_display = getattr(agent, "agent_id", agent.name)
|
|
1812
|
+
|
|
1813
|
+
# Get display name and format it properly
|
|
1814
|
+
# Raw display_name from YAML may contain underscores (e.g., "agentic_coder_optimizer")
|
|
1815
|
+
raw_display_name = getattr(agent, "display_name", agent.name)
|
|
1816
|
+
display_name = self._format_display_name(raw_display_name)
|
|
1817
|
+
|
|
1818
|
+
agents_table.add_row(
|
|
1819
|
+
str(idx), agent_id_display, display_name, source_label, status
|
|
1820
|
+
)
|
|
1821
|
+
|
|
1822
|
+
self.console.print(agents_table)
|
|
1823
|
+
|
|
1824
|
+
# Show legend if there are recommended agents
|
|
1825
|
+
if recommended_count > 0:
|
|
1826
|
+
# Get detection summary for context
|
|
1827
|
+
try:
|
|
1828
|
+
summary = self.recommendation_service.get_detection_summary(
|
|
1829
|
+
str(self.project_dir)
|
|
1830
|
+
)
|
|
1831
|
+
detected_langs = (
|
|
1832
|
+
", ".join(summary.get("detected_languages", [])) or "None"
|
|
1833
|
+
)
|
|
1834
|
+
", ".join(summary.get("detected_frameworks", [])) or "None"
|
|
1835
|
+
self.console.print(
|
|
1836
|
+
f"\n[dim]* = recommended for this project "
|
|
1837
|
+
f"(detected: {detected_langs})[/dim]"
|
|
1838
|
+
)
|
|
1839
|
+
except Exception:
|
|
1840
|
+
self.console.print("\n[dim]* = recommended for this project[/dim]")
|
|
1841
|
+
|
|
1842
|
+
# Show installed vs available count (use deployed_ids for accuracy)
|
|
1843
|
+
# Use agent_id (technical ID) for comparison, not display name
|
|
1844
|
+
installed_count = sum(
|
|
1845
|
+
1 for a in agents if getattr(a, "agent_id", a.name) in deployed_ids
|
|
1846
|
+
)
|
|
1847
|
+
available_count = len(agents) - installed_count
|
|
1848
|
+
self.console.print(
|
|
1849
|
+
f"\n[green]✓ {installed_count} installed[/green] | "
|
|
1850
|
+
f"[dim]{available_count} available[/dim] | "
|
|
1851
|
+
f"[yellow]{recommended_count} recommended[/yellow] | "
|
|
1852
|
+
f"[dim]Total: {len(agents)}[/dim]"
|
|
1853
|
+
)
|
|
1854
|
+
|
|
1855
|
+
def _manage_sources(self) -> None:
|
|
1856
|
+
"""Interactive source management."""
|
|
1857
|
+
self.console.print("\n[bold white]═══ Manage Agent Sources ═══[/bold white]\n")
|
|
1858
|
+
self.console.print(
|
|
1859
|
+
"[dim]Use 'claude-mpm agent-source' command to add/remove sources[/dim]"
|
|
1860
|
+
)
|
|
1861
|
+
self.console.print("\nExamples:")
|
|
1862
|
+
self.console.print(" claude-mpm agent-source add <git-url>")
|
|
1863
|
+
self.console.print(" claude-mpm agent-source remove <identifier>")
|
|
1864
|
+
self.console.print(" claude-mpm agent-source list")
|
|
1865
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1866
|
+
|
|
1867
|
+
def _deploy_agents_unified(self, agents: List[AgentConfig]) -> None:
|
|
1868
|
+
"""Unified agent selection with inline controls for recommended, presets, and collections.
|
|
1869
|
+
|
|
1870
|
+
Design:
|
|
1871
|
+
- Single nested checkbox list with grouped agents by source/category
|
|
1872
|
+
- Inline controls at top: Select all, Select recommended, Select presets
|
|
1873
|
+
- Asterisk (*) marks recommended agents
|
|
1874
|
+
- Visual hierarchy: Source → Category → Individual agents
|
|
1875
|
+
- Loop with visual feedback: Controls update checkmarks immediately
|
|
1876
|
+
"""
|
|
1877
|
+
if not agents:
|
|
1878
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
1879
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1880
|
+
return
|
|
1881
|
+
|
|
1882
|
+
from claude_mpm.utils.agent_filters import (
|
|
1883
|
+
filter_base_agents,
|
|
1884
|
+
get_deployed_agent_ids,
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
# Filter BASE_AGENT but keep deployed agents visible
|
|
1888
|
+
all_agents = filter_base_agents(
|
|
1889
|
+
[
|
|
1890
|
+
{
|
|
1891
|
+
"agent_id": getattr(a, "agent_id", a.name),
|
|
1892
|
+
"name": a.name,
|
|
1893
|
+
"description": a.description,
|
|
1894
|
+
"deployed": getattr(a, "is_deployed", False),
|
|
1895
|
+
}
|
|
1896
|
+
for a in agents
|
|
1897
|
+
]
|
|
1898
|
+
)
|
|
1899
|
+
|
|
1900
|
+
if not all_agents:
|
|
1901
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
1902
|
+
Prompt.ask("\nPress Enter to continue")
|
|
1903
|
+
return
|
|
1904
|
+
|
|
1905
|
+
# Get deployed agent IDs and recommended agents
|
|
1906
|
+
deployed_ids = get_deployed_agent_ids()
|
|
1907
|
+
|
|
1908
|
+
try:
|
|
1909
|
+
recommended_agent_ids = self.recommendation_service.get_recommended_agents(
|
|
1910
|
+
str(self.project_dir)
|
|
1911
|
+
)
|
|
1912
|
+
except Exception as e:
|
|
1913
|
+
self.logger.warning(f"Failed to get recommended agents: {e}")
|
|
1914
|
+
recommended_agent_ids = set()
|
|
1915
|
+
|
|
1916
|
+
# Build mapping: leaf name -> full path for deployed agents
|
|
1917
|
+
# Use agent_id (technical ID) for comparison, not display name
|
|
1918
|
+
deployed_full_paths = set()
|
|
1919
|
+
for agent in agents:
|
|
1920
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1921
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
1922
|
+
if agent_leaf_name in deployed_ids:
|
|
1923
|
+
# Store agent_id for selection tracking (not display name)
|
|
1924
|
+
deployed_full_paths.add(agent_id)
|
|
1925
|
+
|
|
1926
|
+
# Track current selection state (starts with deployed, updated in loop)
|
|
1927
|
+
current_selection = deployed_full_paths.copy()
|
|
1928
|
+
|
|
1929
|
+
# Group agents by source/collection
|
|
1930
|
+
agent_map = {}
|
|
1931
|
+
collections = defaultdict(list)
|
|
1932
|
+
|
|
1933
|
+
for agent in agents:
|
|
1934
|
+
# Use agent_id (technical ID) for comparison, not display name
|
|
1935
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
1936
|
+
if agent_id in {a["agent_id"] for a in all_agents}:
|
|
1937
|
+
# Determine collection ID
|
|
1938
|
+
source_type = getattr(agent, "source_type", "local")
|
|
1939
|
+
if source_type == "remote":
|
|
1940
|
+
source_dict = getattr(agent, "source_dict", {})
|
|
1941
|
+
repo_url = source_dict.get("source", "")
|
|
1942
|
+
if "/" in repo_url:
|
|
1943
|
+
parts = repo_url.rstrip("/").split("/")
|
|
1944
|
+
if len(parts) >= 2:
|
|
1945
|
+
# Use more readable collection name
|
|
1946
|
+
if (
|
|
1947
|
+
"bobmatnyc/claude-mpm" in repo_url
|
|
1948
|
+
or "claude-mpm" in repo_url.lower()
|
|
1949
|
+
):
|
|
1950
|
+
collection_id = "MPM Agents"
|
|
1951
|
+
else:
|
|
1952
|
+
collection_id = f"{parts[-2]}/{parts[-1]}"
|
|
1953
|
+
else:
|
|
1954
|
+
collection_id = "Community Agents"
|
|
1955
|
+
else:
|
|
1956
|
+
collection_id = "Community Agents"
|
|
1957
|
+
else:
|
|
1958
|
+
collection_id = "Local Agents"
|
|
1959
|
+
|
|
1960
|
+
collections[collection_id].append(agent)
|
|
1961
|
+
agent_map[agent_id] = agent
|
|
1962
|
+
|
|
1963
|
+
# Monkey-patch questionary symbols for better visibility
|
|
1964
|
+
questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
|
|
1965
|
+
questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
|
|
1966
|
+
|
|
1967
|
+
# MAIN LOOP: Re-display UI when controls are used
|
|
1968
|
+
while True:
|
|
1969
|
+
# Build unified checkbox choices with inline controls
|
|
1970
|
+
choices = []
|
|
1971
|
+
|
|
1972
|
+
for collection_id in sorted(collections.keys()):
|
|
1973
|
+
agents_in_collection = collections[collection_id]
|
|
1974
|
+
|
|
1975
|
+
# Count selected/total agents in collection
|
|
1976
|
+
# Use agent_id for selection tracking, not display name
|
|
1977
|
+
selected_count = sum(
|
|
1978
|
+
1
|
|
1979
|
+
for agent in agents_in_collection
|
|
1980
|
+
if getattr(agent, "agent_id", agent.name) in current_selection
|
|
1981
|
+
)
|
|
1982
|
+
total_count = len(agents_in_collection)
|
|
1983
|
+
|
|
1984
|
+
# Add collection header
|
|
1985
|
+
choices.append(
|
|
1986
|
+
Separator(
|
|
1987
|
+
f"\n── {collection_id} ({selected_count}/{total_count} selected) ──"
|
|
1988
|
+
)
|
|
1989
|
+
)
|
|
1990
|
+
|
|
1991
|
+
# Determine if all agents in collection are selected
|
|
1992
|
+
all_selected = selected_count == total_count
|
|
1993
|
+
|
|
1994
|
+
# Add inline control: Select/Deselect all from this collection
|
|
1995
|
+
if all_selected:
|
|
1996
|
+
deselect_value = f"__DESELECT_ALL_{collection_id}__"
|
|
1997
|
+
choices.append(
|
|
1998
|
+
Choice(
|
|
1999
|
+
f" [Deselect all from {collection_id}]", # nosec B608
|
|
2000
|
+
value=deselect_value,
|
|
2001
|
+
checked=False,
|
|
2002
|
+
)
|
|
2003
|
+
)
|
|
2004
|
+
else:
|
|
2005
|
+
select_value = f"__SELECT_ALL_{collection_id}__"
|
|
2006
|
+
choices.append(
|
|
2007
|
+
Choice(
|
|
2008
|
+
f" [Select all from {collection_id}]", # nosec B608
|
|
2009
|
+
value=select_value,
|
|
2010
|
+
checked=False,
|
|
2011
|
+
)
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
# Add inline control: Select recommended from this collection
|
|
2015
|
+
recommended_in_collection = [
|
|
2016
|
+
a
|
|
2017
|
+
for a in agents_in_collection
|
|
2018
|
+
if any(
|
|
2019
|
+
a.name == rec_id
|
|
2020
|
+
or a.name.split("/")[-1] == rec_id.split("/")[-1]
|
|
2021
|
+
for rec_id in recommended_agent_ids
|
|
2022
|
+
)
|
|
2023
|
+
]
|
|
2024
|
+
if recommended_in_collection:
|
|
2025
|
+
recommended_selected = sum(
|
|
2026
|
+
1
|
|
2027
|
+
for a in recommended_in_collection
|
|
2028
|
+
if a.name in current_selection
|
|
2029
|
+
)
|
|
2030
|
+
if recommended_selected == len(recommended_in_collection):
|
|
2031
|
+
choices.append(
|
|
2032
|
+
Choice(
|
|
2033
|
+
f" [Deselect recommended ({len(recommended_in_collection)} agents)]",
|
|
2034
|
+
value=f"__DESELECT_REC_{collection_id}__",
|
|
2035
|
+
checked=False,
|
|
2036
|
+
)
|
|
2037
|
+
)
|
|
2038
|
+
else:
|
|
2039
|
+
choices.append(
|
|
2040
|
+
Choice(
|
|
2041
|
+
f" [Select recommended ({len(recommended_in_collection)} agents)]",
|
|
2042
|
+
value=f"__SELECT_REC_{collection_id}__",
|
|
2043
|
+
checked=False,
|
|
2044
|
+
)
|
|
2045
|
+
)
|
|
2046
|
+
|
|
2047
|
+
# Add separator before individual agents
|
|
2048
|
+
choices.append(Separator())
|
|
2049
|
+
|
|
2050
|
+
# Group agents by category within collection (if hierarchical)
|
|
2051
|
+
category_groups = defaultdict(list)
|
|
2052
|
+
for agent in sorted(agents_in_collection, key=lambda a: a.name):
|
|
2053
|
+
# Extract category from hierarchical path (e.g., "engineer/backend/python-engineer")
|
|
2054
|
+
parts = agent.name.split("/")
|
|
2055
|
+
if len(parts) > 1:
|
|
2056
|
+
category = "/".join(parts[:-1]) # e.g., "engineer/backend"
|
|
2057
|
+
else:
|
|
2058
|
+
category = "" # No category
|
|
2059
|
+
category_groups[category].append(agent)
|
|
2060
|
+
|
|
2061
|
+
# Display agents grouped by category
|
|
2062
|
+
for category in sorted(category_groups.keys()):
|
|
2063
|
+
agents_in_category = category_groups[category]
|
|
2064
|
+
|
|
2065
|
+
# Add category separator if hierarchical
|
|
2066
|
+
if category:
|
|
2067
|
+
choices.append(Separator(f" {category}/"))
|
|
2068
|
+
|
|
2069
|
+
# Add individual agents
|
|
2070
|
+
for agent in agents_in_category:
|
|
2071
|
+
# Use agent_id (technical ID) for all tracking/selection
|
|
2072
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
2073
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
2074
|
+
raw_display_name = getattr(
|
|
2075
|
+
agent, "display_name", agent_leaf_name
|
|
2076
|
+
)
|
|
2077
|
+
display_name = self._format_display_name(raw_display_name)
|
|
2078
|
+
|
|
2079
|
+
# Check if agent is required (cannot be unchecked)
|
|
2080
|
+
required_agents = set(self.unified_config.agents.required)
|
|
2081
|
+
is_required = (
|
|
2082
|
+
agent_leaf_name in required_agents
|
|
2083
|
+
or agent_id in required_agents
|
|
2084
|
+
)
|
|
2085
|
+
|
|
2086
|
+
# Format choice text with [Required] indicator
|
|
2087
|
+
if is_required:
|
|
2088
|
+
choice_text = f" {display_name} [Required]"
|
|
2089
|
+
else:
|
|
2090
|
+
choice_text = f" {display_name}"
|
|
2091
|
+
|
|
2092
|
+
# Required agents are always selected
|
|
2093
|
+
is_selected = is_required or agent_id in current_selection
|
|
2094
|
+
|
|
2095
|
+
# Add to current selection if required
|
|
2096
|
+
if is_required:
|
|
2097
|
+
current_selection.add(agent_id)
|
|
2098
|
+
|
|
2099
|
+
choices.append(
|
|
2100
|
+
Choice(
|
|
2101
|
+
title=choice_text,
|
|
2102
|
+
value=agent_id, # Use agent_id for value
|
|
2103
|
+
checked=is_selected,
|
|
2104
|
+
disabled=is_required, # Disable checkbox for required agents
|
|
2105
|
+
)
|
|
2106
|
+
)
|
|
2107
|
+
|
|
2108
|
+
self.console.print("\n[bold cyan]Select Agents to Install[/bold cyan]")
|
|
2109
|
+
self.console.print("[dim][✓] Checked = Installed (uncheck to remove)[/dim]")
|
|
2110
|
+
self.console.print(
|
|
2111
|
+
"[dim][ ] Unchecked = Available (check to install)[/dim]"
|
|
2112
|
+
)
|
|
2113
|
+
self.console.print("[dim][Required] = Core agents (always installed)[/dim]")
|
|
2114
|
+
self.console.print(
|
|
2115
|
+
"[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
|
|
2116
|
+
)
|
|
2117
|
+
|
|
2118
|
+
try:
|
|
2119
|
+
selected_values = questionary.checkbox(
|
|
2120
|
+
"Select agents:",
|
|
2121
|
+
choices=choices,
|
|
2122
|
+
instruction="(Space to toggle, Enter to continue)",
|
|
2123
|
+
style=self.QUESTIONARY_STYLE,
|
|
2124
|
+
).ask()
|
|
2125
|
+
except Exception as e:
|
|
2126
|
+
import sys
|
|
2127
|
+
|
|
2128
|
+
self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
|
|
2129
|
+
self.console.print(
|
|
2130
|
+
"[red]Error: Could not display interactive menu[/red]"
|
|
2131
|
+
)
|
|
2132
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
2133
|
+
if not sys.stdin.isatty():
|
|
2134
|
+
self.console.print("[dim]Interactive terminal required. Use:[/dim]")
|
|
2135
|
+
self.console.print(
|
|
2136
|
+
"[dim] --list-agents to see available agents[/dim]"
|
|
2137
|
+
)
|
|
2138
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2139
|
+
return
|
|
2140
|
+
|
|
2141
|
+
if selected_values is None:
|
|
2142
|
+
self.console.print("[yellow]No changes made[/yellow]")
|
|
2143
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2144
|
+
return
|
|
2145
|
+
|
|
2146
|
+
# Check for inline control selections
|
|
2147
|
+
controls_selected = [v for v in selected_values if v.startswith("__")]
|
|
2148
|
+
|
|
2149
|
+
if controls_selected:
|
|
2150
|
+
# Process controls and update current_selection
|
|
2151
|
+
for control in controls_selected:
|
|
2152
|
+
if control.startswith("__SELECT_ALL_"):
|
|
2153
|
+
collection_id = control.replace("__SELECT_ALL_", "").replace(
|
|
2154
|
+
"__", ""
|
|
2155
|
+
)
|
|
2156
|
+
# Add all agents from this collection to current_selection
|
|
2157
|
+
for agent in collections[collection_id]:
|
|
2158
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
2159
|
+
current_selection.add(agent_id)
|
|
2160
|
+
elif control.startswith("__DESELECT_ALL_"):
|
|
2161
|
+
collection_id = control.replace("__DESELECT_ALL_", "").replace(
|
|
2162
|
+
"__", ""
|
|
2163
|
+
)
|
|
2164
|
+
# Remove all agents from this collection
|
|
2165
|
+
for agent in collections[collection_id]:
|
|
2166
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
2167
|
+
current_selection.discard(agent_id)
|
|
2168
|
+
elif control.startswith("__SELECT_REC_"):
|
|
2169
|
+
collection_id = control.replace("__SELECT_REC_", "").replace(
|
|
2170
|
+
"__", ""
|
|
2171
|
+
)
|
|
2172
|
+
# Add all recommended agents from this collection
|
|
2173
|
+
for agent in collections[collection_id]:
|
|
2174
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
2175
|
+
if any(
|
|
2176
|
+
agent_id == rec_id
|
|
2177
|
+
or agent_id.split("/")[-1] == rec_id.split("/")[-1]
|
|
2178
|
+
for rec_id in recommended_agent_ids
|
|
2179
|
+
):
|
|
2180
|
+
current_selection.add(agent_id)
|
|
2181
|
+
elif control.startswith("__DESELECT_REC_"):
|
|
2182
|
+
collection_id = control.replace("__DESELECT_REC_", "").replace(
|
|
2183
|
+
"__", ""
|
|
2184
|
+
)
|
|
2185
|
+
# Remove all recommended agents from this collection
|
|
2186
|
+
for agent in collections[collection_id]:
|
|
2187
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
2188
|
+
if any(
|
|
2189
|
+
agent_id == rec_id
|
|
2190
|
+
or agent_id.split("/")[-1] == rec_id.split("/")[-1]
|
|
2191
|
+
for rec_id in recommended_agent_ids
|
|
2192
|
+
):
|
|
2193
|
+
current_selection.discard(agent_id)
|
|
2194
|
+
|
|
2195
|
+
# Loop back to re-display with updated selections
|
|
2196
|
+
continue
|
|
2197
|
+
|
|
2198
|
+
# No controls selected - use the individual selections as final
|
|
2199
|
+
final_selection = set(selected_values)
|
|
2200
|
+
|
|
2201
|
+
# Ensure required agents are always in the final selection
|
|
2202
|
+
required_agents = set(self.unified_config.agents.required)
|
|
2203
|
+
for agent in agents:
|
|
2204
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
2205
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
2206
|
+
if agent_leaf_name in required_agents or agent_id in required_agents:
|
|
2207
|
+
final_selection.add(agent_id)
|
|
2208
|
+
|
|
2209
|
+
break
|
|
2210
|
+
|
|
2211
|
+
# Determine changes
|
|
2212
|
+
to_deploy = final_selection - deployed_full_paths
|
|
2213
|
+
to_remove = deployed_full_paths - final_selection
|
|
2214
|
+
|
|
2215
|
+
# Prevent removal of required agents
|
|
2216
|
+
required_agents = set(self.unified_config.agents.required)
|
|
2217
|
+
to_remove_filtered = set()
|
|
2218
|
+
for agent_id in to_remove:
|
|
2219
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
2220
|
+
if (
|
|
2221
|
+
agent_leaf_name not in required_agents
|
|
2222
|
+
and agent_id not in required_agents
|
|
2223
|
+
):
|
|
2224
|
+
to_remove_filtered.add(agent_id)
|
|
2225
|
+
else:
|
|
2226
|
+
self.console.print(
|
|
2227
|
+
f"[yellow]⚠ Cannot remove required agent: {agent_id}[/yellow]"
|
|
2228
|
+
)
|
|
2229
|
+
to_remove = to_remove_filtered
|
|
2230
|
+
|
|
2231
|
+
if not to_deploy and not to_remove:
|
|
2232
|
+
self.console.print("[yellow]No changes needed[/yellow]")
|
|
2233
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2234
|
+
return
|
|
2235
|
+
|
|
2236
|
+
# Show what will happen
|
|
2237
|
+
self.console.print("\n[bold]Changes to apply:[/bold]")
|
|
2238
|
+
if to_deploy:
|
|
2239
|
+
self.console.print(f"[green]Install {len(to_deploy)} agent(s)[/green]")
|
|
2240
|
+
for agent_id in to_deploy:
|
|
2241
|
+
self.console.print(f" + {agent_id}")
|
|
2242
|
+
if to_remove:
|
|
2243
|
+
self.console.print(f"[red]Remove {len(to_remove)} agent(s)[/red]")
|
|
2244
|
+
for agent_id in to_remove:
|
|
2245
|
+
self.console.print(f" - {agent_id}")
|
|
2246
|
+
|
|
2247
|
+
# Confirm
|
|
2248
|
+
if not Confirm.ask("\nApply these changes?", default=True):
|
|
2249
|
+
self.console.print("[yellow]Changes cancelled[/yellow]")
|
|
2250
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2251
|
+
return
|
|
2252
|
+
|
|
2253
|
+
# Execute changes
|
|
2254
|
+
deploy_success = 0
|
|
2255
|
+
deploy_fail = 0
|
|
2256
|
+
remove_success = 0
|
|
2257
|
+
remove_fail = 0
|
|
2258
|
+
|
|
2259
|
+
# Install new agents
|
|
2260
|
+
for agent_id in to_deploy:
|
|
2261
|
+
agent = agent_map.get(agent_id)
|
|
2262
|
+
if agent and self._deploy_single_agent(agent, show_feedback=False):
|
|
2263
|
+
deploy_success += 1
|
|
2264
|
+
self.console.print(f"[green]✓ Installed: {agent_id}[/green]")
|
|
2265
|
+
else:
|
|
2266
|
+
deploy_fail += 1
|
|
2267
|
+
self.console.print(f"[red]✗ Failed to install: {agent_id}[/red]")
|
|
2268
|
+
|
|
2269
|
+
# Remove agents
|
|
2270
|
+
for agent_id in to_remove:
|
|
2271
|
+
try:
|
|
2272
|
+
import json
|
|
2273
|
+
|
|
2274
|
+
# Extract leaf name to match deployed filename
|
|
2275
|
+
leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
|
|
2276
|
+
|
|
2277
|
+
# Remove from all possible locations
|
|
2278
|
+
paths_to_check = [
|
|
2279
|
+
Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
|
|
2280
|
+
Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
2281
|
+
Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
2282
|
+
]
|
|
2283
|
+
|
|
2284
|
+
removed = False
|
|
2285
|
+
for path in paths_to_check:
|
|
2286
|
+
if path.exists():
|
|
2287
|
+
path.unlink()
|
|
2288
|
+
removed = True
|
|
2289
|
+
|
|
2290
|
+
# Also remove from virtual deployment state
|
|
2291
|
+
deployment_state_paths = [
|
|
2292
|
+
Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2293
|
+
Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2294
|
+
]
|
|
2295
|
+
|
|
2296
|
+
for state_path in deployment_state_paths:
|
|
2297
|
+
if state_path.exists():
|
|
2298
|
+
try:
|
|
2299
|
+
with state_path.open() as f:
|
|
2300
|
+
state = json.load(f)
|
|
2301
|
+
agents_in_state = state.get("last_check_results", {}).get(
|
|
2302
|
+
"agents", {}
|
|
2303
|
+
)
|
|
2304
|
+
if leaf_name in agents_in_state:
|
|
2305
|
+
del agents_in_state[leaf_name]
|
|
2306
|
+
removed = True
|
|
2307
|
+
with state_path.open("w") as f:
|
|
2308
|
+
json.dump(state, f, indent=2)
|
|
2309
|
+
except (json.JSONDecodeError, KeyError):
|
|
2310
|
+
pass
|
|
2311
|
+
|
|
2312
|
+
if removed:
|
|
2313
|
+
remove_success += 1
|
|
2314
|
+
self.console.print(f"[green]✓ Removed: {agent_id}[/green]")
|
|
2315
|
+
else:
|
|
2316
|
+
remove_fail += 1
|
|
2317
|
+
self.console.print(f"[yellow]⚠ Not found: {agent_id}[/yellow]")
|
|
2318
|
+
except Exception as e:
|
|
2319
|
+
remove_fail += 1
|
|
2320
|
+
self.console.print(f"[red]✗ Failed to remove {agent_id}: {e}[/red]")
|
|
2321
|
+
|
|
2322
|
+
# Show summary
|
|
2323
|
+
self.console.print()
|
|
2324
|
+
if deploy_success > 0:
|
|
2325
|
+
self.console.print(f"[green]✓ Installed {deploy_success} agent(s)[/green]")
|
|
2326
|
+
if deploy_fail > 0:
|
|
2327
|
+
self.console.print(f"[red]✗ Failed to install {deploy_fail} agent(s)[/red]")
|
|
2328
|
+
if remove_success > 0:
|
|
2329
|
+
self.console.print(f"[green]✓ Removed {remove_success} agent(s)[/green]")
|
|
2330
|
+
if remove_fail > 0:
|
|
2331
|
+
self.console.print(f"[red]✗ Failed to remove {remove_fail} agent(s)[/red]")
|
|
2332
|
+
|
|
2333
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2334
|
+
|
|
2335
|
+
def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
|
|
2336
|
+
"""Manage agent installation state (unified install/remove interface).
|
|
2337
|
+
|
|
2338
|
+
DEPRECATED: Use _deploy_agents_unified instead.
|
|
2339
|
+
This method is kept for backward compatibility but should not be used.
|
|
2340
|
+
"""
|
|
2341
|
+
if not agents:
|
|
2342
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
2343
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2344
|
+
return
|
|
2345
|
+
|
|
2346
|
+
# Get ALL agents (filter BASE_AGENT but keep deployed agents visible)
|
|
2347
|
+
from claude_mpm.utils.agent_filters import (
|
|
2348
|
+
filter_base_agents,
|
|
2349
|
+
get_deployed_agent_ids,
|
|
2350
|
+
)
|
|
2351
|
+
|
|
2352
|
+
# Filter BASE_AGENT but keep deployed agents visible
|
|
2353
|
+
all_agents = filter_base_agents(
|
|
2354
|
+
[
|
|
2355
|
+
{
|
|
2356
|
+
"agent_id": getattr(a, "agent_id", a.name),
|
|
2357
|
+
"name": a.name,
|
|
2358
|
+
"description": a.description,
|
|
2359
|
+
"deployed": getattr(a, "is_deployed", False),
|
|
2360
|
+
}
|
|
2361
|
+
for a in agents
|
|
2362
|
+
]
|
|
2363
|
+
)
|
|
2364
|
+
|
|
2365
|
+
# Get deployed agent IDs (original state - for calculating final changes)
|
|
2366
|
+
# NOTE: deployed_ids contains LEAF NAMES (e.g., "python-engineer")
|
|
2367
|
+
deployed_ids = get_deployed_agent_ids()
|
|
2368
|
+
|
|
2369
|
+
if not all_agents:
|
|
2370
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
2371
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2372
|
+
return
|
|
2373
|
+
|
|
2374
|
+
# Build mapping: leaf name -> full path for deployed agents
|
|
2375
|
+
# This allows comparing deployed_ids (leaf names) with agent.agent_id (full paths)
|
|
2376
|
+
deployed_full_paths = set()
|
|
2377
|
+
for agent in agents:
|
|
2378
|
+
# FIX: Use agent_id (technical ID) instead of display name
|
|
2379
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
2380
|
+
agent_leaf_name = agent_id.split("/")[-1]
|
|
2381
|
+
if agent_leaf_name in deployed_ids:
|
|
2382
|
+
deployed_full_paths.add(agent_id)
|
|
2383
|
+
|
|
2384
|
+
# Track current selection state (starts with deployed full paths, updated after each iteration)
|
|
2385
|
+
current_selection = deployed_full_paths.copy()
|
|
2386
|
+
|
|
2387
|
+
# Loop to allow adjusting selection
|
|
2388
|
+
while True:
|
|
2389
|
+
# Build agent mapping and collections
|
|
2390
|
+
agent_map = {} # For lookup after selection
|
|
2391
|
+
collections = defaultdict(list)
|
|
2392
|
+
|
|
2393
|
+
for agent in agents:
|
|
2394
|
+
# FIX: Use agent_id (technical ID) for comparison
|
|
2395
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
2396
|
+
if agent_id in {a["agent_id"] for a in all_agents}:
|
|
2397
|
+
# Determine collection ID
|
|
2398
|
+
source_type = getattr(agent, "source_type", "local")
|
|
2399
|
+
if source_type == "remote":
|
|
2400
|
+
source_dict = getattr(agent, "source_dict", {})
|
|
2401
|
+
repo_url = source_dict.get("source", "")
|
|
2402
|
+
# Extract repository name from URL
|
|
2403
|
+
if "/" in repo_url:
|
|
2404
|
+
parts = repo_url.rstrip("/").split("/")
|
|
2405
|
+
if len(parts) >= 2:
|
|
2406
|
+
collection_id = f"{parts[-2]}/{parts[-1]}"
|
|
2407
|
+
else:
|
|
2408
|
+
collection_id = "remote"
|
|
2409
|
+
else:
|
|
2410
|
+
collection_id = "remote"
|
|
2411
|
+
else:
|
|
2412
|
+
collection_id = "local"
|
|
2413
|
+
|
|
2414
|
+
collections[collection_id].append(agent)
|
|
2415
|
+
agent_map[agent_id] = agent # FIX: Use agent_id as key
|
|
2416
|
+
|
|
2417
|
+
# STEP 1: Collection-level selection
|
|
2418
|
+
self.console.print("\n[bold cyan]Select Agent Collections[/bold cyan]")
|
|
2419
|
+
self.console.print(
|
|
2420
|
+
"[dim]Checking a collection installs ALL agents in that collection[/dim]"
|
|
2421
|
+
)
|
|
2422
|
+
self.console.print(
|
|
2423
|
+
"[dim]Unchecking a collection removes ALL agents in that collection[/dim]"
|
|
2424
|
+
)
|
|
2425
|
+
self.console.print(
|
|
2426
|
+
"[dim]For partial deployment, use 'Fine-tune individual agents'[/dim]\n"
|
|
2427
|
+
)
|
|
2428
|
+
|
|
2429
|
+
collection_choices = []
|
|
2430
|
+
for collection_id in sorted(collections.keys()):
|
|
2431
|
+
agents_in_collection = collections[collection_id]
|
|
2432
|
+
|
|
2433
|
+
# Check if ANY agent in this collection is currently deployed
|
|
2434
|
+
# This reflects actual deployment state, not just selection
|
|
2435
|
+
# FIX: Use agent_id for comparison with current_selection
|
|
2436
|
+
any_deployed = any(
|
|
2437
|
+
getattr(agent, "agent_id", agent.name) in current_selection
|
|
2438
|
+
for agent in agents_in_collection
|
|
2439
|
+
)
|
|
2440
|
+
|
|
2441
|
+
# Count deployed agents for display
|
|
2442
|
+
# FIX: Use agent_id for comparison with current_selection
|
|
2443
|
+
deployed_count = sum(
|
|
2444
|
+
1
|
|
2445
|
+
for agent in agents_in_collection
|
|
2446
|
+
if getattr(agent, "agent_id", agent.name) in current_selection
|
|
2447
|
+
)
|
|
2448
|
+
|
|
2449
|
+
collection_choices.append(
|
|
2450
|
+
Choice(
|
|
2451
|
+
f"{collection_id} ({deployed_count}/{len(agents_in_collection)} deployed)",
|
|
2452
|
+
value=collection_id,
|
|
2453
|
+
checked=any_deployed,
|
|
2454
|
+
)
|
|
2455
|
+
)
|
|
2456
|
+
|
|
2457
|
+
# Add option to fine-tune individual agents
|
|
2458
|
+
collection_choices.append(Separator())
|
|
2459
|
+
collection_choices.append(
|
|
2460
|
+
Choice(
|
|
2461
|
+
"→ Fine-tune individual agents...",
|
|
2462
|
+
value="__INDIVIDUAL__",
|
|
2463
|
+
checked=False,
|
|
2464
|
+
)
|
|
2465
|
+
)
|
|
2466
|
+
|
|
2467
|
+
# Monkey-patch questionary symbols for better visibility
|
|
2468
|
+
questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
|
|
2469
|
+
questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
|
|
2470
|
+
|
|
2471
|
+
try:
|
|
2472
|
+
selected_collections = questionary.checkbox(
|
|
2473
|
+
"Select agent collections to deploy:",
|
|
2474
|
+
choices=collection_choices,
|
|
2475
|
+
instruction="(Space to toggle, Enter to continue)",
|
|
2476
|
+
style=self.QUESTIONARY_STYLE,
|
|
2477
|
+
).ask()
|
|
2478
|
+
except Exception as e:
|
|
2479
|
+
import sys
|
|
2480
|
+
|
|
2481
|
+
self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
|
|
2482
|
+
self.console.print(
|
|
2483
|
+
"[red]Error: Could not display interactive menu[/red]"
|
|
2484
|
+
)
|
|
2485
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
2486
|
+
if not sys.stdin.isatty():
|
|
2487
|
+
self.console.print("[dim]Interactive terminal required. Use:[/dim]")
|
|
2488
|
+
self.console.print(
|
|
2489
|
+
"[dim] --list-agents to see available agents[/dim]"
|
|
2490
|
+
)
|
|
2491
|
+
self.console.print(
|
|
2492
|
+
"[dim] --enable-agent/--disable-agent for scripting[/dim]"
|
|
2493
|
+
)
|
|
2494
|
+
else:
|
|
2495
|
+
self.console.print(
|
|
2496
|
+
"[dim]This might be a terminal compatibility issue.[/dim]"
|
|
2497
|
+
)
|
|
2498
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2499
|
+
return
|
|
2500
|
+
|
|
2501
|
+
# Handle cancellation
|
|
2502
|
+
if selected_collections is None:
|
|
2503
|
+
import sys
|
|
2504
|
+
|
|
2505
|
+
if not sys.stdin.isatty():
|
|
2506
|
+
self.console.print(
|
|
2507
|
+
"[red]Error: Interactive terminal required for agent selection[/red]"
|
|
2508
|
+
)
|
|
2509
|
+
self.console.print(
|
|
2510
|
+
"[dim]Use --list-agents to see available agents[/dim]"
|
|
2511
|
+
)
|
|
2512
|
+
self.console.print(
|
|
2513
|
+
"[dim]Use --enable-agent/--disable-agent for non-interactive mode[/dim]"
|
|
2514
|
+
)
|
|
2515
|
+
else:
|
|
2516
|
+
self.console.print("[yellow]No changes made[/yellow]")
|
|
2517
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2518
|
+
return
|
|
2519
|
+
|
|
2520
|
+
# STEP 2: Check if user wants individual selection
|
|
2521
|
+
if "__INDIVIDUAL__" in selected_collections:
|
|
2522
|
+
# Remove the __INDIVIDUAL__ marker
|
|
2523
|
+
selected_collections = [
|
|
2524
|
+
c for c in selected_collections if c != "__INDIVIDUAL__"
|
|
2525
|
+
]
|
|
2526
|
+
|
|
2527
|
+
# Build individual agent choices with grouping
|
|
2528
|
+
agent_choices = []
|
|
2529
|
+
for collection_id in sorted(collections.keys()):
|
|
2530
|
+
agents_in_collection = collections[collection_id]
|
|
2531
|
+
|
|
2532
|
+
# Add collection header separator
|
|
2533
|
+
agent_choices.append(
|
|
2534
|
+
Separator(
|
|
2535
|
+
f"\n── {collection_id} ({len(agents_in_collection)} agents) ──"
|
|
2536
|
+
)
|
|
2537
|
+
)
|
|
2538
|
+
|
|
2539
|
+
# Add individual agents from this collection
|
|
2540
|
+
# FIX: Use agent_id for sorting, comparison, and values
|
|
2541
|
+
for agent in sorted(
|
|
2542
|
+
agents_in_collection,
|
|
2543
|
+
key=lambda a: getattr(a, "agent_id", a.name),
|
|
2544
|
+
):
|
|
2545
|
+
agent_id = getattr(agent, "agent_id", agent.name)
|
|
2546
|
+
raw_display_name = getattr(agent, "display_name", agent.name)
|
|
2547
|
+
display_name = self._format_display_name(raw_display_name)
|
|
2548
|
+
is_selected = agent_id in deployed_full_paths
|
|
2549
|
+
|
|
2550
|
+
choice_text = f"{agent_id}"
|
|
2551
|
+
if display_name and display_name != agent_id:
|
|
2552
|
+
choice_text += f" - {display_name}"
|
|
2553
|
+
|
|
2554
|
+
agent_choices.append(
|
|
2555
|
+
Choice(
|
|
2556
|
+
title=choice_text, value=agent_id, checked=is_selected
|
|
2557
|
+
)
|
|
2558
|
+
)
|
|
2559
|
+
|
|
2560
|
+
self.console.print(
|
|
2561
|
+
"\n[bold cyan]Fine-tune Individual Agents[/bold cyan]"
|
|
2562
|
+
)
|
|
2563
|
+
self.console.print(
|
|
2564
|
+
"[dim][✓] Checked = Installed (uncheck to remove)[/dim]"
|
|
2565
|
+
)
|
|
2566
|
+
self.console.print(
|
|
2567
|
+
"[dim][ ] Unchecked = Available (check to install)[/dim]"
|
|
2568
|
+
)
|
|
2569
|
+
self.console.print(
|
|
2570
|
+
"[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
|
|
2571
|
+
)
|
|
2572
|
+
|
|
2573
|
+
try:
|
|
2574
|
+
selected_agent_ids = questionary.checkbox(
|
|
2575
|
+
"Select individual agents:",
|
|
2576
|
+
choices=agent_choices,
|
|
2577
|
+
style=self.QUESTIONARY_STYLE,
|
|
2578
|
+
).ask()
|
|
2579
|
+
except Exception as e:
|
|
2580
|
+
import sys
|
|
2581
|
+
|
|
2582
|
+
self.logger.error(
|
|
2583
|
+
f"Questionary checkbox failed: {e}", exc_info=True
|
|
2584
|
+
)
|
|
2585
|
+
self.console.print(
|
|
2586
|
+
"[red]Error: Could not display interactive menu[/red]"
|
|
2587
|
+
)
|
|
2588
|
+
self.console.print(f"[dim]Reason: {e}[/dim]")
|
|
2589
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2590
|
+
return
|
|
2591
|
+
|
|
2592
|
+
if selected_agent_ids is None:
|
|
2593
|
+
self.console.print("[yellow]No changes made[/yellow]")
|
|
2594
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2595
|
+
return
|
|
2596
|
+
|
|
2597
|
+
# Update current_selection with individual selections
|
|
2598
|
+
current_selection = set(selected_agent_ids)
|
|
2599
|
+
else:
|
|
2600
|
+
# Apply collection-level selections
|
|
2601
|
+
# For each collection, if it's selected, include ALL its agents
|
|
2602
|
+
# If it's not selected, exclude ALL its agents
|
|
2603
|
+
final_selections = set()
|
|
2604
|
+
for collection_id in selected_collections:
|
|
2605
|
+
for agent in collections[collection_id]:
|
|
2606
|
+
# FIX: Use agent_id for selection tracking
|
|
2607
|
+
final_selections.add(getattr(agent, "agent_id", agent.name))
|
|
2608
|
+
|
|
2609
|
+
# Update current_selection
|
|
2610
|
+
# This replaces the previous selection entirely with the new collection selections
|
|
2611
|
+
current_selection = final_selections
|
|
2612
|
+
|
|
2613
|
+
# Determine actions based on ORIGINAL deployed state
|
|
2614
|
+
# Compare full paths to full paths (deployed_full_paths was built from deployed_ids)
|
|
2615
|
+
to_deploy = (
|
|
2616
|
+
current_selection - deployed_full_paths
|
|
2617
|
+
) # Selected but not originally deployed
|
|
2618
|
+
|
|
2619
|
+
# For removal, verify files actually exist before adding to the set
|
|
2620
|
+
# This prevents "Not found" warnings when multiple agents share leaf names
|
|
2621
|
+
to_remove = set()
|
|
2622
|
+
for agent_id in deployed_full_paths - current_selection:
|
|
2623
|
+
# Extract leaf name to check file existence
|
|
2624
|
+
leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
|
|
2625
|
+
|
|
2626
|
+
# Check all possible locations
|
|
2627
|
+
paths_to_check = [
|
|
2628
|
+
Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
|
|
2629
|
+
Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
2630
|
+
Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
|
|
2631
|
+
]
|
|
2632
|
+
|
|
2633
|
+
# Also check virtual deployment state
|
|
2634
|
+
state_exists = False
|
|
2635
|
+
deployment_state_paths = [
|
|
2636
|
+
Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2637
|
+
Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2638
|
+
]
|
|
2639
|
+
|
|
2640
|
+
for state_path in deployment_state_paths:
|
|
2641
|
+
if state_path.exists():
|
|
2642
|
+
try:
|
|
2643
|
+
import json
|
|
2644
|
+
|
|
2645
|
+
with state_path.open() as f:
|
|
2646
|
+
state = json.load(f)
|
|
2647
|
+
agents_in_state = state.get("last_check_results", {}).get(
|
|
2648
|
+
"agents", {}
|
|
2649
|
+
)
|
|
2650
|
+
if leaf_name in agents_in_state:
|
|
2651
|
+
state_exists = True
|
|
2652
|
+
break
|
|
2653
|
+
except (json.JSONDecodeError, KeyError):
|
|
2654
|
+
continue
|
|
2655
|
+
|
|
2656
|
+
# Only add to removal set if file or state entry actually exists
|
|
2657
|
+
if any(p.exists() for p in paths_to_check) or state_exists:
|
|
2658
|
+
to_remove.add(agent_id)
|
|
2659
|
+
|
|
2660
|
+
if not to_deploy and not to_remove:
|
|
2661
|
+
self.console.print(
|
|
2662
|
+
"[yellow]No changes needed - all selected agents are already installed[/yellow]"
|
|
2663
|
+
)
|
|
2664
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2665
|
+
return
|
|
2666
|
+
|
|
2667
|
+
# Show what will happen
|
|
2668
|
+
self.console.print("\n[bold]Changes to apply:[/bold]")
|
|
2669
|
+
if to_deploy:
|
|
2670
|
+
self.console.print(f"[green]Install {len(to_deploy)} agent(s)[/green]")
|
|
2671
|
+
for agent_id in to_deploy:
|
|
2672
|
+
self.console.print(f" + {agent_id}")
|
|
2673
|
+
if to_remove:
|
|
2674
|
+
self.console.print(f"[red]Remove {len(to_remove)} agent(s)[/red]")
|
|
2675
|
+
for agent_id in to_remove:
|
|
2676
|
+
self.console.print(f" - {agent_id}")
|
|
2677
|
+
|
|
2678
|
+
# Ask user to confirm, adjust, or cancel
|
|
2679
|
+
action = questionary.select(
|
|
2680
|
+
"\nWhat would you like to do?",
|
|
2681
|
+
choices=[
|
|
2682
|
+
questionary.Choice("Apply these changes", value="apply"),
|
|
2683
|
+
questionary.Choice("Adjust selection", value="adjust"),
|
|
2684
|
+
questionary.Choice("Cancel", value="cancel"),
|
|
2685
|
+
],
|
|
2686
|
+
default="apply",
|
|
2687
|
+
style=self.QUESTIONARY_STYLE,
|
|
2688
|
+
).ask()
|
|
2689
|
+
|
|
2690
|
+
if action == "cancel":
|
|
2691
|
+
self.console.print("[yellow]Changes cancelled[/yellow]")
|
|
2692
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2693
|
+
return
|
|
2694
|
+
if action == "adjust":
|
|
2695
|
+
# current_selection is already updated, loop will use it
|
|
2696
|
+
continue
|
|
2697
|
+
|
|
2698
|
+
# Execute changes
|
|
2699
|
+
deploy_success = 0
|
|
2700
|
+
deploy_fail = 0
|
|
2701
|
+
remove_success = 0
|
|
2702
|
+
remove_fail = 0
|
|
2703
|
+
|
|
2704
|
+
# Install new agents
|
|
2705
|
+
for agent_id in to_deploy:
|
|
2706
|
+
agent = agent_map.get(agent_id)
|
|
2707
|
+
if agent and self._deploy_single_agent(agent, show_feedback=False):
|
|
2708
|
+
deploy_success += 1
|
|
2709
|
+
self.console.print(f"[green]✓ Installed: {agent_id}[/green]")
|
|
2710
|
+
else:
|
|
2711
|
+
deploy_fail += 1
|
|
2712
|
+
self.console.print(f"[red]✗ Failed to install: {agent_id}[/red]")
|
|
2713
|
+
|
|
2714
|
+
# Remove agents
|
|
2715
|
+
for agent_id in to_remove:
|
|
2716
|
+
try:
|
|
2717
|
+
import json
|
|
2718
|
+
# Note: Path is already imported at module level (line 17)
|
|
2719
|
+
|
|
2720
|
+
# Extract leaf name to match deployed filename
|
|
2721
|
+
# agent_id may be hierarchical (e.g., "engineer/mobile/tauri-engineer")
|
|
2722
|
+
# but deployed files use flattened leaf names (e.g., "tauri-engineer.md")
|
|
2723
|
+
if "/" in agent_id:
|
|
2724
|
+
leaf_name = agent_id.split("/")[-1]
|
|
2725
|
+
else:
|
|
2726
|
+
leaf_name = agent_id
|
|
2727
|
+
|
|
2728
|
+
# Remove from project, legacy, and user locations
|
|
2729
|
+
project_path = (
|
|
2730
|
+
Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md"
|
|
2731
|
+
)
|
|
2732
|
+
legacy_path = Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md"
|
|
2733
|
+
user_path = Path.home() / ".claude" / "agents" / f"{leaf_name}.md"
|
|
2734
|
+
|
|
2735
|
+
removed = False
|
|
2736
|
+
for path in [project_path, legacy_path, user_path]:
|
|
2737
|
+
if path.exists():
|
|
2738
|
+
path.unlink()
|
|
2739
|
+
removed = True
|
|
2740
|
+
|
|
2741
|
+
# Also remove from virtual deployment state
|
|
2742
|
+
deployment_state_paths = [
|
|
2743
|
+
Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2744
|
+
Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
|
|
2745
|
+
]
|
|
2746
|
+
|
|
2747
|
+
for state_path in deployment_state_paths:
|
|
2748
|
+
if state_path.exists():
|
|
2749
|
+
try:
|
|
2750
|
+
with state_path.open() as f:
|
|
2751
|
+
state = json.load(f)
|
|
2752
|
+
|
|
2753
|
+
# Remove agent from deployment state
|
|
2754
|
+
# Deployment state uses leaf names, not full hierarchical paths
|
|
2755
|
+
agents = state.get("last_check_results", {}).get(
|
|
2756
|
+
"agents", {}
|
|
2757
|
+
)
|
|
2758
|
+
if leaf_name in agents:
|
|
2759
|
+
del agents[leaf_name]
|
|
2760
|
+
removed = True
|
|
2761
|
+
|
|
2762
|
+
# Save updated state
|
|
2763
|
+
with state_path.open("w") as f:
|
|
2764
|
+
json.dump(state, f, indent=2)
|
|
2765
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
2766
|
+
# Log but don't fail - physical removal still counts
|
|
2767
|
+
self.logger.debug(
|
|
2768
|
+
f"Failed to update deployment state at {state_path}: {e}"
|
|
2769
|
+
)
|
|
2770
|
+
|
|
2771
|
+
if removed:
|
|
2772
|
+
remove_success += 1
|
|
2773
|
+
self.console.print(f"[green]✓ Removed: {agent_id}[/green]")
|
|
2774
|
+
else:
|
|
2775
|
+
remove_fail += 1
|
|
2776
|
+
self.console.print(f"[yellow]⚠ Not found: {agent_id}[/yellow]")
|
|
2777
|
+
except Exception as e:
|
|
2778
|
+
remove_fail += 1
|
|
2779
|
+
self.console.print(f"[red]✗ Failed to remove {agent_id}: {e}[/red]")
|
|
2780
|
+
|
|
2781
|
+
# Show summary
|
|
2782
|
+
self.console.print()
|
|
2783
|
+
if deploy_success > 0:
|
|
2784
|
+
self.console.print(
|
|
2785
|
+
f"[green]✓ Installed {deploy_success} agent(s)[/green]"
|
|
2786
|
+
)
|
|
2787
|
+
if deploy_fail > 0:
|
|
2788
|
+
self.console.print(
|
|
2789
|
+
f"[red]✗ Failed to install {deploy_fail} agent(s)[/red]"
|
|
2790
|
+
)
|
|
2791
|
+
if remove_success > 0:
|
|
2792
|
+
self.console.print(
|
|
2793
|
+
f"[green]✓ Removed {remove_success} agent(s)[/green]"
|
|
2794
|
+
)
|
|
2795
|
+
if remove_fail > 0:
|
|
2796
|
+
self.console.print(
|
|
2797
|
+
f"[red]✗ Failed to remove {remove_fail} agent(s)[/red]"
|
|
2798
|
+
)
|
|
2799
|
+
|
|
2800
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2801
|
+
# Exit the loop after successful execution
|
|
2802
|
+
break
|
|
2803
|
+
|
|
2804
|
+
def _deploy_agents_preset(self) -> None:
|
|
2805
|
+
"""Install agents using preset configuration."""
|
|
2806
|
+
try:
|
|
2807
|
+
from claude_mpm.services.agents.agent_preset_service import (
|
|
2808
|
+
AgentPresetService,
|
|
2809
|
+
)
|
|
2810
|
+
from claude_mpm.services.agents.git_source_manager import GitSourceManager
|
|
2811
|
+
|
|
2812
|
+
source_manager = GitSourceManager()
|
|
2813
|
+
preset_service = AgentPresetService(source_manager)
|
|
2814
|
+
|
|
2815
|
+
presets = preset_service.list_presets()
|
|
2816
|
+
|
|
2817
|
+
if not presets:
|
|
2818
|
+
self.console.print("[yellow]No presets available[/yellow]")
|
|
2819
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2820
|
+
return
|
|
2821
|
+
|
|
2822
|
+
self.console.print("\n[bold white]═══ Available Presets ═══[/bold white]\n")
|
|
2823
|
+
for idx, preset in enumerate(presets, 1):
|
|
2824
|
+
self.console.print(f" {idx}. [white]{preset['name']}[/white]")
|
|
2825
|
+
self.console.print(f" {preset['description']}")
|
|
2826
|
+
self.console.print(f" [dim]Agents: {len(preset['agents'])}[/dim]\n")
|
|
2827
|
+
|
|
2828
|
+
selection = Prompt.ask("\nEnter preset number (or 'c' to cancel)")
|
|
2829
|
+
if selection.lower() == "c":
|
|
2830
|
+
return
|
|
2831
|
+
|
|
2832
|
+
idx = int(selection) - 1
|
|
2833
|
+
if 0 <= idx < len(presets):
|
|
2834
|
+
preset_name = presets[idx]["name"]
|
|
2835
|
+
|
|
2836
|
+
# Resolve and deploy preset
|
|
2837
|
+
resolution = preset_service.resolve_agents(preset_name)
|
|
2838
|
+
|
|
2839
|
+
if resolution.get("missing_agents"):
|
|
2840
|
+
self.console.print(
|
|
2841
|
+
f"[red]Missing agents: {len(resolution['missing_agents'])}[/red]"
|
|
2842
|
+
)
|
|
2843
|
+
for agent_id in resolution["missing_agents"]:
|
|
2844
|
+
self.console.print(f" • {agent_id}")
|
|
2845
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2846
|
+
return
|
|
2847
|
+
|
|
2848
|
+
# Confirm installation
|
|
2849
|
+
self.console.print(
|
|
2850
|
+
f"\n[bold]Preset '{preset_name}' includes {len(resolution['agents'])} agents[/bold]"
|
|
2851
|
+
)
|
|
2852
|
+
if Confirm.ask("Install all agents?", default=True):
|
|
2853
|
+
installed = 0
|
|
2854
|
+
for agent in resolution["agents"]:
|
|
2855
|
+
# Convert dict to AgentConfig-like object for installation
|
|
2856
|
+
agent_config = AgentConfig(
|
|
2857
|
+
name=agent.get("agent_id", "unknown"),
|
|
2858
|
+
description=agent.get("metadata", {}).get(
|
|
2859
|
+
"description", ""
|
|
2860
|
+
),
|
|
2861
|
+
dependencies=[],
|
|
2862
|
+
)
|
|
2863
|
+
agent_config.source_dict = agent
|
|
2864
|
+
agent_config.full_agent_id = agent.get("agent_id", "unknown")
|
|
2865
|
+
|
|
2866
|
+
if self._deploy_single_agent(agent_config, show_feedback=False):
|
|
2867
|
+
installed += 1
|
|
2868
|
+
|
|
2869
|
+
self.console.print(
|
|
2870
|
+
f"\n[green]✓ Installed {installed}/{len(resolution['agents'])} agents[/green]"
|
|
2871
|
+
)
|
|
2872
|
+
|
|
2873
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2874
|
+
else:
|
|
2875
|
+
self.console.print("[red]Invalid selection[/red]")
|
|
2876
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2877
|
+
|
|
2878
|
+
except Exception as e:
|
|
2879
|
+
self.console.print(f"[red]Error installing preset: {e}[/red]")
|
|
2880
|
+
self.logger.error(f"Preset installation failed: {e}", exc_info=True)
|
|
2881
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2882
|
+
|
|
2883
|
+
def _select_recommended_agents(self, agents: List[AgentConfig]) -> None:
|
|
2884
|
+
"""Select and install recommended agents based on toolchain detection."""
|
|
2885
|
+
if not agents:
|
|
2886
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
2887
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2888
|
+
return
|
|
2889
|
+
|
|
2890
|
+
self.console.clear()
|
|
2891
|
+
self.console.print(
|
|
2892
|
+
"\n[bold white]═══ Recommended Agents for This Project ═══[/bold white]\n"
|
|
2893
|
+
)
|
|
2894
|
+
|
|
2895
|
+
# Get recommended agent IDs
|
|
2896
|
+
try:
|
|
2897
|
+
recommended_agent_ids = self.recommendation_service.get_recommended_agents(
|
|
2898
|
+
str(self.project_dir)
|
|
2899
|
+
)
|
|
2900
|
+
except Exception as e:
|
|
2901
|
+
self.console.print(f"[red]Error detecting toolchain: {e}[/red]")
|
|
2902
|
+
self.logger.error(f"Toolchain detection failed: {e}", exc_info=True)
|
|
2903
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2904
|
+
return
|
|
2905
|
+
|
|
2906
|
+
if not recommended_agent_ids:
|
|
2907
|
+
self.console.print("[yellow]No recommended agents found[/yellow]")
|
|
2908
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2909
|
+
return
|
|
2910
|
+
|
|
2911
|
+
# Get detection summary
|
|
2912
|
+
try:
|
|
2913
|
+
summary = self.recommendation_service.get_detection_summary(
|
|
2914
|
+
str(self.project_dir)
|
|
2915
|
+
)
|
|
2916
|
+
|
|
2917
|
+
self.console.print("[bold]Detected Project Stack:[/bold]")
|
|
2918
|
+
if summary.get("detected_languages"):
|
|
2919
|
+
self.console.print(
|
|
2920
|
+
f" Languages: [cyan]{', '.join(summary['detected_languages'])}[/cyan]"
|
|
2921
|
+
)
|
|
2922
|
+
if summary.get("detected_frameworks"):
|
|
2923
|
+
self.console.print(
|
|
2924
|
+
f" Frameworks: [cyan]{', '.join(summary['detected_frameworks'])}[/cyan]"
|
|
2925
|
+
)
|
|
2926
|
+
self.console.print(
|
|
2927
|
+
f" Detection Quality: [{'green' if summary.get('detection_quality') == 'high' else 'yellow'}]{summary.get('detection_quality', 'unknown')}[/]"
|
|
2928
|
+
)
|
|
2929
|
+
self.console.print()
|
|
2930
|
+
except Exception: # nosec B110 - Suppress broad except for failed safety check
|
|
2931
|
+
# Silent failure on safety check - non-critical feature
|
|
2932
|
+
pass
|
|
2933
|
+
|
|
2934
|
+
# Build mapping: agent_id -> AgentConfig
|
|
2935
|
+
agent_map = {agent.name: agent for agent in agents}
|
|
2936
|
+
|
|
2937
|
+
# Also check leaf names for matching
|
|
2938
|
+
for agent in agents:
|
|
2939
|
+
leaf_name = agent.name.split("/")[-1] if "/" in agent.name else agent.name
|
|
2940
|
+
if leaf_name not in agent_map:
|
|
2941
|
+
agent_map[leaf_name] = agent
|
|
2942
|
+
|
|
2943
|
+
# Find matching agents from available agents
|
|
2944
|
+
matched_agents = []
|
|
2945
|
+
for recommended_id in recommended_agent_ids:
|
|
2946
|
+
# Try full path match first
|
|
2947
|
+
if recommended_id in agent_map:
|
|
2948
|
+
matched_agents.append(agent_map[recommended_id])
|
|
2949
|
+
else:
|
|
2950
|
+
# Try leaf name match
|
|
2951
|
+
recommended_leaf = (
|
|
2952
|
+
recommended_id.split("/")[-1]
|
|
2953
|
+
if "/" in recommended_id
|
|
2954
|
+
else recommended_id
|
|
2955
|
+
)
|
|
2956
|
+
if recommended_leaf in agent_map:
|
|
2957
|
+
matched_agents.append(agent_map[recommended_leaf])
|
|
2958
|
+
|
|
2959
|
+
if not matched_agents:
|
|
2960
|
+
self.console.print(
|
|
2961
|
+
"[yellow]No matching agents found in available sources[/yellow]"
|
|
2962
|
+
)
|
|
2963
|
+
Prompt.ask("\nPress Enter to continue")
|
|
2964
|
+
return
|
|
2965
|
+
|
|
2966
|
+
# Display recommended agents
|
|
2967
|
+
self.console.print(
|
|
2968
|
+
f"[bold]Recommended Agents ({len(matched_agents)}):[/bold]\n"
|
|
2969
|
+
)
|
|
2970
|
+
|
|
2971
|
+
from rich.table import Table
|
|
2972
|
+
|
|
2973
|
+
rec_table = Table(show_header=True, header_style="bold white")
|
|
2974
|
+
rec_table.add_column("#", style="dim", width=4)
|
|
2975
|
+
rec_table.add_column("Agent ID", style="cyan", width=40)
|
|
2976
|
+
rec_table.add_column("Status", style="white", width=15)
|
|
2977
|
+
|
|
2978
|
+
for idx, agent in enumerate(matched_agents, 1):
|
|
2979
|
+
is_installed = getattr(agent, "is_deployed", False)
|
|
2980
|
+
status = (
|
|
2981
|
+
"[green]Already Installed[/green]"
|
|
2982
|
+
if is_installed
|
|
2983
|
+
else "[yellow]Not Installed[/yellow]"
|
|
2984
|
+
)
|
|
2985
|
+
rec_table.add_row(str(idx), agent.name, status)
|
|
2986
|
+
|
|
2987
|
+
self.console.print(rec_table)
|
|
2988
|
+
|
|
2989
|
+
# Count how many need installation
|
|
2990
|
+
to_install = [a for a in matched_agents if not getattr(a, "is_deployed", False)]
|
|
2991
|
+
already_installed = len(matched_agents) - len(to_install)
|
|
2992
|
+
|
|
2993
|
+
self.console.print()
|
|
2994
|
+
if already_installed > 0:
|
|
2995
|
+
self.console.print(
|
|
2996
|
+
f"[green]✓ {already_installed} already installed[/green]"
|
|
2997
|
+
)
|
|
2998
|
+
if to_install:
|
|
2999
|
+
self.console.print(
|
|
3000
|
+
f"[yellow]⚠ {len(to_install)} need installation[/yellow]"
|
|
3001
|
+
)
|
|
3002
|
+
else:
|
|
3003
|
+
self.console.print(
|
|
3004
|
+
"[green]✓ All recommended agents are already installed![/green]"
|
|
3005
|
+
)
|
|
3006
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3007
|
+
return
|
|
3008
|
+
|
|
3009
|
+
# Ask for confirmation
|
|
3010
|
+
self.console.print()
|
|
3011
|
+
if not Confirm.ask(
|
|
3012
|
+
f"Install {len(to_install)} recommended agent(s)?", default=True
|
|
3013
|
+
):
|
|
3014
|
+
self.console.print("[yellow]Installation cancelled[/yellow]")
|
|
3015
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3016
|
+
return
|
|
3017
|
+
|
|
3018
|
+
# Install agents
|
|
3019
|
+
self.console.print("\n[bold]Installing recommended agents...[/bold]\n")
|
|
3020
|
+
|
|
3021
|
+
success_count = 0
|
|
3022
|
+
fail_count = 0
|
|
3023
|
+
|
|
3024
|
+
for agent in to_install:
|
|
3025
|
+
try:
|
|
3026
|
+
if self._deploy_single_agent(agent, show_feedback=False):
|
|
3027
|
+
success_count += 1
|
|
3028
|
+
self.console.print(f"[green]✓ Installed: {agent.name}[/green]")
|
|
3029
|
+
else:
|
|
3030
|
+
fail_count += 1
|
|
3031
|
+
self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
|
|
3032
|
+
except Exception as e:
|
|
3033
|
+
fail_count += 1
|
|
3034
|
+
self.console.print(f"[red]✗ Failed: {agent.name} - {e}[/red]")
|
|
3035
|
+
|
|
3036
|
+
# Show summary
|
|
3037
|
+
self.console.print()
|
|
3038
|
+
if success_count > 0:
|
|
3039
|
+
self.console.print(
|
|
3040
|
+
f"[green]✓ Successfully installed {success_count} agent(s)[/green]"
|
|
3041
|
+
)
|
|
3042
|
+
if fail_count > 0:
|
|
3043
|
+
self.console.print(f"[red]✗ Failed to install {fail_count} agent(s)[/red]")
|
|
3044
|
+
|
|
3045
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3046
|
+
|
|
3047
|
+
def _deploy_single_agent(
|
|
3048
|
+
self, agent: AgentConfig, show_feedback: bool = True
|
|
3049
|
+
) -> bool:
|
|
3050
|
+
"""Install a single agent to the appropriate location."""
|
|
3051
|
+
try:
|
|
3052
|
+
# Check if this is a remote agent with source_dict
|
|
3053
|
+
source_dict = getattr(agent, "source_dict", None)
|
|
3054
|
+
full_agent_id = getattr(agent, "full_agent_id", agent.name)
|
|
3055
|
+
|
|
3056
|
+
if source_dict:
|
|
3057
|
+
# Deploy remote agent using its source file
|
|
3058
|
+
source_file = Path(source_dict.get("source_file", ""))
|
|
3059
|
+
if not source_file.exists():
|
|
3060
|
+
if show_feedback:
|
|
3061
|
+
self.console.print(
|
|
3062
|
+
f"[red]✗ Source file not found: {source_file}[/red]"
|
|
3063
|
+
)
|
|
3064
|
+
return False
|
|
3065
|
+
|
|
3066
|
+
# Determine target file name (use leaf name from hierarchical ID)
|
|
3067
|
+
if "/" in full_agent_id:
|
|
3068
|
+
target_name = full_agent_id.split("/")[-1] + ".md"
|
|
3069
|
+
else:
|
|
3070
|
+
target_name = full_agent_id + ".md"
|
|
3071
|
+
|
|
3072
|
+
# Deploy to project-level agents directory
|
|
3073
|
+
target_dir = self.project_dir / ".claude" / "agents"
|
|
3074
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
3075
|
+
target_file = target_dir / target_name
|
|
3076
|
+
|
|
3077
|
+
if show_feedback:
|
|
3078
|
+
self.console.print(
|
|
3079
|
+
f"\n[white]Installing {full_agent_id}...[/white]"
|
|
3080
|
+
)
|
|
3081
|
+
|
|
3082
|
+
# Copy the agent file
|
|
3083
|
+
import shutil
|
|
3084
|
+
|
|
3085
|
+
shutil.copy2(source_file, target_file)
|
|
3086
|
+
|
|
3087
|
+
if show_feedback:
|
|
3088
|
+
self.console.print(
|
|
3089
|
+
f"[green]✓ Successfully installed {full_agent_id} to {target_file}[/green]"
|
|
3090
|
+
)
|
|
3091
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3092
|
+
|
|
3093
|
+
return True
|
|
3094
|
+
# Legacy local template installation (not implemented here)
|
|
3095
|
+
if show_feedback:
|
|
3096
|
+
self.console.print(
|
|
3097
|
+
"[yellow]Local template installation not yet implemented[/yellow]"
|
|
3098
|
+
)
|
|
3099
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3100
|
+
return False
|
|
3101
|
+
|
|
3102
|
+
except Exception as e:
|
|
3103
|
+
if show_feedback:
|
|
3104
|
+
self.console.print(f"[red]Error installing agent: {e}[/red]")
|
|
3105
|
+
self.logger.error(f"Agent installation failed: {e}", exc_info=True)
|
|
3106
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3107
|
+
return False
|
|
3108
|
+
|
|
3109
|
+
def _remove_agents(self, agents: List[AgentConfig]) -> None:
|
|
3110
|
+
"""Remove installed agents."""
|
|
3111
|
+
# Filter to installed agents only
|
|
3112
|
+
installed = [a for a in agents if getattr(a, "is_deployed", False)]
|
|
3113
|
+
|
|
3114
|
+
if not installed:
|
|
3115
|
+
self.console.print("[yellow]No agents are currently installed[/yellow]")
|
|
3116
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3117
|
+
return
|
|
3118
|
+
|
|
3119
|
+
self.console.print(f"\n[bold]Installed agents ({len(installed)}):[/bold]")
|
|
3120
|
+
for idx, agent in enumerate(installed, 1):
|
|
3121
|
+
raw_display_name = getattr(agent, "display_name", agent.name)
|
|
3122
|
+
display_name = self._format_display_name(raw_display_name)
|
|
3123
|
+
self.console.print(f" {idx}. {agent.name} - {display_name}")
|
|
3124
|
+
|
|
3125
|
+
selection = Prompt.ask("\nEnter agent number to remove (or 'c' to cancel)")
|
|
3126
|
+
if selection.lower() == "c":
|
|
3127
|
+
return
|
|
3128
|
+
|
|
3129
|
+
try:
|
|
3130
|
+
idx = int(selection) - 1
|
|
3131
|
+
if 0 <= idx < len(installed):
|
|
3132
|
+
agent = installed[idx]
|
|
3133
|
+
full_agent_id = getattr(agent, "full_agent_id", agent.name)
|
|
3134
|
+
|
|
3135
|
+
# Determine possible file names (hierarchical and leaf)
|
|
3136
|
+
file_names = [f"{full_agent_id}.md"]
|
|
3137
|
+
if "/" in full_agent_id:
|
|
3138
|
+
leaf_name = full_agent_id.split("/")[-1]
|
|
3139
|
+
file_names.append(f"{leaf_name}.md")
|
|
3140
|
+
|
|
3141
|
+
# Remove from both project and user directories
|
|
3142
|
+
removed = False
|
|
3143
|
+
project_agent_dir = Path.cwd() / ".claude-mpm" / "agents"
|
|
3144
|
+
user_agent_dir = Path.home() / ".claude" / "agents"
|
|
3145
|
+
|
|
3146
|
+
for file_name in file_names:
|
|
3147
|
+
project_file = project_agent_dir / file_name
|
|
3148
|
+
user_file = user_agent_dir / file_name
|
|
3149
|
+
|
|
3150
|
+
if project_file.exists():
|
|
3151
|
+
project_file.unlink()
|
|
3152
|
+
removed = True
|
|
3153
|
+
self.console.print(f"[green]✓ Removed {project_file}[/green]")
|
|
3154
|
+
|
|
3155
|
+
if user_file.exists():
|
|
3156
|
+
user_file.unlink()
|
|
3157
|
+
removed = True
|
|
3158
|
+
self.console.print(f"[green]✓ Removed {user_file}[/green]")
|
|
3159
|
+
|
|
3160
|
+
if removed:
|
|
3161
|
+
self.console.print(
|
|
3162
|
+
f"[green]✓ Successfully removed {full_agent_id}[/green]"
|
|
3163
|
+
)
|
|
3164
|
+
else:
|
|
3165
|
+
self.console.print("[yellow]Agent files not found[/yellow]")
|
|
3166
|
+
|
|
3167
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3168
|
+
else:
|
|
3169
|
+
self.console.print("[red]Invalid selection[/red]")
|
|
3170
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3171
|
+
|
|
3172
|
+
except (ValueError, IndexError):
|
|
3173
|
+
self.console.print("[red]Invalid selection[/red]")
|
|
3174
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3175
|
+
|
|
3176
|
+
def _view_agent_details_enhanced(self, agents: List[AgentConfig]) -> None:
|
|
3177
|
+
"""View detailed agent information with enhanced remote agent details."""
|
|
3178
|
+
if not agents:
|
|
3179
|
+
self.console.print("[yellow]No agents available[/yellow]")
|
|
3180
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3181
|
+
return
|
|
3182
|
+
|
|
3183
|
+
self.console.print(f"\n[bold]Available agents ({len(agents)}):[/bold]")
|
|
3184
|
+
for idx, agent in enumerate(agents, 1):
|
|
3185
|
+
raw_display_name = getattr(agent, "display_name", agent.name)
|
|
3186
|
+
display_name = self._format_display_name(raw_display_name)
|
|
3187
|
+
self.console.print(f" {idx}. {agent.name} - {display_name}")
|
|
3188
|
+
|
|
3189
|
+
selection = Prompt.ask("\nEnter agent number to view (or 'c' to cancel)")
|
|
3190
|
+
if selection.lower() == "c":
|
|
3191
|
+
return
|
|
3192
|
+
|
|
3193
|
+
try:
|
|
3194
|
+
idx = int(selection) - 1
|
|
3195
|
+
if 0 <= idx < len(agents):
|
|
3196
|
+
agent = agents[idx]
|
|
3197
|
+
|
|
3198
|
+
self.console.clear()
|
|
3199
|
+
self.console.print("\n[bold white]═══ Agent Details ═══[/bold white]\n")
|
|
3200
|
+
|
|
3201
|
+
# Basic info
|
|
3202
|
+
self.console.print(f"[bold]ID:[/bold] {agent.name}")
|
|
3203
|
+
raw_display_name = getattr(agent, "display_name", "N/A")
|
|
3204
|
+
display_name = (
|
|
3205
|
+
self._format_display_name(raw_display_name)
|
|
3206
|
+
if raw_display_name != "N/A"
|
|
3207
|
+
else "N/A"
|
|
3208
|
+
)
|
|
3209
|
+
self.console.print(f"[bold]Name:[/bold] {display_name}")
|
|
3210
|
+
self.console.print(f"[bold]Description:[/bold] {agent.description}")
|
|
3211
|
+
|
|
3212
|
+
# Source info
|
|
3213
|
+
source_type = getattr(agent, "source_type", "local")
|
|
3214
|
+
self.console.print(f"[bold]Source Type:[/bold] {source_type}")
|
|
3215
|
+
|
|
3216
|
+
if source_type == "remote":
|
|
3217
|
+
source_dict = getattr(agent, "source_dict", {})
|
|
3218
|
+
category = source_dict.get("category", "N/A")
|
|
3219
|
+
source = source_dict.get("source", "N/A")
|
|
3220
|
+
version = source_dict.get("version", "N/A")
|
|
3221
|
+
|
|
3222
|
+
self.console.print(f"[bold]Category:[/bold] {category}")
|
|
3223
|
+
self.console.print(f"[bold]Source:[/bold] {source}")
|
|
3224
|
+
self.console.print(f"[bold]Version:[/bold] {version[:16]}...")
|
|
3225
|
+
|
|
3226
|
+
# Installation status
|
|
3227
|
+
is_installed = getattr(agent, "is_deployed", False)
|
|
3228
|
+
status = "Installed" if is_installed else "Available"
|
|
3229
|
+
self.console.print(f"[bold]Status:[/bold] {status}")
|
|
3230
|
+
|
|
3231
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3232
|
+
else:
|
|
3233
|
+
self.console.print("[red]Invalid selection[/red]")
|
|
3234
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3235
|
+
|
|
3236
|
+
except (ValueError, IndexError):
|
|
3237
|
+
self.console.print("[red]Invalid selection[/red]")
|
|
3238
|
+
Prompt.ask("\nPress Enter to continue")
|
|
3239
|
+
|
|
3240
|
+
|
|
3241
|
+
def manage_configure(args) -> int:
|
|
3242
|
+
"""Main entry point for configuration management command.
|
|
3243
|
+
|
|
3244
|
+
This function maintains backward compatibility while using the new BaseCommand pattern.
|
|
3245
|
+
"""
|
|
3246
|
+
command = ConfigureCommand()
|
|
3247
|
+
result = command.execute(args)
|
|
3248
|
+
|
|
3249
|
+
# Print result if needed
|
|
3250
|
+
if hasattr(args, "format") and args.format in ["json", "yaml"]:
|
|
3251
|
+
command.print_result(result, args)
|
|
3252
|
+
|
|
3253
|
+
return result.exit_code
|