#!/usr/bin/env bash # UserPromptSubmit hook: inyecta capacidades calientes (TOP/FRESH/PIPELINES) # del registry como additionalContext en cada turno del usuario. # # Cache: ~/.cache/fn_registry/capabilities.txt (TTL 1h). # Fuente: `./fn doctor capabilities --emit-claude-md` desde la raiz del repo. # # NUNCA bloquea: si algo falla, emite contexto vacio y sale 0. set -uo pipefail CACHE_DIR="${HOME}/.cache/fn_registry" CACHE_FILE="${CACHE_DIR}/capabilities.txt" TTL_SECONDS=3600 # Resolve registry root (walks up from cwd, fallback CLAUDE_PROJECT_DIR) resolve_root() { local d="${PWD}" while [ "$d" != "/" ]; do if [ -f "$d/registry.db" ] && [ -x "$d/fn" ]; then printf '%s' "$d" return 0 fi d=$(dirname "$d") done if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -f "${CLAUDE_PROJECT_DIR}/registry.db" ]; then printf '%s' "${CLAUDE_PROJECT_DIR}" return 0 fi return 1 } # Consume stdin (UserPromptSubmit payload) — we don't need it but keep stdin clean cat >/dev/null 2>&1 || true ROOT=$(resolve_root) || exit 0 mkdir -p "$CACHE_DIR" 2>/dev/null || exit 0 # Cache freshness check need_refresh=1 if [ -f "$CACHE_FILE" ]; then now=$(date +%s) mtime=$(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0) age=$((now - mtime)) if [ "$age" -lt "$TTL_SECONDS" ]; then need_refresh=0 fi fi if [ "$need_refresh" -eq 1 ]; then # Regenerate: call fn doctor capabilities --emit-claude-md and process raw=$("$ROOT/fn" doctor capabilities --emit-claude-md 2>/dev/null || true) if [ -z "$raw" ]; then exit 0 fi # Extract top 5 from each section using awk. # Sections detected by "## ... Top" / "## ... Fresh" / "## ... Pipelines". line=$(printf '%s\n' "$raw" | awk ' BEGIN { sec=""; n_top=0; n_fresh=0; n_pipe=0; } /^## .*Top 20/ { sec="TOP"; next } /^## .*Fresh/ { sec="FRESH"; next } /^## .*Pipelines/ { sec="PIPE"; next } /^## / { sec=""; next } /^- `/ { # extract first backticked token s = $0 sub(/^- `/, "", s) i = index(s, "`") if (i == 0) next id = substr(s, 1, i-1) if (sec == "TOP" && n_top < 5) { tops[n_top++] = id } if (sec == "FRESH" && n_fresh < 5) { fresh[n_fresh++] = id } if (sec == "PIPE" && n_pipe < 5) { pipes[n_pipe++] = id } } END { out = "CAPABILITIES (cache 1h):" if (n_top > 0) { line = " TOP: " tops[0] for (i=1; i 0) { line = " FRESH (7d): " fresh[0] for (i=1; i 0) { line = " PIPELINES: " pipes[0] for (i=1; i"$CACHE_FILE" 2>/dev/null || exit 0 fi # Emit cached content as additionalContext if [ ! -s "$CACHE_FILE" ]; then exit 0 fi ctx=$(cat "$CACHE_FILE") if command -v jq >/dev/null 2>&1; then jq -n --arg ctx "$ctx" '{ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: $ctx } }' else # Fallback: print raw text (Claude Code prints stdout as context too) printf '%s\n' "$ctx" fi exit 0