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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE +21 -0
- data/README.md +432 -0
- data/lib/claude_agent_sdk/errors.rb +53 -0
- data/lib/claude_agent_sdk/message_parser.rb +110 -0
- data/lib/claude_agent_sdk/query.rb +442 -0
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +165 -0
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +365 -0
- data/lib/claude_agent_sdk/transport.rb +44 -0
- data/lib/claude_agent_sdk/types.rb +358 -0
- data/lib/claude_agent_sdk/version.rb +5 -0
- data/lib/claude_agent_sdk.rb +256 -0
- metadata +126 -0
| @@ -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,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
         |