mirror of
https://git.openapi.site/https://github.com/desirecore/market.git
synced 2026-04-21 17:10:42 +08:00
feat: 添加 skill-creator 内置技能
适配 DesireCore 系统的技能创建器,兼容 Claude Code 基础格式: - SKILL.md: 完整 frontmatter + L0/L1/L2 分层内容 - init_skill.py: 支持 --format basic|desirecore - quick_validate.py: 移除白名单限制,改 Schema 校验 - package_skill.py: 新增 --install API 安装模式 - references/desirecore-format.md: 完整字段参考
This commit is contained in:
261
skills/skill-creator/scripts/init_skill.py
Executable file
261
skills/skill-creator/scripts/init_skill.py
Executable file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Initializer - Creates a new skill from template
|
||||
|
||||
Usage:
|
||||
init_skill.py <skill-name> --path <path> [--format basic|desirecore]
|
||||
|
||||
Examples:
|
||||
init_skill.py my-new-skill --path ~/.desirecore/skills
|
||||
init_skill.py my-api-helper --path ~/.desirecore/skills --format basic
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import date
|
||||
|
||||
|
||||
# ==================== DesireCore 完整格式模板 ====================
|
||||
|
||||
DESIRECORE_TEMPLATE = """\
|
||||
---
|
||||
name: {skill_name}
|
||||
description: >-
|
||||
[TODO: 完整描述技能用途。必须包含 "Use when" 触发提示,
|
||||
帮助 AI 判断何时使用该技能。]
|
||||
version: 1.0.0
|
||||
type: procedural
|
||||
risk_level: low
|
||||
status: enabled
|
||||
tags:
|
||||
- [TODO: 添加标签]
|
||||
metadata:
|
||||
author: user
|
||||
updated_at: '{today}'
|
||||
---
|
||||
|
||||
# {skill_title}
|
||||
|
||||
## L0:一句话摘要
|
||||
|
||||
[TODO: 用一句话描述这个技能做什么]
|
||||
|
||||
## L1:概述与使用场景
|
||||
|
||||
### 能力描述
|
||||
|
||||
[TODO: 详细描述技能的核心能力]
|
||||
|
||||
### 使用场景
|
||||
|
||||
- [TODO: 场景 1]
|
||||
- [TODO: 场景 2]
|
||||
|
||||
### 核心价值
|
||||
|
||||
- [TODO: 价值 1]
|
||||
|
||||
## L2:详细规范
|
||||
|
||||
### 具体操作步骤
|
||||
|
||||
[TODO: 按步骤描述执行流程]
|
||||
|
||||
### 错误处理
|
||||
|
||||
| 错误场景 | 处理方式 |
|
||||
|---------|---------|
|
||||
| [TODO] | [TODO] |
|
||||
"""
|
||||
|
||||
|
||||
# ==================== Claude Code 基础格式模板 ====================
|
||||
|
||||
BASIC_TEMPLATE = """\
|
||||
---
|
||||
name: {skill_name}
|
||||
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
|
||||
---
|
||||
|
||||
# {skill_title}
|
||||
|
||||
## Overview
|
||||
|
||||
[TODO: 1-2 sentences explaining what this skill enables]
|
||||
|
||||
## [TODO: Replace with first main section]
|
||||
|
||||
[TODO: Add content here]
|
||||
|
||||
## Resources
|
||||
|
||||
This skill includes example resource directories:
|
||||
|
||||
### scripts/
|
||||
Executable code for tasks that require deterministic reliability.
|
||||
|
||||
### references/
|
||||
Documentation and reference material loaded into context as needed.
|
||||
|
||||
### assets/
|
||||
Files used within the output (templates, images, fonts, etc.).
|
||||
|
||||
---
|
||||
|
||||
**Delete any unneeded directories.** Not every skill requires all three.
|
||||
"""
|
||||
|
||||
|
||||
EXAMPLE_SCRIPT = '''\
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example helper script for {skill_name}
|
||||
|
||||
Replace with actual implementation or delete if not needed.
|
||||
"""
|
||||
|
||||
def main():
|
||||
print("Example script for {skill_name}")
|
||||
# TODO: Add actual script logic
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
EXAMPLE_REFERENCE = """\
|
||||
# Reference Documentation for {skill_title}
|
||||
|
||||
Replace with actual reference content or delete if not needed.
|
||||
|
||||
Reference docs are ideal for:
|
||||
- API documentation
|
||||
- Detailed workflow guides
|
||||
- Database schemas
|
||||
- Content too lengthy for main SKILL.md
|
||||
"""
|
||||
|
||||
EXAMPLE_ASSET = """\
|
||||
This is a placeholder for asset files.
|
||||
Replace with actual assets (templates, images, fonts, etc.) or delete if not needed.
|
||||
|
||||
Asset files are NOT loaded into context — they are used within the output.
|
||||
"""
|
||||
|
||||
|
||||
def title_case_skill_name(skill_name):
|
||||
"""Convert hyphenated skill name to Title Case."""
|
||||
return ' '.join(word.capitalize() for word in skill_name.split('-'))
|
||||
|
||||
|
||||
def validate_skill_name(name):
|
||||
"""Validate skill name format (kebab-case)."""
|
||||
if not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', name) and not re.match(r'^[a-z0-9]$', name):
|
||||
return False, "Name must be kebab-case (lowercase letters, digits, hyphens)"
|
||||
if '--' in name:
|
||||
return False, "Name cannot contain consecutive hyphens"
|
||||
if len(name) > 64:
|
||||
return False, f"Name too long ({len(name)} chars, max 64)"
|
||||
return True, ""
|
||||
|
||||
|
||||
def init_skill(skill_name, path, fmt='desirecore'):
|
||||
"""Initialize a new skill directory with template SKILL.md."""
|
||||
skill_dir = Path(path).resolve() / skill_name
|
||||
|
||||
if skill_dir.exists():
|
||||
print(f"❌ Error: Skill directory already exists: {skill_dir}")
|
||||
return None
|
||||
|
||||
# Create skill directory
|
||||
try:
|
||||
skill_dir.mkdir(parents=True, exist_ok=False)
|
||||
print(f"✅ Created skill directory: {skill_dir}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating directory: {e}")
|
||||
return None
|
||||
|
||||
# Create SKILL.md from template
|
||||
skill_title = title_case_skill_name(skill_name)
|
||||
template = DESIRECORE_TEMPLATE if fmt == 'desirecore' else BASIC_TEMPLATE
|
||||
skill_content = template.format(
|
||||
skill_name=skill_name,
|
||||
skill_title=skill_title,
|
||||
today=date.today().isoformat(),
|
||||
)
|
||||
|
||||
skill_md_path = skill_dir / 'SKILL.md'
|
||||
try:
|
||||
skill_md_path.write_text(skill_content)
|
||||
print(f"✅ Created SKILL.md ({fmt} format)")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating SKILL.md: {e}")
|
||||
return None
|
||||
|
||||
# Create resource directories with example files
|
||||
try:
|
||||
scripts_dir = skill_dir / 'scripts'
|
||||
scripts_dir.mkdir(exist_ok=True)
|
||||
example_script = scripts_dir / 'example.py'
|
||||
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
|
||||
example_script.chmod(0o755)
|
||||
print("✅ Created scripts/example.py")
|
||||
|
||||
references_dir = skill_dir / 'references'
|
||||
references_dir.mkdir(exist_ok=True)
|
||||
example_ref = references_dir / 'api_reference.md'
|
||||
example_ref.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
|
||||
print("✅ Created references/api_reference.md")
|
||||
|
||||
assets_dir = skill_dir / 'assets'
|
||||
assets_dir.mkdir(exist_ok=True)
|
||||
example_asset = assets_dir / 'example_asset.txt'
|
||||
example_asset.write_text(EXAMPLE_ASSET)
|
||||
print("✅ Created assets/example_asset.txt")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating resource directories: {e}")
|
||||
return None
|
||||
|
||||
print(f"\n✅ Skill '{skill_name}' initialized at {skill_dir}")
|
||||
print("\nNext steps:")
|
||||
print("1. Edit SKILL.md — complete TODO items and update description")
|
||||
print("2. Customize or delete example files in scripts/, references/, assets/")
|
||||
print("3. Run quick_validate.py to check the skill structure")
|
||||
|
||||
return skill_dir
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Initialize a new skill from template',
|
||||
epilog='Examples:\n'
|
||||
' init_skill.py my-new-skill --path ~/.desirecore/skills\n'
|
||||
' init_skill.py my-api-helper --path ~/.desirecore/skills --format basic',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument('skill_name', help='Skill name (kebab-case, max 64 chars)')
|
||||
parser.add_argument('--path', required=True, help='Parent directory for the skill')
|
||||
parser.add_argument(
|
||||
'--format', choices=['desirecore', 'basic'], default='desirecore',
|
||||
help='Template format: desirecore (full, default) or basic (Claude Code compatible)',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate name
|
||||
valid, msg = validate_skill_name(args.skill_name)
|
||||
if not valid:
|
||||
print(f"❌ Invalid skill name: {msg}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"🚀 Initializing skill: {args.skill_name}")
|
||||
print(f" Location: {args.path}")
|
||||
print(f" Format: {args.format}")
|
||||
print()
|
||||
|
||||
result = init_skill(args.skill_name, args.path, args.format)
|
||||
sys.exit(0 if result else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
213
skills/skill-creator/scripts/package_skill.py
Executable file
213
skills/skill-creator/scripts/package_skill.py
Executable file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Packager & Installer
|
||||
|
||||
Supports two modes:
|
||||
- Package: Create a .skill file (ZIP) for Claude Code distribution
|
||||
- Install: Install directly to DesireCore via HTTP API
|
||||
|
||||
Usage:
|
||||
# Package as .skill file (Claude Code compatible)
|
||||
package_skill.py <path/to/skill-folder> [output-directory]
|
||||
|
||||
# Install to DesireCore via API
|
||||
package_skill.py <path/to/skill-folder> --install [--scope global|agent] [--agent-id <id>]
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import zipfile
|
||||
import argparse
|
||||
import ssl
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
|
||||
# Import validate_skill from sibling script
|
||||
_script_dir = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_script_dir))
|
||||
from quick_validate import validate_skill
|
||||
|
||||
|
||||
# ==================== Package Mode ====================
|
||||
|
||||
def package_skill(skill_path, output_dir=None):
|
||||
"""Package a skill folder into a .skill file (ZIP format)."""
|
||||
skill_path = Path(skill_path).resolve()
|
||||
|
||||
if not skill_path.exists():
|
||||
print(f"❌ Error: Skill folder not found: {skill_path}")
|
||||
return None
|
||||
|
||||
if not skill_path.is_dir():
|
||||
print(f"❌ Error: Path is not a directory: {skill_path}")
|
||||
return None
|
||||
|
||||
skill_md = skill_path / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
print(f"❌ Error: SKILL.md not found in {skill_path}")
|
||||
return None
|
||||
|
||||
# Validate before packaging
|
||||
print("🔍 Validating skill...")
|
||||
valid, errors, warnings = validate_skill(skill_path)
|
||||
if not valid:
|
||||
print(f"❌ Validation failed:")
|
||||
for e in errors:
|
||||
print(f" ✗ {e}")
|
||||
return None
|
||||
if warnings:
|
||||
for w in warnings:
|
||||
print(f" ⚠ {w}")
|
||||
print(f"✅ Validation passed\n")
|
||||
|
||||
# Determine output location
|
||||
skill_name = skill_path.name
|
||||
if output_dir:
|
||||
output_path = Path(output_dir).resolve()
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
output_path = Path.cwd()
|
||||
|
||||
skill_filename = output_path / f"{skill_name}.skill"
|
||||
|
||||
# Create .skill file (zip format)
|
||||
try:
|
||||
with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for file_path in skill_path.rglob('*'):
|
||||
if file_path.is_file():
|
||||
arcname = file_path.relative_to(skill_path.parent)
|
||||
zipf.write(file_path, arcname)
|
||||
print(f" Added: {arcname}")
|
||||
|
||||
print(f"\n✅ Packaged to: {skill_filename}")
|
||||
return skill_filename
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating .skill file: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ==================== Install Mode ====================
|
||||
|
||||
def read_agent_service_port():
|
||||
"""Read Agent Service port from port file."""
|
||||
port_file = Path.home() / '.desirecore' / 'agent-service.port'
|
||||
if not port_file.exists():
|
||||
return None
|
||||
return port_file.read_text().strip()
|
||||
|
||||
|
||||
def install_skill(skill_path, scope='global', agent_id=None):
|
||||
"""Install a skill to DesireCore via HTTP API."""
|
||||
skill_path = Path(skill_path).resolve()
|
||||
skill_md = skill_path / 'SKILL.md'
|
||||
|
||||
if not skill_md.exists():
|
||||
print(f"❌ Error: SKILL.md not found in {skill_path}")
|
||||
return None
|
||||
|
||||
# Validate first
|
||||
print("🔍 Validating skill...")
|
||||
valid, errors, warnings = validate_skill(skill_path)
|
||||
if not valid:
|
||||
print(f"❌ Validation failed:")
|
||||
for e in errors:
|
||||
print(f" ✗ {e}")
|
||||
return None
|
||||
if warnings:
|
||||
for w in warnings:
|
||||
print(f" ⚠ {w}")
|
||||
print(f"✅ Validation passed\n")
|
||||
|
||||
# Check Agent Service
|
||||
port = read_agent_service_port()
|
||||
if not port:
|
||||
print("❌ Error: Agent Service not running (port file not found)")
|
||||
print("\nFallback — install via file system:")
|
||||
if scope == 'agent' and agent_id:
|
||||
print(f" cp -r {skill_path} ~/.desirecore/agents/{agent_id}/skills/")
|
||||
else:
|
||||
print(f" cp -r {skill_path} ~/.desirecore/skills/")
|
||||
return None
|
||||
|
||||
content = skill_md.read_text()
|
||||
skill_id = skill_path.name
|
||||
|
||||
# Build API request
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if scope == 'agent':
|
||||
if not agent_id:
|
||||
print("❌ Error: --agent-id is required for agent scope")
|
||||
return None
|
||||
url = f"https://127.0.0.1:{port}/api/agents/{agent_id}/skills"
|
||||
payload = {"id": skill_id, "fullContent": content}
|
||||
else:
|
||||
url = f"https://127.0.0.1:{port}/api/skills"
|
||||
payload = {"skillId": skill_id, "content": content}
|
||||
|
||||
data = json.dumps(payload).encode('utf-8')
|
||||
req = urllib.request.Request(
|
||||
url, data=data, method='POST',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ctx) as resp:
|
||||
result = json.loads(resp.read())
|
||||
print(f"✅ Installed '{skill_id}' ({scope} scope)")
|
||||
return result
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode('utf-8', errors='replace')
|
||||
print(f"❌ API error ({e.code}): {body}")
|
||||
return None
|
||||
except urllib.error.URLError as e:
|
||||
print(f"❌ Connection error: {e.reason}")
|
||||
print("Is Agent Service running?")
|
||||
return None
|
||||
|
||||
|
||||
# ==================== Main ====================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Package or install a skill',
|
||||
epilog='Examples:\n'
|
||||
' package_skill.py my-skill/ # Package as .skill ZIP\n'
|
||||
' package_skill.py my-skill/ ./dist # Package to specific dir\n'
|
||||
' package_skill.py my-skill/ --install # Install via API (global)\n'
|
||||
' package_skill.py my-skill/ --install --scope agent --agent-id abc123',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument('skill_path', help='Path to skill folder')
|
||||
parser.add_argument('output_dir', nargs='?', default=None,
|
||||
help='Output directory for .skill file (package mode only)')
|
||||
parser.add_argument('--install', action='store_true',
|
||||
help='Install via DesireCore API instead of packaging')
|
||||
parser.add_argument('--scope', choices=['global', 'agent'], default='global',
|
||||
help='Installation scope (default: global)')
|
||||
parser.add_argument('--agent-id',
|
||||
help='Agent ID (required when --scope agent)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.install:
|
||||
print(f"📦 Installing skill: {args.skill_path} ({args.scope} scope)")
|
||||
print()
|
||||
result = install_skill(args.skill_path, args.scope, args.agent_id)
|
||||
else:
|
||||
print(f"📦 Packaging skill: {args.skill_path}")
|
||||
if args.output_dir:
|
||||
print(f" Output: {args.output_dir}")
|
||||
print()
|
||||
result = package_skill(args.skill_path, args.output_dir)
|
||||
|
||||
sys.exit(0 if result else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
164
skills/skill-creator/scripts/quick_validate.py
Executable file
164
skills/skill-creator/scripts/quick_validate.py
Executable file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick validation script for skills.
|
||||
|
||||
Validates against DesireCore SKILL.md frontmatter schema.
|
||||
Also accepts Claude Code basic format (name + description only).
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("Error: PyYAML is required. Install with: pip install pyyaml")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# DesireCore 已知的顶层字段集合
|
||||
# 来源:lib/schemas/agent/skill-frontmatter.ts 的 properties 定义
|
||||
# Schema 设置了 additionalProperties: true,所以未知字段只警告不报错
|
||||
KNOWN_PROPERTIES = {
|
||||
# 核心字段
|
||||
'name', 'description', 'version', 'type', 'requires',
|
||||
'risk_level', 'status', 'tags', 'metadata',
|
||||
# 功能控制
|
||||
'disable-model-invocation', 'disable_model_invocation',
|
||||
'allowed-tools', 'user-invocable', 'argument-hint',
|
||||
'model', 'context', 'agent',
|
||||
# 高级字段
|
||||
'error_message', 'skill_package', 'input_schema', 'output_schema',
|
||||
'market', 'x_desirecore', 'json_output',
|
||||
# Claude Code 兼容字段
|
||||
'license', 'compatibility',
|
||||
}
|
||||
|
||||
VALID_TYPES = {'procedural', 'conversational', 'meta'}
|
||||
VALID_RISK_LEVELS = {'low', 'medium', 'high'}
|
||||
VALID_STATUSES = {'enabled', 'disabled'}
|
||||
VALID_CONTEXTS = {'default', 'fork'}
|
||||
SEMVER_RE = re.compile(r'^\d+\.\d+\.\d+$')
|
||||
KEBAB_RE = re.compile(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$')
|
||||
|
||||
|
||||
def validate_skill(skill_path):
|
||||
"""
|
||||
Validate a skill directory.
|
||||
|
||||
Returns:
|
||||
(valid: bool, errors: list[str], warnings: list[str])
|
||||
"""
|
||||
skill_path = Path(skill_path)
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Check SKILL.md exists
|
||||
skill_md = skill_path / 'SKILL.md'
|
||||
if not skill_md.exists():
|
||||
return False, ["SKILL.md not found"], []
|
||||
|
||||
content = skill_md.read_text()
|
||||
if not content.startswith('---'):
|
||||
return False, ["No YAML frontmatter found (must start with ---)"], []
|
||||
|
||||
# Extract frontmatter
|
||||
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
||||
if not match:
|
||||
return False, ["Invalid frontmatter format (missing closing ---)"], []
|
||||
|
||||
try:
|
||||
frontmatter = yaml.safe_load(match.group(1))
|
||||
if not isinstance(frontmatter, dict):
|
||||
return False, ["Frontmatter must be a YAML dictionary"], []
|
||||
except yaml.YAMLError as e:
|
||||
return False, [f"Invalid YAML: {e}"], []
|
||||
|
||||
# === 必填字段 ===
|
||||
if 'description' not in frontmatter:
|
||||
errors.append("Missing required field: 'description'")
|
||||
|
||||
# === description 质量检查 ===
|
||||
description = frontmatter.get('description', '')
|
||||
if isinstance(description, str):
|
||||
desc_stripped = description.strip()
|
||||
if desc_stripped and len(desc_stripped) < 10:
|
||||
warnings.append("Description is very short — include 'Use when' trigger hints")
|
||||
if len(desc_stripped) > 1024:
|
||||
errors.append(f"Description too long ({len(desc_stripped)} chars, max 1024)")
|
||||
if '<' in desc_stripped or '>' in desc_stripped:
|
||||
warnings.append("Description contains angle brackets (< or >) — may cause parsing issues")
|
||||
|
||||
# === name 格式检查 ===
|
||||
name = frontmatter.get('name', '')
|
||||
if isinstance(name, str) and name.strip():
|
||||
n = name.strip()
|
||||
if len(n) > 64:
|
||||
errors.append(f"Name too long ({len(n)} chars, max 64)")
|
||||
# kebab-case 检查仅当 name 是英文时
|
||||
if re.match(r'^[a-z0-9-]+$', n):
|
||||
if not KEBAB_RE.match(n):
|
||||
warnings.append(f"Name '{n}' starts/ends with hyphen or has consecutive hyphens")
|
||||
|
||||
# === version 格式检查 ===
|
||||
version = frontmatter.get('version')
|
||||
if version is not None and not SEMVER_RE.match(str(version)):
|
||||
warnings.append(f"Version '{version}' is not valid semver (expected x.y.z)")
|
||||
|
||||
# === 枚举字段检查 ===
|
||||
skill_type = frontmatter.get('type')
|
||||
if skill_type is not None and skill_type not in VALID_TYPES:
|
||||
errors.append(f"Invalid type: '{skill_type}'. Must be one of: {', '.join(sorted(VALID_TYPES))}")
|
||||
|
||||
risk = frontmatter.get('risk_level')
|
||||
if risk is not None and risk not in VALID_RISK_LEVELS:
|
||||
errors.append(f"Invalid risk_level: '{risk}'. Must be one of: {', '.join(sorted(VALID_RISK_LEVELS))}")
|
||||
|
||||
status = frontmatter.get('status')
|
||||
if status is not None and status not in VALID_STATUSES:
|
||||
errors.append(f"Invalid status: '{status}'. Must be one of: {', '.join(sorted(VALID_STATUSES))}")
|
||||
|
||||
context = frontmatter.get('context')
|
||||
if context is not None and context not in VALID_CONTEXTS:
|
||||
errors.append(f"Invalid context: '{context}'. Must be one of: {', '.join(sorted(VALID_CONTEXTS))}")
|
||||
|
||||
# === 未知字段警告(不阻断) ===
|
||||
unknown = set(frontmatter.keys()) - KNOWN_PROPERTIES
|
||||
if unknown:
|
||||
warnings.append(f"Unknown fields (will be preserved): {', '.join(sorted(unknown))}")
|
||||
|
||||
valid = len(errors) == 0
|
||||
return valid, errors, warnings
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: quick_validate.py <skill_directory>")
|
||||
print("\nValidates SKILL.md frontmatter against DesireCore schema.")
|
||||
print("Also accepts Claude Code basic format (name + description).")
|
||||
sys.exit(1)
|
||||
|
||||
skill_path = sys.argv[1]
|
||||
valid, errors, warnings = validate_skill(skill_path)
|
||||
|
||||
if valid and not warnings:
|
||||
print(f"✅ Skill is valid!")
|
||||
elif valid and warnings:
|
||||
print(f"✅ Skill is valid (with warnings):")
|
||||
for w in warnings:
|
||||
print(f" ⚠ {w}")
|
||||
else:
|
||||
print(f"❌ Validation failed:")
|
||||
for e in errors:
|
||||
print(f" ✗ {e}")
|
||||
if warnings:
|
||||
print(f" Warnings:")
|
||||
for w in warnings:
|
||||
print(f" ⚠ {w}")
|
||||
|
||||
sys.exit(0 if valid else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user