claude-agent-sdk 0.0.23__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-agent-sdk might be problematic. Click here for more details.

Files changed (32) hide show
  1. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/PKG-INFO +1 -1
  2. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/pyproject.toml +1 -1
  3. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +11 -14
  4. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_version.py +1 -1
  5. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/types.py +1 -0
  6. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_integration.py +1 -1
  7. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_subprocess_buffering.py +30 -2
  8. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_transport.py +1 -1
  9. claude_agent_sdk-0.0.23/tests/mock_transport.py +0 -177
  10. claude_agent_sdk-0.0.23/tests/test_hooks.py +0 -148
  11. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/.gitignore +0 -0
  12. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/LICENSE +0 -0
  13. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/README.md +0 -0
  14. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/__init__.py +0 -0
  15. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_errors.py +0 -0
  16. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/__init__.py +0 -0
  17. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/client.py +0 -0
  18. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/message_parser.py +0 -0
  19. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/query.py +0 -0
  20. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/transport/__init__.py +0 -0
  21. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/client.py +0 -0
  22. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/py.typed +0 -0
  23. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/query.py +0 -0
  24. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/conftest.py +0 -0
  25. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_changelog.py +0 -0
  26. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_client.py +0 -0
  27. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_errors.py +0 -0
  28. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_message_parser.py +0 -0
  29. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_sdk_mcp_integration.py +0 -0
  30. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_streaming_client.py +0 -0
  31. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_tool_callbacks.py +0 -0
  32. {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-agent-sdk
3
- Version: 0.0.23
3
+ Version: 0.1.0
4
4
  Summary: Python SDK for Claude Code
5
5
  Project-URL: Homepage, https://github.com/anthropics/claude-agent-sdk-python
6
6
  Project-URL: Documentation, https://docs.anthropic.com/en/docs/claude-code/sdk
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "claude-agent-sdk"
7
- version = "0.0.23"
7
+ version = "0.1.0"
8
8
  description = "Python SDK for Claude Code"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -24,7 +24,7 @@ from . import Transport
24
24
 
25
25
  logger = logging.getLogger(__name__)
26
26
 
27
- _MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
27
+ _DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
28
28
 
29
29
 
30
30
  class SubprocessCLITransport(Transport):
@@ -48,6 +48,11 @@ class SubprocessCLITransport(Transport):
48
48
  self._stderr_task_group: anyio.abc.TaskGroup | None = None
49
49
  self._ready = False
50
50
  self._exit_error: Exception | None = None # Track process exit errors
51
+ self._max_buffer_size = (
52
+ options.max_buffer_size
53
+ if options.max_buffer_size is not None
54
+ else _DEFAULT_MAX_BUFFER_SIZE
55
+ )
51
56
 
52
57
  def _find_cli(self) -> str:
53
58
  """Find Claude Code CLI binary."""
@@ -66,15 +71,6 @@ class SubprocessCLITransport(Transport):
66
71
  if path.exists() and path.is_file():
67
72
  return str(path)
68
73
 
69
- node_installed = shutil.which("node") is not None
70
-
71
- if not node_installed:
72
- error_msg = "Claude Code requires Node.js, which is not installed.\n\n"
73
- error_msg += "Install Node.js from: https://nodejs.org/\n"
74
- error_msg += "\nAfter installing Node.js, install Claude Code:\n"
75
- error_msg += " npm install -g @anthropic-ai/claude-code"
76
- raise CLINotFoundError(error_msg)
77
-
78
74
  raise CLINotFoundError(
79
75
  "Claude Code not found. Install with:\n"
80
76
  " npm install -g @anthropic-ai/claude-code\n"
@@ -411,12 +407,13 @@ class SubprocessCLITransport(Transport):
411
407
  # Keep accumulating partial JSON until we can parse it
412
408
  json_buffer += json_line
413
409
 
414
- if len(json_buffer) > _MAX_BUFFER_SIZE:
410
+ if len(json_buffer) > self._max_buffer_size:
411
+ buffer_length = len(json_buffer)
415
412
  json_buffer = ""
416
413
  raise SDKJSONDecodeError(
417
- f"JSON message exceeded maximum buffer size of {_MAX_BUFFER_SIZE} bytes",
414
+ f"JSON message exceeded maximum buffer size of {self._max_buffer_size} bytes",
418
415
  ValueError(
419
- f"Buffer size {len(json_buffer)} exceeds limit {_MAX_BUFFER_SIZE}"
416
+ f"Buffer size {buffer_length} exceeds limit {self._max_buffer_size}"
420
417
  ),
421
418
  )
422
419
 
@@ -427,7 +424,7 @@ class SubprocessCLITransport(Transport):
427
424
  except json.JSONDecodeError:
428
425
  # We are speculatively decoding the buffer until we get
429
426
  # a full JSON object. If there is an actual issue, we
430
- # raise an error after _MAX_BUFFER_SIZE.
427
+ # raise an error after exceeding the configured limit.
431
428
  continue
432
429
 
433
430
  except anyio.ClosedResourceError:
@@ -1,3 +1,3 @@
1
1
  """Version information for claude-agent-sdk."""
2
2
 
3
- __version__ = "0.0.23"
3
+ __version__ = "0.1.0"
@@ -320,6 +320,7 @@ class ClaudeAgentOptions:
320
320
  extra_args: dict[str, str | None] = field(
321
321
  default_factory=dict
322
322
  ) # Pass arbitrary CLI flags
323
+ max_buffer_size: int | None = None # Max bytes when buffering CLI stdout
323
324
  debug_stderr: Any = (
324
325
  sys.stderr
325
326
  ) # Deprecated: File-like object for debug output. Use stderr callback instead.
@@ -161,7 +161,7 @@ class TestIntegration:
161
161
  async for _ in query(prompt="test"):
162
162
  pass
163
163
 
164
- assert "Claude Code requires Node.js" in str(exc_info.value)
164
+ assert "Claude Code not found" in str(exc_info.value)
165
165
 
166
166
  anyio.run(_test)
167
167
 
@@ -10,7 +10,7 @@ import pytest
10
10
 
11
11
  from claude_agent_sdk._errors import CLIJSONDecodeError
12
12
  from claude_agent_sdk._internal.transport.subprocess_cli import (
13
- _MAX_BUFFER_SIZE,
13
+ _DEFAULT_MAX_BUFFER_SIZE,
14
14
  SubprocessCLITransport,
15
15
  )
16
16
  from claude_agent_sdk.types import ClaudeAgentOptions
@@ -237,7 +237,7 @@ class TestSubprocessBuffering:
237
237
  """Test that exceeding buffer size raises an appropriate error."""
238
238
 
239
239
  async def _test() -> None:
240
- huge_incomplete = '{"data": "' + "x" * (_MAX_BUFFER_SIZE + 1000)
240
+ huge_incomplete = '{"data": "' + "x" * (_DEFAULT_MAX_BUFFER_SIZE + 1000)
241
241
 
242
242
  transport = SubprocessCLITransport(
243
243
  prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude"
@@ -260,6 +260,34 @@ class TestSubprocessBuffering:
260
260
 
261
261
  anyio.run(_test)
262
262
 
263
+ def test_buffer_size_option(self) -> None:
264
+ """Test that the configurable buffer size option is respected."""
265
+
266
+ async def _test() -> None:
267
+ custom_limit = 512
268
+ huge_incomplete = '{"data": "' + "x" * (custom_limit + 10)
269
+
270
+ transport = SubprocessCLITransport(
271
+ prompt="test",
272
+ options=ClaudeAgentOptions(max_buffer_size=custom_limit),
273
+ cli_path="/usr/bin/claude",
274
+ )
275
+
276
+ mock_process = MagicMock()
277
+ mock_process.returncode = None
278
+ mock_process.wait = AsyncMock(return_value=None)
279
+ transport._process = mock_process
280
+ transport._stdout_stream = MockTextReceiveStream([huge_incomplete])
281
+ transport._stderr_stream = MockTextReceiveStream([])
282
+
283
+ with pytest.raises(CLIJSONDecodeError) as exc_info:
284
+ async for _ in transport.read_messages():
285
+ pass
286
+
287
+ assert f"maximum buffer size of {custom_limit} bytes" in str(exc_info.value)
288
+
289
+ anyio.run(_test)
290
+
263
291
  def test_mixed_complete_and_split_json(self) -> None:
264
292
  """Test handling a mix of complete and split JSON messages."""
265
293
 
@@ -25,7 +25,7 @@ class TestSubprocessCLITransport:
25
25
  ):
26
26
  SubprocessCLITransport(prompt="test", options=ClaudeAgentOptions())
27
27
 
28
- assert "Claude Code requires Node.js" in str(exc_info.value)
28
+ assert "Claude Code not found" in str(exc_info.value)
29
29
 
30
30
  def test_build_command_basic(self):
31
31
  """Test building basic CLI command."""
@@ -1,177 +0,0 @@
1
- """Mock transport implementation for testing."""
2
-
3
- import json
4
- from collections.abc import AsyncIterator
5
- from typing import Any
6
-
7
- import anyio
8
-
9
- from claude_agent_sdk._internal.transport import Transport
10
-
11
-
12
- class MockTransport(Transport):
13
- """Mock transport for testing Query and Client behavior.
14
-
15
- This transport allows tests to:
16
- - Capture all messages written by the SDK
17
- - Simulate responses from the CLI
18
- - Control message flow timing
19
- """
20
-
21
- def __init__(self):
22
- """Initialize mock transport."""
23
- self.written_messages: list[dict[str, Any]] = []
24
- self.messages_to_read: list[dict[str, Any]] = []
25
- self._ready = False
26
- self._read_index = 0
27
- self._write_delay = 0.0 # Optional delay for write operations
28
- self._read_delay = 0.0 # Optional delay between messages
29
-
30
- async def connect(self) -> None:
31
- """Mark transport as ready."""
32
- self._ready = True
33
-
34
- async def write(self, data: str) -> None:
35
- """Capture written messages.
36
-
37
- Args:
38
- data: JSON string with newline
39
- """
40
- if self._write_delay > 0:
41
- await anyio.sleep(self._write_delay)
42
-
43
- # Parse and store the message
44
- message = json.loads(data.strip())
45
- self.written_messages.append(message)
46
-
47
- async def read_messages(self) -> AsyncIterator[dict[str, Any]]:
48
- """Yield messages from the configured list.
49
-
50
- Yields:
51
- Messages added via add_message_to_read()
52
- """
53
- # Auto-respond to initialize requests
54
- responded_to_init = False
55
-
56
- while self._ready:
57
- # Check for initialize requests we need to respond to
58
- if not responded_to_init:
59
- for msg in self.written_messages:
60
- if (
61
- msg.get("type") == "control_request"
62
- and msg.get("request", {}).get("subtype") == "initialize"
63
- ):
64
- # Auto-send initialize response
65
- responded_to_init = True
66
- yield {
67
- "type": "control_response",
68
- "response": {
69
- "subtype": "success",
70
- "request_id": msg.get("request_id"),
71
- "response": {
72
- "commands": [],
73
- "output_style": "default",
74
- "hooks": [], # Will be populated by Query.initialize
75
- },
76
- },
77
- }
78
- break
79
-
80
- # Yield any manually added messages
81
- if self._read_index < len(self.messages_to_read):
82
- if self._read_delay > 0:
83
- await anyio.sleep(self._read_delay)
84
-
85
- message = self.messages_to_read[self._read_index]
86
- self._read_index += 1
87
- yield message
88
- else:
89
- # Small delay to avoid busy loop
90
- await anyio.sleep(0.01)
91
-
92
- async def close(self) -> None:
93
- """Mark transport as not ready."""
94
- self._ready = False
95
-
96
- def is_ready(self) -> bool:
97
- """Check if transport is ready.
98
-
99
- Returns:
100
- True if connect() was called and close() wasn't
101
- """
102
- return self._ready
103
-
104
- async def end_input(self) -> None:
105
- """No-op for mock transport."""
106
- pass
107
-
108
- # Helper methods for testing
109
-
110
- def add_message_to_read(self, message: dict[str, Any]) -> None:
111
- """Add a message that will be returned by read_messages().
112
-
113
- Args:
114
- message: Message dict to be yielded by read_messages()
115
- """
116
- self.messages_to_read.append(message)
117
-
118
- def add_control_request(self, subtype: str, request_id: str, **kwargs: Any) -> None:
119
- """Helper to add a control request message.
120
-
121
- Args:
122
- subtype: Control request subtype (e.g., "initialize", "hook_callback")
123
- request_id: Request ID
124
- **kwargs: Additional request fields
125
- """
126
- self.add_message_to_read(
127
- {
128
- "type": "control_request",
129
- "request_id": request_id,
130
- "request": {"subtype": subtype, **kwargs},
131
- }
132
- )
133
-
134
- def get_written_messages(self, msg_type: str | None = None) -> list[dict[str, Any]]:
135
- """Get written messages, optionally filtered by type.
136
-
137
- Args:
138
- msg_type: Optional message type to filter by
139
-
140
- Returns:
141
- List of written messages
142
- """
143
- if msg_type is None:
144
- return self.written_messages
145
- return [msg for msg in self.written_messages if msg.get("type") == msg_type]
146
-
147
- def get_control_responses(self) -> list[dict[str, Any]]:
148
- """Get all control response messages that were written.
149
-
150
- Returns:
151
- List of control response messages
152
- """
153
- return self.get_written_messages("control_response")
154
-
155
- def get_last_response(self) -> dict[str, Any] | None:
156
- """Get the last written message.
157
-
158
- Returns:
159
- Last written message or None if no messages
160
- """
161
- return self.written_messages[-1] if self.written_messages else None
162
-
163
- def clear(self) -> None:
164
- """Clear all messages and reset state."""
165
- self.written_messages.clear()
166
- self.messages_to_read.clear()
167
- self._read_index = 0
168
-
169
- def set_delays(self, write_delay: float = 0.0, read_delay: float = 0.0) -> None:
170
- """Set artificial delays for testing timing issues.
171
-
172
- Args:
173
- write_delay: Delay in seconds for write operations
174
- read_delay: Delay in seconds between read messages
175
- """
176
- self._write_delay = write_delay
177
- self._read_delay = read_delay
@@ -1,148 +0,0 @@
1
- """Tests for the hooks decorator API."""
2
-
3
- import anyio
4
-
5
- from claude_agent_sdk.hooks import (
6
- PreToolUseHookResponse,
7
- clear_registry,
8
- get_registry,
9
- post_tool_use,
10
- pre_tool_use,
11
- )
12
- from claude_agent_sdk.hooks.executor import execute_hook
13
-
14
-
15
- class TestHooksAPI:
16
- """Test the decorator-based hooks API."""
17
-
18
- def setup_method(self):
19
- """Clear registry before each test."""
20
- clear_registry()
21
-
22
- def test_decorator_registers_and_executes(self):
23
- """Test that decorators register hooks and they execute properly."""
24
-
25
- async def _test():
26
- executed = []
27
-
28
- # Test pre_tool_use decorator
29
- @pre_tool_use(matcher="Bash", timeout=5)
30
- def check_dangerous_commands(tool_name, tool_input):
31
- executed.append(("pre", tool_name, tool_input.to_dict()))
32
- if "rm -rf /" in tool_input.get("command", ""):
33
- return PreToolUseHookResponse(
34
- permission_decision="deny",
35
- permission_decision_reason="Dangerous command blocked",
36
- )
37
- return None # Allow
38
-
39
- # Verify registration
40
- registry = get_registry()
41
- hooks = registry.get_hooks("PreToolUse")
42
- assert len(hooks) == 1
43
- assert hooks[0].matcher == "Bash"
44
- assert hooks[0].timeout == 5
45
-
46
- # Test execution with dangerous command
47
- result = await execute_hook(
48
- check_dangerous_commands,
49
- {"tool_name": "Bash", "tool_input": {"command": "rm -rf /"}},
50
- )
51
- assert isinstance(result, PreToolUseHookResponse)
52
- assert result.permission_decision == "deny"
53
- assert "Dangerous command blocked" in result.permission_decision_reason
54
-
55
- # Test execution with safe command
56
- result = await execute_hook(
57
- check_dangerous_commands,
58
- {"tool_name": "Bash", "tool_input": {"command": "ls -la"}},
59
- )
60
- assert result is None # Allowed
61
-
62
- # Verify hook was called
63
- assert len(executed) == 2
64
- assert executed[0][0] == "pre"
65
- assert executed[0][1] == "Bash"
66
-
67
- anyio.run(_test)
68
-
69
- def test_registry_lifecycle(self):
70
- """Test registry clear and isolation between tests."""
71
-
72
- # Register some hooks
73
- @pre_tool_use()
74
- def hook1():
75
- pass
76
-
77
- @post_tool_use(matcher="Write")
78
- def hook2():
79
- pass
80
-
81
- registry = get_registry()
82
- assert len(registry.get_hooks("PreToolUse")) == 1
83
- assert len(registry.get_hooks("PostToolUse")) == 1
84
-
85
- # Clear registry
86
- clear_registry()
87
- assert len(registry.get_hooks("PreToolUse")) == 0
88
- assert len(registry.get_hooks("PostToolUse")) == 0
89
-
90
- # Register new hooks - verify no interference
91
- @pre_tool_use(matcher="Read")
92
- def hook3():
93
- pass
94
-
95
- assert len(registry.get_hooks("PreToolUse")) == 1
96
- assert registry.get_hooks("PreToolUse")[0].matcher == "Read"
97
-
98
- def test_parameter_introspection(self):
99
- """Test that hooks only receive the parameters they request."""
100
-
101
- async def _test():
102
- received_params = {}
103
-
104
- # Hook that only wants tool_name
105
- def minimal_hook(tool_name):
106
- received_params["minimal"] = {"tool_name": tool_name}
107
- return None
108
-
109
- # Hook that wants multiple params
110
- def full_hook(tool_name, tool_input, tool_use_id):
111
- received_params["full"] = {
112
- "tool_name": tool_name,
113
- "tool_input": tool_input.to_dict(),
114
- "tool_use_id": tool_use_id,
115
- }
116
- return None
117
-
118
- # Test minimal hook - should only get tool_name
119
- await execute_hook(
120
- minimal_hook,
121
- {
122
- "tool_name": "TestTool",
123
- "tool_input": {"data": "test"},
124
- "extra": "ignored",
125
- },
126
- tool_use_id="123",
127
- )
128
-
129
- assert "minimal" in received_params
130
- assert received_params["minimal"] == {"tool_name": "TestTool"}
131
-
132
- # Test full hook - should get all requested params
133
- await execute_hook(
134
- full_hook,
135
- {
136
- "tool_name": "TestTool",
137
- "tool_input": {"data": "test"},
138
- "extra": "also_ignored",
139
- },
140
- tool_use_id="456",
141
- )
142
-
143
- assert "full" in received_params
144
- assert received_params["full"]["tool_name"] == "TestTool"
145
- assert received_params["full"]["tool_input"] == {"data": "test"}
146
- assert received_params["full"]["tool_use_id"] == "456"
147
-
148
- anyio.run(_test)