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
|