Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/scripts/package-skills.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
#
# Package each Skyflow skill directory into a portable zip.
#
# Each zip extracts to <skill-name>/SKILL.md so it can be dropped straight into
# ~/.claude/skills/ (or a project's .claude/skills/) or handed to any harness
# that understands the Agent Skills format. Repo-only cruft is excluded so the
# artifact is clean.
#
# Output: dist/<skill-name>.zip for every skill, plus dist/SHA256SUMS.txt.
set -euo pipefail

SKILLS_DIR="skyflow-skills-plugin/skills"
OUT_DIR="dist"

# Files that exist for repo/contributor purposes and don't belong in a
# portable, runtime-facing skill artifact.
EXCLUDES=(
'*/.DS_Store'
'*/CONTRIBUTING.md'
'*.tmp'
'*.log'
)

if [[ ! -d "$SKILLS_DIR" ]]; then
echo "error: $SKILLS_DIR not found (run from repo root)" >&2
exit 1
fi

rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"
OUT_ABS="$(cd "$OUT_DIR" && pwd)"

shopt -s nullglob
count=0
for skill_path in "$SKILLS_DIR"/*/; do
skill="$(basename "$skill_path")"
echo "Packaging $skill ..."
# Zip from inside SKILLS_DIR so archive paths are <skill>/... (no leading dirs).
( cd "$SKILLS_DIR" && zip -r -q "$OUT_ABS/$skill.zip" "$skill" -x "${EXCLUDES[@]}" )
count=$((count + 1))
done

if [[ "$count" -eq 0 ]]; then
echo "error: no skills found under $SKILLS_DIR" >&2
exit 1
fi

# Integrity manifest (paths relative to dist/).
( cd "$OUT_DIR" && sha256sum ./*.zip > SHA256SUMS.txt )

echo
echo "Packaged $count skill(s) into $OUT_DIR/:"
ls -1 "$OUT_DIR"
109 changes: 109 additions & 0 deletions .github/scripts/validate-skills.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Validate Skyflow skill directories before packaging.

Each skill under skyflow-skills-plugin/skills/<name>/ must contain a SKILL.md
with YAML frontmatter whose `name` matches the directory and whose
`description` is present. Rules mirror the Agent Skills spec so that every
published zip is a valid, portable skill.

Exit code 0 = all valid, 1 = one or more problems (details printed).
"""

from __future__ import annotations

import re
import sys
from pathlib import Path

import yaml

SKILLS_DIR = Path("skyflow-skills-plugin/skills")
FRONTMATTER_RE = re.compile(r"^---\r?\n(.*?)\r?\n---\r?\n", re.DOTALL)
NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
NAME_MAX = 64
DESC_MAX = 1024


def parse_frontmatter(text: str) -> dict | None:
"""Parse the leading --- fenced YAML block into a dict.

Uses a full YAML parser so multi-line values (block scalars, quoted
strings spanning lines) are handled correctly. Returns None if there is
no frontmatter block or it is not a YAML mapping.
"""
m = FRONTMATTER_RE.match(text)
if not m:
return None
try:
data = yaml.safe_load(m.group(1))
except yaml.YAMLError:
return None
return data if isinstance(data, dict) else None


def validate_skill(skill_dir: Path) -> list[str]:
errors: list[str] = []
skill_md = skill_dir / "SKILL.md"
if not skill_md.is_file():
return [f"{skill_dir.name}: missing SKILL.md"]

fm = parse_frontmatter(skill_md.read_text(encoding="utf-8"))
if fm is None:
return [f"{skill_dir.name}: SKILL.md has no valid --- YAML frontmatter block"]

name = fm.get("name")
if not name or not isinstance(name, str):
errors.append(f"{skill_dir.name}: frontmatter missing `name`")
else:
if name != skill_dir.name:
errors.append(
f"{skill_dir.name}: frontmatter name `{name}` does not match directory"
)
if len(name) > NAME_MAX:
errors.append(f"{skill_dir.name}: name exceeds {NAME_MAX} chars")
if not NAME_RE.match(name):
errors.append(
f"{skill_dir.name}: name `{name}` must be lowercase letters, digits, and hyphens"
)

desc = fm.get("description")
if not desc or not isinstance(desc, str) or not desc.strip():
errors.append(f"{skill_dir.name}: frontmatter missing `description`")
elif len(desc) > DESC_MAX:
errors.append(
f"{skill_dir.name}: description exceeds {DESC_MAX} chars ({len(desc)})"
)

return errors


def main() -> int:
if not SKILLS_DIR.is_dir():
print(f"error: {SKILLS_DIR} not found (run from repo root)", file=sys.stderr)
return 1

skill_dirs = sorted(p for p in SKILLS_DIR.iterdir() if p.is_dir())
if not skill_dirs:
print(f"error: no skills found under {SKILLS_DIR}", file=sys.stderr)
return 1

all_errors: list[str] = []
for skill_dir in skill_dirs:
errs = validate_skill(skill_dir)
if errs:
all_errors.extend(errs)
else:
print(f" ok {skill_dir.name}")

if all_errors:
print("\nValidation failed:", file=sys.stderr)
for e in all_errors:
print(f" - {e}", file=sys.stderr)
return 1

print(f"\nAll {len(skill_dirs)} skills valid.")
return 0


if __name__ == "__main__":
sys.exit(main())
75 changes: 75 additions & 0 deletions .github/workflows/package-skills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Package skills

# Build portable, per-skill zips and publish them as GitHub Release assets.
# Source of truth stays in skyflow-skills-plugin/skills/; the zips are pure
# build output and never committed, so the plugin/marketplace install is
# unaffected.
#
# Triggers:
# - push of a version tag (v*) -> release for that tag
# - manual run (workflow_dispatch) -> release for the provided tag,
# defaulting to v<plugin.json version>

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Release tag (defaults to v<plugin.json version>)"
required: false
type: string

permissions:
contents: write

jobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Install dependencies
run: pip install pyyaml

- name: Validate skills
run: python3 .github/scripts/validate-skills.py

- name: Package skills
run: bash .github/scripts/package-skills.sh

- name: Resolve release tag
id: tag
run: |
if [ "${{ github.event_name }}" = "push" ]; then
tag="${GITHUB_REF_NAME}"
elif [ -n "${{ inputs.tag }}" ]; then
tag="${{ inputs.tag }}"
else
version="$(python3 -c 'import json;print(json.load(open("skyflow-skills-plugin/.claude-plugin/plugin.json"))["version"])')"
tag="v${version}"
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "Release tag: ${tag}"

- name: Publish release assets
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
assets=(dist/*.zip dist/SHA256SUMS.txt)
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG exists; uploading assets (clobber)."
gh release upload "$TAG" "${assets[@]}" --clobber
else
echo "Creating release $TAG."
gh release create "$TAG" "${assets[@]}" \
--title "Skyflow skills $TAG" \
--notes "Portable Skyflow skill packages. Download a skill's \`.zip\`, unzip it into \`~/.claude/skills/\` (or a project's \`.claude/skills/\`) or any Agent Skills-compatible harness. Verify downloads against \`SHA256SUMS.txt\`."
fi
28 changes: 28 additions & 0 deletions .github/workflows/validate-skills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Validate skills

# Lightweight check: every skill under skyflow-skills-plugin/skills/ must have a
# valid SKILL.md before it can be merged or packaged. No artifacts are produced.

on:
pull_request:
paths:
- "skyflow-skills-plugin/skills/**"
- ".github/scripts/validate-skills.py"
- ".github/workflows/validate-skills.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install dependencies
run: pip install pyyaml
- name: Validate skill frontmatter
run: python3 .github/scripts/validate-skills.py
28 changes: 28 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,34 @@ After creating a new skill, add it to the Skills section in the main [README.md]
2. Write a paragraph describing what the skill does and its key features
3. The table of contents will be updated automatically if using a markdown formatter

## Packaging and Releasing Skills

Skills are distributed two ways from a single source of truth (the directories under `skyflow-skills-plugin/skills/`):

1. **Plugin** — installed via the marketplace (`/plugin install skyflow-skills@skyflow-marketplace`).
2. **Standalone zips** — one portable `.zip` per skill, published as GitHub Release assets for use in other projects or harnesses.

The zips are build artifacts. They are **not** committed to the repo (`dist/` is gitignored), so the plugin/marketplace install is never affected.

### CI workflows

- **`.github/workflows/validate-skills.yml`** — runs on pull requests that touch skills. It checks every `SKILL.md` has valid frontmatter (`name` matches the directory, lowercase-hyphen, ≤64 chars; `description` present, ≤1024 chars).
- **`.github/workflows/package-skills.yml`** — runs on a pushed version tag (`v*`) or manual dispatch. It validates, zips each skill into `dist/<skill>.zip`, generates `dist/SHA256SUMS.txt`, and attaches everything to the matching GitHub Release.

### Cutting a release

1. Bump `version` in `skyflow-skills-plugin/.claude-plugin/plugin.json`.
2. Tag and push: `git tag v0.6.0 && git push origin v0.6.0`. The package workflow creates the release and uploads the zips automatically.

Alternatively, trigger the **Package skills** workflow manually (Actions tab → *Run workflow*); it defaults the tag to `v<plugin.json version>`.

### Building locally

```sh
python3 .github/scripts/validate-skills.py # validate frontmatter
bash .github/scripts/package-skills.sh # build dist/*.zip + SHA256SUMS.txt
```

## Learn More

For complete documentation on Claude Code plugins, see the [Claude Code Plugins documentation](https://code.claude.com/docs/en/plugins).
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A Claude Code plugin marketplace that enables Skyflow's data privacy and protect
- [Claude Code Plugins for Skyflow](#claude-code-plugins-for-skyflow)
- [Plugins](#plugins)
- [Quick Start](#quick-start)
- [Standalone Skill Downloads](#standalone-skill-downloads)
- [Environment Variables Reference](#environment-variables-reference)
- [Upgrading](#upgrading)
- [Learn More](#learn-more)
Expand Down Expand Up @@ -64,6 +65,24 @@ Most users want `skyflow-skills` plus `skyflow-developer-mcp`. Add `skyflow-runt
- [skyflow-developer-mcp setup](skyflow-developer-mcp-plugin/README.md#set-up-environment-variables)
- [skyflow-runtime-mcp setup](skyflow-runtime-mcp-plugin/README.md#set-up-environment-variables)

## Standalone Skill Downloads

The `skyflow-skills` plugin is the easiest way to get the skills in Claude Code. If you instead want a single skill as a portable file — to drop into another project, share, or use with a different Agent Skills-compatible harness — each skill is also published as a standalone `.zip` on the [Releases page](https://github.com/SkyflowFoundry/claude/releases/latest).

Each archive unzips to a self-contained skill folder (`<skill-name>/SKILL.md` plus its resources). To install one manually:

```sh
# Download the latest build of a skill (stable URL always points at the newest release)
curl -L -O https://github.com/SkyflowFoundry/claude/releases/latest/download/create-vault.zip

# Unzip into your user skills directory (or a project's .claude/skills/)
unzip create-vault.zip -d ~/.claude/skills/
```

Available skills: `call-rest-apis`, `create-vault`, `migrate-sdk-v1-to-v2`, `plan-skyflow-implementation`, `quickstart-js-browser`, `quickstart-node`. A `SHA256SUMS.txt` is attached to each release so you can verify downloads.

> These zips are build artifacts generated from the same skills in this repo — the plugin and the standalone downloads are always in sync.

## Environment Variables Reference

These variables are read by the MCP plugins. See each plugin's README for step-by-step setup.
Expand Down
Loading