claude-agent-sdk 0.1.1__tar.gz → 0.1.2__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 (30) hide show
  1. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/PKG-INFO +1 -1
  2. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/pyproject.toml +1 -1
  3. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/__init__.py +2 -0
  4. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/query.py +9 -3
  5. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_version.py +1 -1
  6. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/types.py +65 -9
  7. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/test_streaming_client.py +13 -6
  8. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/test_tool_callbacks.py +133 -0
  9. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/test_transport.py +23 -12
  10. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/.gitignore +0 -0
  11. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/LICENSE +0 -0
  12. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/README.md +0 -0
  13. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_errors.py +0 -0
  14. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/__init__.py +0 -0
  15. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/client.py +0 -0
  16. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/message_parser.py +0 -0
  17. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/transport/__init__.py +0 -0
  18. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +0 -0
  19. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/client.py +0 -0
  20. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/py.typed +0 -0
  21. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/query.py +0 -0
  22. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/conftest.py +0 -0
  23. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/test_changelog.py +0 -0
  24. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/test_client.py +0 -0
  25. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/test_errors.py +0 -0
  26. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/test_integration.py +0 -0
  27. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/test_message_parser.py +0 -0
  28. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/test_sdk_mcp_integration.py +0 -0
  29. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/tests/test_subprocess_buffering.py +0 -0
  30. {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.2}/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.1.1
3
+ Version: 0.1.2
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.1.1"
7
+ version = "0.1.2"
8
8
  description = "Python SDK for Claude Code"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -23,6 +23,7 @@ from .types import (
23
23
  ContentBlock,
24
24
  HookCallback,
25
25
  HookContext,
26
+ HookJSONOutput,
26
27
  HookMatcher,
27
28
  McpSdkServerConfig,
28
29
  McpServerConfig,
@@ -308,6 +309,7 @@ __all__ = [
308
309
  "PermissionUpdate",
309
310
  "HookCallback",
310
311
  "HookContext",
312
+ "HookJSONOutput",
311
313
  "HookMatcher",
312
314
  # Agent support
313
315
  "AgentDefinition",
@@ -195,6 +195,7 @@ class Query:
195
195
 
196
196
  if subtype == "can_use_tool":
197
197
  permission_request: SDKControlPermissionRequest = request_data # type: ignore[assignment]
198
+ original_input = permission_request["input"]
198
199
  # Handle tool permission request
199
200
  if not self.can_use_tool:
200
201
  raise Exception("canUseTool callback is not provided")
@@ -213,9 +214,14 @@ class Query:
213
214
 
214
215
  # Convert PermissionResult to expected dict format
215
216
  if isinstance(response, PermissionResultAllow):
216
- response_data = {"behavior": "allow"}
217
- if response.updated_input is not None:
218
- response_data["updatedInput"] = response.updated_input
217
+ response_data = {
218
+ "behavior": "allow",
219
+ "updatedInput": (
220
+ response.updated_input
221
+ if response.updated_input is not None
222
+ else original_input
223
+ ),
224
+ }
219
225
  if response.updated_permissions is not None:
220
226
  response_data["updatedPermissions"] = [
221
227
  permission.to_dict()
@@ -1,3 +1,3 @@
1
1
  """Version information for claude-agent-sdk."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.2"
@@ -157,18 +157,74 @@ HookEvent = (
157
157
  )
158
158
 
159
159
 
160
+ # Hook-specific output types
161
+ class PreToolUseHookSpecificOutput(TypedDict):
162
+ """Hook-specific output for PreToolUse events."""
163
+
164
+ hookEventName: Literal["PreToolUse"]
165
+ permissionDecision: NotRequired[Literal["allow", "deny", "ask"]]
166
+ permissionDecisionReason: NotRequired[str]
167
+ updatedInput: NotRequired[dict[str, Any]]
168
+
169
+
170
+ class PostToolUseHookSpecificOutput(TypedDict):
171
+ """Hook-specific output for PostToolUse events."""
172
+
173
+ hookEventName: Literal["PostToolUse"]
174
+ additionalContext: NotRequired[str]
175
+
176
+
177
+ class UserPromptSubmitHookSpecificOutput(TypedDict):
178
+ """Hook-specific output for UserPromptSubmit events."""
179
+
180
+ hookEventName: Literal["UserPromptSubmit"]
181
+ additionalContext: NotRequired[str]
182
+
183
+
184
+ class SessionStartHookSpecificOutput(TypedDict):
185
+ """Hook-specific output for SessionStart events."""
186
+
187
+ hookEventName: Literal["SessionStart"]
188
+ additionalContext: NotRequired[str]
189
+
190
+
191
+ HookSpecificOutput = (
192
+ PreToolUseHookSpecificOutput
193
+ | PostToolUseHookSpecificOutput
194
+ | UserPromptSubmitHookSpecificOutput
195
+ | SessionStartHookSpecificOutput
196
+ )
197
+
198
+
160
199
  # See https://docs.anthropic.com/en/docs/claude-code/hooks#advanced%3A-json-output
161
- # for documentation of the output types. Currently, "continue", "stopReason",
162
- # and "suppressOutput" are not supported in the Python SDK.
163
- class HookJSONOutput(TypedDict):
164
- # Whether to block the action related to the hook.
200
+ # for documentation of the output types.
201
+ class AsyncHookJSONOutput(TypedDict):
202
+ """Async hook output that defers hook execution."""
203
+
204
+ async_: Literal[True] # Using async_ to avoid Python keyword
205
+ asyncTimeout: NotRequired[int]
206
+
207
+
208
+ class SyncHookJSONOutput(TypedDict):
209
+ """Synchronous hook output with control and decision fields."""
210
+
211
+ # Common control fields
212
+ continue_: NotRequired[bool] # Using continue_ to avoid Python keyword
213
+ suppressOutput: NotRequired[bool]
214
+ stopReason: NotRequired[str]
215
+
216
+ # Decision fields
217
+ # Note: "approve" is deprecated for PreToolUse (use permissionDecision instead)
218
+ # For other hooks, only "block" is meaningful
165
219
  decision: NotRequired[Literal["block"]]
166
- # Optionally add a system message that is not visible to Claude but saved in
167
- # the chat transcript.
168
220
  systemMessage: NotRequired[str]
169
- # See each hook's individual "Decision Control" section in the documentation
170
- # for guidance.
171
- hookSpecificOutput: NotRequired[Any]
221
+ reason: NotRequired[str]
222
+
223
+ # Hook-specific outputs
224
+ hookSpecificOutput: NotRequired[HookSpecificOutput]
225
+
226
+
227
+ HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput
172
228
 
173
229
 
174
230
  @dataclass
@@ -633,21 +633,28 @@ assert '"Second"' in stdin_messages[1]
633
633
  print('{"type": "result", "subtype": "success", "duration_ms": 100, "duration_api_ms": 50, "is_error": false, "num_turns": 1, "session_id": "test", "total_cost_usd": 0.001}')
634
634
  """)
635
635
 
636
- Path(test_script).chmod(0o755)
636
+ # Make script executable (Unix-style systems)
637
+ if sys.platform != "win32":
638
+ Path(test_script).chmod(0o755)
637
639
 
638
640
  try:
639
- # Mock _find_cli to return python executing our test script
641
+ # Mock _find_cli to return the test script path directly
640
642
  with patch.object(
641
- SubprocessCLITransport, "_find_cli", return_value=sys.executable
643
+ SubprocessCLITransport, "_find_cli", return_value=test_script
642
644
  ):
643
- # Mock _build_command to add our test script as first argument
645
+ # Mock _build_command to properly execute Python script
644
646
  original_build_command = SubprocessCLITransport._build_command
645
647
 
646
648
  def mock_build_command(self):
647
649
  # Get original command
648
650
  cmd = original_build_command(self)
649
- # Replace the CLI path with python + script
650
- cmd[0] = test_script
651
+ # On Windows, we need to use python interpreter to run the script
652
+ if sys.platform == "win32":
653
+ # Replace first element with python interpreter and script
654
+ cmd[0:1] = [sys.executable, test_script]
655
+ else:
656
+ # On Unix, just use the script directly
657
+ cmd[0] = test_script
651
658
  return cmd
652
659
 
653
660
  with patch.object(
@@ -1,10 +1,13 @@
1
1
  """Tests for tool permission callbacks and hook callbacks."""
2
2
 
3
+ import json
4
+
3
5
  import pytest
4
6
 
5
7
  from claude_agent_sdk import (
6
8
  ClaudeAgentOptions,
7
9
  HookContext,
10
+ HookJSONOutput,
8
11
  HookMatcher,
9
12
  PermissionResultAllow,
10
13
  PermissionResultDeny,
@@ -257,6 +260,136 @@ class TestHookCallbacks:
257
260
  last_response = transport.written_messages[-1]
258
261
  assert '"processed": true' in last_response
259
262
 
263
+ @pytest.mark.asyncio
264
+ async def test_hook_output_fields(self):
265
+ """Test that all SyncHookJSONOutput fields are properly handled."""
266
+
267
+ # Test all SyncHookJSONOutput fields together
268
+ async def comprehensive_hook(
269
+ input_data: dict, tool_use_id: str | None, context: HookContext
270
+ ) -> HookJSONOutput:
271
+ return {
272
+ # Control fields
273
+ "continue_": True,
274
+ "suppressOutput": False,
275
+ "stopReason": "Test stop reason",
276
+ # Decision fields
277
+ "decision": "block",
278
+ "systemMessage": "Test system message",
279
+ "reason": "Test reason for blocking",
280
+ # Hook-specific output with all PreToolUse fields
281
+ "hookSpecificOutput": {
282
+ "hookEventName": "PreToolUse",
283
+ "permissionDecision": "deny",
284
+ "permissionDecisionReason": "Security policy violation",
285
+ "updatedInput": {"modified": "input"},
286
+ },
287
+ }
288
+
289
+ transport = MockTransport()
290
+ hooks = {
291
+ "PreToolUse": [
292
+ {"matcher": {"tool": "TestTool"}, "hooks": [comprehensive_hook]}
293
+ ]
294
+ }
295
+
296
+ query = Query(
297
+ transport=transport, is_streaming_mode=True, can_use_tool=None, hooks=hooks
298
+ )
299
+
300
+ callback_id = "test_comprehensive_hook"
301
+ query.hook_callbacks[callback_id] = comprehensive_hook
302
+
303
+ request = {
304
+ "type": "control_request",
305
+ "request_id": "test-comprehensive",
306
+ "request": {
307
+ "subtype": "hook_callback",
308
+ "callback_id": callback_id,
309
+ "input": {"test": "data"},
310
+ "tool_use_id": "tool-456",
311
+ },
312
+ }
313
+
314
+ await query._handle_control_request(request)
315
+
316
+ # Check response contains all the fields
317
+ assert len(transport.written_messages) > 0
318
+ last_response = transport.written_messages[-1]
319
+
320
+ # Parse the JSON response
321
+ response_data = json.loads(last_response)
322
+ # The hook result is nested at response.response
323
+ result = response_data["response"]["response"]
324
+
325
+ # Verify control fields are present
326
+ assert result.get("continue_") is True or result.get("continue") is True
327
+ assert result.get("suppressOutput") is False
328
+ assert result.get("stopReason") == "Test stop reason"
329
+
330
+ # Verify decision fields are present
331
+ assert result.get("decision") == "block"
332
+ assert result.get("reason") == "Test reason for blocking"
333
+ assert result.get("systemMessage") == "Test system message"
334
+
335
+ # Verify hook-specific output is present
336
+ hook_output = result.get("hookSpecificOutput", {})
337
+ assert hook_output.get("hookEventName") == "PreToolUse"
338
+ assert hook_output.get("permissionDecision") == "deny"
339
+ assert (
340
+ hook_output.get("permissionDecisionReason") == "Security policy violation"
341
+ )
342
+ assert "updatedInput" in hook_output
343
+
344
+ @pytest.mark.asyncio
345
+ async def test_async_hook_output(self):
346
+ """Test AsyncHookJSONOutput type with proper async fields."""
347
+
348
+ async def async_hook(
349
+ input_data: dict, tool_use_id: str | None, context: HookContext
350
+ ) -> HookJSONOutput:
351
+ # Test that async hooks properly use async_ and asyncTimeout fields
352
+ return {
353
+ "async_": True,
354
+ "asyncTimeout": 5000,
355
+ }
356
+
357
+ transport = MockTransport()
358
+ hooks = {"PreToolUse": [{"matcher": None, "hooks": [async_hook]}]}
359
+
360
+ query = Query(
361
+ transport=transport, is_streaming_mode=True, can_use_tool=None, hooks=hooks
362
+ )
363
+
364
+ callback_id = "test_async_hook"
365
+ query.hook_callbacks[callback_id] = async_hook
366
+
367
+ request = {
368
+ "type": "control_request",
369
+ "request_id": "test-async",
370
+ "request": {
371
+ "subtype": "hook_callback",
372
+ "callback_id": callback_id,
373
+ "input": {"test": "async_data"},
374
+ "tool_use_id": None,
375
+ },
376
+ }
377
+
378
+ await query._handle_control_request(request)
379
+
380
+ # Check response contains async fields
381
+ assert len(transport.written_messages) > 0
382
+ last_response = transport.written_messages[-1]
383
+
384
+ # Parse the JSON response
385
+ response_data = json.loads(last_response)
386
+ # The hook result is nested at response.response
387
+ result = response_data["response"]["response"]
388
+
389
+ # The SDK should preserve the async_ field (or convert to "async")
390
+ assert result.get("async_") is True or result.get("async") is True
391
+ assert result.get("asyncTimeout") == 5000
392
+
260
393
 
261
394
  class TestClaudeAgentOptionsIntegration:
262
395
  """Test that callbacks work through ClaudeAgentOptions."""
@@ -44,13 +44,15 @@ class TestSubprocessCLITransport:
44
44
  """Test that cli_path accepts pathlib.Path objects."""
45
45
  from pathlib import Path
46
46
 
47
+ path = Path("/usr/bin/claude")
47
48
  transport = SubprocessCLITransport(
48
49
  prompt="Hello",
49
50
  options=ClaudeAgentOptions(),
50
- cli_path=Path("/usr/bin/claude"),
51
+ cli_path=path,
51
52
  )
52
53
 
53
- assert transport._cli_path == "/usr/bin/claude"
54
+ # Path object is converted to string, compare with str(path)
55
+ assert transport._cli_path == str(path)
54
56
 
55
57
  def test_build_command_with_system_prompt_string(self):
56
58
  """Test building CLI command with system prompt as string."""
@@ -129,19 +131,25 @@ class TestSubprocessCLITransport:
129
131
  """Test building CLI command with add_dirs option."""
130
132
  from pathlib import Path
131
133
 
134
+ dir1 = "/path/to/dir1"
135
+ dir2 = Path("/path/to/dir2")
132
136
  transport = SubprocessCLITransport(
133
137
  prompt="test",
134
- options=ClaudeAgentOptions(
135
- add_dirs=["/path/to/dir1", Path("/path/to/dir2")]
136
- ),
138
+ options=ClaudeAgentOptions(add_dirs=[dir1, dir2]),
137
139
  cli_path="/usr/bin/claude",
138
140
  )
139
141
 
140
142
  cmd = transport._build_command()
141
- cmd_str = " ".join(cmd)
142
143
 
143
- # Check that the command string contains the expected --add-dir flags
144
- assert "--add-dir /path/to/dir1 --add-dir /path/to/dir2" in cmd_str
144
+ # Check that both directories are in the command
145
+ assert "--add-dir" in cmd
146
+ add_dir_indices = [i for i, x in enumerate(cmd) if x == "--add-dir"]
147
+ assert len(add_dir_indices) == 2
148
+
149
+ # The directories should appear after --add-dir flags
150
+ dirs_in_cmd = [cmd[i + 1] for i in add_dir_indices]
151
+ assert dir1 in dirs_in_cmd
152
+ assert str(dir2) in dirs_in_cmd
145
153
 
146
154
  def test_session_continuation(self):
147
155
  """Test session continuation options."""
@@ -322,28 +330,31 @@ class TestSubprocessCLITransport:
322
330
  from pathlib import Path
323
331
 
324
332
  # Test with string path
333
+ string_path = "/path/to/mcp-config.json"
325
334
  transport = SubprocessCLITransport(
326
335
  prompt="test",
327
- options=ClaudeAgentOptions(mcp_servers="/path/to/mcp-config.json"),
336
+ options=ClaudeAgentOptions(mcp_servers=string_path),
328
337
  cli_path="/usr/bin/claude",
329
338
  )
330
339
 
331
340
  cmd = transport._build_command()
332
341
  assert "--mcp-config" in cmd
333
342
  mcp_idx = cmd.index("--mcp-config")
334
- assert cmd[mcp_idx + 1] == "/path/to/mcp-config.json"
343
+ assert cmd[mcp_idx + 1] == string_path
335
344
 
336
345
  # Test with Path object
346
+ path_obj = Path("/path/to/mcp-config.json")
337
347
  transport = SubprocessCLITransport(
338
348
  prompt="test",
339
- options=ClaudeAgentOptions(mcp_servers=Path("/path/to/mcp-config.json")),
349
+ options=ClaudeAgentOptions(mcp_servers=path_obj),
340
350
  cli_path="/usr/bin/claude",
341
351
  )
342
352
 
343
353
  cmd = transport._build_command()
344
354
  assert "--mcp-config" in cmd
345
355
  mcp_idx = cmd.index("--mcp-config")
346
- assert cmd[mcp_idx + 1] == "/path/to/mcp-config.json"
356
+ # Path object gets converted to string, compare with str(path_obj)
357
+ assert cmd[mcp_idx + 1] == str(path_obj)
347
358
 
348
359
  def test_build_command_with_mcp_servers_as_json_string(self):
349
360
  """Test building CLI command with mcp_servers as JSON string."""