claude-agent-sdk 0.1.0__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.
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/PKG-INFO +13 -4
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/README.md +12 -3
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/pyproject.toml +1 -1
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/__init__.py +2 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/query.py +17 -6
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +45 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_version.py +1 -1
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/client.py +18 -8
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/types.py +101 -9
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_streaming_client.py +13 -6
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_tool_callbacks.py +137 -4
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_transport.py +77 -27
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_types.py +2 -2
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/.gitignore +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/LICENSE +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_errors.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/__init__.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/client.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/message_parser.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/transport/__init__.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/py.typed +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/query.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/conftest.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_changelog.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_client.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_errors.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_integration.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_message_parser.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_sdk_mcp_integration.py +0 -0
- {claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/tests/test_subprocess_buffering.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-agent-sdk
|
|
3
|
-
Version: 0.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
|
|
@@ -43,8 +43,8 @@ pip install claude-agent-sdk
|
|
|
43
43
|
|
|
44
44
|
**Prerequisites:**
|
|
45
45
|
- Python 3.10+
|
|
46
|
-
- Node.js
|
|
47
|
-
- Claude Code
|
|
46
|
+
- Node.js
|
|
47
|
+
- Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
|
|
48
48
|
|
|
49
49
|
## Quick Start
|
|
50
50
|
|
|
@@ -92,7 +92,7 @@ options = ClaudeAgentOptions(
|
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
async for message in query(
|
|
95
|
-
prompt="Create a hello.py file",
|
|
95
|
+
prompt="Create a hello.py file",
|
|
96
96
|
options=options
|
|
97
97
|
):
|
|
98
98
|
# Process tool use and results
|
|
@@ -304,6 +304,15 @@ See [examples/quick_start.py](examples/quick_start.py) for a complete working ex
|
|
|
304
304
|
|
|
305
305
|
See [examples/streaming_mode.py](examples/streaming_mode.py) for comprehensive examples involving `ClaudeSDKClient`. You can even run interactive examples in IPython from [examples/streaming_mode_ipython.py](examples/streaming_mode_ipython.py).
|
|
306
306
|
|
|
307
|
+
## Migrating from Claude Code SDK
|
|
308
|
+
|
|
309
|
+
If you're upgrading from the Claude Code SDK (versions < 0.1.0), please see the [CHANGELOG.md](CHANGELOG.md#010) for details on breaking changes and new features, including:
|
|
310
|
+
|
|
311
|
+
- `ClaudeCodeOptions` → `ClaudeAgentOptions` rename
|
|
312
|
+
- Merged system prompt configuration
|
|
313
|
+
- Settings isolation and explicit control
|
|
314
|
+
- New programmatic subagents and session forking features
|
|
315
|
+
|
|
307
316
|
## License
|
|
308
317
|
|
|
309
318
|
MIT
|
|
@@ -10,8 +10,8 @@ pip install claude-agent-sdk
|
|
|
10
10
|
|
|
11
11
|
**Prerequisites:**
|
|
12
12
|
- Python 3.10+
|
|
13
|
-
- Node.js
|
|
14
|
-
- Claude Code
|
|
13
|
+
- Node.js
|
|
14
|
+
- Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
|
|
15
15
|
|
|
16
16
|
## Quick Start
|
|
17
17
|
|
|
@@ -59,7 +59,7 @@ options = ClaudeAgentOptions(
|
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
async for message in query(
|
|
62
|
-
prompt="Create a hello.py file",
|
|
62
|
+
prompt="Create a hello.py file",
|
|
63
63
|
options=options
|
|
64
64
|
):
|
|
65
65
|
# Process tool use and results
|
|
@@ -271,6 +271,15 @@ See [examples/quick_start.py](examples/quick_start.py) for a complete working ex
|
|
|
271
271
|
|
|
272
272
|
See [examples/streaming_mode.py](examples/streaming_mode.py) for comprehensive examples involving `ClaudeSDKClient`. You can even run interactive examples in IPython from [examples/streaming_mode_ipython.py](examples/streaming_mode_ipython.py).
|
|
273
273
|
|
|
274
|
+
## Migrating from Claude Code SDK
|
|
275
|
+
|
|
276
|
+
If you're upgrading from the Claude Code SDK (versions < 0.1.0), please see the [CHANGELOG.md](CHANGELOG.md#010) for details on breaking changes and new features, including:
|
|
277
|
+
|
|
278
|
+
- `ClaudeCodeOptions` → `ClaudeAgentOptions` rename
|
|
279
|
+
- Merged system prompt configuration
|
|
280
|
+
- Settings isolation and explicit control
|
|
281
|
+
- New programmatic subagents and session forking features
|
|
282
|
+
|
|
274
283
|
## License
|
|
275
284
|
|
|
276
285
|
MIT
|
|
@@ -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,13 +214,23 @@ class Query:
|
|
|
213
214
|
|
|
214
215
|
# Convert PermissionResult to expected dict format
|
|
215
216
|
if isinstance(response, PermissionResultAllow):
|
|
216
|
-
response_data = {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
}
|
|
225
|
+
if response.updated_permissions is not None:
|
|
226
|
+
response_data["updatedPermissions"] = [
|
|
227
|
+
permission.to_dict()
|
|
228
|
+
for permission in response.updated_permissions
|
|
229
|
+
]
|
|
220
230
|
elif isinstance(response, PermissionResultDeny):
|
|
221
|
-
response_data = {"
|
|
222
|
-
|
|
231
|
+
response_data = {"behavior": "deny", "message": response.message}
|
|
232
|
+
if response.interrupt:
|
|
233
|
+
response_data["interrupt"] = response.interrupt
|
|
223
234
|
else:
|
|
224
235
|
raise TypeError(
|
|
225
236
|
f"Tool permission callback must return PermissionResult (PermissionResultAllow or PermissionResultDeny), got {type(response)}"
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import shutil
|
|
8
|
+
import sys
|
|
7
9
|
from collections.abc import AsyncIterable, AsyncIterator
|
|
8
10
|
from contextlib import suppress
|
|
9
11
|
from dataclasses import asdict
|
|
@@ -25,6 +27,7 @@ from . import Transport
|
|
|
25
27
|
logger = logging.getLogger(__name__)
|
|
26
28
|
|
|
27
29
|
_DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
|
|
30
|
+
MINIMUM_CLAUDE_CODE_VERSION = "2.0.0"
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
class SubprocessCLITransport(Transport):
|
|
@@ -202,6 +205,8 @@ class SubprocessCLITransport(Transport):
|
|
|
202
205
|
if self._process:
|
|
203
206
|
return
|
|
204
207
|
|
|
208
|
+
await self._check_claude_version()
|
|
209
|
+
|
|
205
210
|
cmd = self._build_command()
|
|
206
211
|
try:
|
|
207
212
|
# Merge environment variables: system -> user -> SDK required
|
|
@@ -448,6 +453,46 @@ class SubprocessCLITransport(Transport):
|
|
|
448
453
|
)
|
|
449
454
|
raise self._exit_error
|
|
450
455
|
|
|
456
|
+
async def _check_claude_version(self) -> None:
|
|
457
|
+
"""Check Claude Code version and warn if below minimum."""
|
|
458
|
+
version_process = None
|
|
459
|
+
try:
|
|
460
|
+
with anyio.fail_after(2): # 2 second timeout
|
|
461
|
+
version_process = await anyio.open_process(
|
|
462
|
+
[self._cli_path, "-v"],
|
|
463
|
+
stdout=PIPE,
|
|
464
|
+
stderr=PIPE,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if version_process.stdout:
|
|
468
|
+
stdout_bytes = await version_process.stdout.receive()
|
|
469
|
+
version_output = stdout_bytes.decode().strip()
|
|
470
|
+
|
|
471
|
+
match = re.match(r"([0-9]+\.[0-9]+\.[0-9]+)", version_output)
|
|
472
|
+
if match:
|
|
473
|
+
version = match.group(1)
|
|
474
|
+
version_parts = [int(x) for x in version.split(".")]
|
|
475
|
+
min_parts = [
|
|
476
|
+
int(x) for x in MINIMUM_CLAUDE_CODE_VERSION.split(".")
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
if version_parts < min_parts:
|
|
480
|
+
warning = (
|
|
481
|
+
f"Warning: Claude Code version {version} is unsupported in the Agent SDK. "
|
|
482
|
+
f"Minimum required version is {MINIMUM_CLAUDE_CODE_VERSION}. "
|
|
483
|
+
"Some features may not work correctly."
|
|
484
|
+
)
|
|
485
|
+
logger.warning(warning)
|
|
486
|
+
print(warning, file=sys.stderr)
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
finally:
|
|
490
|
+
if version_process:
|
|
491
|
+
with suppress(Exception):
|
|
492
|
+
version_process.terminate()
|
|
493
|
+
with suppress(Exception):
|
|
494
|
+
await version_process.wait()
|
|
495
|
+
|
|
451
496
|
def is_ready(self) -> bool:
|
|
452
497
|
"""Check if transport is ready for communication."""
|
|
453
498
|
return self._ready
|
|
@@ -6,6 +6,7 @@ from collections.abc import AsyncIterable, AsyncIterator
|
|
|
6
6
|
from dataclasses import replace
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from . import Transport
|
|
9
10
|
from ._errors import CLIConnectionError
|
|
10
11
|
from .types import ClaudeAgentOptions, HookEvent, HookMatcher, Message, ResultMessage
|
|
11
12
|
|
|
@@ -51,12 +52,17 @@ class ClaudeSDKClient:
|
|
|
51
52
|
exist.
|
|
52
53
|
"""
|
|
53
54
|
|
|
54
|
-
def __init__(
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
options: ClaudeAgentOptions | None = None,
|
|
58
|
+
transport: Transport | None = None,
|
|
59
|
+
):
|
|
55
60
|
"""Initialize Claude SDK client."""
|
|
56
61
|
if options is None:
|
|
57
62
|
options = ClaudeAgentOptions()
|
|
58
63
|
self.options = options
|
|
59
|
-
self.
|
|
64
|
+
self._custom_transport = transport
|
|
65
|
+
self._transport: Transport | None = None
|
|
60
66
|
self._query: Any | None = None
|
|
61
67
|
os.environ["CLAUDE_CODE_ENTRYPOINT"] = "sdk-py-client"
|
|
62
68
|
|
|
@@ -115,10 +121,14 @@ class ClaudeSDKClient:
|
|
|
115
121
|
else:
|
|
116
122
|
options = self.options
|
|
117
123
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
# Use provided custom transport or create subprocess transport
|
|
125
|
+
if self._custom_transport:
|
|
126
|
+
self._transport = self._custom_transport
|
|
127
|
+
else:
|
|
128
|
+
self._transport = SubprocessCLITransport(
|
|
129
|
+
prompt=actual_prompt,
|
|
130
|
+
options=options,
|
|
131
|
+
)
|
|
122
132
|
await self._transport.connect()
|
|
123
133
|
|
|
124
134
|
# Extract SDK MCP servers from options
|
|
@@ -222,7 +232,7 @@ class ClaudeSDKClient:
|
|
|
222
232
|
|
|
223
233
|
Args:
|
|
224
234
|
model: The model to use, or None to use default. Examples:
|
|
225
|
-
- 'claude-sonnet-4-
|
|
235
|
+
- 'claude-sonnet-4-5'
|
|
226
236
|
- 'claude-opus-4-1-20250805'
|
|
227
237
|
- 'claude-opus-4-20250514'
|
|
228
238
|
|
|
@@ -233,7 +243,7 @@ class ClaudeSDKClient:
|
|
|
233
243
|
await client.query("Help me understand this problem")
|
|
234
244
|
|
|
235
245
|
# Switch to a different model for implementation
|
|
236
|
-
await client.set_model('claude-
|
|
246
|
+
await client.set_model('claude-sonnet-4-5')
|
|
237
247
|
await client.query("Now implement the solution")
|
|
238
248
|
```
|
|
239
249
|
"""
|
|
@@ -70,6 +70,42 @@ class PermissionUpdate:
|
|
|
70
70
|
directories: list[str] | None = None
|
|
71
71
|
destination: PermissionUpdateDestination | None = None
|
|
72
72
|
|
|
73
|
+
def to_dict(self) -> dict[str, Any]:
|
|
74
|
+
"""Convert PermissionUpdate to dictionary format matching TypeScript control protocol."""
|
|
75
|
+
result: dict[str, Any] = {
|
|
76
|
+
"type": self.type,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Add destination for all variants
|
|
80
|
+
if self.destination is not None:
|
|
81
|
+
result["destination"] = self.destination
|
|
82
|
+
|
|
83
|
+
# Handle different type variants
|
|
84
|
+
if self.type in ["addRules", "replaceRules", "removeRules"]:
|
|
85
|
+
# Rules-based variants require rules and behavior
|
|
86
|
+
if self.rules is not None:
|
|
87
|
+
result["rules"] = [
|
|
88
|
+
{
|
|
89
|
+
"toolName": rule.tool_name,
|
|
90
|
+
"ruleContent": rule.rule_content,
|
|
91
|
+
}
|
|
92
|
+
for rule in self.rules
|
|
93
|
+
]
|
|
94
|
+
if self.behavior is not None:
|
|
95
|
+
result["behavior"] = self.behavior
|
|
96
|
+
|
|
97
|
+
elif self.type == "setMode":
|
|
98
|
+
# Mode variant requires mode
|
|
99
|
+
if self.mode is not None:
|
|
100
|
+
result["mode"] = self.mode
|
|
101
|
+
|
|
102
|
+
elif self.type in ["addDirectories", "removeDirectories"]:
|
|
103
|
+
# Directory variants require directories
|
|
104
|
+
if self.directories is not None:
|
|
105
|
+
result["directories"] = self.directories
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
|
|
73
109
|
|
|
74
110
|
# Tool callback types
|
|
75
111
|
@dataclass
|
|
@@ -121,18 +157,74 @@ HookEvent = (
|
|
|
121
157
|
)
|
|
122
158
|
|
|
123
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
|
+
|
|
124
199
|
# See https://docs.anthropic.com/en/docs/claude-code/hooks#advanced%3A-json-output
|
|
125
|
-
# for documentation of the output types.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
129
219
|
decision: NotRequired[Literal["block"]]
|
|
130
|
-
# Optionally add a system message that is not visible to Claude but saved in
|
|
131
|
-
# the chat transcript.
|
|
132
220
|
systemMessage: NotRequired[str]
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
221
|
+
reason: NotRequired[str]
|
|
222
|
+
|
|
223
|
+
# Hook-specific outputs
|
|
224
|
+
hookSpecificOutput: NotRequired[HookSpecificOutput]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput
|
|
136
228
|
|
|
137
229
|
|
|
138
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
|
-
|
|
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
|
|
641
|
+
# Mock _find_cli to return the test script path directly
|
|
640
642
|
with patch.object(
|
|
641
|
-
SubprocessCLITransport, "_find_cli", return_value=
|
|
643
|
+
SubprocessCLITransport, "_find_cli", return_value=test_script
|
|
642
644
|
):
|
|
643
|
-
# Mock _build_command to
|
|
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
|
-
#
|
|
650
|
-
|
|
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,
|
|
@@ -90,7 +93,7 @@ class TestToolPermissionCallbacks:
|
|
|
90
93
|
# Check response was sent
|
|
91
94
|
assert len(transport.written_messages) == 1
|
|
92
95
|
response = transport.written_messages[0]
|
|
93
|
-
assert '"
|
|
96
|
+
assert '"behavior": "allow"' in response
|
|
94
97
|
|
|
95
98
|
@pytest.mark.asyncio
|
|
96
99
|
async def test_permission_callback_deny(self):
|
|
@@ -125,8 +128,8 @@ class TestToolPermissionCallbacks:
|
|
|
125
128
|
# Check response
|
|
126
129
|
assert len(transport.written_messages) == 1
|
|
127
130
|
response = transport.written_messages[0]
|
|
128
|
-
assert '"
|
|
129
|
-
assert '"
|
|
131
|
+
assert '"behavior": "deny"' in response
|
|
132
|
+
assert '"message": "Security policy violation"' in response
|
|
130
133
|
|
|
131
134
|
@pytest.mark.asyncio
|
|
132
135
|
async def test_permission_callback_input_modification(self):
|
|
@@ -164,7 +167,7 @@ class TestToolPermissionCallbacks:
|
|
|
164
167
|
# Check response includes modified input
|
|
165
168
|
assert len(transport.written_messages) == 1
|
|
166
169
|
response = transport.written_messages[0]
|
|
167
|
-
assert '"
|
|
170
|
+
assert '"behavior": "allow"' in response
|
|
168
171
|
assert '"safe_mode": true' in response
|
|
169
172
|
|
|
170
173
|
@pytest.mark.asyncio
|
|
@@ -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=
|
|
51
|
+
cli_path=path,
|
|
51
52
|
)
|
|
52
53
|
|
|
53
|
-
|
|
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."""
|
|
@@ -106,7 +108,7 @@ class TestSubprocessCLITransport:
|
|
|
106
108
|
options=ClaudeAgentOptions(
|
|
107
109
|
allowed_tools=["Read", "Write"],
|
|
108
110
|
disallowed_tools=["Bash"],
|
|
109
|
-
model="claude-
|
|
111
|
+
model="claude-sonnet-4-5",
|
|
110
112
|
permission_mode="acceptEdits",
|
|
111
113
|
max_turns=5,
|
|
112
114
|
),
|
|
@@ -119,7 +121,7 @@ class TestSubprocessCLITransport:
|
|
|
119
121
|
assert "--disallowedTools" in cmd
|
|
120
122
|
assert "Bash" in cmd
|
|
121
123
|
assert "--model" in cmd
|
|
122
|
-
assert "claude-
|
|
124
|
+
assert "claude-sonnet-4-5" in cmd
|
|
123
125
|
assert "--permission-mode" in cmd
|
|
124
126
|
assert "acceptEdits" in cmd
|
|
125
127
|
assert "--max-turns" in cmd
|
|
@@ -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
|
|
144
|
-
assert "--add-dir
|
|
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."""
|
|
@@ -163,6 +171,16 @@ class TestSubprocessCLITransport:
|
|
|
163
171
|
|
|
164
172
|
async def _test():
|
|
165
173
|
with patch("anyio.open_process") as mock_exec:
|
|
174
|
+
# Mock version check process
|
|
175
|
+
mock_version_process = MagicMock()
|
|
176
|
+
mock_version_process.stdout = MagicMock()
|
|
177
|
+
mock_version_process.stdout.receive = AsyncMock(
|
|
178
|
+
return_value=b"2.0.0 (Claude Code)"
|
|
179
|
+
)
|
|
180
|
+
mock_version_process.terminate = MagicMock()
|
|
181
|
+
mock_version_process.wait = AsyncMock()
|
|
182
|
+
|
|
183
|
+
# Mock main process
|
|
166
184
|
mock_process = MagicMock()
|
|
167
185
|
mock_process.returncode = None
|
|
168
186
|
mock_process.terminate = MagicMock()
|
|
@@ -175,7 +193,8 @@ class TestSubprocessCLITransport:
|
|
|
175
193
|
mock_stdin.aclose = AsyncMock()
|
|
176
194
|
mock_process.stdin = mock_stdin
|
|
177
195
|
|
|
178
|
-
|
|
196
|
+
# Return version process first, then main process
|
|
197
|
+
mock_exec.side_effect = [mock_version_process, mock_process]
|
|
179
198
|
|
|
180
199
|
transport = SubprocessCLITransport(
|
|
181
200
|
prompt="test",
|
|
@@ -311,28 +330,31 @@ class TestSubprocessCLITransport:
|
|
|
311
330
|
from pathlib import Path
|
|
312
331
|
|
|
313
332
|
# Test with string path
|
|
333
|
+
string_path = "/path/to/mcp-config.json"
|
|
314
334
|
transport = SubprocessCLITransport(
|
|
315
335
|
prompt="test",
|
|
316
|
-
options=ClaudeAgentOptions(mcp_servers=
|
|
336
|
+
options=ClaudeAgentOptions(mcp_servers=string_path),
|
|
317
337
|
cli_path="/usr/bin/claude",
|
|
318
338
|
)
|
|
319
339
|
|
|
320
340
|
cmd = transport._build_command()
|
|
321
341
|
assert "--mcp-config" in cmd
|
|
322
342
|
mcp_idx = cmd.index("--mcp-config")
|
|
323
|
-
assert cmd[mcp_idx + 1] ==
|
|
343
|
+
assert cmd[mcp_idx + 1] == string_path
|
|
324
344
|
|
|
325
345
|
# Test with Path object
|
|
346
|
+
path_obj = Path("/path/to/mcp-config.json")
|
|
326
347
|
transport = SubprocessCLITransport(
|
|
327
348
|
prompt="test",
|
|
328
|
-
options=ClaudeAgentOptions(mcp_servers=
|
|
349
|
+
options=ClaudeAgentOptions(mcp_servers=path_obj),
|
|
329
350
|
cli_path="/usr/bin/claude",
|
|
330
351
|
)
|
|
331
352
|
|
|
332
353
|
cmd = transport._build_command()
|
|
333
354
|
assert "--mcp-config" in cmd
|
|
334
355
|
mcp_idx = cmd.index("--mcp-config")
|
|
335
|
-
|
|
356
|
+
# Path object gets converted to string, compare with str(path_obj)
|
|
357
|
+
assert cmd[mcp_idx + 1] == str(path_obj)
|
|
336
358
|
|
|
337
359
|
def test_build_command_with_mcp_servers_as_json_string(self):
|
|
338
360
|
"""Test building CLI command with mcp_servers as JSON string."""
|
|
@@ -363,13 +385,25 @@ class TestSubprocessCLITransport:
|
|
|
363
385
|
with patch(
|
|
364
386
|
"anyio.open_process", new_callable=AsyncMock
|
|
365
387
|
) as mock_open_process:
|
|
388
|
+
# Mock version check process
|
|
389
|
+
mock_version_process = MagicMock()
|
|
390
|
+
mock_version_process.stdout = MagicMock()
|
|
391
|
+
mock_version_process.stdout.receive = AsyncMock(
|
|
392
|
+
return_value=b"2.0.0 (Claude Code)"
|
|
393
|
+
)
|
|
394
|
+
mock_version_process.terminate = MagicMock()
|
|
395
|
+
mock_version_process.wait = AsyncMock()
|
|
396
|
+
|
|
397
|
+
# Mock main process
|
|
366
398
|
mock_process = MagicMock()
|
|
367
399
|
mock_process.stdout = MagicMock()
|
|
368
400
|
mock_stdin = MagicMock()
|
|
369
401
|
mock_stdin.aclose = AsyncMock() # Add async aclose method
|
|
370
402
|
mock_process.stdin = mock_stdin
|
|
371
403
|
mock_process.returncode = None
|
|
372
|
-
|
|
404
|
+
|
|
405
|
+
# Return version process first, then main process
|
|
406
|
+
mock_open_process.side_effect = [mock_version_process, mock_process]
|
|
373
407
|
|
|
374
408
|
transport = SubprocessCLITransport(
|
|
375
409
|
prompt="test",
|
|
@@ -379,11 +413,13 @@ class TestSubprocessCLITransport:
|
|
|
379
413
|
|
|
380
414
|
await transport.connect()
|
|
381
415
|
|
|
382
|
-
# Verify open_process was called
|
|
383
|
-
mock_open_process.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
416
|
+
# Verify open_process was called twice (version check + main process)
|
|
417
|
+
assert mock_open_process.call_count == 2
|
|
418
|
+
|
|
419
|
+
# Check the second call (main process) for env vars
|
|
420
|
+
second_call_kwargs = mock_open_process.call_args_list[1].kwargs
|
|
421
|
+
assert "env" in second_call_kwargs
|
|
422
|
+
env_passed = second_call_kwargs["env"]
|
|
387
423
|
|
|
388
424
|
# Check that custom env var was passed
|
|
389
425
|
assert env_passed["MY_TEST_VAR"] == test_value
|
|
@@ -410,13 +446,25 @@ class TestSubprocessCLITransport:
|
|
|
410
446
|
with patch(
|
|
411
447
|
"anyio.open_process", new_callable=AsyncMock
|
|
412
448
|
) as mock_open_process:
|
|
449
|
+
# Mock version check process
|
|
450
|
+
mock_version_process = MagicMock()
|
|
451
|
+
mock_version_process.stdout = MagicMock()
|
|
452
|
+
mock_version_process.stdout.receive = AsyncMock(
|
|
453
|
+
return_value=b"2.0.0 (Claude Code)"
|
|
454
|
+
)
|
|
455
|
+
mock_version_process.terminate = MagicMock()
|
|
456
|
+
mock_version_process.wait = AsyncMock()
|
|
457
|
+
|
|
458
|
+
# Mock main process
|
|
413
459
|
mock_process = MagicMock()
|
|
414
460
|
mock_process.stdout = MagicMock()
|
|
415
461
|
mock_stdin = MagicMock()
|
|
416
462
|
mock_stdin.aclose = AsyncMock() # Add async aclose method
|
|
417
463
|
mock_process.stdin = mock_stdin
|
|
418
464
|
mock_process.returncode = None
|
|
419
|
-
|
|
465
|
+
|
|
466
|
+
# Return version process first, then main process
|
|
467
|
+
mock_open_process.side_effect = [mock_version_process, mock_process]
|
|
420
468
|
|
|
421
469
|
transport = SubprocessCLITransport(
|
|
422
470
|
prompt="test",
|
|
@@ -426,11 +474,13 @@ class TestSubprocessCLITransport:
|
|
|
426
474
|
|
|
427
475
|
await transport.connect()
|
|
428
476
|
|
|
429
|
-
# Verify open_process was called
|
|
430
|
-
mock_open_process.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
477
|
+
# Verify open_process was called twice (version check + main process)
|
|
478
|
+
assert mock_open_process.call_count == 2
|
|
479
|
+
|
|
480
|
+
# Check the second call (main process) for user
|
|
481
|
+
second_call_kwargs = mock_open_process.call_args_list[1].kwargs
|
|
482
|
+
assert "user" in second_call_kwargs
|
|
483
|
+
user_passed = second_call_kwargs["user"]
|
|
434
484
|
|
|
435
485
|
# Check that user was passed
|
|
436
486
|
assert user_passed == "claude"
|
|
@@ -145,7 +145,7 @@ class TestOptions:
|
|
|
145
145
|
def test_claude_code_options_with_model_specification(self):
|
|
146
146
|
"""Test Options with model specification."""
|
|
147
147
|
options = ClaudeAgentOptions(
|
|
148
|
-
model="claude-
|
|
148
|
+
model="claude-sonnet-4-5", permission_prompt_tool_name="CustomTool"
|
|
149
149
|
)
|
|
150
|
-
assert options.model == "claude-
|
|
150
|
+
assert options.model == "claude-sonnet-4-5"
|
|
151
151
|
assert options.permission_prompt_tool_name == "CustomTool"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_agent_sdk-0.1.0 → claude_agent_sdk-0.1.2}/src/claude_agent_sdk/_internal/message_parser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|