master
John Lauer Publish 0.29.0 d8af92c 3d ago
#!/bin/bash
# PostToolUse(SendUserFile) hook - SHIPPED AND MANAGED BY adom-shotlog.
# When an AI SENDS an image to the human with a caption, that caption becomes
# the description of the shot made from the same file (matched by content via
# `shotlog annotate`), so the shotlog card says what the AI said about it -
# not just "Auto-captured screenshot Claude read for analysis: <file>".
# An image that was sent but never Read is injected with the caption.
# Installed by `adom-shotlog install` and re-verified at every
# `adom-shotlog serve` startup; edits here are overwritten.
#
# Reads the hook JSON on stdin:
#   {"tool_name":"SendUserFile","tool_input":{"files":[...],"caption":"..."},"cwd":"..."}
# Always exits 0 so it can never block a send.

# Same switch as the read-hook: auto-capture off in the viewer's Preferences.
[ -f "$HOME/.claude/hooks/.shotlog-read-hook-off" ] && exit 0

INPUT="$(cat)"
[ "$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)" = "SendUserFile" ] || exit 0
CAPTION="$(echo "$INPUT" | jq -r '.tool_input.caption // empty' 2>/dev/null)"
# No caption: nothing to carry over (the file name is all the read-hook had too).
[ -z "$(printf '%s' "$CAPTION" | tr -d '[:space:]')" ] && exit 0
command -v shotlog >/dev/null 2>&1 || exit 0
CWD="$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)"

# Thread + context exactly as the read-hook derives them (used only where the
# shot has none, or when the image was never read and is injected here).
SID="$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)"
TP="$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)"
THREAD=""; CONTEXT=""
if [ -n "$TP" ] && [ -f "$TP" ]; then
  THREAD="$(grep '"type":"custom-title"' "$TP" 2>/dev/null | tail -1 | jq -r '.customTitle // empty' 2>/dev/null)"
  [ -z "$THREAD" ] && THREAD="$(grep '"type":"ai-title"' "$TP" 2>/dev/null | tail -1 | jq -r '.aiTitle // empty' 2>/dev/null)"
  CONTEXT="$(grep '"type":"last-prompt"' "$TP" 2>/dev/null | tail -1 | jq -r '.lastPrompt // empty' 2>/dev/null | head -c 220)"
fi
[ -z "$THREAD" ] && [ -n "$SID" ] && THREAD="thread ${SID:0:8}"

shotlog health >/dev/null 2>&1 || { (cd "${SHOTLOG_HOME:-$HOME/project}" && shotlog serve >/dev/null 2>&1 &) ; sleep 1; }

echo "$INPUT" | jq -r '.tool_input.files[]? // empty' 2>/dev/null | while IFS= read -r f; do
  [ -z "$f" ] && continue
  case "$f" in /*) ;; *) [ -n "$CWD" ] && f="$CWD/$f" ;; esac
  case "$f" in
    *.png|*.jpg|*.jpeg|*.PNG|*.JPG|*.JPEG) ;;   # the same types the read-hook captures
    *) continue ;;
  esac
  [ -f "$f" ] || continue
  # A file already inside shotlog's own store is a stored COPY (possibly
  # resized): its fingerprint never matches the original, so --inject-missing
  # would add a duplicate card. Same skip as the read-hook.
  case "$f" in */screenshots/shotlog/*) continue ;; esac
  for attempt in 1 2 3; do
    shotlog annotate -c read-captures --inject-missing -d "$CAPTION" \
      ${THREAD:+--thread "$THREAD"} ${CONTEXT:+--context "$CONTEXT"} "$f" >/dev/null 2>&1 && break
    sleep 2
  done
done

exit 0