diff --git a/deploy/telegram-bridge/cc-tg-notify-pretool b/deploy/telegram-bridge/cc-tg-notify-pretool new file mode 100755 index 0000000..b279a69 --- /dev/null +++ b/deploy/telegram-bridge/cc-tg-notify-pretool @@ -0,0 +1,124 @@ +#!/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