food-market/deploy/telegram-bridge/cc-tg-notify-pretool
nns 92d2eb0432 feat(infra): PreToolUse hook for Telegram progress feed + rate-limited batching
Hook /usr/local/bin/cc-tg-notify-pretool читает JSON через stdin
(tool_name + tool_input) и шлёт короткую строку прогресса в Telegram
перед каждым tool-вызовом — чтобы юзер видел «он работает» а не
просто ждал финального ответа от Stop hook.

Форматы (по требованию ТЗ):
- Bash       → 🔨 ${description} либо 🔨 Bash: ${command[:80]}
- Edit/Write/Read → ✏️/📝/📖 + basename(file_path)
- Grep/Glob  → 🔍/🌐 + pattern[:30/50]
- WebFetch/Search → 🌍/🔎 + url|query[:60]
- Task       → 🎯 + description[:60]
- TodoWrite  → skip (шумно)
- прочие     → 🔧 ${tool_name}

Дебаунс через flock + фоновый sleep:
- Каждый вызов append'ит строку в /tmp/cc-tg-pretool-buffer.txt и
  бампит /tmp/cc-tg-pretool-last (epoch ms) под flock'ом.
- Спавнит фоновый sleep 1.5s; после пробуждения проверяет,
  чей timestamp в LAST — если не его, выходит молча. Только
  «последний» hook реально шлёт батч одним сообщением.
- Это даёт пачке tool-вызовов один Telegram-апдейт, а не 5–10.
- Если буфер длиннее 20 строк — режется хвостом (свежие важнее).

Off-switch: touch /tmp/cc-tg-quiet — pretool-hook сразу выходит.
Stop hook продолжает работать.

Hook регистрируется в ~/.claude/settings.json под PreToolUse с
matcher="" (все tools без фильтра); фильтр TodoWrite — внутри
скрипта.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:12:11 +05:00

125 lines
4.6 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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