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>
This commit is contained in:
parent
a7fcc9f9e1
commit
92d2eb0432
124
deploy/telegram-bridge/cc-tg-notify-pretool
Executable file
124
deploy/telegram-bridge/cc-tg-notify-pretool
Executable file
|
|
@ -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
|
||||
Loading…
Reference in a new issue