claude-mpm 4.15.2__py3-none-any.whl → 4.20.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_ENGINEER.md +286 -0
- claude_mpm/agents/BASE_PM.md +255 -23
- claude_mpm/agents/PM_INSTRUCTIONS.md +40 -0
- claude_mpm/agents/agent_loader.py +4 -4
- claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
- claude_mpm/agents/templates/api_qa.json +7 -1
- claude_mpm/agents/templates/clerk-ops.json +8 -1
- claude_mpm/agents/templates/code_analyzer.json +4 -1
- claude_mpm/agents/templates/dart_engineer.json +11 -1
- claude_mpm/agents/templates/data_engineer.json +11 -1
- claude_mpm/agents/templates/documentation.json +6 -1
- claude_mpm/agents/templates/engineer.json +18 -1
- claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
- claude_mpm/agents/templates/golang_engineer.json +11 -1
- claude_mpm/agents/templates/java_engineer.json +12 -2
- claude_mpm/agents/templates/local_ops_agent.json +216 -37
- claude_mpm/agents/templates/nextjs_engineer.json +11 -1
- claude_mpm/agents/templates/ops.json +8 -1
- claude_mpm/agents/templates/php-engineer.json +11 -1
- claude_mpm/agents/templates/project_organizer.json +9 -2
- claude_mpm/agents/templates/prompt-engineer.json +5 -1
- claude_mpm/agents/templates/python_engineer.json +19 -4
- claude_mpm/agents/templates/qa.json +7 -1
- claude_mpm/agents/templates/react_engineer.json +11 -1
- claude_mpm/agents/templates/refactoring_engineer.json +8 -1
- claude_mpm/agents/templates/research.json +4 -1
- claude_mpm/agents/templates/ruby-engineer.json +11 -1
- claude_mpm/agents/templates/rust_engineer.json +23 -8
- claude_mpm/agents/templates/security.json +6 -1
- claude_mpm/agents/templates/svelte-engineer.json +225 -0
- claude_mpm/agents/templates/ticketing.json +6 -1
- claude_mpm/agents/templates/typescript_engineer.json +11 -1
- claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
- claude_mpm/agents/templates/version_control.json +8 -1
- claude_mpm/agents/templates/web_qa.json +7 -1
- claude_mpm/agents/templates/web_ui.json +11 -1
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/configure.py +164 -16
- claude_mpm/cli/commands/configure_agent_display.py +6 -6
- claude_mpm/cli/commands/configure_behavior_manager.py +8 -8
- claude_mpm/cli/commands/configure_navigation.py +20 -18
- claude_mpm/cli/commands/configure_startup_manager.py +14 -14
- claude_mpm/cli/commands/configure_template_editor.py +8 -8
- claude_mpm/cli/commands/mpm_init.py +109 -24
- claude_mpm/cli/commands/skills.py +434 -0
- claude_mpm/cli/executor.py +2 -0
- claude_mpm/cli/interactive/__init__.py +3 -0
- claude_mpm/cli/interactive/skills_wizard.py +491 -0
- claude_mpm/cli/parsers/base_parser.py +7 -0
- claude_mpm/cli/parsers/skills_parser.py +137 -0
- claude_mpm/cli/startup.py +83 -0
- claude_mpm/commands/mpm-auto-configure.md +52 -0
- claude_mpm/commands/mpm-help.md +3 -0
- claude_mpm/commands/mpm-init.md +112 -6
- claude_mpm/commands/mpm-version.md +113 -0
- claude_mpm/commands/mpm.md +1 -0
- claude_mpm/config/agent_config.py +2 -2
- claude_mpm/constants.py +12 -0
- claude_mpm/core/config.py +42 -0
- claude_mpm/core/enums.py +18 -0
- claude_mpm/core/factories.py +1 -1
- claude_mpm/core/optimized_agent_loader.py +3 -3
- claude_mpm/core/types.py +2 -9
- claude_mpm/dashboard/static/js/dashboard.js +0 -14
- claude_mpm/dashboard/templates/index.html +3 -41
- claude_mpm/hooks/__init__.py +8 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
- claude_mpm/hooks/session_resume_hook.py +121 -0
- claude_mpm/models/resume_log.py +340 -0
- claude_mpm/services/agents/auto_config_manager.py +1 -1
- claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
- claude_mpm/services/agents/deployment/agent_validator.py +17 -1
- claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
- claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
- claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
- claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
- claude_mpm/services/agents/local_template_manager.py +1 -1
- claude_mpm/services/agents/recommender.py +47 -0
- claude_mpm/services/cli/resume_service.py +617 -0
- claude_mpm/services/cli/session_manager.py +87 -0
- claude_mpm/services/cli/session_resume_helper.py +352 -0
- claude_mpm/services/core/models/health.py +1 -28
- claude_mpm/services/core/path_resolver.py +1 -1
- claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
- claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
- claude_mpm/services/infrastructure/monitoring/base.py +5 -13
- claude_mpm/services/infrastructure/monitoring/network.py +7 -6
- claude_mpm/services/infrastructure/monitoring/process.py +13 -12
- claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
- claude_mpm/services/infrastructure/monitoring/service.py +16 -15
- claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
- claude_mpm/services/local_ops/__init__.py +1 -1
- claude_mpm/services/local_ops/crash_detector.py +1 -1
- claude_mpm/services/local_ops/health_checks/http_check.py +2 -1
- claude_mpm/services/local_ops/health_checks/process_check.py +2 -1
- claude_mpm/services/local_ops/health_checks/resource_check.py +2 -1
- claude_mpm/services/local_ops/health_manager.py +1 -1
- claude_mpm/services/local_ops/restart_manager.py +1 -1
- claude_mpm/services/mcp_config_manager.py +7 -131
- claude_mpm/services/session_manager.py +205 -1
- claude_mpm/services/shared/async_service_base.py +16 -27
- claude_mpm/services/shared/lifecycle_service_base.py +1 -14
- claude_mpm/services/socketio/handlers/__init__.py +5 -2
- claude_mpm/services/socketio/handlers/hook.py +10 -0
- claude_mpm/services/socketio/handlers/registry.py +4 -2
- claude_mpm/services/socketio/server/main.py +7 -7
- claude_mpm/services/unified/deployment_strategies/local.py +1 -1
- claude_mpm/services/version_service.py +104 -1
- claude_mpm/skills/__init__.py +42 -0
- claude_mpm/skills/agent_skills_injector.py +331 -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 +75 -0
- claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +184 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +107 -0
- claude_mpm/skills/bundled/collaboration/requesting-code-review/code-reviewer.md +146 -0
- claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +118 -0
- claude_mpm/skills/bundled/database-migration.md +199 -0
- claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +177 -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 +175 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/common-failures.md +213 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +314 -0
- claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +227 -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/json-data-handling.md +223 -0
- claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +74 -0
- claude_mpm/skills/bundled/main/internal-comms/SKILL.md +32 -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 +328 -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/scripts/connections.py +150 -0
- claude_mpm/skills/bundled/main/mcp-builder/scripts/evaluation.py +372 -0
- claude_mpm/skills/bundled/main/skill-creator/SKILL.md +209 -0
- claude_mpm/skills/bundled/main/skill-creator/scripts/init_skill.py +302 -0
- claude_mpm/skills/bundled/main/skill-creator/scripts/package_skill.py +111 -0
- claude_mpm/skills/bundled/main/skill-creator/scripts/quick_validate.py +65 -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 +567 -0
- claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
- claude_mpm/skills/bundled/security-scanning.md +327 -0
- claude_mpm/skills/bundled/systematic-debugging.md +473 -0
- claude_mpm/skills/bundled/test-driven-development.md +378 -0
- claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +123 -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/testing-anti-patterns/SKILL.md +304 -0
- claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +96 -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 +40 -0
- claude_mpm/skills/bundled/testing/webapp-testing/examples/static_html_automation.py +34 -0
- claude_mpm/skills/bundled/testing/webapp-testing/scripts/with_server.py +107 -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 +310 -0
- claude_mpm/skills/skills_registry.py +351 -0
- claude_mpm/skills/skills_service.py +730 -0
- claude_mpm/utils/agent_dependency_loader.py +2 -2
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/METADATA +211 -33
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/RECORD +195 -115
- claude_mpm/agents/INSTRUCTIONS_OLD_DEPRECATED.md +0 -602
- claude_mpm/dashboard/static/css/code-tree.css +0 -1639
- claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
- claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
- claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
- claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
- claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
- claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
"""Skills Service - Core service for managing Claude Code skills.
|
|
2
|
+
|
|
3
|
+
This module implements the Skills Service layer for Claude MPM's Skills Integration system.
|
|
4
|
+
It handles skill discovery, deployment, validation, and registry management.
|
|
5
|
+
|
|
6
|
+
Design:
|
|
7
|
+
- Discovers skills from bundled/ directory
|
|
8
|
+
- Deploys skills to .claude/skills/
|
|
9
|
+
- Validates SKILL.md format against 16 validation rules
|
|
10
|
+
- Manages skills registry (config/skills_registry.yaml)
|
|
11
|
+
- Supports version checking and updates
|
|
12
|
+
- Graceful degradation (warn but continue on errors)
|
|
13
|
+
|
|
14
|
+
References:
|
|
15
|
+
- Design: docs/design/claude-mpm-skills-integration-design.md
|
|
16
|
+
- Spec: docs/design/SKILL-MD-FORMAT-SPECIFICATION.md
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
import shutil
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
import yaml
|
|
25
|
+
|
|
26
|
+
from claude_mpm.core.mixins import LoggerMixin
|
|
27
|
+
|
|
28
|
+
# Security constants
|
|
29
|
+
MAX_YAML_SIZE = 10 * 1024 * 1024 # 10MB limit to prevent YAML bombs
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SkillsService(LoggerMixin):
|
|
33
|
+
"""Manages Claude Code skills discovery, deployment, and registry.
|
|
34
|
+
|
|
35
|
+
This service provides:
|
|
36
|
+
- Discovery of bundled skills
|
|
37
|
+
- Deployment to .claude/skills/
|
|
38
|
+
- Validation against SKILL.md format specification
|
|
39
|
+
- Registry management (skill-to-agent mappings)
|
|
40
|
+
- Version checking and updates
|
|
41
|
+
- Graceful error handling
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> service = SkillsService()
|
|
45
|
+
>>> result = service.deploy_bundled_skills()
|
|
46
|
+
>>> print(f"Deployed {len(result['deployed'])} skills")
|
|
47
|
+
>>>
|
|
48
|
+
>>> skills = service.get_skills_for_agent('engineer')
|
|
49
|
+
>>> print(f"Engineer has {len(skills)} skills")
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
"""Initialize Skills Service.
|
|
54
|
+
|
|
55
|
+
Sets up paths for:
|
|
56
|
+
- project_root: Root directory of the project
|
|
57
|
+
- bundled_skills_path: Source bundled skills (src/claude_mpm/skills/bundled)
|
|
58
|
+
- deployed_skills_path: Deployment target (.claude/skills/)
|
|
59
|
+
- registry_path: Skills registry YAML (config/skills_registry.yaml)
|
|
60
|
+
"""
|
|
61
|
+
super().__init__()
|
|
62
|
+
self.project_root: Path = self._get_project_root()
|
|
63
|
+
self.bundled_skills_path: Path = Path(__file__).parent / "bundled"
|
|
64
|
+
self.deployed_skills_path: Path = self.project_root / ".claude" / "skills"
|
|
65
|
+
self.registry_path: Path = (
|
|
66
|
+
Path(__file__).parent.parent.parent.parent / "config" / "skills_registry.yaml"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Load registry
|
|
70
|
+
self.registry: Dict[str, Any] = self._load_registry()
|
|
71
|
+
|
|
72
|
+
def _get_project_root(self) -> Path:
|
|
73
|
+
"""Get project root directory.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Path to project root (directory containing .git or current working directory)
|
|
77
|
+
"""
|
|
78
|
+
# Start from current file and traverse up to find project root
|
|
79
|
+
current = Path.cwd()
|
|
80
|
+
|
|
81
|
+
# Look for .git directory or pyproject.toml
|
|
82
|
+
for parent in [current] + list(current.parents):
|
|
83
|
+
if (parent / ".git").exists() or (parent / "pyproject.toml").exists():
|
|
84
|
+
return parent
|
|
85
|
+
|
|
86
|
+
# Fallback to current directory
|
|
87
|
+
return current
|
|
88
|
+
|
|
89
|
+
def _validate_safe_path(self, base: Path, target: Path) -> bool:
|
|
90
|
+
"""Ensure target path is within base directory to prevent path traversal.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
base: Base directory that should contain the target
|
|
94
|
+
target: Target path to validate
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if path is safe, False otherwise
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
target.resolve().relative_to(base.resolve())
|
|
101
|
+
return True
|
|
102
|
+
except ValueError:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def _load_registry(self) -> Dict[str, Any]:
|
|
106
|
+
"""Load skills registry mapping skills to agents with security checks.
|
|
107
|
+
|
|
108
|
+
The registry file (config/skills_registry.yaml) contains:
|
|
109
|
+
- version: Registry version
|
|
110
|
+
- last_updated: Last update timestamp
|
|
111
|
+
- skill_sources: Source repositories
|
|
112
|
+
- agent_skills: Mapping of agent IDs to skills
|
|
113
|
+
- skills_metadata: Metadata for each skill
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dict containing registry data, or empty dict if graceful degradation
|
|
117
|
+
|
|
118
|
+
Note:
|
|
119
|
+
This method logs warnings but doesn't raise to allow graceful degradation.
|
|
120
|
+
Skills features will be unavailable if registry fails to load.
|
|
121
|
+
"""
|
|
122
|
+
if not self.registry_path.exists():
|
|
123
|
+
self.logger.warning(
|
|
124
|
+
f"Skills registry not found: {self.registry_path}\n"
|
|
125
|
+
f"Skills features will be unavailable. Run 'claude-mpm skills deploy' to initialize."
|
|
126
|
+
)
|
|
127
|
+
return {}
|
|
128
|
+
|
|
129
|
+
# Check file size to prevent YAML bomb
|
|
130
|
+
try:
|
|
131
|
+
file_size = self.registry_path.stat().st_size
|
|
132
|
+
if file_size > MAX_YAML_SIZE:
|
|
133
|
+
self.logger.error(
|
|
134
|
+
f"Registry file too large: {file_size} bytes (max {MAX_YAML_SIZE})"
|
|
135
|
+
)
|
|
136
|
+
return {}
|
|
137
|
+
except OSError as e:
|
|
138
|
+
self.logger.error(f"Failed to stat registry file: {e}")
|
|
139
|
+
return {}
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
with open(self.registry_path, encoding='utf-8') as f:
|
|
143
|
+
registry = yaml.safe_load(f)
|
|
144
|
+
if not registry:
|
|
145
|
+
self.logger.warning(f"Empty registry file: {self.registry_path}")
|
|
146
|
+
return {}
|
|
147
|
+
self.logger.debug(f"Loaded skills registry from {self.registry_path}")
|
|
148
|
+
return registry
|
|
149
|
+
except yaml.YAMLError as e:
|
|
150
|
+
self.logger.error(f"Invalid YAML in registry: {e}")
|
|
151
|
+
return {}
|
|
152
|
+
except OSError as e:
|
|
153
|
+
self.logger.error(f"Failed to read registry file: {e}")
|
|
154
|
+
return {}
|
|
155
|
+
|
|
156
|
+
def discover_bundled_skills(self) -> List[Dict[str, Any]]:
|
|
157
|
+
"""Discover all skills in bundled directory.
|
|
158
|
+
|
|
159
|
+
Scans bundled_skills_path for skills organized by category:
|
|
160
|
+
bundled/
|
|
161
|
+
├── development/
|
|
162
|
+
│ ├── test-driven-development/
|
|
163
|
+
│ │ └── SKILL.md
|
|
164
|
+
│ └── systematic-debugging/
|
|
165
|
+
│ └── SKILL.md
|
|
166
|
+
└── testing/
|
|
167
|
+
└── ...
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of skill dictionaries containing:
|
|
171
|
+
- name: Skill name (directory name)
|
|
172
|
+
- category: Category (parent directory name)
|
|
173
|
+
- path: Full path to skill directory
|
|
174
|
+
- metadata: Parsed YAML frontmatter from SKILL.md
|
|
175
|
+
"""
|
|
176
|
+
skills = []
|
|
177
|
+
|
|
178
|
+
if not self.bundled_skills_path.exists():
|
|
179
|
+
self.logger.warning(f"Bundled skills path not found: {self.bundled_skills_path}")
|
|
180
|
+
return skills
|
|
181
|
+
|
|
182
|
+
for category_dir in self.bundled_skills_path.iterdir():
|
|
183
|
+
if not category_dir.is_dir() or category_dir.name.startswith('.'):
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
for skill_dir in category_dir.iterdir():
|
|
187
|
+
if not skill_dir.is_dir():
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
skill_md = skill_dir / "SKILL.md"
|
|
191
|
+
if skill_md.exists():
|
|
192
|
+
metadata = self._parse_skill_metadata(skill_md)
|
|
193
|
+
skills.append({
|
|
194
|
+
'name': skill_dir.name,
|
|
195
|
+
'category': category_dir.name,
|
|
196
|
+
'path': skill_dir,
|
|
197
|
+
'metadata': metadata
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
self.logger.info(f"Discovered {len(skills)} bundled skills")
|
|
201
|
+
return skills
|
|
202
|
+
|
|
203
|
+
def _parse_skill_metadata(self, skill_md: Path) -> Dict[str, Any]:
|
|
204
|
+
"""Extract YAML frontmatter from SKILL.md.
|
|
205
|
+
|
|
206
|
+
Parses the YAML frontmatter section at the beginning of SKILL.md files:
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
name: skill-name
|
|
210
|
+
description: Brief description
|
|
211
|
+
version: 1.0.0
|
|
212
|
+
category: development
|
|
213
|
+
...
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
skill_md: Path to SKILL.md file
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dict containing frontmatter metadata, or empty dict if parsing fails
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
content = skill_md.read_text(encoding='utf-8')
|
|
224
|
+
|
|
225
|
+
# Match YAML frontmatter: ---\n...yaml...\n---
|
|
226
|
+
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
|
227
|
+
|
|
228
|
+
if not match:
|
|
229
|
+
self.logger.warning(f"No YAML frontmatter found in {skill_md}")
|
|
230
|
+
return {}
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
metadata = yaml.safe_load(match.group(1))
|
|
234
|
+
return metadata or {}
|
|
235
|
+
except yaml.YAMLError as e:
|
|
236
|
+
self.logger.error(f"Failed to parse YAML frontmatter in {skill_md}: {e}")
|
|
237
|
+
return {}
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.logger.error(f"Failed to read skill file {skill_md}: {e}")
|
|
240
|
+
return {}
|
|
241
|
+
|
|
242
|
+
def deploy_bundled_skills(self, force: bool = False) -> Dict[str, Any]:
|
|
243
|
+
"""Deploy bundled skills to .claude/skills/ directory.
|
|
244
|
+
|
|
245
|
+
Copies skills from bundled/ to .claude/skills/ maintaining directory structure.
|
|
246
|
+
Skips already-deployed skills unless force=True.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
force: If True, redeploy even if skill already exists
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Dict containing:
|
|
253
|
+
- deployed: List of successfully deployed skill names
|
|
254
|
+
- skipped: List of skipped skill names (already deployed)
|
|
255
|
+
- errors: List of dicts with 'skill' and 'error' keys
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
>>> result = service.deploy_bundled_skills(force=True)
|
|
259
|
+
>>> print(f"Deployed: {len(result['deployed'])}")
|
|
260
|
+
>>> print(f"Errors: {len(result['errors'])}")
|
|
261
|
+
"""
|
|
262
|
+
skills = self.discover_bundled_skills()
|
|
263
|
+
deployed = []
|
|
264
|
+
skipped = []
|
|
265
|
+
errors = []
|
|
266
|
+
|
|
267
|
+
# Ensure deployment directory exists
|
|
268
|
+
self.deployed_skills_path.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
|
|
270
|
+
for skill in skills:
|
|
271
|
+
try:
|
|
272
|
+
# Create category directory in deployment location
|
|
273
|
+
target_category_dir = self.deployed_skills_path / skill['category']
|
|
274
|
+
target_category_dir.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
|
|
276
|
+
# Target path for this skill
|
|
277
|
+
target_dir = target_category_dir / skill['name']
|
|
278
|
+
|
|
279
|
+
# SECURITY: Validate path is within deployed_skills_path
|
|
280
|
+
if not self._validate_safe_path(self.deployed_skills_path, target_dir):
|
|
281
|
+
raise ValueError(f"Path traversal attempt detected: {target_dir}")
|
|
282
|
+
|
|
283
|
+
# Check if already deployed
|
|
284
|
+
if target_dir.exists() and not force:
|
|
285
|
+
skipped.append(skill['name'])
|
|
286
|
+
self.logger.debug(f"Skipped {skill['name']} (already deployed)")
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
# Deploy skill
|
|
290
|
+
if target_dir.exists():
|
|
291
|
+
# SECURITY: Verify again before deletion and check for symlinks
|
|
292
|
+
if not self._validate_safe_path(self.deployed_skills_path, target_dir):
|
|
293
|
+
raise ValueError("Refusing to delete path outside skills directory")
|
|
294
|
+
|
|
295
|
+
if target_dir.is_symlink():
|
|
296
|
+
self.logger.warning(f"Refusing to delete symlink: {target_dir}")
|
|
297
|
+
target_dir.unlink()
|
|
298
|
+
else:
|
|
299
|
+
shutil.rmtree(target_dir)
|
|
300
|
+
|
|
301
|
+
shutil.copytree(skill['path'], target_dir)
|
|
302
|
+
|
|
303
|
+
deployed.append(skill['name'])
|
|
304
|
+
self.logger.debug(f"Deployed skill: {skill['name']}")
|
|
305
|
+
|
|
306
|
+
except (ValueError, OSError) as e:
|
|
307
|
+
self.logger.error(f"Failed to deploy {skill['name']}: {e}")
|
|
308
|
+
errors.append({'skill': skill['name'], 'error': str(e)})
|
|
309
|
+
|
|
310
|
+
self.logger.info(
|
|
311
|
+
f"Skills deployment: {len(deployed)} deployed, "
|
|
312
|
+
f"{len(skipped)} skipped, {len(errors)} errors"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
'deployed': deployed,
|
|
317
|
+
'skipped': skipped,
|
|
318
|
+
'errors': errors
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
def get_skills_for_agent(self, agent_id: str) -> List[str]:
|
|
322
|
+
"""Get list of skills assigned to specific agent.
|
|
323
|
+
|
|
324
|
+
Reads from registry['agent_skills'][agent_id] and combines
|
|
325
|
+
'required' and 'optional' skill lists.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
agent_id: Agent identifier (e.g., 'engineer', 'python_engineer')
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
List of skill names assigned to this agent
|
|
332
|
+
|
|
333
|
+
Example:
|
|
334
|
+
>>> skills = service.get_skills_for_agent('engineer')
|
|
335
|
+
>>> # Returns: ['test-driven-development', 'systematic-debugging', ...]
|
|
336
|
+
"""
|
|
337
|
+
if 'agent_skills' not in self.registry:
|
|
338
|
+
return []
|
|
339
|
+
|
|
340
|
+
agent_skills = self.registry['agent_skills'].get(agent_id, {})
|
|
341
|
+
|
|
342
|
+
# Combine required and optional skills
|
|
343
|
+
required = agent_skills.get('required', [])
|
|
344
|
+
optional = agent_skills.get('optional', [])
|
|
345
|
+
|
|
346
|
+
return required + optional
|
|
347
|
+
|
|
348
|
+
def validate_skill(self, skill_name: str) -> Dict[str, Any]:
|
|
349
|
+
"""Validate skill structure and metadata.
|
|
350
|
+
|
|
351
|
+
Searches for skill in deployed or bundled locations and validates:
|
|
352
|
+
- SKILL.md exists
|
|
353
|
+
- YAML frontmatter is valid
|
|
354
|
+
- Required fields are present (name, description, version, category)
|
|
355
|
+
- Field formats and lengths are correct
|
|
356
|
+
- Progressive disclosure structure is valid
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
skill_name: Name of skill to validate
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Dict containing:
|
|
363
|
+
- valid: True if all critical checks pass
|
|
364
|
+
- errors: List of error messages
|
|
365
|
+
- warnings: List of warning messages
|
|
366
|
+
- metadata: Parsed metadata (if valid)
|
|
367
|
+
"""
|
|
368
|
+
# Find skill in deployed or bundled paths
|
|
369
|
+
skill_paths = [
|
|
370
|
+
self.deployed_skills_path,
|
|
371
|
+
self.bundled_skills_path
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
for base_path in skill_paths:
|
|
375
|
+
if not base_path.exists():
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
for category_dir in base_path.iterdir():
|
|
379
|
+
if not category_dir.is_dir():
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
skill_dir = category_dir / skill_name
|
|
383
|
+
if skill_dir.exists():
|
|
384
|
+
return self._validate_skill_structure(skill_dir)
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
'valid': False,
|
|
388
|
+
'errors': [f"Skill not found: {skill_name}"],
|
|
389
|
+
'warnings': []
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
def _validate_skill_structure(self, skill_dir: Path) -> Dict[str, Any]:
|
|
393
|
+
"""Validate skill directory structure.
|
|
394
|
+
|
|
395
|
+
Implements validation rules from SKILL-MD-FORMAT-SPECIFICATION.md:
|
|
396
|
+
- Rule 1: SKILL.md exists
|
|
397
|
+
- Rule 2: YAML frontmatter present
|
|
398
|
+
- Rule 5: Required fields present
|
|
399
|
+
- Rule 6: Name format valid
|
|
400
|
+
- Rule 8: Description length valid
|
|
401
|
+
- Additional format checks
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
skill_dir: Path to skill directory
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Dict with validation results (valid, errors, warnings, metadata)
|
|
408
|
+
"""
|
|
409
|
+
errors = []
|
|
410
|
+
warnings = []
|
|
411
|
+
|
|
412
|
+
# Rule 1: Check SKILL.md exists
|
|
413
|
+
skill_md = skill_dir / "SKILL.md"
|
|
414
|
+
if not skill_md.exists():
|
|
415
|
+
errors.append("Missing SKILL.md")
|
|
416
|
+
return {
|
|
417
|
+
'valid': False,
|
|
418
|
+
'errors': errors,
|
|
419
|
+
'warnings': warnings
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# Parse and validate metadata
|
|
423
|
+
metadata = self._parse_skill_metadata(skill_md)
|
|
424
|
+
|
|
425
|
+
if not metadata:
|
|
426
|
+
errors.append("Missing or invalid YAML frontmatter")
|
|
427
|
+
return {
|
|
428
|
+
'valid': False,
|
|
429
|
+
'errors': errors,
|
|
430
|
+
'warnings': warnings
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# Rule 5: Required fields
|
|
434
|
+
required_fields = ['name', 'description', 'version', 'category', 'progressive_disclosure']
|
|
435
|
+
for field in required_fields:
|
|
436
|
+
if field not in metadata:
|
|
437
|
+
errors.append(f"Missing required field: {field}")
|
|
438
|
+
|
|
439
|
+
# Rule 6: Name format
|
|
440
|
+
if 'name' in metadata:
|
|
441
|
+
name = metadata['name']
|
|
442
|
+
if not re.match(r'^[a-z][a-z0-9-]*[a-z0-9]$', name):
|
|
443
|
+
errors.append(f"Invalid name format: {name}")
|
|
444
|
+
|
|
445
|
+
# Rule 8: Description length
|
|
446
|
+
if 'description' in metadata:
|
|
447
|
+
desc_len = len(metadata['description'])
|
|
448
|
+
if desc_len < 10 or desc_len > 150:
|
|
449
|
+
errors.append(f"Description must be 10-150 characters (found {desc_len})")
|
|
450
|
+
|
|
451
|
+
# Check for optional directories
|
|
452
|
+
if (skill_dir / "scripts").exists():
|
|
453
|
+
warnings.append("Contains scripts/ directory")
|
|
454
|
+
|
|
455
|
+
if (skill_dir / "references").exists():
|
|
456
|
+
warnings.append("Contains references/ directory")
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
'valid': len(errors) == 0,
|
|
460
|
+
'errors': errors,
|
|
461
|
+
'warnings': warnings,
|
|
462
|
+
'metadata': metadata
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
def check_for_updates(self) -> Dict[str, Any]:
|
|
466
|
+
"""Compare versions of bundled vs deployed skills.
|
|
467
|
+
|
|
468
|
+
Checks each deployed skill against its bundled version to identify:
|
|
469
|
+
- Skills with available updates
|
|
470
|
+
- Skills only in bundled (not deployed)
|
|
471
|
+
- Skills only in deployed (orphaned)
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Dict containing:
|
|
475
|
+
- updates_available: List of dicts with skill names and versions
|
|
476
|
+
- up_to_date: List of skill names
|
|
477
|
+
- not_deployed: List of skill names (in bundled, not deployed)
|
|
478
|
+
- orphaned: List of skill names (in deployed, not bundled)
|
|
479
|
+
"""
|
|
480
|
+
bundled = {s['name']: s for s in self.discover_bundled_skills()}
|
|
481
|
+
|
|
482
|
+
# Discover deployed skills
|
|
483
|
+
deployed = {}
|
|
484
|
+
if self.deployed_skills_path.exists():
|
|
485
|
+
for category_dir in self.deployed_skills_path.iterdir():
|
|
486
|
+
if not category_dir.is_dir():
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
for skill_dir in category_dir.iterdir():
|
|
490
|
+
if not skill_dir.is_dir():
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
skill_md = skill_dir / "SKILL.md"
|
|
494
|
+
if skill_md.exists():
|
|
495
|
+
metadata = self._parse_skill_metadata(skill_md)
|
|
496
|
+
deployed[skill_dir.name] = {
|
|
497
|
+
'name': skill_dir.name,
|
|
498
|
+
'category': category_dir.name,
|
|
499
|
+
'path': skill_dir,
|
|
500
|
+
'metadata': metadata
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
updates_available = []
|
|
504
|
+
up_to_date = []
|
|
505
|
+
not_deployed = []
|
|
506
|
+
orphaned = []
|
|
507
|
+
|
|
508
|
+
# Check for updates
|
|
509
|
+
for name, bundled_skill in bundled.items():
|
|
510
|
+
bundled_version = bundled_skill['metadata'].get('version', '0.0.0')
|
|
511
|
+
|
|
512
|
+
if name not in deployed:
|
|
513
|
+
not_deployed.append(name)
|
|
514
|
+
else:
|
|
515
|
+
deployed_version = deployed[name]['metadata'].get('version', '0.0.0')
|
|
516
|
+
|
|
517
|
+
if deployed_version != bundled_version:
|
|
518
|
+
updates_available.append({
|
|
519
|
+
'name': name,
|
|
520
|
+
'current_version': deployed_version,
|
|
521
|
+
'new_version': bundled_version
|
|
522
|
+
})
|
|
523
|
+
else:
|
|
524
|
+
up_to_date.append(name)
|
|
525
|
+
|
|
526
|
+
# Check for orphaned skills
|
|
527
|
+
for name in deployed:
|
|
528
|
+
if name not in bundled:
|
|
529
|
+
orphaned.append(name)
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
'updates_available': updates_available,
|
|
533
|
+
'up_to_date': up_to_date,
|
|
534
|
+
'not_deployed': not_deployed,
|
|
535
|
+
'orphaned': orphaned
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
def update_skills(self, skill_names: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
539
|
+
"""Update specific or all skills.
|
|
540
|
+
|
|
541
|
+
Redeploys skills from bundled to deployed location.
|
|
542
|
+
If skill_names is None, updates all skills with available updates.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
skill_names: List of skill names to update, or None for all
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Dict containing:
|
|
549
|
+
- updated: List of successfully updated skill names
|
|
550
|
+
- errors: List of dicts with 'skill' and 'error' keys
|
|
551
|
+
"""
|
|
552
|
+
if skill_names is None:
|
|
553
|
+
# Get all skills with available updates
|
|
554
|
+
check_result = self.check_for_updates()
|
|
555
|
+
skill_names = [s['name'] for s in check_result['updates_available']]
|
|
556
|
+
|
|
557
|
+
if not skill_names:
|
|
558
|
+
self.logger.info("No skills to update")
|
|
559
|
+
return {'updated': [], 'errors': []}
|
|
560
|
+
|
|
561
|
+
updated = []
|
|
562
|
+
errors = []
|
|
563
|
+
|
|
564
|
+
bundled = {s['name']: s for s in self.discover_bundled_skills()}
|
|
565
|
+
|
|
566
|
+
for skill_name in skill_names:
|
|
567
|
+
if skill_name not in bundled:
|
|
568
|
+
errors.append({
|
|
569
|
+
'skill': skill_name,
|
|
570
|
+
'error': 'Skill not found in bundled skills'
|
|
571
|
+
})
|
|
572
|
+
continue
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
skill = bundled[skill_name]
|
|
576
|
+
target_dir = self.deployed_skills_path / skill['category'] / skill['name']
|
|
577
|
+
|
|
578
|
+
# SECURITY: Validate path is within deployed_skills_path
|
|
579
|
+
if not self._validate_safe_path(self.deployed_skills_path, target_dir):
|
|
580
|
+
raise ValueError(f"Path traversal attempt detected: {target_dir}")
|
|
581
|
+
|
|
582
|
+
# Remove old version
|
|
583
|
+
if target_dir.exists():
|
|
584
|
+
# SECURITY: Check for symlinks before deletion
|
|
585
|
+
if target_dir.is_symlink():
|
|
586
|
+
self.logger.warning(f"Refusing to delete symlink: {target_dir}")
|
|
587
|
+
target_dir.unlink()
|
|
588
|
+
else:
|
|
589
|
+
shutil.rmtree(target_dir)
|
|
590
|
+
|
|
591
|
+
# Deploy new version
|
|
592
|
+
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
593
|
+
shutil.copytree(skill['path'], target_dir)
|
|
594
|
+
|
|
595
|
+
updated.append(skill_name)
|
|
596
|
+
self.logger.info(f"Updated skill: {skill_name}")
|
|
597
|
+
|
|
598
|
+
except (ValueError, OSError) as e:
|
|
599
|
+
errors.append({'skill': skill_name, 'error': str(e)})
|
|
600
|
+
self.logger.error(f"Failed to update {skill_name}: {e}")
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
'updated': updated,
|
|
604
|
+
'errors': errors
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
def install_updates(self, updates: List[Dict[str, Any]], force: bool = False) -> Dict[str, Any]:
|
|
608
|
+
"""Install skill updates from update check results.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
updates: List of update dicts from check_for_updates()
|
|
612
|
+
force: Force update even if versions match
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
Dict containing updated skills and errors
|
|
616
|
+
"""
|
|
617
|
+
skill_names = [update['skill'] for update in updates]
|
|
618
|
+
return self.update_skills(skill_names)
|
|
619
|
+
|
|
620
|
+
def get_skill_path(self, skill_name: str) -> Optional[Path]:
|
|
621
|
+
"""Get the path to a deployed skill.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
skill_name: Name of the skill
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
Path to the skill directory, or None if not found
|
|
628
|
+
"""
|
|
629
|
+
if self.deployed_skills_path.exists():
|
|
630
|
+
for category_dir in self.deployed_skills_path.iterdir():
|
|
631
|
+
if not category_dir.is_dir():
|
|
632
|
+
continue
|
|
633
|
+
|
|
634
|
+
skill_dir = category_dir / skill_name
|
|
635
|
+
if skill_dir.exists():
|
|
636
|
+
return skill_dir
|
|
637
|
+
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
def parse_skill_metadata(self, content: str) -> Dict[str, Any]:
|
|
641
|
+
"""Parse metadata from SKILL.md content.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
content: Content of SKILL.md file
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
Dict with extracted metadata
|
|
648
|
+
"""
|
|
649
|
+
metadata = {}
|
|
650
|
+
lines = content.split('\n')
|
|
651
|
+
|
|
652
|
+
for line in lines[:50]: # Check first 50 lines for metadata
|
|
653
|
+
line = line.strip()
|
|
654
|
+
|
|
655
|
+
# Parse YAML-style metadata
|
|
656
|
+
if line.startswith('version:'):
|
|
657
|
+
metadata['version'] = line.split(':', 1)[1].strip()
|
|
658
|
+
elif line.startswith('description:'):
|
|
659
|
+
metadata['description'] = line.split(':', 1)[1].strip()
|
|
660
|
+
elif line.startswith('category:'):
|
|
661
|
+
metadata['category'] = line.split(':', 1)[1].strip()
|
|
662
|
+
elif line.startswith('source:'):
|
|
663
|
+
metadata['source'] = line.split(':', 1)[1].strip()
|
|
664
|
+
|
|
665
|
+
return metadata
|
|
666
|
+
|
|
667
|
+
def get_agents_for_skill(self, skill_name: str) -> List[str]:
|
|
668
|
+
"""Get list of agents that use a specific skill.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
skill_name: Name of the skill
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
List of agent IDs that use this skill
|
|
675
|
+
"""
|
|
676
|
+
agents = []
|
|
677
|
+
registry = self._load_registry()
|
|
678
|
+
|
|
679
|
+
agent_capabilities = registry.get('agent_capabilities', {})
|
|
680
|
+
for agent_id, capabilities in agent_capabilities.items():
|
|
681
|
+
primary_workflows = capabilities.get('primary_workflows', [])
|
|
682
|
+
enhanced_capabilities = capabilities.get('enhanced_capabilities', [])
|
|
683
|
+
all_skills = primary_workflows + enhanced_capabilities
|
|
684
|
+
|
|
685
|
+
if skill_name in all_skills:
|
|
686
|
+
agents.append(agent_id)
|
|
687
|
+
|
|
688
|
+
return agents
|
|
689
|
+
|
|
690
|
+
def get_config_path(self, scope: str = "project") -> Path:
|
|
691
|
+
"""Get the configuration file path for a given scope.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
scope: Configuration scope (system, user, project)
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
Path to the configuration file
|
|
698
|
+
"""
|
|
699
|
+
if scope == "system":
|
|
700
|
+
# System-wide config (bundled)
|
|
701
|
+
return self.bundled_skills_path.parent.parent / "config" / "skills_registry.yaml"
|
|
702
|
+
if scope == "user":
|
|
703
|
+
# User config (~/.config/claude-mpm/)
|
|
704
|
+
home = Path.home()
|
|
705
|
+
return home / ".config" / "claude-mpm" / "skills_registry.yaml"
|
|
706
|
+
# project
|
|
707
|
+
# Project config (.claude/)
|
|
708
|
+
project_root = self._get_project_root()
|
|
709
|
+
return project_root / ".claude" / "skills_config.yaml"
|
|
710
|
+
|
|
711
|
+
def create_default_config(self, scope: str = "project") -> None:
|
|
712
|
+
"""Create a default configuration file.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
scope: Configuration scope (system, user, project)
|
|
716
|
+
"""
|
|
717
|
+
config_path = self.get_config_path(scope)
|
|
718
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
719
|
+
|
|
720
|
+
default_config = {
|
|
721
|
+
'version': '2.0.0',
|
|
722
|
+
'skills': {
|
|
723
|
+
'auto_deploy': True,
|
|
724
|
+
'update_check': True
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
import yaml
|
|
729
|
+
config_path.write_text(yaml.dump(default_config, default_flow_style=False))
|
|
730
|
+
self.logger.info(f"Created default config at {config_path}")
|