claude-mpm 4.20.3__py3-none-any.whl → 5.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_PM.md +35 -6
- claude_mpm/agents/OUTPUT_STYLE.md +3 -48
- claude_mpm/agents/PM_INSTRUCTIONS.md +1241 -667
- claude_mpm/agents/PM_INSTRUCTIONS_TEACH.md +1322 -0
- claude_mpm/agents/WORKFLOW.md +75 -2
- claude_mpm/agents/__init__.py +6 -0
- claude_mpm/agents/agent_loader.py +1 -4
- claude_mpm/agents/base_agent.json +6 -3
- claude_mpm/agents/base_agent_loader.py +10 -35
- claude_mpm/agents/frontmatter_validator.py +1 -1
- claude_mpm/agents/templates/circuit-breakers.md +1254 -0
- claude_mpm/agents/templates/context-management-examples.md +544 -0
- claude_mpm/agents/templates/{pm_red_flags.md → pm-red-flags.md} +89 -19
- claude_mpm/agents/templates/pr-workflow-examples.md +427 -0
- claude_mpm/agents/templates/research-gate-examples.md +669 -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/cli/__init__.py +37 -2
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/agent_source.py +774 -0
- claude_mpm/cli/commands/agent_state_manager.py +188 -30
- claude_mpm/cli/commands/agents.py +959 -36
- claude_mpm/cli/commands/agents_cleanup.py +210 -0
- claude_mpm/cli/commands/agents_discover.py +338 -0
- claude_mpm/cli/commands/aggregate.py +1 -1
- claude_mpm/cli/commands/analyze.py +3 -3
- claude_mpm/cli/commands/auto_configure.py +537 -239
- claude_mpm/cli/commands/cleanup.py +1 -1
- claude_mpm/cli/commands/config.py +7 -4
- claude_mpm/cli/commands/configure.py +924 -45
- claude_mpm/cli/commands/configure_agent_display.py +4 -4
- claude_mpm/cli/commands/configure_navigation.py +63 -46
- claude_mpm/cli/commands/debug.py +12 -12
- claude_mpm/cli/commands/doctor.py +10 -2
- claude_mpm/cli/commands/hook_errors.py +277 -0
- claude_mpm/cli/commands/local_deploy.py +1 -4
- claude_mpm/cli/commands/mcp_install_commands.py +1 -1
- claude_mpm/cli/commands/mpm_init/__init__.py +73 -0
- claude_mpm/cli/commands/mpm_init/core.py +573 -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/modes.py +397 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +442 -0
- claude_mpm/cli/commands/mpm_init_cli.py +396 -0
- claude_mpm/cli/commands/mpm_init_handler.py +67 -1
- claude_mpm/cli/commands/postmortem.py +401 -0
- claude_mpm/cli/commands/run.py +125 -167
- claude_mpm/cli/commands/skill_source.py +694 -0
- claude_mpm/cli/commands/skills.py +835 -44
- claude_mpm/cli/executor.py +78 -3
- claude_mpm/cli/interactive/agent_wizard.py +1032 -47
- claude_mpm/cli/parsers/agent_source_parser.py +171 -0
- claude_mpm/cli/parsers/agents_parser.py +256 -4
- claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
- claude_mpm/cli/parsers/base_parser.py +53 -0
- claude_mpm/cli/parsers/config_parser.py +96 -43
- claude_mpm/cli/parsers/mpm_init_parser.py +42 -0
- claude_mpm/cli/parsers/skill_source_parser.py +169 -0
- claude_mpm/cli/parsers/skills_parser.py +145 -0
- claude_mpm/cli/parsers/source_parser.py +138 -0
- claude_mpm/cli/startup.py +564 -108
- claude_mpm/cli/startup_display.py +480 -0
- claude_mpm/cli/utils.py +1 -1
- claude_mpm/cli_module/commands.py +1 -1
- claude_mpm/commands/{mpm-auto-configure.md → mpm-agents-auto-configure.md} +9 -0
- claude_mpm/commands/mpm-agents-detect.md +9 -0
- claude_mpm/commands/{mpm-agents.md → mpm-agents-list.md} +9 -0
- claude_mpm/commands/mpm-agents-recommend.md +9 -0
- claude_mpm/commands/{mpm-config.md → mpm-config-view.md} +9 -0
- claude_mpm/commands/mpm-doctor.md +9 -0
- claude_mpm/commands/mpm-help.md +17 -2
- claude_mpm/commands/mpm-init.md +28 -3
- claude_mpm/commands/mpm-monitor.md +9 -0
- claude_mpm/commands/mpm-postmortem.md +123 -0
- claude_mpm/commands/mpm-session-resume.md +381 -0
- claude_mpm/commands/mpm-status.md +9 -0
- claude_mpm/commands/{mpm-organize.md → mpm-ticket-organize.md} +9 -0
- claude_mpm/commands/mpm-ticket-view.md +552 -0
- claude_mpm/commands/mpm-version.md +9 -0
- claude_mpm/commands/mpm.md +11 -0
- claude_mpm/config/agent_presets.py +488 -0
- claude_mpm/config/agent_sources.py +325 -0
- claude_mpm/config/skill_presets.py +392 -0
- claude_mpm/config/skill_sources.py +590 -0
- claude_mpm/constants.py +13 -0
- claude_mpm/core/api_validator.py +1 -1
- claude_mpm/core/claude_runner.py +19 -35
- claude_mpm/core/config.py +24 -0
- claude_mpm/core/constants.py +1 -1
- claude_mpm/core/framework/__init__.py +3 -16
- claude_mpm/core/framework/loaders/file_loader.py +54 -101
- claude_mpm/core/framework/loaders/instruction_loader.py +25 -5
- claude_mpm/core/framework/processors/metadata_processor.py +1 -1
- claude_mpm/core/hook_error_memory.py +381 -0
- claude_mpm/core/hook_manager.py +41 -2
- claude_mpm/core/interactive_session.py +131 -10
- claude_mpm/core/interfaces.py +56 -1
- claude_mpm/core/logger.py +3 -1
- claude_mpm/core/oneshot_session.py +110 -8
- claude_mpm/core/protocols/__init__.py +23 -0
- claude_mpm/core/protocols/runner_protocol.py +103 -0
- claude_mpm/core/protocols/session_protocol.py +131 -0
- claude_mpm/core/shared/singleton_manager.py +11 -4
- claude_mpm/core/system_context.py +38 -0
- claude_mpm/core/unified_config.py +22 -0
- claude_mpm/dashboard/static/css/activity.css +69 -69
- claude_mpm/dashboard/static/css/connection-status.css +10 -10
- claude_mpm/dashboard/static/css/dashboard.css +15 -15
- claude_mpm/dashboard/static/js/components/activity-tree.js +178 -178
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +101 -101
- claude_mpm/dashboard/static/js/components/agent-inference.js +31 -31
- claude_mpm/dashboard/static/js/components/build-tracker.js +59 -59
- claude_mpm/dashboard/static/js/components/code-simple.js +107 -107
- claude_mpm/dashboard/static/js/components/connection-debug.js +101 -101
- claude_mpm/dashboard/static/js/components/diff-viewer.js +113 -113
- claude_mpm/dashboard/static/js/components/event-viewer.js +12 -12
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +57 -57
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +74 -74
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +6 -6
- claude_mpm/dashboard/static/js/components/file-viewer.js +42 -42
- claude_mpm/dashboard/static/js/components/module-viewer.js +27 -27
- claude_mpm/dashboard/static/js/components/session-manager.js +14 -14
- claude_mpm/dashboard/static/js/components/socket-manager.js +1 -1
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +14 -14
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +110 -110
- claude_mpm/dashboard/static/js/components/working-directory.js +8 -8
- claude_mpm/dashboard/static/js/connection-manager.js +76 -76
- claude_mpm/dashboard/static/js/dashboard.js +76 -58
- claude_mpm/dashboard/static/js/extension-error-handler.js +22 -22
- claude_mpm/dashboard/static/js/socket-client.js +138 -121
- claude_mpm/dashboard/templates/code_simple.html +23 -23
- claude_mpm/dashboard/templates/index.html +18 -18
- claude_mpm/experimental/cli_enhancements.py +1 -5
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +3 -1
- claude_mpm/hooks/claude_hooks/hook_handler.py +24 -7
- claude_mpm/hooks/claude_hooks/installer.py +45 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/hooks/failure_learning/__init__.py +2 -8
- claude_mpm/hooks/failure_learning/failure_detection_hook.py +1 -6
- claude_mpm/hooks/failure_learning/fix_detection_hook.py +1 -6
- claude_mpm/hooks/failure_learning/learning_extraction_hook.py +1 -6
- claude_mpm/hooks/kuzu_response_hook.py +1 -5
- claude_mpm/hooks/templates/pre_tool_use_simple.py +78 -0
- claude_mpm/hooks/templates/pre_tool_use_template.py +323 -0
- claude_mpm/models/git_repository.py +198 -0
- claude_mpm/scripts/claude-hook-handler.sh +3 -3
- claude_mpm/scripts/start_activity_logging.py +3 -1
- claude_mpm/services/agents/agent_builder.py +45 -9
- claude_mpm/services/agents/agent_preset_service.py +238 -0
- claude_mpm/services/agents/agent_selection_service.py +484 -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/agent_deployment.py +126 -2
- claude_mpm/services/agents/deployment/agent_discovery_service.py +105 -73
- claude_mpm/services/agents/deployment/agent_format_converter.py +1 -1
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +1 -5
- claude_mpm/services/agents/deployment/agent_metrics_collector.py +3 -3
- claude_mpm/services/agents/deployment/agent_restore_handler.py +1 -4
- claude_mpm/services/agents/deployment/agent_template_builder.py +236 -15
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +101 -15
- claude_mpm/services/agents/deployment/async_agent_deployment.py +2 -1
- claude_mpm/services/agents/deployment/facade/deployment_facade.py +3 -3
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +115 -15
- claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +2 -2
- claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +1 -4
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +363 -0
- claude_mpm/services/agents/deployment/single_agent_deployer.py +2 -2
- claude_mpm/services/agents/deployment/system_instructions_deployer.py +168 -46
- claude_mpm/services/agents/deployment/validation/deployment_validator.py +2 -2
- claude_mpm/services/agents/git_source_manager.py +629 -0
- claude_mpm/services/agents/loading/framework_agent_loader.py +9 -12
- claude_mpm/services/agents/local_template_manager.py +50 -10
- 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 +1087 -0
- claude_mpm/services/agents/startup_sync.py +239 -0
- claude_mpm/services/agents/toolchain_detector.py +474 -0
- claude_mpm/services/analysis/__init__.py +25 -0
- claude_mpm/services/analysis/postmortem_reporter.py +474 -0
- claude_mpm/services/analysis/postmortem_service.py +765 -0
- claude_mpm/services/cli/session_pause_manager.py +504 -0
- claude_mpm/services/cli/session_resume_helper.py +36 -16
- claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
- claude_mpm/services/command_deployment_service.py +200 -6
- claude_mpm/services/core/base.py +31 -11
- claude_mpm/services/core/interfaces/__init__.py +1 -3
- claude_mpm/services/core/interfaces/health.py +1 -4
- claude_mpm/services/core/interfaces.py +56 -1
- claude_mpm/services/core/models/__init__.py +2 -11
- claude_mpm/services/core/models/agent_config.py +3 -0
- claude_mpm/services/core/models/process.py +4 -0
- claude_mpm/services/diagnostics/checks/__init__.py +4 -0
- claude_mpm/services/diagnostics/checks/agent_check.py +0 -2
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +577 -0
- claude_mpm/services/diagnostics/checks/instructions_check.py +1 -2
- claude_mpm/services/diagnostics/checks/mcp_check.py +0 -1
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
- claude_mpm/services/diagnostics/checks/monitor_check.py +0 -1
- claude_mpm/services/diagnostics/checks/skill_sources_check.py +587 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +9 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +40 -10
- claude_mpm/services/diagnostics/models.py +21 -0
- claude_mpm/services/event_bus/direct_relay.py +3 -3
- claude_mpm/services/event_bus/event_bus.py +36 -3
- claude_mpm/services/event_bus/relay.py +23 -7
- claude_mpm/services/events/consumers/logging.py +1 -2
- claude_mpm/services/git/__init__.py +21 -0
- claude_mpm/services/git/git_operations_service.py +494 -0
- claude_mpm/services/github/__init__.py +21 -0
- claude_mpm/services/github/github_cli_service.py +397 -0
- claude_mpm/services/infrastructure/monitoring/__init__.py +1 -5
- claude_mpm/services/infrastructure/monitoring/aggregator.py +1 -6
- claude_mpm/services/infrastructure/monitoring/resources.py +1 -1
- 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 +5 -13
- claude_mpm/services/local_ops/health_checks/__init__.py +1 -3
- claude_mpm/services/local_ops/health_manager.py +1 -4
- claude_mpm/services/local_ops/process_manager.py +1 -1
- claude_mpm/services/local_ops/resource_monitor.py +2 -2
- claude_mpm/services/mcp_config_manager.py +75 -145
- claude_mpm/services/mcp_gateway/auto_configure.py +31 -25
- claude_mpm/services/mcp_gateway/config/configuration.py +1 -1
- claude_mpm/services/mcp_gateway/core/process_pool.py +41 -26
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +1 -6
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -2
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +1 -1
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +26 -21
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +6 -2
- claude_mpm/services/mcp_service_verifier.py +6 -3
- claude_mpm/services/memory/failure_tracker.py +19 -4
- claude_mpm/services/memory/optimizer.py +1 -1
- claude_mpm/services/model/model_router.py +8 -9
- claude_mpm/services/monitor/daemon.py +29 -9
- claude_mpm/services/monitor/daemon_manager.py +96 -19
- claude_mpm/services/monitor/server.py +2 -2
- claude_mpm/services/native_agent_converter.py +356 -0
- claude_mpm/services/port_manager.py +1 -1
- claude_mpm/services/pr/__init__.py +14 -0
- claude_mpm/services/pr/pr_template_service.py +329 -0
- claude_mpm/services/project/documentation_manager.py +2 -1
- claude_mpm/services/project/project_organizer.py +4 -0
- claude_mpm/services/project/toolchain_analyzer.py +3 -1
- claude_mpm/services/runner_configuration_service.py +17 -3
- claude_mpm/services/self_upgrade_service.py +165 -7
- claude_mpm/services/session_management_service.py +16 -4
- claude_mpm/services/skills/__init__.py +18 -0
- claude_mpm/services/skills/git_skill_source_manager.py +1169 -0
- claude_mpm/services/skills/skill_discovery_service.py +568 -0
- claude_mpm/services/skills_config.py +547 -0
- claude_mpm/services/skills_deployer.py +955 -0
- claude_mpm/services/socketio/handlers/connection.py +1 -1
- claude_mpm/services/socketio/handlers/git.py +2 -2
- claude_mpm/services/socketio/server/core.py +1 -4
- claude_mpm/services/socketio/server/main.py +1 -3
- claude_mpm/services/system_instructions_service.py +1 -3
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +0 -3
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +0 -1
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +1 -1
- claude_mpm/services/unified/deployment_strategies/vercel.py +1 -5
- claude_mpm/services/unified/unified_deployment.py +1 -5
- claude_mpm/services/version_control/conflict_resolution.py +6 -4
- claude_mpm/services/visualization/__init__.py +1 -5
- claude_mpm/services/visualization/mermaid_generator.py +2 -3
- claude_mpm/skills/__init__.py +3 -3
- claude_mpm/skills/agent_skills_injector.py +42 -49
- claude_mpm/skills/bundled/infrastructure/env-manager/scripts/validate_env.py +576 -0
- claude_mpm/skills/bundled/main/mcp-builder/scripts/connections.py +17 -10
- claude_mpm/skills/bundled/main/mcp-builder/scripts/evaluation.py +92 -39
- claude_mpm/skills/bundled/main/skill-creator/scripts/init_skill.py +13 -12
- claude_mpm/skills/bundled/main/skill-creator/scripts/package_skill.py +5 -3
- claude_mpm/skills/bundled/main/skill-creator/scripts/quick_validate.py +19 -12
- claude_mpm/skills/bundled/performance-profiling.md +6 -0
- claude_mpm/skills/bundled/testing/webapp-testing/examples/console_logging.py +6 -6
- claude_mpm/skills/bundled/testing/webapp-testing/examples/element_discovery.py +13 -9
- claude_mpm/skills/bundled/testing/webapp-testing/examples/static_html_automation.py +8 -8
- claude_mpm/skills/bundled/testing/webapp-testing/scripts/with_server.py +37 -15
- claude_mpm/skills/skills_registry.py +44 -48
- claude_mpm/skills/skills_service.py +117 -108
- 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/tools/__main__.py +8 -8
- 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/utils/agent_dependency_loader.py +80 -13
- claude_mpm/utils/agent_filters.py +288 -0
- claude_mpm/utils/dependency_cache.py +3 -1
- claude_mpm/utils/gitignore.py +244 -0
- claude_mpm/utils/log_cleanup.py +3 -3
- claude_mpm/utils/migration.py +372 -0
- claude_mpm/utils/progress.py +387 -0
- claude_mpm/utils/robust_installer.py +3 -5
- claude_mpm/utils/structured_questions.py +619 -0
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/METADATA +496 -65
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/RECORD +328 -416
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -17
- claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +0 -3
- claude_mpm/agents/templates/agent-manager.json +0 -273
- claude_mpm/agents/templates/agentic-coder-optimizer.json +0 -248
- claude_mpm/agents/templates/api_qa.json +0 -180
- claude_mpm/agents/templates/circuit_breakers.md +0 -638
- claude_mpm/agents/templates/clerk-ops.json +0 -235
- claude_mpm/agents/templates/code_analyzer.json +0 -101
- claude_mpm/agents/templates/content-agent.json +0 -358
- claude_mpm/agents/templates/dart_engineer.json +0 -307
- claude_mpm/agents/templates/data_engineer.json +0 -225
- claude_mpm/agents/templates/documentation.json +0 -211
- claude_mpm/agents/templates/engineer.json +0 -210
- claude_mpm/agents/templates/gcp_ops_agent.json +0 -253
- claude_mpm/agents/templates/golang_engineer.json +0 -270
- claude_mpm/agents/templates/imagemagick.json +0 -264
- claude_mpm/agents/templates/java_engineer.json +0 -346
- claude_mpm/agents/templates/local_ops_agent.json +0 -1840
- claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +0 -39
- claude_mpm/agents/templates/logs/prompts/agent_engineer_20250901_010124_142.md +0 -400
- claude_mpm/agents/templates/memory_manager.json +0 -158
- claude_mpm/agents/templates/nextjs_engineer.json +0 -285
- claude_mpm/agents/templates/ops.json +0 -185
- claude_mpm/agents/templates/php-engineer.json +0 -281
- claude_mpm/agents/templates/product_owner.json +0 -338
- claude_mpm/agents/templates/project_organizer.json +0 -140
- claude_mpm/agents/templates/prompt-engineer.json +0 -737
- claude_mpm/agents/templates/python_engineer.json +0 -387
- claude_mpm/agents/templates/qa.json +0 -242
- claude_mpm/agents/templates/react_engineer.json +0 -238
- claude_mpm/agents/templates/refactoring_engineer.json +0 -276
- claude_mpm/agents/templates/research.json +0 -188
- claude_mpm/agents/templates/ruby-engineer.json +0 -280
- claude_mpm/agents/templates/rust_engineer.json +0 -275
- claude_mpm/agents/templates/security.json +0 -202
- claude_mpm/agents/templates/svelte-engineer.json +0 -225
- claude_mpm/agents/templates/ticketing.json +0 -177
- claude_mpm/agents/templates/typescript_engineer.json +0 -285
- claude_mpm/agents/templates/vercel_ops_agent.json +0 -412
- claude_mpm/agents/templates/version_control.json +0 -157
- claude_mpm/agents/templates/web_qa.json +0 -399
- claude_mpm/agents/templates/web_ui.json +0 -189
- claude_mpm/cli/commands/mpm_init.py +0 -2093
- claude_mpm/commands/mpm-tickets.md +0 -102
- claude_mpm/dashboard/.claude-mpm/socketio-instances.json +0 -1
- claude_mpm/dashboard/react/components/DataInspector/DataInspector.module.css +0 -188
- claude_mpm/dashboard/react/components/EventViewer/EventViewer.module.css +0 -156
- claude_mpm/dashboard/react/components/shared/ConnectionStatus.module.css +0 -38
- claude_mpm/dashboard/react/components/shared/FilterBar.module.css +0 -92
- claude_mpm/dashboard/static/archive/activity_dashboard_fixed.html +0 -248
- claude_mpm/dashboard/static/archive/activity_dashboard_test.html +0 -61
- claude_mpm/dashboard/static/archive/test_activity_connection.html +0 -179
- claude_mpm/dashboard/static/archive/test_claude_tree_tab.html +0 -68
- claude_mpm/dashboard/static/archive/test_dashboard.html +0 -409
- claude_mpm/dashboard/static/archive/test_dashboard_fixed.html +0 -519
- claude_mpm/dashboard/static/archive/test_dashboard_verification.html +0 -181
- claude_mpm/dashboard/static/archive/test_file_data.html +0 -315
- claude_mpm/dashboard/static/archive/test_file_tree_empty_state.html +0 -243
- claude_mpm/dashboard/static/archive/test_file_tree_fix.html +0 -234
- claude_mpm/dashboard/static/archive/test_file_tree_rename.html +0 -117
- claude_mpm/dashboard/static/archive/test_file_tree_tab.html +0 -115
- claude_mpm/dashboard/static/archive/test_file_viewer.html +0 -224
- claude_mpm/dashboard/static/archive/test_final_activity.html +0 -220
- claude_mpm/dashboard/static/archive/test_tab_fix.html +0 -139
- claude_mpm/dashboard/static/built/assets/events.DjpNxWNo.css +0 -1
- claude_mpm/dashboard/static/built/components/activity-tree.js +0 -2
- claude_mpm/dashboard/static/built/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/built/components/agent-inference.js +0 -2
- claude_mpm/dashboard/static/built/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/built/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/built/components/code-tree/tree-breadcrumb.js +0 -353
- claude_mpm/dashboard/static/built/components/code-tree/tree-constants.js +0 -235
- claude_mpm/dashboard/static/built/components/code-tree/tree-search.js +0 -409
- claude_mpm/dashboard/static/built/components/code-tree/tree-utils.js +0 -435
- claude_mpm/dashboard/static/built/components/code-tree.js +0 -2
- claude_mpm/dashboard/static/built/components/code-viewer.js +0 -2
- claude_mpm/dashboard/static/built/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/built/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/built/components/event-processor.js +0 -2
- claude_mpm/dashboard/static/built/components/event-viewer.js +0 -2
- claude_mpm/dashboard/static/built/components/export-manager.js +0 -2
- claude_mpm/dashboard/static/built/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/built/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/built/components/file-tool-tracker.js +0 -2
- claude_mpm/dashboard/static/built/components/file-viewer.js +0 -2
- claude_mpm/dashboard/static/built/components/hud-library-loader.js +0 -2
- claude_mpm/dashboard/static/built/components/hud-manager.js +0 -2
- claude_mpm/dashboard/static/built/components/hud-visualizer.js +0 -2
- claude_mpm/dashboard/static/built/components/module-viewer.js +0 -2
- claude_mpm/dashboard/static/built/components/nav-bar.js +0 -145
- claude_mpm/dashboard/static/built/components/page-structure.js +0 -429
- claude_mpm/dashboard/static/built/components/session-manager.js +0 -2
- claude_mpm/dashboard/static/built/components/socket-manager.js +0 -2
- claude_mpm/dashboard/static/built/components/ui-state-manager.js +0 -2
- claude_mpm/dashboard/static/built/components/unified-data-viewer.js +0 -2
- claude_mpm/dashboard/static/built/components/working-directory.js +0 -2
- claude_mpm/dashboard/static/built/connection-manager.js +0 -536
- claude_mpm/dashboard/static/built/dashboard.js +0 -2
- claude_mpm/dashboard/static/built/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/built/react/events.js +0 -30
- claude_mpm/dashboard/static/built/shared/dom-helpers.js +0 -396
- claude_mpm/dashboard/static/built/shared/event-bus.js +0 -330
- claude_mpm/dashboard/static/built/shared/event-filter-service.js +0 -540
- claude_mpm/dashboard/static/built/shared/logger.js +0 -385
- claude_mpm/dashboard/static/built/shared/page-structure.js +0 -249
- claude_mpm/dashboard/static/built/shared/tooltip-service.js +0 -253
- claude_mpm/dashboard/static/built/socket-client.js +0 -2
- claude_mpm/dashboard/static/built/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/dist/assets/events.DjpNxWNo.css +0 -1
- claude_mpm/dashboard/static/dist/components/activity-tree.js +0 -2
- claude_mpm/dashboard/static/dist/components/agent-inference.js +0 -2
- claude_mpm/dashboard/static/dist/components/code-tree.js +0 -2
- claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
- claude_mpm/dashboard/static/dist/components/event-processor.js +0 -2
- claude_mpm/dashboard/static/dist/components/event-viewer.js +0 -2
- claude_mpm/dashboard/static/dist/components/export-manager.js +0 -2
- claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +0 -2
- claude_mpm/dashboard/static/dist/components/file-viewer.js +0 -2
- claude_mpm/dashboard/static/dist/components/hud-library-loader.js +0 -2
- claude_mpm/dashboard/static/dist/components/hud-manager.js +0 -2
- claude_mpm/dashboard/static/dist/components/hud-visualizer.js +0 -2
- claude_mpm/dashboard/static/dist/components/module-viewer.js +0 -2
- claude_mpm/dashboard/static/dist/components/session-manager.js +0 -2
- claude_mpm/dashboard/static/dist/components/socket-manager.js +0 -2
- claude_mpm/dashboard/static/dist/components/ui-state-manager.js +0 -2
- claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +0 -2
- claude_mpm/dashboard/static/dist/components/working-directory.js +0 -2
- claude_mpm/dashboard/static/dist/dashboard.js +0 -2
- claude_mpm/dashboard/static/dist/react/events.js +0 -30
- claude_mpm/dashboard/static/dist/socket-client.js +0 -2
- claude_mpm/dashboard/static/events.html +0 -607
- claude_mpm/dashboard/static/index.html +0 -635
- claude_mpm/dashboard/static/js/shared/dom-helpers.js +0 -396
- claude_mpm/dashboard/static/js/shared/event-bus.js +0 -330
- claude_mpm/dashboard/static/js/shared/logger.js +0 -385
- claude_mpm/dashboard/static/js/shared/tooltip-service.js +0 -253
- claude_mpm/dashboard/static/js/stores/dashboard-store.js +0 -562
- claude_mpm/dashboard/static/legacy/activity.html +0 -736
- claude_mpm/dashboard/static/legacy/agents.html +0 -786
- claude_mpm/dashboard/static/legacy/files.html +0 -747
- claude_mpm/dashboard/static/legacy/tools.html +0 -831
- claude_mpm/dashboard/static/monitors.html +0 -431
- claude_mpm/dashboard/static/production/events.html +0 -659
- claude_mpm/dashboard/static/production/main.html +0 -698
- claude_mpm/dashboard/static/production/monitors.html +0 -483
- claude_mpm/dashboard/static/test-archive/dashboard.html +0 -635
- claude_mpm/dashboard/static/test-archive/debug-events.html +0 -147
- claude_mpm/dashboard/static/test-archive/test-navigation.html +0 -256
- claude_mpm/dashboard/static/test-archive/test-react-exports.html +0 -180
- claude_mpm/dashboard/static/test-archive/test_debug.html +0 -25
- claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +0 -75
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +0 -184
- claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +0 -107
- claude_mpm/skills/bundled/collaboration/requesting-code-review/code-reviewer.md +0 -146
- claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +0 -118
- claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +0 -177
- claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +0 -119
- claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +0 -148
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +0 -483
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +0 -452
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +0 -449
- claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +0 -411
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +0 -14
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +0 -58
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +0 -68
- claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +0 -69
- claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +0 -175
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/common-failures.md +0 -213
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +0 -314
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +0 -227
- claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +0 -74
- claude_mpm/skills/bundled/main/internal-comms/SKILL.md +0 -32
- claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +0 -47
- claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +0 -65
- claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +0 -30
- claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +0 -16
- claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +0 -328
- claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +0 -602
- claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +0 -915
- claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +0 -916
- claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +0 -752
- claude_mpm/skills/bundled/main/skill-creator/SKILL.md +0 -209
- claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +0 -123
- claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +0 -145
- claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +0 -543
- claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +0 -741
- claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +0 -470
- claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +0 -458
- claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +0 -639
- claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +0 -304
- claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +0 -96
- claude_mpm/tools/code_tree_analyzer.py +0 -1825
- /claude_mpm/agents/templates/{git_file_tracking.md → git-file-tracking.md} +0 -0
- /claude_mpm/agents/templates/{pm_examples.md → pm-examples.md} +0 -0
- /claude_mpm/agents/templates/{response_format.md → response-format.md} +0 -0
- /claude_mpm/agents/templates/{validation_templates.md → validation-templates.md} +0 -0
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/WHEEL +0 -0
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.20.3.dist-info → claude_mpm-5.1.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
"""Skills Deployer Service - Deploy Claude Code skills from GitHub.
|
|
2
|
+
|
|
3
|
+
WHY: Claude Code loads skills at STARTUP ONLY from ~/.claude/skills/ directory.
|
|
4
|
+
This service manages downloading skills from external GitHub repositories and
|
|
5
|
+
deploying them to Claude Code's skills directory with automatic restart warnings.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Downloads from https://github.com/bobmatnyc/claude-mpm-skills by default
|
|
9
|
+
- Deploys to ~/.claude/skills/ (Claude Code's directory), NOT project directory
|
|
10
|
+
- Integrates with ToolchainAnalyzer for automatic language detection
|
|
11
|
+
- Handles Claude Code restart requirement (skills only load at startup)
|
|
12
|
+
- Provides filtering by toolchain and categories
|
|
13
|
+
- Graceful error handling with actionable messages
|
|
14
|
+
|
|
15
|
+
ARCHITECTURE:
|
|
16
|
+
1. GitHub Download: Fetch ZIP archive from repository
|
|
17
|
+
2. Manifest Parsing: Read skill metadata from manifest.json
|
|
18
|
+
3. Filtering: Apply toolchain and category filters
|
|
19
|
+
4. Deployment: Copy skills to ~/.claude/skills/
|
|
20
|
+
5. Restart Detection: Warn if Claude Code is running
|
|
21
|
+
6. Cleanup: Remove temporary files
|
|
22
|
+
|
|
23
|
+
References:
|
|
24
|
+
- Research: docs/research/skills-research.md
|
|
25
|
+
- GitHub Repo: https://github.com/bobmatnyc/claude-mpm-skills
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import platform
|
|
30
|
+
import shutil
|
|
31
|
+
import subprocess
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, Dict, List, Optional
|
|
34
|
+
|
|
35
|
+
from claude_mpm.core.mixins import LoggerMixin
|
|
36
|
+
from claude_mpm.services.skills_config import SkillsConfig
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SkillsDeployerService(LoggerMixin):
|
|
40
|
+
"""Deploy Claude Code skills from external GitHub repositories.
|
|
41
|
+
|
|
42
|
+
This service:
|
|
43
|
+
- Downloads skills from GitHub repositories
|
|
44
|
+
- Deploys to ~/.claude/skills/ directory
|
|
45
|
+
- Filters by toolchain (python, javascript, rust, etc.)
|
|
46
|
+
- Filters by categories (testing, debugging, web, etc.)
|
|
47
|
+
- Detects Claude Code process and warns about restart requirement
|
|
48
|
+
- Provides deployment summaries and error handling
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> deployer = SkillsDeployerService()
|
|
52
|
+
>>> result = deployer.deploy_skills(toolchain=['python'], categories=['testing'])
|
|
53
|
+
>>> print(f"Deployed {result['deployed_count']} skills")
|
|
54
|
+
>>> print(f"Restart Claude Code: {result['restart_required']}")
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
DEFAULT_REPO_URL = "https://github.com/bobmatnyc/claude-mpm-skills"
|
|
58
|
+
CLAUDE_SKILLS_DIR = Path.home() / ".claude" / "skills"
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
repo_url: Optional[str] = None,
|
|
63
|
+
toolchain_analyzer: Optional[any] = None,
|
|
64
|
+
):
|
|
65
|
+
"""Initialize Skills Deployer Service.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
repo_url: GitHub repository URL (default: bobmatnyc/claude-mpm-skills)
|
|
69
|
+
toolchain_analyzer: Optional ToolchainAnalyzer for auto-detection
|
|
70
|
+
"""
|
|
71
|
+
super().__init__()
|
|
72
|
+
self.repo_url = repo_url or self.DEFAULT_REPO_URL
|
|
73
|
+
self.toolchain_analyzer = toolchain_analyzer
|
|
74
|
+
self.skills_config = SkillsConfig()
|
|
75
|
+
|
|
76
|
+
# Ensure Claude skills directory exists
|
|
77
|
+
self.CLAUDE_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
def deploy_skills(
|
|
80
|
+
self,
|
|
81
|
+
collection: Optional[str] = None,
|
|
82
|
+
toolchain: Optional[List[str]] = None,
|
|
83
|
+
categories: Optional[List[str]] = None,
|
|
84
|
+
force: bool = False,
|
|
85
|
+
) -> Dict:
|
|
86
|
+
"""Deploy skills from GitHub repository.
|
|
87
|
+
|
|
88
|
+
This is the main entry point for skill deployment. It:
|
|
89
|
+
1. Downloads skills from GitHub collection
|
|
90
|
+
2. Parses manifest for metadata
|
|
91
|
+
3. Filters by toolchain and categories
|
|
92
|
+
4. Deploys to ~/.claude/skills/
|
|
93
|
+
5. Warns about Claude Code restart
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
collection: Collection name to deploy from (default: uses default collection)
|
|
97
|
+
toolchain: Filter by toolchain (e.g., ['python', 'javascript'])
|
|
98
|
+
categories: Filter by categories (e.g., ['testing', 'debugging'])
|
|
99
|
+
force: Overwrite existing skills
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict containing:
|
|
103
|
+
- deployed_count: Number of skills deployed
|
|
104
|
+
- skipped_count: Number of skills skipped
|
|
105
|
+
- errors: List of error messages
|
|
106
|
+
- deployed_skills: List of deployed skill names
|
|
107
|
+
- restart_required: True if Claude Code needs restart
|
|
108
|
+
- restart_instructions: Message about restarting
|
|
109
|
+
- collection: Collection name used for deployment
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
>>> result = deployer.deploy_skills(collection="obra-superpowers")
|
|
113
|
+
>>> result = deployer.deploy_skills(toolchain=['python']) # Uses default
|
|
114
|
+
>>> if result['restart_required']:
|
|
115
|
+
>>> print(result['restart_instructions'])
|
|
116
|
+
"""
|
|
117
|
+
# Determine which collection to use
|
|
118
|
+
collection_name = collection or self.skills_config.get_default_collection()
|
|
119
|
+
|
|
120
|
+
self.logger.info(f"Deploying skills from collection '{collection_name}'")
|
|
121
|
+
|
|
122
|
+
# Step 1: Download skills from GitHub collection
|
|
123
|
+
try:
|
|
124
|
+
skills_data = self._download_from_github(collection_name)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.logger.error(f"Failed to download skills: {e}")
|
|
127
|
+
return {
|
|
128
|
+
"deployed_count": 0,
|
|
129
|
+
"skipped_count": 0,
|
|
130
|
+
"errors": [f"Download failed: {e}"],
|
|
131
|
+
"deployed_skills": [],
|
|
132
|
+
"restart_required": False,
|
|
133
|
+
"restart_instructions": "",
|
|
134
|
+
"collection": collection_name,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Step 2: Parse manifest and flatten skills
|
|
138
|
+
manifest = skills_data.get("manifest", {})
|
|
139
|
+
try:
|
|
140
|
+
skills = self._flatten_manifest_skills(manifest)
|
|
141
|
+
except ValueError as e:
|
|
142
|
+
self.logger.error(f"Invalid manifest structure: {e}")
|
|
143
|
+
return {
|
|
144
|
+
"deployed_count": 0,
|
|
145
|
+
"skipped_count": 0,
|
|
146
|
+
"errors": [f"Invalid manifest: {e}"],
|
|
147
|
+
"deployed_skills": [],
|
|
148
|
+
"restart_required": False,
|
|
149
|
+
"restart_instructions": "",
|
|
150
|
+
"collection": collection_name,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
self.logger.info(f"Found {len(skills)} skills in repository")
|
|
154
|
+
|
|
155
|
+
# Step 3: Filter skills
|
|
156
|
+
filtered_skills = self._filter_skills(skills, toolchain, categories)
|
|
157
|
+
|
|
158
|
+
self.logger.info(
|
|
159
|
+
f"After filtering: {len(filtered_skills)} skills to deploy"
|
|
160
|
+
f" (toolchain={toolchain}, categories={categories})"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Step 4: Deploy skills
|
|
164
|
+
deployed = []
|
|
165
|
+
skipped = []
|
|
166
|
+
errors = []
|
|
167
|
+
|
|
168
|
+
for skill in filtered_skills:
|
|
169
|
+
try:
|
|
170
|
+
# Validate skill is a dictionary
|
|
171
|
+
if not isinstance(skill, dict):
|
|
172
|
+
self.logger.error(f"Invalid skill format: {skill}")
|
|
173
|
+
errors.append(f"Invalid skill format: {skill}")
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
result = self._deploy_skill(skill, skills_data["temp_dir"], force=force)
|
|
177
|
+
if result["deployed"]:
|
|
178
|
+
deployed.append(skill["name"])
|
|
179
|
+
elif result["skipped"]:
|
|
180
|
+
skipped.append(skill["name"])
|
|
181
|
+
if result["error"]:
|
|
182
|
+
errors.append(result["error"])
|
|
183
|
+
except Exception as e:
|
|
184
|
+
skill_name = (
|
|
185
|
+
skill.get("name", "unknown")
|
|
186
|
+
if isinstance(skill, dict)
|
|
187
|
+
else "unknown"
|
|
188
|
+
)
|
|
189
|
+
self.logger.error(f"Failed to deploy {skill_name}: {e}")
|
|
190
|
+
errors.append(f"{skill_name}: {e}")
|
|
191
|
+
|
|
192
|
+
# Step 5: Cleanup
|
|
193
|
+
self._cleanup(skills_data["temp_dir"])
|
|
194
|
+
|
|
195
|
+
# Step 6: Check if Claude Code restart needed
|
|
196
|
+
restart_required = len(deployed) > 0
|
|
197
|
+
restart_instructions = ""
|
|
198
|
+
|
|
199
|
+
if restart_required:
|
|
200
|
+
claude_running = self._is_claude_code_running()
|
|
201
|
+
if claude_running:
|
|
202
|
+
restart_instructions = (
|
|
203
|
+
"⚠️ Claude Code is currently running.\n"
|
|
204
|
+
"Skills are only loaded at STARTUP.\n"
|
|
205
|
+
"Please restart Claude Code for new skills to be available.\n\n"
|
|
206
|
+
"How to restart Claude Code:\n"
|
|
207
|
+
"1. Close all Claude Code windows\n"
|
|
208
|
+
"2. Quit Claude Code completely (Cmd+Q on Mac, Alt+F4 on Windows)\n"
|
|
209
|
+
"3. Re-launch Claude Code\n"
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
restart_instructions = (
|
|
213
|
+
"✓ Claude Code is not currently running.\n"
|
|
214
|
+
"Skills will be available when you launch Claude Code.\n"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
self.logger.info(
|
|
218
|
+
f"Deployment complete: {len(deployed)} deployed, "
|
|
219
|
+
f"{len(skipped)} skipped, {len(errors)} errors"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
"deployed_count": len(deployed),
|
|
224
|
+
"skipped_count": len(skipped),
|
|
225
|
+
"errors": errors,
|
|
226
|
+
"deployed_skills": deployed,
|
|
227
|
+
"skipped_skills": skipped,
|
|
228
|
+
"restart_required": restart_required,
|
|
229
|
+
"restart_instructions": restart_instructions,
|
|
230
|
+
"collection": collection_name,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
def list_available_skills(self, collection: Optional[str] = None) -> Dict:
|
|
234
|
+
"""List all available skills from GitHub repository.
|
|
235
|
+
|
|
236
|
+
Downloads manifest and returns skill metadata grouped by category
|
|
237
|
+
and toolchain.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
collection: Collection name to list from (default: uses default collection)
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Dict containing:
|
|
244
|
+
- total_skills: Total number of available skills
|
|
245
|
+
- by_category: Skills grouped by category
|
|
246
|
+
- by_toolchain: Skills grouped by toolchain
|
|
247
|
+
- skills: Full list of skill metadata
|
|
248
|
+
- collection: Collection name used
|
|
249
|
+
|
|
250
|
+
Example:
|
|
251
|
+
>>> result = deployer.list_available_skills(collection="obra-superpowers")
|
|
252
|
+
>>> result = deployer.list_available_skills() # Uses default
|
|
253
|
+
>>> print(f"Available: {result['total_skills']} skills")
|
|
254
|
+
>>> for category, skills in result['by_category'].items():
|
|
255
|
+
>>> print(f"{category}: {len(skills)} skills")
|
|
256
|
+
"""
|
|
257
|
+
collection_name = collection or self.skills_config.get_default_collection()
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
skills_data = self._download_from_github(collection_name)
|
|
261
|
+
manifest = skills_data.get("manifest", {})
|
|
262
|
+
|
|
263
|
+
# Flatten skills from manifest (supports both legacy and new structure)
|
|
264
|
+
try:
|
|
265
|
+
skills = self._flatten_manifest_skills(manifest)
|
|
266
|
+
except ValueError as e:
|
|
267
|
+
self.logger.error(f"Failed to parse manifest: {e}")
|
|
268
|
+
return {
|
|
269
|
+
"total_skills": 0,
|
|
270
|
+
"by_category": {},
|
|
271
|
+
"by_toolchain": {},
|
|
272
|
+
"skills": [],
|
|
273
|
+
"error": str(e),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Group by category
|
|
277
|
+
by_category = {}
|
|
278
|
+
for skill in skills:
|
|
279
|
+
if not isinstance(skill, dict):
|
|
280
|
+
continue
|
|
281
|
+
category = skill.get("category", "uncategorized")
|
|
282
|
+
if category not in by_category:
|
|
283
|
+
by_category[category] = []
|
|
284
|
+
by_category[category].append(skill)
|
|
285
|
+
|
|
286
|
+
# Group by toolchain
|
|
287
|
+
by_toolchain = {}
|
|
288
|
+
for skill in skills:
|
|
289
|
+
if not isinstance(skill, dict):
|
|
290
|
+
continue
|
|
291
|
+
toolchains = skill.get("toolchain", [])
|
|
292
|
+
if isinstance(toolchains, str):
|
|
293
|
+
toolchains = [toolchains]
|
|
294
|
+
elif not isinstance(toolchains, list):
|
|
295
|
+
toolchains = []
|
|
296
|
+
|
|
297
|
+
for toolchain in toolchains:
|
|
298
|
+
if toolchain not in by_toolchain:
|
|
299
|
+
by_toolchain[toolchain] = []
|
|
300
|
+
by_toolchain[toolchain].append(skill)
|
|
301
|
+
|
|
302
|
+
# Cleanup
|
|
303
|
+
self._cleanup(skills_data["temp_dir"])
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"total_skills": len(skills),
|
|
307
|
+
"by_category": by_category,
|
|
308
|
+
"by_toolchain": by_toolchain,
|
|
309
|
+
"skills": skills,
|
|
310
|
+
"collection": collection_name,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.logger.error(f"Failed to list available skills: {e}")
|
|
315
|
+
return {
|
|
316
|
+
"total_skills": 0,
|
|
317
|
+
"by_category": {},
|
|
318
|
+
"by_toolchain": {},
|
|
319
|
+
"skills": [],
|
|
320
|
+
"error": str(e),
|
|
321
|
+
"collection": collection_name,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
def check_deployed_skills(self) -> Dict:
|
|
325
|
+
"""Check which skills are currently deployed.
|
|
326
|
+
|
|
327
|
+
Scans ~/.claude/skills/ directory for deployed skills.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Dict containing:
|
|
331
|
+
- deployed_count: Number of deployed skills
|
|
332
|
+
- skills: List of deployed skill names with paths
|
|
333
|
+
- claude_skills_dir: Path to Claude skills directory
|
|
334
|
+
|
|
335
|
+
Example:
|
|
336
|
+
>>> result = deployer.check_deployed_skills()
|
|
337
|
+
>>> print(f"Currently deployed: {result['deployed_count']} skills")
|
|
338
|
+
"""
|
|
339
|
+
deployed_skills = []
|
|
340
|
+
|
|
341
|
+
if self.CLAUDE_SKILLS_DIR.exists():
|
|
342
|
+
for skill_dir in self.CLAUDE_SKILLS_DIR.iterdir():
|
|
343
|
+
if skill_dir.is_dir() and not skill_dir.name.startswith("."):
|
|
344
|
+
# Check for SKILL.md
|
|
345
|
+
skill_md = skill_dir / "SKILL.md"
|
|
346
|
+
if skill_md.exists():
|
|
347
|
+
deployed_skills.append(
|
|
348
|
+
{"name": skill_dir.name, "path": str(skill_dir)}
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
"deployed_count": len(deployed_skills),
|
|
353
|
+
"skills": deployed_skills,
|
|
354
|
+
"claude_skills_dir": str(self.CLAUDE_SKILLS_DIR),
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
def remove_skills(self, skill_names: Optional[List[str]] = None) -> Dict:
|
|
358
|
+
"""Remove deployed skills.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
skill_names: List of skill names to remove, or None to remove all
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Dict containing:
|
|
365
|
+
- removed_count: Number of skills removed
|
|
366
|
+
- removed_skills: List of removed skill names
|
|
367
|
+
- errors: List of error messages
|
|
368
|
+
|
|
369
|
+
Example:
|
|
370
|
+
>>> # Remove specific skills
|
|
371
|
+
>>> result = deployer.remove_skills(['test-skill', 'debug-skill'])
|
|
372
|
+
>>> # Remove all skills
|
|
373
|
+
>>> result = deployer.remove_skills()
|
|
374
|
+
"""
|
|
375
|
+
removed = []
|
|
376
|
+
errors = []
|
|
377
|
+
|
|
378
|
+
if not self.CLAUDE_SKILLS_DIR.exists():
|
|
379
|
+
return {
|
|
380
|
+
"removed_count": 0,
|
|
381
|
+
"removed_skills": [],
|
|
382
|
+
"errors": ["Claude skills directory does not exist"],
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# Get all skills if no specific names provided
|
|
386
|
+
if skill_names is None:
|
|
387
|
+
skill_names = [
|
|
388
|
+
d.name
|
|
389
|
+
for d in self.CLAUDE_SKILLS_DIR.iterdir()
|
|
390
|
+
if d.is_dir() and not d.name.startswith(".")
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
for skill_name in skill_names:
|
|
394
|
+
skill_dir = self.CLAUDE_SKILLS_DIR / skill_name
|
|
395
|
+
|
|
396
|
+
if not skill_dir.exists():
|
|
397
|
+
errors.append(f"Skill not found: {skill_name}")
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
# Security: Validate path is within CLAUDE_SKILLS_DIR
|
|
402
|
+
if not self._validate_safe_path(self.CLAUDE_SKILLS_DIR, skill_dir):
|
|
403
|
+
raise ValueError(f"Path traversal attempt detected: {skill_dir}")
|
|
404
|
+
|
|
405
|
+
# Remove skill directory
|
|
406
|
+
if skill_dir.is_symlink():
|
|
407
|
+
self.logger.warning(f"Removing symlink: {skill_dir}")
|
|
408
|
+
skill_dir.unlink()
|
|
409
|
+
else:
|
|
410
|
+
shutil.rmtree(skill_dir)
|
|
411
|
+
|
|
412
|
+
removed.append(skill_name)
|
|
413
|
+
self.logger.info(f"Removed skill: {skill_name}")
|
|
414
|
+
|
|
415
|
+
except Exception as e:
|
|
416
|
+
self.logger.error(f"Failed to remove {skill_name}: {e}")
|
|
417
|
+
errors.append(f"{skill_name}: {e}")
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
"removed_count": len(removed),
|
|
421
|
+
"removed_skills": removed,
|
|
422
|
+
"errors": errors,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
def _download_from_github(self, collection_name: str) -> Dict:
|
|
426
|
+
"""Download skills repository from GitHub using git clone/pull.
|
|
427
|
+
|
|
428
|
+
Logic:
|
|
429
|
+
1. Get collection config from SkillsConfig
|
|
430
|
+
2. Determine target directory: ~/.claude/skills/{collection_name}/
|
|
431
|
+
3. Check if directory exists:
|
|
432
|
+
- Exists + is git repo → git pull (update)
|
|
433
|
+
- Exists + not git repo → error (manual cleanup needed)
|
|
434
|
+
- Not exists → git clone (first install)
|
|
435
|
+
4. Parse manifest.json from collection
|
|
436
|
+
5. Update last_update timestamp in config
|
|
437
|
+
6. Return skills data
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
collection_name: Name of collection to download
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Dict containing:
|
|
444
|
+
- temp_dir: Path to collection directory (not temp, but kept for compatibility)
|
|
445
|
+
- manifest: Parsed manifest.json
|
|
446
|
+
- repo_dir: Path to repository directory
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
ValueError: If collection not found or disabled
|
|
450
|
+
Exception: If git operations fail
|
|
451
|
+
"""
|
|
452
|
+
# Get collection configuration
|
|
453
|
+
collection_config = self.skills_config.get_collection(collection_name)
|
|
454
|
+
if not collection_config:
|
|
455
|
+
raise ValueError(
|
|
456
|
+
f"Collection '{collection_name}' not found. "
|
|
457
|
+
f"Use 'claude-mpm skills collection add' to add it."
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if not collection_config.get("enabled", True):
|
|
461
|
+
raise ValueError(
|
|
462
|
+
f"Collection '{collection_name}' is disabled. "
|
|
463
|
+
f"Use 'claude-mpm skills collection enable {collection_name}' to enable it."
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
repo_url = collection_config["url"]
|
|
467
|
+
target_dir = self.CLAUDE_SKILLS_DIR / collection_name
|
|
468
|
+
|
|
469
|
+
self.logger.info(f"Processing collection '{collection_name}' from {repo_url}")
|
|
470
|
+
|
|
471
|
+
# Check if directory exists and handle accordingly
|
|
472
|
+
if target_dir.exists():
|
|
473
|
+
git_dir = target_dir / ".git"
|
|
474
|
+
|
|
475
|
+
if git_dir.exists():
|
|
476
|
+
# Update existing: git pull
|
|
477
|
+
self.logger.info(
|
|
478
|
+
f"Updating existing collection '{collection_name}' at {target_dir}"
|
|
479
|
+
)
|
|
480
|
+
try:
|
|
481
|
+
result = subprocess.run(
|
|
482
|
+
["git", "pull"],
|
|
483
|
+
cwd=target_dir,
|
|
484
|
+
capture_output=True,
|
|
485
|
+
text=True,
|
|
486
|
+
check=True,
|
|
487
|
+
timeout=60,
|
|
488
|
+
)
|
|
489
|
+
self.logger.debug(f"Git pull output: {result.stdout}")
|
|
490
|
+
|
|
491
|
+
except subprocess.CalledProcessError as e:
|
|
492
|
+
raise Exception(
|
|
493
|
+
f"Failed to update collection '{collection_name}': {e.stderr}"
|
|
494
|
+
) from e
|
|
495
|
+
except subprocess.TimeoutExpired as e:
|
|
496
|
+
raise Exception(
|
|
497
|
+
f"Git pull timed out for collection '{collection_name}'"
|
|
498
|
+
) from e
|
|
499
|
+
else:
|
|
500
|
+
# Directory exists but not a git repo - error
|
|
501
|
+
raise ValueError(
|
|
502
|
+
f"Directory {target_dir} exists but is not a git repository. "
|
|
503
|
+
f"Please remove it manually and try again:\n"
|
|
504
|
+
f" rm -rf {target_dir}"
|
|
505
|
+
)
|
|
506
|
+
else:
|
|
507
|
+
# First install: git clone
|
|
508
|
+
self.logger.info(
|
|
509
|
+
f"Installing new collection '{collection_name}' to {target_dir}"
|
|
510
|
+
)
|
|
511
|
+
try:
|
|
512
|
+
result = subprocess.run(
|
|
513
|
+
["git", "clone", repo_url, str(target_dir)],
|
|
514
|
+
capture_output=True,
|
|
515
|
+
text=True,
|
|
516
|
+
check=True,
|
|
517
|
+
timeout=120,
|
|
518
|
+
)
|
|
519
|
+
self.logger.debug(f"Git clone output: {result.stdout}")
|
|
520
|
+
|
|
521
|
+
except subprocess.CalledProcessError as e:
|
|
522
|
+
raise Exception(
|
|
523
|
+
f"Failed to clone collection '{collection_name}': {e.stderr}"
|
|
524
|
+
) from e
|
|
525
|
+
except subprocess.TimeoutExpired as e:
|
|
526
|
+
raise Exception(
|
|
527
|
+
f"Git clone timed out for collection '{collection_name}'"
|
|
528
|
+
) from e
|
|
529
|
+
|
|
530
|
+
# Update last_update timestamp
|
|
531
|
+
self.skills_config.update_collection_timestamp(collection_name)
|
|
532
|
+
|
|
533
|
+
# Parse manifest.json
|
|
534
|
+
manifest_path = target_dir / "manifest.json"
|
|
535
|
+
if not manifest_path.exists():
|
|
536
|
+
raise Exception(
|
|
537
|
+
f"manifest.json not found in collection '{collection_name}' at {target_dir}"
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
542
|
+
manifest = json.load(f)
|
|
543
|
+
except json.JSONDecodeError as e:
|
|
544
|
+
raise Exception(
|
|
545
|
+
f"Invalid manifest.json in collection '{collection_name}': {e}"
|
|
546
|
+
) from e
|
|
547
|
+
|
|
548
|
+
self.logger.info(
|
|
549
|
+
f"Successfully loaded collection '{collection_name}' from {target_dir}"
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# Return data in same format as before for compatibility
|
|
553
|
+
# Note: temp_dir is now the persistent collection directory
|
|
554
|
+
return {"temp_dir": target_dir, "manifest": manifest, "repo_dir": target_dir}
|
|
555
|
+
|
|
556
|
+
def _flatten_manifest_skills(self, manifest: Dict) -> List[Dict]:
|
|
557
|
+
"""Flatten skills from manifest, supporting both structures.
|
|
558
|
+
|
|
559
|
+
Supports both legacy flat list and new nested dict structures:
|
|
560
|
+
- Legacy: {"skills": [skill1, skill2, ...]}
|
|
561
|
+
- New: {"skills": {"universal": [...], "toolchains": {...}}}
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
manifest: The manifest dictionary
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
List of flattened skill dictionaries
|
|
568
|
+
|
|
569
|
+
Raises:
|
|
570
|
+
ValueError: If manifest structure is invalid
|
|
571
|
+
|
|
572
|
+
Example:
|
|
573
|
+
>>> # Legacy flat structure
|
|
574
|
+
>>> manifest = {"skills": [{"name": "skill1"}, {"name": "skill2"}]}
|
|
575
|
+
>>> skills = deployer._flatten_manifest_skills(manifest)
|
|
576
|
+
>>> len(skills) # 2
|
|
577
|
+
|
|
578
|
+
>>> # New nested structure
|
|
579
|
+
>>> manifest = {
|
|
580
|
+
... "skills": {
|
|
581
|
+
... "universal": [{"name": "skill1"}],
|
|
582
|
+
... "toolchains": {"python": [{"name": "skill2"}]}
|
|
583
|
+
... }
|
|
584
|
+
... }
|
|
585
|
+
>>> skills = deployer._flatten_manifest_skills(manifest)
|
|
586
|
+
>>> len(skills) # 2
|
|
587
|
+
"""
|
|
588
|
+
skills_data = manifest.get("skills", {})
|
|
589
|
+
|
|
590
|
+
# Handle legacy flat list structure
|
|
591
|
+
if isinstance(skills_data, list):
|
|
592
|
+
self.logger.debug(
|
|
593
|
+
f"Using legacy flat manifest structure ({len(skills_data)} skills)"
|
|
594
|
+
)
|
|
595
|
+
return skills_data
|
|
596
|
+
|
|
597
|
+
# Handle new nested dict structure
|
|
598
|
+
if isinstance(skills_data, dict):
|
|
599
|
+
flat_skills = []
|
|
600
|
+
|
|
601
|
+
# Add universal skills
|
|
602
|
+
universal_skills = skills_data.get("universal", [])
|
|
603
|
+
if isinstance(universal_skills, list):
|
|
604
|
+
flat_skills.extend(universal_skills)
|
|
605
|
+
self.logger.debug(f"Added {len(universal_skills)} universal skills")
|
|
606
|
+
|
|
607
|
+
# Add toolchain-specific skills
|
|
608
|
+
toolchains = skills_data.get("toolchains", {})
|
|
609
|
+
if isinstance(toolchains, dict):
|
|
610
|
+
for toolchain_name, toolchain_skills in toolchains.items():
|
|
611
|
+
if isinstance(toolchain_skills, list):
|
|
612
|
+
flat_skills.extend(toolchain_skills)
|
|
613
|
+
self.logger.debug(
|
|
614
|
+
f"Added {len(toolchain_skills)} {toolchain_name} skills"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
self.logger.info(
|
|
618
|
+
f"Flattened {len(flat_skills)} total skills from nested structure"
|
|
619
|
+
)
|
|
620
|
+
return flat_skills
|
|
621
|
+
|
|
622
|
+
# Invalid structure
|
|
623
|
+
raise ValueError(
|
|
624
|
+
f"Skills manifest must be a list or dict, got {type(skills_data).__name__}"
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
def _filter_skills(
|
|
628
|
+
self,
|
|
629
|
+
skills: List[Dict],
|
|
630
|
+
toolchain: Optional[List[str]] = None,
|
|
631
|
+
categories: Optional[List[str]] = None,
|
|
632
|
+
) -> List[Dict]:
|
|
633
|
+
"""Filter skills by toolchain and categories.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
skills: List of skill metadata dicts
|
|
637
|
+
toolchain: List of toolchains to include (None = all)
|
|
638
|
+
categories: List of categories to include (None = all)
|
|
639
|
+
|
|
640
|
+
Returns:
|
|
641
|
+
Filtered list of skills
|
|
642
|
+
"""
|
|
643
|
+
# Ensure skills is a list and contains dicts
|
|
644
|
+
if not isinstance(skills, list):
|
|
645
|
+
return []
|
|
646
|
+
|
|
647
|
+
# Filter out non-dict items
|
|
648
|
+
filtered = [s for s in skills if isinstance(s, dict)]
|
|
649
|
+
|
|
650
|
+
# Filter by toolchain
|
|
651
|
+
if toolchain:
|
|
652
|
+
toolchain_lower = [t.lower() for t in toolchain]
|
|
653
|
+
filtered = [
|
|
654
|
+
s
|
|
655
|
+
for s in filtered
|
|
656
|
+
if isinstance(s, dict)
|
|
657
|
+
and any(
|
|
658
|
+
t.lower() in toolchain_lower
|
|
659
|
+
for t in (
|
|
660
|
+
s.get("toolchain", [])
|
|
661
|
+
if isinstance(s.get("toolchain"), list)
|
|
662
|
+
else ([s.get("toolchain")] if s.get("toolchain") else [])
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
]
|
|
666
|
+
|
|
667
|
+
# Filter by categories
|
|
668
|
+
if categories:
|
|
669
|
+
categories_lower = [c.lower() for c in categories]
|
|
670
|
+
filtered = [
|
|
671
|
+
s
|
|
672
|
+
for s in filtered
|
|
673
|
+
if isinstance(s, dict)
|
|
674
|
+
and s.get("category", "").lower() in categories_lower
|
|
675
|
+
]
|
|
676
|
+
|
|
677
|
+
return filtered
|
|
678
|
+
|
|
679
|
+
def _deploy_skill(
|
|
680
|
+
self, skill: Dict, collection_dir: Path, force: bool = False
|
|
681
|
+
) -> Dict:
|
|
682
|
+
"""Deploy a single skill to ~/.claude/skills/.
|
|
683
|
+
|
|
684
|
+
NOTE: With multi-collection support, skills are now stored in collection
|
|
685
|
+
subdirectories. This method creates symlinks or copies to maintain the
|
|
686
|
+
flat structure that Claude Code expects in ~/.claude/skills/.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
skill: Skill metadata dict
|
|
690
|
+
collection_dir: Collection directory containing skills
|
|
691
|
+
force: Overwrite if already exists
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
Dict with deployed, skipped, error flags
|
|
695
|
+
"""
|
|
696
|
+
skill_name = skill["name"]
|
|
697
|
+
target_dir = self.CLAUDE_SKILLS_DIR / skill_name
|
|
698
|
+
|
|
699
|
+
# Check if already deployed
|
|
700
|
+
if target_dir.exists() and not force:
|
|
701
|
+
self.logger.debug(f"Skipped {skill_name} (already deployed)")
|
|
702
|
+
return {"deployed": False, "skipped": True, "error": None}
|
|
703
|
+
|
|
704
|
+
# Find skill source in collection directory
|
|
705
|
+
# Updated structure: collection_dir / skills / category / skill-name
|
|
706
|
+
# OR: collection_dir / universal / skill-name
|
|
707
|
+
# OR: collection_dir / toolchains / toolchain-name / skill-name
|
|
708
|
+
|
|
709
|
+
skills_base = collection_dir / "skills"
|
|
710
|
+
category = skill.get("category", "")
|
|
711
|
+
|
|
712
|
+
# Try multiple possible locations
|
|
713
|
+
source_dir = None
|
|
714
|
+
search_paths = []
|
|
715
|
+
|
|
716
|
+
# Try category-based path
|
|
717
|
+
if category and skills_base.exists():
|
|
718
|
+
search_paths.append(skills_base / category / skill_name)
|
|
719
|
+
|
|
720
|
+
# Try universal/toolchains structure
|
|
721
|
+
if (collection_dir / "universal").exists():
|
|
722
|
+
search_paths.append(collection_dir / "universal" / skill_name)
|
|
723
|
+
|
|
724
|
+
if (collection_dir / "toolchains").exists():
|
|
725
|
+
toolchain_dir = collection_dir / "toolchains"
|
|
726
|
+
for tc in toolchain_dir.iterdir():
|
|
727
|
+
if tc.is_dir():
|
|
728
|
+
search_paths.append(tc / skill_name)
|
|
729
|
+
|
|
730
|
+
# Search in all possible locations
|
|
731
|
+
for path in search_paths:
|
|
732
|
+
if path.exists():
|
|
733
|
+
source_dir = path
|
|
734
|
+
break
|
|
735
|
+
|
|
736
|
+
# Fallback: search recursively for skill in skills directory
|
|
737
|
+
if not source_dir and skills_base.exists():
|
|
738
|
+
for cat_dir in skills_base.iterdir():
|
|
739
|
+
if not cat_dir.is_dir():
|
|
740
|
+
continue
|
|
741
|
+
potential = cat_dir / skill_name
|
|
742
|
+
if potential.exists():
|
|
743
|
+
source_dir = potential
|
|
744
|
+
break
|
|
745
|
+
|
|
746
|
+
if not source_dir or not source_dir.exists():
|
|
747
|
+
return {
|
|
748
|
+
"deployed": False,
|
|
749
|
+
"skipped": False,
|
|
750
|
+
"error": f"Skill source not found: {skill_name} (searched in {collection_dir})",
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
# Security: Validate paths
|
|
754
|
+
if not self._validate_safe_path(collection_dir, source_dir):
|
|
755
|
+
return {
|
|
756
|
+
"deployed": False,
|
|
757
|
+
"skipped": False,
|
|
758
|
+
"error": f"Invalid source path: {source_dir}",
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if not self._validate_safe_path(self.CLAUDE_SKILLS_DIR, target_dir):
|
|
762
|
+
return {
|
|
763
|
+
"deployed": False,
|
|
764
|
+
"skipped": False,
|
|
765
|
+
"error": f"Invalid target path: {target_dir}",
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
try:
|
|
769
|
+
# Remove existing if force
|
|
770
|
+
if target_dir.exists():
|
|
771
|
+
if target_dir.is_symlink():
|
|
772
|
+
target_dir.unlink()
|
|
773
|
+
else:
|
|
774
|
+
shutil.rmtree(target_dir)
|
|
775
|
+
|
|
776
|
+
# Copy skill to Claude skills directory
|
|
777
|
+
# NOTE: We use copy instead of symlink to maintain Claude Code compatibility
|
|
778
|
+
shutil.copytree(source_dir, target_dir)
|
|
779
|
+
|
|
780
|
+
self.logger.debug(
|
|
781
|
+
f"Deployed {skill_name} from {source_dir} to {target_dir}"
|
|
782
|
+
)
|
|
783
|
+
return {"deployed": True, "skipped": False, "error": None}
|
|
784
|
+
|
|
785
|
+
except Exception as e:
|
|
786
|
+
return {"deployed": False, "skipped": False, "error": str(e)}
|
|
787
|
+
|
|
788
|
+
def _validate_safe_path(self, base: Path, target: Path) -> bool:
|
|
789
|
+
"""Ensure target path is within base directory (security).
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
base: Base directory
|
|
793
|
+
target: Target path to validate
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
True if path is safe, False otherwise
|
|
797
|
+
"""
|
|
798
|
+
try:
|
|
799
|
+
target.resolve().relative_to(base.resolve())
|
|
800
|
+
return True
|
|
801
|
+
except ValueError:
|
|
802
|
+
return False
|
|
803
|
+
|
|
804
|
+
def _is_claude_code_running(self) -> bool:
|
|
805
|
+
"""Check if Claude Code process is running.
|
|
806
|
+
|
|
807
|
+
Returns:
|
|
808
|
+
True if Claude Code is running, False otherwise
|
|
809
|
+
"""
|
|
810
|
+
try:
|
|
811
|
+
if platform.system() == "Windows":
|
|
812
|
+
result = subprocess.run(
|
|
813
|
+
["tasklist"], check=False, capture_output=True, text=True, timeout=5
|
|
814
|
+
)
|
|
815
|
+
return "claude" in result.stdout.lower()
|
|
816
|
+
# macOS and Linux
|
|
817
|
+
result = subprocess.run(
|
|
818
|
+
["ps", "aux"], check=False, capture_output=True, text=True, timeout=5
|
|
819
|
+
)
|
|
820
|
+
# Look for "Claude Code" or "claude-code" process
|
|
821
|
+
return (
|
|
822
|
+
"claude code" in result.stdout.lower()
|
|
823
|
+
or "claude-code" in result.stdout.lower()
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
except Exception as e:
|
|
827
|
+
self.logger.debug(f"Failed to check Claude Code process: {e}")
|
|
828
|
+
return False
|
|
829
|
+
|
|
830
|
+
def _cleanup(self, temp_dir: Path) -> None:
|
|
831
|
+
"""Cleanup temporary directory.
|
|
832
|
+
|
|
833
|
+
NOTE: With multi-collection support, temp_dir is now the persistent
|
|
834
|
+
collection directory, so we DON'T delete it. This method is kept for
|
|
835
|
+
backward compatibility but is now a no-op.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
temp_dir: Collection directory (not deleted)
|
|
839
|
+
"""
|
|
840
|
+
# NO-OP: Collection directories are persistent, not temporary
|
|
841
|
+
# Skills are deployed from collection directories to Claude skills dir
|
|
842
|
+
self.logger.debug(f"Collection directory preserved at {temp_dir} (not deleted)")
|
|
843
|
+
|
|
844
|
+
# === Collection Management Methods ===
|
|
845
|
+
|
|
846
|
+
def list_collections(self) -> Dict[str, Any]:
|
|
847
|
+
"""List all configured skill collections.
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
Dict containing:
|
|
851
|
+
- collections: Dict of collection configurations
|
|
852
|
+
- default_collection: Name of default collection
|
|
853
|
+
- enabled_count: Number of enabled collections
|
|
854
|
+
|
|
855
|
+
Example:
|
|
856
|
+
>>> result = deployer.list_collections()
|
|
857
|
+
>>> for name, config in result['collections'].items():
|
|
858
|
+
>>> print(f"{name}: {config['url']} (priority: {config['priority']})")
|
|
859
|
+
"""
|
|
860
|
+
collections = self.skills_config.get_collections()
|
|
861
|
+
default = self.skills_config.get_default_collection()
|
|
862
|
+
enabled = self.skills_config.get_enabled_collections()
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
"collections": collections,
|
|
866
|
+
"default_collection": default,
|
|
867
|
+
"enabled_count": len(enabled),
|
|
868
|
+
"total_count": len(collections),
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
def add_collection(self, name: str, url: str, priority: int = 99) -> Dict[str, Any]:
|
|
872
|
+
"""Add a new skill collection.
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
name: Collection name (must be unique)
|
|
876
|
+
url: GitHub repository URL
|
|
877
|
+
priority: Collection priority (lower = higher priority, default: 99)
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
Dict with operation result
|
|
881
|
+
|
|
882
|
+
Example:
|
|
883
|
+
>>> deployer.add_collection("obra-superpowers", "https://github.com/obra/superpowers")
|
|
884
|
+
"""
|
|
885
|
+
return self.skills_config.add_collection(name, url, priority)
|
|
886
|
+
|
|
887
|
+
def remove_collection(self, name: str) -> Dict[str, Any]:
|
|
888
|
+
"""Remove a skill collection.
|
|
889
|
+
|
|
890
|
+
Args:
|
|
891
|
+
name: Collection name to remove
|
|
892
|
+
|
|
893
|
+
Returns:
|
|
894
|
+
Dict with operation result
|
|
895
|
+
|
|
896
|
+
Example:
|
|
897
|
+
>>> deployer.remove_collection("obra-superpowers")
|
|
898
|
+
"""
|
|
899
|
+
result = self.skills_config.remove_collection(name)
|
|
900
|
+
|
|
901
|
+
# Also remove the collection directory
|
|
902
|
+
collection_dir = self.CLAUDE_SKILLS_DIR / name
|
|
903
|
+
if collection_dir.exists():
|
|
904
|
+
try:
|
|
905
|
+
shutil.rmtree(collection_dir)
|
|
906
|
+
self.logger.info(f"Removed collection directory: {collection_dir}")
|
|
907
|
+
result["directory_removed"] = True
|
|
908
|
+
except Exception as e:
|
|
909
|
+
self.logger.warning(f"Failed to remove directory {collection_dir}: {e}")
|
|
910
|
+
result["directory_removed"] = False
|
|
911
|
+
result["directory_error"] = str(e)
|
|
912
|
+
|
|
913
|
+
return result
|
|
914
|
+
|
|
915
|
+
def enable_collection(self, name: str) -> Dict[str, Any]:
|
|
916
|
+
"""Enable a disabled collection.
|
|
917
|
+
|
|
918
|
+
Args:
|
|
919
|
+
name: Collection name
|
|
920
|
+
|
|
921
|
+
Returns:
|
|
922
|
+
Dict with operation result
|
|
923
|
+
|
|
924
|
+
Example:
|
|
925
|
+
>>> deployer.enable_collection("obra-superpowers")
|
|
926
|
+
"""
|
|
927
|
+
return self.skills_config.enable_collection(name)
|
|
928
|
+
|
|
929
|
+
def disable_collection(self, name: str) -> Dict[str, Any]:
|
|
930
|
+
"""Disable a collection without removing it.
|
|
931
|
+
|
|
932
|
+
Args:
|
|
933
|
+
name: Collection name
|
|
934
|
+
|
|
935
|
+
Returns:
|
|
936
|
+
Dict with operation result
|
|
937
|
+
|
|
938
|
+
Example:
|
|
939
|
+
>>> deployer.disable_collection("obra-superpowers")
|
|
940
|
+
"""
|
|
941
|
+
return self.skills_config.disable_collection(name)
|
|
942
|
+
|
|
943
|
+
def set_default_collection(self, name: str) -> Dict[str, Any]:
|
|
944
|
+
"""Set the default collection for deployments.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
name: Collection name to set as default
|
|
948
|
+
|
|
949
|
+
Returns:
|
|
950
|
+
Dict with operation result
|
|
951
|
+
|
|
952
|
+
Example:
|
|
953
|
+
>>> deployer.set_default_collection("obra-superpowers")
|
|
954
|
+
"""
|
|
955
|
+
return self.skills_config.set_default_collection(name)
|