#!/usr/bin/env bash # Claude Code PreToolUse hook: шлёт короткую строку в Telegram перед # каждым tool-call'ом для ощущения «активности». Дебаунс 1.5с — пока # tool-вызовы летят пачкой, копим в /tmp буфер и шлём одним сообщением # через 1.5 секунды тишины. # # Конфиг — /etc/food-market/telegram.env. Логи — /var/log/cc-tg-notify.log. # Off-switch: создать /tmp/cc-tg-quiet — все pretool-уведомления # скипаются (Stop hook продолжает работать). set -u ENV_FILE="/etc/food-market/telegram.env" LOG_FILE="${CC_TG_LOG:-/var/log/cc-tg-notify.log}" BUF="/tmp/cc-tg-pretool-buffer.txt" LAST="/tmp/cc-tg-pretool-last" LOCK="/tmp/cc-tg-pretool.lock" QUIET_FLAG="/tmp/cc-tg-quiet" DEBOUNCE_SEC="1.5" MAX_LINES=20 log() { printf '%s [pretool] %s\n' "$(date -Is)" "$*" >>"$LOG_FILE" 2>/dev/null || true; } [[ -f "$QUIET_FLAG" ]] && exit 0 if [[ -r "$ENV_FILE" ]]; then # shellcheck disable=SC1090 set -a; source "$ENV_FILE"; set +a fi TOKEN="${TELEGRAM_BOT_TOKEN:-}" CHAT_ID="${TELEGRAM_CHAT_ID:-}" [[ -z "$TOKEN" || -z "$CHAT_ID" ]] && exit 0 INPUT_JSON="" if [[ ! -t 0 ]]; then INPUT_JSON="$(cat)"; fi [[ -z "$INPUT_JSON" ]] && exit 0 TOOL="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_name // empty' 2>/dev/null)" [[ -z "$TOOL" || "$TOOL" == "TodoWrite" ]] && exit 0 # Извлекаем поле tool_input под нужный тип. cut -c обрезает многобайтные # UTF-8 неаккуратно, но для urlencode результат остаётся валидным. LINE="" case "$TOOL" in Bash) DESC="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.description // empty' 2>/dev/null | tr '\n' ' ' | head -c 100)" CMD="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.command // empty' 2>/dev/null | tr '\n' ' ' | head -c 80)" if [[ -n "$DESC" ]]; then LINE="🔨 $DESC"; else LINE="🔨 Bash: $CMD"; fi ;; Edit) FP="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.file_path // empty' 2>/dev/null)" LINE="✏️ Edit: $(basename "${FP:-?}")" ;; Write) FP="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.file_path // empty' 2>/dev/null)" LINE="📝 Write: $(basename "${FP:-?}")" ;; Read) FP="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.file_path // empty' 2>/dev/null)" LINE="📖 Read: $(basename "${FP:-?}")" ;; Grep) P="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.pattern // empty' 2>/dev/null | head -c 30)" LINE="🔍 Grep: \"$P\"" ;; Glob) P="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.pattern // empty' 2>/dev/null | head -c 50)" LINE="🌐 Glob: $P" ;; WebFetch) U="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.url // empty' 2>/dev/null | head -c 60)" LINE="🌍 Fetch: $U" ;; WebSearch) Q="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.query // empty' 2>/dev/null | head -c 60)" LINE="🔎 Search: $Q" ;; Task) D="$(printf '%s' "$INPUT_JSON" | jq -r '.tool_input.description // empty' 2>/dev/null | head -c 60)" LINE="🎯 Task: $D" ;; *) LINE="🔧 $TOOL" ;; esac [[ -z "$LINE" ]] && exit 0 NOW="$(date +%s%N | cut -c1-13)" # Append + bump LAST под flock'ом — конкурентные hook'и не теряют строки. ( flock 9 echo "$LINE" >> "$BUF" echo "$NOW" > "$LAST" ) 9>"$LOCK" # Дебаунс-flusher в фоне. Каждый hook спавнит свой sleep, но только # тот, чей NOW совпал с финальным LAST после задержки, реально шлёт — # остальные тихо выходят. ( sleep "$DEBOUNCE_SEC" ( flock 9 LAST_TS="$(cat "$LAST" 2>/dev/null || echo 0)" if [[ "$LAST_TS" != "$NOW" ]]; then # Пришёл более свежий tool — он заfflushит сам. exit 0 fi [[ -s "$BUF" ]] || exit 0 # Если буфер длиннее MAX_LINES — режем хвост (свежие строки важнее). BODY="$(tail -n "$MAX_LINES" "$BUF")" : > "$BUF" curl -fsS -m 10 -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \ --data-urlencode "chat_id=${CHAT_ID}" \ --data-urlencode "text=${BODY}" \ --data-urlencode "disable_notification=true" \ --data-urlencode "disable_web_page_preview=true" \ >/dev/null 2>&1 || log "send failed" ) 9>"$LOCK" ) & # Не ждём фоновую задачу — Claude Code продолжает выполнение tool'а. disown 2>/dev/null || true exit 0