claude-code-sdk-ruby 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/.rspec +3 -0
- data/.rubocop.yml +19 -0
- data/.ruby-version +1 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +43 -0
- data/LICENSE +21 -0
- data/README.md +220 -0
- data/Rakefile +13 -0
- data/claude-code-sdk-ruby.gemspec +36 -0
- data/examples/async_multiple_queries.rb +36 -0
- data/examples/basic_usage.rb +82 -0
- data/examples/enumerator_usage.rb +50 -0
- data/examples/error_handling.rb +31 -0
- data/examples/mcp_servers.rb +44 -0
- data/examples/with_options.rb +41 -0
- data/lib/claude_sdk/errors.rb +75 -0
- data/lib/claude_sdk/internal/client.rb +122 -0
- data/lib/claude_sdk/internal/transport/subprocess_cli.rb +362 -0
- data/lib/claude_sdk/internal/transport.rb +53 -0
- data/lib/claude_sdk/types.rb +427 -0
- data/lib/claude_sdk/version.rb +5 -0
- data/lib/claude_sdk.rb +59 -0
- data/ruby-sdk-maintenance-swarm.yml +67 -0
- metadata +95 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClaudeSDK
|
4
|
+
# Base exception for all Claude SDK errors
|
5
|
+
#
|
6
|
+
# @abstract All Claude SDK errors inherit from this class
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
# Raised when unable to connect to Claude Code
|
10
|
+
#
|
11
|
+
# @see CLINotFoundError
|
12
|
+
class CLIConnectionError < Error; end
|
13
|
+
|
14
|
+
# Raised when Claude Code is not found or not installed
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# raise CLINotFoundError.new("Claude Code not found", cli_path: "/usr/local/bin/claude")
|
18
|
+
class CLINotFoundError < CLIConnectionError
|
19
|
+
# @return [String, nil] the path where Claude Code was expected
|
20
|
+
attr_reader :cli_path
|
21
|
+
|
22
|
+
# @param message [String] the error message
|
23
|
+
# @param cli_path [String, nil] the path where Claude Code was expected
|
24
|
+
def initialize(message: "Claude Code not found", cli_path: nil)
|
25
|
+
@cli_path = cli_path
|
26
|
+
message = "#{message}: #{cli_path}" if cli_path
|
27
|
+
super(message)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Raised when the CLI process fails
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# raise ProcessError.new("Command failed", exit_code: 1, stderr: "Error output")
|
35
|
+
class ProcessError < Error
|
36
|
+
# @return [Integer, nil] the process exit code
|
37
|
+
attr_reader :exit_code
|
38
|
+
|
39
|
+
# @return [String, nil] the stderr output from the process
|
40
|
+
attr_reader :stderr
|
41
|
+
|
42
|
+
# @param message [String] the error message
|
43
|
+
# @param exit_code [Integer, nil] the process exit code
|
44
|
+
# @param stderr [String, nil] the stderr output
|
45
|
+
def initialize(message, exit_code: nil, stderr: nil)
|
46
|
+
@exit_code = exit_code
|
47
|
+
@stderr = stderr
|
48
|
+
|
49
|
+
message = "#{message} (exit code: #{exit_code})" if exit_code
|
50
|
+
message = "#{message}\nError output: #{stderr}" if stderr
|
51
|
+
|
52
|
+
super(message)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Raised when unable to decode JSON from CLI output
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# raise CLIJSONDecodeError.new("invalid json", original_error: JSON::ParserError.new)
|
60
|
+
class CLIJSONDecodeError < Error
|
61
|
+
# @return [String] the line that failed to decode
|
62
|
+
attr_reader :line
|
63
|
+
|
64
|
+
# @return [Exception] the original JSON parsing error
|
65
|
+
attr_reader :original_error
|
66
|
+
|
67
|
+
# @param line [String] the line that failed to decode
|
68
|
+
# @param original_error [Exception] the original JSON parsing error
|
69
|
+
def initialize(line:, original_error:)
|
70
|
+
@line = line
|
71
|
+
@original_error = original_error
|
72
|
+
super("Failed to decode JSON: #{line[0..99]}...")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "async"
|
4
|
+
require_relative "transport/subprocess_cli"
|
5
|
+
|
6
|
+
module ClaudeSDK
|
7
|
+
module Internal
|
8
|
+
# Internal client implementation for processing queries through transport
|
9
|
+
#
|
10
|
+
# This client handles the communication with Claude Code CLI via the
|
11
|
+
# transport layer and parses the received messages into Ruby objects.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# client = InternalClient.new
|
15
|
+
# Async do
|
16
|
+
# client.process_query("Hello Claude", options) do |message|
|
17
|
+
# puts message
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
class InternalClient
|
21
|
+
# Initialize the internal client
|
22
|
+
def initialize
|
23
|
+
# Currently no initialization needed
|
24
|
+
end
|
25
|
+
|
26
|
+
# Process a query through transport
|
27
|
+
#
|
28
|
+
# @param prompt [String] the prompt to send to Claude
|
29
|
+
# @param options [ClaudeCodeOptions] configuration options
|
30
|
+
# @yield [Messages::User, Messages::Assistant, Messages::System, Messages::Result] parsed message
|
31
|
+
# @return [Enumerator] if no block given
|
32
|
+
def process_query(prompt:, options:)
|
33
|
+
return enum_for(:process_query, prompt: prompt, options: options) unless block_given?
|
34
|
+
|
35
|
+
transport = SubprocessCLI.new(
|
36
|
+
prompt: prompt,
|
37
|
+
options: options,
|
38
|
+
)
|
39
|
+
|
40
|
+
begin
|
41
|
+
transport.connect
|
42
|
+
|
43
|
+
transport.receive_messages do |data|
|
44
|
+
message = parse_message(data)
|
45
|
+
yield message if message
|
46
|
+
end
|
47
|
+
ensure
|
48
|
+
transport.disconnect
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Parse message from CLI output
|
55
|
+
#
|
56
|
+
# @param data [Hash] raw message data from CLI
|
57
|
+
# @return [Messages::User, Messages::Assistant, Messages::System, Messages::Result, nil] parsed message
|
58
|
+
def parse_message(data)
|
59
|
+
case data["type"]
|
60
|
+
when "user"
|
61
|
+
Messages::User.new(
|
62
|
+
content: data.dig("message", "content"),
|
63
|
+
)
|
64
|
+
|
65
|
+
when "assistant"
|
66
|
+
content_blocks = parse_content_blocks(data.dig("message", "content") || [])
|
67
|
+
Messages::Assistant.new(content: content_blocks)
|
68
|
+
|
69
|
+
when "system"
|
70
|
+
Messages::System.new(
|
71
|
+
subtype: data["subtype"],
|
72
|
+
data: data,
|
73
|
+
)
|
74
|
+
|
75
|
+
when "result"
|
76
|
+
Messages::Result.new(
|
77
|
+
subtype: data["subtype"],
|
78
|
+
duration_ms: data["duration_ms"],
|
79
|
+
duration_api_ms: data["duration_api_ms"],
|
80
|
+
is_error: data["is_error"],
|
81
|
+
num_turns: data["num_turns"],
|
82
|
+
session_id: data["session_id"],
|
83
|
+
total_cost_usd: data["total_cost_usd"],
|
84
|
+
usage: data["usage"],
|
85
|
+
result: data["result"],
|
86
|
+
)
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Parse content blocks from assistant message
|
92
|
+
#
|
93
|
+
# @param blocks [Array<Hash>] raw content blocks
|
94
|
+
# @return [Array<ContentBlock::Text, ContentBlock::ToolUse, ContentBlock::ToolResult>] parsed blocks
|
95
|
+
def parse_content_blocks(blocks)
|
96
|
+
blocks.map do |block|
|
97
|
+
case block["type"]
|
98
|
+
when "text"
|
99
|
+
ContentBlock::Text.new(
|
100
|
+
text: block["text"],
|
101
|
+
)
|
102
|
+
|
103
|
+
when "tool_use"
|
104
|
+
ContentBlock::ToolUse.new(
|
105
|
+
id: block["id"],
|
106
|
+
name: block["name"],
|
107
|
+
input: block["input"],
|
108
|
+
)
|
109
|
+
|
110
|
+
when "tool_result"
|
111
|
+
ContentBlock::ToolResult.new(
|
112
|
+
tool_use_id: block["tool_use_id"],
|
113
|
+
content: block["content"],
|
114
|
+
is_error: block["is_error"],
|
115
|
+
)
|
116
|
+
|
117
|
+
end
|
118
|
+
end.compact
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,362 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "async"
|
4
|
+
require "open3"
|
5
|
+
require "json"
|
6
|
+
require "logger"
|
7
|
+
require "pathname"
|
8
|
+
require "timeout"
|
9
|
+
require_relative "../transport"
|
10
|
+
|
11
|
+
module ClaudeSDK
|
12
|
+
module Internal
|
13
|
+
# Subprocess transport implementation using Claude Code CLI
|
14
|
+
#
|
15
|
+
# This transport launches the Claude Code CLI as a subprocess and
|
16
|
+
# communicates with it via JSON streaming on stdout/stderr.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# transport = SubprocessCLI.new(
|
20
|
+
# prompt: "Hello Claude",
|
21
|
+
# options: ClaudeCodeOptions.new
|
22
|
+
# )
|
23
|
+
# transport.connect
|
24
|
+
# transport.receive_messages do |message|
|
25
|
+
# puts message
|
26
|
+
# end
|
27
|
+
# transport.disconnect
|
28
|
+
class SubprocessCLI < Transport
|
29
|
+
# Maximum buffer size for JSON messages (1MB)
|
30
|
+
MAX_BUFFER_SIZE = 1024 * 1024
|
31
|
+
|
32
|
+
# @return [Logger] the logger instance
|
33
|
+
attr_reader :logger
|
34
|
+
|
35
|
+
# Initialize subprocess transport
|
36
|
+
#
|
37
|
+
# @param prompt [String] the prompt to send to Claude
|
38
|
+
# @param options [ClaudeCodeOptions] configuration options
|
39
|
+
# @param cli_path [String, Pathname, nil] path to Claude CLI binary
|
40
|
+
def initialize(prompt:, options:, cli_path: nil) # rubocop:disable Lint/MissingSuper
|
41
|
+
@prompt = prompt
|
42
|
+
@options = options
|
43
|
+
@cli_path = cli_path ? cli_path.to_s : find_cli
|
44
|
+
@cwd = options.cwd&.to_s
|
45
|
+
@pid = nil
|
46
|
+
@stdin = nil
|
47
|
+
@stdout = nil
|
48
|
+
@stderr = nil
|
49
|
+
@wait_thread = nil
|
50
|
+
@logger = Logger.new($stderr)
|
51
|
+
@logger.level = ENV["CLAUDE_SDK_DEBUG"] ? Logger::DEBUG : Logger::INFO
|
52
|
+
end
|
53
|
+
|
54
|
+
# Connect to Claude Code CLI
|
55
|
+
#
|
56
|
+
# @return [void]
|
57
|
+
# @raise [CLIConnectionError] if unable to start the CLI
|
58
|
+
def connect
|
59
|
+
return if @pid
|
60
|
+
|
61
|
+
cmd = build_command
|
62
|
+
logger.debug("Executing command: #{cmd.join(" ")}")
|
63
|
+
|
64
|
+
begin
|
65
|
+
env = ENV.to_h.merge("CLAUDE_CODE_ENTRYPOINT" => "sdk-ruby")
|
66
|
+
|
67
|
+
# Build spawn options
|
68
|
+
spawn_options = {}
|
69
|
+
spawn_options[:chdir] = @cwd if @cwd
|
70
|
+
|
71
|
+
# Use Open3 to spawn process with pipes
|
72
|
+
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(env, *cmd, **spawn_options)
|
73
|
+
@pid = @wait_thread.pid
|
74
|
+
|
75
|
+
# Close stdin since we don't need it
|
76
|
+
@stdin.close
|
77
|
+
rescue Errno::ENOENT
|
78
|
+
# Check if error is from working directory or CLI
|
79
|
+
raise CLIConnectionError, "Working directory does not exist: #{@cwd}" if @cwd && !File.directory?(@cwd)
|
80
|
+
|
81
|
+
raise CLINotFoundError.new(
|
82
|
+
message: "Claude Code not found",
|
83
|
+
cli_path: @cli_path,
|
84
|
+
)
|
85
|
+
rescue StandardError => e
|
86
|
+
raise CLIConnectionError, "Failed to start Claude Code: #{e.message}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Disconnect from Claude Code CLI
|
91
|
+
#
|
92
|
+
# @return [void]
|
93
|
+
def disconnect
|
94
|
+
return unless @pid
|
95
|
+
|
96
|
+
begin
|
97
|
+
# Try to terminate gracefully
|
98
|
+
Process.kill("INT", @pid)
|
99
|
+
|
100
|
+
# Wait for process to exit with timeout using the wait thread
|
101
|
+
if @wait_thread&.alive?
|
102
|
+
begin
|
103
|
+
Timeout.timeout(5) do
|
104
|
+
@wait_thread.join
|
105
|
+
end
|
106
|
+
rescue Timeout::Error
|
107
|
+
# Force kill if it doesn't exit gracefully
|
108
|
+
begin
|
109
|
+
Process.kill("KILL", @pid)
|
110
|
+
rescue StandardError
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
begin
|
114
|
+
@wait_thread.join
|
115
|
+
rescue StandardError
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
121
|
+
# Process already gone
|
122
|
+
ensure
|
123
|
+
@stdin&.close
|
124
|
+
@stdout&.close
|
125
|
+
@stderr&.close
|
126
|
+
@stdin = nil
|
127
|
+
@stdout = nil
|
128
|
+
@stderr = nil
|
129
|
+
@pid = nil
|
130
|
+
@wait_thread = nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Send request (not used for CLI transport - args passed via command line)
|
135
|
+
#
|
136
|
+
# @param messages [Array<Hash>] messages (ignored)
|
137
|
+
# @param options [Hash] options (ignored)
|
138
|
+
# @return [void]
|
139
|
+
def send_request(messages, options)
|
140
|
+
# Not used - all arguments passed via command line
|
141
|
+
end
|
142
|
+
|
143
|
+
# Receive messages from CLI output
|
144
|
+
#
|
145
|
+
# @yield [Hash] parsed JSON message from CLI
|
146
|
+
# @return [Enumerator<Hash>] if no block given
|
147
|
+
# @raise [ProcessError] if CLI exits with non-zero status
|
148
|
+
# @raise [CLIJSONDecodeError] if unable to parse JSON
|
149
|
+
def receive_messages
|
150
|
+
return enum_for(:receive_messages) unless block_given?
|
151
|
+
|
152
|
+
raise CLIConnectionError, "Not connected" unless @pid
|
153
|
+
|
154
|
+
json_buffer = ""
|
155
|
+
stderr_lines = []
|
156
|
+
|
157
|
+
# Read stdout directly (not in async task to avoid conflicts)
|
158
|
+
logger.debug("Starting to read stdout...")
|
159
|
+
if @stdout
|
160
|
+
begin
|
161
|
+
@stdout.each_line do |line|
|
162
|
+
logger.debug("Read line: #{line.inspect}")
|
163
|
+
line_str = line.strip
|
164
|
+
next if line_str.empty?
|
165
|
+
|
166
|
+
# Split by newlines in case multiple JSON objects are on one line
|
167
|
+
json_lines = line_str.split("\n")
|
168
|
+
|
169
|
+
json_lines.each do |json_line|
|
170
|
+
json_line.strip!
|
171
|
+
next if json_line.empty?
|
172
|
+
|
173
|
+
# Keep accumulating partial JSON until we can parse it
|
174
|
+
json_buffer += json_line
|
175
|
+
|
176
|
+
if json_buffer.length > MAX_BUFFER_SIZE
|
177
|
+
json_buffer = ""
|
178
|
+
raise CLIJSONDecodeError.new(
|
179
|
+
line: "Buffer exceeded #{MAX_BUFFER_SIZE} bytes",
|
180
|
+
original_error: StandardError.new("Buffer overflow"),
|
181
|
+
)
|
182
|
+
end
|
183
|
+
|
184
|
+
begin
|
185
|
+
data = JSON.parse(json_buffer)
|
186
|
+
json_buffer = ""
|
187
|
+
logger.debug("Parsed JSON: #{data["type"]}")
|
188
|
+
yield data
|
189
|
+
rescue JSON::ParserError
|
190
|
+
# Keep accumulating
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
rescue IOError, Errno::EBADF
|
195
|
+
# Stream was closed, that's ok
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Collect stderr
|
200
|
+
if @stderr
|
201
|
+
begin
|
202
|
+
@stderr.each_line do |line|
|
203
|
+
stderr_lines << line.strip
|
204
|
+
end
|
205
|
+
rescue IOError, Errno::EBADF
|
206
|
+
# Stream was closed, that's ok
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Wait for process completion
|
211
|
+
exit_status = nil
|
212
|
+
if @wait_thread
|
213
|
+
logger.debug("Waiting for process to complete...")
|
214
|
+
@wait_thread.join
|
215
|
+
exit_status = @wait_thread.value.exitstatus
|
216
|
+
logger.debug("Process completed with status: #{exit_status}")
|
217
|
+
end
|
218
|
+
|
219
|
+
stderr_output = stderr_lines.join("\n")
|
220
|
+
|
221
|
+
# Check exit status
|
222
|
+
if exit_status && exit_status != 0
|
223
|
+
raise ProcessError.new(
|
224
|
+
"Command failed with exit code #{exit_status}",
|
225
|
+
exit_code: exit_status,
|
226
|
+
stderr: stderr_output,
|
227
|
+
)
|
228
|
+
elsif !stderr_output.empty?
|
229
|
+
logger.debug("Process stderr: #{stderr_output}")
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Check if subprocess is running
|
234
|
+
#
|
235
|
+
# @return [Boolean] true if connected and running
|
236
|
+
def connected?
|
237
|
+
return false unless @pid
|
238
|
+
|
239
|
+
# Check if process is still running
|
240
|
+
Process.kill(0, @pid)
|
241
|
+
true
|
242
|
+
rescue Errno::ESRCH, Errno::EPERM
|
243
|
+
false
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
# Find Claude Code CLI binary
|
249
|
+
#
|
250
|
+
# @return [String] path to CLI binary
|
251
|
+
# @raise [CLINotFoundError] if CLI not found
|
252
|
+
def find_cli
|
253
|
+
# Check PATH first
|
254
|
+
if (cli_path = which("claude"))
|
255
|
+
return cli_path
|
256
|
+
end
|
257
|
+
|
258
|
+
# Check common locations
|
259
|
+
locations = [
|
260
|
+
Pathname.new(File.expand_path("~/.npm-global/bin/claude")),
|
261
|
+
Pathname.new("/usr/local/bin/claude"),
|
262
|
+
Pathname.new(File.expand_path("~/.local/bin/claude")),
|
263
|
+
Pathname.new(File.expand_path("~/node_modules/.bin/claude")),
|
264
|
+
Pathname.new(File.expand_path("~/.yarn/bin/claude")),
|
265
|
+
]
|
266
|
+
|
267
|
+
locations.each do |path|
|
268
|
+
return path.to_s if path.exist? && path.file?
|
269
|
+
end
|
270
|
+
|
271
|
+
# Check if Node.js is installed
|
272
|
+
node_installed = !which("node").nil?
|
273
|
+
|
274
|
+
unless node_installed
|
275
|
+
error_msg = "Claude Code requires Node.js, which is not installed.\n\n"
|
276
|
+
error_msg += "Install Node.js from: https://nodejs.org/\n"
|
277
|
+
error_msg += "\nAfter installing Node.js, install Claude Code:\n"
|
278
|
+
error_msg += " npm install -g @anthropic-ai/claude-code"
|
279
|
+
raise CLINotFoundError.new(message: error_msg, cli_path: @cli_path)
|
280
|
+
end
|
281
|
+
|
282
|
+
# Node is installed but Claude Code isn't
|
283
|
+
error_msg = <<~MSG
|
284
|
+
Claude Code not found. Install with:
|
285
|
+
npm install -g @anthropic-ai/claude-code
|
286
|
+
|
287
|
+
If already installed locally, try:
|
288
|
+
export PATH="$HOME/node_modules/.bin:$PATH"
|
289
|
+
|
290
|
+
Or specify the path when creating transport:
|
291
|
+
SubprocessCLI.new(..., cli_path: '/path/to/claude')
|
292
|
+
MSG
|
293
|
+
raise CLINotFoundError.new(message: error_msg, cli_path: @cli_path)
|
294
|
+
end
|
295
|
+
|
296
|
+
# Cross-platform which command
|
297
|
+
#
|
298
|
+
# @param cmd [String] command to find
|
299
|
+
# @return [String, nil] path to command or nil
|
300
|
+
def which(cmd)
|
301
|
+
exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
|
302
|
+
ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
|
303
|
+
exts.each do |ext|
|
304
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
305
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
nil
|
309
|
+
end
|
310
|
+
|
311
|
+
# Build CLI command with arguments
|
312
|
+
#
|
313
|
+
# @return [Array<String>] command array
|
314
|
+
def build_command
|
315
|
+
cmd = [@cli_path, "--output-format", "stream-json", "--verbose"]
|
316
|
+
|
317
|
+
cmd.push("--system-prompt", @options.system_prompt) if @options.system_prompt
|
318
|
+
|
319
|
+
cmd.push("--append-system-prompt", @options.append_system_prompt) if @options.append_system_prompt
|
320
|
+
|
321
|
+
if @options.allowed_tools && !@options.allowed_tools.empty?
|
322
|
+
cmd.push("--allowedTools", @options.allowed_tools.join(","))
|
323
|
+
end
|
324
|
+
|
325
|
+
cmd.push("--max-turns", @options.max_turns.to_s) if @options.max_turns
|
326
|
+
|
327
|
+
if @options.disallowed_tools && !@options.disallowed_tools.empty?
|
328
|
+
cmd.push("--disallowedTools", @options.disallowed_tools.join(","))
|
329
|
+
end
|
330
|
+
|
331
|
+
cmd.push("--model", @options.model) if @options.model
|
332
|
+
|
333
|
+
if @options.permission_prompt_tool_name
|
334
|
+
cmd.push("--permission-prompt-tool", @options.permission_prompt_tool_name)
|
335
|
+
end
|
336
|
+
|
337
|
+
cmd.push("--permission-mode", @options.permission_mode.to_s) if @options.permission_mode
|
338
|
+
|
339
|
+
cmd.push("--continue") if @options.continue_conversation
|
340
|
+
|
341
|
+
cmd.push("--resume", @options.resume) if @options.resume
|
342
|
+
|
343
|
+
if @options.mcp_servers && !@options.mcp_servers.empty?
|
344
|
+
mcp_config = { "mcpServers" => serialize_mcp_servers }
|
345
|
+
cmd.push("--mcp-config", JSON.generate(mcp_config))
|
346
|
+
end
|
347
|
+
|
348
|
+
cmd.push("--print", @prompt)
|
349
|
+
cmd
|
350
|
+
end
|
351
|
+
|
352
|
+
# Serialize MCP servers for CLI
|
353
|
+
#
|
354
|
+
# @return [Hash] serialized MCP servers
|
355
|
+
def serialize_mcp_servers
|
356
|
+
@options.mcp_servers.transform_values do |server|
|
357
|
+
server.respond_to?(:to_h) ? server.to_h : server
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClaudeSDK
|
4
|
+
module Internal
|
5
|
+
# Abstract transport interface for Claude communication
|
6
|
+
#
|
7
|
+
# @abstract Subclass and override {#connect}, {#disconnect}, {#send_request},
|
8
|
+
# {#receive_messages}, and {#connected?} to implement a transport
|
9
|
+
class Transport
|
10
|
+
# Initialize connection
|
11
|
+
#
|
12
|
+
# @abstract
|
13
|
+
# @return [void]
|
14
|
+
def connect
|
15
|
+
raise NotImplementedError, "#{self.class}#connect not implemented"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Close connection
|
19
|
+
#
|
20
|
+
# @abstract
|
21
|
+
# @return [void]
|
22
|
+
def disconnect
|
23
|
+
raise NotImplementedError, "#{self.class}#disconnect not implemented"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Send request to Claude
|
27
|
+
#
|
28
|
+
# @abstract
|
29
|
+
# @param messages [Array<Hash>] the messages to send
|
30
|
+
# @param options [Hash] additional options
|
31
|
+
# @return [void]
|
32
|
+
def send_request(messages, options)
|
33
|
+
raise NotImplementedError, "#{self.class}#send_request not implemented"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Receive messages from Claude
|
37
|
+
#
|
38
|
+
# @abstract
|
39
|
+
# @return [Enumerator<Hash>] yields message hashes
|
40
|
+
def receive_messages
|
41
|
+
raise NotImplementedError, "#{self.class}#receive_messages not implemented"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Check if transport is connected
|
45
|
+
#
|
46
|
+
# @abstract
|
47
|
+
# @return [Boolean] true if connected
|
48
|
+
def connected?
|
49
|
+
raise NotImplementedError, "#{self.class}#connected? not implemented"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|