Add minimum Claude Code version check (2.0.0+) (#206)

- Add version check in subprocess transport that runs `claude -v` on
connect
- Display warning to stderr if version is below 2.0.0
- Update README prerequisites to specify Claude Code 2.0.0+
- Version check is non-blocking (warns but continues execution)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Ashwin Bhat
2025-10-04 21:24:38 -07:00
committed by GitHub
parent 2a9693e258
commit 70358589cf
3 changed files with 98 additions and 14 deletions

View File

@@ -11,7 +11,7 @@ pip install claude-agent-sdk
**Prerequisites:**
- Python 3.10+
- Node.js
- Claude Code: `npm install -g @anthropic-ai/claude-code`
- Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
## Quick Start

View File

@@ -3,7 +3,9 @@
import json
import logging
import os
import re
import shutil
import sys
from collections.abc import AsyncIterable, AsyncIterator
from contextlib import suppress
from dataclasses import asdict
@@ -25,6 +27,7 @@ from . import Transport
logger = logging.getLogger(__name__)
_DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
MINIMUM_CLAUDE_CODE_VERSION = "2.0.0"
class SubprocessCLITransport(Transport):
@@ -202,6 +205,8 @@ class SubprocessCLITransport(Transport):
if self._process:
return
await self._check_claude_version()
cmd = self._build_command()
try:
# Merge environment variables: system -> user -> SDK required
@@ -448,6 +453,46 @@ class SubprocessCLITransport(Transport):
)
raise self._exit_error
async def _check_claude_version(self) -> None:
"""Check Claude Code version and warn if below minimum."""
version_process = None
try:
with anyio.fail_after(2): # 2 second timeout
version_process = await anyio.open_process(
[self._cli_path, "-v"],
stdout=PIPE,
stderr=PIPE,
)
if version_process.stdout:
stdout_bytes = await version_process.stdout.receive()
version_output = stdout_bytes.decode().strip()
match = re.match(r"([0-9]+\.[0-9]+\.[0-9]+)", version_output)
if match:
version = match.group(1)
version_parts = [int(x) for x in version.split(".")]
min_parts = [
int(x) for x in MINIMUM_CLAUDE_CODE_VERSION.split(".")
]
if version_parts < min_parts:
warning = (
f"Warning: Claude Code version {version} is unsupported in the Agent SDK. "
f"Minimum required version is {MINIMUM_CLAUDE_CODE_VERSION}. "
"Some features may not work correctly."
)
logger.warning(warning)
print(warning, file=sys.stderr)
except Exception:
pass
finally:
if version_process:
with suppress(Exception):
version_process.terminate()
with suppress(Exception):
await version_process.wait()
def is_ready(self) -> bool:
"""Check if transport is ready for communication."""
return self._ready

View File

@@ -163,6 +163,16 @@ class TestSubprocessCLITransport:
async def _test():
with patch("anyio.open_process") as mock_exec:
# Mock version check process
mock_version_process = MagicMock()
mock_version_process.stdout = MagicMock()
mock_version_process.stdout.receive = AsyncMock(
return_value=b"2.0.0 (Claude Code)"
)
mock_version_process.terminate = MagicMock()
mock_version_process.wait = AsyncMock()
# Mock main process
mock_process = MagicMock()
mock_process.returncode = None
mock_process.terminate = MagicMock()
@@ -175,7 +185,8 @@ class TestSubprocessCLITransport:
mock_stdin.aclose = AsyncMock()
mock_process.stdin = mock_stdin
mock_exec.return_value = mock_process
# Return version process first, then main process
mock_exec.side_effect = [mock_version_process, mock_process]
transport = SubprocessCLITransport(
prompt="test",
@@ -363,13 +374,25 @@ class TestSubprocessCLITransport:
with patch(
"anyio.open_process", new_callable=AsyncMock
) as mock_open_process:
# Mock version check process
mock_version_process = MagicMock()
mock_version_process.stdout = MagicMock()
mock_version_process.stdout.receive = AsyncMock(
return_value=b"2.0.0 (Claude Code)"
)
mock_version_process.terminate = MagicMock()
mock_version_process.wait = AsyncMock()
# Mock main process
mock_process = MagicMock()
mock_process.stdout = MagicMock()
mock_stdin = MagicMock()
mock_stdin.aclose = AsyncMock() # Add async aclose method
mock_process.stdin = mock_stdin
mock_process.returncode = None
mock_open_process.return_value = mock_process
# Return version process first, then main process
mock_open_process.side_effect = [mock_version_process, mock_process]
transport = SubprocessCLITransport(
prompt="test",
@@ -379,11 +402,13 @@ class TestSubprocessCLITransport:
await transport.connect()
# Verify open_process was called with correct env vars
mock_open_process.assert_called_once()
call_kwargs = mock_open_process.call_args.kwargs
assert "env" in call_kwargs
env_passed = call_kwargs["env"]
# Verify open_process was called twice (version check + main process)
assert mock_open_process.call_count == 2
# Check the second call (main process) for env vars
second_call_kwargs = mock_open_process.call_args_list[1].kwargs
assert "env" in second_call_kwargs
env_passed = second_call_kwargs["env"]
# Check that custom env var was passed
assert env_passed["MY_TEST_VAR"] == test_value
@@ -410,13 +435,25 @@ class TestSubprocessCLITransport:
with patch(
"anyio.open_process", new_callable=AsyncMock
) as mock_open_process:
# Mock version check process
mock_version_process = MagicMock()
mock_version_process.stdout = MagicMock()
mock_version_process.stdout.receive = AsyncMock(
return_value=b"2.0.0 (Claude Code)"
)
mock_version_process.terminate = MagicMock()
mock_version_process.wait = AsyncMock()
# Mock main process
mock_process = MagicMock()
mock_process.stdout = MagicMock()
mock_stdin = MagicMock()
mock_stdin.aclose = AsyncMock() # Add async aclose method
mock_process.stdin = mock_stdin
mock_process.returncode = None
mock_open_process.return_value = mock_process
# Return version process first, then main process
mock_open_process.side_effect = [mock_version_process, mock_process]
transport = SubprocessCLITransport(
prompt="test",
@@ -426,11 +463,13 @@ class TestSubprocessCLITransport:
await transport.connect()
# Verify open_process was called with correct user
mock_open_process.assert_called_once()
call_kwargs = mock_open_process.call_args.kwargs
assert "user" in call_kwargs
user_passed = call_kwargs["user"]
# Verify open_process was called twice (version check + main process)
assert mock_open_process.call_count == 2
# Check the second call (main process) for user
second_call_kwargs = mock_open_process.call_args_list[1].kwargs
assert "user" in second_call_kwargs
user_passed = second_call_kwargs["user"]
# Check that user was passed
assert user_passed == "claude"