clawd-code-sdk 0.1.76__tar.gz → 0.1.78__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.
Files changed (50) hide show
  1. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/PKG-INFO +1 -1
  2. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/pyproject.toml +1 -1
  3. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_errors.py +33 -0
  4. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/message_parser.py +0 -2
  5. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/query.py +22 -20
  6. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/transport/subprocess_cli.py +10 -13
  7. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/client.py +12 -8
  8. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/__init__.py +10 -0
  9. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/base.py +9 -0
  10. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/hooks.py +12 -0
  11. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/input_types.py +22 -2
  12. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/mcp.py +65 -4
  13. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/messages.py +4 -2
  14. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/options.py +2 -2
  15. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/server_info.py +21 -9
  16. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/storage/helpers.py +8 -37
  17. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_client.py +150 -2
  18. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/.gitignore +0 -0
  19. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/LICENSE +0 -0
  20. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/README.md +0 -0
  21. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/__init__.py +0 -0
  22. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_bundled/.gitignore +0 -0
  23. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/__init__.py +0 -0
  24. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/transport/__init__.py +0 -0
  25. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_version.py +0 -0
  26. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/anthropic_types.py +0 -0
  27. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/mcp_utils.py +0 -0
  28. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/agents.py +0 -0
  29. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/content_blocks.py +0 -0
  30. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/control.py +0 -0
  31. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/output_types.py +0 -0
  32. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/permissions.py +0 -0
  33. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/sandbox.py +0 -0
  34. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/tool_use_results.py +0 -0
  35. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/py.typed +0 -0
  36. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/query.py +0 -0
  37. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/storage/ARCHITECTURE.md +0 -0
  38. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/storage/__init__.py +0 -0
  39. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/storage/models.py +0 -0
  40. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/usage.py +0 -0
  41. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/conftest.py +0 -0
  42. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_changelog.py +0 -0
  43. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_errors.py +0 -0
  44. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_integration.py +0 -0
  45. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_message_parser.py +0 -0
  46. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_sdk_mcp_integration.py +0 -0
  47. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_streaming_client.py +0 -0
  48. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_subprocess_buffering.py +0 -0
  49. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_tool_callbacks.py +0 -0
  50. {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_transport.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clawd-code-sdk
3
- Version: 0.1.76
3
+ Version: 0.1.78
4
4
  Summary: Python SDK for Claude Code
5
5
  Project-URL: Documentation, https://github.com/phil65/claude-agent-sdk-python
6
6
  Project-URL: Homepage, https://github.com/phil65/claude-agent-sdk-python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "clawd-code-sdk"
3
- version = "0.1.76"
3
+ version = "0.1.78"
4
4
  description = "Python SDK for Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -142,3 +142,36 @@ class ServerError(APIError):
142
142
 
143
143
  def __init__(self, message: str, model: str | None = None):
144
144
  super().__init__(message, "server_error", model)
145
+
146
+
147
+ class ControlRequestError(ClaudeSDKError):
148
+ """Raised when a control protocol request fails."""
149
+
150
+ def __init__(self, message: str, subtype: str | None = None):
151
+ self.subtype = subtype
152
+ super().__init__(message)
153
+
154
+
155
+ class ControlRequestTimeoutError(ControlRequestError):
156
+ """Raised when a control protocol request times out."""
157
+
158
+
159
+ class McpError(ControlRequestError):
160
+ """Raised when an MCP-related control request fails.
161
+
162
+ Attributes:
163
+ error_code: The JSON-RPC error code from the MCP server.
164
+ server_name: The name of the MCP server, if known.
165
+ """
166
+
167
+ def __init__(
168
+ self,
169
+ message: str,
170
+ *,
171
+ error_code: int | None = None,
172
+ server_name: str | None = None,
173
+ subtype: str | None = None,
174
+ ):
175
+ self.error_code = error_code
176
+ self.server_name = server_name
177
+ super().__init__(message, subtype=subtype)
@@ -92,8 +92,6 @@ def parse_message(data: dict[str, Any]) -> Message:
92
92
  return ToolUseSummaryMessage(**summary_data)
93
93
  case {"type": "auth_status", **auth_data}:
94
94
  # Convert camelCase isAuthenticating to snake_case
95
- if "isAuthenticating" in auth_data:
96
- auth_data["is_authenticating"] = auth_data.pop("isAuthenticating")
97
95
  return AuthStatusMessage(**auth_data)
98
96
  case {"type": unknown_type}:
99
97
  raise MessageParseError(f"Unknown message type: {unknown_type}", data)
@@ -12,6 +12,7 @@ import anyenv
12
12
  import anyio
13
13
  from pydantic import BaseModel
14
14
 
15
+ from clawd_code_sdk._errors import ControlRequestError, ControlRequestTimeoutError
15
16
  from clawd_code_sdk.models import (
16
17
  ControlResponse,
17
18
  PermissionResultAllow,
@@ -27,6 +28,7 @@ from clawd_code_sdk.models import (
27
28
  ToolPermissionContext,
28
29
  parse_control_request,
29
30
  )
31
+ from clawd_code_sdk.models.control import ControlErrorResponse
30
32
  from clawd_code_sdk.models.mcp import JSONRPCError, JSONRPCErrorResponse, JSONRPCResultResponse
31
33
  from clawd_code_sdk.models.server_info import ClaudeCodeServerInfo
32
34
 
@@ -98,6 +100,7 @@ class Query:
98
100
  system_prompt: str | None = None,
99
101
  append_system_prompt: str | None = None,
100
102
  json_schema: dict[str, Any] | None = None,
103
+ prompt_suggestions: bool | None = None,
101
104
  ):
102
105
  """Initialize Query with transport and callbacks.
103
106
 
@@ -111,6 +114,7 @@ class Query:
111
114
  system_prompt: Optional system prompt to send via initialize
112
115
  append_system_prompt: Optional text to append to preset system prompt
113
116
  json_schema: Optional JSON schema for structured output
117
+ prompt_suggestions: Optional flag to enable prompt suggestions
114
118
  """
115
119
  self._initialize_timeout = initialize_timeout
116
120
  self.transport = transport
@@ -121,6 +125,7 @@ class Query:
121
125
  self._system_prompt = system_prompt
122
126
  self._append_system_prompt = append_system_prompt
123
127
  self._json_schema = json_schema
128
+ self._prompt_suggestions = prompt_suggestions
124
129
  # Control protocol state
125
130
  self.pending_control_responses: dict[str, anyio.Event] = {}
126
131
  self.pending_control_results: dict[str, dict[str, Any] | Exception] = {}
@@ -184,6 +189,8 @@ class Query:
184
189
  request["appendSystemPrompt"] = self._append_system_prompt
185
190
  if self._json_schema is not None:
186
191
  request["jsonSchema"] = self._json_schema
192
+ if self._prompt_suggestions is not None:
193
+ request["promptSuggestions"] = self._prompt_suggestions
187
194
 
188
195
  # Use longer timeout for initialize since MCP servers may take time to start
189
196
  response = await self._send_control_request(request, timeout=self._initialize_timeout)
@@ -234,7 +241,9 @@ class Query:
234
241
  event = self.pending_control_responses[request_id]
235
242
  if response.get("subtype") == "error":
236
243
  msg = response.get("error", "Unknown error")
237
- self.pending_control_results[request_id] = Exception(msg)
244
+ self.pending_control_results[request_id] = ControlRequestError(
245
+ msg, subtype=response.get("subtype")
246
+ )
238
247
  else:
239
248
  self.pending_control_results[request_id] = response
240
249
  event.set()
@@ -304,10 +313,8 @@ class Query:
304
313
  await self.transport.write(anyenv.dump_json(success_response) + "\n")
305
314
 
306
315
  except Exception as e:
307
- error_response = SDKControlResponse(
308
- type="control_response",
309
- response={"subtype": "error", "request_id": request_id, "error": str(e)},
310
- )
316
+ response = ControlErrorResponse(subtype="error", request_id=request_id, error=str(e))
317
+ error_response = SDKControlResponse(type="control_response", response=response)
311
318
  await self.transport.write(anyenv.dump_json(error_response) + "\n")
312
319
 
313
320
  async def _handle_permission_request(
@@ -315,8 +322,7 @@ class Query:
315
322
  ) -> PermissionResult:
316
323
  """Handle a tool permission request."""
317
324
  if not self.can_use_tool:
318
- msg = "canUseTool callback is not provided"
319
- raise RuntimeError(msg)
325
+ raise RuntimeError("canUseTool callback is not provided")
320
326
 
321
327
  context = ToolPermissionContext(
322
328
  tool_use_id=req.tool_use_id,
@@ -332,10 +338,8 @@ class Query:
332
338
 
333
339
  async def _handle_hook_callback(self, req: SDKHookCallbackRequest) -> dict[str, Any]:
334
340
  """Handle a hook callback request."""
335
- callback = self.hook_callbacks.get(req.callback_id)
336
- if not callback:
337
- msg = f"No hook callback found for ID: {req.callback_id}"
338
- raise RuntimeError(msg)
341
+ if not (callback := self.hook_callbacks.get(req.callback_id)):
342
+ raise RuntimeError(f"No hook callback found for ID: {req.callback_id}")
339
343
 
340
344
  hook_output = await callback(req.input, req.tool_use_id, {"signal": None})
341
345
  # Strip trailing underscores from Python-safe names (async_, continue_) for CLI
@@ -375,7 +379,10 @@ class Query:
375
379
  except TimeoutError as e:
376
380
  self.pending_control_responses.pop(request_id, None)
377
381
  self.pending_control_results.pop(request_id, None)
378
- raise Exception(f"Control request timeout: {request.get('subtype')}") from e
382
+ subtype = request.get("subtype")
383
+ raise ControlRequestTimeoutError(
384
+ f"Control request timeout: {subtype}", subtype=subtype
385
+ ) from e
379
386
 
380
387
  async def _handle_sdk_mcp_request(
381
388
  self, server_name: str, message: JSONRPCMessage
@@ -418,12 +425,7 @@ class Query:
418
425
  await self._send_control_request({"subtype": "mcp_reconnect", "serverName": server_name})
419
426
 
420
427
  async def mcp_toggle(self, server_name: str, *, enabled: bool) -> None:
421
- """Enable or disable an MCP server.
422
-
423
- Args:
424
- server_name: Name of the MCP server to toggle
425
- enabled: Whether the server should be enabled
426
- """
428
+ """Enable or disable an MCP server."""
427
429
  await self._send_control_request(
428
430
  {"subtype": "mcp_toggle", "serverName": server_name, "enabled": enabled}
429
431
  )
@@ -605,12 +607,12 @@ async def process_mcp_request(message: JSONRPCMessage, server: McpServer) -> JSO
605
607
  match method:
606
608
  case "initialize":
607
609
  # Handle MCP initialization - hardcoded for tools only, no listChanged
608
- result = {
610
+ init_result = {
609
611
  "protocolVersion": "2024-11-05",
610
612
  "capabilities": {"tools": {}}, # Tools capability without listChanged
611
613
  "serverInfo": {"name": server.name, "version": server.version or "1.0.0"},
612
614
  }
613
- return JSONRPCResultResponse(jsonrpc="2.0", id=msg_id, result=result)
615
+ return JSONRPCResultResponse(jsonrpc="2.0", id=msg_id, result=init_result)
614
616
 
615
617
  case "tools/list" if handler := server.request_handlers.get(ListToolsRequest):
616
618
  request = ListToolsRequest()
@@ -161,15 +161,18 @@ class SubprocessCLITransport(Transport):
161
161
  if self._options.resume_session_at:
162
162
  cmd.extend(["--resume-session-at", self._options.resume_session_at])
163
163
 
164
- if self._options.debug or self._options.debug_file:
165
- cmd.append("--debug")
166
-
167
164
  if self._options.debug_file:
168
165
  cmd.extend(["--debug-file", self._options.debug_file])
169
166
 
170
167
  if self._options.strict_mcp_config:
171
168
  cmd.append("--strict-mcp-config")
172
169
 
170
+ match self._options.worktree:
171
+ case True:
172
+ cmd.append("--worktree")
173
+ case str():
174
+ cmd.extend(["--worktree", self._options.worktree])
175
+
173
176
  sources_value = ",".join(self._options.setting_sources or [])
174
177
  cmd.extend(["--setting-sources", sources_value])
175
178
 
@@ -286,7 +289,6 @@ class SubprocessCLITransport(Transport):
286
289
 
287
290
  # Always collect stderr lines for error reporting
288
291
  self._stderr_lines.append(line_str)
289
-
290
292
  # Call the stderr callback if provided
291
293
  if self._options.stderr:
292
294
  self._options.stderr(line_str)
@@ -401,13 +403,11 @@ class SubprocessCLITransport(Transport):
401
403
  json_buffer = ""
402
404
  raise SDKJSONDecodeError(
403
405
  f"JSON message exceeded maximum buffer size of {self._max_buffer_size} bytes",
404
- ValueError(
405
- f"Buffer size {buffer_length} exceeds limit {self._max_buffer_size}"
406
- ),
406
+ ValueError(f"{buffer_length=} exceeds {self._max_buffer_size=}"),
407
407
  )
408
408
 
409
409
  try:
410
- data = anyenv.load_json(json_buffer)
410
+ data = anyenv.load_json(json_buffer, return_type=dict)
411
411
  json_buffer = ""
412
412
  yield data
413
413
  except anyenv.JsonLoadError:
@@ -439,11 +439,8 @@ class SubprocessCLITransport(Transport):
439
439
  # Use exit code for error detection
440
440
  if returncode is not None and returncode != 0:
441
441
  stderr_output = "\n".join(self._stderr_lines) if self._stderr_lines else None
442
- self._exit_error = ProcessError(
443
- f"Command failed with exit code {returncode}",
444
- exit_code=returncode,
445
- stderr=stderr_output,
446
- )
442
+ msg = f"Command failed with exit code {returncode}"
443
+ self._exit_error = ProcessError(msg, exit_code=returncode, stderr=stderr_output)
447
444
  raise self._exit_error
448
445
 
449
446
 
@@ -10,7 +10,13 @@ from typing import TYPE_CHECKING, Any
10
10
  import anyenv
11
11
 
12
12
  from clawd_code_sdk._errors import CLIConnectionError
13
- from clawd_code_sdk.models import ClaudeAgentOptions, ResultMessage, UserPromptMessage
13
+ from clawd_code_sdk.models import (
14
+ ClaudeAgentOptions,
15
+ McpStdioServerConfig,
16
+ ResultMessage,
17
+ UserPromptMessage,
18
+ )
19
+ from clawd_code_sdk.models.mcp import McpStatusResponse
14
20
  from clawd_code_sdk.models.messages import AssistantMessage
15
21
 
16
22
 
@@ -136,6 +142,7 @@ class ClaudeSDKClient:
136
142
  system_prompt=system_prompt,
137
143
  append_system_prompt=append_system_prompt,
138
144
  json_schema=json_schema,
145
+ prompt_suggestions=self.options.prompt_suggestions,
139
146
  )
140
147
  # Start reading messages and initialize
141
148
  await self._query.start()
@@ -246,19 +253,16 @@ class ClaudeSDKClient:
246
253
  query = self._ensure_connected()
247
254
  await query.rewind_files(user_message_id)
248
255
 
249
- async def get_mcp_status(self) -> dict[str, Any]:
256
+ async def get_mcp_status(self) -> McpStatusResponse:
250
257
  """Get current MCP server connection status.
251
258
 
252
259
  Returns:
253
- Dictionary with MCP server status information. Contains a
254
- 'mcpServers' key with a list of server status objects, each having:
255
- - 'name': Server name (str)
256
- - 'status': Connection status ('connected', 'pending', 'failed',
257
- 'needs-auth', 'disabled')
260
+ Validated MCP status response containing server statuses,
261
+ configurations, tools, and connection information.
258
262
  """
259
263
  query = self._ensure_connected()
260
264
  result = await query.get_mcp_status()
261
- return result
265
+ return McpStatusResponse.model_validate(result)
262
266
 
263
267
  async def set_mcp_servers(self, servers: dict[str, McpServerConfig]) -> dict[str, Any]:
264
268
  """Add, replace, or remove MCP servers dynamically mid-session.
@@ -112,11 +112,16 @@ from .mcp import (
112
112
  JSONRPCRequest,
113
113
  JSONRPCResponse,
114
114
  JSONRPCResultResponse,
115
+ McpConnectionStatus,
115
116
  McpHttpServerConfig,
116
117
  McpSdkServerConfig,
117
118
  McpServerConfig,
119
+ McpServerStatusEntry,
120
+ McpServerVersionInfo,
118
121
  McpSSEServerConfig,
122
+ McpStatusResponse,
119
123
  McpStdioServerConfig,
124
+ McpToolStatus,
120
125
  RequestId,
121
126
  SdkPluginConfig,
122
127
  )
@@ -289,11 +294,16 @@ __all__ = [
289
294
  "JSONRPCRequest",
290
295
  "JSONRPCResponse",
291
296
  "JSONRPCResultResponse",
297
+ "McpConnectionStatus",
292
298
  "McpHttpServerConfig",
293
299
  "McpSdkServerConfig",
294
300
  "McpServerConfig",
301
+ "McpServerStatusEntry",
302
+ "McpServerVersionInfo",
295
303
  "McpSSEServerConfig",
304
+ "McpStatusResponse",
296
305
  "McpStdioServerConfig",
306
+ "McpToolStatus",
297
307
  "RequestId",
298
308
  "SdkPluginConfig",
299
309
  # sandbox
@@ -4,6 +4,15 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Literal, TypedDict
6
6
 
7
+ from pydantic import BaseModel, ConfigDict
8
+ from pydantic.alias_generators import to_camel
9
+
10
+
11
+ class ClaudeCodeBaseModel(BaseModel):
12
+ """Base model for all Claude Code Pydantic models."""
13
+
14
+ model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel, extra="forbid")
15
+
7
16
 
8
17
  # Permission modes
9
18
  # - 'default': Standard behavior, prompts for dangerous operations
@@ -27,6 +27,7 @@ HookEvent = Literal[
27
27
  "Setup",
28
28
  "TeammateIdle",
29
29
  "TaskCompleted",
30
+ "ConfigChange",
30
31
  ]
31
32
 
32
33
 
@@ -170,6 +171,16 @@ class TaskCompletedHookInput(BaseHookInput):
170
171
  team_name: NotRequired[str]
171
172
 
172
173
 
174
+ class ConfigChangeHookInput(BaseHookInput):
175
+ """Input data for ConfigChange hook events."""
176
+
177
+ hook_event_name: Literal["ConfigChange"]
178
+ source: Literal[
179
+ "user_settings", "project_settings", "local_settings", "policy_settings", "skills"
180
+ ]
181
+ file_path: NotRequired[str]
182
+
183
+
173
184
  # Union type for all hook inputs
174
185
  HookInput = (
175
186
  PreToolUseHookInput
@@ -187,6 +198,7 @@ HookInput = (
187
198
  | SetupHookInput
188
199
  | TeammateIdleHookInput
189
200
  | TaskCompletedHookInput
201
+ | ConfigChangeHookInput
190
202
  )
191
203
 
192
204
 
@@ -9,6 +9,10 @@ from __future__ import annotations
9
9
  from typing import Literal, NotRequired, TypedDict
10
10
 
11
11
 
12
+ ModelName = Literal["sonnet", "opus", "haiku"]
13
+ PermissionMode = Literal["acceptEdits", "bypassPermissions", "default", "dontAsk", "plan"]
14
+
15
+
12
16
  class AgentInput(TypedDict):
13
17
  """Input for the Task tool. Launches a new agent to handle complex, multi-step tasks."""
14
18
 
@@ -18,6 +22,20 @@ class AgentInput(TypedDict):
18
22
  """The task for the agent to perform."""
19
23
  subagent_type: str
20
24
  """The type of specialized agent to use for this task."""
25
+ model: NotRequired[ModelName]
26
+ """Optional model to use for the agent."""
27
+ resume: NotRequired[str]
28
+ """Optional agent ID to resume from in order to continue from the previous execution transcript."""
29
+ run_in_background: NotRequired[bool]
30
+ """Whether to run the agent in the background."""
31
+ max_turns: NotRequired[int]
32
+ """Maximum number of agentic turns (API round-trips) before stopping. Used internally for warmup."""
33
+ name: NotRequired[str]
34
+ """Name for the spawned agent."""
35
+ team_name: NotRequired[str]
36
+ """Team name for spawning. Uses current team context if omitted."""
37
+ mode: NotRequired[PermissionMode]
38
+ """Permission mode for spawned teammate (e.g., "plan" to require plan approval)."""
21
39
 
22
40
 
23
41
  class AskUserQuestionOption(TypedDict):
@@ -96,9 +114,11 @@ class FileReadInput(TypedDict):
96
114
  file_path: str
97
115
  """The absolute path to the file to read."""
98
116
  offset: NotRequired[int]
99
- """The line number to start reading from."""
117
+ """The line number to start reading from. Only provide if the file is too large to read at once."""
100
118
  limit: NotRequired[int]
101
- """The number of lines to read."""
119
+ """The number of lines to read. Only provide if the file is too large to read at once"""
120
+ pages: NotRequired[str]
121
+ """Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Maximum 20 pages per request."""
102
122
 
103
123
 
104
124
  class FileWriteInput(TypedDict):
@@ -2,7 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Literal, NotRequired, TypedDict
5
+ from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict
6
+
7
+ from pydantic import Field
8
+
9
+ from clawd_code_sdk.models.base import ClaudeCodeBaseModel
6
10
 
7
11
 
8
12
  if TYPE_CHECKING:
@@ -12,6 +16,8 @@ if TYPE_CHECKING:
12
16
  RequestId = str | int
13
17
  JSONRPC_VERSION = "2.0"
14
18
 
19
+ McpConnectionStatus = Literal["connected", "pending", "failed", "needs-auth", "disabled"]
20
+
15
21
 
16
22
  # JSON-RPC types (from MCP specification)
17
23
 
@@ -30,7 +36,7 @@ class JSONRPCRequest(TypedDict):
30
36
  jsonrpc: str
31
37
  id: RequestId
32
38
  method: str
33
- params: NotRequired[dict[str, object]]
39
+ params: NotRequired[dict[str, Any]]
34
40
 
35
41
 
36
42
  class JSONRPCNotification(TypedDict):
@@ -38,7 +44,7 @@ class JSONRPCNotification(TypedDict):
38
44
 
39
45
  jsonrpc: str
40
46
  method: str
41
- params: NotRequired[dict[str, object]]
47
+ params: NotRequired[dict[str, Any]]
42
48
 
43
49
 
44
50
  class JSONRPCResultResponse(TypedDict):
@@ -46,7 +52,7 @@ class JSONRPCResultResponse(TypedDict):
46
52
 
47
53
  jsonrpc: str
48
54
  id: RequestId
49
- result: dict[str, object]
55
+ result: dict[str, Any]
50
56
 
51
57
 
52
58
  class JSONRPCErrorResponse(TypedDict):
@@ -111,3 +117,58 @@ class SdkPluginConfig(TypedDict):
111
117
 
112
118
  type: Literal["local"]
113
119
  path: str
120
+
121
+
122
+ # Pydantic models for MCP status responses
123
+
124
+
125
+ class ToolAnnotations(ClaudeCodeBaseModel):
126
+ """
127
+ Additional properties describing a Tool to clients.
128
+ """
129
+
130
+ title: str | None = None
131
+ """A human-readable title for the tool."""
132
+
133
+ read_only_hint: bool | None = None
134
+ """Read-only hint."""
135
+
136
+ destructive_hint: bool | None = None
137
+ """Destructive hint."""
138
+
139
+ idempotent_hint: bool | None = None
140
+ """Idempodent hint."""
141
+
142
+ open_world_hint: bool | None = None
143
+ """Open-world hint."""
144
+
145
+
146
+ class McpToolStatus(ClaudeCodeBaseModel):
147
+ """Status information for a single MCP tool."""
148
+
149
+ name: str
150
+ annotations: ToolAnnotations = Field(default_factory=ToolAnnotations)
151
+
152
+
153
+ class McpServerVersionInfo(ClaudeCodeBaseModel):
154
+ """Server version information returned in MCP status."""
155
+
156
+ name: str
157
+ version: str
158
+
159
+
160
+ class McpServerStatusEntry(ClaudeCodeBaseModel):
161
+ """Status information for a single MCP server."""
162
+
163
+ name: str
164
+ status: McpConnectionStatus
165
+ server_info: McpServerVersionInfo | None = None
166
+ config: dict[str, Any] = Field(default_factory=dict)
167
+ scope: str | None = None
168
+ tools: list[McpToolStatus] = Field(default_factory=list)
169
+
170
+
171
+ class McpStatusResponse(ClaudeCodeBaseModel):
172
+ """Response from get_mcp_status() containing all MCP server statuses."""
173
+
174
+ mcp_servers: list[McpServerStatusEntry] = Field(default_factory=list)
@@ -91,7 +91,7 @@ class UserMessage(BaseMessage):
91
91
 
92
92
 
93
93
  @dataclass(kw_only=True)
94
- class AssistantMessage(BaseMessage):
94
+ class AssistantMessage:
95
95
  """Assistant message with content blocks."""
96
96
 
97
97
  type: Literal["assistant"] = "assistant"
@@ -99,6 +99,8 @@ class AssistantMessage(BaseMessage):
99
99
  model: str
100
100
  parent_tool_use_id: str | None = None
101
101
  error: AssistantMessageError | None = None
102
+ session_id: str | None = None # not sure these two are needed.
103
+ uuid: str | None = None
102
104
 
103
105
  def raise_if_api_error(self) -> None:
104
106
  """Raise the appropriate API exception if error is set.
@@ -391,7 +393,7 @@ class AuthStatusMessage(BaseMessage):
391
393
  """Authentication status update."""
392
394
 
393
395
  type: Literal["auth_status"] = "auth_status"
394
- is_authenticating: bool = False
396
+ isAuthenticating: bool = False # noqa: N815
395
397
  output: list[str] | None = None
396
398
  error: str | None = None
397
399
 
@@ -76,14 +76,14 @@ class ClaudeAgentOptions:
76
76
  allow_dangerously_skip_permissions: bool = False
77
77
  # Resume from a specific message UUID (use with `resume`).
78
78
  resume_session_at: str | None = None
79
- # Enable debug mode for the Claude Code process.
80
- debug: bool = False
81
79
  # Write debug logs to a specific file path. Implicitly enables debug mode.
82
80
  debug_file: str | None = None
83
81
  # Enforce strict validation of MCP server configurations.
84
82
  strict_mcp_config: bool = False
85
83
  # Enable 1M token context window (Sonnet 4/4.5 only).
86
84
  context_1m: bool = False
85
+ prompt_suggestions: bool | None = None
86
+ worktree: bool | str = False
87
87
 
88
88
  def build_settings_value(self) -> str | None:
89
89
  """Build settings value, merging sandbox settings if provided.
@@ -2,17 +2,17 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from pydantic import BaseModel, ConfigDict, Field
6
- from pydantic.alias_generators import to_camel
5
+ from typing import Literal
7
6
 
7
+ from pydantic import Field
8
8
 
9
- class ClaudeCodeBasemodel(BaseModel):
10
- """Base model."""
9
+ from clawd_code_sdk.models.base import ClaudeCodeBaseModel
11
10
 
12
- model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
13
11
 
12
+ EffortLevel = Literal["low", "medium", "high", "max"]
14
13
 
15
- class ClaudeCodeModelInfo(ClaudeCodeBasemodel):
14
+
15
+ class ClaudeCodeModelInfo(ClaudeCodeBaseModel):
16
16
  """Information about an available AI model from Claude Code."""
17
17
 
18
18
  value: str
@@ -24,8 +24,17 @@ class ClaudeCodeModelInfo(ClaudeCodeBasemodel):
24
24
  description: str
25
25
  """Full description including capabilities and pricing."""
26
26
 
27
+ supports_effort: bool | None = None
28
+ """Whether the model supports effort setting."""
29
+
30
+ supported_effort_levels: list[EffortLevel] | None = None
31
+ """Supported effort levels."""
32
+
33
+ supports_adaptive_thinking: bool | None = None
34
+ """Whether the model supports adaptive thinking."""
27
35
 
28
- class ClaudeCodeCommandInfo(ClaudeCodeBasemodel):
36
+
37
+ class ClaudeCodeCommandInfo(ClaudeCodeBaseModel):
29
38
  """Information about an available slash command from Claude Code."""
30
39
 
31
40
  name: str
@@ -38,7 +47,7 @@ class ClaudeCodeCommandInfo(ClaudeCodeBasemodel):
38
47
  """Usage hint for command arguments (may be empty string)."""
39
48
 
40
49
 
41
- class ClaudeCodeAccountInfo(ClaudeCodeBasemodel):
50
+ class ClaudeCodeAccountInfo(ClaudeCodeBaseModel):
42
51
  """Account information from Claude Code."""
43
52
 
44
53
  email: str | None = None
@@ -54,7 +63,7 @@ class ClaudeCodeAccountInfo(ClaudeCodeBasemodel):
54
63
  """Where API key comes from (e.g., "ANTHROPIC_API_KEY")."""
55
64
 
56
65
 
57
- class ClaudeCodeServerInfo(ClaudeCodeBasemodel):
66
+ class ClaudeCodeServerInfo(ClaudeCodeBaseModel):
58
67
  """Complete server initialization info from Claude Code.
59
68
 
60
69
  This is returned by the Claude Code server during initialization and contains
@@ -76,3 +85,6 @@ class ClaudeCodeServerInfo(ClaudeCodeBasemodel):
76
85
 
77
86
  account: ClaudeCodeAccountInfo | None = Field(default=None)
78
87
  """Account and authentication information."""
88
+
89
+ pid: int | None = None
90
+ """Process id."""
@@ -6,7 +6,7 @@ See ARCHITECTURE.md for detailed documentation of the storage format.
6
6
  from __future__ import annotations
7
7
 
8
8
  import logging
9
- from typing import TYPE_CHECKING
9
+ from pathlib import Path
10
10
 
11
11
  import anyenv
12
12
  from pydantic import TypeAdapter, ValidationError
@@ -14,9 +14,6 @@ from pydantic import TypeAdapter, ValidationError
14
14
  from clawd_code_sdk.storage.models import ClaudeJSONLEntry
15
15
 
16
16
 
17
- if TYPE_CHECKING:
18
- from pathlib import Path
19
-
20
17
  logger = logging.getLogger(__name__)
21
18
 
22
19
 
@@ -70,28 +67,16 @@ def read_session(session_path: Path) -> list[ClaudeJSONLEntry]:
70
67
  entry = adapter.validate_python(data)
71
68
  entries.append(entry)
72
69
  except anyenv.JsonLoadError as e:
73
- logger.warning(
74
- "Failed to parse JSONL line (path: %s, error: %s, raw_line: %s)",
75
- str(session_path),
76
- str(e),
77
- raw_line,
78
- )
70
+ msg = "Failed to parse JSONL line (path: %s, error: %s, raw_line: %s)"
71
+ logger.warning(msg, str(session_path), str(e), raw_line)
79
72
  except ValidationError as e:
80
- logger.warning(
81
- "Failed to validate JSONL entry (path: %s, error: %s)",
82
- str(session_path),
83
- str(e),
84
- )
73
+ msg = "Failed to validate JSONL entry (path: %s, error: %s)"
74
+ logger.warning(msg, str(session_path), str(e))
85
75
  return entries
86
76
 
87
77
 
88
78
  def get_claude_data_dir() -> Path:
89
- """Get the Claude Code data directory path.
90
-
91
- Claude Code stores data in ~/.claude rather than the XDG data directory.
92
- """
93
- from pathlib import Path
94
-
79
+ """Get the Claude Code data directory path (~/.claude)."""
95
80
  return Path.home() / ".claude"
96
81
 
97
82
 
@@ -115,27 +100,13 @@ def path_to_claude_dir_name(project_path: str) -> str:
115
100
 
116
101
 
117
102
  def get_latest_session(project_path: str) -> Path | None:
118
- """Get the most recent session file for a project.
119
-
120
- Args:
121
- project_path: The project path
122
-
123
- Returns:
124
- Path to the latest session file, or None if no sessions exist
125
- """
103
+ """Get the path for the most recent session file for given project if existing."""
126
104
  sessions = list_project_sessions(project_path)
127
105
  return sessions[0] if sessions else None
128
106
 
129
107
 
130
108
  def list_project_sessions(project_path: str) -> list[Path]:
131
- """List all session files for a project.
132
-
133
- Args:
134
- project_path: The project path (will be converted to Claude's format)
135
-
136
- Returns:
137
- List of session file paths, sorted by modification time (newest first)
138
- """
109
+ """List all session files for given project path, sorted by modification time (newest first)."""
139
110
  projects_dir = get_claude_projects_dir()
140
111
  project_dir_name = path_to_claude_dir_name(project_path)
141
112
  project_dir = projects_dir / project_dir_name
@@ -12,11 +12,13 @@ from clawd_code_sdk import (
12
12
  AssistantMessage,
13
13
  AuthenticationError,
14
14
  ClaudeAgentOptions,
15
+ ClaudeSDKClient,
15
16
  InvalidRequestError,
16
17
  RateLimitError,
17
18
  ServerError,
18
19
  query,
19
20
  )
21
+ from clawd_code_sdk.models.mcp import McpServerStatusEntry, McpStatusResponse
20
22
 
21
23
 
22
24
  def create_mock_transport_with_messages(messages: list[dict]):
@@ -55,8 +57,11 @@ def create_mock_transport_with_messages(messages: list[dict]):
55
57
  "response": {
56
58
  "request_id": msg.get("request_id"),
57
59
  "subtype": "success",
58
- "commands": [],
59
- "output_style": "default",
60
+ "response": {
61
+ "commands": [],
62
+ "outputStyle": "default",
63
+ "pid": 12345,
64
+ },
60
65
  },
61
66
  }
62
67
  break
@@ -365,3 +370,146 @@ class TestAPIErrorRaising:
365
370
  assert messages[0].content[0].text == "Hello!"
366
371
 
367
372
  anyio.run(_test)
373
+
374
+
375
+ def _create_control_protocol_transport(
376
+ control_responses: dict[str, dict],
377
+ ) -> AsyncMock:
378
+ """Create a mock transport that handles initialization and responds to control requests.
379
+
380
+ Args:
381
+ control_responses: Mapping of control request subtype to response payload.
382
+ The "initialize" subtype is handled automatically.
383
+ """
384
+ mock_transport = AsyncMock()
385
+ mock_transport.connect = AsyncMock()
386
+ mock_transport.close = AsyncMock()
387
+ mock_transport.end_input = AsyncMock()
388
+
389
+ written_messages: list[str] = []
390
+
391
+ async def mock_write(data: str) -> None:
392
+ written_messages.append(data)
393
+
394
+ mock_transport.write = AsyncMock(side_effect=mock_write)
395
+
396
+ init_response = {
397
+ "subtype": "success",
398
+ "response": {
399
+ "commands": [],
400
+ "outputStyle": "default",
401
+ "pid": 12345,
402
+ },
403
+ }
404
+ all_responses = {"initialize": init_response, **control_responses}
405
+
406
+ async def mock_receive():
407
+ last_check = 0
408
+ timeout_counter = 0
409
+ while timeout_counter < 200:
410
+ await asyncio.sleep(0.01)
411
+ timeout_counter += 1
412
+
413
+ for msg_str in written_messages[last_check:]:
414
+ try:
415
+ msg = json.loads(msg_str.strip())
416
+ if msg.get("type") != "control_request":
417
+ continue
418
+ subtype = msg.get("request", {}).get("subtype")
419
+ if subtype in all_responses:
420
+ yield {
421
+ "type": "control_response",
422
+ "response": {
423
+ "request_id": msg.get("request_id"),
424
+ **all_responses[subtype],
425
+ },
426
+ }
427
+ except (json.JSONDecodeError, KeyError, AttributeError):
428
+ pass
429
+ last_check = len(written_messages)
430
+
431
+ mock_transport.read_messages = mock_receive
432
+ return mock_transport
433
+
434
+
435
+ class TestGetMcpStatus:
436
+ """Test get_mcp_status returns validated McpStatusResponse."""
437
+
438
+ def test_get_mcp_status_parses_response(self):
439
+ """Test that get_mcp_status returns a validated McpStatusResponse."""
440
+
441
+ async def _test():
442
+ mcp_status_payload = {
443
+ "subtype": "success",
444
+ "response": {
445
+ "mcpServers": [
446
+ {
447
+ "name": "git",
448
+ "status": "connected",
449
+ "serverInfo": {"name": "mcp-git", "version": "1.26.0"},
450
+ "config": {
451
+ "type": "stdio",
452
+ "command": "uvx",
453
+ "args": ["mcp-server-git"],
454
+ },
455
+ "scope": "dynamic",
456
+ "tools": [
457
+ {"name": "git_status", "annotations": {}},
458
+ {"name": "git_log", "annotations": {}},
459
+ ],
460
+ }
461
+ ]
462
+ },
463
+ }
464
+ mock_transport = _create_control_protocol_transport({"mcp_status": mcp_status_payload})
465
+
466
+ client = ClaudeSDKClient(transport=mock_transport)
467
+ await client.connect()
468
+ try:
469
+ status = await client.get_mcp_status()
470
+
471
+ assert isinstance(status, McpStatusResponse)
472
+ assert len(status.mcp_servers) == 1
473
+
474
+ server = status.mcp_servers[0]
475
+ assert isinstance(server, McpServerStatusEntry)
476
+ assert server.name == "git"
477
+ assert server.status == "connected"
478
+ assert server.scope == "dynamic"
479
+ assert server.server_info is not None
480
+ assert server.server_info.name == "mcp-git"
481
+ assert server.server_info.version == "1.26.0"
482
+ assert server.config == {
483
+ "type": "stdio",
484
+ "command": "uvx",
485
+ "args": ["mcp-server-git"],
486
+ }
487
+ assert len(server.tools) == 2
488
+ assert server.tools[0].name == "git_status"
489
+ assert server.tools[1].name == "git_log"
490
+ finally:
491
+ await client.disconnect()
492
+
493
+ anyio.run(_test)
494
+
495
+ def test_get_mcp_status_empty_servers(self):
496
+ """Test get_mcp_status with no MCP servers configured."""
497
+
498
+ async def _test():
499
+ mcp_status_payload = {
500
+ "subtype": "success",
501
+ "response": {"mcpServers": []},
502
+ }
503
+ mock_transport = _create_control_protocol_transport({"mcp_status": mcp_status_payload})
504
+
505
+ client = ClaudeSDKClient(transport=mock_transport)
506
+ await client.connect()
507
+ try:
508
+ status = await client.get_mcp_status()
509
+
510
+ assert isinstance(status, McpStatusResponse)
511
+ assert len(status.mcp_servers) == 0
512
+ finally:
513
+ await client.disconnect()
514
+
515
+ anyio.run(_test)
File without changes