4e8b5af6c4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
122 lines
3.7 KiB
Bash
Executable File
122 lines
3.7 KiB
Bash
Executable File
#!/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<n_top; i++) line = line ", " tops[i]
|
|
out = out "\n" line
|
|
}
|
|
if (n_fresh > 0) {
|
|
line = " FRESH (7d): " fresh[0]
|
|
for (i=1; i<n_fresh; i++) line = line ", " fresh[i]
|
|
out = out "\n" line
|
|
}
|
|
if (n_pipe > 0) {
|
|
line = " PIPELINES: " pipes[0]
|
|
for (i=1; i<n_pipe; i++) line = line ", " pipes[i]
|
|
out = out "\n" line
|
|
}
|
|
print out
|
|
}
|
|
')
|
|
|
|
if [ -z "$line" ]; then
|
|
exit 0
|
|
fi
|
|
printf '%s\n' "$line" >"$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
|