claude-agent-sdk 0.4.0 → 0.4.2
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 +35 -3
- data/README.md +227 -11
- data/lib/claude_agent_sdk/query.rb +96 -28
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +11 -7
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +17 -9
- data/lib/claude_agent_sdk/types.rb +140 -4
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +58 -17
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db31632430bb28dc8f07903bfc8634a8c437da9dc988f2237606948ecc71f7e0
|
|
4
|
+
data.tar.gz: e7574810137063aef37a0a78fb9d855645a0a479ba0b909b63a491f0ea7060f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e394102a18bace179f7745b2b10cc721393ff25e8dcbc75f9492e6822aa6a80afa1455eb168b6567857cbf10a7d1d2e96e7293e43b043ad69ac2b41f95a3eb99
|
|
7
|
+
data.tar.gz: 03b94423b5d2369eb974e9cf82b2523e36aea4a09d05181e86528da11b3432d8685256705989210941b644333b5f9d3173b2efa0fd3b5f1b74e5e5aea3705bc6
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ 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.4.2] - 2026-02-07
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **MCP response fidelity:** Non-text content (images, binary data) is now preserved in SDK MCP tool responses instead of being silently dropped
|
|
12
|
+
- **MCP error key:** Tool error flag is now sent as `isError` (camelCase) matching the JSON-RPC spec, instead of `is_error` which the CLI ignored
|
|
13
|
+
- **MCP structured content:** `structuredContent` is now passed through in tool responses
|
|
14
|
+
- **ENV pollution:** `query()` and `Client.connect` no longer mutate the global `ENV`; entrypoint is passed via transport options
|
|
15
|
+
- **Symbol key env:** Fixed symbol keys in `env` option causing spawn failures (PR #7)
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `ClaudeAgentSDK.flexible_fetch` helper for tolerant hash key lookup (symbol/string, camelCase/snake_case)
|
|
19
|
+
- Gated real CLI integration tests (`RUN_REAL_INTEGRATION=1`) with budget cap
|
|
20
|
+
- `CLAUDE.md` architecture guide for contributors
|
|
21
|
+
|
|
22
|
+
## [0.4.1] - 2026-02-05
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
#### Hook Parity
|
|
27
|
+
- Added hook event support for `PostToolUseFailure`, `Notification`, `SubagentStart`, and `PermissionRequest`
|
|
28
|
+
- Expanded `SubagentStop` hook inputs with `agent_id`, `agent_transcript_path`, and `agent_type`
|
|
29
|
+
- Added hook-specific outputs for new hook events
|
|
30
|
+
- Added `updatedMCPToolOutput` support to `PostToolUse` hook outputs
|
|
31
|
+
|
|
32
|
+
#### MCP Status APIs
|
|
33
|
+
- `get_mcp_status` on `Query` and `Client` for live MCP connection status (streaming mode)
|
|
34
|
+
- `get_server_info` on `Client` as a parity alias for server initialization info
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- Hook input parsing now supports both symbol and string keys
|
|
38
|
+
- Hook callback timeouts are enforced and control request cancellation is handled cleanly
|
|
39
|
+
|
|
8
40
|
## [0.4.0] - 2026-01-06
|
|
9
41
|
|
|
10
42
|
### Added
|
|
@@ -54,7 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
54
86
|
### Improved
|
|
55
87
|
- Better long-term maintenance by leveraging official SDK updates
|
|
56
88
|
- Aligned with Python SDK implementation pattern (using official MCP library)
|
|
57
|
-
- All
|
|
89
|
+
- All tests passing with full backward compatibility maintained
|
|
58
90
|
|
|
59
91
|
### Technical Details
|
|
60
92
|
- Creates dynamic `MCP::Tool`, `MCP::Resource`, and `MCP::Prompt` classes from block-based definitions
|
|
@@ -75,7 +107,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
75
107
|
- **Critical:** Replaced `Async::Process` with Ruby's built-in `Open3` for subprocess management
|
|
76
108
|
- Fixed "uninitialized constant Async::Process" error that prevented the gem from working
|
|
77
109
|
- Process management now uses standard Ruby threads instead of async tasks
|
|
78
|
-
- All
|
|
110
|
+
- All tests passing
|
|
79
111
|
|
|
80
112
|
## [0.1.1] - 2025-10-14
|
|
81
113
|
|
|
@@ -84,7 +116,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
84
116
|
- Fixed issue where SDK couldn't find Claude Code when accessed via shell alias
|
|
85
117
|
|
|
86
118
|
### Added
|
|
87
|
-
- Comprehensive test suite
|
|
119
|
+
- Comprehensive test suite (RSpec)
|
|
88
120
|
- Test documentation in spec/README.md
|
|
89
121
|
|
|
90
122
|
### Changed
|
data/README.md
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
- [Tools Configuration](#tools-configuration)
|
|
23
23
|
- [Sandbox Settings](#sandbox-settings)
|
|
24
24
|
- [File Checkpointing & Rewind](#file-checkpointing--rewind)
|
|
25
|
+
- [Rails Integration](#rails-integration)
|
|
25
26
|
- [Types](#types)
|
|
26
27
|
- [Error Handling](#error-handling)
|
|
27
28
|
- [Examples](#examples)
|
|
@@ -33,6 +34,10 @@
|
|
|
33
34
|
Add this line to your application's Gemfile:
|
|
34
35
|
|
|
35
36
|
```ruby
|
|
37
|
+
# Recommended: Use the latest from GitHub for newest features
|
|
38
|
+
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
|
|
39
|
+
|
|
40
|
+
# Or use a stable version from RubyGems
|
|
36
41
|
gem 'claude-agent-sdk', '~> 0.4.0'
|
|
37
42
|
```
|
|
38
43
|
|
|
@@ -42,7 +47,7 @@ And then execute:
|
|
|
42
47
|
bundle install
|
|
43
48
|
```
|
|
44
49
|
|
|
45
|
-
Or install
|
|
50
|
+
Or install directly from RubyGems:
|
|
46
51
|
|
|
47
52
|
```bash
|
|
48
53
|
gem install claude-agent-sdk
|
|
@@ -53,6 +58,16 @@ gem install claude-agent-sdk
|
|
|
53
58
|
- Node.js
|
|
54
59
|
- Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
|
|
55
60
|
|
|
61
|
+
### Agentic Coding Skill
|
|
62
|
+
|
|
63
|
+
If you're using [Claude Code](https://claude.ai/claude-code) or another agentic coding tool that supports [skills](https://skills.sh), you can install the SDK skill:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx skills add https://github.com/ya-luotao/claude-agent-sdk-ruby --skill claude-agent-sdk-ruby
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This skill teaches your AI coding assistant about the SDK's APIs, patterns, and best practices, making it easier to get help writing code that uses this SDK.
|
|
70
|
+
|
|
56
71
|
## Quick Start
|
|
57
72
|
|
|
58
73
|
```ruby
|
|
@@ -204,10 +219,18 @@ Async do
|
|
|
204
219
|
# Change AI model during conversation
|
|
205
220
|
client.set_model('claude-sonnet-4-5')
|
|
206
221
|
|
|
222
|
+
# Get MCP server connection status
|
|
223
|
+
status = client.get_mcp_status
|
|
224
|
+
puts "MCP status: #{status}"
|
|
225
|
+
|
|
207
226
|
# Get server initialization info
|
|
208
227
|
info = client.server_info
|
|
209
228
|
puts "Available commands: #{info}"
|
|
210
229
|
|
|
230
|
+
# (Parity alias) Get server initialization info
|
|
231
|
+
info = client.get_server_info
|
|
232
|
+
puts "Available commands: #{info}"
|
|
233
|
+
|
|
211
234
|
client.disconnect
|
|
212
235
|
end.wait
|
|
213
236
|
```
|
|
@@ -365,6 +388,21 @@ For complete examples, see [examples/mcp_calculator.rb](examples/mcp_calculator.
|
|
|
365
388
|
|
|
366
389
|
A **hook** is a Ruby proc/lambda that the Claude Code *application* (*not* Claude) invokes at specific points of the Claude agent loop. Hooks can provide deterministic processing and automated feedback for Claude. Read more in [Claude Code Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks).
|
|
367
390
|
|
|
391
|
+
### Supported Events
|
|
392
|
+
|
|
393
|
+
All hook input objects include common fields like `session_id`, `transcript_path`, `cwd`, and `permission_mode`.
|
|
394
|
+
|
|
395
|
+
- `PreToolUse` → `PreToolUseHookInput` (`tool_name`, `tool_input`)
|
|
396
|
+
- `PostToolUse` → `PostToolUseHookInput` (`tool_name`, `tool_input`, `tool_response`)
|
|
397
|
+
- `PostToolUseFailure` → `PostToolUseFailureHookInput` (`tool_name`, `tool_input`, `tool_use_id`, `error`, `is_interrupt`)
|
|
398
|
+
- `UserPromptSubmit` → `UserPromptSubmitHookInput` (`prompt`)
|
|
399
|
+
- `Stop` → `StopHookInput` (`stop_hook_active`)
|
|
400
|
+
- `SubagentStop` → `SubagentStopHookInput` (`stop_hook_active`, `agent_id`, `agent_transcript_path`, `agent_type`)
|
|
401
|
+
- `PreCompact` → `PreCompactHookInput` (`trigger`, `custom_instructions`)
|
|
402
|
+
- `Notification` → `NotificationHookInput` (`message`, `title`, `notification_type`)
|
|
403
|
+
- `SubagentStart` → `SubagentStartHookInput` (`agent_id`, `agent_type`)
|
|
404
|
+
- `PermissionRequest` → `PermissionRequestHookInput` (`tool_name`, `tool_input`, `permission_suggestions`)
|
|
405
|
+
|
|
368
406
|
### Example
|
|
369
407
|
|
|
370
408
|
```ruby
|
|
@@ -373,13 +411,12 @@ require 'async'
|
|
|
373
411
|
|
|
374
412
|
Async do
|
|
375
413
|
# Define a hook that blocks dangerous bash commands
|
|
376
|
-
bash_hook = lambda do |input,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return {} unless tool_name == 'Bash'
|
|
414
|
+
bash_hook = lambda do |input, _tool_use_id, _context|
|
|
415
|
+
# Hook inputs are typed objects (e.g., PreToolUseHookInput) with Ruby-style accessors
|
|
416
|
+
return {} unless input.respond_to?(:tool_name) && input.tool_name == 'Bash'
|
|
381
417
|
|
|
382
|
-
|
|
418
|
+
tool_input = input.tool_input || {}
|
|
419
|
+
command = tool_input[:command] || tool_input['command'] || ''
|
|
383
420
|
block_patterns = ['rm -rf', 'foo.sh']
|
|
384
421
|
|
|
385
422
|
block_patterns.each do |pattern|
|
|
@@ -677,11 +714,164 @@ Async do
|
|
|
677
714
|
end
|
|
678
715
|
|
|
679
716
|
client.disconnect
|
|
680
|
-
end
|
|
717
|
+
end.wait
|
|
681
718
|
```
|
|
682
719
|
|
|
683
720
|
> **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.
|
|
684
721
|
|
|
722
|
+
## Rails Integration
|
|
723
|
+
|
|
724
|
+
The SDK integrates well with Rails applications. Here are common patterns:
|
|
725
|
+
|
|
726
|
+
### ActionCable Streaming
|
|
727
|
+
|
|
728
|
+
Stream Claude responses to the frontend in real-time:
|
|
729
|
+
|
|
730
|
+
```ruby
|
|
731
|
+
# app/jobs/chat_agent_job.rb
|
|
732
|
+
class ChatAgentJob < ApplicationJob
|
|
733
|
+
queue_as :claude_agents
|
|
734
|
+
|
|
735
|
+
def perform(chat_id, message_content)
|
|
736
|
+
Async do
|
|
737
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
738
|
+
system_prompt: { type: 'preset', preset: 'claude_code' },
|
|
739
|
+
permission_mode: 'bypassPermissions'
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
743
|
+
|
|
744
|
+
begin
|
|
745
|
+
client.connect
|
|
746
|
+
client.query(message_content)
|
|
747
|
+
|
|
748
|
+
client.receive_response do |message|
|
|
749
|
+
case message
|
|
750
|
+
when ClaudeAgentSDK::AssistantMessage
|
|
751
|
+
text = extract_text(message)
|
|
752
|
+
ChatChannel.broadcast_to(chat_id, { type: 'chunk', content: text })
|
|
753
|
+
|
|
754
|
+
when ClaudeAgentSDK::ResultMessage
|
|
755
|
+
ChatChannel.broadcast_to(chat_id, {
|
|
756
|
+
type: 'complete',
|
|
757
|
+
content: message.result,
|
|
758
|
+
cost: message.total_cost_usd
|
|
759
|
+
})
|
|
760
|
+
end
|
|
761
|
+
end
|
|
762
|
+
ensure
|
|
763
|
+
client.disconnect
|
|
764
|
+
end
|
|
765
|
+
end.wait
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
private
|
|
769
|
+
|
|
770
|
+
def extract_text(message)
|
|
771
|
+
message.content
|
|
772
|
+
.select { |b| b.is_a?(ClaudeAgentSDK::TextBlock) }
|
|
773
|
+
.map(&:text)
|
|
774
|
+
.join("\n\n")
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Session Resumption
|
|
780
|
+
|
|
781
|
+
Persist Claude sessions for multi-turn conversations:
|
|
782
|
+
|
|
783
|
+
```ruby
|
|
784
|
+
# app/models/chat_session.rb
|
|
785
|
+
class ChatSession < ApplicationRecord
|
|
786
|
+
# Columns: id, claude_session_id, user_id, created_at, updated_at
|
|
787
|
+
|
|
788
|
+
def send_message(content)
|
|
789
|
+
options = build_options
|
|
790
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
791
|
+
|
|
792
|
+
Async do
|
|
793
|
+
client.connect
|
|
794
|
+
client.query(content, session_id: claude_session_id ? nil : generate_session_id)
|
|
795
|
+
|
|
796
|
+
client.receive_response do |message|
|
|
797
|
+
if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
798
|
+
# Save session ID for next message
|
|
799
|
+
update!(claude_session_id: message.session_id)
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
ensure
|
|
803
|
+
client.disconnect
|
|
804
|
+
end.wait
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
private
|
|
808
|
+
|
|
809
|
+
def build_options
|
|
810
|
+
opts = {
|
|
811
|
+
permission_mode: 'bypassPermissions',
|
|
812
|
+
setting_sources: []
|
|
813
|
+
}
|
|
814
|
+
opts[:resume] = claude_session_id if claude_session_id.present?
|
|
815
|
+
ClaudeAgentSDK::ClaudeAgentOptions.new(**opts)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def generate_session_id
|
|
819
|
+
"chat_#{id}_#{Time.current.to_i}"
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### Background Jobs with Error Handling
|
|
825
|
+
|
|
826
|
+
```ruby
|
|
827
|
+
class ClaudeAgentJob < ApplicationJob
|
|
828
|
+
queue_as :claude_agents
|
|
829
|
+
retry_on ClaudeAgentSDK::ProcessError, wait: :polynomially_longer, attempts: 3
|
|
830
|
+
|
|
831
|
+
def perform(task_id)
|
|
832
|
+
task = Task.find(task_id)
|
|
833
|
+
|
|
834
|
+
Async do
|
|
835
|
+
execute_agent(task)
|
|
836
|
+
end.wait
|
|
837
|
+
|
|
838
|
+
rescue ClaudeAgentSDK::CLINotFoundError => e
|
|
839
|
+
task.update!(status: 'failed', error: 'Claude CLI not installed')
|
|
840
|
+
raise
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
private
|
|
844
|
+
|
|
845
|
+
def execute_agent(task)
|
|
846
|
+
# ... agent execution
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### HTTP MCP Servers
|
|
852
|
+
|
|
853
|
+
Connect to remote tool services:
|
|
854
|
+
|
|
855
|
+
```ruby
|
|
856
|
+
mcp_servers = {
|
|
857
|
+
'api_tools' => ClaudeAgentSDK::McpHttpServerConfig.new(
|
|
858
|
+
url: ENV['MCP_SERVER_URL'],
|
|
859
|
+
headers: { 'Authorization' => "Bearer #{ENV['MCP_TOKEN']}" }
|
|
860
|
+
).to_h
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
864
|
+
mcp_servers: mcp_servers,
|
|
865
|
+
permission_mode: 'bypassPermissions'
|
|
866
|
+
)
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
For complete examples, see:
|
|
870
|
+
- [examples/rails_actioncable_example.rb](examples/rails_actioncable_example.rb)
|
|
871
|
+
- [examples/rails_background_job_example.rb](examples/rails_background_job_example.rb)
|
|
872
|
+
- [examples/session_resumption_example.rb](examples/session_resumption_example.rb)
|
|
873
|
+
- [examples/http_mcp_server_example.rb](examples/http_mcp_server_example.rb)
|
|
874
|
+
|
|
685
875
|
## Types
|
|
686
876
|
|
|
687
877
|
See [lib/claude_agent_sdk/types.rb](lib/claude_agent_sdk/types.rb) for complete type definitions.
|
|
@@ -926,22 +1116,48 @@ See the [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-co
|
|
|
926
1116
|
|
|
927
1117
|
## Examples
|
|
928
1118
|
|
|
1119
|
+
### Core Examples
|
|
1120
|
+
|
|
929
1121
|
| Example | Description |
|
|
930
1122
|
|---------|-------------|
|
|
931
1123
|
| [examples/quick_start.rb](examples/quick_start.rb) | Basic `query()` usage with options |
|
|
932
1124
|
| [examples/client_example.rb](examples/client_example.rb) | Interactive Client usage |
|
|
933
1125
|
| [examples/streaming_input_example.rb](examples/streaming_input_example.rb) | Streaming input for multi-turn conversations |
|
|
1126
|
+
| [examples/session_resumption_example.rb](examples/session_resumption_example.rb) | Multi-turn conversations with session persistence |
|
|
1127
|
+
| [examples/structured_output_example.rb](examples/structured_output_example.rb) | JSON schema structured output |
|
|
1128
|
+
| [examples/error_handling_example.rb](examples/error_handling_example.rb) | Error handling with `AssistantMessage.error` |
|
|
1129
|
+
|
|
1130
|
+
### MCP Server Examples
|
|
1131
|
+
|
|
1132
|
+
| Example | Description |
|
|
1133
|
+
|---------|-------------|
|
|
934
1134
|
| [examples/mcp_calculator.rb](examples/mcp_calculator.rb) | Custom tools with SDK MCP servers |
|
|
935
1135
|
| [examples/mcp_resources_prompts_example.rb](examples/mcp_resources_prompts_example.rb) | MCP resources and prompts |
|
|
1136
|
+
| [examples/http_mcp_server_example.rb](examples/http_mcp_server_example.rb) | HTTP/SSE MCP server configuration |
|
|
1137
|
+
|
|
1138
|
+
### Hooks & Permissions
|
|
1139
|
+
|
|
1140
|
+
| Example | Description |
|
|
1141
|
+
|---------|-------------|
|
|
936
1142
|
| [examples/hooks_example.rb](examples/hooks_example.rb) | Using hooks to control tool execution |
|
|
1143
|
+
| [examples/advanced_hooks_example.rb](examples/advanced_hooks_example.rb) | Typed hook inputs/outputs |
|
|
937
1144
|
| [examples/permission_callback_example.rb](examples/permission_callback_example.rb) | Dynamic tool permission control |
|
|
938
|
-
|
|
1145
|
+
|
|
1146
|
+
### Advanced Features
|
|
1147
|
+
|
|
1148
|
+
| Example | Description |
|
|
1149
|
+
|---------|-------------|
|
|
939
1150
|
| [examples/budget_control_example.rb](examples/budget_control_example.rb) | Budget control with `max_budget_usd` |
|
|
940
1151
|
| [examples/fallback_model_example.rb](examples/fallback_model_example.rb) | Fallback model configuration |
|
|
941
|
-
| [examples/advanced_hooks_example.rb](examples/advanced_hooks_example.rb) | Typed hook inputs/outputs |
|
|
942
|
-
| [examples/error_handling_example.rb](examples/error_handling_example.rb) | Error handling with `AssistantMessage.error` |
|
|
943
1152
|
| [examples/extended_thinking_example.rb](examples/extended_thinking_example.rb) | Extended thinking (API parity) |
|
|
944
1153
|
|
|
1154
|
+
### Rails Integration
|
|
1155
|
+
|
|
1156
|
+
| Example | Description |
|
|
1157
|
+
|---------|-------------|
|
|
1158
|
+
| [examples/rails_actioncable_example.rb](examples/rails_actioncable_example.rb) | ActionCable streaming to frontend |
|
|
1159
|
+
| [examples/rails_background_job_example.rb](examples/rails_background_job_example.rb) | Background jobs with session resumption |
|
|
1160
|
+
|
|
945
1161
|
## Development
|
|
946
1162
|
|
|
947
1163
|
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
|
@@ -30,8 +30,10 @@ module ClaudeAgentSDK
|
|
|
30
30
|
@pending_control_responses = {}
|
|
31
31
|
@pending_control_results = {}
|
|
32
32
|
@hook_callbacks = {}
|
|
33
|
+
@hook_callback_timeouts = {}
|
|
33
34
|
@next_callback_id = 0
|
|
34
35
|
@request_counter = 0
|
|
36
|
+
@inflight_control_request_tasks = {}
|
|
35
37
|
|
|
36
38
|
# Message stream
|
|
37
39
|
@message_queue = Async::Queue.new
|
|
@@ -59,6 +61,7 @@ module ClaudeAgentSDK
|
|
|
59
61
|
callback_id = "hook_#{@next_callback_id}"
|
|
60
62
|
@next_callback_id += 1
|
|
61
63
|
@hook_callbacks[callback_id] = callback
|
|
64
|
+
@hook_callback_timeouts[callback_id] = matcher[:timeout] if matcher[:timeout]
|
|
62
65
|
callback_ids << callback_id
|
|
63
66
|
end
|
|
64
67
|
hooks_config[event] << {
|
|
@@ -103,9 +106,19 @@ module ClaudeAgentSDK
|
|
|
103
106
|
when 'control_response'
|
|
104
107
|
handle_control_response(message)
|
|
105
108
|
when 'control_request'
|
|
106
|
-
|
|
109
|
+
request_id = message[:request_id]
|
|
110
|
+
task = Async do
|
|
111
|
+
begin
|
|
112
|
+
handle_control_request(message)
|
|
113
|
+
ensure
|
|
114
|
+
@inflight_control_request_tasks.delete(request_id) if request_id
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
@inflight_control_request_tasks[request_id] = task if request_id
|
|
107
118
|
when 'control_cancel_request'
|
|
108
|
-
|
|
119
|
+
request_id = message[:request_id] || message[:requestId]
|
|
120
|
+
task = request_id ? @inflight_control_request_tasks[request_id] : nil
|
|
121
|
+
task&.stop
|
|
109
122
|
next
|
|
110
123
|
else
|
|
111
124
|
# Regular SDK messages go to the queue
|
|
@@ -163,6 +176,17 @@ module ClaudeAgentSDK
|
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
178
|
@transport.write(JSON.generate(success_response) + "\n")
|
|
179
|
+
rescue Async::Stop
|
|
180
|
+
# Cancellation requested; respond with an error so the CLI can unblock.
|
|
181
|
+
cancelled_response = {
|
|
182
|
+
type: 'control_response',
|
|
183
|
+
response: {
|
|
184
|
+
subtype: 'error',
|
|
185
|
+
request_id: request_id,
|
|
186
|
+
error: 'Cancelled'
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
@transport.write(JSON.generate(cancelled_response) + "\n")
|
|
166
190
|
rescue StandardError => e
|
|
167
191
|
# Send error response
|
|
168
192
|
error_response = {
|
|
@@ -228,7 +252,17 @@ module ClaudeAgentSDK
|
|
|
228
252
|
hook_input,
|
|
229
253
|
request_data[:tool_use_id],
|
|
230
254
|
context
|
|
231
|
-
)
|
|
255
|
+
) unless @hook_callback_timeouts[callback_id]
|
|
256
|
+
|
|
257
|
+
if (timeout = @hook_callback_timeouts[callback_id])
|
|
258
|
+
hook_output = Async::Task.current.with_timeout(timeout) do
|
|
259
|
+
callback.call(
|
|
260
|
+
hook_input,
|
|
261
|
+
request_data[:tool_use_id],
|
|
262
|
+
context
|
|
263
|
+
)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
232
266
|
|
|
233
267
|
# Convert Ruby-safe field names to CLI-expected names
|
|
234
268
|
convert_hook_output_for_cli(hook_output)
|
|
@@ -236,46 +270,79 @@ module ClaudeAgentSDK
|
|
|
236
270
|
|
|
237
271
|
def parse_hook_input(input_data)
|
|
238
272
|
event_name = input_data[:hook_event_name] || input_data['hook_event_name']
|
|
273
|
+
fetch = ->(key) { input_data[key] || input_data[key.to_s] }
|
|
239
274
|
base_args = {
|
|
240
|
-
session_id:
|
|
241
|
-
transcript_path:
|
|
242
|
-
cwd:
|
|
243
|
-
permission_mode:
|
|
275
|
+
session_id: fetch.call(:session_id),
|
|
276
|
+
transcript_path: fetch.call(:transcript_path),
|
|
277
|
+
cwd: fetch.call(:cwd),
|
|
278
|
+
permission_mode: fetch.call(:permission_mode)
|
|
244
279
|
}
|
|
245
280
|
|
|
246
281
|
case event_name
|
|
247
282
|
when 'PreToolUse'
|
|
248
283
|
PreToolUseHookInput.new(
|
|
249
|
-
tool_name:
|
|
250
|
-
tool_input:
|
|
284
|
+
tool_name: fetch.call(:tool_name),
|
|
285
|
+
tool_input: fetch.call(:tool_input),
|
|
251
286
|
**base_args
|
|
252
287
|
)
|
|
253
288
|
when 'PostToolUse'
|
|
254
289
|
PostToolUseHookInput.new(
|
|
255
|
-
tool_name:
|
|
256
|
-
tool_input:
|
|
257
|
-
tool_response:
|
|
290
|
+
tool_name: fetch.call(:tool_name),
|
|
291
|
+
tool_input: fetch.call(:tool_input),
|
|
292
|
+
tool_response: fetch.call(:tool_response),
|
|
293
|
+
**base_args
|
|
294
|
+
)
|
|
295
|
+
when 'PostToolUseFailure'
|
|
296
|
+
PostToolUseFailureHookInput.new(
|
|
297
|
+
tool_name: fetch.call(:tool_name),
|
|
298
|
+
tool_input: fetch.call(:tool_input),
|
|
299
|
+
tool_use_id: fetch.call(:tool_use_id),
|
|
300
|
+
error: fetch.call(:error),
|
|
301
|
+
is_interrupt: fetch.call(:is_interrupt),
|
|
258
302
|
**base_args
|
|
259
303
|
)
|
|
260
304
|
when 'UserPromptSubmit'
|
|
261
305
|
UserPromptSubmitHookInput.new(
|
|
262
|
-
prompt:
|
|
306
|
+
prompt: fetch.call(:prompt),
|
|
263
307
|
**base_args
|
|
264
308
|
)
|
|
265
309
|
when 'Stop'
|
|
266
310
|
StopHookInput.new(
|
|
267
|
-
stop_hook_active:
|
|
311
|
+
stop_hook_active: fetch.call(:stop_hook_active),
|
|
268
312
|
**base_args
|
|
269
313
|
)
|
|
270
314
|
when 'SubagentStop'
|
|
271
315
|
SubagentStopHookInput.new(
|
|
272
|
-
stop_hook_active:
|
|
316
|
+
stop_hook_active: fetch.call(:stop_hook_active),
|
|
317
|
+
agent_id: fetch.call(:agent_id),
|
|
318
|
+
agent_transcript_path: fetch.call(:agent_transcript_path),
|
|
319
|
+
agent_type: fetch.call(:agent_type),
|
|
320
|
+
**base_args
|
|
321
|
+
)
|
|
322
|
+
when 'Notification'
|
|
323
|
+
NotificationHookInput.new(
|
|
324
|
+
message: fetch.call(:message),
|
|
325
|
+
title: fetch.call(:title),
|
|
326
|
+
notification_type: fetch.call(:notification_type),
|
|
327
|
+
**base_args
|
|
328
|
+
)
|
|
329
|
+
when 'SubagentStart'
|
|
330
|
+
SubagentStartHookInput.new(
|
|
331
|
+
agent_id: fetch.call(:agent_id),
|
|
332
|
+
agent_type: fetch.call(:agent_type),
|
|
333
|
+
**base_args
|
|
334
|
+
)
|
|
335
|
+
when 'PermissionRequest'
|
|
336
|
+
PermissionRequestHookInput.new(
|
|
337
|
+
tool_name: fetch.call(:tool_name),
|
|
338
|
+
tool_input: fetch.call(:tool_input),
|
|
339
|
+
permission_suggestions: fetch.call(:permission_suggestions),
|
|
273
340
|
**base_args
|
|
274
341
|
)
|
|
275
342
|
when 'PreCompact'
|
|
276
343
|
PreCompactHookInput.new(
|
|
277
|
-
trigger:
|
|
278
|
-
custom_instructions:
|
|
344
|
+
trigger: fetch.call(:trigger),
|
|
345
|
+
custom_instructions: fetch.call(:custom_instructions),
|
|
279
346
|
**base_args
|
|
280
347
|
)
|
|
281
348
|
else
|
|
@@ -455,19 +522,14 @@ module ClaudeAgentSDK
|
|
|
455
522
|
|
|
456
523
|
# Call the tool
|
|
457
524
|
result = server.call_tool(tool_name, arguments)
|
|
525
|
+
content = ClaudeAgentSDK.flexible_fetch(result, 'content', 'content') || []
|
|
526
|
+
response_data = { content: content }
|
|
458
527
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if result[:content]
|
|
462
|
-
result[:content].each do |item|
|
|
463
|
-
if item[:type] == 'text'
|
|
464
|
-
content << { type: 'text', text: item[:text] }
|
|
465
|
-
end
|
|
466
|
-
end
|
|
467
|
-
end
|
|
528
|
+
is_error = ClaudeAgentSDK.flexible_fetch(result, 'isError', 'is_error')
|
|
529
|
+
response_data[:isError] = !!is_error unless is_error.nil?
|
|
468
530
|
|
|
469
|
-
|
|
470
|
-
response_data[:
|
|
531
|
+
structured_content = ClaudeAgentSDK.flexible_fetch(result, 'structuredContent', 'structured_content')
|
|
532
|
+
response_data[:structuredContent] = structured_content unless structured_content.nil?
|
|
471
533
|
|
|
472
534
|
{
|
|
473
535
|
jsonrpc: '2.0',
|
|
@@ -530,6 +592,12 @@ module ClaudeAgentSDK
|
|
|
530
592
|
|
|
531
593
|
public
|
|
532
594
|
|
|
595
|
+
# Get current MCP server connection status (only works with streaming mode)
|
|
596
|
+
# @return [Hash] MCP status information, including mcpServers list
|
|
597
|
+
def get_mcp_status
|
|
598
|
+
send_control_request({ subtype: 'mcp_status' })
|
|
599
|
+
end
|
|
600
|
+
|
|
533
601
|
# Send interrupt control request
|
|
534
602
|
def interrupt
|
|
535
603
|
send_control_request({ subtype: 'interrupt' })
|
|
@@ -172,15 +172,19 @@ module ClaudeAgentSDK
|
|
|
172
172
|
# Filter out server_context and pass remaining args to handler
|
|
173
173
|
result = @tool_def.handler.call(args)
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
{
|
|
178
|
-
type: item[:type],
|
|
179
|
-
text: item[:text]
|
|
180
|
-
}
|
|
175
|
+
content = ClaudeAgentSDK.flexible_fetch(result, 'content', 'content')
|
|
176
|
+
unless result.is_a?(Hash) && content
|
|
177
|
+
raise "Tool '#{@tool_def.name}' must return a hash with :content key"
|
|
181
178
|
end
|
|
182
179
|
|
|
183
|
-
|
|
180
|
+
is_error = ClaudeAgentSDK.flexible_fetch(result, 'isError', 'is_error')
|
|
181
|
+
structured_content = ClaudeAgentSDK.flexible_fetch(result, 'structuredContent', 'structured_content')
|
|
182
|
+
|
|
183
|
+
MCP::Tool::Response.new(
|
|
184
|
+
content,
|
|
185
|
+
error: !!is_error,
|
|
186
|
+
structured_content: structured_content
|
|
187
|
+
)
|
|
184
188
|
end
|
|
185
189
|
|
|
186
190
|
private
|
|
@@ -64,10 +64,17 @@ module ClaudeAgentSDK
|
|
|
64
64
|
if @options.system_prompt
|
|
65
65
|
if @options.system_prompt.is_a?(String)
|
|
66
66
|
cmd.concat(['--system-prompt', @options.system_prompt])
|
|
67
|
-
elsif @options.system_prompt.is_a?(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
elsif @options.system_prompt.is_a?(SystemPromptPreset)
|
|
68
|
+
cmd.concat(['--system-prompt-preset', @options.system_prompt.preset]) if @options.system_prompt.preset
|
|
69
|
+
cmd.concat(['--append-system-prompt', @options.system_prompt.append]) if @options.system_prompt.append
|
|
70
|
+
elsif @options.system_prompt.is_a?(Hash)
|
|
71
|
+
prompt_type = @options.system_prompt[:type] || @options.system_prompt['type']
|
|
72
|
+
if prompt_type == 'preset'
|
|
73
|
+
preset = @options.system_prompt[:preset] || @options.system_prompt['preset']
|
|
74
|
+
append = @options.system_prompt[:append] || @options.system_prompt['append']
|
|
75
|
+
cmd.concat(['--system-prompt-preset', preset]) if preset
|
|
76
|
+
cmd.concat(['--append-system-prompt', append]) if append
|
|
77
|
+
end
|
|
71
78
|
end
|
|
72
79
|
end
|
|
73
80
|
|
|
@@ -246,10 +253,10 @@ module ClaudeAgentSDK
|
|
|
246
253
|
cmd = build_command
|
|
247
254
|
|
|
248
255
|
# Build environment
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
256
|
+
# Convert symbol keys to strings for spawn compatibility
|
|
257
|
+
custom_env = @options.env.transform_keys { |k| k.to_s }
|
|
258
|
+
process_env = ENV.to_h.merge('CLAUDE_AGENT_SDK_VERSION' => VERSION).merge(custom_env)
|
|
259
|
+
process_env['CLAUDE_CODE_ENTRYPOINT'] ||= 'sdk-rb'
|
|
253
260
|
process_env['PWD'] = @cwd.to_s if @cwd
|
|
254
261
|
|
|
255
262
|
# Determine stderr handling
|
|
@@ -446,7 +453,8 @@ module ClaudeAgentSDK
|
|
|
446
453
|
|
|
447
454
|
def check_claude_version
|
|
448
455
|
begin
|
|
449
|
-
|
|
456
|
+
stdout, stderr, = Open3.capture3(@cli_path.to_s, '-v')
|
|
457
|
+
output = (stdout.to_s + stderr.to_s).strip
|
|
450
458
|
if match = output.match(/([0-9]+\.[0-9]+\.[0-9]+)/)
|
|
451
459
|
version = match[1]
|
|
452
460
|
version_parts = version.split('.').map(&:to_i)
|
|
@@ -14,7 +14,18 @@ module ClaudeAgentSDK
|
|
|
14
14
|
PERMISSION_BEHAVIORS = %w[allow deny ask].freeze
|
|
15
15
|
|
|
16
16
|
# Type constants for hook events
|
|
17
|
-
HOOK_EVENTS = %w[
|
|
17
|
+
HOOK_EVENTS = %w[
|
|
18
|
+
PreToolUse
|
|
19
|
+
PostToolUse
|
|
20
|
+
PostToolUseFailure
|
|
21
|
+
UserPromptSubmit
|
|
22
|
+
Stop
|
|
23
|
+
SubagentStop
|
|
24
|
+
PreCompact
|
|
25
|
+
Notification
|
|
26
|
+
SubagentStart
|
|
27
|
+
PermissionRequest
|
|
28
|
+
].freeze
|
|
18
29
|
|
|
19
30
|
# Type constants for assistant message errors
|
|
20
31
|
ASSISTANT_MESSAGE_ERRORS = %w[authentication_failed billing_error rate_limit invalid_request server_error unknown].freeze
|
|
@@ -305,12 +316,71 @@ module ClaudeAgentSDK
|
|
|
305
316
|
|
|
306
317
|
# SubagentStop hook input
|
|
307
318
|
class SubagentStopHookInput < BaseHookInput
|
|
308
|
-
attr_accessor :hook_event_name, :stop_hook_active
|
|
319
|
+
attr_accessor :hook_event_name, :stop_hook_active, :agent_id, :agent_transcript_path, :agent_type
|
|
309
320
|
|
|
310
|
-
def initialize(hook_event_name: 'SubagentStop', stop_hook_active: false,
|
|
321
|
+
def initialize(hook_event_name: 'SubagentStop', stop_hook_active: false, agent_id: nil,
|
|
322
|
+
agent_transcript_path: nil, agent_type: nil, **base_args)
|
|
311
323
|
super(**base_args)
|
|
312
324
|
@hook_event_name = hook_event_name
|
|
313
325
|
@stop_hook_active = stop_hook_active
|
|
326
|
+
@agent_id = agent_id
|
|
327
|
+
@agent_transcript_path = agent_transcript_path
|
|
328
|
+
@agent_type = agent_type
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# PostToolUseFailure hook input
|
|
333
|
+
class PostToolUseFailureHookInput < BaseHookInput
|
|
334
|
+
attr_accessor :hook_event_name, :tool_name, :tool_input, :tool_use_id, :error, :is_interrupt
|
|
335
|
+
|
|
336
|
+
def initialize(hook_event_name: 'PostToolUseFailure', tool_name: nil, tool_input: nil, tool_use_id: nil,
|
|
337
|
+
error: nil, is_interrupt: nil, **base_args)
|
|
338
|
+
super(**base_args)
|
|
339
|
+
@hook_event_name = hook_event_name
|
|
340
|
+
@tool_name = tool_name
|
|
341
|
+
@tool_input = tool_input
|
|
342
|
+
@tool_use_id = tool_use_id
|
|
343
|
+
@error = error
|
|
344
|
+
@is_interrupt = is_interrupt
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Notification hook input
|
|
349
|
+
class NotificationHookInput < BaseHookInput
|
|
350
|
+
attr_accessor :hook_event_name, :message, :title, :notification_type
|
|
351
|
+
|
|
352
|
+
def initialize(hook_event_name: 'Notification', message: nil, title: nil, notification_type: nil, **base_args)
|
|
353
|
+
super(**base_args)
|
|
354
|
+
@hook_event_name = hook_event_name
|
|
355
|
+
@message = message
|
|
356
|
+
@title = title
|
|
357
|
+
@notification_type = notification_type
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# SubagentStart hook input
|
|
362
|
+
class SubagentStartHookInput < BaseHookInput
|
|
363
|
+
attr_accessor :hook_event_name, :agent_id, :agent_type
|
|
364
|
+
|
|
365
|
+
def initialize(hook_event_name: 'SubagentStart', agent_id: nil, agent_type: nil, **base_args)
|
|
366
|
+
super(**base_args)
|
|
367
|
+
@hook_event_name = hook_event_name
|
|
368
|
+
@agent_id = agent_id
|
|
369
|
+
@agent_type = agent_type
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# PermissionRequest hook input
|
|
374
|
+
class PermissionRequestHookInput < BaseHookInput
|
|
375
|
+
attr_accessor :hook_event_name, :tool_name, :tool_input, :permission_suggestions
|
|
376
|
+
|
|
377
|
+
def initialize(hook_event_name: 'PermissionRequest', tool_name: nil, tool_input: nil, permission_suggestions: nil,
|
|
378
|
+
**base_args)
|
|
379
|
+
super(**base_args)
|
|
380
|
+
@hook_event_name = hook_event_name
|
|
381
|
+
@tool_name = tool_name
|
|
382
|
+
@tool_input = tool_input
|
|
383
|
+
@permission_suggestions = permission_suggestions
|
|
314
384
|
end
|
|
315
385
|
end
|
|
316
386
|
|
|
@@ -348,10 +418,28 @@ module ClaudeAgentSDK
|
|
|
348
418
|
|
|
349
419
|
# PostToolUse hook specific output
|
|
350
420
|
class PostToolUseHookSpecificOutput
|
|
421
|
+
attr_accessor :hook_event_name, :additional_context, :updated_mcp_tool_output
|
|
422
|
+
|
|
423
|
+
def initialize(additional_context: nil, updated_mcp_tool_output: nil)
|
|
424
|
+
@hook_event_name = 'PostToolUse'
|
|
425
|
+
@additional_context = additional_context
|
|
426
|
+
@updated_mcp_tool_output = updated_mcp_tool_output
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def to_h
|
|
430
|
+
result = { hookEventName: @hook_event_name }
|
|
431
|
+
result[:additionalContext] = @additional_context if @additional_context
|
|
432
|
+
result[:updatedMCPToolOutput] = @updated_mcp_tool_output if @updated_mcp_tool_output
|
|
433
|
+
result
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# PostToolUseFailure hook specific output
|
|
438
|
+
class PostToolUseFailureHookSpecificOutput
|
|
351
439
|
attr_accessor :hook_event_name, :additional_context
|
|
352
440
|
|
|
353
441
|
def initialize(additional_context: nil)
|
|
354
|
-
@hook_event_name = '
|
|
442
|
+
@hook_event_name = 'PostToolUseFailure'
|
|
355
443
|
@additional_context = additional_context
|
|
356
444
|
end
|
|
357
445
|
|
|
@@ -378,6 +466,54 @@ module ClaudeAgentSDK
|
|
|
378
466
|
end
|
|
379
467
|
end
|
|
380
468
|
|
|
469
|
+
# Notification hook specific output
|
|
470
|
+
class NotificationHookSpecificOutput
|
|
471
|
+
attr_accessor :hook_event_name, :additional_context
|
|
472
|
+
|
|
473
|
+
def initialize(additional_context: nil)
|
|
474
|
+
@hook_event_name = 'Notification'
|
|
475
|
+
@additional_context = additional_context
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def to_h
|
|
479
|
+
result = { hookEventName: @hook_event_name }
|
|
480
|
+
result[:additionalContext] = @additional_context if @additional_context
|
|
481
|
+
result
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# SubagentStart hook specific output
|
|
486
|
+
class SubagentStartHookSpecificOutput
|
|
487
|
+
attr_accessor :hook_event_name, :additional_context
|
|
488
|
+
|
|
489
|
+
def initialize(additional_context: nil)
|
|
490
|
+
@hook_event_name = 'SubagentStart'
|
|
491
|
+
@additional_context = additional_context
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def to_h
|
|
495
|
+
result = { hookEventName: @hook_event_name }
|
|
496
|
+
result[:additionalContext] = @additional_context if @additional_context
|
|
497
|
+
result
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# PermissionRequest hook specific output
|
|
502
|
+
class PermissionRequestHookSpecificOutput
|
|
503
|
+
attr_accessor :hook_event_name, :decision
|
|
504
|
+
|
|
505
|
+
def initialize(decision: nil)
|
|
506
|
+
@hook_event_name = 'PermissionRequest'
|
|
507
|
+
@decision = decision
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def to_h
|
|
511
|
+
result = { hookEventName: @hook_event_name }
|
|
512
|
+
result[:decision] = @decision if @decision
|
|
513
|
+
result
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
381
517
|
# SessionStart hook specific output
|
|
382
518
|
class SessionStartHookSpecificOutput
|
|
383
519
|
attr_accessor :hook_event_name, :additional_context
|
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -14,6 +14,16 @@ require 'securerandom'
|
|
|
14
14
|
|
|
15
15
|
# Claude Agent SDK for Ruby
|
|
16
16
|
module ClaudeAgentSDK
|
|
17
|
+
# Look up a value in a hash that may use symbol or string keys in camelCase or snake_case.
|
|
18
|
+
# Returns the first non-nil value found, preserving false as a meaningful value.
|
|
19
|
+
def self.flexible_fetch(hash, camel_key, snake_key)
|
|
20
|
+
val = hash[camel_key.to_sym]
|
|
21
|
+
val = hash[camel_key.to_s] if val.nil?
|
|
22
|
+
val = hash[snake_key.to_sym] if val.nil?
|
|
23
|
+
val = hash[snake_key.to_s] if val.nil?
|
|
24
|
+
val
|
|
25
|
+
end
|
|
26
|
+
|
|
17
27
|
# Query Claude Code for one-shot or unidirectional streaming interactions
|
|
18
28
|
#
|
|
19
29
|
# This function is ideal for simple, stateless queries where you don't need
|
|
@@ -51,7 +61,7 @@ module ClaudeAgentSDK
|
|
|
51
61
|
return enum_for(:query, prompt: prompt, options: options) unless block
|
|
52
62
|
|
|
53
63
|
options ||= ClaudeAgentOptions.new
|
|
54
|
-
|
|
64
|
+
options = options.dup_with(env: (options.env || {}).merge('CLAUDE_CODE_ENTRYPOINT' => 'sdk-rb'))
|
|
55
65
|
|
|
56
66
|
Async do
|
|
57
67
|
transport = SubprocessCLITransport.new(prompt, options)
|
|
@@ -126,36 +136,37 @@ module ClaudeAgentSDK
|
|
|
126
136
|
@transport = nil
|
|
127
137
|
@query_handler = nil
|
|
128
138
|
@connected = false
|
|
129
|
-
ENV['CLAUDE_CODE_ENTRYPOINT'] = 'sdk-rb-client'
|
|
130
139
|
end
|
|
131
140
|
|
|
132
|
-
# Connect to Claude with optional initial prompt
|
|
141
|
+
# Connect to Claude with optional initial prompt.
|
|
142
|
+
#
|
|
143
|
+
# Client always uses streaming mode for bidirectional communication. If you
|
|
144
|
+
# pass a String, it will be sent as an initial user message after the
|
|
145
|
+
# connection is established. If you pass an Enumerator, it should yield
|
|
146
|
+
# JSONL messages (e.g., from ClaudeAgentSDK::Streaming.user_message).
|
|
147
|
+
#
|
|
133
148
|
# @param prompt [String, Enumerator, nil] Initial prompt or message stream
|
|
134
149
|
def connect(prompt = nil)
|
|
135
150
|
return if @connected
|
|
136
151
|
|
|
152
|
+
raise ArgumentError, "prompt must be a String, an Enumerator, or nil (got #{prompt.class})" unless prompt.nil? || prompt.is_a?(String) || prompt.respond_to?(:each)
|
|
153
|
+
|
|
137
154
|
# Validate and configure permission settings
|
|
138
155
|
configured_options = @options
|
|
139
156
|
if @options.can_use_tool
|
|
140
|
-
# can_use_tool requires streaming mode
|
|
141
|
-
if prompt.is_a?(String)
|
|
142
|
-
raise ArgumentError, 'can_use_tool callback requires streaming mode'
|
|
143
|
-
end
|
|
144
|
-
|
|
145
157
|
# can_use_tool and permission_prompt_tool_name are mutually exclusive
|
|
146
|
-
if @options.permission_prompt_tool_name
|
|
147
|
-
raise ArgumentError, 'can_use_tool callback cannot be used with permission_prompt_tool_name'
|
|
148
|
-
end
|
|
158
|
+
raise ArgumentError, 'can_use_tool callback cannot be used with permission_prompt_tool_name' if @options.permission_prompt_tool_name
|
|
149
159
|
|
|
150
160
|
# Set permission_prompt_tool_name to stdio for control protocol
|
|
151
161
|
configured_options = @options.dup_with(permission_prompt_tool_name: 'stdio')
|
|
152
162
|
end
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
configured_options = configured_options.dup_with(
|
|
165
|
+
env: (configured_options.env || {}).merge('CLAUDE_CODE_ENTRYPOINT' => 'sdk-rb-client')
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Client always uses streaming mode; keep stdin open for bidirectional communication.
|
|
169
|
+
@transport = SubprocessCLITransport.new([].to_enum, configured_options)
|
|
159
170
|
@transport.connect
|
|
160
171
|
|
|
161
172
|
# Extract SDK MCP servers
|
|
@@ -183,6 +194,20 @@ module ClaudeAgentSDK
|
|
|
183
194
|
@query_handler.initialize_protocol
|
|
184
195
|
|
|
185
196
|
@connected = true
|
|
197
|
+
|
|
198
|
+
# Optionally send initial prompt/messages after connection is ready.
|
|
199
|
+
case prompt
|
|
200
|
+
when nil
|
|
201
|
+
nil
|
|
202
|
+
when String
|
|
203
|
+
query(prompt)
|
|
204
|
+
else
|
|
205
|
+
prompt.each do |message_json|
|
|
206
|
+
message_json = message_json.to_s
|
|
207
|
+
message_json += "\n" unless message_json.end_with?("\n")
|
|
208
|
+
@transport.write(message_json)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
186
211
|
end
|
|
187
212
|
|
|
188
213
|
# Send a query to Claude
|
|
@@ -259,6 +284,20 @@ module ClaudeAgentSDK
|
|
|
259
284
|
@query_handler&.instance_variable_get(:@initialization_result)
|
|
260
285
|
end
|
|
261
286
|
|
|
287
|
+
# Get current MCP server connection status (only works with streaming mode)
|
|
288
|
+
# @return [Hash] MCP status information, including mcpServers list
|
|
289
|
+
def get_mcp_status
|
|
290
|
+
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
291
|
+
@query_handler.get_mcp_status
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Get server initialization info including available commands and output styles
|
|
295
|
+
# @return [Hash] Server info
|
|
296
|
+
def get_server_info
|
|
297
|
+
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
298
|
+
server_info
|
|
299
|
+
end
|
|
300
|
+
|
|
262
301
|
# Disconnect from Claude
|
|
263
302
|
def disconnect
|
|
264
303
|
return unless @connected
|
|
@@ -278,10 +317,12 @@ module ClaudeAgentSDK
|
|
|
278
317
|
hooks.each do |event, matchers|
|
|
279
318
|
internal_hooks[event.to_s] = []
|
|
280
319
|
matchers.each do |matcher|
|
|
281
|
-
|
|
320
|
+
config = {
|
|
282
321
|
matcher: matcher.matcher,
|
|
283
322
|
hooks: matcher.hooks
|
|
284
323
|
}
|
|
324
|
+
config[:timeout] = matcher.timeout if matcher.timeout
|
|
325
|
+
internal_hooks[event.to_s] << config
|
|
285
326
|
end
|
|
286
327
|
end
|
|
287
328
|
internal_hooks
|
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.
|
|
4
|
+
version: 0.4.2
|
|
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-02-06 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|