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,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module ClaudeAgent
|
|
6
|
+
# Options passed to a spawn function for creating a Claude Code process (TypeScript SDK parity)
|
|
7
|
+
#
|
|
8
|
+
# This allows custom process creation for VMs, containers, remote execution, etc.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# options = SpawnOptions.new(
|
|
12
|
+
# command: "/usr/local/bin/claude",
|
|
13
|
+
# args: ["--output-format", "stream-json"],
|
|
14
|
+
# cwd: "/my/project",
|
|
15
|
+
# env: { "CLAUDE_CODE_ENTRYPOINT" => "sdk-rb" }
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
SpawnOptions = Data.define(:command, :args, :cwd, :env, :abort_signal) do
|
|
19
|
+
def initialize(command:, args: [], cwd: nil, env: {}, abort_signal: nil)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get the full command line as an array
|
|
24
|
+
# @return [Array<String>]
|
|
25
|
+
def to_command_array
|
|
26
|
+
[ command, *args ]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Interface for spawned process (TypeScript SDK parity)
|
|
31
|
+
#
|
|
32
|
+
# Custom spawn functions must return an object that responds to these methods.
|
|
33
|
+
# This allows wrapping SSH connections, Docker exec, VM instances, etc.
|
|
34
|
+
#
|
|
35
|
+
# @abstract Implement all methods for custom process types
|
|
36
|
+
#
|
|
37
|
+
module SpawnedProcess
|
|
38
|
+
# Write data to process stdin
|
|
39
|
+
# @param data [String] Data to write
|
|
40
|
+
# @return [void]
|
|
41
|
+
def write(data)
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Read from process stdout
|
|
46
|
+
# @yield [String] Lines from stdout
|
|
47
|
+
# @return [void]
|
|
48
|
+
def read_stdout
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Read from process stderr
|
|
53
|
+
# @yield [String] Lines from stderr
|
|
54
|
+
# @return [void]
|
|
55
|
+
def read_stderr
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Close stdin to signal end of input
|
|
60
|
+
# @return [void]
|
|
61
|
+
def close_stdin
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Terminate the process gracefully (SIGTERM equivalent)
|
|
66
|
+
# @param timeout [Numeric] Seconds to wait before force kill
|
|
67
|
+
# @return [void]
|
|
68
|
+
def terminate(timeout: 5)
|
|
69
|
+
raise NotImplementedError
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Force kill the process (SIGKILL equivalent)
|
|
73
|
+
# @return [void]
|
|
74
|
+
def kill
|
|
75
|
+
raise NotImplementedError
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check if process is still running
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
def running?
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get process exit status
|
|
85
|
+
# @return [Integer, nil]
|
|
86
|
+
def exit_status
|
|
87
|
+
raise NotImplementedError
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Close all streams
|
|
91
|
+
# @return [void]
|
|
92
|
+
def close
|
|
93
|
+
raise NotImplementedError
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Local spawned process wrapping Open3.popen3 (TypeScript SDK parity)
|
|
98
|
+
#
|
|
99
|
+
# This is the default implementation used when no custom spawn function is provided.
|
|
100
|
+
#
|
|
101
|
+
# @example
|
|
102
|
+
# process = LocalSpawnedProcess.spawn(options)
|
|
103
|
+
# process.write('{"type":"user"}\n')
|
|
104
|
+
# process.read_stdout { |line| puts line }
|
|
105
|
+
# process.close
|
|
106
|
+
#
|
|
107
|
+
class LocalSpawnedProcess
|
|
108
|
+
include SpawnedProcess
|
|
109
|
+
|
|
110
|
+
attr_reader :pid, :stdin, :stdout, :stderr, :wait_thread
|
|
111
|
+
|
|
112
|
+
# Spawn a new local process
|
|
113
|
+
# @param spawn_options [SpawnOptions] Options for spawning
|
|
114
|
+
# @return [LocalSpawnedProcess]
|
|
115
|
+
def self.spawn(spawn_options)
|
|
116
|
+
cmd = spawn_options.to_command_array
|
|
117
|
+
env = spawn_options.env || {}
|
|
118
|
+
cwd = spawn_options.cwd
|
|
119
|
+
|
|
120
|
+
opts = {}
|
|
121
|
+
opts[:chdir] = cwd if cwd && Dir.exist?(cwd)
|
|
122
|
+
|
|
123
|
+
stdin, stdout, stderr, wait_thread = Open3.popen3(env, *cmd, **opts)
|
|
124
|
+
|
|
125
|
+
new(stdin: stdin, stdout: stdout, stderr: stderr, wait_thread: wait_thread)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def initialize(stdin:, stdout:, stderr:, wait_thread:)
|
|
129
|
+
@stdin = stdin
|
|
130
|
+
@stdout = stdout
|
|
131
|
+
@stderr = stderr
|
|
132
|
+
@wait_thread = wait_thread
|
|
133
|
+
@killed = false
|
|
134
|
+
@mutex = Mutex.new
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def write(data)
|
|
138
|
+
@mutex.synchronize do
|
|
139
|
+
return if @stdin.closed?
|
|
140
|
+
|
|
141
|
+
@stdin.write(data)
|
|
142
|
+
@stdin.write("\n") unless data.end_with?("\n")
|
|
143
|
+
@stdin.flush
|
|
144
|
+
end
|
|
145
|
+
rescue Errno::EPIPE
|
|
146
|
+
# Process terminated
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def read_stdout(&block)
|
|
150
|
+
return enum_for(:read_stdout) unless block_given?
|
|
151
|
+
|
|
152
|
+
@stdout.each_line(&block)
|
|
153
|
+
rescue IOError
|
|
154
|
+
# Stream closed
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def read_stderr(&block)
|
|
158
|
+
return enum_for(:read_stderr) unless block_given?
|
|
159
|
+
|
|
160
|
+
@stderr.each_line(&block)
|
|
161
|
+
rescue IOError
|
|
162
|
+
# Stream closed
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def close_stdin
|
|
166
|
+
@mutex.synchronize do
|
|
167
|
+
@stdin.close unless @stdin.closed?
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def terminate(timeout: 5)
|
|
172
|
+
return unless running?
|
|
173
|
+
|
|
174
|
+
pid = @wait_thread.pid
|
|
175
|
+
begin
|
|
176
|
+
Process.kill("TERM", pid)
|
|
177
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
178
|
+
return
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
unless @wait_thread.join(timeout)
|
|
182
|
+
kill
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def kill
|
|
187
|
+
return unless running?
|
|
188
|
+
|
|
189
|
+
@mutex.synchronize { @killed = true }
|
|
190
|
+
pid = @wait_thread.pid
|
|
191
|
+
begin
|
|
192
|
+
Process.kill("KILL", pid)
|
|
193
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
194
|
+
# Already dead
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def running?
|
|
199
|
+
@wait_thread.alive?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def exit_status
|
|
203
|
+
@wait_thread.value&.exitstatus
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def killed?
|
|
207
|
+
@killed
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def close
|
|
211
|
+
@mutex.synchronize do
|
|
212
|
+
@stdin.close unless @stdin.closed?
|
|
213
|
+
@stdout.close unless @stdout.closed?
|
|
214
|
+
@stderr.close unless @stderr.closed?
|
|
215
|
+
end
|
|
216
|
+
@wait_thread.value
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Default spawn function for local subprocess execution
|
|
221
|
+
#
|
|
222
|
+
# This lambda is used when no custom spawn_claude_code_process is provided.
|
|
223
|
+
# It creates a LocalSpawnedProcess using Open3.popen3.
|
|
224
|
+
#
|
|
225
|
+
# @example Custom spawn for Docker
|
|
226
|
+
# custom_spawn = ->(opts) {
|
|
227
|
+
# docker_cmd = ["docker", "exec", "-i", "my-container", opts.command, *opts.args]
|
|
228
|
+
# DockerProcess.new(docker_cmd, env: opts.env)
|
|
229
|
+
# }
|
|
230
|
+
# options = ClaudeAgent::Options.new(spawn_claude_code_process: custom_spawn)
|
|
231
|
+
#
|
|
232
|
+
DEFAULT_SPAWN = ->(spawn_options) {
|
|
233
|
+
LocalSpawnedProcess.spawn(spawn_options)
|
|
234
|
+
}.freeze
|
|
235
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
module Transport
|
|
5
|
+
# Abstract base class for transport implementations
|
|
6
|
+
#
|
|
7
|
+
# Transports handle the low-level communication with Claude Code CLI
|
|
8
|
+
# or other backends. They are responsible for:
|
|
9
|
+
# - Starting/stopping the connection
|
|
10
|
+
# - Writing messages (JSON Lines format)
|
|
11
|
+
# - Reading and parsing responses
|
|
12
|
+
#
|
|
13
|
+
# @abstract Subclass and implement all abstract methods
|
|
14
|
+
#
|
|
15
|
+
class Base
|
|
16
|
+
# Establish the connection
|
|
17
|
+
# @return [void]
|
|
18
|
+
def connect
|
|
19
|
+
raise NotImplementedError, "#{self.class} must implement #connect"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Write data to the transport
|
|
23
|
+
# @param data [String] JSON string to write (newline will be added)
|
|
24
|
+
# @return [void]
|
|
25
|
+
def write(data)
|
|
26
|
+
raise NotImplementedError, "#{self.class} must implement #write"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Read messages from the transport
|
|
30
|
+
# @yield [Hash] Parsed JSON messages
|
|
31
|
+
# @return [Enumerator] If no block given
|
|
32
|
+
def read_messages(&block)
|
|
33
|
+
raise NotImplementedError, "#{self.class} must implement #read_messages"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Signal end of input (close stdin for subprocess)
|
|
37
|
+
# @return [void]
|
|
38
|
+
def end_input
|
|
39
|
+
raise NotImplementedError, "#{self.class} must implement #end_input"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Close the transport and cleanup resources
|
|
43
|
+
# @return [void]
|
|
44
|
+
def close
|
|
45
|
+
raise NotImplementedError, "#{self.class} must implement #close"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if the transport is ready for communication
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def ready?
|
|
51
|
+
raise NotImplementedError, "#{self.class} must implement #ready?"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if the transport is connected
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def connected?
|
|
57
|
+
raise NotImplementedError, "#{self.class} must implement #connected?"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|