claude-agent-sdk 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c723c3e660d135ab87311e27be6a80b8d13127f9e42789a66b1d30ec0395278
4
- data.tar.gz: 0652f808dd792a6bef8db32afc567567fe9f016abdf095f1d13ae0ac07af0d03
3
+ metadata.gz: 52b6649f2a72ef33e746d168e4016dcc72c1439157fbe5b971edbd55b0a0d404
4
+ data.tar.gz: a344cc38d96ef7185630c1e1518a5cf6d08a855f40b39092728236d5a48df50d
5
5
  SHA512:
6
- metadata.gz: 399925ff55a9a23ab54cbe97adb18fea804be6c92cf21aa3ca3fdcd4cbe54693dfc8b77b85f15ed2eaf1517acaecd09935701d0248fb99a23be6325d151c99aa
7
- data.tar.gz: dd14802c444f4d6609cbb8215e79c3f5d8c4d9453e20d0d3af40cd853d653625c93e117082118beee2c5fb0ad29447a170687a44d56f88be6785affb8d118060
6
+ metadata.gz: 18a32d856585a36aa39a9aa586829a305c099cd4686a338e6f1df5310490abb81808821940a6be5d6c830a1fc86cb1cd95dab5ca8e2c5737574e3e91da513f5f
7
+ data.tar.gz: c6dadb9b32d373e1fac9246b5bafc8456f3779cf0187abceb7bd85ecdd96b823c95c49c751db9bcd4b384ceb34141057b9e48d4540366d1e3d7d951a69e0aa7f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,57 @@ 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.9.0] - 2026-03-12
9
+
10
+ Port of Python SDK v0.1.48 parity improvements.
11
+
12
+ ### Added
13
+
14
+ #### Typed Rate Limit Events
15
+ - `RateLimitInfo` class with `status`, `resets_at`, `rate_limit_type`, `utilization`, `overage_status`, `overage_resets_at`, `overage_disabled_reason`, `raw` attributes
16
+ - `RATE_LIMIT_STATUSES` constant (`allowed`, `allowed_warning`, `rejected`)
17
+ - `RATE_LIMIT_TYPES` constant (`five_hour`, `seven_day`, `seven_day_opus`, `seven_day_sonnet`, `overage`)
18
+ - `RateLimitEvent` now has typed `rate_limit_info`, `uuid`, `session_id` attributes (previously raw `data` hash)
19
+ - Backward-compatible `data` accessor on `RateLimitEvent` returns raw hash from `rate_limit_info.raw`
20
+
21
+ #### MCP Status Output Types
22
+ - `McpClaudeAIProxyServerConfig` type for `claudeai-proxy` servers in MCP status responses
23
+ - `McpSdkServerConfigStatus` type for serializable SDK server config in status responses
24
+ - `McpServerStatus.parse` handles `claudeai-proxy` config type
25
+
26
+ #### Effort Level
27
+ - `effort` option now supports `"max"` value in addition to `"low"`, `"medium"`, `"high"`
28
+
29
+ ## [0.8.1] - 2026-03-08
30
+
31
+ Python SDK parity fixes for one-shot `query()` control protocol and CLI transport.
32
+
33
+ ### Fixed
34
+
35
+ #### One-Shot Query Control Protocol
36
+ - **Hooks and `can_use_tool` in `query()`:** One-shot `query()` now passes `hooks`, `can_use_tool`, and SDK MCP servers through to the `Query` handler, matching the Python SDK (previously these were Client-only)
37
+ - **`can_use_tool` validation:** String prompts with `can_use_tool` now raise `ArgumentError` (streaming mode required); conflicting `permission_prompt_tool_name` also raises early
38
+ - **`session_id` parity:** One-shot queries now send `session_id: ''` (was `'default'`), matching Python SDK behavior
39
+ - **Premature stdin close:** Added `wait_for_result_and_end_input` that holds stdin open until the first result when hooks or SDK MCP servers need control message exchange
40
+ - **`stream_input` stdin leak:** Moved `end_input` to `ensure` block so stdin is always closed even when the stream enumerator raises
41
+ - **`Async::Condition` race:** Added `@first_result_received` flag guard to prevent lost signals when result arrives before `wait` is called
42
+
43
+ #### CLI Transport Parity
44
+ - **File checkpointing:** Moved from deprecated `--enable-file-checkpointing` CLI flag to `CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING` environment variable
45
+ - **Partial messages:** Now also sets `CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING=1` environment variable when `include_partial_messages` is enabled
46
+ - **Tools preset:** `ToolsPreset` objects and preset hashes now map to `--tools default` (was `--tools <json>`)
47
+ - **Plugins:** Changed from `--plugins <json>` to `--plugin-dir <path>` per-plugin, matching current CLI interface
48
+ - **Plugin type:** `SdkPluginConfig` now defaults to `type: 'local'` (was `'plugin'`), normalizes legacy `'plugin'` type
49
+ - **Rewind control request:** Changed key from `userMessageUuid` to `user_message_id` for Python SDK parity
50
+ - **Settings file with sandbox:** When sandbox is enabled and settings is a file path, now reads and parses the file to merge sandbox settings (raises on missing/invalid files instead of silently dropping settings)
51
+
52
+ #### Hook Input Parsing
53
+ - **Falsy value preservation:** `parse_hook_input` now uses `key?`-based lookup instead of `||`, correctly preserving `false` and `nil` values (e.g., `stop_hook_active: false`)
54
+ - **Empty hooks normalization:** `query()` now skips empty matcher lists and normalizes hooks to `nil` when no matchers survive, preventing unnecessary 60s close-wait timeout
55
+
56
+ ### Changed
57
+ - **`build_command` refactored:** Extracted `build_settings_args`, `build_tools_args`, `build_output_format_args`, `build_mcp_servers_args`, `build_plugins_args` private helpers to reduce method complexity
58
+
8
59
  ## [0.8.0] - 2026-03-05
9
60
 
10
61
  Port of Python SDK v0.1.46 features.
@@ -120,7 +120,23 @@ module ClaudeAgentSDK
120
120
  end
121
121
 
122
122
  def self.parse_rate_limit_event(data)
123
- RateLimitEvent.new(data: data)
123
+ info = data[:rate_limit_info] || {}
124
+ rate_limit_info = RateLimitInfo.new(
125
+ status: info[:status],
126
+ resets_at: info[:resetsAt],
127
+ rate_limit_type: info[:rateLimitType],
128
+ utilization: info[:utilization],
129
+ overage_status: info[:overageStatus],
130
+ overage_resets_at: info[:overageResetsAt],
131
+ overage_disabled_reason: info[:overageDisabledReason],
132
+ raw: info
133
+ )
134
+ RateLimitEvent.new(
135
+ rate_limit_info: rate_limit_info,
136
+ uuid: data[:uuid],
137
+ session_id: data[:session_id],
138
+ raw_data: data
139
+ )
124
140
  end
125
141
 
126
142
  def self.parse_content_block(block)
@@ -22,6 +22,8 @@ module ClaudeAgentSDK
22
22
 
23
23
  CONTROL_REQUEST_TIMEOUT_ENV_VAR = 'CLAUDE_AGENT_SDK_CONTROL_REQUEST_TIMEOUT_SECONDS'
24
24
  DEFAULT_CONTROL_REQUEST_TIMEOUT_SECONDS = 1200.0
25
+ STREAM_CLOSE_TIMEOUT_ENV_VAR = 'CLAUDE_CODE_STREAM_CLOSE_TIMEOUT'
26
+ DEFAULT_STREAM_CLOSE_TIMEOUT_SECONDS = 60.0
25
27
 
26
28
  def initialize(transport:, is_streaming_mode:, can_use_tool: nil, hooks: nil, sdk_mcp_servers: nil, agents: nil)
27
29
  @transport = transport
@@ -42,6 +44,8 @@ module ClaudeAgentSDK
42
44
 
43
45
  # Message stream
44
46
  @message_queue = Async::Queue.new
47
+ @first_result_received = false
48
+ @first_result_condition = Async::Condition.new
45
49
  @task = nil
46
50
  @initialized = false
47
51
  @closed = false
@@ -124,6 +128,16 @@ module ClaudeAgentSDK
124
128
  DEFAULT_CONTROL_REQUEST_TIMEOUT_SECONDS
125
129
  end
126
130
 
131
+ def stream_close_timeout_seconds
132
+ raw_value = ENV.fetch(STREAM_CLOSE_TIMEOUT_ENV_VAR, nil)
133
+ return DEFAULT_STREAM_CLOSE_TIMEOUT_SECONDS if raw_value.nil? || raw_value.strip.empty?
134
+
135
+ value = Float(raw_value) / 1000.0
136
+ value.positive? ? value : DEFAULT_STREAM_CLOSE_TIMEOUT_SECONDS
137
+ rescue ArgumentError
138
+ DEFAULT_STREAM_CLOSE_TIMEOUT_SECONDS
139
+ end
140
+
127
141
  def read_messages
128
142
  @transport.read_messages do |message|
129
143
  break if @closed
@@ -150,6 +164,10 @@ module ClaudeAgentSDK
150
164
  task&.stop
151
165
  next
152
166
  else
167
+ if message[:type] == 'result' && !@first_result_received
168
+ @first_result_received = true
169
+ @first_result_condition.signal
170
+ end
153
171
  # Regular SDK messages go to the queue
154
172
  @message_queue.enqueue(message)
155
173
  end
@@ -164,6 +182,10 @@ module ClaudeAgentSDK
164
182
  # Put error in queue so iterators can handle it
165
183
  @message_queue.enqueue({ type: 'error', error: e })
166
184
  ensure
185
+ unless @first_result_received
186
+ @first_result_received = true
187
+ @first_result_condition.signal
188
+ end
167
189
  # Always signal end of stream
168
190
  @message_queue.enqueue({ type: 'end' })
169
191
  end
@@ -308,7 +330,13 @@ module ClaudeAgentSDK
308
330
 
309
331
  def parse_hook_input(input_data)
310
332
  event_name = input_data[:hook_event_name] || input_data['hook_event_name']
311
- fetch = ->(key) { input_data[key] || input_data[key.to_s] }
333
+ fetch = lambda do |key|
334
+ if input_data.key?(key)
335
+ input_data[key]
336
+ elsif input_data.key?(key.to_s)
337
+ input_data[key.to_s]
338
+ end
339
+ end
312
340
  base_args = {
313
341
  session_id: fetch.call(:session_id),
314
342
  transcript_path: fetch.call(:transcript_path),
@@ -704,20 +732,42 @@ module ClaudeAgentSDK
704
732
  def rewind_files(user_message_uuid)
705
733
  send_control_request({
706
734
  subtype: 'rewind_files',
707
- userMessageUuid: user_message_uuid
735
+ user_message_id: user_message_uuid
708
736
  })
709
737
  end
710
738
 
739
+ # Wait for the first result before closing stdin when hooks or SDK MCP
740
+ # servers may still need to exchange control messages with the CLI.
741
+ def wait_for_result_and_end_input
742
+ if !@first_result_received &&
743
+ ((@sdk_mcp_servers && !@sdk_mcp_servers.empty?) || (@hooks && !@hooks.empty?))
744
+ Async::Task.current.with_timeout(stream_close_timeout_seconds) do
745
+ @first_result_condition.wait unless @first_result_received
746
+ end
747
+ end
748
+ rescue Async::TimeoutError
749
+ nil
750
+ ensure
751
+ @transport.end_input
752
+ end
753
+
711
754
  # Stream input messages to transport
712
755
  def stream_input(stream)
713
756
  stream.each do |message|
714
757
  break if @closed
715
- @transport.write(JSON.generate(message) + "\n")
758
+ serialized = if message.is_a?(Hash)
759
+ JSON.generate(message) + "\n"
760
+ else
761
+ message.to_s
762
+ end
763
+ serialized += "\n" unless serialized.end_with?("\n")
764
+ @transport.write(serialized)
716
765
  end
717
- @transport.end_input
718
766
  rescue StandardError => e
719
767
  # Log error but don't raise
720
768
  warn "Error streaming input: #{e.message}"
769
+ ensure
770
+ wait_for_result_and_end_input
721
771
  end
722
772
 
723
773
  # Receive SDK messages (not control messages)
@@ -75,14 +75,14 @@ module ClaudeAgentSDK
75
75
  elsif @options.system_prompt.is_a?(String)
76
76
  cmd.concat(['--system-prompt', @options.system_prompt])
77
77
  elsif @options.system_prompt.is_a?(SystemPromptPreset)
78
- cmd.concat(['--system-prompt-preset', @options.system_prompt.preset]) if @options.system_prompt.preset
78
+ # Preset activates the default Claude Code system prompt by not passing --system-prompt ""
79
+ # Only --append-system-prompt is passed if append text is provided
79
80
  cmd.concat(['--append-system-prompt', @options.system_prompt.append]) if @options.system_prompt.append
80
81
  elsif @options.system_prompt.is_a?(Hash)
81
82
  prompt_type = @options.system_prompt[:type] || @options.system_prompt['type']
82
83
  if prompt_type == 'preset'
83
- preset = @options.system_prompt[:preset] || @options.system_prompt['preset']
84
84
  append = @options.system_prompt[:append] || @options.system_prompt['append']
85
- cmd.concat(['--system-prompt-preset', preset]) if preset
85
+ # Preset activates the default Claude Code system prompt by not passing --system-prompt ""
86
86
  cmd.concat(['--append-system-prompt', append]) if append
87
87
  end
88
88
  end
@@ -98,46 +98,7 @@ module ClaudeAgentSDK
98
98
  cmd.concat(['--resume', @options.resume]) if @options.resume
99
99
 
100
100
  # Settings handling with sandbox merge
101
- # Sandbox settings are merged into the main settings JSON
102
- if @options.settings || @options.sandbox
103
- settings_hash = {}
104
- settings_is_path = false
105
-
106
- # Parse existing settings if provided
107
- if @options.settings
108
- if @options.settings.is_a?(String)
109
- begin
110
- settings_hash = JSON.parse(@options.settings)
111
- rescue JSON::ParserError
112
- # If not valid JSON, treat as file path and pass as-is
113
- settings_is_path = true
114
- cmd.concat(['--settings', @options.settings])
115
- if @options.sandbox
116
- warn "Warning: Cannot merge sandbox settings when settings is a file path. " \
117
- "Sandbox settings will be ignored. Use a Hash or JSON string for settings " \
118
- "to enable sandbox merging."
119
- end
120
- end
121
- elsif @options.settings.is_a?(Hash)
122
- settings_hash = @options.settings.dup
123
- end
124
- end
125
-
126
- # Merge sandbox settings if provided (only when settings is not a file path)
127
- if !settings_is_path && @options.sandbox
128
- sandbox_hash = if @options.sandbox.is_a?(SandboxSettings)
129
- @options.sandbox.to_h
130
- else
131
- @options.sandbox
132
- end
133
- settings_hash[:sandbox] = sandbox_hash unless sandbox_hash.empty?
134
- end
135
-
136
- # Output merged settings (only when settings is not a file path)
137
- if !settings_is_path && !settings_hash.empty?
138
- cmd.concat(['--settings', JSON.generate(settings_hash)])
139
- end
140
- end
101
+ build_settings_args(cmd)
141
102
 
142
103
  # Budget limit option
143
104
  cmd.concat(['--max-budget-usd', @options.max_budget_usd.to_s]) if @options.max_budget_usd
@@ -146,7 +107,7 @@ module ClaudeAgentSDK
146
107
  thinking_tokens = resolve_thinking_tokens
147
108
  cmd.concat(['--max-thinking-tokens', thinking_tokens.to_s]) unless thinking_tokens.nil?
148
109
 
149
- # Effort level
110
+ # Effort level (valid values: low, medium, high, max)
150
111
  cmd.concat(['--effort', @options.effort.to_s]) if @options.effort
151
112
 
152
113
  # Betas option for enabling experimental features
@@ -155,39 +116,15 @@ module ClaudeAgentSDK
155
116
  end
156
117
 
157
118
  # Tools option for base tools selection
158
- if @options.tools
159
- if @options.tools.is_a?(Array)
160
- cmd.concat(['--tools', @options.tools.join(',')])
161
- elsif @options.tools.is_a?(ToolsPreset)
162
- cmd.concat(['--tools', JSON.generate(@options.tools.to_h)])
163
- elsif @options.tools.is_a?(Hash)
164
- cmd.concat(['--tools', JSON.generate(@options.tools)])
165
- end
166
- end
119
+ build_tools_args(cmd)
167
120
 
168
121
  # Append allowed tools option
169
122
  if @options.append_allowed_tools && !@options.append_allowed_tools.empty?
170
123
  cmd.concat(['--append-allowed-tools', @options.append_allowed_tools.join(',')])
171
124
  end
172
125
 
173
- # File checkpointing for rewind support
174
- cmd << '--enable-file-checkpointing' if @options.enable_file_checkpointing
175
-
176
126
  # JSON schema for structured output
177
- # Accepts either:
178
- # 1. Direct schema: { type: 'object', properties: {...} }
179
- # 2. Wrapped format: { type: 'json_schema', schema: {...} }
180
- if @options.output_format
181
- schema = if @options.output_format.is_a?(Hash) && @options.output_format[:type] == 'json_schema'
182
- @options.output_format[:schema]
183
- elsif @options.output_format.is_a?(Hash) && @options.output_format['type'] == 'json_schema'
184
- @options.output_format['schema']
185
- else
186
- @options.output_format
187
- end
188
- schema_json = schema.is_a?(String) ? schema : JSON.generate(schema)
189
- cmd.concat(['--json-schema', schema_json])
190
- end
127
+ build_output_format_args(cmd)
191
128
 
192
129
  # Add directories
193
130
  @options.add_dirs.each do |dir|
@@ -195,23 +132,7 @@ module ClaudeAgentSDK
195
132
  end
196
133
 
197
134
  # MCP servers
198
- if @options.mcp_servers && !@options.mcp_servers.empty?
199
- if @options.mcp_servers.is_a?(Hash)
200
- servers_for_cli = {}
201
- @options.mcp_servers.each do |name, config|
202
- if config.is_a?(Hash) && config[:type] == 'sdk'
203
- # For SDK servers, exclude instance field
204
- sdk_config = config.reject { |k, _| k == :instance }
205
- servers_for_cli[name] = sdk_config
206
- else
207
- servers_for_cli[name] = config
208
- end
209
- end
210
- cmd.concat(['--mcp-config', JSON.generate({ mcpServers: servers_for_cli })]) unless servers_for_cli.empty?
211
- else
212
- cmd.concat(['--mcp-config', @options.mcp_servers.to_s])
213
- end
214
- end
135
+ build_mcp_servers_args(cmd)
215
136
 
216
137
  cmd << '--include-partial-messages' if @options.include_partial_messages
217
138
  cmd << '--fork-session' if @options.fork_session
@@ -220,12 +141,7 @@ module ClaudeAgentSDK
220
141
  # to avoid OS ARG_MAX limits with large agent configurations.
221
142
 
222
143
  # Plugins
223
- if @options.plugins && !@options.plugins.empty?
224
- plugins_config = @options.plugins.map do |plugin|
225
- plugin.is_a?(SdkPluginConfig) ? plugin.to_h : plugin
226
- end
227
- cmd.concat(['--plugins', JSON.generate(plugins_config)])
228
- end
144
+ build_plugins_args(cmd)
229
145
 
230
146
  # Setting sources
231
147
  sources_value = @options.setting_sources ? @options.setting_sources.join(',') : ''
@@ -264,6 +180,10 @@ module ClaudeAgentSDK
264
180
  # the env hash on top of the parent environment; a nil value actively unsets.
265
181
  process_env = ENV.to_h.merge('CLAUDECODE' => nil, 'CLAUDE_AGENT_SDK_VERSION' => VERSION).merge(custom_env)
266
182
  process_env['CLAUDE_CODE_ENTRYPOINT'] ||= 'sdk-rb'
183
+ process_env['CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING'] = 'true' if @options.enable_file_checkpointing
184
+ if @options.include_partial_messages
185
+ process_env['CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING'] ||= '1'
186
+ end
267
187
  process_env['PWD'] = @cwd.to_s if @cwd
268
188
 
269
189
  # Determine stderr handling
@@ -554,6 +474,107 @@ module ClaudeAgentSDK
554
474
 
555
475
  private
556
476
 
477
+ def build_settings_args(cmd)
478
+ return unless @options.settings || @options.sandbox
479
+
480
+ settings_hash = {}
481
+ settings_is_path = false
482
+
483
+ if @options.settings
484
+ if @options.settings.is_a?(String)
485
+ begin
486
+ settings_hash = JSON.parse(@options.settings)
487
+ rescue JSON::ParserError
488
+ if @options.sandbox
489
+ settings_hash = load_settings_file(@options.settings)
490
+ else
491
+ settings_is_path = true
492
+ cmd.concat(['--settings', @options.settings])
493
+ end
494
+ end
495
+ elsif @options.settings.is_a?(Hash)
496
+ settings_hash = @options.settings.dup
497
+ end
498
+ end
499
+
500
+ if !settings_is_path && @options.sandbox
501
+ sandbox_hash = @options.sandbox.is_a?(SandboxSettings) ? @options.sandbox.to_h : @options.sandbox
502
+ settings_hash[:sandbox] = sandbox_hash unless sandbox_hash.empty?
503
+ end
504
+
505
+ cmd.concat(['--settings', JSON.generate(settings_hash)]) if !settings_is_path && !settings_hash.empty?
506
+ end
507
+
508
+ def build_tools_args(cmd)
509
+ return if @options.tools.nil?
510
+
511
+ if @options.tools.is_a?(Array)
512
+ tools_value = @options.tools.empty? ? '' : @options.tools.join(',')
513
+ cmd.concat(['--tools', tools_value])
514
+ elsif @options.tools.is_a?(ToolsPreset)
515
+ cmd.concat(['--tools', 'default'])
516
+ elsif @options.tools.is_a?(Hash)
517
+ if (@options.tools[:type] || @options.tools['type']) == 'preset'
518
+ cmd.concat(['--tools', 'default'])
519
+ else
520
+ cmd.concat(['--tools', JSON.generate(@options.tools)])
521
+ end
522
+ end
523
+ end
524
+
525
+ def build_output_format_args(cmd)
526
+ return unless @options.output_format
527
+
528
+ schema = if @options.output_format.is_a?(Hash) && @options.output_format[:type] == 'json_schema'
529
+ @options.output_format[:schema]
530
+ elsif @options.output_format.is_a?(Hash) && @options.output_format['type'] == 'json_schema'
531
+ @options.output_format['schema']
532
+ else
533
+ @options.output_format
534
+ end
535
+ schema_json = schema.is_a?(String) ? schema : JSON.generate(schema)
536
+ cmd.concat(['--json-schema', schema_json])
537
+ end
538
+
539
+ def build_mcp_servers_args(cmd)
540
+ return unless @options.mcp_servers && !@options.mcp_servers.empty?
541
+
542
+ if @options.mcp_servers.is_a?(Hash)
543
+ servers_for_cli = {}
544
+ @options.mcp_servers.each do |name, config|
545
+ servers_for_cli[name] = if config.is_a?(Hash) && config[:type] == 'sdk'
546
+ config.reject { |k, _| k == :instance }
547
+ else
548
+ config
549
+ end
550
+ end
551
+ cmd.concat(['--mcp-config', JSON.generate({ mcpServers: servers_for_cli })]) unless servers_for_cli.empty?
552
+ else
553
+ cmd.concat(['--mcp-config', @options.mcp_servers.to_s])
554
+ end
555
+ end
556
+
557
+ def build_plugins_args(cmd)
558
+ return unless @options.plugins && !@options.plugins.empty?
559
+
560
+ @options.plugins.each do |plugin|
561
+ plugin_config = plugin.is_a?(SdkPluginConfig) ? plugin.to_h : plugin
562
+ plugin_type = plugin_config[:type] || plugin_config['type']
563
+ plugin_path = plugin_config[:path] || plugin_config['path']
564
+
565
+ raise ArgumentError, "Unsupported plugin type: #{plugin_type.inspect}" unless %w[local plugin].include?(plugin_type)
566
+ next unless plugin_path
567
+
568
+ cmd.concat(['--plugin-dir', plugin_path])
569
+ end
570
+ end
571
+
572
+ def load_settings_file(path)
573
+ raise CLIConnectionError, "Settings file not found: #{path}" unless File.file?(path)
574
+
575
+ JSON.parse(File.read(path))
576
+ end
577
+
557
578
  def resolve_thinking_tokens
558
579
  if @options.thinking
559
580
  case @options.thinking
@@ -212,12 +212,44 @@ module ClaudeAgentSDK
212
212
  end
213
213
  end
214
214
 
215
- # Rate limit event emitted by Claude Code CLI when API rate limits are hit
215
+ # Type constants for rate limit statuses
216
+ RATE_LIMIT_STATUSES = %w[allowed allowed_warning rejected].freeze
217
+
218
+ # Type constants for rate limit types
219
+ RATE_LIMIT_TYPES = %w[five_hour seven_day seven_day_opus seven_day_sonnet overage].freeze
220
+
221
+ # Rate limit info with typed fields
222
+ class RateLimitInfo
223
+ attr_accessor :status, :resets_at, :rate_limit_type, :utilization,
224
+ :overage_status, :overage_resets_at, :overage_disabled_reason, :raw
225
+
226
+ def initialize(status:, resets_at: nil, rate_limit_type: nil, utilization: nil,
227
+ overage_status: nil, overage_resets_at: nil, overage_disabled_reason: nil, raw: {})
228
+ @status = status
229
+ @resets_at = resets_at
230
+ @rate_limit_type = rate_limit_type
231
+ @utilization = utilization
232
+ @overage_status = overage_status
233
+ @overage_resets_at = overage_resets_at
234
+ @overage_disabled_reason = overage_disabled_reason
235
+ @raw = raw
236
+ end
237
+ end
238
+
239
+ # Rate limit event emitted when rate limit info changes
216
240
  class RateLimitEvent
217
- attr_accessor :data
241
+ attr_accessor :rate_limit_info, :uuid, :session_id
218
242
 
219
- def initialize(data:)
220
- @data = data
243
+ def initialize(rate_limit_info:, uuid:, session_id:, raw_data: nil)
244
+ @rate_limit_info = rate_limit_info
245
+ @uuid = uuid
246
+ @session_id = session_id
247
+ @raw_data = raw_data
248
+ end
249
+
250
+ # Backward-compatible accessor returning the full raw event payload
251
+ def data
252
+ @raw_data || {}
221
253
  end
222
254
  end
223
255
 
@@ -753,6 +785,37 @@ module ClaudeAgentSDK
753
785
  end
754
786
  end
755
787
 
788
+ # Output-only serializable version of McpSdkServerConfig (without live instance)
789
+ # Returned in MCP status responses
790
+ class McpSdkServerConfigStatus
791
+ attr_accessor :type, :name
792
+
793
+ def initialize(type: 'sdk', name:)
794
+ @type = type
795
+ @name = name
796
+ end
797
+
798
+ def to_h
799
+ { type: @type, name: @name }
800
+ end
801
+ end
802
+
803
+ # Claude.ai proxy MCP server config
804
+ # Output-only type that appears in status responses for servers proxied through Claude.ai
805
+ class McpClaudeAIProxyServerConfig
806
+ attr_accessor :type, :url, :id
807
+
808
+ def initialize(type: 'claudeai-proxy', url:, id:)
809
+ @type = type
810
+ @url = url
811
+ @id = id
812
+ end
813
+
814
+ def to_h
815
+ { type: @type, url: @url, id: @id }
816
+ end
817
+ end
818
+
756
819
  # Status of a single MCP server connection
757
820
  class McpServerStatus
758
821
  attr_accessor :name, :status, :server_info, :error, :config, :scope, :tools
@@ -770,17 +833,31 @@ module ClaudeAgentSDK
770
833
  def self.parse(data)
771
834
  server_info = (McpServerInfo.new(name: data[:serverInfo][:name], version: data[:serverInfo][:version]) if data[:serverInfo])
772
835
  tools = data[:tools]&.map { |t| McpToolInfo.parse(t) }
836
+ config = parse_config(data[:config])
773
837
 
774
838
  new(
775
839
  name: data[:name],
776
840
  status: data[:status],
777
841
  server_info: server_info,
778
842
  error: data[:error],
779
- config: data[:config],
843
+ config: config,
780
844
  scope: data[:scope],
781
845
  tools: tools
782
846
  )
783
847
  end
848
+
849
+ def self.parse_config(config)
850
+ return config unless config.is_a?(Hash) && config[:type]
851
+
852
+ case config[:type]
853
+ when 'claudeai-proxy'
854
+ McpClaudeAIProxyServerConfig.new(url: config[:url], id: config[:id])
855
+ when 'sdk'
856
+ McpSdkServerConfigStatus.new(name: config[:name])
857
+ else
858
+ config
859
+ end
860
+ end
784
861
  end
785
862
 
786
863
  # Response from get_mcp_status containing all server statuses
@@ -866,8 +943,10 @@ module ClaudeAgentSDK
866
943
  class SdkPluginConfig
867
944
  attr_accessor :type, :path
868
945
 
869
- def initialize(path:)
870
- @type = 'plugin'
946
+ def initialize(path:, type: 'local')
947
+ raise ArgumentError, "unsupported plugin type: #{type}" unless %w[local plugin].include?(type)
948
+
949
+ @type = 'local'
871
950
  @path = path
872
951
  end
873
952
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.8.0'
4
+ VERSION = '0.9.0'
5
5
  end
@@ -82,29 +82,66 @@ module ClaudeAgentSDK
82
82
  return enum_for(:query, prompt: prompt, options: options) unless block
83
83
 
84
84
  options ||= ClaudeAgentOptions.new
85
- options = options.dup_with(env: (options.env || {}).merge('CLAUDE_CODE_ENTRYPOINT' => 'sdk-rb'))
85
+
86
+ configured_options = options
87
+ if options.can_use_tool
88
+ if prompt.is_a?(String)
89
+ raise ArgumentError,
90
+ 'can_use_tool callback requires streaming mode. Please provide prompt as an Enumerator instead of a String.'
91
+ end
92
+
93
+ raise ArgumentError, 'can_use_tool callback cannot be used with permission_prompt_tool_name' if options.permission_prompt_tool_name
94
+
95
+ configured_options = options.dup_with(permission_prompt_tool_name: 'stdio')
96
+ end
97
+
98
+ configured_options = configured_options.dup_with(
99
+ env: (configured_options.env || {}).merge('CLAUDE_CODE_ENTRYPOINT' => 'sdk-rb')
100
+ )
86
101
 
87
102
  Async do
88
103
  # Always use streaming mode with control protocol (matches Python SDK).
89
104
  # This sends agents via initialize request instead of CLI args,
90
105
  # avoiding OS ARG_MAX limits.
91
- transport = SubprocessCLITransport.new(options)
106
+ transport = SubprocessCLITransport.new(configured_options)
92
107
  begin
93
108
  transport.connect
94
109
 
95
110
  # Extract SDK MCP servers
96
111
  sdk_mcp_servers = {}
97
- if options.mcp_servers.is_a?(Hash)
98
- options.mcp_servers.each do |name, config|
112
+ if configured_options.mcp_servers.is_a?(Hash)
113
+ configured_options.mcp_servers.each do |name, config|
99
114
  sdk_mcp_servers[name] = config[:instance] if config.is_a?(Hash) && config[:type] == 'sdk'
100
115
  end
101
116
  end
102
117
 
118
+ hooks = nil
119
+ if configured_options.hooks
120
+ hooks = {}
121
+ configured_options.hooks.each do |event, matchers|
122
+ next if matchers.nil? || matchers.empty?
123
+
124
+ entries = []
125
+ matchers.each do |matcher|
126
+ config = {
127
+ matcher: matcher.matcher,
128
+ hooks: matcher.hooks
129
+ }
130
+ config[:timeout] = matcher.timeout if matcher.timeout
131
+ entries << config
132
+ end
133
+ hooks[event.to_s] = entries unless entries.empty?
134
+ end
135
+ hooks = nil if hooks.empty?
136
+ end
137
+
103
138
  # Create Query handler for control protocol
104
139
  query_handler = Query.new(
105
140
  transport: transport,
106
141
  is_streaming_mode: true,
107
- agents: options.agents,
142
+ can_use_tool: configured_options.can_use_tool,
143
+ hooks: hooks,
144
+ agents: configured_options.agents,
108
145
  sdk_mcp_servers: sdk_mcp_servers
109
146
  )
110
147
 
@@ -120,19 +157,13 @@ module ClaudeAgentSDK
120
157
  type: 'user',
121
158
  message: { role: 'user', content: prompt },
122
159
  parent_tool_use_id: nil,
123
- session_id: 'default'
160
+ session_id: ''
124
161
  }
125
162
  transport.write(JSON.generate(message) + "\n")
126
- transport.end_input
163
+ query_handler.wait_for_result_and_end_input
127
164
  elsif prompt.is_a?(Enumerator) || prompt.respond_to?(:each)
128
165
  Async do
129
- begin
130
- prompt.each do |message_json|
131
- transport.write(message_json)
132
- end
133
- ensure
134
- transport.end_input
135
- end
166
+ query_handler.stream_input(prompt)
136
167
  end
137
168
  end
138
169
 
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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Community Contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-05 00:00:00.000000000 Z
10
+ date: 2026-03-12 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async
@@ -123,6 +123,7 @@ metadata:
123
123
  source_code_uri: https://github.com/ya-luotao/claude-agent-sdk-ruby
124
124
  changelog_uri: https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/CHANGELOG.md
125
125
  documentation_uri: https://docs.anthropic.com/en/docs/claude-code/sdk
126
+ allowed_push_host: https://rubygems.org
126
127
  rdoc_options: []
127
128
  require_paths:
128
129
  - lib