diff --git a/plugins/warp/scripts/on-post-tool-use.sh b/plugins/warp/scripts/on-post-tool-use.sh index 568e5b3..6a386b6 100755 --- a/plugins/warp/scripts/on-post-tool-use.sh +++ b/plugins/warp/scripts/on-post-tool-use.sh @@ -3,22 +3,26 @@ # Sends a structured Warp notification after a tool call completes, # transitioning the session status from Blocked back to Running. +[ -z "${WARP_CLI_AGENT_PROTOCOL_VERSION:-}" ] && exit 0 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/should-use-structured.sh" -# No legacy equivalent for this hook if ! should_use_structured; then exit 0 fi -source "$SCRIPT_DIR/build-payload.sh" - -# Read hook input from stdin -INPUT=$(cat) +PROTOCOL_VERSION="${WARP_CLI_AGENT_PROTOCOL_VERSION:-1}" +[[ "$PROTOCOL_VERSION" =~ ^[0-9]+$ ]] || PROTOCOL_VERSION=1 +[ "$PROTOCOL_VERSION" -gt 1 ] && PROTOCOL_VERSION=1 -TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) +BODY=$(jq -nc \ + --argjson v "$PROTOCOL_VERSION" \ + --arg agent "claude" \ + --arg event "tool_complete" \ + '{v:$v, agent:$agent, event:$event} + + (input | {session_id: (.session_id // ""), cwd: (.cwd // ""), project: ((.cwd // "") | sub("/+$"; "") | sub(".*/"; "")), tool_name: (.tool_name // "")})' 2>/dev/null) || exit 0 -BODY=$(build_payload "$INPUT" "tool_complete" \ - --arg tool_name "$TOOL_NAME") +[ -z "$BODY" ] && exit 0 -"$SCRIPT_DIR/warp-notify.sh" "warp://cli-agent" "$BODY" +printf '\033]777;notify;%s;%s\007' "warp://cli-agent" "$BODY" > /dev/tty 2>/dev/null & diff --git a/plugins/warp/tests/test-hooks.sh b/plugins/warp/tests/test-hooks.sh index 754bdd0..55006c9 100755 --- a/plugins/warp/tests/test-hooks.sh +++ b/plugins/warp/tests/test-hooks.sh @@ -287,6 +287,87 @@ for HOOK in on-permission-request.sh on-prompt-submit.sh on-post-tool-use.sh; do assert_eq "$HOOK exits 0 without protocol version" "0" "$?" done +# --- PostToolUse payload tests --- +# The optimized on-post-tool-use.sh inlines payload construction and writes +# to /dev/tty. We use `script` to capture pty output and extract the JSON. + +run_hook_capture() { + local hook="$1" + local input="$2" + local tmpfile inputfile + tmpfile=$(mktemp) + inputfile=$(mktemp) + printf '%s' "$input" > "$inputfile" + script -q "$tmpfile" bash -c "bash \"$HOOK_DIR/$hook\" < \"$inputfile\"; wait; sleep 0.2" >/dev/null 2>&1 + local payload + payload=$(tr -d '\r' < "$tmpfile" | grep -o '{[^}]*}' | tail -1) + rm -f "$tmpfile" "$inputfile" + echo "$payload" +} + +echo "" +echo "=== on-post-tool-use.sh (payload) ===" + +echo "" +echo "--- Basic payload construction ---" +export WARP_CLI_AGENT_PROTOCOL_VERSION=1 +export WARP_CLIENT_VERSION="v0.2026.04.29.08.57.preview_01" + +PAYLOAD=$(run_hook_capture "on-post-tool-use.sh" '{"tool_name":"Read","session_id":"sess-456","cwd":"/Users/alice/my-project"}') +assert_json_field "v is 1" "$PAYLOAD" ".v" "1" +assert_json_field "agent is claude" "$PAYLOAD" ".agent" "claude" +assert_json_field "event is tool_complete" "$PAYLOAD" ".event" "tool_complete" +assert_json_field "session_id extracted" "$PAYLOAD" ".session_id" "sess-456" +assert_json_field "cwd extracted" "$PAYLOAD" ".cwd" "/Users/alice/my-project" +assert_json_field "project is basename of cwd" "$PAYLOAD" ".project" "my-project" +assert_json_field "tool_name extracted" "$PAYLOAD" ".tool_name" "Read" + +echo "" +echo "--- Trailing slash in cwd ---" +PAYLOAD=$(run_hook_capture "on-post-tool-use.sh" '{"tool_name":"Read","session_id":"s1","cwd":"/Users/alice/project/"}') +assert_json_field "trailing slash stripped for project" "$PAYLOAD" ".project" "project" + +echo "" +echo "--- Missing fields produce empty strings ---" +PAYLOAD=$(run_hook_capture "on-post-tool-use.sh" '{"tool_name":"Bash"}') +assert_json_field "missing session_id is empty" "$PAYLOAD" ".session_id" "" +assert_json_field "missing cwd is empty" "$PAYLOAD" ".cwd" "" +assert_json_field "missing cwd gives empty project" "$PAYLOAD" ".project" "" +assert_json_field "tool_name still works" "$PAYLOAD" ".tool_name" "Bash" + +echo "" +echo "--- Protocol version negotiation (inline) ---" +export WARP_CLI_AGENT_PROTOCOL_VERSION=99 +PAYLOAD=$(run_hook_capture "on-post-tool-use.sh" '{"tool_name":"Edit","session_id":"s1","cwd":"/tmp"}') +assert_json_field "protocol capped to 1 when warp declares 99" "$PAYLOAD" ".v" "1" + +export WARP_CLI_AGENT_PROTOCOL_VERSION=1 +PAYLOAD=$(run_hook_capture "on-post-tool-use.sh" '{"tool_name":"Edit","session_id":"s1","cwd":"/tmp"}') +assert_json_field "protocol is 1 when warp declares 1" "$PAYLOAD" ".v" "1" + +echo "" +echo "--- Non-numeric protocol version falls back to 1 ---" +export WARP_CLI_AGENT_PROTOCOL_VERSION="garbage" +PAYLOAD=$(run_hook_capture "on-post-tool-use.sh" '{"tool_name":"Edit","session_id":"s1","cwd":"/tmp"}') +assert_json_field "non-numeric protocol falls back to 1" "$PAYLOAD" ".v" "1" + +echo "" +echo "--- Early exit without protocol version ---" +unset WARP_CLI_AGENT_PROTOCOL_VERSION +PAYLOAD=$(run_hook_capture "on-post-tool-use.sh" '{"tool_name":"Read","session_id":"s1","cwd":"/tmp"}') +assert_eq "no output when WARP_CLI_AGENT_PROTOCOL_VERSION unset" "" "$PAYLOAD" + +echo "" +echo "--- Early exit for broken Warp version ---" +export WARP_CLI_AGENT_PROTOCOL_VERSION=1 +export WARP_CLIENT_VERSION="v0.2026.03.25.08.24.stable_05" +PAYLOAD=$(run_hook_capture "on-post-tool-use.sh" '{"tool_name":"Read","session_id":"s1","cwd":"/tmp"}') +assert_eq "no output for broken stable version" "" "$PAYLOAD" + +# Clean up +unset WARP_CLI_AGENT_PROTOCOL_VERSION +unset WARP_CLIENT_VERSION + # --- Summary --- echo ""