ScuttleBot
feat: codex Stop hook for mirroring agent responses to IRC Codex CLI's Stop hook fires after each conversation turn and includes last_assistant_message. The new scuttlebot-stop.sh posts the response to all relay channels, matching the gemini AfterAgent behavior. Also updated hooks.json to register the Stop hook alongside existing pre-tool-use and post-tool-use hooks.
Commit
606670ed8028ba37b08730343b86bbe94ecdfa6a6881bc5c018fc2adb8170faf
Parent
4e1ede94a29bee6…
1 file changed
+81
| --- a/skills/openai-relay/hooks/scuttlebot-stop.sh | ||
| +++ b/skills/openai-relay/hooks/scuttlebot-stop.sh | ||
| @@ -0,0 +1,81 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# Stop hook for Codex agents. Posts final assistant reply to scuttlebot IRC. | |
| 3 | + | |
| 4 | +SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}" | |
| 5 | +if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then | |
| 6 | + set -a | |
| 7 | + . "$SCUTTLEBOT_CONFIG_FILE" | |
| 8 | + set +a | |
| 9 | +fi | |
| 10 | +if [ -n "${SCUTTLEBOT_CHANNEL_STATE_FILE:-}" ] && [ -f "$SCUTTLEBOT_CHANNEL_STATE_FILE" ]; then | |
| 11 | + set -a | |
| 12 | + . "$SCUTTLEBOT_CHANNEL_STATE_FILE" | |
| 13 | + set +a | |
| 14 | +fi | |
| 15 | + | |
| 16 | +SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" | |
| 17 | +SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}" | |
| 18 | +SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}" | |
| 19 | +SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}" | |
| 20 | + | |
| 21 | +normalize_channel() { | |
| 22 | + local channel="$1" | |
| 23 | + channel="${channel//[$' \t\r\n']/}" | |
| 24 | + channel="${channel#\#}" | |
| 25 | + printf '%s' "$channel" | |
| 26 | +} | |
| 27 | + | |
| 28 | +relay_channels() { | |
| 29 | + local raw="${SCUTTLEBOT_CHANNELS:-$SCUTTLEBOT_CHANNEL}" | |
| 30 | + local IFS=',' | |
| 31 | + local item channel seen="" | |
| 32 | + read -r -a items <<< "$raw" | |
| 33 | + for item in "${items[@]}"; do | |
| 34 | + channel=$(normalize_channel "$item") | |
| 35 | + [ -n "$channel" ] || continue | |
| 36 | + case ",$seen," in | |
| 37 | + *,"$channel",*) ;; | |
| 38 | + *) | |
| 39 | + seen="${seen:+$seen,}$channel" | |
| 40 | + printf '%s\n' "$channel" | |
| 41 | + ;; | |
| 42 | + esac | |
| 43 | + done | |
| 44 | +} | |
| 45 | + | |
| 46 | +sanitize() { | |
| 47 | + printf '%s' "$1" | tr -cs '[:alnum:]_-' '-' | |
| 48 | +} | |
| 49 | + | |
| 50 | +input=$(cat) | |
| 51 | + | |
| 52 | +cwd=$(echo "$input" | jq -r '.cwd // empty') | |
| 53 | +[ -z "$cwd" ] && cwd=$(pwd) | |
| 54 | +base_name=$(sanitize "$(basename "$cwd")") | |
| 55 | +session_suffix=$(echo "$input" | jq -r '.session_id // empty' | head -c 8) | |
| 56 | +[ -z "$session_suffix" ] && session_suffix=$PPID | |
| 57 | +default_nick="codex-${base_name}-$(sanitize "$session_suffix")" | |
| 58 | +SCUTTLEBOT_NICK="${SCUTTLEBOT_NICK:-$default_nick}" | |
| 59 | + | |
| 60 | +[ "$SCUTTLEBOT_HOOKS_ENABLED" = "0" ] && exit 0 | |
| 61 | +[ "$SCUTTLEBOT_HOOKS_ENABLED" = "false" ] && exit 0 | |
| 62 | +[ -z "$SCUTTLEBOT_TOKEN" ] && exit 0 | |
| 63 | + | |
| 64 | +response=$(echo "$input" | jq -r '.last_assistant_message // empty') | |
| 65 | +[ -z "$response" ] && exit 0 | |
| 66 | + | |
| 67 | +# Truncate long responses. | |
| 68 | +response=$(printf '%s' "$response" | head -c 360) | |
| 69 | + | |
| 70 | +payload="{\"text\": $(printf '%s' "$response" | jq -Rs .), \"nick\": \"$SCUTTLEBOT_NICK\"}" | |
| 71 | +for channel in $(relay_channels); do | |
| 72 | + curl -sf -X POST "$SCUTTLEBOT_URL/v1/channels/$channel/messages" \ | |
| 73 | + --connect-timeout 1 \ | |
| 74 | + --max-time 2 \ | |
| 75 | + -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ | |
| 76 | + -H "Content-Type: application/json" \ | |
| 77 | + -d "$payload" \ | |
| 78 | + > /dev/null || true | |
| 79 | +done | |
| 80 | + | |
| 81 | +exit 0 |
| --- a/skills/openai-relay/hooks/scuttlebot-stop.sh | |
| +++ b/skills/openai-relay/hooks/scuttlebot-stop.sh | |
| @@ -0,0 +1,81 @@ | |
| --- a/skills/openai-relay/hooks/scuttlebot-stop.sh | |
| +++ b/skills/openai-relay/hooks/scuttlebot-stop.sh | |
| @@ -0,0 +1,81 @@ | |
| 1 | #!/bin/bash |
| 2 | # Stop hook for Codex agents. Posts final assistant reply to scuttlebot IRC. |
| 3 | |
| 4 | SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}" |
| 5 | if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then |
| 6 | set -a |
| 7 | . "$SCUTTLEBOT_CONFIG_FILE" |
| 8 | set +a |
| 9 | fi |
| 10 | if [ -n "${SCUTTLEBOT_CHANNEL_STATE_FILE:-}" ] && [ -f "$SCUTTLEBOT_CHANNEL_STATE_FILE" ]; then |
| 11 | set -a |
| 12 | . "$SCUTTLEBOT_CHANNEL_STATE_FILE" |
| 13 | set +a |
| 14 | fi |
| 15 | |
| 16 | SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" |
| 17 | SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}" |
| 18 | SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}" |
| 19 | SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}" |
| 20 | |
| 21 | normalize_channel() { |
| 22 | local channel="$1" |
| 23 | channel="${channel//[$' \t\r\n']/}" |
| 24 | channel="${channel#\#}" |
| 25 | printf '%s' "$channel" |
| 26 | } |
| 27 | |
| 28 | relay_channels() { |
| 29 | local raw="${SCUTTLEBOT_CHANNELS:-$SCUTTLEBOT_CHANNEL}" |
| 30 | local IFS=',' |
| 31 | local item channel seen="" |
| 32 | read -r -a items <<< "$raw" |
| 33 | for item in "${items[@]}"; do |
| 34 | channel=$(normalize_channel "$item") |
| 35 | [ -n "$channel" ] || continue |
| 36 | case ",$seen," in |
| 37 | *,"$channel",*) ;; |
| 38 | *) |
| 39 | seen="${seen:+$seen,}$channel" |
| 40 | printf '%s\n' "$channel" |
| 41 | ;; |
| 42 | esac |
| 43 | done |
| 44 | } |
| 45 | |
| 46 | sanitize() { |
| 47 | printf '%s' "$1" | tr -cs '[:alnum:]_-' '-' |
| 48 | } |
| 49 | |
| 50 | input=$(cat) |
| 51 | |
| 52 | cwd=$(echo "$input" | jq -r '.cwd // empty') |
| 53 | [ -z "$cwd" ] && cwd=$(pwd) |
| 54 | base_name=$(sanitize "$(basename "$cwd")") |
| 55 | session_suffix=$(echo "$input" | jq -r '.session_id // empty' | head -c 8) |
| 56 | [ -z "$session_suffix" ] && session_suffix=$PPID |
| 57 | default_nick="codex-${base_name}-$(sanitize "$session_suffix")" |
| 58 | SCUTTLEBOT_NICK="${SCUTTLEBOT_NICK:-$default_nick}" |
| 59 | |
| 60 | [ "$SCUTTLEBOT_HOOKS_ENABLED" = "0" ] && exit 0 |
| 61 | [ "$SCUTTLEBOT_HOOKS_ENABLED" = "false" ] && exit 0 |
| 62 | [ -z "$SCUTTLEBOT_TOKEN" ] && exit 0 |
| 63 | |
| 64 | response=$(echo "$input" | jq -r '.last_assistant_message // empty') |
| 65 | [ -z "$response" ] && exit 0 |
| 66 | |
| 67 | # Truncate long responses. |
| 68 | response=$(printf '%s' "$response" | head -c 360) |
| 69 | |
| 70 | payload="{\"text\": $(printf '%s' "$response" | jq -Rs .), \"nick\": \"$SCUTTLEBOT_NICK\"}" |
| 71 | for channel in $(relay_channels); do |
| 72 | curl -sf -X POST "$SCUTTLEBOT_URL/v1/channels/$channel/messages" \ |
| 73 | --connect-timeout 1 \ |
| 74 | --max-time 2 \ |
| 75 | -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ |
| 76 | -H "Content-Type: application/json" \ |
| 77 | -d "$payload" \ |
| 78 | > /dev/null || true |
| 79 | done |
| 80 | |
| 81 | exit 0 |