Files
vince-winkintel__gitlab-cli…/scripts/build-claude-skill.sh
T
Vince Lozada 276a2a045a feat: add build-claude-skill.sh for Claude.ai integration (#33)
* feat: add build-claude-skill.sh for Claude.ai integration

Closes #32

Claude.ai organization skill upload requires a zip containing exactly
one SKILL.md. The existing release zip contains 38 SKILL.md files
(one per sub-skill), causing an upload error.

Changes:
- scripts/build-claude-skill.sh: merges all sub-skill SKILL.md files
  into a single consolidated SKILL.md, strips OpenClaw YAML frontmatter,
  and packages the result into claude-skill.zip
- README.md: documents Claude.ai installation via pre-built zip download
  or running the build script locally
- .gitignore: excludes claude-skill.zip (build artifact)

* fix: correct ZIP structure and add required YAML frontmatter

Per Claude.ai skill spec (support.claude.com):
- ZIP must contain files inside a subdirectory, not at the root:
    claude-skill.zip/gitlab-cli-skills/SKILL.md  ✅
    claude-skill.zip/SKILL.md                     ❌
- SKILL.md must start with YAML frontmatter containing
  'name' and 'description' fields (required by Claude.ai
  to decide when to invoke the skill)

Previously the script stripped all frontmatter and placed
SKILL.md directly in the zip root — both violations of spec.

---------

Co-authored-by: Ubuntu <ubuntu@vps-9755df38.vps.ovh.us>
2026-03-03 10:16:00 -06:00

152 lines
5.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# build-claude-skill.sh
#
# Generates claude-skill.zip — a single-SKILL.md zip suitable for upload to
# Claude.ai organization settings (which requires exactly one SKILL.md).
#
# Usage:
# bash scripts/build-claude-skill.sh [--output <path>] [--root <repo-root>]
#
# Options:
# --output Path for the output zip file (default: ./claude-skill.zip)
# --root Repo root directory (default: directory of this script's parent)
#
# Output:
# claude-skill.zip containing a single merged SKILL.md
#
# The merged file includes:
# 1. The top-level SKILL.md (OpenClaw frontmatter stripped)
# 2. Each sub-skill SKILL.md in alphabetical order (frontmatter stripped)
set -euo pipefail
# ── Resolve paths ─────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUTPUT_ZIP="$REPO_ROOT/claude-skill.zip"
# ── Parse args ────────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--output) OUTPUT_ZIP="$2"; shift 2 ;;
--root) REPO_ROOT="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
# ── Temp workspace ────────────────────────────────────────────────────────────
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
MERGED="$WORK_DIR/SKILL.md"
# ── Helper: strip YAML frontmatter and emit content ───────────────────────────
# Frontmatter is the block between the first pair of `---` lines.
strip_frontmatter() {
local file="$1"
awk '
BEGIN { in_front=0; done=0 }
/^---$/ && !done {
if (!in_front) { in_front=1; next }
else { in_front=0; done=1; next }
}
!in_front { print }
' "$file"
}
# ── Build merged SKILL.md ─────────────────────────────────────────────────────
echo "Building merged SKILL.md from $REPO_ROOT..."
# Required YAML frontmatter (Claude.ai requires name + description)
cat >> "$MERGED" <<'FRONTMATTER'
---
name: GitLab CLI Skills
description: Comprehensive GitLab CLI (glab) command reference and workflows for all GitLab operations. Use when working with merge requests, CI/CD pipelines, issues, releases, repositories, authentication, variables, labels, milestones, snippets, or any glab command. Covers 37+ sub-commands including glab mr, glab ci, glab issue, glab repo, glab release, glab variable, and more.
dependencies:
- glab
---
FRONTMATTER
# Intro body
cat >> "$MERGED" <<'INTRO'
# GitLab CLI Skills — Comprehensive glab Reference
This skill provides complete reference and workflows for the GitLab CLI (`glab`).
It covers authentication, merge requests, CI/CD pipelines, issues, releases,
repositories, and 30+ other glab commands.
---
INTRO
# 1. Top-level skill (overview + routing) — strip its frontmatter since we wrote our own
TOP_LEVEL="$REPO_ROOT/SKILL.md"
if [[ -f "$TOP_LEVEL" ]]; then
echo "## Overview" >> "$MERGED"
echo "" >> "$MERGED"
strip_frontmatter "$TOP_LEVEL" >> "$MERGED"
echo "" >> "$MERGED"
echo "---" >> "$MERGED"
echo "" >> "$MERGED"
fi
# 2. Sub-skills in alphabetical order (any directory containing a SKILL.md,
# excluding the root itself and the scripts/ directory)
mapfile -t SUB_SKILLS < <(
find "$REPO_ROOT" -mindepth 2 -maxdepth 2 -name "SKILL.md" \
! -path "$REPO_ROOT/scripts/*" \
| sort
)
TOTAL=${#SUB_SKILLS[@]}
COUNT=0
for skill_file in "${SUB_SKILLS[@]}"; do
sub_dir="$(basename "$(dirname "$skill_file")")"
COUNT=$((COUNT + 1))
echo " [$COUNT/$TOTAL] $sub_dir"
# Section heading derived from directory name (e.g. glab-mr → glab mr)
heading="${sub_dir//-/ }"
{
echo "## $heading"
echo ""
strip_frontmatter "$skill_file"
echo ""
echo "---"
echo ""
} >> "$MERGED"
done
# ── Package into zip ──────────────────────────────────────────────────────────
# Claude.ai requires files inside a subdirectory, not at the zip root:
# claude-skill.zip
# └── gitlab-cli-skills/
# └── SKILL.md
rm -f "$OUTPUT_ZIP"
# Use zip if available, otherwise fall back to python3 (always present)
if command -v zip &>/dev/null; then
mkdir -p "$WORK_DIR/gitlab-cli-skills"
cp "$MERGED" "$WORK_DIR/gitlab-cli-skills/SKILL.md"
(cd "$WORK_DIR" && zip -qr "$OUTPUT_ZIP" gitlab-cli-skills/)
else
python3 -c "
import zipfile, sys
output, source = sys.argv[1], sys.argv[2]
with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.write(source, 'gitlab-cli-skills/SKILL.md')
" "$OUTPUT_ZIP" "$MERGED"
fi
LINES=$(wc -l < "$MERGED")
SIZE=$(wc -c < "$MERGED")
echo ""
echo "✅ Done."
echo " Merged: $TOTAL sub-skills + top-level"
echo " Lines: $LINES"
echo " Size: $SIZE bytes"
echo " Output: $OUTPUT_ZIP"