Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/claude_agent_sdk/_internal/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions src/claude_agent_sdk/_internal/transport/subprocess_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down
1 change: 1 addition & 0 deletions src/claude_agent_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 17 additions & 9 deletions tests/test_subprocess_buffering.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
71 changes: 29 additions & 42 deletions tests/test_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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"
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -198,8 +198,7 @@ async def _test():

transport = SubprocessCLITransport(
prompt="test",
options=ClaudeAgentOptions(),
cli_path="/usr/bin/claude",
options=make_options(),
)

await transport.connect()
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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(
Expand Down Expand Up @@ -408,7 +397,6 @@ async def _test():
transport = SubprocessCLITransport(
prompt="test",
options=options,
cli_path="/usr/bin/claude",
)

await transport.connect()
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -469,7 +457,6 @@ async def _test():
transport = SubprocessCLITransport(
prompt="test",
options=options,
cli_path="/usr/bin/claude",
)

await transport.connect()
Expand Down
Loading