Initial commit — tg-stream, tg-task, Claude Code hooks
This commit is contained in:
35
hooks/bash-heartbeat-post.sh
Executable file
35
hooks/bash-heartbeat-post.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse hook on the Bash tool — if the wrapped command took longer than
|
||||
# DURATION_FLOOR seconds, send a "done in Ns · <label>" completion ping so the
|
||||
# user always knows when the long thing finished.
|
||||
#
|
||||
# Pairs with bash-heartbeat-pre.sh which writes /tmp/.bash-hb-<PPID>.
|
||||
#
|
||||
# Input: JSON {"tool_name":"Bash","tool_input":{...},"tool_response":{...}}
|
||||
# Output: always exit 0.
|
||||
|
||||
set -u
|
||||
INPUT=$(cat) >/dev/null
|
||||
|
||||
DURATION_FLOOR=5
|
||||
STATE_FILE="/tmp/.bash-hb-$PPID"
|
||||
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
START=$(cut -d'|' -f1 "$STATE_FILE")
|
||||
LABEL=$(cut -d'|' -f2- "$STATE_FILE")
|
||||
rm -f "$STATE_FILE"
|
||||
|
||||
NOW=$(date +%s)
|
||||
ELAPSED=$((NOW - START))
|
||||
|
||||
if (( ELAPSED < DURATION_FLOOR )); then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fire the completion ping (no-stream so it lands instantly)
|
||||
/host/root/openclaw/tg-stream --no-stream "✅ done in ${ELAPSED}s · $LABEL" >/dev/null 2>&1 &
|
||||
|
||||
exit 0
|
||||
57
hooks/bash-heartbeat-pre.sh
Executable file
57
hooks/bash-heartbeat-pre.sh
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse hook on the Bash tool — fire a Telegram heartbeat *before* the
|
||||
# command runs, so the user sees "I'm doing X" within ~1 second of any Bash
|
||||
# call, no matter what command Claude is running.
|
||||
#
|
||||
# Also records the start time at /tmp/.bash-hb-<pid> so the matching
|
||||
# PostToolUse hook can compute duration and decide whether to send a
|
||||
# completion ping.
|
||||
#
|
||||
# Skips heartbeats for certain noisy / tg-stream-itself commands so we don't
|
||||
# loop. Skips when the description doesn't carry useful info.
|
||||
#
|
||||
# Input: JSON {"tool_name":"Bash","tool_input":{"command":"...","description":"..."}}
|
||||
# Output: always exit 0 (advisory hook, never blocks).
|
||||
|
||||
set -u
|
||||
INPUT=$(cat)
|
||||
|
||||
PARSED=$(node -e '
|
||||
let raw = "";
|
||||
process.stdin.on("data", c => raw += c);
|
||||
process.stdin.on("end", () => {
|
||||
try {
|
||||
const j = JSON.parse(raw);
|
||||
const cmd = (j.tool_input && j.tool_input.command) || "";
|
||||
const desc = (j.tool_input && j.tool_input.description) || "";
|
||||
// Skip if this Bash call is itself a Telegram stream/task (avoid loops)
|
||||
if (/tg-stream|tg-task|telegram\.org\/bot/.test(cmd)) {
|
||||
console.log("SKIP");
|
||||
return;
|
||||
}
|
||||
// Skip trivial reads (ls, cat, echo on their own)
|
||||
if (/^(ls|pwd|whoami|date|echo)( |$)/.test(cmd.trim())) {
|
||||
console.log("SKIP");
|
||||
return;
|
||||
}
|
||||
console.log("FIRE\t" + (desc || cmd.slice(0, 80)).replace(/\t/g, " "));
|
||||
} catch (e) {
|
||||
console.log("SKIP");
|
||||
}
|
||||
});
|
||||
' <<<"$INPUT")
|
||||
|
||||
ACTION=$(echo "$PARSED" | cut -f1)
|
||||
LABEL=$(echo "$PARSED" | cut -f2-)
|
||||
|
||||
if [[ "$ACTION" != "FIRE" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Record start time keyed by parent shell PID so PostToolUse can pair it
|
||||
echo "$(date +%s)|$LABEL" > "/tmp/.bash-hb-$PPID" 2>/dev/null || true
|
||||
|
||||
# Fire the heartbeat (no-stream so it lands instantly without typing animation)
|
||||
/host/root/openclaw/tg-stream --no-stream "🔧 $LABEL" >/dev/null 2>&1 &
|
||||
|
||||
exit 0
|
||||
61
hooks/redirect-telegram-reply.sh
Executable file
61
hooks/redirect-telegram-reply.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse hook for Claude Code — redirect plain telegram reply tool calls
|
||||
# through tg-stream so streaming behavior is enforced infrastructurally.
|
||||
#
|
||||
# Install in ~/.claude/settings.json under hooks.PreToolUse, matching tools:
|
||||
# mcp__plugin_telegram_telegram__reply
|
||||
#
|
||||
# Behavior:
|
||||
# - If the call has no `files` attachment, BLOCK it and emit a stderr
|
||||
# message telling Claude to use /host/root/openclaw/tg-stream instead.
|
||||
# - If the call HAS `files`, PASS THROUGH (tg-stream doesn't do attachments).
|
||||
#
|
||||
# Input: JSON on stdin {"tool_name":"...", "tool_input": {...}}
|
||||
# Output: exit 0 = allow, exit 2 = block (stderr surfaced to Claude).
|
||||
#
|
||||
# Uses node (always present in this container) instead of jq (not installed).
|
||||
|
||||
set -u
|
||||
INPUT=$(cat)
|
||||
|
||||
DECISION=$(node -e '
|
||||
let raw = "";
|
||||
process.stdin.on("data", c => raw += c);
|
||||
process.stdin.on("end", () => {
|
||||
try {
|
||||
const j = JSON.parse(raw);
|
||||
const tool = j.tool_name || "";
|
||||
if (tool !== "mcp__plugin_telegram_telegram__reply") { console.log("ALLOW"); return; }
|
||||
const files = (j.tool_input && j.tool_input.files) || [];
|
||||
if (Array.isArray(files) && files.length > 0) { console.log("ALLOW"); return; }
|
||||
console.log("BLOCK");
|
||||
} catch (e) {
|
||||
console.log("ALLOW"); // fail open on parse error
|
||||
}
|
||||
});
|
||||
' <<<"$INPUT")
|
||||
|
||||
if [[ "$DECISION" != "BLOCK" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat >&2 <<'EOF'
|
||||
BLOCKED by /host/root/openclaw/hooks/redirect-telegram-reply.sh
|
||||
|
||||
Plain telegram reply is disabled for substantive messages. Use the streaming
|
||||
tool instead so the user gets a typing animation rather than a wall of text:
|
||||
|
||||
/host/root/openclaw/tg-stream "your message"
|
||||
/host/root/openclaw/tg-stream --header "🔧 working" "body text"
|
||||
/host/root/openclaw/tg-stream --no-stream "short ack"
|
||||
echo "from stdin" | /host/root/openclaw/tg-stream
|
||||
|
||||
For long-running tasks, wrap them so heartbeats are sent automatically:
|
||||
|
||||
/host/root/openclaw/tg-task "label" -- <command...>
|
||||
|
||||
If you genuinely need to send a file attachment (image/document), call the
|
||||
mcp__plugin_telegram_telegram__reply tool again with the `files` parameter
|
||||
set — the hook passes attachment-bearing calls through.
|
||||
EOF
|
||||
exit 2
|
||||
Reference in New Issue
Block a user