Files
antigravity-skills-reference/skills/skill-installer/scripts/package_skill.py
ProgramadorBrasil 61ec71c5c7 feat: add 52 specialized AI agent skills (#217)
New skills covering 10 categories:

**Security & Audit**: 007 (STRIDE/PASTA/OWASP), cred-omega (secrets management)
**AI Personas**: Karpathy, Hinton, Sutskever, LeCun (4 sub-skills), Altman, Musk, Gates, Jobs, Buffett
**Multi-agent Orchestration**: agent-orchestrator, task-intelligence, multi-advisor
**Code Analysis**: matematico-tao (Terence Tao-inspired mathematical code analysis)
**Social & Messaging**: Instagram Graph API, Telegram Bot, WhatsApp Cloud API, social-orchestrator
**Image Generation**: AI Studio (Gemini), Stability AI, ComfyUI Gateway, image-studio router
**Brazilian Domain**: 6 auction specialist modules, 2 legal advisors, auctioneers data scraper
**Product & Growth**: design, invention, monetization, analytics, growth engine
**DevOps & LLM Ops**: Docker/CI-CD/AWS, RAG/embeddings/fine-tuning
**Skill Governance**: installer, sentinel auditor, context management

Each skill includes:
- Standardized YAML frontmatter (name, description, risk, source, tags, tools)
- Structured sections (Overview, When to Use, How it Works, Best Practices)
- Python scripts and reference documentation where applicable
- Cross-platform compatibility (Claude Code, Antigravity, Cursor, Gemini CLI, Codex CLI)

Co-authored-by: ProgramadorBrasil <214873561+ProgramadorBrasil@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:04:07 +01:00

418 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Skill Packager - Create ZIP files for Claude.ai web/desktop upload.
Claude Code (CLI) and Claude.ai (web/desktop) have SEPARATE skill systems.
Skills in .claude/skills/ work in the terminal but do NOT appear in the
Claude.ai web Habilidades (Skills) settings page.
To install a skill in Claude.ai web/desktop, you need to upload a ZIP file
through Settings > Capabilities > Skills > Upload skill.
This script packages skills into the correct ZIP format.
Usage:
python package_skill.py --source "C:\\path\\to\\skill"
python package_skill.py --source "C:\\path\\to\\skill" --output "C:\\Users\\renat\\Desktop"
python package_skill.py --all
python package_skill.py --all --output "C:\\Users\\renat\\Desktop"
"""
import os
import sys
import json
import re
import zipfile
from pathlib import Path
# ── Configuration ──────────────────────────────────────────────────────────
SKILLS_ROOT = Path(r"C:\Users\renat\skills")
DEFAULT_OUTPUT = Path(os.path.expanduser("~")) / "Desktop"
# Directories to exclude from ZIP
EXCLUDE_DIRS = {
".git", "__pycache__", "node_modules", ".venv", "venv",
".tox", ".mypy_cache", ".pytest_cache", "data",
}
# File patterns to exclude
EXCLUDE_FILES = {
".env", ".gitignore", ".DS_Store", "Thumbs.db",
"credentials.json", "token.json",
}
EXCLUDE_EXTENSIONS = {
".pyc", ".pyo", ".db", ".sqlite", ".sqlite3",
".log", ".tmp", ".bak",
}
# ── YAML Frontmatter Parser ───────────────────────────────────────────────
def parse_yaml_frontmatter(path: Path) -> dict:
"""Extract YAML frontmatter from a SKILL.md file."""
try:
text = path.read_text(encoding="utf-8")
except Exception:
return {}
match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
if not match:
return {}
try:
import yaml
return yaml.safe_load(match.group(1)) or {}
except Exception:
result = {}
block = match.group(1)
for key in ("name", "description", "version"):
m = re.search(rf'^{key}:\s*["\']?(.+?)["\']?\s*$', block, re.MULTILINE)
if m:
result[key] = m.group(1).strip()
else:
m2 = re.search(
rf'^{key}:\s*>-?\s*\n((?:\s+.+\n?)+)', block, re.MULTILINE
)
if m2:
lines = m2.group(1).strip().split("\n")
result[key] = " ".join(line.strip() for line in lines)
return result
# ── Validation ─────────────────────────────────────────────────────────────
def validate_for_web(skill_dir: Path) -> dict:
"""Validate skill meets Claude.ai web upload requirements."""
errors = []
warnings = []
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
errors.append("SKILL.md not found")
return {"valid": False, "errors": errors, "warnings": warnings}
meta = parse_yaml_frontmatter(skill_md)
# Name: required, lowercase, letters/numbers/hyphens only, max 64 chars
name = meta.get("name", "")
if not name:
errors.append("Missing 'name' field in frontmatter")
else:
if name != name.lower():
warnings.append(f"Name '{name}' should be lowercase: '{name.lower()}'")
name_lower = name.lower()
if len(name_lower) == 1:
if not re.match(r'^[a-z0-9]$', name_lower):
warnings.append(f"Name '{name}' should only contain lowercase letters or numbers")
elif not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', name_lower):
warnings.append(f"Name '{name}' should only contain lowercase letters, numbers, and hyphens")
if len(name) > 64:
errors.append(f"Name exceeds 64 characters: {len(name)}")
if "anthropic" in name_lower or "claude" in name_lower:
errors.append(f"Name cannot contain reserved words 'anthropic' or 'claude'")
# Description: required, max 1024 chars
desc = meta.get("description", "")
if not desc:
errors.append("Missing 'description' field in frontmatter")
elif len(desc) > 1024:
warnings.append(f"Description exceeds 1024 chars ({len(desc)}), may be truncated")
return {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
"name": name,
"description": desc[:120] if desc else "",
}
def should_include(file_path: Path, skill_dir: Path) -> bool:
"""Check if a file should be included in the ZIP."""
rel = file_path.relative_to(skill_dir)
# Check directory exclusions
for part in rel.parts[:-1]:
if part in EXCLUDE_DIRS:
return False
# Check file exclusions
if file_path.name in EXCLUDE_FILES:
return False
# Check extension exclusions
if file_path.suffix.lower() in EXCLUDE_EXTENSIONS:
return False
return True
# ── Packaging ──────────────────────────────────────────────────────────────
def package_skill(skill_dir: Path, output_dir: Path = None) -> dict:
"""
Package a skill directory into a ZIP file for Claude.ai upload.
The ZIP format required by Claude.ai:
skill-name.zip
└── skill-name/
├── SKILL.md
├── scripts/
├── references/
└── ...
"""
skill_dir = Path(skill_dir).resolve()
if not skill_dir.exists():
return {"success": False, "error": f"Directory not found: {skill_dir}"}
# Validate
validation = validate_for_web(skill_dir)
if not validation["valid"]:
return {
"success": False,
"error": f"Validation failed: {'; '.join(validation['errors'])}",
"validation": validation,
}
skill_name = validation["name"] or skill_dir.name
skill_name_lower = skill_name.lower()
# Determine output path
if output_dir is None:
output_dir = DEFAULT_OUTPUT
output_dir = Path(output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
zip_path = output_dir / f"{skill_name_lower}.zip"
# Collect files
files_to_include = []
for root, dirs, files in os.walk(skill_dir):
# Filter directories in-place to skip excluded ones
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
for f in files:
fp = Path(root) / f
if should_include(fp, skill_dir):
files_to_include.append(fp)
if not files_to_include:
return {"success": False, "error": "No files to package"}
# Create ZIP with skill folder as root
# CRITICAL: ZIP paths MUST use forward slashes, not Windows backslashes
try:
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for fp in sorted(files_to_include):
rel_path = fp.relative_to(skill_dir)
# Convert Windows backslash to forward slash for ZIP compatibility
rel_posix = rel_path.as_posix()
archive_path = f"{skill_name_lower}/{rel_posix}"
zf.write(fp, archive_path)
# Verify ZIP is not empty and valid
with zipfile.ZipFile(zip_path, "r") as zf_check:
entries = zf_check.namelist()
if not entries:
zip_path.unlink(missing_ok=True)
return {"success": False, "error": "ZIP was created empty (no files written)"}
# Verify no backslash paths leaked through
bad_paths = [e for e in entries if "\\" in e]
if bad_paths:
zip_path.unlink(missing_ok=True)
return {
"success": False,
"error": f"ZIP contains backslash paths (invalid): {bad_paths[:3]}",
}
# Get ZIP size
zip_size = zip_path.stat().st_size
zip_size_kb = zip_size / 1024
except Exception as e:
return {"success": False, "error": f"ZIP creation failed: {e}"}
return {
"success": True,
"zip_path": str(zip_path),
"skill_name": skill_name_lower,
"files_count": len(files_to_include),
"zip_size_kb": round(zip_size_kb, 1),
"validation": validation,
"upload_instructions": (
"Para instalar no Claude.ai web/desktop:\n"
"1. Abra claude.ai > Settings > Capabilities > Skills\n"
"2. Clique em 'Upload skill'\n"
f"3. Selecione o arquivo: {zip_path}\n"
"4. A skill aparecera na lista de Habilidades"
),
}
def package_all(output_dir: Path = None) -> dict:
"""Package all installed skills."""
results = []
# Find all skill directories with SKILL.md
for item in sorted(SKILLS_ROOT.iterdir()):
if not item.is_dir():
continue
if item.name.startswith("."):
continue
if item.name == "agent-orchestrator":
continue # Meta-skill, not for web upload
skill_md = item / "SKILL.md"
if skill_md.exists():
result = package_skill(item, output_dir)
results.append(result)
# Also check nested skills
for parent in SKILLS_ROOT.iterdir():
if not parent.is_dir() or parent.name.startswith("."):
continue
for child in parent.iterdir():
if child.is_dir() and (child / "SKILL.md").exists():
if child.name != "agent-orchestrator":
result = package_skill(child, output_dir)
results.append(result)
success = sum(1 for r in results if r["success"])
failed = sum(1 for r in results if not r["success"])
return {
"total": len(results),
"success": success,
"failed": failed,
"results": results,
}
# ── Verify Existing ZIPs ──────────────────────────────────────────────────
def verify_zips(output_dir: Path = None) -> dict:
"""Verify all existing skill ZIPs for integrity.
Checks:
- ZIP is valid and not corrupted
- Contains SKILL.md
- No backslash paths
- Not empty
- Matches a known skill name
"""
if output_dir is None:
output_dir = DEFAULT_OUTPUT
results = []
zip_files = sorted(output_dir.glob("*.zip"))
for zip_path in zip_files:
entry = {
"file": str(zip_path),
"name": zip_path.stem,
"size_kb": round(zip_path.stat().st_size / 1024, 1),
"valid": False,
"issues": [],
}
try:
with zipfile.ZipFile(zip_path, "r") as zf:
entries = zf.namelist()
if not entries:
entry["issues"].append("ZIP is empty")
results.append(entry)
continue
# Check for backslash paths
bad_paths = [e for e in entries if "\\" in e]
if bad_paths:
entry["issues"].append(f"Contains backslash paths: {bad_paths[:3]}")
# Check for SKILL.md
has_skill_md = any(e.endswith("SKILL.md") for e in entries)
if not has_skill_md:
entry["issues"].append("Missing SKILL.md")
# Test integrity
bad_file = zf.testzip()
if bad_file:
entry["issues"].append(f"Corrupted file in ZIP: {bad_file}")
entry["file_count"] = len(entries)
entry["valid"] = len(entry["issues"]) == 0
except zipfile.BadZipFile:
entry["issues"].append("Not a valid ZIP file")
except Exception as e:
entry["issues"].append(f"Error reading ZIP: {e}")
results.append(entry)
valid = sum(1 for r in results if r["valid"])
invalid = sum(1 for r in results if not r["valid"])
return {
"total": len(results),
"valid": valid,
"invalid": invalid,
"output_dir": str(output_dir),
"results": results,
}
# ── CLI Entry Point ───────────────────────────────────────────────────────
def main():
args = sys.argv[1:]
source = None
output_dir = None
do_all = "--all" in args
do_verify = "--verify" in args
if "--source" in args:
idx = args.index("--source")
if idx + 1 < len(args):
source = args[idx + 1]
if "--output" in args:
idx = args.index("--output")
if idx + 1 < len(args):
output_dir = Path(args[idx + 1])
if do_verify:
result = verify_zips(Path(output_dir) if output_dir else None)
print(json.dumps(result, indent=2, ensure_ascii=False))
sys.exit(0 if result["invalid"] == 0 else 1)
if not source and not do_all:
print(json.dumps({
"error": "Usage: python package_skill.py <command>",
"commands": {
"--source <path>": "Package a single skill",
"--source <path> --output <dir>": "Package to specific directory",
"--all": "Package all installed skills",
"--all --output <dir>": "Package all to specific directory",
"--verify": "Verify integrity of all existing ZIPs",
"--verify --output <dir>": "Verify ZIPs in specific directory",
},
}, indent=2))
sys.exit(1)
if source:
result = package_skill(Path(source), output_dir)
print(json.dumps(result, indent=2, ensure_ascii=False))
sys.exit(0 if result["success"] else 1)
elif do_all:
result = package_all(output_dir)
print(json.dumps(result, indent=2, ensure_ascii=False))
sys.exit(0 if result["failed"] == 0 else 1)
if __name__ == "__main__":
main()