claude-agent-sdk 0.1.1 → 0.1.3

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: 4ce875efdba662100db9de2523598a28193fda59c45f5e720ab5c69724e5e83e
4
- data.tar.gz: 1cd9a2342b14ba9ec4c8775bc8fa4b47fa519112f5a43fdcb87e832c9c8f2f43
3
+ metadata.gz: d1b1a2ec74f15062544d42a0df92058e256bab2ed29754e19133bfc1340cddcf
4
+ data.tar.gz: f01aea04b3f32e51b5d98e6994d52c8e2864259fa86d1c5bdc64104486b63687
5
5
  SHA512:
6
- metadata.gz: 3142185738aa66feb78e7770d91b67a1c81303db80a8b8730e9c9388e8c754588a3dc3737a2eedff1bac36f025a664cd6b02f16adbd2436324644f6015fb188c
7
- data.tar.gz: f13e3d95e5f47c0da0a0a48f8b9fc969996f6f6d17f51ff418dbeebda8adb72acb22281e895ab4b5b3f4f2e26952285a17d7f51fd55d0ac953a4e80fb5660d10
6
+ metadata.gz: ef15b15c77ceb22fc0b40884ecc1e79a884b9b7d043b21420ef0faa34d638206eec6284ab6afeb015097d721c75c0e00ce0f3db4c63e7f8e90305bda2e468b16
7
+ data.tar.gz: 74ee1d9b848c2e84336fb3a12e8147b35243f64ad6c1d352fe451a39c16a92cdaa92303acb20cf053495c580171bfe36d60f5a53bee98d2781da0b1b261870d3
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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.1.3] - 2025-10-15
9
+
10
+ ### Added
11
+ - **MCP resource support:** Full support for MCP resources (list, read, subscribe operations)
12
+ - **MCP prompt support:** Support for MCP prompts (list, get operations)
13
+ - **Streaming input support:** Added streaming capabilities for input handling
14
+ - Feature complete MCP implementation matching Python SDK functionality
15
+
16
+ ## [0.1.2] - 2025-10-14
17
+
18
+ ### Fixed
19
+ - **Critical:** Replaced `Async::Process` with Ruby's built-in `Open3` for subprocess management
20
+ - Fixed "uninitialized constant Async::Process" error that prevented the gem from working
21
+ - Process management now uses standard Ruby threads instead of async tasks
22
+ - All 86 tests passing
23
+
8
24
  ## [0.1.1] - 2025-10-14
9
25
 
10
26
  ### Fixed
data/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # Claude Agent SDK for Ruby
2
2
 
3
- > **⚠️ DISCLAIMER**: This is an **unofficial, community-maintained** Ruby SDK for Claude Agent. It is not officially supported or maintained by Anthropic. For official SDK support, please refer to the [Python SDK](https://github.com/anthropics/claude-code/tree/main/agent-sdk/python).
3
+ > **⚠️ DISCLAIMER**: This is an **unofficial, community-maintained** Ruby SDK for Claude Agent. It is not officially supported or maintained by Anthropic. For official SDK support, please refer to the [Python SDK](https://docs.claude.com/en/api/agent-sdk/python).
4
4
  >
5
5
  > This implementation is based on the official Python SDK and aims to provide feature parity for Ruby developers. Use at your own risk.
6
6
 
7
7
  Ruby SDK for Claude Agent. See the [Claude Agent SDK documentation](https://docs.anthropic.com/en/docs/claude-code/sdk) for more information.
8
8
 
9
+ [![Gem Version](https://badge.fury.io/rb/claude-agent-sdk.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/claude-agent-sdk)
10
+
9
11
  ## Installation
10
12
 
11
13
  Add this line to your application's Gemfile:
@@ -92,6 +94,41 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
92
94
  )
93
95
  ```
94
96
 
97
+ ### Streaming Input
98
+
99
+ The `query()` function supports streaming input, allowing you to send multiple messages dynamically instead of a single prompt string.
100
+
101
+ ```ruby
102
+ require 'claude_agent_sdk'
103
+
104
+ # Create a stream of messages
105
+ messages = ['Hello!', 'What is 2+2?', 'Thanks!']
106
+ stream = ClaudeAgentSDK::Streaming.from_array(messages)
107
+
108
+ # Query with streaming input
109
+ ClaudeAgentSDK.query(prompt: stream) do |message|
110
+ puts message if message.is_a?(ClaudeAgentSDK::AssistantMessage)
111
+ end
112
+ ```
113
+
114
+ You can also create custom streaming enumerators:
115
+
116
+ ```ruby
117
+ # Dynamic message generation
118
+ stream = Enumerator.new do |yielder|
119
+ yielder << ClaudeAgentSDK::Streaming.user_message("First message")
120
+ # Do some processing...
121
+ yielder << ClaudeAgentSDK::Streaming.user_message("Second message")
122
+ yielder << ClaudeAgentSDK::Streaming.user_message("Third message")
123
+ end
124
+
125
+ ClaudeAgentSDK.query(prompt: stream) do |message|
126
+ # Process responses
127
+ end
128
+ ```
129
+
130
+ For a complete example, see [examples/streaming_input_example.rb](examples/streaming_input_example.rb).
131
+
95
132
  ## Client
96
133
 
97
134
  `ClaudeAgentSDK::Client` supports bidirectional, interactive conversations with Claude Code. Unlike `query()`, `Client` enables **custom tools**, **hooks**, and **permission callbacks**, all of which can be defined as Ruby procs/lambdas. See [lib/claude_agent_sdk.rb](lib/claude_agent_sdk.rb).
@@ -193,6 +230,58 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
193
230
  )
194
231
  ```
195
232
 
233
+ #### MCP Resources and Prompts
234
+
235
+ SDK MCP servers can also expose **resources** (data sources) and **prompts** (reusable templates):
236
+
237
+ ```ruby
238
+ # Create a resource (data source Claude can read)
239
+ config_resource = ClaudeAgentSDK.create_resource(
240
+ uri: 'config://app/settings',
241
+ name: 'Application Settings',
242
+ description: 'Current app configuration',
243
+ mime_type: 'application/json'
244
+ ) do
245
+ config_data = { app_name: 'MyApp', version: '1.0.0' }
246
+ {
247
+ contents: [{
248
+ uri: 'config://app/settings',
249
+ mimeType: 'application/json',
250
+ text: JSON.pretty_generate(config_data)
251
+ }]
252
+ }
253
+ end
254
+
255
+ # Create a prompt template
256
+ review_prompt = ClaudeAgentSDK.create_prompt(
257
+ name: 'code_review',
258
+ description: 'Review code for best practices',
259
+ arguments: [
260
+ { name: 'code', description: 'Code to review', required: true }
261
+ ]
262
+ ) do |args|
263
+ {
264
+ messages: [{
265
+ role: 'user',
266
+ content: {
267
+ type: 'text',
268
+ text: "Review this code: #{args[:code]}"
269
+ }
270
+ }]
271
+ }
272
+ end
273
+
274
+ # Create server with tools, resources, and prompts
275
+ server = ClaudeAgentSDK.create_sdk_mcp_server(
276
+ name: 'dev-tools',
277
+ tools: [my_tool],
278
+ resources: [config_resource],
279
+ prompts: [review_prompt]
280
+ )
281
+ ```
282
+
283
+ For complete examples, see [examples/mcp_resources_prompts_example.rb](examples/mcp_resources_prompts_example.rb).
284
+
196
285
  ### Basic Client Usage
197
286
 
198
287
  ```ruby
@@ -415,7 +504,9 @@ See the following examples for complete working code:
415
504
 
416
505
  - [examples/quick_start.rb](examples/quick_start.rb) - Basic `query()` usage with options
417
506
  - [examples/client_example.rb](examples/client_example.rb) - Interactive Client usage
507
+ - [examples/streaming_input_example.rb](examples/streaming_input_example.rb) - Streaming input for multi-turn conversations
418
508
  - [examples/mcp_calculator.rb](examples/mcp_calculator.rb) - Custom tools with SDK MCP servers
509
+ - [examples/mcp_resources_prompts_example.rb](examples/mcp_resources_prompts_example.rb) - MCP resources and prompts
419
510
  - [examples/hooks_example.rb](examples/hooks_example.rb) - Using hooks to control tool execution
420
511
  - [examples/permission_callback_example.rb](examples/permission_callback_example.rb) - Dynamic tool permission control
421
512
 
@@ -423,10 +514,6 @@ See the following examples for complete working code:
423
514
 
424
515
  After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
425
516
 
426
- ## Contributing
427
-
428
- Bug reports and pull requests are welcome on GitHub at https://github.com/anthropics/claude-agent-sdk-ruby.
429
-
430
517
  ## License
431
518
 
432
519
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -314,6 +314,14 @@ module ClaudeAgentSDK
314
314
  handle_mcp_tools_list(server, message)
315
315
  when 'tools/call'
316
316
  handle_mcp_tools_call(server, message, params)
317
+ when 'resources/list'
318
+ handle_mcp_resources_list(server, message)
319
+ when 'resources/read'
320
+ handle_mcp_resources_read(server, message, params)
321
+ when 'prompts/list'
322
+ handle_mcp_prompts_list(server, message)
323
+ when 'prompts/get'
324
+ handle_mcp_prompts_get(server, message, params)
317
325
  when 'notifications/initialized'
318
326
  { jsonrpc: '2.0', result: {} }
319
327
  else
@@ -332,12 +340,17 @@ module ClaudeAgentSDK
332
340
  end
333
341
 
334
342
  def handle_mcp_initialize(server, message)
343
+ capabilities = {}
344
+ capabilities[:tools] = {} if server.tools && !server.tools.empty?
345
+ capabilities[:resources] = {} if server.resources && !server.resources.empty?
346
+ capabilities[:prompts] = {} if server.prompts && !server.prompts.empty?
347
+
335
348
  {
336
349
  jsonrpc: '2.0',
337
350
  id: message[:id],
338
351
  result: {
339
352
  protocolVersion: '2024-11-05',
340
- capabilities: { tools: {} },
353
+ capabilities: capabilities,
341
354
  serverInfo: {
342
355
  name: server.name,
343
356
  version: server.version || '1.0.0'
@@ -384,6 +397,58 @@ module ClaudeAgentSDK
384
397
  }
385
398
  end
386
399
 
400
+ def handle_mcp_resources_list(server, message)
401
+ # List resources from the SDK MCP server
402
+ resources_data = server.list_resources
403
+ {
404
+ jsonrpc: '2.0',
405
+ id: message[:id],
406
+ result: { resources: resources_data }
407
+ }
408
+ end
409
+
410
+ def handle_mcp_resources_read(server, message, params)
411
+ # Read a resource from the SDK MCP server
412
+ uri = params[:uri]
413
+ raise 'Missing uri parameter for resources/read' unless uri
414
+
415
+ # Read the resource
416
+ result = server.read_resource(uri)
417
+
418
+ {
419
+ jsonrpc: '2.0',
420
+ id: message[:id],
421
+ result: result
422
+ }
423
+ end
424
+
425
+ def handle_mcp_prompts_list(server, message)
426
+ # List prompts from the SDK MCP server
427
+ prompts_data = server.list_prompts
428
+ {
429
+ jsonrpc: '2.0',
430
+ id: message[:id],
431
+ result: { prompts: prompts_data }
432
+ }
433
+ end
434
+
435
+ def handle_mcp_prompts_get(server, message, params)
436
+ # Get a prompt from the SDK MCP server
437
+ name = params[:name]
438
+ raise 'Missing name parameter for prompts/get' unless name
439
+
440
+ arguments = params[:arguments] || {}
441
+
442
+ # Get the prompt
443
+ result = server.get_prompt(name, arguments)
444
+
445
+ {
446
+ jsonrpc: '2.0',
447
+ id: message[:id],
448
+ result: result
449
+ }
450
+ end
451
+
387
452
  public
388
453
 
389
454
  # Send interrupt control request
@@ -6,14 +6,23 @@ module ClaudeAgentSDK
6
6
  # Unlike external MCP servers that run as separate processes, SDK MCP servers
7
7
  # run directly in your application's process, providing better performance
8
8
  # and simpler deployment.
9
+ #
10
+ # Supports:
11
+ # - Tools: Executable functions that Claude can call
12
+ # - Resources: Data sources that can be read (files, databases, APIs, etc.)
13
+ # - Prompts: Reusable prompt templates with arguments
9
14
  class SdkMcpServer
10
- attr_reader :name, :version, :tools
15
+ attr_reader :name, :version, :tools, :resources, :prompts
11
16
 
12
- def initialize(name:, version: '1.0.0', tools: [])
17
+ def initialize(name:, version: '1.0.0', tools: [], resources: [], prompts: [])
13
18
  @name = name
14
19
  @version = version
15
20
  @tools = tools
21
+ @resources = resources
22
+ @prompts = prompts
16
23
  @tool_map = tools.each_with_object({}) { |tool, hash| hash[tool.name] = tool }
24
+ @resource_map = resources.each_with_object({}) { |res, hash| hash[res.uri] = res }
25
+ @prompt_map = prompts.each_with_object({}) { |prompt, hash| hash[prompt.name] = prompt }
17
26
  end
18
27
 
19
28
  # List all available tools
@@ -47,6 +56,68 @@ module ClaudeAgentSDK
47
56
  result
48
57
  end
49
58
 
59
+ # List all available resources
60
+ # @return [Array<Hash>] Array of resource definitions
61
+ def list_resources
62
+ @resources.map do |resource|
63
+ {
64
+ uri: resource.uri,
65
+ name: resource.name,
66
+ description: resource.description,
67
+ mimeType: resource.mime_type
68
+ }.compact
69
+ end
70
+ end
71
+
72
+ # Read a resource by URI
73
+ # @param uri [String] Resource URI
74
+ # @return [Hash] Resource content
75
+ def read_resource(uri)
76
+ resource = @resource_map[uri]
77
+ raise "Resource '#{uri}' not found" unless resource
78
+
79
+ # Call the resource's reader
80
+ content = resource.reader.call
81
+
82
+ # Ensure content has the expected format
83
+ unless content.is_a?(Hash) && content[:contents]
84
+ raise "Resource '#{uri}' must return a hash with :contents key"
85
+ end
86
+
87
+ content
88
+ end
89
+
90
+ # List all available prompts
91
+ # @return [Array<Hash>] Array of prompt definitions
92
+ def list_prompts
93
+ @prompts.map do |prompt|
94
+ {
95
+ name: prompt.name,
96
+ description: prompt.description,
97
+ arguments: prompt.arguments
98
+ }.compact
99
+ end
100
+ end
101
+
102
+ # Get a prompt by name
103
+ # @param name [String] Prompt name
104
+ # @param arguments [Hash] Arguments to fill in the prompt template
105
+ # @return [Hash] Prompt with filled-in arguments
106
+ def get_prompt(name, arguments = {})
107
+ prompt = @prompt_map[name]
108
+ raise "Prompt '#{name}' not found" unless prompt
109
+
110
+ # Call the prompt's generator
111
+ result = prompt.generator.call(arguments)
112
+
113
+ # Ensure result has the expected format
114
+ unless result.is_a?(Hash) && result[:messages]
115
+ raise "Prompt '#{name}' must return a hash with :messages key"
116
+ end
117
+
118
+ result
119
+ end
120
+
50
121
  private
51
122
 
52
123
  def convert_input_schema(schema)
@@ -130,11 +201,123 @@ module ClaudeAgentSDK
130
201
  )
131
202
  end
132
203
 
204
+ # Helper function to create a resource definition
205
+ #
206
+ # @param uri [String] Unique identifier for the resource (e.g., "file:///path/to/file")
207
+ # @param name [String] Human-readable name
208
+ # @param description [String, nil] Optional description
209
+ # @param mime_type [String, nil] Optional MIME type (e.g., "text/plain", "application/json")
210
+ # @param reader [Proc] Block that returns the resource content
211
+ # @return [SdkMcpResource] Resource definition
212
+ #
213
+ # @example File resource
214
+ # resource = create_resource(
215
+ # uri: 'file:///config/settings.json',
216
+ # name: 'Application Settings',
217
+ # description: 'Current application configuration',
218
+ # mime_type: 'application/json'
219
+ # ) do
220
+ # content = File.read('/path/to/settings.json')
221
+ # {
222
+ # contents: [{
223
+ # uri: 'file:///config/settings.json',
224
+ # mimeType: 'application/json',
225
+ # text: content
226
+ # }]
227
+ # }
228
+ # end
229
+ #
230
+ # @example Database resource
231
+ # resource = create_resource(
232
+ # uri: 'db://users/count',
233
+ # name: 'User Count',
234
+ # description: 'Total number of registered users'
235
+ # ) do
236
+ # count = User.count
237
+ # {
238
+ # contents: [{
239
+ # uri: 'db://users/count',
240
+ # mimeType: 'text/plain',
241
+ # text: count.to_s
242
+ # }]
243
+ # }
244
+ # end
245
+ def self.create_resource(uri:, name:, description: nil, mime_type: nil, &reader)
246
+ raise ArgumentError, 'Block required for resource reader' unless reader
247
+
248
+ SdkMcpResource.new(
249
+ uri: uri,
250
+ name: name,
251
+ description: description,
252
+ mime_type: mime_type,
253
+ reader: reader
254
+ )
255
+ end
256
+
257
+ # Helper function to create a prompt definition
258
+ #
259
+ # @param name [String] Unique identifier for the prompt
260
+ # @param description [String, nil] Optional description
261
+ # @param arguments [Array<Hash>, nil] Optional argument definitions
262
+ # @param generator [Proc] Block that generates prompt messages
263
+ # @return [SdkMcpPrompt] Prompt definition
264
+ #
265
+ # @example Simple prompt
266
+ # prompt = create_prompt(
267
+ # name: 'code_review',
268
+ # description: 'Review code for best practices'
269
+ # ) do |args|
270
+ # {
271
+ # messages: [
272
+ # {
273
+ # role: 'user',
274
+ # content: {
275
+ # type: 'text',
276
+ # text: 'Please review this code for best practices and suggest improvements.'
277
+ # }
278
+ # }
279
+ # ]
280
+ # }
281
+ # end
282
+ #
283
+ # @example Prompt with arguments
284
+ # prompt = create_prompt(
285
+ # name: 'git_commit',
286
+ # description: 'Generate a git commit message',
287
+ # arguments: [
288
+ # { name: 'changes', description: 'Description of changes', required: true }
289
+ # ]
290
+ # ) do |args|
291
+ # {
292
+ # messages: [
293
+ # {
294
+ # role: 'user',
295
+ # content: {
296
+ # type: 'text',
297
+ # text: "Generate a concise git commit message for: #{args[:changes]}"
298
+ # }
299
+ # }
300
+ # ]
301
+ # }
302
+ # end
303
+ def self.create_prompt(name:, description: nil, arguments: nil, &generator)
304
+ raise ArgumentError, 'Block required for prompt generator' unless generator
305
+
306
+ SdkMcpPrompt.new(
307
+ name: name,
308
+ description: description,
309
+ arguments: arguments,
310
+ generator: generator
311
+ )
312
+ end
313
+
133
314
  # Create an SDK MCP server
134
315
  #
135
316
  # @param name [String] Unique identifier for the server
136
317
  # @param version [String] Server version (default: '1.0.0')
137
318
  # @param tools [Array<SdkMcpTool>] List of tool definitions
319
+ # @param resources [Array<SdkMcpResource>] List of resource definitions
320
+ # @param prompts [Array<SdkMcpPrompt>] List of prompt definitions
138
321
  # @return [Hash] MCP server configuration for ClaudeAgentOptions
139
322
  #
140
323
  # @example Simple calculator server
@@ -152,8 +335,31 @@ module ClaudeAgentSDK
152
335
  # mcp_servers: { calc: calculator },
153
336
  # allowed_tools: ['mcp__calc__add']
154
337
  # )
155
- def self.create_sdk_mcp_server(name:, version: '1.0.0', tools: [])
156
- server = SdkMcpServer.new(name: name, version: version, tools: tools)
338
+ #
339
+ # @example Server with resources and prompts
340
+ # config_resource = ClaudeAgentSDK.create_resource(
341
+ # uri: 'config://app',
342
+ # name: 'App Config'
343
+ # ) { { contents: [{ uri: 'config://app', text: 'config data' }] } }
344
+ #
345
+ # review_prompt = ClaudeAgentSDK.create_prompt(
346
+ # name: 'review',
347
+ # description: 'Code review'
348
+ # ) { { messages: [{ role: 'user', content: { type: 'text', text: 'Review this' } }] } }
349
+ #
350
+ # server = ClaudeAgentSDK.create_sdk_mcp_server(
351
+ # name: 'dev-tools',
352
+ # resources: [config_resource],
353
+ # prompts: [review_prompt]
354
+ # )
355
+ def self.create_sdk_mcp_server(name:, version: '1.0.0', tools: [], resources: [], prompts: [])
356
+ server = SdkMcpServer.new(
357
+ name: name,
358
+ version: version,
359
+ tools: tools,
360
+ resources: resources,
361
+ prompts: prompts
362
+ )
157
363
 
158
364
  # Return configuration for ClaudeAgentOptions
159
365
  {
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module ClaudeAgentSDK
6
+ # Streaming input helpers for Claude Agent SDK
7
+ module Streaming
8
+ # Create a user message for streaming input
9
+ #
10
+ # @param content [String] The message content
11
+ # @param session_id [String] Session identifier
12
+ # @param parent_tool_use_id [String, nil] Parent tool use ID if responding to a tool
13
+ # @return [String] JSON-encoded message
14
+ def self.user_message(content, session_id: 'default', parent_tool_use_id: nil)
15
+ message = {
16
+ type: 'user',
17
+ message: {
18
+ role: 'user',
19
+ content: content
20
+ },
21
+ parent_tool_use_id: parent_tool_use_id,
22
+ session_id: session_id
23
+ }
24
+ JSON.generate(message) + "\n"
25
+ end
26
+
27
+ # Create an Enumerator from an array of messages
28
+ #
29
+ # @param messages [Array<String>] Array of message strings
30
+ # @param session_id [String] Session identifier
31
+ # @return [Enumerator] Enumerator yielding JSON-encoded messages
32
+ #
33
+ # @example
34
+ # messages = ['Hello', 'What is 2+2?', 'Thanks!']
35
+ # stream = ClaudeAgentSDK::Streaming.from_array(messages)
36
+ def self.from_array(messages, session_id: 'default')
37
+ Enumerator.new do |yielder|
38
+ messages.each do |content|
39
+ yielder << user_message(content, session_id: session_id)
40
+ end
41
+ end
42
+ end
43
+
44
+ # Create an Enumerator from a block
45
+ #
46
+ # @yield Block that yields message strings
47
+ # @param session_id [String] Session identifier
48
+ # @return [Enumerator] Enumerator yielding JSON-encoded messages
49
+ #
50
+ # @example
51
+ # stream = ClaudeAgentSDK::Streaming.from_block do |yielder|
52
+ # yielder.yield('First message')
53
+ # sleep 1
54
+ # yielder.yield('Second message')
55
+ # end
56
+ def self.from_block(session_id: 'default', &block)
57
+ Enumerator.new do |yielder|
58
+ collector = Object.new
59
+ def collector.yield(content)
60
+ @content = content
61
+ end
62
+ def collector.content
63
+ @content
64
+ end
65
+
66
+ inner_enum = Enumerator.new(&block)
67
+ inner_enum.each do |content|
68
+ yielder << user_message(content, session_id: session_id)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -162,23 +162,14 @@ module ClaudeAgentSDK
162
162
  should_pipe_stderr = @options.stderr || @options.extra_args.key?('debug-to-stderr')
163
163
 
164
164
  begin
165
- # Start process
166
- @process = Async::Process::Child.new(
167
- *cmd,
168
- stdin: :pipe,
169
- stdout: :pipe,
170
- stderr: should_pipe_stderr ? :pipe : nil,
171
- chdir: @cwd&.to_s,
172
- env: process_env
173
- )
165
+ # Start process using Open3
166
+ opts = { chdir: @cwd&.to_s }.compact
174
167
 
175
- @stdout = @process.stdout
176
- @stdin = @process.stdin if @is_streaming
168
+ @stdin, @stdout, @stderr, @process = Open3.popen3(process_env, *cmd, opts)
177
169
 
178
170
  # Handle stderr if piped
179
- if should_pipe_stderr && @process.stderr
180
- @stderr = @process.stderr
181
- @stderr_task = Async do
171
+ if should_pipe_stderr && @stderr
172
+ @stderr_task = Thread.new do
182
173
  handle_stderr
183
174
  rescue StandardError
184
175
  # Ignore errors during stderr reading
@@ -186,7 +177,8 @@ module ClaudeAgentSDK
186
177
  end
187
178
 
188
179
  # Close stdin for non-streaming mode
189
- @process.stdin.close unless @is_streaming
180
+ @stdin.close unless @is_streaming
181
+ @stdin = nil unless @is_streaming
190
182
 
191
183
  @ready = true
192
184
  rescue Errno::ENOENT => e
@@ -223,8 +215,11 @@ module ClaudeAgentSDK
223
215
  @ready = false
224
216
  return unless @process
225
217
 
226
- # Cancel stderr task
227
- @stderr_task&.stop
218
+ # Kill stderr thread
219
+ if @stderr_task&.alive?
220
+ @stderr_task.kill
221
+ @stderr_task.join(1) rescue nil
222
+ end
228
223
 
229
224
  # Close streams
230
225
  begin
@@ -232,6 +227,11 @@ module ClaudeAgentSDK
232
227
  rescue StandardError
233
228
  # Ignore
234
229
  end
230
+ begin
231
+ @stdout&.close
232
+ rescue StandardError
233
+ # Ignore
234
+ end
235
235
  begin
236
236
  @stderr&.close
237
237
  rescue StandardError
@@ -240,8 +240,8 @@ module ClaudeAgentSDK
240
240
 
241
241
  # Terminate process
242
242
  begin
243
- @process.terminate
244
- @process.wait
243
+ Process.kill('TERM', @process.pid) if @process.alive?
244
+ @process.value
245
245
  rescue StandardError
246
246
  # Ignore
247
247
  end
@@ -250,12 +250,13 @@ module ClaudeAgentSDK
250
250
  @stdout = nil
251
251
  @stdin = nil
252
252
  @stderr = nil
253
+ @stderr_task = nil
253
254
  @exit_error = nil
254
255
  end
255
256
 
256
257
  def write(data)
257
258
  raise CLIConnectionError, 'ProcessTransport is not ready for writing' unless @ready && @stdin
258
- raise CLIConnectionError, "Cannot write to terminated process" if @process && @process.status
259
+ raise CLIConnectionError, "Cannot write to terminated process" if @process && !@process.alive?
259
260
 
260
261
  raise CLIConnectionError, "Cannot write to process that exited with error: #{@exit_error}" if @exit_error
261
262
 
@@ -326,7 +327,7 @@ module ClaudeAgentSDK
326
327
  end
327
328
 
328
329
  # Check process completion
329
- status = @process.wait
330
+ status = @process.value
330
331
  returncode = status.exitstatus
331
332
 
332
333
  if returncode && returncode != 0
@@ -355,4 +355,29 @@ module ClaudeAgentSDK
355
355
  @handler = handler
356
356
  end
357
357
  end
358
+
359
+ # SDK MCP Resource definition
360
+ class SdkMcpResource
361
+ attr_accessor :uri, :name, :description, :mime_type, :reader
362
+
363
+ def initialize(uri:, name:, description: nil, mime_type: nil, reader:)
364
+ @uri = uri
365
+ @name = name
366
+ @description = description
367
+ @mime_type = mime_type
368
+ @reader = reader
369
+ end
370
+ end
371
+
372
+ # SDK MCP Prompt definition
373
+ class SdkMcpPrompt
374
+ attr_accessor :name, :description, :arguments, :generator
375
+
376
+ def initialize(name:, description: nil, arguments: nil, generator:)
377
+ @name = name
378
+ @description = description
379
+ @arguments = arguments
380
+ @generator = generator
381
+ end
382
+ end
358
383
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.3'
5
5
  end
@@ -8,6 +8,7 @@ require_relative 'claude_agent_sdk/subprocess_cli_transport'
8
8
  require_relative 'claude_agent_sdk/message_parser'
9
9
  require_relative 'claude_agent_sdk/query'
10
10
  require_relative 'claude_agent_sdk/sdk_mcp_server'
11
+ require_relative 'claude_agent_sdk/streaming'
11
12
  require 'async'
12
13
  require 'securerandom'
13
14
 
@@ -18,7 +19,7 @@ module ClaudeAgentSDK
18
19
  # This function is ideal for simple, stateless queries where you don't need
19
20
  # bidirectional communication or conversation management.
20
21
  #
21
- # @param prompt [String] The prompt to send to Claude
22
+ # @param prompt [String, Enumerator] The prompt to send to Claude, or an Enumerator for streaming input
22
23
  # @param options [ClaudeAgentOptions] Optional configuration
23
24
  # @yield [Message] Each message from the conversation
24
25
  # @return [Enumerator] if no block given
@@ -40,6 +41,12 @@ module ClaudeAgentSDK
40
41
  # end
41
42
  # end
42
43
  # end
44
+ #
45
+ # @example Streaming input
46
+ # messages = Streaming.from_array(['Hello', 'What is 2+2?', 'Thanks!'])
47
+ # ClaudeAgentSDK.query(prompt: messages) do |message|
48
+ # puts message
49
+ # end
43
50
  def self.query(prompt:, options: nil, &block)
44
51
  return enum_for(:query, prompt: prompt, options: options) unless block
45
52
 
@@ -50,6 +57,21 @@ module ClaudeAgentSDK
50
57
  transport = SubprocessCLITransport.new(prompt, options)
51
58
  begin
52
59
  transport.connect
60
+
61
+ # If prompt is an Enumerator, write each message to stdin
62
+ if prompt.is_a?(Enumerator) || prompt.respond_to?(:each)
63
+ Async do
64
+ begin
65
+ prompt.each do |message_json|
66
+ transport.write(message_json)
67
+ end
68
+ ensure
69
+ transport.end_input
70
+ end
71
+ end
72
+ end
73
+
74
+ # Read and yield messages
53
75
  transport.read_messages do |data|
54
76
  message = MessageParser.parse(data)
55
77
  block.call(message)
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.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Community Contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-10-14 00:00:00.000000000 Z
10
+ date: 2025-10-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async
@@ -94,6 +94,7 @@ files:
94
94
  - lib/claude_agent_sdk/message_parser.rb
95
95
  - lib/claude_agent_sdk/query.rb
96
96
  - lib/claude_agent_sdk/sdk_mcp_server.rb
97
+ - lib/claude_agent_sdk/streaming.rb
97
98
  - lib/claude_agent_sdk/subprocess_cli_transport.rb
98
99
  - lib/claude_agent_sdk/transport.rb
99
100
  - lib/claude_agent_sdk/types.rb