claude-agent-sdk 0.7.3 → 0.8.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +190 -7
- data/lib/claude_agent_sdk/message_parser.rb +28 -5
- data/lib/claude_agent_sdk/query.rb +39 -4
- data/lib/claude_agent_sdk/sessions.rb +499 -0
- data/lib/claude_agent_sdk/types.rb +176 -11
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +42 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c723c3e660d135ab87311e27be6a80b8d13127f9e42789a66b1d30ec0395278
|
|
4
|
+
data.tar.gz: 0652f808dd792a6bef8db32afc567567fe9f016abdf095f1d13ae0ac07af0d03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 399925ff55a9a23ab54cbe97adb18fea804be6c92cf21aa3ca3fdcd4cbe54693dfc8b77b85f15ed2eaf1517acaecd09935701d0248fb99a23be6325d151c99aa
|
|
7
|
+
data.tar.gz: dd14802c444f4d6609cbb8215e79c3f5d8c4d9453e20d0d3af40cd853d653625c93e117082118beee2c5fb0ad29447a170687a44d56f88be6785affb8d118060
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.0] - 2026-03-05
|
|
9
|
+
|
|
10
|
+
Port of Python SDK v0.1.46 features.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
#### Task Message Types
|
|
15
|
+
- `TaskStartedMessage`, `TaskProgressMessage`, `TaskNotificationMessage` — typed `SystemMessage` subclasses for background task lifecycle events
|
|
16
|
+
- `TASK_NOTIFICATION_STATUSES` constant (`completed`, `failed`, `stopped`)
|
|
17
|
+
- `MessageParser` dispatches on `subtype` within `system` messages, falling back to generic `SystemMessage` for unknown subtypes
|
|
18
|
+
|
|
19
|
+
#### MCP Server Control
|
|
20
|
+
- `reconnect_mcp_server(server_name)` on `Query` and `Client` — retry failed MCP server connections
|
|
21
|
+
- `toggle_mcp_server(server_name, enabled)` on `Query` and `Client` — enable/disable MCP servers live
|
|
22
|
+
- `stop_task(task_id)` on `Query` and `Client` — stop a running background task
|
|
23
|
+
|
|
24
|
+
#### Subagent Context on Hook Inputs
|
|
25
|
+
- `agent_id` and `agent_type` attributes on `PreToolUseHookInput`, `PostToolUseHookInput`, `PostToolUseFailureHookInput`, `PermissionRequestHookInput`
|
|
26
|
+
- Populated when hooks fire inside subagents, allowing attribution of tool calls to specific agents
|
|
27
|
+
|
|
28
|
+
#### Result Message
|
|
29
|
+
- `stop_reason` attribute on `ResultMessage` (e.g., `'end_turn'`, `'max_tokens'`, `'stop_sequence'`)
|
|
30
|
+
|
|
31
|
+
#### Typed MCP Status Response
|
|
32
|
+
- `McpServerInfo`, `McpToolAnnotations`, `McpToolInfo`, `McpServerStatus`, `McpStatusResponse` types
|
|
33
|
+
- `.parse` class methods for hydrating from raw CLI response hashes
|
|
34
|
+
- `MCP_SERVER_CONNECTION_STATUSES` constant (`connected`, `failed`, `needs-auth`, `pending`, `disabled`)
|
|
35
|
+
|
|
36
|
+
#### Session Browsing
|
|
37
|
+
- `ClaudeAgentSDK.list_sessions(directory:, limit:, include_worktrees:)` — list sessions from `~/.claude/projects/` JSONL files
|
|
38
|
+
- `ClaudeAgentSDK.get_session_messages(session_id:, directory:, limit:, offset:)` — reconstruct conversation chain from session transcript
|
|
39
|
+
- `SDKSessionInfo` type with `session_id`, `summary`, `last_modified`, `file_size`, `custom_title`, `first_prompt`, `git_branch`, `cwd`
|
|
40
|
+
- `SessionMessage` type with `type`, `uuid`, `session_id`, `message`
|
|
41
|
+
- Pure filesystem operations — no CLI subprocess required
|
|
42
|
+
- Git worktree-aware session scanning
|
|
43
|
+
- `parentUuid` chain walking with cycle detection for robust conversation reconstruction
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- **`McpToolAnnotations.parse` losing `false` values:** `readOnly: false` was evaluated as `false || nil → nil` due to `||` short-circuiting. Now uses `.key?` to check presence before falling back to snake_case keys.
|
|
47
|
+
|
|
8
48
|
## [0.7.3] - 2026-02-26
|
|
9
49
|
|
|
10
50
|
### Fixed
|
data/README.md
CHANGED
|
@@ -6,6 +6,119 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://badge.fury.io/rb/claude-agent-sdk)
|
|
8
8
|
|
|
9
|
+
### Feature Parity with Python SDK (v0.1.46)
|
|
10
|
+
|
|
11
|
+
| Feature | Python | Ruby |
|
|
12
|
+
|---------|:------:|:----:|
|
|
13
|
+
| One-shot `query()` | `query()` | `query()` |
|
|
14
|
+
| Bidirectional `Client` | `ClaudeSDKClient` | `Client` |
|
|
15
|
+
| Streaming input | `AsyncIterable` | `Enumerator` |
|
|
16
|
+
| Custom tools (SDK MCP servers) | `@tool` decorator | `create_tool` block |
|
|
17
|
+
| MCP resources & prompts | ✅ | ✅ |
|
|
18
|
+
| Hooks (all 10 events) | ✅ | ✅ |
|
|
19
|
+
| Permission callbacks (`can_use_tool`) | ✅ | ✅ |
|
|
20
|
+
| Structured output | ✅ | ✅ |
|
|
21
|
+
| Thinking config (adaptive/enabled/disabled) | ✅ | ✅ |
|
|
22
|
+
| Effort levels | ✅ | ✅ |
|
|
23
|
+
| Programmatic subagents | ✅ | ✅ |
|
|
24
|
+
| Sandbox settings | ✅ | ✅ |
|
|
25
|
+
| Beta features (1M context) | ✅ | ✅ |
|
|
26
|
+
| File checkpointing & rewind | ✅ | ✅ |
|
|
27
|
+
| Session browsing (`list_sessions`, `get_session_messages`) | ✅ | ✅ |
|
|
28
|
+
| Task message types (started/progress/notification) | ✅ | ✅ |
|
|
29
|
+
| MCP server control (reconnect/toggle/stop) | ✅ | ✅ |
|
|
30
|
+
| Subagent context on hook inputs | ✅ | ✅ |
|
|
31
|
+
| Typed MCP status response | ✅ | ✅ |
|
|
32
|
+
| `stop_reason` on `ResultMessage` | ✅ | ✅ |
|
|
33
|
+
| Fallback model | ✅ | ✅ |
|
|
34
|
+
| Plugin support | ✅ | ✅ |
|
|
35
|
+
| Rails integration (configure block, ActionCable) | — | ✅ |
|
|
36
|
+
| Bundled CLI binary | ✅ | — |
|
|
37
|
+
|
|
38
|
+
<details>
|
|
39
|
+
<summary><strong>Usage & Implementation Differences</strong></summary>
|
|
40
|
+
|
|
41
|
+
#### Async model
|
|
42
|
+
|
|
43
|
+
Python uses `async`/`await` with `anyio` (works with both asyncio and Trio). Ruby uses the [`async`](https://github.com/socketry/async) gem with fibers — no `await` keyword needed, blocking calls yield automatically.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# Python
|
|
47
|
+
async with ClaudeSDKClient(options) as client:
|
|
48
|
+
await client.query("Hello")
|
|
49
|
+
async for msg in client.receive_messages():
|
|
50
|
+
print(msg)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Ruby
|
|
55
|
+
Async do
|
|
56
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
57
|
+
client.connect
|
|
58
|
+
client.query("Hello")
|
|
59
|
+
client.receive_messages { |msg| puts msg }
|
|
60
|
+
client.disconnect
|
|
61
|
+
end.wait
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Custom tools
|
|
65
|
+
|
|
66
|
+
Python uses a `@tool` decorator. Ruby uses `create_tool` with a block.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Python
|
|
70
|
+
@tool(name="add", description="Add numbers", input_schema={...})
|
|
71
|
+
def add(a: int, b: int) -> str:
|
|
72
|
+
return str(a + b)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# Ruby
|
|
77
|
+
add = ClaudeAgentSDK.create_tool("add", "Add numbers", { a: :number, b: :number }) do |args|
|
|
78
|
+
{ content: [{ type: "text", text: (args[:a] + args[:b]).to_s }] }
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Streaming input
|
|
83
|
+
|
|
84
|
+
Python uses `AsyncIterable`. Ruby uses `Enumerator` or any `#each`-able.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# Python
|
|
88
|
+
async def messages():
|
|
89
|
+
yield {"type": "user", "message": {"role": "user", "content": "Hello"}}
|
|
90
|
+
|
|
91
|
+
async for msg in query(prompt=messages(), options=options):
|
|
92
|
+
print(msg)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# Ruby
|
|
97
|
+
messages = ClaudeAgentSDK::Streaming.from_array(["Hello", "Follow up"])
|
|
98
|
+
ClaudeAgentSDK.query(prompt: messages) { |msg| puts msg }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Types
|
|
102
|
+
|
|
103
|
+
Python uses `dataclass` with type annotations and `TypedDict`. Ruby uses plain classes with `attr_accessor` and keyword args — no runtime type checking, but the same structure.
|
|
104
|
+
|
|
105
|
+
#### Configuration defaults
|
|
106
|
+
|
|
107
|
+
Python passes options directly. Ruby adds `ClaudeAgentSDK.configure` for global defaults that merge with per-call options — handy for Rails initializers.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# Ruby-only: global defaults
|
|
111
|
+
ClaudeAgentSDK.configure do |config|
|
|
112
|
+
config.default_options = { model: "sonnet", permission_mode: "bypassPermissions" }
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Subprocess transport
|
|
117
|
+
|
|
118
|
+
Both SDKs spawn `claude` CLI as a subprocess with stream-JSON over stdin/stdout. Python uses `anyio.open_process`; Ruby uses `Open3.popen3` with a background `Thread` for stderr. The wire protocol is identical.
|
|
119
|
+
|
|
120
|
+
</details>
|
|
121
|
+
|
|
9
122
|
## Table of Contents
|
|
10
123
|
|
|
11
124
|
- [Installation](#installation)
|
|
@@ -23,6 +136,7 @@
|
|
|
23
136
|
- [Tools Configuration](#tools-configuration)
|
|
24
137
|
- [Sandbox Settings](#sandbox-settings)
|
|
25
138
|
- [File Checkpointing & Rewind](#file-checkpointing--rewind)
|
|
139
|
+
- [Session Browsing](#session-browsing)
|
|
26
140
|
- [Rails Integration](#rails-integration)
|
|
27
141
|
- [Types](#types)
|
|
28
142
|
- [Error Handling](#error-handling)
|
|
@@ -39,7 +153,7 @@ Add this line to your application's Gemfile:
|
|
|
39
153
|
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
|
|
40
154
|
|
|
41
155
|
# Or use a stable version from RubyGems
|
|
42
|
-
gem 'claude-agent-sdk', '~> 0.
|
|
156
|
+
gem 'claude-agent-sdk', '~> 0.8.0'
|
|
43
157
|
```
|
|
44
158
|
|
|
45
159
|
And then execute:
|
|
@@ -225,13 +339,18 @@ Async do
|
|
|
225
339
|
puts "MCP status: #{status}"
|
|
226
340
|
|
|
227
341
|
# Get server initialization info
|
|
228
|
-
info = client.server_info
|
|
229
|
-
puts "Available commands: #{info}"
|
|
230
|
-
|
|
231
|
-
# (Parity alias) Get server initialization info
|
|
232
342
|
info = client.get_server_info
|
|
233
343
|
puts "Available commands: #{info}"
|
|
234
344
|
|
|
345
|
+
# Reconnect a failed MCP server
|
|
346
|
+
client.reconnect_mcp_server('my-server')
|
|
347
|
+
|
|
348
|
+
# Enable or disable an MCP server
|
|
349
|
+
client.toggle_mcp_server('my-server', false)
|
|
350
|
+
|
|
351
|
+
# Stop a running background task
|
|
352
|
+
client.stop_task('task_abc123')
|
|
353
|
+
|
|
235
354
|
client.disconnect
|
|
236
355
|
end.wait
|
|
237
356
|
```
|
|
@@ -774,6 +893,47 @@ end.wait
|
|
|
774
893
|
|
|
775
894
|
> **Note:** The `uuid` field on `UserMessage` is populated by the CLI and represents checkpoint identifiers. Rewinding to a UUID restores file state to what it was at that point in the conversation.
|
|
776
895
|
|
|
896
|
+
## Session Browsing
|
|
897
|
+
|
|
898
|
+
Browse and inspect previous Claude Code sessions directly from Ruby — no CLI subprocess required.
|
|
899
|
+
|
|
900
|
+
### Listing Sessions
|
|
901
|
+
|
|
902
|
+
```ruby
|
|
903
|
+
# List all sessions (sorted by most recent first)
|
|
904
|
+
sessions = ClaudeAgentSDK.list_sessions
|
|
905
|
+
sessions.each do |session|
|
|
906
|
+
puts "#{session.session_id}: #{session.summary} (#{session.git_branch})"
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
# List sessions for a specific directory
|
|
910
|
+
sessions = ClaudeAgentSDK.list_sessions(directory: '/path/to/project', limit: 10)
|
|
911
|
+
|
|
912
|
+
# Include git worktree sessions
|
|
913
|
+
sessions = ClaudeAgentSDK.list_sessions(directory: '.', include_worktrees: true)
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
Each `SDKSessionInfo` includes:
|
|
917
|
+
- `session_id`, `summary`, `last_modified`, `file_size`
|
|
918
|
+
- `custom_title`, `first_prompt`, `git_branch`, `cwd`
|
|
919
|
+
|
|
920
|
+
### Reading Session Messages
|
|
921
|
+
|
|
922
|
+
```ruby
|
|
923
|
+
# Get the full conversation from a session
|
|
924
|
+
messages = ClaudeAgentSDK.get_session_messages(session_id: 'abc-123-...')
|
|
925
|
+
messages.each do |msg|
|
|
926
|
+
puts "[#{msg.type}] #{msg.message}"
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
# Paginate through messages
|
|
930
|
+
page = ClaudeAgentSDK.get_session_messages(session_id: 'abc-123-...', offset: 10, limit: 20)
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
Each `SessionMessage` includes `type` (`"user"` or `"assistant"`), `uuid`, `session_id`, and `message` (raw API dict).
|
|
934
|
+
|
|
935
|
+
> **Note:** Session browsing reads `~/.claude/projects/` JSONL files directly. It respects the `CLAUDE_CONFIG_DIR` environment variable and automatically detects git worktrees.
|
|
936
|
+
|
|
777
937
|
## Rails Integration
|
|
778
938
|
|
|
779
939
|
The SDK integrates well with Rails applications. Here are common patterns:
|
|
@@ -966,13 +1126,26 @@ end
|
|
|
966
1126
|
|
|
967
1127
|
#### SystemMessage
|
|
968
1128
|
|
|
969
|
-
System message with metadata.
|
|
1129
|
+
System message with metadata. Task lifecycle events are typed subclasses.
|
|
970
1130
|
|
|
971
1131
|
```ruby
|
|
972
1132
|
class SystemMessage
|
|
973
|
-
attr_accessor :subtype, # String ('init', etc.)
|
|
1133
|
+
attr_accessor :subtype, # String ('init', 'task_started', 'task_progress', 'task_notification', etc.)
|
|
974
1134
|
:data # Hash
|
|
975
1135
|
end
|
|
1136
|
+
|
|
1137
|
+
# Typed subclasses (all inherit from SystemMessage, so is_a?(SystemMessage) still works)
|
|
1138
|
+
class TaskStartedMessage < SystemMessage
|
|
1139
|
+
attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1142
|
+
class TaskProgressMessage < SystemMessage
|
|
1143
|
+
attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
class TaskNotificationMessage < SystemMessage
|
|
1147
|
+
attr_accessor :task_id, :status, :output_file, :summary, :uuid, :session_id, :tool_use_id, :usage
|
|
1148
|
+
end
|
|
976
1149
|
```
|
|
977
1150
|
|
|
978
1151
|
#### ResultMessage
|
|
@@ -987,6 +1160,7 @@ class ResultMessage
|
|
|
987
1160
|
:is_error, # Boolean
|
|
988
1161
|
:num_turns, # Integer
|
|
989
1162
|
:session_id, # String
|
|
1163
|
+
:stop_reason, # String | nil ('end_turn', 'max_tokens', 'stop_sequence')
|
|
990
1164
|
:total_cost_usd, # Float | nil
|
|
991
1165
|
:usage, # Hash | nil
|
|
992
1166
|
:result, # String | nil (final text result)
|
|
@@ -1111,6 +1285,13 @@ end
|
|
|
1111
1285
|
| `McpSSEServerConfig` | MCP server config for SSE transport |
|
|
1112
1286
|
| `McpHttpServerConfig` | MCP server config for HTTP transport |
|
|
1113
1287
|
| `SdkPluginConfig` | SDK plugin configuration |
|
|
1288
|
+
| `McpServerStatus` | Status of a single MCP server connection (with `.parse`) |
|
|
1289
|
+
| `McpStatusResponse` | Response from `get_mcp_status` containing all server statuses (with `.parse`) |
|
|
1290
|
+
| `McpServerInfo` | MCP server name and version |
|
|
1291
|
+
| `McpToolInfo` | MCP tool name, description, and annotations |
|
|
1292
|
+
| `McpToolAnnotations` | MCP tool annotation hints (`read_only`, `destructive`, `open_world`) |
|
|
1293
|
+
| `SDKSessionInfo` | Session metadata from `list_sessions` |
|
|
1294
|
+
| `SessionMessage` | Single message from `get_session_messages` |
|
|
1114
1295
|
| `SandboxSettings` | Sandbox settings for isolated command execution |
|
|
1115
1296
|
| `SandboxNetworkConfig` | Network configuration for sandbox |
|
|
1116
1297
|
| `SandboxIgnoreViolations` | Configure which sandbox violations to ignore |
|
|
@@ -1126,6 +1307,8 @@ end
|
|
|
1126
1307
|
| `SETTING_SOURCES` | Available setting sources |
|
|
1127
1308
|
| `HOOK_EVENTS` | Available hook events |
|
|
1128
1309
|
| `ASSISTANT_MESSAGE_ERRORS` | Possible error types in AssistantMessage |
|
|
1310
|
+
| `TASK_NOTIFICATION_STATUSES` | Task lifecycle notification statuses (`completed`, `failed`, `stopped`) |
|
|
1311
|
+
| `MCP_SERVER_CONNECTION_STATUSES` | MCP server connection states (`connected`, `failed`, `needs-auth`, `pending`, `disabled`) |
|
|
1129
1312
|
|
|
1130
1313
|
## Error Handling
|
|
1131
1314
|
|
|
@@ -66,10 +66,32 @@ module ClaudeAgentSDK
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def self.parse_system_message(data)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
case data[:subtype]
|
|
70
|
+
when 'task_started'
|
|
71
|
+
TaskStartedMessage.new(
|
|
72
|
+
subtype: data[:subtype], data: data,
|
|
73
|
+
task_id: data[:task_id], description: data[:description],
|
|
74
|
+
uuid: data[:uuid], session_id: data[:session_id],
|
|
75
|
+
tool_use_id: data[:tool_use_id], task_type: data[:task_type]
|
|
76
|
+
)
|
|
77
|
+
when 'task_progress'
|
|
78
|
+
TaskProgressMessage.new(
|
|
79
|
+
subtype: data[:subtype], data: data,
|
|
80
|
+
task_id: data[:task_id], description: data[:description],
|
|
81
|
+
usage: data[:usage], uuid: data[:uuid], session_id: data[:session_id],
|
|
82
|
+
tool_use_id: data[:tool_use_id], last_tool_name: data[:last_tool_name]
|
|
83
|
+
)
|
|
84
|
+
when 'task_notification'
|
|
85
|
+
TaskNotificationMessage.new(
|
|
86
|
+
subtype: data[:subtype], data: data,
|
|
87
|
+
task_id: data[:task_id], status: data[:status],
|
|
88
|
+
output_file: data[:output_file], summary: data[:summary],
|
|
89
|
+
uuid: data[:uuid], session_id: data[:session_id],
|
|
90
|
+
tool_use_id: data[:tool_use_id], usage: data[:usage]
|
|
91
|
+
)
|
|
92
|
+
else
|
|
93
|
+
SystemMessage.new(subtype: data[:subtype], data: data)
|
|
94
|
+
end
|
|
73
95
|
end
|
|
74
96
|
|
|
75
97
|
def self.parse_result_message(data)
|
|
@@ -80,10 +102,11 @@ module ClaudeAgentSDK
|
|
|
80
102
|
is_error: data[:is_error],
|
|
81
103
|
num_turns: data[:num_turns],
|
|
82
104
|
session_id: data[:session_id],
|
|
105
|
+
stop_reason: data[:stop_reason],
|
|
83
106
|
total_cost_usd: data[:total_cost_usd],
|
|
84
107
|
usage: data[:usage],
|
|
85
108
|
result: data[:result],
|
|
86
|
-
structured_output: data[:structured_output]
|
|
109
|
+
structured_output: data[:structured_output]
|
|
87
110
|
)
|
|
88
111
|
end
|
|
89
112
|
|
|
@@ -316,13 +316,19 @@ module ClaudeAgentSDK
|
|
|
316
316
|
permission_mode: fetch.call(:permission_mode)
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
# Subagent context fields shared by tool-lifecycle hooks
|
|
320
|
+
subagent_args = {
|
|
321
|
+
agent_id: fetch.call(:agent_id),
|
|
322
|
+
agent_type: fetch.call(:agent_type)
|
|
323
|
+
}
|
|
324
|
+
|
|
319
325
|
case event_name
|
|
320
326
|
when 'PreToolUse'
|
|
321
327
|
PreToolUseHookInput.new(
|
|
322
328
|
tool_name: fetch.call(:tool_name),
|
|
323
329
|
tool_input: fetch.call(:tool_input),
|
|
324
330
|
tool_use_id: fetch.call(:tool_use_id),
|
|
325
|
-
**base_args
|
|
331
|
+
**subagent_args, **base_args
|
|
326
332
|
)
|
|
327
333
|
when 'PostToolUse'
|
|
328
334
|
PostToolUseHookInput.new(
|
|
@@ -330,7 +336,7 @@ module ClaudeAgentSDK
|
|
|
330
336
|
tool_input: fetch.call(:tool_input),
|
|
331
337
|
tool_response: fetch.call(:tool_response),
|
|
332
338
|
tool_use_id: fetch.call(:tool_use_id),
|
|
333
|
-
**base_args
|
|
339
|
+
**subagent_args, **base_args
|
|
334
340
|
)
|
|
335
341
|
when 'PostToolUseFailure'
|
|
336
342
|
PostToolUseFailureHookInput.new(
|
|
@@ -339,7 +345,7 @@ module ClaudeAgentSDK
|
|
|
339
345
|
tool_use_id: fetch.call(:tool_use_id),
|
|
340
346
|
error: fetch.call(:error),
|
|
341
347
|
is_interrupt: fetch.call(:is_interrupt),
|
|
342
|
-
**base_args
|
|
348
|
+
**subagent_args, **base_args
|
|
343
349
|
)
|
|
344
350
|
when 'UserPromptSubmit'
|
|
345
351
|
UserPromptSubmitHookInput.new(
|
|
@@ -377,7 +383,7 @@ module ClaudeAgentSDK
|
|
|
377
383
|
tool_name: fetch.call(:tool_name),
|
|
378
384
|
tool_input: fetch.call(:tool_input),
|
|
379
385
|
permission_suggestions: fetch.call(:permission_suggestions),
|
|
380
|
-
**base_args
|
|
386
|
+
**subagent_args, **base_args
|
|
381
387
|
)
|
|
382
388
|
when 'PreCompact'
|
|
383
389
|
PreCompactHookInput.new(
|
|
@@ -662,6 +668,35 @@ module ClaudeAgentSDK
|
|
|
662
668
|
})
|
|
663
669
|
end
|
|
664
670
|
|
|
671
|
+
# Reconnect a failed MCP server
|
|
672
|
+
# @param server_name [String] Name of the MCP server to reconnect
|
|
673
|
+
def reconnect_mcp_server(server_name)
|
|
674
|
+
send_control_request({
|
|
675
|
+
subtype: 'mcp_reconnect',
|
|
676
|
+
serverName: server_name
|
|
677
|
+
})
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
# Enable or disable an MCP server
|
|
681
|
+
# @param server_name [String] Name of the MCP server
|
|
682
|
+
# @param enabled [Boolean] Whether to enable or disable
|
|
683
|
+
def toggle_mcp_server(server_name, enabled)
|
|
684
|
+
send_control_request({
|
|
685
|
+
subtype: 'mcp_toggle',
|
|
686
|
+
serverName: server_name,
|
|
687
|
+
enabled: enabled
|
|
688
|
+
})
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Stop a running background task
|
|
692
|
+
# @param task_id [String] The ID of the task to stop
|
|
693
|
+
def stop_task(task_id)
|
|
694
|
+
send_control_request({
|
|
695
|
+
subtype: 'stop_task',
|
|
696
|
+
task_id: task_id
|
|
697
|
+
})
|
|
698
|
+
end
|
|
699
|
+
|
|
665
700
|
# Rewind files to a previous checkpoint (v0.1.15+)
|
|
666
701
|
# Restores file state to what it was at the given user message
|
|
667
702
|
# Requires enable_file_checkpointing to be true in options
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
require 'shellwords'
|
|
7
|
+
|
|
8
|
+
module ClaudeAgentSDK
|
|
9
|
+
# Session info returned by list_sessions
|
|
10
|
+
class SDKSessionInfo
|
|
11
|
+
attr_accessor :session_id, :summary, :last_modified, :file_size,
|
|
12
|
+
:custom_title, :first_prompt, :git_branch, :cwd
|
|
13
|
+
|
|
14
|
+
def initialize(session_id:, summary:, last_modified:, file_size:,
|
|
15
|
+
custom_title: nil, first_prompt: nil, git_branch: nil, cwd: nil)
|
|
16
|
+
@session_id = session_id
|
|
17
|
+
@summary = summary
|
|
18
|
+
@last_modified = last_modified
|
|
19
|
+
@file_size = file_size
|
|
20
|
+
@custom_title = custom_title
|
|
21
|
+
@first_prompt = first_prompt
|
|
22
|
+
@git_branch = git_branch
|
|
23
|
+
@cwd = cwd
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A single message from a session transcript
|
|
28
|
+
class SessionMessage
|
|
29
|
+
attr_accessor :type, :uuid, :session_id, :message, :parent_tool_use_id
|
|
30
|
+
|
|
31
|
+
def initialize(type:, uuid:, session_id:, message:, parent_tool_use_id: nil)
|
|
32
|
+
@type = type
|
|
33
|
+
@uuid = uuid
|
|
34
|
+
@session_id = session_id
|
|
35
|
+
@message = message
|
|
36
|
+
@parent_tool_use_id = parent_tool_use_id
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Session browsing functions
|
|
41
|
+
module Sessions # rubocop:disable Metrics/ModuleLength
|
|
42
|
+
LITE_READ_BUF_SIZE = 65_536
|
|
43
|
+
MAX_SANITIZED_LENGTH = 200
|
|
44
|
+
|
|
45
|
+
UUID_RE = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
46
|
+
|
|
47
|
+
SKIP_FIRST_PROMPT_PATTERN = %r{\A(?:<local-command-stdout>|<session-start-hook>|<tick>|<goal>|
|
|
48
|
+
\[Request\ interrupted\ by\ user[^\]]*\]|
|
|
49
|
+
\s*<ide_opened_file>[\s\S]*</ide_opened_file>\s*\z|
|
|
50
|
+
\s*<ide_selection>[\s\S]*</ide_selection>\s*\z)}x
|
|
51
|
+
|
|
52
|
+
COMMAND_NAME_RE = %r{<command-name>(.*?)</command-name>}
|
|
53
|
+
|
|
54
|
+
SANITIZE_RE = /[^a-zA-Z0-9]/
|
|
55
|
+
|
|
56
|
+
module_function
|
|
57
|
+
|
|
58
|
+
# Match TypeScript's simpleHash: signed 32-bit integer, base-36 output
|
|
59
|
+
def simple_hash(str)
|
|
60
|
+
h = 0
|
|
61
|
+
str.each_char do |ch|
|
|
62
|
+
char_code = ch.ord
|
|
63
|
+
h = ((h << 5) - h + char_code) & 0xFFFFFFFF
|
|
64
|
+
h -= 0x100000000 if h >= 0x80000000
|
|
65
|
+
end
|
|
66
|
+
h = h.abs
|
|
67
|
+
|
|
68
|
+
return '0' if h.zero?
|
|
69
|
+
|
|
70
|
+
digits = '0123456789abcdefghijklmnopqrstuvwxyz'
|
|
71
|
+
out = []
|
|
72
|
+
n = h
|
|
73
|
+
while n.positive?
|
|
74
|
+
out.unshift(digits[n % 36])
|
|
75
|
+
n /= 36
|
|
76
|
+
end
|
|
77
|
+
out.join
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Sanitize a filesystem path to a project directory name
|
|
81
|
+
def sanitize_path(name)
|
|
82
|
+
sanitized = name.gsub(SANITIZE_RE, '-')
|
|
83
|
+
return sanitized if sanitized.length <= MAX_SANITIZED_LENGTH
|
|
84
|
+
|
|
85
|
+
"#{sanitized[0, MAX_SANITIZED_LENGTH]}-#{simple_hash(name)}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Get the Claude config directory
|
|
89
|
+
def config_dir
|
|
90
|
+
ENV.fetch('CLAUDE_CONFIG_DIR', File.expand_path('~/.claude'))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Find the project directory for a given path
|
|
94
|
+
def find_project_dir(path)
|
|
95
|
+
projects_dir = File.join(config_dir, 'projects')
|
|
96
|
+
return nil unless File.directory?(projects_dir)
|
|
97
|
+
|
|
98
|
+
sanitized = sanitize_path(path)
|
|
99
|
+
exact_path = File.join(projects_dir, sanitized)
|
|
100
|
+
return exact_path if File.directory?(exact_path)
|
|
101
|
+
|
|
102
|
+
# For long paths, scan for prefix match
|
|
103
|
+
if sanitized.length > MAX_SANITIZED_LENGTH
|
|
104
|
+
prefix = sanitized[0, MAX_SANITIZED_LENGTH + 1] # includes the trailing '-'
|
|
105
|
+
Dir.children(projects_dir).each do |child|
|
|
106
|
+
candidate = File.join(projects_dir, child)
|
|
107
|
+
return candidate if File.directory?(candidate) && child.start_with?(prefix)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Extract a JSON string field value from raw text without full JSON parse
|
|
115
|
+
def extract_json_string_field(text, key, last: false)
|
|
116
|
+
search_patterns = ["\"#{key}\":\"", "\"#{key}\": \""]
|
|
117
|
+
result = nil
|
|
118
|
+
|
|
119
|
+
search_patterns.each do |pattern|
|
|
120
|
+
pos = 0
|
|
121
|
+
loop do
|
|
122
|
+
idx = text.index(pattern, pos)
|
|
123
|
+
break unless idx
|
|
124
|
+
|
|
125
|
+
value_start = idx + pattern.length
|
|
126
|
+
value = extract_json_string_value(text, value_start)
|
|
127
|
+
if value
|
|
128
|
+
result = unescape_json_string(value)
|
|
129
|
+
return result unless last
|
|
130
|
+
end
|
|
131
|
+
pos = value_start
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
result
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Extract string value starting at pos (handles escapes)
|
|
139
|
+
def extract_json_string_value(text, start)
|
|
140
|
+
pos = start
|
|
141
|
+
while pos < text.length
|
|
142
|
+
ch = text[pos]
|
|
143
|
+
if ch == '\\'
|
|
144
|
+
pos += 2
|
|
145
|
+
elsif ch == '"'
|
|
146
|
+
return text[start...pos]
|
|
147
|
+
else
|
|
148
|
+
pos += 1
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Unescape a JSON string value
|
|
155
|
+
def unescape_json_string(str)
|
|
156
|
+
JSON.parse("\"#{str}\"")
|
|
157
|
+
rescue JSON::ParserError
|
|
158
|
+
str
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Extract the first meaningful user prompt from the head of a JSONL file
|
|
162
|
+
def extract_first_prompt_from_head(head)
|
|
163
|
+
command_fallback = nil
|
|
164
|
+
|
|
165
|
+
head.each_line do |line|
|
|
166
|
+
next unless line.include?('"type":"user"') || line.include?('"type": "user"')
|
|
167
|
+
next if line.include?('"tool_result"')
|
|
168
|
+
next if line.include?('"isMeta":true') || line.include?('"isMeta": true')
|
|
169
|
+
next if line.include?('"isCompactSummary":true') || line.include?('"isCompactSummary": true')
|
|
170
|
+
|
|
171
|
+
entry = JSON.parse(line, symbolize_names: false)
|
|
172
|
+
content = entry.dig('message', 'content')
|
|
173
|
+
next unless content
|
|
174
|
+
|
|
175
|
+
texts = if content.is_a?(String)
|
|
176
|
+
[content]
|
|
177
|
+
elsif content.is_a?(Array)
|
|
178
|
+
content.filter_map { |block| block['text'] if block.is_a?(Hash) && block['type'] == 'text' }
|
|
179
|
+
else
|
|
180
|
+
next
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
texts.each do |text|
|
|
184
|
+
text = text.gsub(/\n+/, ' ').strip
|
|
185
|
+
next if text.empty?
|
|
186
|
+
|
|
187
|
+
if (m = text.match(COMMAND_NAME_RE))
|
|
188
|
+
command_fallback ||= m[1]
|
|
189
|
+
next
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
next if text.match?(SKIP_FIRST_PROMPT_PATTERN)
|
|
193
|
+
|
|
194
|
+
return text.length > 200 ? "#{text[0, 200]}…" : text
|
|
195
|
+
end
|
|
196
|
+
rescue JSON::ParserError
|
|
197
|
+
next
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
command_fallback || ''
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Read a single session file with lite (head/tail) strategy
|
|
204
|
+
def read_session_lite(file_path, project_path)
|
|
205
|
+
stat = File.stat(file_path)
|
|
206
|
+
return nil if stat.size.zero? # rubocop:disable Style/ZeroLengthPredicate
|
|
207
|
+
|
|
208
|
+
head, tail = read_head_tail(file_path, stat.size)
|
|
209
|
+
|
|
210
|
+
# Check first line for sidechain
|
|
211
|
+
first_line = head.lines.first || ''
|
|
212
|
+
return nil if first_line.include?('"isSidechain":true') || first_line.include?('"isSidechain": true')
|
|
213
|
+
|
|
214
|
+
build_session_info(file_path, head, tail, stat, project_path)
|
|
215
|
+
rescue StandardError
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def read_head_tail(file_path, size)
|
|
220
|
+
head = tail = nil
|
|
221
|
+
File.open(file_path, 'rb') do |f|
|
|
222
|
+
head = (f.read(LITE_READ_BUF_SIZE) || '').force_encoding('UTF-8')
|
|
223
|
+
tail = if size > LITE_READ_BUF_SIZE
|
|
224
|
+
f.seek([0, size - LITE_READ_BUF_SIZE].max)
|
|
225
|
+
(f.read(LITE_READ_BUF_SIZE) || '').force_encoding('UTF-8')
|
|
226
|
+
else
|
|
227
|
+
head
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
[head, tail]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def build_session_info(file_path, head, tail, stat, project_path)
|
|
234
|
+
custom_title = extract_json_string_field(tail, 'customTitle', last: true)
|
|
235
|
+
first_prompt = extract_first_prompt_from_head(head)
|
|
236
|
+
summary = custom_title || extract_json_string_field(tail, 'summary', last: true) || first_prompt
|
|
237
|
+
return nil if summary.nil? || summary.strip.empty?
|
|
238
|
+
|
|
239
|
+
SDKSessionInfo.new(
|
|
240
|
+
session_id: File.basename(file_path, '.jsonl'),
|
|
241
|
+
summary: summary,
|
|
242
|
+
last_modified: (stat.mtime.to_f * 1000).to_i,
|
|
243
|
+
file_size: stat.size,
|
|
244
|
+
custom_title: custom_title,
|
|
245
|
+
first_prompt: first_prompt,
|
|
246
|
+
git_branch: extract_json_string_field(tail, 'gitBranch', last: true) ||
|
|
247
|
+
extract_json_string_field(head, 'gitBranch', last: false),
|
|
248
|
+
cwd: extract_json_string_field(head, 'cwd', last: false) || project_path
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Read all sessions from a project directory
|
|
253
|
+
def read_sessions_from_dir(project_dir, project_path = nil)
|
|
254
|
+
return [] unless File.directory?(project_dir)
|
|
255
|
+
|
|
256
|
+
sessions = []
|
|
257
|
+
Dir.glob(File.join(project_dir, '*.jsonl')).each do |file_path|
|
|
258
|
+
stem = File.basename(file_path, '.jsonl')
|
|
259
|
+
next unless stem.match?(UUID_RE)
|
|
260
|
+
|
|
261
|
+
session = read_session_lite(file_path, project_path)
|
|
262
|
+
sessions << session if session
|
|
263
|
+
end
|
|
264
|
+
sessions
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# List sessions for a directory (or all sessions)
|
|
268
|
+
# @param directory [String, nil] Working directory to list sessions for
|
|
269
|
+
# @param limit [Integer, nil] Maximum number of sessions to return
|
|
270
|
+
# @param include_worktrees [Boolean] Whether to include git worktree sessions
|
|
271
|
+
# @return [Array<SDKSessionInfo>] Sessions sorted by last_modified descending
|
|
272
|
+
def list_sessions(directory: nil, limit: nil, include_worktrees: true)
|
|
273
|
+
sessions = if directory
|
|
274
|
+
list_sessions_for_directory(directory, include_worktrees)
|
|
275
|
+
else
|
|
276
|
+
list_all_sessions
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Sort by last_modified descending
|
|
280
|
+
sessions.sort_by! { |s| -s.last_modified }
|
|
281
|
+
sessions = sessions.first(limit) if limit
|
|
282
|
+
sessions
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Get messages from a session transcript
|
|
286
|
+
# @param session_id [String] The session UUID
|
|
287
|
+
# @param directory [String, nil] Working directory to search in
|
|
288
|
+
# @param limit [Integer, nil] Maximum number of messages
|
|
289
|
+
# @param offset [Integer] Number of messages to skip
|
|
290
|
+
# @return [Array<SessionMessage>] Ordered messages from the session
|
|
291
|
+
def get_session_messages(session_id:, directory: nil, limit: nil, offset: 0)
|
|
292
|
+
return [] unless session_id.match?(UUID_RE)
|
|
293
|
+
|
|
294
|
+
file_path = find_session_file(session_id, directory)
|
|
295
|
+
return [] unless file_path && File.exist?(file_path)
|
|
296
|
+
|
|
297
|
+
entries = parse_jsonl_entries(file_path)
|
|
298
|
+
chain = build_conversation_chain(entries)
|
|
299
|
+
messages = filter_visible_messages(chain)
|
|
300
|
+
|
|
301
|
+
# Apply offset and limit
|
|
302
|
+
messages = messages[offset..] || []
|
|
303
|
+
messages = messages.first(limit) if limit
|
|
304
|
+
messages
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# -- Private helpers --
|
|
308
|
+
|
|
309
|
+
def list_sessions_for_directory(directory, include_worktrees)
|
|
310
|
+
path = File.realpath(directory).unicode_normalize(:nfc)
|
|
311
|
+
|
|
312
|
+
worktree_paths = []
|
|
313
|
+
worktree_paths = detect_worktrees(path) if include_worktrees
|
|
314
|
+
|
|
315
|
+
if worktree_paths.length <= 1
|
|
316
|
+
project_dir = find_project_dir(path)
|
|
317
|
+
return project_dir ? read_sessions_from_dir(project_dir, path) : []
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Multiple worktrees: scan all project dirs for matches
|
|
321
|
+
all_sessions = []
|
|
322
|
+
worktree_paths.each do |wt_path|
|
|
323
|
+
project_dir = find_project_dir(wt_path)
|
|
324
|
+
next unless project_dir
|
|
325
|
+
|
|
326
|
+
all_sessions.concat(read_sessions_from_dir(project_dir, wt_path))
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
deduplicate_sessions(all_sessions)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def list_all_sessions
|
|
333
|
+
projects_dir = File.join(config_dir, 'projects')
|
|
334
|
+
return [] unless File.directory?(projects_dir)
|
|
335
|
+
|
|
336
|
+
all_sessions = []
|
|
337
|
+
Dir.children(projects_dir).each do |child|
|
|
338
|
+
dir = File.join(projects_dir, child)
|
|
339
|
+
next unless File.directory?(dir)
|
|
340
|
+
|
|
341
|
+
all_sessions.concat(read_sessions_from_dir(dir))
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
deduplicate_sessions(all_sessions)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def deduplicate_sessions(sessions)
|
|
348
|
+
by_id = {}
|
|
349
|
+
sessions.each do |s|
|
|
350
|
+
existing = by_id[s.session_id]
|
|
351
|
+
by_id[s.session_id] = s if existing.nil? || s.last_modified > existing.last_modified
|
|
352
|
+
end
|
|
353
|
+
by_id.values
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def detect_worktrees(path)
|
|
357
|
+
output = `git -C #{Shellwords.escape(path)} worktree list --porcelain 2>/dev/null`
|
|
358
|
+
return [path] unless $CHILD_STATUS.success?
|
|
359
|
+
|
|
360
|
+
paths = output.lines.filter_map do |line|
|
|
361
|
+
line.strip.delete_prefix('worktree ') if line.start_with?('worktree ')
|
|
362
|
+
end
|
|
363
|
+
paths.empty? ? [path] : paths
|
|
364
|
+
rescue StandardError
|
|
365
|
+
[path]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def find_session_file(session_id, directory)
|
|
369
|
+
projects_dir = File.join(config_dir, 'projects')
|
|
370
|
+
return nil unless File.directory?(projects_dir)
|
|
371
|
+
|
|
372
|
+
if directory
|
|
373
|
+
path = File.realpath(directory).unicode_normalize(:nfc)
|
|
374
|
+
project_dir = find_project_dir(path)
|
|
375
|
+
if project_dir
|
|
376
|
+
candidate = File.join(project_dir, "#{session_id}.jsonl")
|
|
377
|
+
return candidate if File.exist?(candidate)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Try worktrees
|
|
381
|
+
detect_worktrees(path).each do |wt_path|
|
|
382
|
+
pd = find_project_dir(wt_path)
|
|
383
|
+
next unless pd
|
|
384
|
+
|
|
385
|
+
candidate = File.join(pd, "#{session_id}.jsonl")
|
|
386
|
+
return candidate if File.exist?(candidate)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Scan all project dirs
|
|
391
|
+
Dir.children(projects_dir).each do |child|
|
|
392
|
+
dir = File.join(projects_dir, child)
|
|
393
|
+
next unless File.directory?(dir)
|
|
394
|
+
|
|
395
|
+
candidate = File.join(dir, "#{session_id}.jsonl")
|
|
396
|
+
return candidate if File.exist?(candidate)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
nil
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def parse_jsonl_entries(file_path)
|
|
403
|
+
entries = []
|
|
404
|
+
valid_types = %w[user assistant progress system attachment].freeze
|
|
405
|
+
|
|
406
|
+
File.foreach(file_path) do |line|
|
|
407
|
+
entry = JSON.parse(line.strip, symbolize_names: false)
|
|
408
|
+
next unless entry.is_a?(Hash)
|
|
409
|
+
next unless valid_types.include?(entry['type'])
|
|
410
|
+
next unless entry['uuid'].is_a?(String)
|
|
411
|
+
|
|
412
|
+
entries << entry
|
|
413
|
+
rescue JSON::ParserError
|
|
414
|
+
next
|
|
415
|
+
end
|
|
416
|
+
entries
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def build_conversation_chain(entries)
|
|
420
|
+
return [] if entries.empty?
|
|
421
|
+
|
|
422
|
+
by_uuid = {}
|
|
423
|
+
by_position = {}
|
|
424
|
+
parent_uuids = Set.new
|
|
425
|
+
|
|
426
|
+
entries.each_with_index do |entry, idx|
|
|
427
|
+
by_uuid[entry['uuid']] = entry
|
|
428
|
+
by_position[entry['uuid']] = idx
|
|
429
|
+
parent_uuids << entry['parentUuid'] if entry['parentUuid']
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Terminals: entries whose uuid is not any other entry's parentUuid
|
|
433
|
+
terminals = Set.new(by_uuid.keys) - parent_uuids
|
|
434
|
+
|
|
435
|
+
# Walk back from each terminal to find the nearest user/assistant leaf
|
|
436
|
+
leaf_candidates = terminals.filter_map do |uuid|
|
|
437
|
+
walk_to_leaf(by_uuid, uuid)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Keep only main-chain candidates (not sidechain, team, or meta)
|
|
441
|
+
main_leaves = leaf_candidates.reject do |e|
|
|
442
|
+
e['isSidechain'] || e['teamName'] || e['isMeta']
|
|
443
|
+
end
|
|
444
|
+
return [] if main_leaves.empty?
|
|
445
|
+
|
|
446
|
+
# Pick the leaf with highest file position, walk to root
|
|
447
|
+
best_leaf = main_leaves.max_by { |e| by_position[e['uuid']] || 0 }
|
|
448
|
+
walk_to_root(by_uuid, best_leaf)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def walk_to_leaf(by_uuid, uuid)
|
|
452
|
+
visited = Set.new
|
|
453
|
+
current = by_uuid[uuid]
|
|
454
|
+
while current
|
|
455
|
+
return current if %w[user assistant].include?(current['type'])
|
|
456
|
+
return nil unless visited.add?(current['uuid'])
|
|
457
|
+
|
|
458
|
+
parent = current['parentUuid']
|
|
459
|
+
current = parent ? by_uuid[parent] : nil
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def walk_to_root(by_uuid, leaf)
|
|
464
|
+
chain = []
|
|
465
|
+
visited = Set.new
|
|
466
|
+
current = leaf
|
|
467
|
+
while current
|
|
468
|
+
break unless visited.add?(current['uuid'])
|
|
469
|
+
|
|
470
|
+
chain << current
|
|
471
|
+
parent = current['parentUuid']
|
|
472
|
+
current = parent ? by_uuid[parent] : nil
|
|
473
|
+
end
|
|
474
|
+
chain.reverse
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def filter_visible_messages(chain)
|
|
478
|
+
chain.filter_map do |entry|
|
|
479
|
+
next unless %w[user assistant].include?(entry['type'])
|
|
480
|
+
next if entry['isMeta']
|
|
481
|
+
next if entry['isSidechain']
|
|
482
|
+
next if entry['teamName']
|
|
483
|
+
|
|
484
|
+
SessionMessage.new(
|
|
485
|
+
type: entry['type'],
|
|
486
|
+
uuid: entry['uuid'],
|
|
487
|
+
session_id: entry['sessionId'] || entry['session_id'] || '',
|
|
488
|
+
message: entry['message']
|
|
489
|
+
)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
private_class_method :list_sessions_for_directory, :list_all_sessions,
|
|
494
|
+
:deduplicate_sessions, :detect_worktrees,
|
|
495
|
+
:find_session_file, :parse_jsonl_entries,
|
|
496
|
+
:build_conversation_chain, :walk_to_leaf, :walk_to_root,
|
|
497
|
+
:filter_visible_messages, :read_head_tail, :build_session_info
|
|
498
|
+
end
|
|
499
|
+
end
|
|
@@ -124,23 +124,79 @@ module ClaudeAgentSDK
|
|
|
124
124
|
end
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
+
# Task lifecycle notification statuses
|
|
128
|
+
TASK_NOTIFICATION_STATUSES = %w[completed failed stopped].freeze
|
|
129
|
+
|
|
130
|
+
# Task started system message (subagent/background task started)
|
|
131
|
+
class TaskStartedMessage < SystemMessage
|
|
132
|
+
attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type
|
|
133
|
+
|
|
134
|
+
def initialize(subtype:, data:, task_id:, description:, uuid:, session_id:,
|
|
135
|
+
tool_use_id: nil, task_type: nil)
|
|
136
|
+
super(subtype: subtype, data: data)
|
|
137
|
+
@task_id = task_id
|
|
138
|
+
@description = description
|
|
139
|
+
@uuid = uuid
|
|
140
|
+
@session_id = session_id
|
|
141
|
+
@tool_use_id = tool_use_id
|
|
142
|
+
@task_type = task_type
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Task progress system message (periodic update from a running task)
|
|
147
|
+
class TaskProgressMessage < SystemMessage
|
|
148
|
+
attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name
|
|
149
|
+
|
|
150
|
+
def initialize(subtype:, data:, task_id:, description:, usage:, uuid:, session_id:,
|
|
151
|
+
tool_use_id: nil, last_tool_name: nil)
|
|
152
|
+
super(subtype: subtype, data: data)
|
|
153
|
+
@task_id = task_id
|
|
154
|
+
@description = description
|
|
155
|
+
@usage = usage
|
|
156
|
+
@uuid = uuid
|
|
157
|
+
@session_id = session_id
|
|
158
|
+
@tool_use_id = tool_use_id
|
|
159
|
+
@last_tool_name = last_tool_name
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Task notification system message (task completed/failed/stopped)
|
|
164
|
+
class TaskNotificationMessage < SystemMessage
|
|
165
|
+
attr_accessor :task_id, :status, :output_file, :summary, :uuid, :session_id, :tool_use_id, :usage
|
|
166
|
+
|
|
167
|
+
def initialize(subtype:, data:, task_id:, status:, output_file:, summary:, uuid:, session_id:,
|
|
168
|
+
tool_use_id: nil, usage: nil)
|
|
169
|
+
super(subtype: subtype, data: data)
|
|
170
|
+
@task_id = task_id
|
|
171
|
+
@status = status
|
|
172
|
+
@output_file = output_file
|
|
173
|
+
@summary = summary
|
|
174
|
+
@uuid = uuid
|
|
175
|
+
@session_id = session_id
|
|
176
|
+
@tool_use_id = tool_use_id
|
|
177
|
+
@usage = usage
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
127
181
|
# Result message with cost and usage information
|
|
128
182
|
class ResultMessage
|
|
129
183
|
attr_accessor :subtype, :duration_ms, :duration_api_ms, :is_error,
|
|
130
|
-
:num_turns, :session_id, :total_cost_usd, :usage, :result, :structured_output
|
|
184
|
+
:num_turns, :session_id, :stop_reason, :total_cost_usd, :usage, :result, :structured_output
|
|
131
185
|
|
|
132
186
|
def initialize(subtype:, duration_ms:, duration_api_ms:, is_error:,
|
|
133
|
-
num_turns:, session_id:,
|
|
187
|
+
num_turns:, session_id:, stop_reason: nil, total_cost_usd: nil,
|
|
188
|
+
usage: nil, result: nil, structured_output: nil)
|
|
134
189
|
@subtype = subtype
|
|
135
190
|
@duration_ms = duration_ms
|
|
136
191
|
@duration_api_ms = duration_api_ms
|
|
137
192
|
@is_error = is_error
|
|
138
193
|
@num_turns = num_turns
|
|
139
194
|
@session_id = session_id
|
|
195
|
+
@stop_reason = stop_reason
|
|
140
196
|
@total_cost_usd = total_cost_usd
|
|
141
197
|
@usage = usage
|
|
142
198
|
@result = result
|
|
143
|
-
@structured_output = structured_output
|
|
199
|
+
@structured_output = structured_output
|
|
144
200
|
end
|
|
145
201
|
end
|
|
146
202
|
|
|
@@ -320,29 +376,34 @@ module ClaudeAgentSDK
|
|
|
320
376
|
|
|
321
377
|
# PreToolUse hook input
|
|
322
378
|
class PreToolUseHookInput < BaseHookInput
|
|
323
|
-
attr_accessor :hook_event_name, :tool_name, :tool_input, :tool_use_id
|
|
379
|
+
attr_accessor :hook_event_name, :tool_name, :tool_input, :tool_use_id, :agent_id, :agent_type
|
|
324
380
|
|
|
325
|
-
def initialize(hook_event_name: 'PreToolUse', tool_name: nil, tool_input: nil, tool_use_id: nil,
|
|
381
|
+
def initialize(hook_event_name: 'PreToolUse', tool_name: nil, tool_input: nil, tool_use_id: nil,
|
|
382
|
+
agent_id: nil, agent_type: nil, **base_args)
|
|
326
383
|
super(**base_args)
|
|
327
384
|
@hook_event_name = hook_event_name
|
|
328
385
|
@tool_name = tool_name
|
|
329
386
|
@tool_input = tool_input
|
|
330
387
|
@tool_use_id = tool_use_id
|
|
388
|
+
@agent_id = agent_id
|
|
389
|
+
@agent_type = agent_type
|
|
331
390
|
end
|
|
332
391
|
end
|
|
333
392
|
|
|
334
393
|
# PostToolUse hook input
|
|
335
394
|
class PostToolUseHookInput < BaseHookInput
|
|
336
|
-
attr_accessor :hook_event_name, :tool_name, :tool_input, :tool_response, :tool_use_id
|
|
395
|
+
attr_accessor :hook_event_name, :tool_name, :tool_input, :tool_response, :tool_use_id, :agent_id, :agent_type
|
|
337
396
|
|
|
338
397
|
def initialize(hook_event_name: 'PostToolUse', tool_name: nil, tool_input: nil, tool_response: nil,
|
|
339
|
-
tool_use_id: nil, **base_args)
|
|
398
|
+
tool_use_id: nil, agent_id: nil, agent_type: nil, **base_args)
|
|
340
399
|
super(**base_args)
|
|
341
400
|
@hook_event_name = hook_event_name
|
|
342
401
|
@tool_name = tool_name
|
|
343
402
|
@tool_input = tool_input
|
|
344
403
|
@tool_response = tool_response
|
|
345
404
|
@tool_use_id = tool_use_id
|
|
405
|
+
@agent_id = agent_id
|
|
406
|
+
@agent_type = agent_type
|
|
346
407
|
end
|
|
347
408
|
end
|
|
348
409
|
|
|
@@ -385,10 +446,11 @@ module ClaudeAgentSDK
|
|
|
385
446
|
|
|
386
447
|
# PostToolUseFailure hook input
|
|
387
448
|
class PostToolUseFailureHookInput < BaseHookInput
|
|
388
|
-
attr_accessor :hook_event_name, :tool_name, :tool_input, :tool_use_id, :error, :is_interrupt
|
|
449
|
+
attr_accessor :hook_event_name, :tool_name, :tool_input, :tool_use_id, :error, :is_interrupt,
|
|
450
|
+
:agent_id, :agent_type
|
|
389
451
|
|
|
390
452
|
def initialize(hook_event_name: 'PostToolUseFailure', tool_name: nil, tool_input: nil, tool_use_id: nil,
|
|
391
|
-
error: nil, is_interrupt: nil, **base_args)
|
|
453
|
+
error: nil, is_interrupt: nil, agent_id: nil, agent_type: nil, **base_args)
|
|
392
454
|
super(**base_args)
|
|
393
455
|
@hook_event_name = hook_event_name
|
|
394
456
|
@tool_name = tool_name
|
|
@@ -396,6 +458,8 @@ module ClaudeAgentSDK
|
|
|
396
458
|
@tool_use_id = tool_use_id
|
|
397
459
|
@error = error
|
|
398
460
|
@is_interrupt = is_interrupt
|
|
461
|
+
@agent_id = agent_id
|
|
462
|
+
@agent_type = agent_type
|
|
399
463
|
end
|
|
400
464
|
end
|
|
401
465
|
|
|
@@ -426,15 +490,17 @@ module ClaudeAgentSDK
|
|
|
426
490
|
|
|
427
491
|
# PermissionRequest hook input
|
|
428
492
|
class PermissionRequestHookInput < BaseHookInput
|
|
429
|
-
attr_accessor :hook_event_name, :tool_name, :tool_input, :permission_suggestions
|
|
493
|
+
attr_accessor :hook_event_name, :tool_name, :tool_input, :permission_suggestions, :agent_id, :agent_type
|
|
430
494
|
|
|
431
495
|
def initialize(hook_event_name: 'PermissionRequest', tool_name: nil, tool_input: nil, permission_suggestions: nil,
|
|
432
|
-
**base_args)
|
|
496
|
+
agent_id: nil, agent_type: nil, **base_args)
|
|
433
497
|
super(**base_args)
|
|
434
498
|
@hook_event_name = hook_event_name
|
|
435
499
|
@tool_name = tool_name
|
|
436
500
|
@tool_input = tool_input
|
|
437
501
|
@permission_suggestions = permission_suggestions
|
|
502
|
+
@agent_id = agent_id
|
|
503
|
+
@agent_type = agent_type
|
|
438
504
|
end
|
|
439
505
|
end
|
|
440
506
|
|
|
@@ -632,6 +698,105 @@ module ClaudeAgentSDK
|
|
|
632
698
|
end
|
|
633
699
|
end
|
|
634
700
|
|
|
701
|
+
# MCP status response types
|
|
702
|
+
|
|
703
|
+
# MCP server connection status values
|
|
704
|
+
MCP_SERVER_CONNECTION_STATUSES = %w[connected failed needs-auth pending disabled].freeze
|
|
705
|
+
|
|
706
|
+
# MCP server info (name and version)
|
|
707
|
+
class McpServerInfo
|
|
708
|
+
attr_accessor :name, :version
|
|
709
|
+
|
|
710
|
+
def initialize(name:, version: nil)
|
|
711
|
+
@name = name
|
|
712
|
+
@version = version
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# MCP tool annotation hints
|
|
717
|
+
class McpToolAnnotations
|
|
718
|
+
attr_accessor :read_only, :destructive, :open_world
|
|
719
|
+
|
|
720
|
+
def initialize(read_only: nil, destructive: nil, open_world: nil)
|
|
721
|
+
@read_only = read_only
|
|
722
|
+
@destructive = destructive
|
|
723
|
+
@open_world = open_world
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def self.parse(data)
|
|
727
|
+
return nil unless data
|
|
728
|
+
|
|
729
|
+
new(
|
|
730
|
+
read_only: data.key?(:readOnly) ? data[:readOnly] : data[:read_only],
|
|
731
|
+
destructive: data[:destructive],
|
|
732
|
+
open_world: data.key?(:openWorld) ? data[:openWorld] : data[:open_world]
|
|
733
|
+
)
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# MCP tool info (name, description, annotations)
|
|
738
|
+
class McpToolInfo
|
|
739
|
+
attr_accessor :name, :description, :annotations
|
|
740
|
+
|
|
741
|
+
def initialize(name:, description: nil, annotations: nil)
|
|
742
|
+
@name = name
|
|
743
|
+
@description = description
|
|
744
|
+
@annotations = annotations
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def self.parse(data)
|
|
748
|
+
new(
|
|
749
|
+
name: data[:name],
|
|
750
|
+
description: data[:description],
|
|
751
|
+
annotations: McpToolAnnotations.parse(data[:annotations])
|
|
752
|
+
)
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# Status of a single MCP server connection
|
|
757
|
+
class McpServerStatus
|
|
758
|
+
attr_accessor :name, :status, :server_info, :error, :config, :scope, :tools
|
|
759
|
+
|
|
760
|
+
def initialize(name:, status:, server_info: nil, error: nil, config: nil, scope: nil, tools: nil)
|
|
761
|
+
@name = name
|
|
762
|
+
@status = status
|
|
763
|
+
@server_info = server_info
|
|
764
|
+
@error = error
|
|
765
|
+
@config = config
|
|
766
|
+
@scope = scope
|
|
767
|
+
@tools = tools
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def self.parse(data)
|
|
771
|
+
server_info = (McpServerInfo.new(name: data[:serverInfo][:name], version: data[:serverInfo][:version]) if data[:serverInfo])
|
|
772
|
+
tools = data[:tools]&.map { |t| McpToolInfo.parse(t) }
|
|
773
|
+
|
|
774
|
+
new(
|
|
775
|
+
name: data[:name],
|
|
776
|
+
status: data[:status],
|
|
777
|
+
server_info: server_info,
|
|
778
|
+
error: data[:error],
|
|
779
|
+
config: data[:config],
|
|
780
|
+
scope: data[:scope],
|
|
781
|
+
tools: tools
|
|
782
|
+
)
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# Response from get_mcp_status containing all server statuses
|
|
787
|
+
class McpStatusResponse
|
|
788
|
+
attr_accessor :mcp_servers
|
|
789
|
+
|
|
790
|
+
def initialize(mcp_servers:)
|
|
791
|
+
@mcp_servers = mcp_servers
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def self.parse(data)
|
|
795
|
+
servers = (data[:mcpServers] || []).map { |s| McpServerStatus.parse(s) }
|
|
796
|
+
new(mcp_servers: servers)
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
635
800
|
# MCP Server configurations
|
|
636
801
|
class McpStdioServerConfig
|
|
637
802
|
attr_accessor :type, :command, :args, :env
|
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative 'claude_agent_sdk/message_parser'
|
|
|
10
10
|
require_relative 'claude_agent_sdk/query'
|
|
11
11
|
require_relative 'claude_agent_sdk/sdk_mcp_server'
|
|
12
12
|
require_relative 'claude_agent_sdk/streaming'
|
|
13
|
+
require_relative 'claude_agent_sdk/sessions'
|
|
13
14
|
require 'async'
|
|
14
15
|
require 'securerandom'
|
|
15
16
|
|
|
@@ -58,6 +59,25 @@ module ClaudeAgentSDK
|
|
|
58
59
|
# ClaudeAgentSDK.query(prompt: messages) do |message|
|
|
59
60
|
# puts message
|
|
60
61
|
# end
|
|
62
|
+
# List sessions for a directory (or all sessions)
|
|
63
|
+
# @param directory [String, nil] Working directory to list sessions for
|
|
64
|
+
# @param limit [Integer, nil] Maximum number of sessions to return
|
|
65
|
+
# @param include_worktrees [Boolean] Whether to include git worktree sessions
|
|
66
|
+
# @return [Array<SDKSessionInfo>] Sessions sorted by last_modified descending
|
|
67
|
+
def self.list_sessions(directory: nil, limit: nil, include_worktrees: true)
|
|
68
|
+
Sessions.list_sessions(directory: directory, limit: limit, include_worktrees: include_worktrees)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get messages from a session transcript
|
|
72
|
+
# @param session_id [String] The session UUID
|
|
73
|
+
# @param directory [String, nil] Working directory to search in
|
|
74
|
+
# @param limit [Integer, nil] Maximum number of messages
|
|
75
|
+
# @param offset [Integer] Number of messages to skip
|
|
76
|
+
# @return [Array<SessionMessage>] Ordered messages from the session
|
|
77
|
+
def self.get_session_messages(session_id:, directory: nil, limit: nil, offset: 0)
|
|
78
|
+
Sessions.get_session_messages(session_id: session_id, directory: directory, limit: limit, offset: offset)
|
|
79
|
+
end
|
|
80
|
+
|
|
61
81
|
def self.query(prompt:, options: nil, &block)
|
|
62
82
|
return enum_for(:query, prompt: prompt, options: options) unless block
|
|
63
83
|
|
|
@@ -310,6 +330,28 @@ module ClaudeAgentSDK
|
|
|
310
330
|
@query_handler.set_model(model)
|
|
311
331
|
end
|
|
312
332
|
|
|
333
|
+
# Reconnect a failed MCP server
|
|
334
|
+
# @param server_name [String] Name of the MCP server to reconnect
|
|
335
|
+
def reconnect_mcp_server(server_name)
|
|
336
|
+
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
337
|
+
@query_handler.reconnect_mcp_server(server_name)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Enable or disable an MCP server
|
|
341
|
+
# @param server_name [String] Name of the MCP server
|
|
342
|
+
# @param enabled [Boolean] Whether to enable or disable
|
|
343
|
+
def toggle_mcp_server(server_name, enabled)
|
|
344
|
+
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
345
|
+
@query_handler.toggle_mcp_server(server_name, enabled)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Stop a running background task
|
|
349
|
+
# @param task_id [String] The ID of the task to stop
|
|
350
|
+
def stop_task(task_id)
|
|
351
|
+
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
352
|
+
@query_handler.stop_task(task_id)
|
|
353
|
+
end
|
|
354
|
+
|
|
313
355
|
# Rewind files to a previous checkpoint (v0.1.15+)
|
|
314
356
|
# Restores file state to what it was at the given user message
|
|
315
357
|
# Requires enable_file_checkpointing to be true in options
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-agent-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Community Contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-03-05 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|
|
@@ -109,6 +109,7 @@ files:
|
|
|
109
109
|
- lib/claude_agent_sdk/message_parser.rb
|
|
110
110
|
- lib/claude_agent_sdk/query.rb
|
|
111
111
|
- lib/claude_agent_sdk/sdk_mcp_server.rb
|
|
112
|
+
- lib/claude_agent_sdk/sessions.rb
|
|
112
113
|
- lib/claude_agent_sdk/streaming.rb
|
|
113
114
|
- lib/claude_agent_sdk/subprocess_cli_transport.rb
|
|
114
115
|
- lib/claude_agent_sdk/transport.rb
|