#!/usr/bin/env bash # Scribe plugin — sync stored Processes into auto-surfacing local skills. # # Fetches `GET /api/plugin/processes` and writes one # ~/.claude/skills/scribe-proc-/SKILL.md # per Process. The stub's frontmatter `description` is the auto-surface trigger; # its body tells Claude to call get_process() and follow the LIVE procedure # from Scribe (the DB stays the single source of truth — the stub is a pointer). # # WHY LOCAL ~/.claude/skills (not the plugin dir): the plugin is git-cloned and # identical on every install, so instance-specific stubs can't live inside it. # Personal skills in ~/.claude/skills are live-detected by Claude Code within the # session, so a freshly written stub auto-surfaces without a restart. # # TRIGGERS: this runs at SessionStart (alongside the context hook) so stubs stay # fresh each session, and on demand via the `/scribe:sync` command. # # FAIL-OPEN & SILENT: never blocks a session; emits NOTHING on stdout (so it's # safe as a second SessionStart hook). On any fetch failure it exits without # touching existing stubs — a transient outage must not wipe the user's skills. # Config mirrors the context hook (CLAUDE_PLUGIN_OPTION_* / SCRIBE_* override). set -uo pipefail command -v jq >/dev/null 2>&1 || exit 0 command -v curl >/dev/null 2>&1 || exit 0 url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_api_endpoint:-}} token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_api_token:-}} # Guard against an unexpanded `${...}` placeholder arriving as a literal. case "$url" in *'${'*) url="" ;; esac case "$token" in *'${'*) token="" ;; esac [ -n "$url" ] && [ -n "$token" ] || exit 0 body=$(curl -fsS --max-time 8 \ -H "Authorization: Bearer ${token}" \ "${url%/}/api/plugin/processes" 2>/dev/null) || exit 0 [ -n "$body" ] || exit 0 count=$(printf '%s' "$body" | jq -r '.processes | length' 2>/dev/null) || exit 0 [ -n "$count" ] && [ "$count" != "null" ] || exit 0 skills_dir="${HOME}/.claude/skills" mkdir -p "$skills_dir" 2>/dev/null || exit 0 # Slugs written this run — anything else under scribe-proc-* is pruned below. managed=" " i=0 while [ "$i" -lt "$count" ]; do name=$(printf '%s' "$body" | jq -r ".processes[$i].name // empty" 2>/dev/null) slug=$(printf '%s' "$body" | jq -r ".processes[$i].slug // empty" 2>/dev/null) desc=$(printf '%s' "$body" | jq -r ".processes[$i].description // empty" 2>/dev/null) i=$((i + 1)) [ -n "$slug" ] && [ -n "$name" ] || continue dir="${skills_dir}/scribe-proc-${slug}" mkdir -p "$dir" 2>/dev/null || continue # description folded to one line — YAML scalar must not contain a newline. desc=$(printf '%s' "$desc" | tr '\n' ' ') { printf -- '---\n' printf 'name: scribe-proc-%s\n' "$slug" printf 'description: %s\n' "$desc" printf -- '---\n\n' printf '\n\n' printf 'This is a saved **Scribe Process**: `%s`.\n\n' "$name" printf 'Do not improvise it. Call `get_process("%s")` via the Scribe MCP server to fetch the live procedure, then follow the returned body verbatim — including any "clarify first" steps it contains.\n' "$name" } > "${dir}/SKILL.md" 2>/dev/null || continue managed="${managed}${slug} " done # Prune stubs whose Process no longer exists (scoped to our scribe-proc-* prefix). for d in "${skills_dir}"/scribe-proc-*; do [ -d "$d" ] || continue slug=$(basename "$d"); slug=${slug#scribe-proc-} case "$managed" in *" $slug "*) : ;; *) rm -rf "$d" 2>/dev/null ;; esac done exit 0