claude-agent-sdk 0.1.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.
@@ -0,0 +1,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeAgentSDK
4
+ # Content Blocks
5
+
6
+ # Text content block
7
+ class TextBlock
8
+ attr_accessor :text
9
+
10
+ def initialize(text:)
11
+ @text = text
12
+ end
13
+ end
14
+
15
+ # Thinking content block
16
+ class ThinkingBlock
17
+ attr_accessor :thinking, :signature
18
+
19
+ def initialize(thinking:, signature:)
20
+ @thinking = thinking
21
+ @signature = signature
22
+ end
23
+ end
24
+
25
+ # Tool use content block
26
+ class ToolUseBlock
27
+ attr_accessor :id, :name, :input
28
+
29
+ def initialize(id:, name:, input:)
30
+ @id = id
31
+ @name = name
32
+ @input = input
33
+ end
34
+ end
35
+
36
+ # Tool result content block
37
+ class ToolResultBlock
38
+ attr_accessor :tool_use_id, :content, :is_error
39
+
40
+ def initialize(tool_use_id:, content: nil, is_error: nil)
41
+ @tool_use_id = tool_use_id
42
+ @content = content
43
+ @is_error = is_error
44
+ end
45
+ end
46
+
47
+ # Message Types
48
+
49
+ # User message
50
+ class UserMessage
51
+ attr_accessor :content, :parent_tool_use_id
52
+
53
+ def initialize(content:, parent_tool_use_id: nil)
54
+ @content = content
55
+ @parent_tool_use_id = parent_tool_use_id
56
+ end
57
+ end
58
+
59
+ # Assistant message with content blocks
60
+ class AssistantMessage
61
+ attr_accessor :content, :model, :parent_tool_use_id
62
+
63
+ def initialize(content:, model:, parent_tool_use_id: nil)
64
+ @content = content
65
+ @model = model
66
+ @parent_tool_use_id = parent_tool_use_id
67
+ end
68
+ end
69
+
70
+ # System message with metadata
71
+ class SystemMessage
72
+ attr_accessor :subtype, :data
73
+
74
+ def initialize(subtype:, data:)
75
+ @subtype = subtype
76
+ @data = data
77
+ end
78
+ end
79
+
80
+ # Result message with cost and usage information
81
+ class ResultMessage
82
+ attr_accessor :subtype, :duration_ms, :duration_api_ms, :is_error,
83
+ :num_turns, :session_id, :total_cost_usd, :usage, :result
84
+
85
+ def initialize(subtype:, duration_ms:, duration_api_ms:, is_error:,
86
+ num_turns:, session_id:, total_cost_usd: nil, usage: nil, result: nil)
87
+ @subtype = subtype
88
+ @duration_ms = duration_ms
89
+ @duration_api_ms = duration_api_ms
90
+ @is_error = is_error
91
+ @num_turns = num_turns
92
+ @session_id = session_id
93
+ @total_cost_usd = total_cost_usd
94
+ @usage = usage
95
+ @result = result
96
+ end
97
+ end
98
+
99
+ # Stream event for partial message updates
100
+ class StreamEvent
101
+ attr_accessor :uuid, :session_id, :event, :parent_tool_use_id
102
+
103
+ def initialize(uuid:, session_id:, event:, parent_tool_use_id: nil)
104
+ @uuid = uuid
105
+ @session_id = session_id
106
+ @event = event
107
+ @parent_tool_use_id = parent_tool_use_id
108
+ end
109
+ end
110
+
111
+ # Agent definition configuration
112
+ class AgentDefinition
113
+ attr_accessor :description, :prompt, :tools, :model
114
+
115
+ def initialize(description:, prompt:, tools: nil, model: nil)
116
+ @description = description
117
+ @prompt = prompt
118
+ @tools = tools
119
+ @model = model
120
+ end
121
+ end
122
+
123
+ # Permission rule value
124
+ class PermissionRuleValue
125
+ attr_accessor :tool_name, :rule_content
126
+
127
+ def initialize(tool_name:, rule_content: nil)
128
+ @tool_name = tool_name
129
+ @rule_content = rule_content
130
+ end
131
+ end
132
+
133
+ # Permission update configuration
134
+ class PermissionUpdate
135
+ attr_accessor :type, :rules, :behavior, :mode, :directories, :destination
136
+
137
+ def initialize(type:, rules: nil, behavior: nil, mode: nil, directories: nil, destination: nil)
138
+ @type = type
139
+ @rules = rules
140
+ @behavior = behavior
141
+ @mode = mode
142
+ @directories = directories
143
+ @destination = destination
144
+ end
145
+
146
+ def to_h
147
+ result = { type: @type }
148
+ result[:destination] = @destination if @destination
149
+
150
+ case @type
151
+ when 'addRules', 'replaceRules', 'removeRules'
152
+ if @rules
153
+ result[:rules] = @rules.map do |rule|
154
+ {
155
+ toolName: rule.tool_name,
156
+ ruleContent: rule.rule_content
157
+ }
158
+ end
159
+ end
160
+ result[:behavior] = @behavior if @behavior
161
+ when 'setMode'
162
+ result[:mode] = @mode if @mode
163
+ when 'addDirectories', 'removeDirectories'
164
+ result[:directories] = @directories if @directories
165
+ end
166
+
167
+ result
168
+ end
169
+ end
170
+
171
+ # Tool permission context
172
+ class ToolPermissionContext
173
+ attr_accessor :signal, :suggestions
174
+
175
+ def initialize(signal: nil, suggestions: [])
176
+ @signal = signal
177
+ @suggestions = suggestions
178
+ end
179
+ end
180
+
181
+ # Permission results
182
+ class PermissionResultAllow
183
+ attr_accessor :behavior, :updated_input, :updated_permissions
184
+
185
+ def initialize(updated_input: nil, updated_permissions: nil)
186
+ @behavior = 'allow'
187
+ @updated_input = updated_input
188
+ @updated_permissions = updated_permissions
189
+ end
190
+ end
191
+
192
+ class PermissionResultDeny
193
+ attr_accessor :behavior, :message, :interrupt
194
+
195
+ def initialize(message: '', interrupt: false)
196
+ @behavior = 'deny'
197
+ @message = message
198
+ @interrupt = interrupt
199
+ end
200
+ end
201
+
202
+ # Hook matcher configuration
203
+ class HookMatcher
204
+ attr_accessor :matcher, :hooks
205
+
206
+ def initialize(matcher: nil, hooks: [])
207
+ @matcher = matcher
208
+ @hooks = hooks
209
+ end
210
+ end
211
+
212
+ # MCP Server configurations
213
+ class McpStdioServerConfig
214
+ attr_accessor :type, :command, :args, :env
215
+
216
+ def initialize(command:, args: nil, env: nil, type: 'stdio')
217
+ @type = type
218
+ @command = command
219
+ @args = args
220
+ @env = env
221
+ end
222
+
223
+ def to_h
224
+ result = { type: @type, command: @command }
225
+ result[:args] = @args if @args
226
+ result[:env] = @env if @env
227
+ result
228
+ end
229
+ end
230
+
231
+ class McpSSEServerConfig
232
+ attr_accessor :type, :url, :headers
233
+
234
+ def initialize(url:, headers: nil)
235
+ @type = 'sse'
236
+ @url = url
237
+ @headers = headers
238
+ end
239
+
240
+ def to_h
241
+ result = { type: @type, url: @url }
242
+ result[:headers] = @headers if @headers
243
+ result
244
+ end
245
+ end
246
+
247
+ class McpHttpServerConfig
248
+ attr_accessor :type, :url, :headers
249
+
250
+ def initialize(url:, headers: nil)
251
+ @type = 'http'
252
+ @url = url
253
+ @headers = headers
254
+ end
255
+
256
+ def to_h
257
+ result = { type: @type, url: @url }
258
+ result[:headers] = @headers if @headers
259
+ result
260
+ end
261
+ end
262
+
263
+ class McpSdkServerConfig
264
+ attr_accessor :type, :name, :instance
265
+
266
+ def initialize(name:, instance:)
267
+ @type = 'sdk'
268
+ @name = name
269
+ @instance = instance
270
+ end
271
+
272
+ def to_h
273
+ { type: @type, name: @name, instance: @instance }
274
+ end
275
+ end
276
+
277
+ # Claude Agent Options for configuring queries
278
+ class ClaudeAgentOptions
279
+ attr_accessor :allowed_tools, :system_prompt, :mcp_servers, :permission_mode,
280
+ :continue_conversation, :resume, :max_turns, :disallowed_tools,
281
+ :model, :permission_prompt_tool_name, :cwd, :cli_path, :settings,
282
+ :add_dirs, :env, :extra_args, :max_buffer_size, :stderr,
283
+ :can_use_tool, :hooks, :user, :include_partial_messages,
284
+ :fork_session, :agents, :setting_sources
285
+
286
+ def initialize(
287
+ allowed_tools: [],
288
+ system_prompt: nil,
289
+ mcp_servers: {},
290
+ permission_mode: nil,
291
+ continue_conversation: false,
292
+ resume: nil,
293
+ max_turns: nil,
294
+ disallowed_tools: [],
295
+ model: nil,
296
+ permission_prompt_tool_name: nil,
297
+ cwd: nil,
298
+ cli_path: nil,
299
+ settings: nil,
300
+ add_dirs: [],
301
+ env: {},
302
+ extra_args: {},
303
+ max_buffer_size: nil,
304
+ stderr: nil,
305
+ can_use_tool: nil,
306
+ hooks: nil,
307
+ user: nil,
308
+ include_partial_messages: false,
309
+ fork_session: false,
310
+ agents: nil,
311
+ setting_sources: nil
312
+ )
313
+ @allowed_tools = allowed_tools
314
+ @system_prompt = system_prompt
315
+ @mcp_servers = mcp_servers
316
+ @permission_mode = permission_mode
317
+ @continue_conversation = continue_conversation
318
+ @resume = resume
319
+ @max_turns = max_turns
320
+ @disallowed_tools = disallowed_tools
321
+ @model = model
322
+ @permission_prompt_tool_name = permission_prompt_tool_name
323
+ @cwd = cwd
324
+ @cli_path = cli_path
325
+ @settings = settings
326
+ @add_dirs = add_dirs
327
+ @env = env
328
+ @extra_args = extra_args
329
+ @max_buffer_size = max_buffer_size
330
+ @stderr = stderr
331
+ @can_use_tool = can_use_tool
332
+ @hooks = hooks
333
+ @user = user
334
+ @include_partial_messages = include_partial_messages
335
+ @fork_session = fork_session
336
+ @agents = agents
337
+ @setting_sources = setting_sources
338
+ end
339
+
340
+ def dup_with(**changes)
341
+ new_options = self.dup
342
+ changes.each { |key, value| new_options.send("#{key}=", value) }
343
+ new_options
344
+ end
345
+ end
346
+
347
+ # SDK MCP Tool definition
348
+ class SdkMcpTool
349
+ attr_accessor :name, :description, :input_schema, :handler
350
+
351
+ def initialize(name:, description:, input_schema:, handler:)
352
+ @name = name
353
+ @description = description
354
+ @input_schema = input_schema
355
+ @handler = handler
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeAgentSDK
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'claude_agent_sdk/version'
4
+ require_relative 'claude_agent_sdk/errors'
5
+ require_relative 'claude_agent_sdk/types'
6
+ require_relative 'claude_agent_sdk/transport'
7
+ require_relative 'claude_agent_sdk/subprocess_cli_transport'
8
+ require_relative 'claude_agent_sdk/message_parser'
9
+ require_relative 'claude_agent_sdk/query'
10
+ require_relative 'claude_agent_sdk/sdk_mcp_server'
11
+ require 'async'
12
+ require 'securerandom'
13
+
14
+ # Claude Agent SDK for Ruby
15
+ module ClaudeAgentSDK
16
+ # Query Claude Code for one-shot or unidirectional streaming interactions
17
+ #
18
+ # This function is ideal for simple, stateless queries where you don't need
19
+ # bidirectional communication or conversation management.
20
+ #
21
+ # @param prompt [String] The prompt to send to Claude
22
+ # @param options [ClaudeAgentOptions] Optional configuration
23
+ # @yield [Message] Each message from the conversation
24
+ # @return [Enumerator] if no block given
25
+ #
26
+ # @example Simple query
27
+ # ClaudeAgentSDK.query(prompt: "What is 2 + 2?") do |message|
28
+ # puts message
29
+ # end
30
+ #
31
+ # @example With options
32
+ # options = ClaudeAgentSDK::ClaudeAgentOptions.new(
33
+ # allowed_tools: ['Read', 'Bash'],
34
+ # permission_mode: 'acceptEdits'
35
+ # )
36
+ # ClaudeAgentSDK.query(prompt: "Create a hello.rb file", options: options) do |msg|
37
+ # if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
38
+ # msg.content.each do |block|
39
+ # puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
40
+ # end
41
+ # end
42
+ # end
43
+ def self.query(prompt:, options: nil, &block)
44
+ return enum_for(:query, prompt: prompt, options: options) unless block
45
+
46
+ options ||= ClaudeAgentOptions.new
47
+ ENV['CLAUDE_CODE_ENTRYPOINT'] = 'sdk-rb'
48
+
49
+ Async do
50
+ transport = SubprocessCLITransport.new(prompt, options)
51
+ begin
52
+ transport.connect
53
+ transport.read_messages do |data|
54
+ message = MessageParser.parse(data)
55
+ block.call(message)
56
+ end
57
+ ensure
58
+ transport.close
59
+ end
60
+ end.wait
61
+ end
62
+
63
+ # Client for bidirectional, interactive conversations with Claude Code
64
+ #
65
+ # This client provides full control over the conversation flow with support
66
+ # for streaming, hooks, permission callbacks, and dynamic message sending.
67
+ #
68
+ # @example Basic usage
69
+ # Async do
70
+ # client = ClaudeAgentSDK::Client.new
71
+ # client.connect
72
+ #
73
+ # client.query("What is the capital of France?")
74
+ # client.receive_response do |msg|
75
+ # puts msg if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
76
+ # end
77
+ #
78
+ # client.disconnect
79
+ # end
80
+ #
81
+ # @example With hooks
82
+ # options = ClaudeAgentOptions.new(
83
+ # hooks: {
84
+ # 'PreToolUse' => [
85
+ # HookMatcher.new(
86
+ # matcher: 'Bash',
87
+ # hooks: [
88
+ # ->(input, tool_use_id, context) {
89
+ # # Return hook output
90
+ # {}
91
+ # }
92
+ # ]
93
+ # )
94
+ # ]
95
+ # }
96
+ # )
97
+ # client = ClaudeAgentSDK::Client.new(options: options)
98
+ class Client
99
+ attr_reader :query_handler
100
+
101
+ def initialize(options: nil)
102
+ @options = options || ClaudeAgentOptions.new
103
+ @transport = nil
104
+ @query_handler = nil
105
+ @connected = false
106
+ ENV['CLAUDE_CODE_ENTRYPOINT'] = 'sdk-rb-client'
107
+ end
108
+
109
+ # Connect to Claude with optional initial prompt
110
+ # @param prompt [String, Enumerator, nil] Initial prompt or message stream
111
+ def connect(prompt = nil)
112
+ return if @connected
113
+
114
+ # Validate and configure permission settings
115
+ configured_options = @options
116
+ if @options.can_use_tool
117
+ # can_use_tool requires streaming mode
118
+ if prompt.is_a?(String)
119
+ raise ArgumentError, 'can_use_tool callback requires streaming mode'
120
+ end
121
+
122
+ # can_use_tool and permission_prompt_tool_name are mutually exclusive
123
+ if @options.permission_prompt_tool_name
124
+ raise ArgumentError, 'can_use_tool callback cannot be used with permission_prompt_tool_name'
125
+ end
126
+
127
+ # Set permission_prompt_tool_name to stdio for control protocol
128
+ configured_options = @options.dup_with(permission_prompt_tool_name: 'stdio')
129
+ end
130
+
131
+ # Create transport
132
+ actual_prompt = prompt || ''
133
+ @transport = SubprocessCLITransport.new(actual_prompt, configured_options)
134
+ @transport.connect
135
+
136
+ # Extract SDK MCP servers
137
+ sdk_mcp_servers = {}
138
+ if configured_options.mcp_servers.is_a?(Hash)
139
+ configured_options.mcp_servers.each do |name, config|
140
+ sdk_mcp_servers[name] = config[:instance] if config.is_a?(Hash) && config[:type] == 'sdk'
141
+ end
142
+ end
143
+
144
+ # Convert hooks to internal format
145
+ hooks = convert_hooks_to_internal_format(configured_options.hooks) if configured_options.hooks
146
+
147
+ # Create Query handler
148
+ @query_handler = Query.new(
149
+ transport: @transport,
150
+ is_streaming_mode: true,
151
+ can_use_tool: configured_options.can_use_tool,
152
+ hooks: hooks,
153
+ sdk_mcp_servers: sdk_mcp_servers
154
+ )
155
+
156
+ # Start query handler and initialize
157
+ @query_handler.start
158
+ @query_handler.initialize_protocol
159
+
160
+ @connected = true
161
+ end
162
+
163
+ # Send a query to Claude
164
+ # @param prompt [String] The prompt to send
165
+ # @param session_id [String] Session identifier
166
+ def query(prompt, session_id: 'default')
167
+ raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
168
+
169
+ message = {
170
+ type: 'user',
171
+ message: { role: 'user', content: prompt },
172
+ parent_tool_use_id: nil,
173
+ session_id: session_id
174
+ }
175
+ @transport.write(JSON.generate(message) + "\n")
176
+ end
177
+
178
+ # Receive all messages from Claude
179
+ # @yield [Message] Each message received
180
+ def receive_messages(&block)
181
+ return enum_for(:receive_messages) unless block
182
+
183
+ raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
184
+
185
+ @query_handler.receive_messages do |data|
186
+ message = MessageParser.parse(data)
187
+ block.call(message)
188
+ end
189
+ end
190
+
191
+ # Receive messages until a ResultMessage is received
192
+ # @yield [Message] Each message received
193
+ def receive_response(&block)
194
+ return enum_for(:receive_response) unless block
195
+
196
+ receive_messages do |message|
197
+ block.call(message)
198
+ break if message.is_a?(ResultMessage)
199
+ end
200
+ end
201
+
202
+ # Send interrupt signal
203
+ def interrupt
204
+ raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
205
+ @query_handler.interrupt
206
+ end
207
+
208
+ # Change permission mode during conversation
209
+ # @param mode [String] Permission mode ('default', 'acceptEdits', 'bypassPermissions')
210
+ def set_permission_mode(mode)
211
+ raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
212
+ @query_handler.set_permission_mode(mode)
213
+ end
214
+
215
+ # Change the AI model during conversation
216
+ # @param model [String, nil] Model name or nil for default
217
+ def set_model(model)
218
+ raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
219
+ @query_handler.set_model(model)
220
+ end
221
+
222
+ # Get server initialization info
223
+ # @return [Hash, nil] Server info or nil
224
+ def server_info
225
+ @query_handler&.instance_variable_get(:@initialization_result)
226
+ end
227
+
228
+ # Disconnect from Claude
229
+ def disconnect
230
+ return unless @connected
231
+
232
+ @query_handler&.close
233
+ @query_handler = nil
234
+ @transport = nil
235
+ @connected = false
236
+ end
237
+
238
+ private
239
+
240
+ def convert_hooks_to_internal_format(hooks)
241
+ return nil unless hooks
242
+
243
+ internal_hooks = {}
244
+ hooks.each do |event, matchers|
245
+ internal_hooks[event.to_s] = []
246
+ matchers.each do |matcher|
247
+ internal_hooks[event.to_s] << {
248
+ matcher: matcher.matcher,
249
+ hooks: matcher.hooks
250
+ }
251
+ end
252
+ end
253
+ internal_hooks
254
+ end
255
+ end
256
+ end