Skill: casperkwok/skill-release (version 1.0.0)

--- name: clawhub-deploy description: Publish and manage Claude Code skills on ClawHub (clawhub.ai). Use when user wants to "publish skill", "update skill version", "release to clawhub", "clawhub publish", "clawhub update", or manage skill versions on the ClawHub registry. version: 1.0.0 metadata: openclaw: requires: env: - CLAWHUB_TOKEN bins: - clawhub - node - npm --- # ClawHub Publish - Skill 发布助手 帮助用户将 Claude Code skills 发布到 ClawHub (clawhub.ai) 并管理版本更新。 ## 核心能力 - **发布新 Skill**: 首次发布到 ClawHub - **更新版本**: 发布新版本（自动递增版本号、生成 changelog） - **检查状态**: 验证 CLI 安装、登录状态、已发布技能列表 - **规范校验**: 检查 SKILL.md 是否符合 ClawHub 要求 ## 前置检查 执行任何命令前，自动检查： 1. **clawhub CLI 安装**: `clawhub --version` 2. **登录状态**: `clawhub whoami` 3. **SKILL.md 存在**: 目标目录是否有 SKILL.md 4. **frontmatter 完整**: name / description / version 是否齐全 缺少时自动引导修复。 ## 常用命令 | 命令 | 说明 | |------|------| | `/clawhub-publish status` | 检查 CLI、登录状态、已发布技能 | | `/clawhub-publish publish <path>` | 发布新 skill | | `/clawhub-publish update <path>` | 更新已有 skill 版本 | | `/clawhub-publish check <path>` | 检查 skill 是否符合规范 | ## 工作流程 ### 首次发布 ``` 1. 检查 CLI & 登录状态 2. 检查 SKILL.md 规范 3. 询问版本号（默认 1.0.0） 4. 询问 changelog 5. 执行 clawhub publish 6. 返回发布链接 ``` ### 版本更新 ``` 1. 检查当前已发布版本 2. 建议新版本号（自动递增 patch） 3. 询问 changelog（自动生成 diff 摘要辅助） 4. 更新 SKILL.md 中的 version 5. 执行 clawhub publish 6. 返回更新链接 ``` ## ClawHub 规范要求 ### SKILL.md 必须包含 - **frontmatter**: `name`, `description`, `version` - **metadata** (推荐): `openclaw.requires.env`, `openclaw.requires.bins` - **description**: 决定触发时机，需清晰全面 ### 目录结构 ``` skill-name/ ├── SKILL.md # 必须 ├── README.md # 推荐 ├── scripts/ # 可选 ├── prompts/ # 可选 ├── references/ # 可选 ├── templates/ # 可选 └── requirements.txt # Python 依赖 ``` ## 安装依赖 ```bash npm i -g clawhub clawhub login ```

path: README.md
content: # ClawHub Publish

Publish and manage Claude Code skills on ClawHub (clawhub.ai).

## Features

- **Publish**: First-time publish with validation
- **Update**: Auto-bump version (major/minor/patch) with changelog
- **Check**: Validate skill format, CLI, and auth status
- **Dry-run**: Preview publish without actually doing it

## Quick Start

### 1. Check readiness

```
/clawhub-publish check /path/to/your-skill
```

### 2. Publish (first time)

```
/clawhub-publish publish /path/to/your-skill
```

### 3. Update version

```
/clawhub-publish update /path/to/your-skill --bump minor
```

## Commands

| Command | Description |
|---------|-------------|
| `/clawhub-publish status` | Check CLI, auth, and published skills |
| `/clawhub-publish check <path>` | Validate skill format |
| `/clawhub-publish publish <path>` | Publish new skill |
| `/clawhub-publish update <path>` | Update existing skill version |

## Auto-generated Changelog

When you don't provide a changelog, the skill will:
1. Try to read `CHANGELOG.md`
2. Fall back to recent git commits
3. Use a default message

## Version Bumping

- `--bump patch`: 1.0.0 → 1.0.1 (default)
- `--bump minor`: 1.0.0 → 1.1.0
- `--bump major`: 1.0.0 → 2.0.0

## Requirements

```bash
npm i -g clawhub
clawhub login
```


path: prompts/main.md
content: # ClawHub 发布协调员

你是 ClawHub 发布助手，帮助用户将 Claude Code skills 发布到 clawhub.ai。

## 核心职责
1. 检查发布环境（CLI、登录、skill 规范）
2. 引导用户完成发布/更新流程
3. 生成 changelog 和版本号建议

## 前置检查（必须执行）

每次用户请求发布/更新时，按顺序检查：

### 1. CLI 安装
运行 `clawhub --version`
- 未安装 → 提示：`npm i -g clawhub`

### 2. 登录状态
运行 `clawhub whoami`
- 未登录 → 提示：`clawhub login`（会跳转到浏览器 OAuth）

### 3. SKILL.md 存在
检查目标目录是否有 `SKILL.md`
- 不存在 → 错误：这不是一个有效的 skill 目录

### 4. Frontmatter 完整
读取 SKILL.md frontmatter，检查：
- `name` 是否存在
- `description` 是否存在且长度 > 20
- `version` 是否存在（格式 x.y.z）

### 5. Metadata（推荐）
检查是否有 `metadata.openclaw.requires`：
- `env`: 所需环境变量
- `bins`: 所需二进制程序

缺失时建议补充。

## 发布流程

### 首次发布
```
1. 验证前置检查全部通过
2. 确认 skill 目录路径
3. 确认版本号（默认 1.0.0）
4. 询问 changelog（可自动生成摘要）
5. 运行：clawhub publish <path> --slug <name> --version <ver> --changelog "<txt>"
6. 返回发布成功信息和链接
```

### 版本更新
```
1. 验证前置检查全部通过
2. 查询当前已发布版本（clawhub inspect <slug>）
3. 建议新版本号：
   - patch +1: 修复/小改动
   - minor +1: 新功能
   - major +1: 破坏性变更
4. 询问用户选择版本类型
5. 自动更新 SKILL.md 中的 version
6. 生成 changelog：
   - 询问主要变更点
   - 或根据 git diff 生成摘要
7. 运行：clawhub publish <path> --version <new-ver> --changelog "<txt>"
8. 返回更新成功信息
```

## 自动生成 Changelog

当用户不确定 changelog 写什么时：
1. 尝试读取目标目录的 `CHANGELOG.md` 或 git log
2. 提取最近的提交信息或变更摘要
3. 用简洁语言总结为一句话 changelog

## 交互风格
- 简洁专业，每步只问必要信息
- 出错时给出明确的修复命令
- 成功后给出可点击的 clawhub 链接

## 禁止事项
- 不要猜测用户的 skill slug，从 SKILL.md frontmatter 读取
- 不要跳过前置检查直接发布
- 不要在用户未确认版本号时自动发布


path: scripts/check.py
content: #!/usr/bin/env python3
"""Check ClawHub publish readiness: CLI, auth, and skill format."""

import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Optional


def run_cmd(cmd: list[str], timeout: int = 10) -> tuple[int, str, str]:
    """Run a shell command and return (returncode, stdout, stderr)."""
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return result.returncode, result.stdout, result.stderr
    except FileNotFoundError:
        return 1, "", f"Command not found: {cmd[0]}"
    except subprocess.TimeoutExpired:
        return 1, "", "Command timed out"


def check_cli() -> tuple[bool, str]:
    """Check if clawhub CLI is installed."""
    rc, stdout, stderr = run_cmd(["clawhub", "-V"])
    if rc == 0:
        version = stdout.strip() or "installed"
        return True, f"clawhub CLI: {version}"
    return False, "clawhub CLI not found. Install: npm i -g clawhub"


def check_auth() -> tuple[bool, str]:
    """Check if user is logged in to clawhub."""
    rc, stdout, stderr = run_cmd(["clawhub", "whoami"])
    if rc == 0:
        # whoami output: "- Checking token\n✔ casperkwok" (stdout may have spinner text)
        combined = (stdout + stderr).strip()
        # Extract last non-empty line after any spinner/progress text
        lines = [l.strip() for l in combined.split("\n") if l.strip() and not l.startswith("-")]
        user = lines[-1].lstrip("✔ ").strip() if lines else "unknown"
        return True, f"Logged in as: {user}"
    return False, "Not logged in. Run: clawhub login"


def parse_frontmatter(skill_path: Path) -> Optional[dict]:
    """Parse YAML frontmatter from SKILL.md."""
    skill_md = skill_path / "SKILL.md"
    if not skill_md.exists():
        return None

    content = skill_md.read_text(encoding="utf-8")
    match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
    if not match:
        return None

    frontmatter_text = match.group(1)
    # Simple YAML parsing: extract top-level keys (name, description, version, metadata)
    data = {}
    for line in frontmatter_text.split("\n"):
        stripped = line.strip()
        if not stripped or stripped.startswith("#"):
            continue
        # Only capture top-level keys (no leading spaces)
        if not line.startswith(" ") and not line.startswith("\t"):
            top_match = re.match(r"^(\w+):\s*(.*)$", stripped)
            if top_match:
                key, value = top_match.groups()
                value = value.strip()
                if value and value not in ("{}", ""):
                    # Remove quotes
                    if (value.startswith('"') and value.endswith('"')) or \
                       (value.startswith("'") and value.endswith("'")):
                        value = value[1:-1]
                    data[key] = value
                else:
                    data[key] = ""
    return data


def check_skill_format(skill_path: Path) -> tuple[bool, list[str]]:
    """Check if skill directory meets ClawHub requirements."""
    errors = []
    warnings = []

    # Check SKILL.md exists
    skill_md = skill_path / "SKILL.md"
    if not skill_md.exists():
        errors.append("SKILL.md not found")
        return False, errors + warnings

    # Parse frontmatter
    frontmatter = parse_frontmatter(skill_path)
    if not frontmatter:
        errors.append("SKILL.md missing YAML frontmatter")
        return False, errors + warnings

    # Check required fields
    required_fields = ["name", "description", "version"]
    for field in required_fields:
        if field not in frontmatter or not frontmatter[field]:
            errors.append(f"SKILL.md frontmatter missing: {field}")

    # Check description quality
    desc = frontmatter.get("description", "")
    if len(desc) < 20:
        warnings.append(f"description is short ({len(desc)} chars), should be > 20")

    # Check version format
    version = frontmatter.get("version", "")
    if version and not re.match(r"^\d+\.\d+\.\d+$", version):
        warnings.append(f"version '{version}' should follow semver (x.y.z)")

    # Check metadata (recommended)
    if "metadata" not in frontmatter:
        warnings.append("metadata.openclaw.requires recommended for declaring runtime deps")

    return len(errors) == 0, errors + warnings


def check_slug_available(slug: str) -> tuple[bool, str]:
    """Check if a skill slug is already published."""
    rc, stdout, stderr = run_cmd(["clawhub", "inspect", slug])
    if rc == 0:
        return False, f"Slug '{slug}' already published. Use update instead."
    return True, f"Slug '{slug}' is available"


def main() -> int:
    parser = argparse.ArgumentParser(description="Check ClawHub publish readiness")
    parser.add_argument("--path", "-p", type=str, default=".", help="Skill directory path")
    parser.add_argument("--slug", "-s", type=str, help="Check if slug is available")
    args = parser.parse_args()

    skill_path = Path(args.path)
    all_ok = True

    print("=" * 50)
    print("ClawHub Publish Readiness Check")
    print("=" * 50)

    # 1. CLI
    print("\n[1/4] Checking clawhub CLI...")
    ok, msg = check_cli()
    print(f"  {'✓' if ok else '✗'} {msg}")
    all_ok = all_ok and ok

    # 2. Auth
    print("\n[2/4] Checking authentication...")
    ok, msg = check_auth()
    print(f"  {'✓' if ok else '✗'} {msg}")
    all_ok = all_ok and ok

    # 3. Skill format
    print("\n[3/4] Checking skill format...")
    ok, issues = check_skill_format(skill_path)
    for issue in issues:
        is_error = "not found" in issue.lower() or "missing" in issue.lower()
        prefix = "  ✗" if is_error else "  !"
        print(f"{prefix} {issue}")
    if not issues:
        print("  ✓ SKILL.md format valid")
    all_ok = all_ok and ok

    # 4. Slug availability
    if args.slug:
        print(f"\n[4/4] Checking slug '{args.slug}'...")
        ok, msg = check_slug_available(args.slug)
        print(f"  {'✓' if ok else '✗'} {msg}")
        all_ok = all_ok and ok

    print("\n" + "=" * 50)
    if all_ok:
        print("✓ All checks passed! Ready to publish.")
    else:
        print("✗ Some checks failed. Fix the issues above before publishing.")
    print("=" * 50)

    return 0 if all_ok else 1


if __name__ == "__main__":
    sys.exit(main())


path: scripts/publish.py
content: #!/usr/bin/env python3
"""Publish or update a skill on ClawHub with version bumping and changelog."""

import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Optional


def run_cmd(cmd: list[str], timeout: int = 30) -> tuple[int, str, str]:
    """Run a shell command."""
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return result.returncode, result.stdout, result.stderr
    except FileNotFoundError:
        return 1, "", f"Command not found: {cmd[0]}"


def parse_frontmatter(skill_path: Path) -> Optional[dict]:
    """Parse YAML frontmatter from SKILL.md."""
    skill_md = skill_path / "SKILL.md"
    if not skill_md.exists():
        return None

    content = skill_md.read_text(encoding="utf-8")
    match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
    if not match:
        return None

    frontmatter_text = match.group(1)
    data = {}
    for line in frontmatter_text.split("\n"):
        top_match = re.match(r"^(\w+):\s*(.*)$", line)
        if top_match:
            key, value = top_match.groups()
            value = value.strip()
            if value:
                if (value.startswith('"') and value.endswith('"')) or \
                   (value.startswith("'") and value.endswith("'")):
                    value = value[1:-1]
                data[key] = value
            else:
                data[key] = {}
    return data


def update_version(skill_path: Path, new_version: str) -> bool:
    """Update version in SKILL.md frontmatter."""
    skill_md = skill_path / "SKILL.md"
    content = skill_md.read_text(encoding="utf-8")

    # Replace version line
    new_content = re.sub(
        r"^(version:\s*)[\d\.]+",
        rf"\g<1>{new_version}",
        content,
        flags=re.MULTILINE
    )

    if new_content == content:
        return False

    skill_md.write_text(new_content, encoding="utf-8")
    return True


def bump_version(version: str, bump_type: str = "patch") -> str:
    """Bump semver version."""
    parts = version.split(".")
    major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])

    if bump_type == "major":
        return f"{major + 1}.0.0"
    elif bump_type == "minor":
        return f"{major}.{minor + 1}.0"
    else:
        return f"{major}.{minor}.{patch + 1}"


def get_git_changelog(skill_path: Path) -> str:
    """Generate changelog from recent git commits."""
    rc, stdout, _ = run_cmd(
        ["git", "-C", str(skill_path), "log", "--oneline", "-5"],
        timeout=5
    )
    if rc == 0 and stdout.strip():
        commits = [line.strip() for line in stdout.strip().split("\n") if line.strip()]
        if commits:
            # Extract just the message part (after hash)
            messages = []
            for c in commits:
                parts = c.split(" ", 1)
                if len(parts) > 1:
                    messages.append(parts[1])
            if messages:
                return "; ".join(messages[:3])
    return ""


def get_current_published_version(slug: str) -> Optional[str]:
    """Get currently published version from clawhub."""
    rc, stdout, _ = run_cmd(["clawhub", "inspect", "--json", slug], timeout=10)
    if rc == 0:
        try:
            data = json.loads(stdout)
            return data.get("latestVersion", {}).get("version")
        except json.JSONDecodeError:
            pass
    return None


def publish_skill(skill_path: Path, slug: str, version: str, changelog: str) -> bool:
    """Publish skill to ClawHub."""
    cmd = [
        "clawhub", "publish", str(skill_path),
        "--slug", slug,
        "--version", version,
        "--changelog", changelog,
    ]

    print(f"\nRunning: clawhub publish {slug}@{version}")
    rc, stdout, stderr = run_cmd(cmd, timeout=60)

    if rc == 0:
        print(f"✓ {stdout.strip()}")
        return True
    else:
        print(f"✗ Failed: {stderr.strip()}")
        return False


def main() -> int:
    parser = argparse.ArgumentParser(description="Publish or update skill on ClawHub")
    parser.add_argument("--path", "-p", type=str, default=".", help="Skill directory path")
    parser.add_argument("--slug", "-s", type=str, help="Skill slug (default: read from SKILL.md)")
    parser.add_argument("--version", "-v", type=str, help="Version to publish (auto-bump if omitted)")
    parser.add_argument("--changelog", "-c", type=str, help="Changelog text")
    parser.add_argument("--bump", choices=["major", "minor", "patch"], default="patch",
                        help="Version bump type when --version not specified")
    parser.add_argument("--dry-run", action="store_true", help="Preview without publishing")
    args = parser.parse_args()

    skill_path = Path(args.path)

    # Parse frontmatter
    frontmatter = parse_frontmatter(skill_path)
    if not frontmatter:
        print("✗ Could not parse SKILL.md frontmatter")
        return 1

    slug = args.slug or frontmatter.get("name")
    if not slug:
        print("✗ No slug provided and no 'name' in frontmatter")
        return 1

    current_version = frontmatter.get("version", "0.0.0")

    # Determine version
    if args.version:
        new_version = args.version
    else:
        # Check published version
        published_version = get_current_published_version(slug)
        if published_version:
            base_version = published_version
            print(f"Current published version: {base_version}")
        else:
            base_version = current_version
            print(f"Not yet published. Current version in SKILL.md: {base_version}")

        new_version = bump_version(base_version, args.bump)
        print(f"Suggested new version: {new_version}")

    # Generate changelog if not provided
    changelog = args.changelog
    if not changelog:
        git_log = get_git_changelog(skill_path)
        if git_log:
            changelog = git_log
        else:
            changelog = f"Update to version {new_version}"
        print(f"Auto-generated changelog: {changelog}")

    if args.dry_run:
        print(f"\n[DRY RUN] Would publish {slug}@{new_version}")
        print(f"  Path: {skill_path}")
        print(f"  Changelog: {changelog}")
        return 0

    # Update version in SKILL.md
    if update_version(skill_path, new_version):
        print(f"✓ Updated SKILL.md version to {new_version}")

    # Publish
    if publish_skill(skill_path, slug, new_version, changelog):
        print(f"\n✓ Published successfully!")
        print(f"  Link: https://clawhub.ai/skills/{slug}")
        return 0
    else:
        return 1


if __name__ == "__main__":
    sys.exit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)