QwenClaw v2.0 - Complete Rebuild with ALL 81+ Skills

This commit is contained in:
AI Agent
2026-02-26 20:08:00 +04:00
Unverified
parent 7e297c53b9
commit 69cf7e8a05
475 changed files with 82593 additions and 110 deletions

View File

@@ -0,0 +1,368 @@
#!/usr/bin/env python3
"""Validate all Claude Code plugins conform to specs."""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
import yaml
def parse_frontmatter(content: str) -> tuple[dict | None, str]:
"""Parse YAML frontmatter from markdown content."""
if not content.startswith("---"):
return None, content
parts = content.split("---", 2)
if len(parts) < 3:
return None, content
try:
frontmatter = yaml.safe_load(parts[1])
return frontmatter, parts[2].strip()
except yaml.YAMLError:
return None, content
def validate_plugin_json(plugin_dir: Path) -> list[str]:
"""Validate .claude-plugin/plugin.json exists and is valid."""
errors = []
plugin_json = plugin_dir / ".claude-plugin" / "plugin.json"
if not plugin_json.exists():
errors.append(f"{plugin_dir.name}: Missing .claude-plugin/plugin.json")
return errors
try:
with open(plugin_json) as f:
config = json.load(f)
if "name" not in config:
errors.append(f"{plugin_dir.name}: plugin.json missing 'name' field")
elif config["name"] != plugin_dir.name:
errors.append(f"{plugin_dir.name}: plugin.json name '{config['name']}' doesn't match directory name")
except json.JSONDecodeError as e:
errors.append(f"{plugin_dir.name}: Invalid plugin.json - {e}")
return errors
def validate_skills(plugin_dir: Path) -> list[str]:
"""Validate skills conform to Claude Code specs."""
errors = []
skills_dir = plugin_dir / "skills"
if not skills_dir.exists():
return errors
for skill_path in skills_dir.iterdir():
if not skill_path.is_dir():
continue
prefix = f"{plugin_dir.name}/skills/{skill_path.name}"
# Check directory name is kebab-case
if not re.match(r"^[a-z0-9-]+$", skill_path.name):
errors.append(f"{prefix}: Directory must be kebab-case")
skill_md = skill_path / "SKILL.md"
if not skill_md.exists():
errors.append(f"{prefix}: Missing SKILL.md")
continue
content = skill_md.read_text()
frontmatter, body = parse_frontmatter(content)
if not frontmatter:
errors.append(f"{prefix}/SKILL.md: Missing YAML frontmatter")
continue
# Validate name field
if "name" not in frontmatter:
errors.append(f"{prefix}/SKILL.md: Missing 'name' field")
else:
name = frontmatter["name"]
if not isinstance(name, str):
errors.append(f"{prefix}/SKILL.md: 'name' must be string")
elif len(name) > 64:
errors.append(f"{prefix}/SKILL.md: 'name' exceeds 64 chars ({len(name)})")
elif not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", name):
errors.append(f"{prefix}/SKILL.md: 'name' must be kebab-case: '{name}'")
# Validate description field
if "description" not in frontmatter:
errors.append(f"{prefix}/SKILL.md: Missing 'description' field")
else:
desc = frontmatter["description"]
if not isinstance(desc, str):
errors.append(f"{prefix}/SKILL.md: 'description' must be string")
elif len(desc) > 600:
errors.append(f"{prefix}/SKILL.md: 'description' exceeds 600 chars ({len(desc)})")
# Check body exists
if not body or len(body.strip()) < 20:
errors.append(f"{prefix}/SKILL.md: Body content too short")
return errors
def validate_agents(plugin_dir: Path) -> list[str]:
"""Validate agents conform to Claude Code specs."""
errors = []
agents_dir = plugin_dir / "agents"
if not agents_dir.exists():
return errors
valid_models = {"inherit", "sonnet", "opus", "haiku"}
valid_colors = {"blue", "cyan", "green", "yellow", "magenta", "red"}
for agent_file in agents_dir.iterdir():
if not agent_file.is_file() or agent_file.suffix != ".md":
continue
prefix = f"{plugin_dir.name}/agents/{agent_file.name}"
name = agent_file.stem
# Check filename is kebab-case
if not re.match(r"^[a-z0-9-]+$", name):
errors.append(f"{prefix}: Filename must be kebab-case")
content = agent_file.read_text()
frontmatter, body = parse_frontmatter(content)
if not frontmatter:
errors.append(f"{prefix}: Missing YAML frontmatter")
continue
# Validate name field
if "name" not in frontmatter:
errors.append(f"{prefix}: Missing 'name' field")
else:
agent_name = frontmatter["name"]
if not isinstance(agent_name, str):
errors.append(f"{prefix}: 'name' must be string")
elif len(agent_name) < 3 or len(agent_name) > 50:
errors.append(f"{prefix}: 'name' must be 3-50 chars ({len(agent_name)})")
elif not re.match(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$", agent_name):
errors.append(
f"{prefix}: 'name' must be lowercase with hyphens, start/end alphanumeric: '{agent_name}'"
)
# Validate description field
if "description" not in frontmatter:
errors.append(f"{prefix}: Missing 'description' field")
else:
desc = frontmatter["description"]
if not isinstance(desc, str):
errors.append(f"{prefix}: 'description' must be string")
elif len(desc) < 10 or len(desc) > 5000:
errors.append(f"{prefix}: 'description' must be 10-5000 chars ({len(desc)})")
# Validate model field
if "model" not in frontmatter:
errors.append(f"{prefix}: Missing 'model' field")
elif frontmatter["model"] not in valid_models:
errors.append(f"{prefix}: 'model' must be one of {valid_models}: '{frontmatter['model']}'")
# Validate color field
if "color" not in frontmatter:
errors.append(f"{prefix}: Missing 'color' field")
elif frontmatter["color"] not in valid_colors:
errors.append(f"{prefix}: 'color' must be one of {valid_colors}: '{frontmatter['color']}'")
# Validate tools field if present
if "tools" in frontmatter:
tools = frontmatter["tools"]
if not isinstance(tools, list):
errors.append(f"{prefix}: 'tools' must be array")
# Check body exists
if not body or len(body.strip()) < 20:
errors.append(f"{prefix}: System prompt too short (<20 chars)")
elif len(body.strip()) > 10000:
errors.append(f"{prefix}: System prompt too long (>10000 chars)")
return errors
def validate_commands(plugin_dir: Path) -> list[str]:
"""Validate commands conform to Claude Code specs."""
errors = []
commands_dir = plugin_dir / "commands"
if not commands_dir.exists():
return errors
valid_models = {"sonnet", "opus", "haiku"}
for cmd_file in commands_dir.rglob("*.md"):
prefix = f"{plugin_dir.name}/commands/{cmd_file.relative_to(commands_dir)}"
name = cmd_file.stem
# Check filename is kebab-case
if not re.match(r"^[a-z0-9-]+$", name):
errors.append(f"{prefix}: Filename must be kebab-case")
content = cmd_file.read_text()
frontmatter, body = parse_frontmatter(content)
# Frontmatter is optional for commands
if frontmatter:
# Validate model if present
if "model" in frontmatter and frontmatter["model"] not in valid_models:
errors.append(f"{prefix}: 'model' must be one of {valid_models}: '{frontmatter['model']}'")
# Validate disable-model-invocation if present
if "disable-model-invocation" in frontmatter:
if not isinstance(frontmatter["disable-model-invocation"], bool):
errors.append(f"{prefix}: 'disable-model-invocation' must be boolean")
# Check body exists
if not body and not (frontmatter and body == ""):
# If no frontmatter, content is the body
if not content.strip():
errors.append(f"{prefix}: Command body is empty")
return errors
def validate_hooks(plugin_dir: Path) -> list[str]:
"""Validate hooks conform to Claude Code specs."""
errors = []
hooks_dir = plugin_dir / "hooks"
if not hooks_dir.exists():
return errors
hooks_json = hooks_dir / "hooks.json"
if not hooks_json.exists():
errors.append(f"{plugin_dir.name}/hooks: Missing hooks.json")
return errors
try:
with open(hooks_json) as f:
config = json.load(f)
except json.JSONDecodeError as e:
errors.append(f"{plugin_dir.name}/hooks/hooks.json: Invalid JSON - {e}")
return errors
# Check for wrapper format
if "hooks" not in config:
errors.append(f"{plugin_dir.name}/hooks/hooks.json: Must use wrapper format with 'hooks' field")
return errors
valid_events = {
"PreToolUse",
"PostToolUse",
"Stop",
"SubagentStop",
"SessionStart",
"SessionEnd",
"UserPromptSubmit",
"PreCompact",
"Notification",
}
hooks_config = config["hooks"]
for event, hook_list in hooks_config.items():
if event not in valid_events:
errors.append(f"{plugin_dir.name}/hooks/hooks.json: Invalid event '{event}'. Must be one of {valid_events}")
continue
if not isinstance(hook_list, list):
errors.append(f"{plugin_dir.name}/hooks/hooks.json: '{event}' must be array")
continue
for i, hook_entry in enumerate(hook_list):
if not isinstance(hook_entry, dict):
continue
hooks = hook_entry.get("hooks", [])
for j, hook in enumerate(hooks):
if not isinstance(hook, dict):
continue
hook_type = hook.get("type")
if hook_type == "command":
cmd = hook.get("command", "")
# Check for ${CLAUDE_PLUGIN_ROOT} usage (only for script paths, not inline commands)
is_inline_cmd = any(op in cmd for op in [" ", "|", ";", "&&", "||", "$("])
if cmd and not cmd.startswith("${CLAUDE_PLUGIN_ROOT}") and not is_inline_cmd:
if "/" in cmd and not cmd.startswith("$"):
errors.append(
f"{plugin_dir.name}/hooks/hooks.json: "
f"{event}[{i}].hooks[{j}] should use ${{CLAUDE_PLUGIN_ROOT}}"
)
# Check script exists
if cmd and "${CLAUDE_PLUGIN_ROOT}" in cmd:
script_path = cmd.replace("${CLAUDE_PLUGIN_ROOT}", str(plugin_dir))
if not Path(script_path).exists():
errors.append(f"{plugin_dir.name}/hooks/hooks.json: Script not found: {cmd}")
elif hook_type == "prompt":
if "prompt" not in hook:
errors.append(
f"{plugin_dir.name}/hooks/hooks.json: {event}[{i}].hooks[{j}] missing 'prompt' field"
)
# Validate script naming in hooks/scripts/
scripts_dir = hooks_dir / "scripts"
if scripts_dir.exists():
for script in scripts_dir.iterdir():
if script.is_file() and script.suffix in {".py", ".sh"}:
name = script.stem
if not re.match(r"^[a-z0-9_]+$", name):
errors.append(f"{plugin_dir.name}/hooks/scripts/{script.name}: Script name must use snake_case")
return errors
def validate_mcp(plugin_dir: Path) -> list[str]:
"""Validate MCP configuration if present."""
errors = []
mcp_json = plugin_dir / ".mcp.json"
if not mcp_json.exists():
return errors
try:
with open(mcp_json) as f:
json.load(f)
except json.JSONDecodeError as e:
errors.append(f"{plugin_dir.name}/.mcp.json: Invalid JSON - {e}")
return errors
def main():
"""Validate all plugins and return exit code."""
plugins_dir = Path("plugins")
if not plugins_dir.exists():
print("No plugins directory found")
return 0
all_errors = []
for plugin_dir in sorted(plugins_dir.iterdir()):
if not plugin_dir.is_dir():
continue
if plugin_dir.name.startswith("."):
continue
all_errors.extend(validate_plugin_json(plugin_dir))
all_errors.extend(validate_skills(plugin_dir))
all_errors.extend(validate_agents(plugin_dir))
all_errors.extend(validate_commands(plugin_dir))
all_errors.extend(validate_hooks(plugin_dir))
all_errors.extend(validate_mcp(plugin_dir))
if all_errors:
print("Plugin Validation Failed:")
for error in all_errors:
print(f" - {error}")
return 1
print("All plugins validated successfully")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,28 @@
name: Validate Plugins
on:
push:
branches: [main]
paths:
- "plugins/**"
pull_request:
branches: [main]
paths:
- "plugins/**"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install pyyaml
- name: Validate plugins
run: python .github/scripts/validate_plugins.py