diff --git a/src/claude_agent_sdk/_internal/client.py b/src/claude_agent_sdk/_internal/client.py index dbb6d194..6dbc8776 100644 --- a/src/claude_agent_sdk/_internal/client.py +++ b/src/claude_agent_sdk/_internal/client.py @@ -71,7 +71,8 @@ async def process_query( chosen_transport = transport else: chosen_transport = SubprocessCLITransport( - prompt=prompt, options=configured_options + prompt=prompt, + options=configured_options, ) # Connect transport diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index bd9fc59d..ecea3ff4 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -37,12 +37,15 @@ def __init__( self, prompt: str | AsyncIterable[dict[str, Any]], options: ClaudeAgentOptions, - cli_path: str | Path | None = None, ): self._prompt = prompt self._is_streaming = not isinstance(prompt, str) self._options = options - self._cli_path = str(cli_path) if cli_path else self._find_cli() + self._cli_path = ( + str(options.cli_path) + if options.cli_path is not None + else self._find_cli() + ) self._cwd = str(options.cwd) if options.cwd else None self._process: Process | None = None self._stdout_stream: TextReceiveStream | None = None @@ -79,8 +82,8 @@ def _find_cli(self) -> str: " npm install -g @anthropic-ai/claude-code\n" "\nIf already installed locally, try:\n" ' export PATH="$HOME/node_modules/.bin:$PATH"\n' - "\nOr specify the path when creating transport:\n" - " SubprocessCLITransport(..., cli_path='/path/to/claude')" + "\nOr provide the path via ClaudeAgentOptions:\n" + " ClaudeAgentOptions(cli_path='/path/to/claude')" ) def _build_command(self) -> list[str]: diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index 82a57ad5..be1cb996 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -512,6 +512,7 @@ class ClaudeAgentOptions: model: str | None = None permission_prompt_tool_name: str | None = None cwd: str | Path | None = None + cli_path: str | Path | None = None settings: str | None = None add_dirs: list[str | Path] = field(default_factory=list) env: dict[str, str] = field(default_factory=dict) diff --git a/tests/test_subprocess_buffering.py b/tests/test_subprocess_buffering.py index 9437e028..4ff903dc 100644 --- a/tests/test_subprocess_buffering.py +++ b/tests/test_subprocess_buffering.py @@ -15,6 +15,15 @@ ) from claude_agent_sdk.types import ClaudeAgentOptions +DEFAULT_CLI_PATH = "/usr/bin/claude" + + +def make_options(**kwargs: object) -> ClaudeAgentOptions: + """Construct ClaudeAgentOptions with a default CLI path for tests.""" + + cli_path = kwargs.pop("cli_path", DEFAULT_CLI_PATH) + return ClaudeAgentOptions(cli_path=cli_path, **kwargs) + class MockTextReceiveStream: """Mock TextReceiveStream for testing.""" @@ -51,7 +60,7 @@ async def _test() -> None: buffered_line = json.dumps(json_obj1) + "\n" + json.dumps(json_obj2) transport = SubprocessCLITransport( - prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude" + prompt="test", options=make_options() ) mock_process = MagicMock() @@ -86,7 +95,7 @@ async def _test() -> None: buffered_line = json.dumps(json_obj1) + "\n" + json.dumps(json_obj2) transport = SubprocessCLITransport( - prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude" + prompt="test", options=make_options() ) mock_process = MagicMock() @@ -116,7 +125,7 @@ async def _test() -> None: buffered_line = json.dumps(json_obj1) + "\n\n\n" + json.dumps(json_obj2) transport = SubprocessCLITransport( - prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude" + prompt="test", options=make_options() ) mock_process = MagicMock() @@ -162,7 +171,7 @@ async def _test() -> None: part3 = complete_json[250:] transport = SubprocessCLITransport( - prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude" + prompt="test", options=make_options() ) mock_process = MagicMock() @@ -210,7 +219,7 @@ async def _test() -> None: ] transport = SubprocessCLITransport( - prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude" + prompt="test", options=make_options() ) mock_process = MagicMock() @@ -240,7 +249,7 @@ async def _test() -> None: huge_incomplete = '{"data": "' + "x" * (_DEFAULT_MAX_BUFFER_SIZE + 1000) transport = SubprocessCLITransport( - prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude" + prompt="test", options=make_options() ) mock_process = MagicMock() @@ -269,8 +278,7 @@ async def _test() -> None: transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions(max_buffer_size=custom_limit), - cli_path="/usr/bin/claude", + options=make_options(max_buffer_size=custom_limit), ) mock_process = MagicMock() @@ -310,7 +318,7 @@ async def _test() -> None: ] transport = SubprocessCLITransport( - prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude" + prompt="test", options=make_options() ) mock_process = MagicMock() diff --git a/tests/test_transport.py b/tests/test_transport.py index 93538f4a..8962d71c 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -10,6 +10,15 @@ from claude_agent_sdk._internal.transport.subprocess_cli import SubprocessCLITransport from claude_agent_sdk.types import ClaudeAgentOptions +DEFAULT_CLI_PATH = "/usr/bin/claude" + + +def make_options(**kwargs: object) -> ClaudeAgentOptions: + """Construct options using the standard CLI path unless overridden.""" + + cli_path = kwargs.pop("cli_path", DEFAULT_CLI_PATH) + return ClaudeAgentOptions(cli_path=cli_path, **kwargs) + class TestSubprocessCLITransport: """Test subprocess transport implementation.""" @@ -29,9 +38,7 @@ def test_find_cli_not_found(self): def test_build_command_basic(self): """Test building basic CLI command.""" - transport = SubprocessCLITransport( - prompt="Hello", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude" - ) + transport = SubprocessCLITransport(prompt="Hello", options=make_options()) cmd = transport._build_command() assert cmd[0] == "/usr/bin/claude" @@ -47,8 +54,7 @@ def test_cli_path_accepts_pathlib_path(self): path = Path("/usr/bin/claude") transport = SubprocessCLITransport( prompt="Hello", - options=ClaudeAgentOptions(), - cli_path=path, + options=ClaudeAgentOptions(cli_path=path), ) # Path object is converted to string, compare with str(path) @@ -58,10 +64,9 @@ def test_build_command_with_system_prompt_string(self): """Test building CLI command with system prompt as string.""" transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions( + options=make_options( system_prompt="Be helpful", ), - cli_path="/usr/bin/claude", ) cmd = transport._build_command() @@ -72,10 +77,9 @@ def test_build_command_with_system_prompt_preset(self): """Test building CLI command with system prompt preset.""" transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions( + options=make_options( system_prompt={"type": "preset", "preset": "claude_code"}, ), - cli_path="/usr/bin/claude", ) cmd = transport._build_command() @@ -86,14 +90,13 @@ def test_build_command_with_system_prompt_preset_and_append(self): """Test building CLI command with system prompt preset and append.""" transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions( + options=make_options( system_prompt={ "type": "preset", "preset": "claude_code", "append": "Be concise.", }, ), - cli_path="/usr/bin/claude", ) cmd = transport._build_command() @@ -105,14 +108,13 @@ def test_build_command_with_options(self): """Test building CLI command with options.""" transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions( + options=make_options( allowed_tools=["Read", "Write"], disallowed_tools=["Bash"], model="claude-sonnet-4-5", permission_mode="acceptEdits", max_turns=5, ), - cli_path="/usr/bin/claude", ) cmd = transport._build_command() @@ -135,8 +137,7 @@ def test_build_command_with_add_dirs(self): dir2 = Path("/path/to/dir2") transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions(add_dirs=[dir1, dir2]), - cli_path="/usr/bin/claude", + options=make_options(add_dirs=[dir1, dir2]), ) cmd = transport._build_command() @@ -155,10 +156,9 @@ def test_session_continuation(self): """Test session continuation options.""" transport = SubprocessCLITransport( prompt="Continue from before", - options=ClaudeAgentOptions( + options=make_options( continue_conversation=True, resume="session-123" ), - cli_path="/usr/bin/claude", ) cmd = transport._build_command() @@ -198,8 +198,7 @@ async def _test(): transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions(), - cli_path="/usr/bin/claude", + options=make_options(), ) await transport.connect() @@ -215,9 +214,7 @@ def test_read_messages(self): """Test reading messages from CLI output.""" # This test is simplified to just test the transport creation # The full async stream handling is tested in integration tests - transport = SubprocessCLITransport( - prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude" - ) + transport = SubprocessCLITransport(prompt="test", options=make_options()) # The transport now just provides raw message reading via read_messages() # So we just verify the transport can be created and basic structure is correct @@ -231,8 +228,7 @@ def test_connect_with_nonexistent_cwd(self): async def _test(): transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions(cwd="/this/directory/does/not/exist"), - cli_path="/usr/bin/claude", + options=make_options(cwd="/this/directory/does/not/exist"), ) with pytest.raises(CLIConnectionError) as exc_info: @@ -246,8 +242,7 @@ def test_build_command_with_settings_file(self): """Test building CLI command with settings as file path.""" transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions(settings="/path/to/settings.json"), - cli_path="/usr/bin/claude", + options=make_options(settings="/path/to/settings.json"), ) cmd = transport._build_command() @@ -259,8 +254,7 @@ def test_build_command_with_settings_json(self): settings_json = '{"permissions": {"allow": ["Bash(ls:*)"]}}' transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions(settings=settings_json), - cli_path="/usr/bin/claude", + options=make_options(settings=settings_json), ) cmd = transport._build_command() @@ -271,14 +265,13 @@ def test_build_command_with_extra_args(self): """Test building CLI command with extra_args for future flags.""" transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions( + options=make_options( extra_args={ "new-flag": "value", "boolean-flag": None, "another-option": "test-value", } ), - cli_path="/usr/bin/claude", ) cmd = transport._build_command() @@ -309,8 +302,7 @@ def test_build_command_with_mcp_servers(self): transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions(mcp_servers=mcp_servers), - cli_path="/usr/bin/claude", + options=make_options(mcp_servers=mcp_servers), ) cmd = transport._build_command() @@ -333,8 +325,7 @@ def test_build_command_with_mcp_servers_as_file_path(self): string_path = "/path/to/mcp-config.json" transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions(mcp_servers=string_path), - cli_path="/usr/bin/claude", + options=make_options(mcp_servers=string_path), ) cmd = transport._build_command() @@ -346,8 +337,7 @@ def test_build_command_with_mcp_servers_as_file_path(self): path_obj = Path("/path/to/mcp-config.json") transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions(mcp_servers=path_obj), - cli_path="/usr/bin/claude", + options=make_options(mcp_servers=path_obj), ) cmd = transport._build_command() @@ -361,8 +351,7 @@ def test_build_command_with_mcp_servers_as_json_string(self): json_config = '{"mcpServers": {"server": {"type": "stdio", "command": "test"}}}' transport = SubprocessCLITransport( prompt="test", - options=ClaudeAgentOptions(mcp_servers=json_config), - cli_path="/usr/bin/claude", + options=make_options(mcp_servers=json_config), ) cmd = transport._build_command() @@ -379,7 +368,7 @@ async def _test(): "MY_TEST_VAR": test_value, } - options = ClaudeAgentOptions(env=custom_env) + options = make_options(env=custom_env) # Mock the subprocess to capture the env argument with patch( @@ -408,7 +397,6 @@ async def _test(): transport = SubprocessCLITransport( prompt="test", options=options, - cli_path="/usr/bin/claude", ) await transport.connect() @@ -440,7 +428,7 @@ def test_connect_as_different_user(self): async def _test(): custom_user = "claude" - options = ClaudeAgentOptions(user=custom_user) + options = make_options(user=custom_user) # Mock the subprocess to capture the env argument with patch( @@ -469,7 +457,6 @@ async def _test(): transport = SubprocessCLITransport( prompt="test", options=options, - cli_path="/usr/bin/claude", ) await transport.connect()