claude_agent 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/.claude/commands/spec/complete.md +105 -0
- data/.claude/commands/spec/update.md +95 -0
- data/.claude/rules/conventions.md +622 -0
- data/.claude/rules/git.md +86 -0
- data/.claude/rules/pull-requests.md +31 -0
- data/.claude/rules/releases.md +177 -0
- data/.claude/rules/testing.md +267 -0
- data/.claude/settings.json +49 -0
- data/CHANGELOG.md +13 -0
- data/CLAUDE.md +94 -0
- data/LICENSE.txt +21 -0
- data/README.md +679 -0
- data/Rakefile +63 -0
- data/SPEC.md +558 -0
- data/lib/claude_agent/abort_controller.rb +113 -0
- data/lib/claude_agent/client.rb +298 -0
- data/lib/claude_agent/content_blocks.rb +163 -0
- data/lib/claude_agent/control_protocol.rb +717 -0
- data/lib/claude_agent/errors.rb +103 -0
- data/lib/claude_agent/hooks.rb +228 -0
- data/lib/claude_agent/mcp/server.rb +166 -0
- data/lib/claude_agent/mcp/tool.rb +137 -0
- data/lib/claude_agent/message_parser.rb +262 -0
- data/lib/claude_agent/messages.rb +421 -0
- data/lib/claude_agent/options.rb +264 -0
- data/lib/claude_agent/permissions.rb +164 -0
- data/lib/claude_agent/query.rb +90 -0
- data/lib/claude_agent/sandbox_settings.rb +139 -0
- data/lib/claude_agent/spawn.rb +235 -0
- data/lib/claude_agent/transport/base.rb +61 -0
- data/lib/claude_agent/transport/subprocess.rb +432 -0
- data/lib/claude_agent/types.rb +193 -0
- data/lib/claude_agent/version.rb +5 -0
- data/lib/claude_agent.rb +28 -0
- data/sig/claude_agent.rbs +912 -0
- data/sig/manifest.yaml +5 -0
- metadata +97 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "json"
|
|
5
|
+
require "timeout"
|
|
6
|
+
|
|
7
|
+
module ClaudeAgent
|
|
8
|
+
module Transport
|
|
9
|
+
# Subprocess transport that communicates with Claude Code CLI
|
|
10
|
+
#
|
|
11
|
+
# This transport spawns the Claude Code CLI as a subprocess and
|
|
12
|
+
# communicates via stdin/stdout using JSON Lines protocol.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# transport = ClaudeAgent::Transport::Subprocess.new(options)
|
|
16
|
+
# transport.connect
|
|
17
|
+
# transport.write('{"type":"user","message":{"role":"user","content":"Hello"}}')
|
|
18
|
+
# transport.read_messages { |msg| puts msg }
|
|
19
|
+
# transport.close
|
|
20
|
+
#
|
|
21
|
+
class Subprocess < Base
|
|
22
|
+
MINIMUM_CLI_VERSION = "2.0.0"
|
|
23
|
+
DEFAULT_BUFFER_SIZE = 1_048_576 # 1MB
|
|
24
|
+
VERSION_CHECK_TIMEOUT = 2
|
|
25
|
+
|
|
26
|
+
attr_reader :options, :cli_path, :process
|
|
27
|
+
|
|
28
|
+
# @param options [ClaudeAgent::Options, nil] Configuration options
|
|
29
|
+
# @param cli_path [String, nil] Override path to Claude CLI
|
|
30
|
+
def initialize(options: nil, cli_path: nil)
|
|
31
|
+
super()
|
|
32
|
+
@options = options || Options.new
|
|
33
|
+
@cli_path = cli_path || @options.cli_path || find_cli_path
|
|
34
|
+
@stdin = nil
|
|
35
|
+
@stdout = nil
|
|
36
|
+
@stderr = nil
|
|
37
|
+
@wait_thread = nil
|
|
38
|
+
@process = nil # Custom spawned process (SpawnedProcess)
|
|
39
|
+
@connected = false
|
|
40
|
+
@killed = false
|
|
41
|
+
@buffer = +""
|
|
42
|
+
@max_buffer_size = @options.max_buffer_size || DEFAULT_BUFFER_SIZE
|
|
43
|
+
@mutex = Mutex.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Start the CLI subprocess
|
|
47
|
+
# @param streaming [Boolean] Whether to use streaming mode
|
|
48
|
+
# @param prompt [String, nil] Initial prompt for non-streaming mode
|
|
49
|
+
# @return [void]
|
|
50
|
+
def connect(streaming: true, prompt: nil)
|
|
51
|
+
raise CLIConnectionError, "Already connected" if @connected
|
|
52
|
+
|
|
53
|
+
check_cli_version! unless skip_version_check?
|
|
54
|
+
|
|
55
|
+
cmd = build_command(streaming: streaming, prompt: prompt)
|
|
56
|
+
env = @options.to_env
|
|
57
|
+
|
|
58
|
+
# Build spawn options for custom spawn function support
|
|
59
|
+
spawn_options = SpawnOptions.new(
|
|
60
|
+
command: @cli_path,
|
|
61
|
+
args: cmd.drop(1), # Remove command itself since it's in :command
|
|
62
|
+
cwd: working_directory,
|
|
63
|
+
env: env,
|
|
64
|
+
abort_signal: @options.abort_signal
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Use custom spawn function if provided, otherwise use default
|
|
68
|
+
spawn_func = @options.spawn_claude_code_process || DEFAULT_SPAWN
|
|
69
|
+
|
|
70
|
+
if spawn_func
|
|
71
|
+
@process = spawn_func.call(spawn_options)
|
|
72
|
+
# Extract streams from process for compatibility
|
|
73
|
+
if @process.respond_to?(:stdin)
|
|
74
|
+
@stdin = @process.stdin
|
|
75
|
+
@stdout = @process.stdout
|
|
76
|
+
@stderr = @process.stderr
|
|
77
|
+
@wait_thread = @process.wait_thread if @process.respond_to?(:wait_thread)
|
|
78
|
+
else
|
|
79
|
+
# Custom process - use wrapper methods
|
|
80
|
+
@stdin = ProcessStdinWrapper.new(@process)
|
|
81
|
+
@stdout = ProcessStdoutWrapper.new(@process)
|
|
82
|
+
@stderr = nil # Custom processes handle stderr internally
|
|
83
|
+
@wait_thread = nil
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
# Fallback to direct Open3 spawn
|
|
87
|
+
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(env, *cmd, chdir: working_directory)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@connected = true
|
|
91
|
+
|
|
92
|
+
# Always start stderr reader to prevent pipe buffer from filling up
|
|
93
|
+
start_stderr_reader if @stderr
|
|
94
|
+
|
|
95
|
+
# For non-streaming mode with --print, close stdin immediately
|
|
96
|
+
# The prompt is passed as command-line argument, not via stdin
|
|
97
|
+
unless streaming
|
|
98
|
+
if @process&.respond_to?(:close_stdin)
|
|
99
|
+
@process.close_stdin
|
|
100
|
+
elsif @stdin && !@stdin.closed?
|
|
101
|
+
@stdin.close
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
rescue Errno::ENOENT => e
|
|
105
|
+
raise CLINotFoundError, "Claude CLI not found at '#{@cli_path}': #{e.message}"
|
|
106
|
+
rescue => e
|
|
107
|
+
close
|
|
108
|
+
raise CLIConnectionError, "Failed to start CLI: #{e.message}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Write a JSON message to stdin
|
|
112
|
+
# @param data [String] JSON string to write
|
|
113
|
+
# @return [void]
|
|
114
|
+
def write(data)
|
|
115
|
+
raise CLIConnectionError, "Not connected" unless @connected
|
|
116
|
+
raise CLIConnectionError, "stdin closed" unless @stdin && !@stdin.closed?
|
|
117
|
+
|
|
118
|
+
@mutex.synchronize do
|
|
119
|
+
@stdin.write(data)
|
|
120
|
+
@stdin.write("\n") unless data.end_with?("\n")
|
|
121
|
+
@stdin.flush
|
|
122
|
+
end
|
|
123
|
+
rescue Errno::EPIPE
|
|
124
|
+
raise CLIConnectionError, "Broken pipe - CLI process may have terminated"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Read and parse JSON messages from stdout
|
|
128
|
+
# @yield [Hash] Parsed JSON messages
|
|
129
|
+
# @return [Enumerator] If no block given
|
|
130
|
+
def read_messages
|
|
131
|
+
return enum_for(:read_messages) unless block_given?
|
|
132
|
+
|
|
133
|
+
raise CLIConnectionError, "Not connected" unless @connected
|
|
134
|
+
raise CLIConnectionError, "stdout closed" unless @stdout && !@stdout.closed?
|
|
135
|
+
|
|
136
|
+
@stdout.each_line do |line|
|
|
137
|
+
line = line.strip
|
|
138
|
+
next if line.empty?
|
|
139
|
+
|
|
140
|
+
begin
|
|
141
|
+
message = JSON.parse(line)
|
|
142
|
+
yield message
|
|
143
|
+
rescue JSON::ParserError
|
|
144
|
+
# Buffer partial JSON (in case of split lines)
|
|
145
|
+
@buffer << line
|
|
146
|
+
if @buffer.bytesize > @max_buffer_size
|
|
147
|
+
raise JSONDecodeError.new("Buffer overflow while parsing JSON", raw_content: @buffer[0..500])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Try to parse buffer
|
|
151
|
+
begin
|
|
152
|
+
message = JSON.parse(@buffer)
|
|
153
|
+
@buffer = +""
|
|
154
|
+
yield message
|
|
155
|
+
rescue JSON::ParserError
|
|
156
|
+
# Keep buffering
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Close stdin to signal end of input
|
|
163
|
+
# @return [void]
|
|
164
|
+
def end_input
|
|
165
|
+
return unless @stdin && !@stdin.closed?
|
|
166
|
+
|
|
167
|
+
@mutex.synchronize do
|
|
168
|
+
@stdin.close
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Close all streams and wait for process to exit
|
|
173
|
+
# @return [Integer, nil] Exit status
|
|
174
|
+
def close
|
|
175
|
+
# Use custom process close if available
|
|
176
|
+
if @process&.respond_to?(:close)
|
|
177
|
+
@process.close
|
|
178
|
+
exit_status = @process.exit_status if @process.respond_to?(:exit_status)
|
|
179
|
+
@connected = false
|
|
180
|
+
@process = nil
|
|
181
|
+
@stdin = @stdout = @stderr = @wait_thread = nil
|
|
182
|
+
return exit_status
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
@mutex.synchronize do
|
|
186
|
+
@stdin&.close unless @stdin&.closed?
|
|
187
|
+
@stdout&.close unless @stdout&.closed?
|
|
188
|
+
@stderr&.close unless @stderr&.closed?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
exit_status = @wait_thread&.value&.exitstatus
|
|
192
|
+
@connected = false
|
|
193
|
+
@stdin = @stdout = @stderr = @wait_thread = nil
|
|
194
|
+
|
|
195
|
+
exit_status
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Check if transport is ready
|
|
199
|
+
# @return [Boolean]
|
|
200
|
+
def ready?
|
|
201
|
+
@connected && @stdin && !@stdin.closed? && @stdout && !@stdout.closed?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Check if transport is connected
|
|
205
|
+
# @return [Boolean]
|
|
206
|
+
def connected?
|
|
207
|
+
@connected
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Get the exit status of the CLI process
|
|
211
|
+
# @return [Integer, nil]
|
|
212
|
+
def exit_status
|
|
213
|
+
return @process.exit_status if @process&.respond_to?(:exit_status)
|
|
214
|
+
|
|
215
|
+
@wait_thread&.value&.exitstatus
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Check if the CLI process is still running
|
|
219
|
+
# @return [Boolean]
|
|
220
|
+
def running?
|
|
221
|
+
return @process.running? if @process&.respond_to?(:running?)
|
|
222
|
+
|
|
223
|
+
@wait_thread&.alive? || false
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Check if the CLI process was killed externally
|
|
227
|
+
# @return [Boolean]
|
|
228
|
+
def killed?
|
|
229
|
+
@killed || (@wait_thread && !@wait_thread.alive? && !@connected)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Terminate the CLI process gracefully (SIGTERM)
|
|
233
|
+
# @param timeout [Numeric] Seconds to wait before force kill
|
|
234
|
+
# @return [void]
|
|
235
|
+
def terminate(timeout: 5)
|
|
236
|
+
# Use custom process terminate if available
|
|
237
|
+
if @process&.respond_to?(:terminate)
|
|
238
|
+
@process.terminate(timeout: timeout)
|
|
239
|
+
return
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
return unless @wait_thread&.alive?
|
|
243
|
+
|
|
244
|
+
pid = nil
|
|
245
|
+
@mutex.synchronize do
|
|
246
|
+
pid = @wait_thread&.pid
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
return unless pid
|
|
250
|
+
|
|
251
|
+
begin
|
|
252
|
+
Process.kill("TERM", pid)
|
|
253
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
254
|
+
# Process already dead or no permission
|
|
255
|
+
return
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Wait for graceful shutdown
|
|
259
|
+
unless @wait_thread.join(timeout)
|
|
260
|
+
kill
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Force kill the CLI process (SIGKILL)
|
|
265
|
+
# @return [void]
|
|
266
|
+
def kill
|
|
267
|
+
# Use custom process kill if available
|
|
268
|
+
if @process&.respond_to?(:kill)
|
|
269
|
+
@mutex.synchronize { @killed = true }
|
|
270
|
+
@process.kill
|
|
271
|
+
return
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
return unless @wait_thread&.alive?
|
|
275
|
+
|
|
276
|
+
pid = nil
|
|
277
|
+
@mutex.synchronize do
|
|
278
|
+
pid = @wait_thread&.pid
|
|
279
|
+
@killed = true
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
return unless pid
|
|
283
|
+
|
|
284
|
+
begin
|
|
285
|
+
Process.kill("KILL", pid)
|
|
286
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
287
|
+
# Process already dead or no permission
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
private
|
|
292
|
+
|
|
293
|
+
def find_cli_path
|
|
294
|
+
# Check common locations
|
|
295
|
+
paths = [
|
|
296
|
+
`which claude 2>/dev/null`.strip,
|
|
297
|
+
"/usr/local/bin/claude",
|
|
298
|
+
"/opt/homebrew/bin/claude",
|
|
299
|
+
File.expand_path("~/.local/bin/claude")
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
paths.find { |p| !p.empty? && File.executable?(p) } || "claude"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def check_cli_version!
|
|
306
|
+
version_output = `#{@cli_path} -v 2>&1`.strip
|
|
307
|
+
# Parse version like "claude 2.1.0" or just "2.1.0"
|
|
308
|
+
version_match = version_output.match(/(\d+\.\d+\.\d+)/)
|
|
309
|
+
|
|
310
|
+
unless version_match
|
|
311
|
+
raise CLIVersionError.new(nil)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
found_version = version_match[1]
|
|
315
|
+
unless version_satisfies?(found_version, MINIMUM_CLI_VERSION)
|
|
316
|
+
raise CLIVersionError.new(found_version)
|
|
317
|
+
end
|
|
318
|
+
rescue Errno::ENOENT
|
|
319
|
+
raise CLINotFoundError
|
|
320
|
+
rescue Errno::ETIMEDOUT, Timeout::Error
|
|
321
|
+
# Skip version check on timeout
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def version_satisfies?(found, minimum)
|
|
325
|
+
found_parts = found.split(".").map(&:to_i)
|
|
326
|
+
minimum_parts = minimum.split(".").map(&:to_i)
|
|
327
|
+
|
|
328
|
+
found_parts.zip(minimum_parts).each do |f, m|
|
|
329
|
+
f ||= 0
|
|
330
|
+
m ||= 0
|
|
331
|
+
return true if f > m
|
|
332
|
+
return false if f < m
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
true
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def skip_version_check?
|
|
339
|
+
ENV["CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK"] == "true"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def build_command(streaming:, prompt: nil)
|
|
343
|
+
cmd = [ @cli_path ]
|
|
344
|
+
|
|
345
|
+
# Add options-based arguments
|
|
346
|
+
cmd.concat(@options.to_cli_args)
|
|
347
|
+
|
|
348
|
+
# Output is always stream-json
|
|
349
|
+
cmd.push("--output-format", "stream-json")
|
|
350
|
+
|
|
351
|
+
# Input format only for streaming mode
|
|
352
|
+
cmd.push("--input-format", "stream-json") if streaming
|
|
353
|
+
|
|
354
|
+
# Always add verbose for better debugging (must be before -- separator)
|
|
355
|
+
cmd.push("--verbose")
|
|
356
|
+
|
|
357
|
+
# For non-streaming mode, add prompt as positional argument after --
|
|
358
|
+
if !streaming && prompt
|
|
359
|
+
cmd.push("--print")
|
|
360
|
+
cmd.push("--")
|
|
361
|
+
cmd.push(prompt)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
cmd
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def working_directory
|
|
368
|
+
dir = @options.cwd&.to_s
|
|
369
|
+
(dir && Dir.exist?(dir)) ? dir : Dir.pwd
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def start_stderr_reader
|
|
373
|
+
Thread.new do
|
|
374
|
+
@stderr.each_line do |line|
|
|
375
|
+
# Call callback if provided, otherwise just drain
|
|
376
|
+
@options.stderr_callback&.call(line.chomp)
|
|
377
|
+
rescue
|
|
378
|
+
# Ignore callback errors
|
|
379
|
+
end
|
|
380
|
+
rescue IOError
|
|
381
|
+
# Stream closed, exit thread
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Wrapper for custom process stdin to match IO interface
|
|
387
|
+
# @api private
|
|
388
|
+
class ProcessStdinWrapper
|
|
389
|
+
def initialize(process)
|
|
390
|
+
@process = process
|
|
391
|
+
@closed = false
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def write(data)
|
|
395
|
+
@process.write(data)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def flush
|
|
399
|
+
# Custom processes handle their own flushing
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def close
|
|
403
|
+
@process.close_stdin
|
|
404
|
+
@closed = true
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def closed?
|
|
408
|
+
@closed
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Wrapper for custom process stdout to match IO interface
|
|
413
|
+
# @api private
|
|
414
|
+
class ProcessStdoutWrapper
|
|
415
|
+
def initialize(process)
|
|
416
|
+
@process = process
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def each_line(&block)
|
|
420
|
+
@process.read_stdout(&block)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def close
|
|
424
|
+
# Custom processes handle their own closing
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def closed?
|
|
428
|
+
!@process.running?
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
# Assistant message error types (TypeScript SDK parity)
|
|
5
|
+
# Used to categorize errors returned by the assistant
|
|
6
|
+
ASSISTANT_MESSAGE_ERROR_TYPES = %w[
|
|
7
|
+
authentication_failed
|
|
8
|
+
billing_error
|
|
9
|
+
rate_limit
|
|
10
|
+
invalid_request
|
|
11
|
+
server_error
|
|
12
|
+
unknown
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
# API key source types (TypeScript SDK parity)
|
|
16
|
+
# Indicates where the API key was sourced from
|
|
17
|
+
API_KEY_SOURCES = %w[user project org temporary].freeze
|
|
18
|
+
|
|
19
|
+
# Tools preset configuration (TypeScript SDK parity)
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# preset = ToolsPreset.new(preset: "claude_code")
|
|
23
|
+
# options = ClaudeAgent::Options.new(tools: preset)
|
|
24
|
+
#
|
|
25
|
+
ToolsPreset = Data.define(:type, :preset) do
|
|
26
|
+
def initialize(type: "preset", preset: "claude_code")
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
{ type: type, preset: preset }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
# Return type for supported_commands() (TypeScript SDK parity)
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# cmd = SlashCommand.new(name: "commit", description: "Create a commit", argument_hint: "[message]")
|
|
38
|
+
# cmd.name # => "commit"
|
|
39
|
+
# cmd.description # => "Create a commit"
|
|
40
|
+
#
|
|
41
|
+
SlashCommand = Data.define(:name, :description, :argument_hint) do
|
|
42
|
+
def initialize(name:, description: nil, argument_hint: nil)
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Return type for supported_models() (TypeScript SDK parity)
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# model = ModelInfo.new(value: "claude-3-opus", display_name: "Claude 3 Opus", description: "Most capable")
|
|
51
|
+
# model.value # => "claude-3-opus"
|
|
52
|
+
# model.display_name # => "Claude 3 Opus"
|
|
53
|
+
#
|
|
54
|
+
ModelInfo = Data.define(:value, :display_name, :description) do
|
|
55
|
+
def initialize(value:, display_name: nil, description: nil)
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Return type for mcp_server_status() (TypeScript SDK parity)
|
|
61
|
+
# Status values: "connected", "failed", "needs-auth", "pending"
|
|
62
|
+
#
|
|
63
|
+
# @example
|
|
64
|
+
# status = McpServerStatus.new(name: "filesystem", status: "connected", server_info: {name: "fs", version: "1.0"})
|
|
65
|
+
#
|
|
66
|
+
McpServerStatus = Data.define(:name, :status, :server_info) do
|
|
67
|
+
def initialize(name:, status:, server_info: nil)
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Return type for account_info() (TypeScript SDK parity)
|
|
73
|
+
#
|
|
74
|
+
# @example
|
|
75
|
+
# info = AccountInfo.new(email: "user@example.com", organization: "Acme Corp")
|
|
76
|
+
#
|
|
77
|
+
AccountInfo = Data.define(:email, :organization, :subscription_type, :token_source, :api_key_source) do
|
|
78
|
+
def initialize(email: nil, organization: nil, subscription_type: nil, token_source: nil, api_key_source: nil)
|
|
79
|
+
super
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Per-model usage statistics returned in result messages (TypeScript SDK parity)
|
|
84
|
+
#
|
|
85
|
+
# @example
|
|
86
|
+
# usage = ModelUsage.new(input_tokens: 100, output_tokens: 50, cost_usd: 0.01)
|
|
87
|
+
#
|
|
88
|
+
ModelUsage = Data.define(
|
|
89
|
+
:input_tokens,
|
|
90
|
+
:output_tokens,
|
|
91
|
+
:cache_read_input_tokens,
|
|
92
|
+
:cache_creation_input_tokens,
|
|
93
|
+
:web_search_requests,
|
|
94
|
+
:cost_usd,
|
|
95
|
+
:context_window
|
|
96
|
+
) do
|
|
97
|
+
def initialize(
|
|
98
|
+
input_tokens: 0,
|
|
99
|
+
output_tokens: 0,
|
|
100
|
+
cache_read_input_tokens: 0,
|
|
101
|
+
cache_creation_input_tokens: 0,
|
|
102
|
+
web_search_requests: 0,
|
|
103
|
+
cost_usd: 0.0,
|
|
104
|
+
context_window: nil
|
|
105
|
+
)
|
|
106
|
+
super
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Permission denial information in result messages (TypeScript SDK parity)
|
|
111
|
+
#
|
|
112
|
+
SDKPermissionDenial = Data.define(:tool_name, :tool_use_id, :tool_input) do
|
|
113
|
+
def initialize(tool_name:, tool_use_id:, tool_input:)
|
|
114
|
+
super
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Result of set_mcp_servers() control method (TypeScript SDK parity)
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# result = McpSetServersResult.new(
|
|
122
|
+
# added: ["server1"],
|
|
123
|
+
# removed: ["old-server"],
|
|
124
|
+
# errors: {"server2" => "Connection failed"}
|
|
125
|
+
# )
|
|
126
|
+
#
|
|
127
|
+
McpSetServersResult = Data.define(:added, :removed, :errors) do
|
|
128
|
+
def initialize(added: [], removed: [], errors: {})
|
|
129
|
+
super
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Result of rewind_files() control method (TypeScript SDK parity)
|
|
134
|
+
#
|
|
135
|
+
# @example
|
|
136
|
+
# result = RewindFilesResult.new(
|
|
137
|
+
# can_rewind: true,
|
|
138
|
+
# files_changed: ["src/foo.rb", "src/bar.rb"],
|
|
139
|
+
# insertions: 10,
|
|
140
|
+
# deletions: 5
|
|
141
|
+
# )
|
|
142
|
+
#
|
|
143
|
+
RewindFilesResult = Data.define(:can_rewind, :error, :files_changed, :insertions, :deletions) do
|
|
144
|
+
def initialize(can_rewind:, error: nil, files_changed: nil, insertions: nil, deletions: nil)
|
|
145
|
+
super
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Agent definition for custom subagents (TypeScript SDK parity)
|
|
150
|
+
#
|
|
151
|
+
# @example
|
|
152
|
+
# agent = AgentDefinition.new(
|
|
153
|
+
# description: "Runs tests and reports results",
|
|
154
|
+
# prompt: "You are a test runner...",
|
|
155
|
+
# tools: ["Read", "Grep", "Glob", "Bash"],
|
|
156
|
+
# model: "haiku"
|
|
157
|
+
# )
|
|
158
|
+
#
|
|
159
|
+
AgentDefinition = Data.define(
|
|
160
|
+
:description,
|
|
161
|
+
:prompt,
|
|
162
|
+
:tools,
|
|
163
|
+
:disallowed_tools,
|
|
164
|
+
:model,
|
|
165
|
+
:mcp_servers,
|
|
166
|
+
:critical_system_reminder
|
|
167
|
+
) do
|
|
168
|
+
def initialize(
|
|
169
|
+
description:,
|
|
170
|
+
prompt:,
|
|
171
|
+
tools: nil,
|
|
172
|
+
disallowed_tools: nil,
|
|
173
|
+
model: nil,
|
|
174
|
+
mcp_servers: nil,
|
|
175
|
+
critical_system_reminder: nil
|
|
176
|
+
)
|
|
177
|
+
super
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def to_h
|
|
181
|
+
result = {
|
|
182
|
+
description: description,
|
|
183
|
+
prompt: prompt
|
|
184
|
+
}
|
|
185
|
+
result[:tools] = tools if tools
|
|
186
|
+
result[:disallowedTools] = disallowed_tools if disallowed_tools
|
|
187
|
+
result[:model] = model if model
|
|
188
|
+
result[:mcpServers] = mcp_servers if mcp_servers
|
|
189
|
+
result[:criticalSystemReminder_EXPERIMENTAL] = critical_system_reminder if critical_system_reminder
|
|
190
|
+
result
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
data/lib/claude_agent.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/inflections"
|
|
4
|
+
require "active_support/core_ext/hash/keys"
|
|
5
|
+
|
|
6
|
+
require_relative "claude_agent/version"
|
|
7
|
+
require_relative "claude_agent/errors"
|
|
8
|
+
require_relative "claude_agent/types" # TypeScript SDK parity types
|
|
9
|
+
require_relative "claude_agent/sandbox_settings" # Sandbox configuration types
|
|
10
|
+
require_relative "claude_agent/abort_controller" # Abort/cancel support (TypeScript SDK parity)
|
|
11
|
+
require_relative "claude_agent/spawn" # Custom spawn support (TypeScript SDK parity)
|
|
12
|
+
require_relative "claude_agent/options"
|
|
13
|
+
require_relative "claude_agent/content_blocks"
|
|
14
|
+
require_relative "claude_agent/messages"
|
|
15
|
+
require_relative "claude_agent/message_parser"
|
|
16
|
+
require_relative "claude_agent/hooks"
|
|
17
|
+
require_relative "claude_agent/permissions"
|
|
18
|
+
require_relative "claude_agent/control_protocol"
|
|
19
|
+
require_relative "claude_agent/transport/base"
|
|
20
|
+
require_relative "claude_agent/transport/subprocess"
|
|
21
|
+
require_relative "claude_agent/mcp/tool"
|
|
22
|
+
require_relative "claude_agent/mcp/server"
|
|
23
|
+
require_relative "claude_agent/query"
|
|
24
|
+
require_relative "claude_agent/client"
|
|
25
|
+
|
|
26
|
+
module ClaudeAgent
|
|
27
|
+
# Re-export key classes at module level for convenience
|
|
28
|
+
end
|