From 6b11de7443c07949de4b0d98c5e2164a5bc8a908 Mon Sep 17 00:00:00 2001 From: Tim Kaufmann Date: Fri, 22 Aug 2025 01:20:35 +0200 Subject: [PATCH] Add blockAndSend() and approveAndSend() methods for immediate response handling These methods combine decision-making with sending, eliminating the need for extended classes when replacing tool output. This is particularly useful for middleware hooks that intercept and replace tool functionality. - blockAndSend(): Block tool execution and immediately send response - approveAndSend(): Approve tool execution and immediately send response - Added tests for both new methods Use case: When building bridges (e.g., routing Claude's WebSearch to Perplexity), these methods allow clean interception without needing custom hook classes. --- src/Hooks/Response.php | 27 ++++++++++++ tests/Hooks/ExtendedResponseTest.php | 61 ++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 tests/Hooks/ExtendedResponseTest.php diff --git a/src/Hooks/Response.php b/src/Hooks/Response.php index 9df8d01..3ed7b26 100644 --- a/src/Hooks/Response.php +++ b/src/Hooks/Response.php @@ -38,6 +38,19 @@ public function block(string $reason): self return $this; } + + /** + * Block the tool call and immediately send the response + * Useful for replacing tool output entirely + * + * @param string $reason The reason/content to provide instead of tool execution + */ + public function blockAndSend(string $reason): never + { + $this->data['decision'] = 'block'; + $this->data['reason'] = $reason; + $this->send(); + } /** * Approve the tool call (PreToolUse only) @@ -51,6 +64,20 @@ public function approve(string $reason = ''): self return $this; } + + /** + * Approve the tool call and immediately send the response + * + * @param string $reason Optional approval reason + */ + public function approveAndSend(string $reason = ''): never + { + $this->data['decision'] = 'approve'; + if ($reason) { + $this->data['reason'] = $reason; + } + $this->send(); + } /** * Suppress output from transcript mode diff --git a/tests/Hooks/ExtendedResponseTest.php b/tests/Hooks/ExtendedResponseTest.php new file mode 100644 index 0000000..8b80c4f --- /dev/null +++ b/tests/Hooks/ExtendedResponseTest.php @@ -0,0 +1,61 @@ +blockAndSend('Tool execution blocked'); + } catch (SystemExit $e) { + // Expected + } + $output = ob_get_clean(); + + $data = json_decode($output, true); + expect($data)->toBeArray(); + expect($data['decision'])->toBe('block'); + expect($data['reason'])->toBe('Tool execution blocked'); + } + }; + + $response->testBlockAndSend(); +}); + +it('can approve and send immediately', function () { + $response = new class extends Response { + public function testApproveAndSend(): void { + ob_start(); + try { + $this->approveAndSend('Tool execution approved'); + } catch (SystemExit $e) { + // Expected + } + $output = ob_get_clean(); + + $data = json_decode($output, true); + expect($data)->toBeArray(); + expect($data['decision'])->toBe('approve'); + expect($data['reason'])->toBe('Tool execution approved'); + } + }; + + $response->testApproveAndSend(); +}); + +it('blockAndSend terminates execution', function () { + $response = new Response(); + + expect(function () use ($response) { + $response->blockAndSend('Blocked'); + })->toThrow(SystemExit::class); +}); + +it('approveAndSend terminates execution', function () { + $response = new Response(); + + expect(function () use ($response) { + $response->approveAndSend('Approved'); + })->toThrow(SystemExit::class); +}); \ No newline at end of file