Files
Clawd BotandClaude Opus 4.6 ca9b510922 chore: align with upstream openclaw/openclaw and overlay local additions
- Reset master to upstream/main (16,697 commits)
- Overlay 2,271 local-only files (skills, tools, workspace, configs, apps)
- Restore IDENTITY.md and USER.md templates
- Build verified, gateway running, Discord working

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-03 07:40:46 +01:00

334 lines
11 KiB
Bash

#!/usr/bin/env bash
# skill-dev.sh — local skill development pipeline for OpenClaw
# Usage: skill-dev.sh <subcommand> [args]
#
# Subcommands:
# new <name> Scaffold a new skill dir with SKILL.md under ~/clawd/skills/
# link <path> Symlink an external skill directory into ~/clawd/skills/
# test <name> Validate an existing skill's structure and SKILL.md frontmatter
set -euo pipefail
SKILLS_DIR="${HOME}/clawd/skills"
SCRIPT_NAME="$(basename "$0")"
usage() {
cat <<EOF
Usage: ${SCRIPT_NAME} <subcommand> [args]
Subcommands:
new <name> Scaffold a new skill at ${SKILLS_DIR}/<name>/SKILL.md
link <path> Symlink an external skill directory into ${SKILLS_DIR}/
test <name> Validate skill structure and SKILL.md frontmatter
Examples:
${SCRIPT_NAME} new my-skill
${SCRIPT_NAME} link /path/to/external/my-skill
${SCRIPT_NAME} test weather
${SCRIPT_NAME} test my-skill
EOF
}
# ── helpers ──────────────────────────────────────────────────────────────────
die() { echo "ERROR: $*" >&2; exit 1; }
info() { echo " $*"; }
ok() { echo "[ok] $*"; }
fail() { echo "[fail] $*" >&2; }
require_arg() {
[[ $# -ge 2 ]] || die "subcommand '${1}' requires an argument. Run: ${SCRIPT_NAME} --help"
}
# ── new ───────────────────────────────────────────────────────────────────────
cmd_new() {
local name="${1:-}"
[[ -n "${name}" ]] || die "'new' requires a skill name. Usage: ${SCRIPT_NAME} new <name>"
# Sanitise: lowercase, hyphens only
local safe_name
safe_name="$(echo "${name}" | tr '[:upper:]' '[:lower:]' | tr '_' '-' | tr -cd 'a-z0-9-')"
[[ "${safe_name}" == "${name}" ]] || echo "Note: sanitised name '${name}' -> '${safe_name}'"
local skill_dir="${SKILLS_DIR}/${safe_name}"
[[ ! -e "${skill_dir}" ]] || die "Skill '${safe_name}' already exists at ${skill_dir}"
[[ -d "${SKILLS_DIR}" ]] || die "Skills directory not found: ${SKILLS_DIR}"
mkdir -p "${skill_dir}/scripts"
# Write SKILL.md with YAML frontmatter (the format openclaw expects)
cat > "${skill_dir}/SKILL.md" << EOF
---
name: ${safe_name}
description: "TODO: one-sentence description of what this skill does."
metadata:
{
"openclaw":
{
"emoji": "🔧",
"requires": { "bins": [] },
},
}
---
# ${safe_name} Skill
TODO: Describe what this skill enables the agent to do.
## Usage
\`\`\`bash
# Example command
echo "replace with real usage"
\`\`\`
## Notes
- Add context, tips, and edge cases here.
EOF
# Write a starter helper script
cat > "${skill_dir}/scripts/${safe_name}.sh" << EOF
#!/usr/bin/env bash
# ${safe_name}.sh — helper script for the ${safe_name} skill
# Usage: ${safe_name}.sh <subcommand> [args]
set -euo pipefail
case "\${1:-}" in
help|--help|-h)
echo "Usage: ${safe_name}.sh <subcommand>"
;;
*)
echo "Unknown subcommand: \${1:-}"
exit 1
;;
esac
EOF
chmod +x "${skill_dir}/scripts/${safe_name}.sh"
echo "Scaffolded skill: ${safe_name}"
info "Location : ${skill_dir}/"
info "SKILL.md : ${skill_dir}/SKILL.md"
info "Script : ${skill_dir}/scripts/${safe_name}.sh"
echo ""
echo "Next steps:"
info "1. Edit ${skill_dir}/SKILL.md — fill in description, requirements, usage docs"
info "2. Edit ${skill_dir}/scripts/${safe_name}.sh — implement your helper"
info "3. Validate: ${SCRIPT_NAME} test ${safe_name}"
info "4. Check readiness: openclaw skills check"
}
# ── link ─────────────────────────────────────────────────────────────────────
cmd_link() {
local src_path="${1:-}"
[[ -n "${src_path}" ]] || die "'link' requires a path. Usage: ${SCRIPT_NAME} link <path>"
# Resolve to absolute path
src_path="$(realpath "${src_path}" 2>/dev/null)" || die "Path does not exist: ${1}"
[[ -d "${src_path}" ]] || die "Not a directory: ${src_path}"
# Determine skill name from the directory or from SKILL.md frontmatter
local skill_name
local skill_md="${src_path}/SKILL.md"
if [[ -f "${skill_md}" ]]; then
# Try to extract name from frontmatter
skill_name="$(awk '/^---/{if(++c==2)exit} c==1 && /^name:/{gsub(/^name:[[:space:]]*/, ""); gsub(/[[:space:]]*$/, ""); print}' "${skill_md}")"
fi
# Fall back to directory name if name not found in frontmatter
if [[ -z "${skill_name}" ]]; then
skill_name="$(basename "${src_path}")"
echo "Warning: could not read 'name' from SKILL.md; using directory name '${skill_name}'"
fi
local link_target="${SKILLS_DIR}/${skill_name}"
if [[ -L "${link_target}" ]]; then
local existing_target
existing_target="$(readlink -f "${link_target}")"
if [[ "${existing_target}" == "${src_path}" ]]; then
echo "Already linked: ${link_target} -> ${src_path}"
exit 0
else
die "A symlink for '${skill_name}' already exists pointing to ${existing_target}. Remove it first."
fi
fi
[[ ! -e "${link_target}" ]] || die "A directory already exists at ${link_target}. Remove or rename it first."
ln -s "${src_path}" "${link_target}"
echo "Linked skill: ${skill_name}"
info "Symlink : ${link_target}"
info "Source : ${src_path}"
echo ""
info "Verify with: openclaw skills info ${skill_name}"
}
# ── test ─────────────────────────────────────────────────────────────────────
cmd_test() {
local name="${1:-}"
[[ -n "${name}" ]] || die "'test' requires a skill name. Usage: ${SCRIPT_NAME} test <name>"
local skill_dir="${SKILLS_DIR}/${name}"
local skill_md="${skill_dir}/SKILL.md"
echo "Testing skill: ${name}"
echo ""
local pass=0
local fail_count=0
check() {
local label="$1"
local result="$2" # "ok" or error message
if [[ "${result}" == "ok" ]]; then
ok "${label}"
((pass++)) || true
else
fail "${label}: ${result}"
((fail_count++)) || true
fi
}
# 1. Skill directory exists
if [[ -d "${skill_dir}" ]]; then
check "skill directory exists" "ok"
else
check "skill directory exists" "not found at ${skill_dir}"
echo ""
echo "Result: FAIL (${fail_count} check(s) failed)"
exit 1
fi
# 2. SKILL.md exists
if [[ -f "${skill_md}" ]]; then
check "SKILL.md present" "ok"
else
check "SKILL.md present" "missing at ${skill_md}"
echo ""
echo "Result: FAIL (${fail_count} check(s) failed)"
exit 1
fi
# 3. SKILL.md has opening frontmatter delimiter
local first_line
first_line="$(head -1 "${skill_md}")"
if [[ "${first_line}" == "---" ]]; then
check "frontmatter opening '---'" "ok"
else
check "frontmatter opening '---'" "first line is '${first_line}', expected '---'"
fi
# 4. SKILL.md has a 'name' field in frontmatter
local fm_name
fm_name="$(awk '/^---/{if(++c==2)exit} c==1 && /^name:/{gsub(/^name:[[:space:]]*/, ""); gsub(/[[:space:]]*$/, ""); print}' "${skill_md}")"
if [[ -n "${fm_name}" ]]; then
check "frontmatter 'name' field present (value: '${fm_name}')" "ok"
else
check "frontmatter 'name' field present" "missing or empty"
fi
# 5. name in frontmatter matches directory name
if [[ -n "${fm_name}" ]] && [[ "${fm_name}" == "${name}" ]]; then
check "frontmatter name matches directory name" "ok"
elif [[ -n "${fm_name}" ]]; then
check "frontmatter name matches directory name" "frontmatter says '${fm_name}', directory is '${name}'"
fi
# 6. SKILL.md has a 'description' field
local fm_desc
fm_desc="$(awk '/^---/{if(++c==2)exit} c==1 && /^description:/{gsub(/^description:[[:space:]]*/, ""); gsub(/^["'\'']|["'\''][[:space:]]*$/, ""); print}' "${skill_md}")"
if [[ -n "${fm_desc}" ]]; then
check "frontmatter 'description' field present" "ok"
info " description: ${fm_desc:0:80}$([ ${#fm_desc} -gt 80 ] && echo '...' || true)"
else
check "frontmatter 'description' field present" "missing or empty"
fi
# 7. Closing frontmatter delimiter exists
local close_count
close_count="$(grep -c '^---$' "${skill_md}" || true)"
if [[ "${close_count}" -ge 2 ]]; then
check "frontmatter closing '---'" "ok"
else
check "frontmatter closing '---'" "only ${close_count} '---' delimiter(s) found; need at least 2"
fi
# 8. Markdown body is non-empty (content after second ---)
local body_lines
body_lines="$(awk '/^---/{if(++c==2){found=1;next}} found{print}' "${skill_md}" | grep -v '^[[:space:]]*$' | wc -l || true)"
if [[ "${body_lines}" -gt 0 ]]; then
check "markdown body non-empty (${body_lines} non-blank lines)" "ok"
else
check "markdown body non-empty" "body is empty after frontmatter"
fi
# 9. scripts/ directory check (optional — warn if present scripts aren't executable)
if [[ -d "${skill_dir}/scripts" ]]; then
local non_exec_scripts=()
while IFS= read -r -d '' script_file; do
[[ -x "${script_file}" ]] || non_exec_scripts+=("${script_file}")
done < <(find "${skill_dir}/scripts" -maxdepth 1 -type f \( -name "*.sh" -o -name "*.py" -o -name "*.js" \) -print0 2>/dev/null)
if [[ ${#non_exec_scripts[@]} -eq 0 ]]; then
check "scripts/ — all scripts executable" "ok"
else
for f in "${non_exec_scripts[@]}"; do
check "script executable: $(basename "${f}")" "not executable (run: chmod +x ${f})"
done
fi
fi
# 10. Check binary requirements declared in frontmatter
local bins_line
bins_line="$(awk '/^---/{if(++c==2)exit} c==1 && /\"bins\"/{print}' "${skill_md}" | head -1)"
if [[ -n "${bins_line}" ]]; then
# Extract quoted strings from the bins array
local -a declared_bins
while IFS= read -r bin; do
[[ -n "${bin}" ]] && declared_bins+=("${bin}")
done < <(echo "${bins_line}" | grep -oE '"[^"]+"' | tr -d '"' || true)
for bin in "${declared_bins[@]}"; do
if command -v "${bin}" &>/dev/null; then
check "required binary '${bin}'" "ok"
else
check "required binary '${bin}'" "not found in PATH"
fi
done
fi
# 11. openclaw skills check for this skill (if CLI available)
if command -v openclaw &>/dev/null; then
local oc_status
oc_status="$(openclaw skills check 2>/dev/null | grep -F "${name}" | head -1 || true)"
if echo "${oc_status}" | grep -q "✓\|ready"; then
check "openclaw skills check: ready" "ok"
elif echo "${oc_status}" | grep -q "✗\|missing"; then
check "openclaw skills check: ready" "reported as not ready — check binary requirements above"
else
info "(openclaw skills check: skill not found in output — may need gateway restart)"
fi
fi
echo ""
echo "Result: $( [[ ${fail_count} -eq 0 ]] && echo "PASS (${pass} checks)" || echo "FAIL (${fail_count} failed, ${pass} passed)" )"
[[ ${fail_count} -eq 0 ]]
}
# ── dispatch ──────────────────────────────────────────────────────────────────
case "${1:-}" in
new) shift; cmd_new "$@" ;;
link) shift; cmd_link "$@" ;;
test) shift; cmd_test "$@" ;;
--help|-h|help) usage ;;
"") usage; exit 1 ;;
*) die "Unknown subcommand: '${1}'. Run: ${SCRIPT_NAME} --help" ;;
esac