From d71c967fa12681909e98ff238cb1d1c87829e0c2 Mon Sep 17 00:00:00 2001 From: Dickson Tsai Date: Fri, 5 Sep 2025 11:10:32 +0900 Subject: [PATCH] Adopt exit_error pattern --- .../_internal/transport/subprocess_cli.py | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/claude_code_sdk/_internal/transport/subprocess_cli.py b/src/claude_code_sdk/_internal/transport/subprocess_cli.py index d3c8a14..0451868 100644 --- a/src/claude_code_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_code_sdk/_internal/transport/subprocess_cli.py @@ -44,6 +44,7 @@ class SubprocessCLITransport(Transport): self._stdin_stream: TextSendStream | None = None self._stderr_file: Any = None # tempfile.NamedTemporaryFile self._ready = False + self._exit_error: Exception | None = None # Track process exit errors def _find_cli(self) -> str: """Find Claude Code CLI binary.""" @@ -211,12 +212,16 @@ class SubprocessCLITransport(Transport): except FileNotFoundError as e: # Check if the error comes from the working directory or the CLI if self._cwd and not Path(self._cwd).exists(): - raise CLIConnectionError( - f"Working directory does not exist: {self._cwd}" - ) from e - raise CLINotFoundError(f"Claude Code not found at: {self._cli_path}") from e + error = CLIConnectionError(f"Working directory does not exist: {self._cwd}") + self._exit_error = error + raise error from e + error = CLINotFoundError(f"Claude Code not found at: {self._cli_path}") + self._exit_error = error + raise error from e except Exception as e: - raise CLIConnectionError(f"Failed to start Claude Code: {e}") from e + error = CLIConnectionError(f"Failed to start Claude Code: {e}") + self._exit_error = error + raise error from e async def close(self) -> None: """Close the transport and clean up resources.""" @@ -257,13 +262,28 @@ class SubprocessCLITransport(Transport): self._stdout_stream = None self._stderr_stream = None self._stdin_stream = None + self._exit_error = None async def write(self, data: str) -> None: """Write raw data to the transport.""" - if not self._stdin_stream: - raise CLIConnectionError("Cannot write: stdin not available") + # Check if ready (like TypeScript) + if not self._ready or not self._stdin_stream: + raise CLIConnectionError("ProcessTransport is not ready for writing") - await self._stdin_stream.send(data) + # Check if process is still alive (like TypeScript) + if self._process and self._process.returncode is not None: + raise CLIConnectionError(f"Cannot write to terminated process (exit code: {self._process.returncode})") + + # Check for exit errors (like TypeScript) + if self._exit_error: + raise CLIConnectionError(f"Cannot write to process that exited with error: {self._exit_error}") from self._exit_error + + try: + await self._stdin_stream.send(data) + except Exception as e: + self._ready = False # Mark as not ready (like TypeScript) + self._exit_error = CLIConnectionError(f"Failed to write to process stdin: {e}") + raise self._exit_error from e async def end_input(self) -> None: """End the input stream (close stdin).""" @@ -333,11 +353,12 @@ class SubprocessCLITransport(Transport): # Use exit code for error detection, not string matching if returncode is not None and returncode != 0: - raise ProcessError( + self._exit_error = ProcessError( f"Command failed with exit code {returncode}", exit_code=returncode, stderr=stderr_output, ) + raise self._exit_error elif stderr_output: # Log stderr for debugging but don't fail on non-zero exit logger.debug(f"Process stderr: {stderr_output}")