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