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,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module ClaudeAgent
|
|
6
|
+
# Permission modes for tool execution (TypeScript SDK parity)
|
|
7
|
+
PERMISSION_MODES = %w[default acceptEdits plan bypassPermissions delegate dontAsk].freeze
|
|
8
|
+
|
|
9
|
+
# Configuration options for ClaudeAgent queries and clients
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# options = ClaudeAgent::Options.new(
|
|
13
|
+
# model: "claude-sonnet-4-5-20250514",
|
|
14
|
+
# max_turns: 10
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# @example With tools and permissions
|
|
18
|
+
# options = ClaudeAgent::Options.new(
|
|
19
|
+
# tools: ["Read", "Write", "Bash"],
|
|
20
|
+
# permission_mode: "acceptEdits",
|
|
21
|
+
# can_use_tool: ->(name, input, context) { { behavior: "allow" } }
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
class Options
|
|
25
|
+
# Default values for options that have non-nil defaults
|
|
26
|
+
DEFAULTS = {
|
|
27
|
+
allowed_tools: [],
|
|
28
|
+
disallowed_tools: [],
|
|
29
|
+
allow_dangerously_skip_permissions: false,
|
|
30
|
+
continue_conversation: false,
|
|
31
|
+
fork_session: false,
|
|
32
|
+
strict_mcp_config: false,
|
|
33
|
+
mcp_servers: {},
|
|
34
|
+
add_dirs: [],
|
|
35
|
+
env: {},
|
|
36
|
+
extra_args: {},
|
|
37
|
+
plugins: [],
|
|
38
|
+
include_partial_messages: false,
|
|
39
|
+
enable_file_checkpointing: false,
|
|
40
|
+
persist_session: true,
|
|
41
|
+
betas: []
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# All configurable attributes
|
|
45
|
+
ATTRIBUTES = %i[
|
|
46
|
+
tools allowed_tools disallowed_tools
|
|
47
|
+
system_prompt append_system_prompt
|
|
48
|
+
model fallback_model
|
|
49
|
+
permission_mode permission_prompt_tool_name can_use_tool allow_dangerously_skip_permissions
|
|
50
|
+
continue_conversation resume fork_session resume_session_at
|
|
51
|
+
max_turns max_budget_usd max_thinking_tokens
|
|
52
|
+
strict_mcp_config mcp_servers hooks
|
|
53
|
+
settings sandbox cwd add_dirs env user
|
|
54
|
+
cli_path extra_args agents setting_sources plugins
|
|
55
|
+
include_partial_messages output_format enable_file_checkpointing
|
|
56
|
+
persist_session betas max_buffer_size stderr_callback
|
|
57
|
+
abort_controller spawn_claude_code_process
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
attr_accessor(*ATTRIBUTES)
|
|
61
|
+
|
|
62
|
+
def initialize(**kwargs)
|
|
63
|
+
merged = DEFAULTS.merge(kwargs)
|
|
64
|
+
ATTRIBUTES.each do |attr|
|
|
65
|
+
instance_variable_set(:"@#{attr}", merged[attr])
|
|
66
|
+
end
|
|
67
|
+
validate!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Build CLI arguments from options
|
|
71
|
+
# @return [Array<String>] CLI arguments
|
|
72
|
+
def to_cli_args
|
|
73
|
+
[].tap do |args|
|
|
74
|
+
args.concat(system_prompt_args)
|
|
75
|
+
args.concat(model_args)
|
|
76
|
+
args.concat(tools_args)
|
|
77
|
+
args.concat(permission_args)
|
|
78
|
+
args.concat(conversation_args)
|
|
79
|
+
args.concat(limits_args)
|
|
80
|
+
args.concat(mcp_args)
|
|
81
|
+
args.concat(settings_args)
|
|
82
|
+
args.concat(environment_args)
|
|
83
|
+
args.concat(output_args)
|
|
84
|
+
args.concat(extra_cli_args)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Build environment variables for CLI process
|
|
89
|
+
# @return [Hash] Environment variables
|
|
90
|
+
def to_env
|
|
91
|
+
env.dup.tap do |process_env|
|
|
92
|
+
process_env["CLAUDE_CODE_ENTRYPOINT"] = "sdk-rb"
|
|
93
|
+
process_env["CLAUDE_AGENT_SDK_VERSION"] = ClaudeAgent::VERSION
|
|
94
|
+
process_env["CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING"] = "true" if enable_file_checkpointing
|
|
95
|
+
process_env["PWD"] = cwd.to_s if cwd
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Check if SDK MCP servers are configured
|
|
100
|
+
# @return [Boolean]
|
|
101
|
+
def has_sdk_mcp_servers?
|
|
102
|
+
return false unless mcp_servers.is_a?(Hash)
|
|
103
|
+
|
|
104
|
+
mcp_servers.any? { |_, v| v.is_a?(Hash) && v[:type] == "sdk" }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if hooks are configured
|
|
108
|
+
# @return [Boolean]
|
|
109
|
+
def has_hooks?
|
|
110
|
+
hooks.is_a?(Hash) && hooks.any?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get the abort signal from the controller
|
|
114
|
+
# @return [AbortSignal, nil]
|
|
115
|
+
def abort_signal
|
|
116
|
+
abort_controller&.signal
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# --- CLI Argument Builders ---
|
|
122
|
+
|
|
123
|
+
def system_prompt_args
|
|
124
|
+
[].tap do |args|
|
|
125
|
+
if system_prompt
|
|
126
|
+
case system_prompt
|
|
127
|
+
when String then args.push("--system-prompt", system_prompt)
|
|
128
|
+
when Hash then args.push("--system-prompt", JSON.generate(system_prompt))
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
args.push("--append-system-prompt", append_system_prompt) if append_system_prompt
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def model_args
|
|
136
|
+
[].tap do |args|
|
|
137
|
+
args.push("--model", model) if model
|
|
138
|
+
args.push("--fallback-model", fallback_model) if fallback_model
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def tools_args
|
|
143
|
+
[].tap do |args|
|
|
144
|
+
if tools
|
|
145
|
+
case tools
|
|
146
|
+
when Array then args.push("--tools", tools.join(","))
|
|
147
|
+
when ToolsPreset then args.push("--tools", JSON.generate(tools.to_h))
|
|
148
|
+
when Hash then args.push("--tools", JSON.generate(tools))
|
|
149
|
+
else args.push("--tools", tools.to_s)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
args.push("--allowedTools", allowed_tools.join(",")) if allowed_tools.any?
|
|
153
|
+
args.push("--disallowedTools", disallowed_tools.join(",")) if disallowed_tools.any?
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def permission_args
|
|
158
|
+
[].tap do |args|
|
|
159
|
+
args.push("--permission-mode", permission_mode) if permission_mode
|
|
160
|
+
args.push("--permission-prompt-tool", permission_prompt_tool_name) if permission_prompt_tool_name
|
|
161
|
+
args.push("--dangerously-skip-permissions") if allow_dangerously_skip_permissions
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def conversation_args
|
|
166
|
+
[].tap do |args|
|
|
167
|
+
args.push("--continue") if continue_conversation
|
|
168
|
+
args.push("--resume", resume) if resume
|
|
169
|
+
args.push("--fork-session") if fork_session
|
|
170
|
+
args.push("--resume-session-at", resume_session_at) if resume_session_at
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def limits_args
|
|
175
|
+
[].tap do |args|
|
|
176
|
+
args.push("--max-turns", max_turns.to_s) if max_turns
|
|
177
|
+
args.push("--max-budget-usd", max_budget_usd.to_s) if max_budget_usd
|
|
178
|
+
args.push("--max-thinking-tokens", max_thinking_tokens.to_s) if max_thinking_tokens
|
|
179
|
+
args.push("--strict-mcp-config") if strict_mcp_config
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def mcp_args
|
|
184
|
+
[].tap do |args|
|
|
185
|
+
if mcp_servers.is_a?(Hash) && mcp_servers.any?
|
|
186
|
+
external_servers = mcp_servers.reject { |_, v| v.is_a?(Hash) && v[:type] == "sdk" }
|
|
187
|
+
args.push("--mcp-config", JSON.generate(external_servers)) if external_servers.any?
|
|
188
|
+
elsif mcp_servers.is_a?(String)
|
|
189
|
+
args.push("--mcp-config", mcp_servers)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def settings_args
|
|
195
|
+
[].tap do |args|
|
|
196
|
+
args.push("--settings", settings) if settings
|
|
197
|
+
if sandbox
|
|
198
|
+
sandbox_json = sandbox.respond_to?(:to_h) ? sandbox.to_h : sandbox
|
|
199
|
+
args.push("--sandbox", JSON.generate(sandbox_json))
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def environment_args
|
|
205
|
+
[].tap do |args|
|
|
206
|
+
args.push("--user", user) if user
|
|
207
|
+
add_dirs.each { |dir| args.push("--add-dir", dir.to_s) }
|
|
208
|
+
args.push("--setting-sources", setting_sources.join(",")) if setting_sources&.any?
|
|
209
|
+
plugins.each do |plugin|
|
|
210
|
+
dir = plugin.is_a?(Hash) ? plugin[:dir] : plugin
|
|
211
|
+
args.push("--plugin-dir", dir.to_s)
|
|
212
|
+
end
|
|
213
|
+
args.push("--betas", betas.join(",")) if betas.any?
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def output_args
|
|
218
|
+
[].tap do |args|
|
|
219
|
+
args.push("--enable-file-checkpointing") if enable_file_checkpointing
|
|
220
|
+
args.push("--no-persist-session") if persist_session == false
|
|
221
|
+
args.push("--json-schema", JSON.generate(output_format)) if output_format
|
|
222
|
+
args.push("--include-partial-messages") if include_partial_messages
|
|
223
|
+
if agents
|
|
224
|
+
agents_hash = agents.transform_values { |a| a.respond_to?(:to_h) ? a.to_h : a }
|
|
225
|
+
args.push("--agents", JSON.generate(agents_hash))
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def extra_cli_args
|
|
231
|
+
[].tap do |args|
|
|
232
|
+
extra_args.each do |key, value|
|
|
233
|
+
flag = key.to_s.start_with?("--") ? key.to_s : "--#{key}"
|
|
234
|
+
value.nil? ? args.push(flag) : args.push(flag, value.to_s)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# --- Validation ---
|
|
240
|
+
|
|
241
|
+
def validate!
|
|
242
|
+
if permission_mode && !PERMISSION_MODES.include?(permission_mode)
|
|
243
|
+
raise ConfigurationError, "Invalid permission_mode: #{permission_mode}. Must be one of: #{PERMISSION_MODES.join(", ")}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
if permission_mode == "bypassPermissions" && !allow_dangerously_skip_permissions
|
|
247
|
+
raise ConfigurationError,
|
|
248
|
+
"Must set allow_dangerously_skip_permissions: true to use bypassPermissions mode"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
if can_use_tool && !can_use_tool.respond_to?(:call)
|
|
252
|
+
raise ConfigurationError, "can_use_tool must be callable (Proc, Lambda, or object responding to #call)"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
if max_turns && (!max_turns.is_a?(Integer) || max_turns < 1)
|
|
256
|
+
raise ConfigurationError, "max_turns must be a positive integer"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
if max_budget_usd && (!max_budget_usd.is_a?(Numeric) || max_budget_usd <= 0)
|
|
260
|
+
raise ConfigurationError, "max_budget_usd must be a positive number"
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
# Result of a permission check (allow)
|
|
5
|
+
#
|
|
6
|
+
# @example Allow with modified input
|
|
7
|
+
# PermissionResultAllow.new(
|
|
8
|
+
# updated_input: input.merge("safe" => true)
|
|
9
|
+
# )
|
|
10
|
+
#
|
|
11
|
+
PermissionResultAllow = Data.define(:updated_input, :updated_permissions) do
|
|
12
|
+
def initialize(updated_input: nil, updated_permissions: nil)
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def behavior
|
|
17
|
+
"allow"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
h = { behavior: "allow" }
|
|
22
|
+
h[:updatedInput] = updated_input if updated_input
|
|
23
|
+
h[:updatedPermissions] = updated_permissions&.map { |p| p.respond_to?(:to_h) ? p.to_h : p } if updated_permissions
|
|
24
|
+
h
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Result of a permission check (deny)
|
|
29
|
+
#
|
|
30
|
+
# @example Deny with message
|
|
31
|
+
# PermissionResultDeny.new(
|
|
32
|
+
# message: "Operation not allowed",
|
|
33
|
+
# interrupt: true
|
|
34
|
+
# )
|
|
35
|
+
#
|
|
36
|
+
PermissionResultDeny = Data.define(:message, :interrupt) do
|
|
37
|
+
def initialize(message: "", interrupt: false)
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def behavior
|
|
42
|
+
"deny"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
{ behavior: "deny", message: message, interrupt: interrupt }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Valid permission update types
|
|
51
|
+
PERMISSION_UPDATE_TYPES = %w[
|
|
52
|
+
addRules
|
|
53
|
+
replaceRules
|
|
54
|
+
removeRules
|
|
55
|
+
setMode
|
|
56
|
+
addDirectories
|
|
57
|
+
removeDirectories
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
# Permission update request
|
|
61
|
+
#
|
|
62
|
+
# @example Add rules
|
|
63
|
+
# PermissionUpdate.new(
|
|
64
|
+
# type: "addRules",
|
|
65
|
+
# rules: [{tool_name: "Read", behavior: "allow"}]
|
|
66
|
+
# )
|
|
67
|
+
#
|
|
68
|
+
PermissionUpdate = Data.define(
|
|
69
|
+
:type,
|
|
70
|
+
:rules,
|
|
71
|
+
:behavior,
|
|
72
|
+
:mode,
|
|
73
|
+
:directories,
|
|
74
|
+
:destination
|
|
75
|
+
) do
|
|
76
|
+
def initialize(
|
|
77
|
+
type:,
|
|
78
|
+
rules: nil,
|
|
79
|
+
behavior: nil,
|
|
80
|
+
mode: nil,
|
|
81
|
+
directories: nil,
|
|
82
|
+
destination: nil
|
|
83
|
+
)
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_h
|
|
88
|
+
h = { type: type }
|
|
89
|
+
h[:rules] = rules.map { |r| normalize_rule(r) } if rules
|
|
90
|
+
h[:behavior] = behavior if behavior
|
|
91
|
+
h[:mode] = mode if mode
|
|
92
|
+
h[:directories] = directories if directories
|
|
93
|
+
h[:destination] = destination if destination
|
|
94
|
+
h
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def normalize_rule(rule)
|
|
100
|
+
return rule unless rule.is_a?(Hash)
|
|
101
|
+
|
|
102
|
+
# Convert snake_case to camelCase
|
|
103
|
+
# Note: behavior is NOT part of PermissionRuleValue per TypeScript SDK
|
|
104
|
+
{
|
|
105
|
+
toolName: rule[:tool_name] || rule[:toolName],
|
|
106
|
+
ruleContent: rule[:rule_content] || rule[:ruleContent]
|
|
107
|
+
}.compact
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Permission rule value (TypeScript SDK parity)
|
|
112
|
+
# Note: behavior is on PermissionUpdate, not on individual rules
|
|
113
|
+
#
|
|
114
|
+
PermissionRuleValue = Data.define(:tool_name, :rule_content) do
|
|
115
|
+
def initialize(tool_name: nil, rule_content: nil)
|
|
116
|
+
super
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def to_h
|
|
120
|
+
{
|
|
121
|
+
toolName: tool_name,
|
|
122
|
+
ruleContent: rule_content
|
|
123
|
+
}.compact
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Valid permission update destinations (TypeScript SDK parity)
|
|
128
|
+
PERMISSION_UPDATE_DESTINATIONS = %w[
|
|
129
|
+
userSettings
|
|
130
|
+
projectSettings
|
|
131
|
+
localSettings
|
|
132
|
+
session
|
|
133
|
+
cliArg
|
|
134
|
+
].freeze
|
|
135
|
+
|
|
136
|
+
# Context provided to can_use_tool callbacks (TypeScript SDK parity)
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# context = ToolPermissionContext.new(
|
|
140
|
+
# permission_suggestions: [update1, update2],
|
|
141
|
+
# blocked_path: "/etc/passwd",
|
|
142
|
+
# decision_reason: "Path outside allowed directories",
|
|
143
|
+
# tool_use_id: "tool_123",
|
|
144
|
+
# agent_id: "agent_456"
|
|
145
|
+
# )
|
|
146
|
+
#
|
|
147
|
+
ToolPermissionContext = Data.define(
|
|
148
|
+
:permission_suggestions,
|
|
149
|
+
:blocked_path,
|
|
150
|
+
:decision_reason,
|
|
151
|
+
:tool_use_id,
|
|
152
|
+
:agent_id
|
|
153
|
+
) do
|
|
154
|
+
def initialize(
|
|
155
|
+
permission_suggestions: nil,
|
|
156
|
+
blocked_path: nil,
|
|
157
|
+
decision_reason: nil,
|
|
158
|
+
tool_use_id: nil,
|
|
159
|
+
agent_id: nil
|
|
160
|
+
)
|
|
161
|
+
super
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
class << self
|
|
5
|
+
# One-shot query to Claude Code CLI
|
|
6
|
+
#
|
|
7
|
+
# This is a simple, stateless interface for sending a single prompt
|
|
8
|
+
# and receiving all responses. For interactive conversations, use
|
|
9
|
+
# {ClaudeAgent::Client} instead.
|
|
10
|
+
#
|
|
11
|
+
# @param prompt [String] The prompt to send to Claude
|
|
12
|
+
# @param options [Options, nil] Configuration options
|
|
13
|
+
# @param transport [Transport::Base, nil] Custom transport (default: Subprocess)
|
|
14
|
+
# @return [Enumerator<Message>] Enumerator yielding Message objects
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# ClaudeAgent.query(prompt: "What is 2+2?").each do |message|
|
|
18
|
+
# case message
|
|
19
|
+
# when ClaudeAgent::AssistantMessage
|
|
20
|
+
# puts message.text
|
|
21
|
+
# when ClaudeAgent::ResultMessage
|
|
22
|
+
# puts "Cost: $#{message.total_cost_usd}"
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Collect all messages
|
|
27
|
+
# messages = ClaudeAgent.query(prompt: "Hello").to_a
|
|
28
|
+
# result = messages.last
|
|
29
|
+
# puts "Completed in #{result.duration_ms}ms"
|
|
30
|
+
#
|
|
31
|
+
# @example With custom options
|
|
32
|
+
# options = ClaudeAgent::Options.new(
|
|
33
|
+
# model: "claude-sonnet-4-5-20250514",
|
|
34
|
+
# max_turns: 5,
|
|
35
|
+
# permission_mode: "acceptEdits"
|
|
36
|
+
# )
|
|
37
|
+
# ClaudeAgent.query(prompt: "Fix the bug", options: options).each { |m| puts m }
|
|
38
|
+
#
|
|
39
|
+
def query(prompt:, options: nil, transport: nil)
|
|
40
|
+
options ||= Options.new
|
|
41
|
+
transport ||= Transport::Subprocess.new(options: options)
|
|
42
|
+
|
|
43
|
+
Enumerator.new do |yielder|
|
|
44
|
+
# Set entrypoint environment variable
|
|
45
|
+
ENV["CLAUDE_CODE_ENTRYPOINT"] = "sdk-rb"
|
|
46
|
+
|
|
47
|
+
# Determine mode based on hooks/MCP servers
|
|
48
|
+
streaming = options.has_hooks? || options.has_sdk_mcp_servers?
|
|
49
|
+
|
|
50
|
+
if streaming
|
|
51
|
+
# Use streaming mode with control protocol
|
|
52
|
+
protocol = ControlProtocol.new(transport: transport, options: options)
|
|
53
|
+
begin
|
|
54
|
+
# Register abort handler if abort controller is provided
|
|
55
|
+
if options.abort_signal
|
|
56
|
+
options.abort_signal.on_abort do
|
|
57
|
+
protocol.abort! rescue nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
protocol.start(streaming: true)
|
|
62
|
+
protocol.send_user_message(prompt)
|
|
63
|
+
transport.end_input unless options.has_hooks? || options.has_sdk_mcp_servers?
|
|
64
|
+
|
|
65
|
+
protocol.each_message do |message|
|
|
66
|
+
yielder << message
|
|
67
|
+
break if message.is_a?(ResultMessage)
|
|
68
|
+
end
|
|
69
|
+
ensure
|
|
70
|
+
protocol.stop
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
# Simple mode - just send prompt and read responses
|
|
74
|
+
parser = MessageParser.new
|
|
75
|
+
begin
|
|
76
|
+
transport.connect(streaming: false, prompt: prompt)
|
|
77
|
+
|
|
78
|
+
transport.read_messages do |raw|
|
|
79
|
+
message = parser.parse(raw)
|
|
80
|
+
yielder << message
|
|
81
|
+
break if message.is_a?(ResultMessage)
|
|
82
|
+
end
|
|
83
|
+
ensure
|
|
84
|
+
transport.close
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeAgent
|
|
4
|
+
# Network-specific configuration for sandbox mode (TypeScript SDK parity)
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# network = SandboxNetworkConfig.new(
|
|
8
|
+
# allow_local_binding: true,
|
|
9
|
+
# allow_unix_sockets: ["/var/run/docker.sock"],
|
|
10
|
+
# allowed_domains: ["api.example.com"]
|
|
11
|
+
# )
|
|
12
|
+
#
|
|
13
|
+
SandboxNetworkConfig = Data.define(
|
|
14
|
+
:allowed_domains,
|
|
15
|
+
:allow_local_binding,
|
|
16
|
+
:allow_unix_sockets,
|
|
17
|
+
:allow_all_unix_sockets,
|
|
18
|
+
:http_proxy_port,
|
|
19
|
+
:socks_proxy_port
|
|
20
|
+
) do
|
|
21
|
+
def initialize(
|
|
22
|
+
allowed_domains: [],
|
|
23
|
+
allow_local_binding: false,
|
|
24
|
+
allow_unix_sockets: [],
|
|
25
|
+
allow_all_unix_sockets: false,
|
|
26
|
+
http_proxy_port: nil,
|
|
27
|
+
socks_proxy_port: nil
|
|
28
|
+
)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
result = {}
|
|
34
|
+
result[:allowedDomains] = allowed_domains unless allowed_domains.empty?
|
|
35
|
+
result[:allowLocalBinding] = allow_local_binding if allow_local_binding
|
|
36
|
+
result[:allowUnixSockets] = allow_unix_sockets unless allow_unix_sockets.empty?
|
|
37
|
+
result[:allowAllUnixSockets] = allow_all_unix_sockets if allow_all_unix_sockets
|
|
38
|
+
result[:httpProxyPort] = http_proxy_port if http_proxy_port
|
|
39
|
+
result[:socksProxyPort] = socks_proxy_port if socks_proxy_port
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Configuration for ignoring specific sandbox violations (TypeScript SDK parity)
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# ignore = SandboxIgnoreViolations.new(
|
|
48
|
+
# file: ["/tmp/*"],
|
|
49
|
+
# network: ["localhost:*"]
|
|
50
|
+
# )
|
|
51
|
+
#
|
|
52
|
+
SandboxIgnoreViolations = Data.define(:file, :network) do
|
|
53
|
+
def initialize(file: [], network: [])
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_h
|
|
58
|
+
result = {}
|
|
59
|
+
result[:file] = file unless file.empty?
|
|
60
|
+
result[:network] = network unless network.empty?
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Custom ripgrep configuration for sandbox mode (TypeScript SDK parity)
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# ripgrep = SandboxRipgrepConfig.new(
|
|
69
|
+
# command: "/usr/local/bin/rg",
|
|
70
|
+
# args: ["--hidden"]
|
|
71
|
+
# )
|
|
72
|
+
#
|
|
73
|
+
SandboxRipgrepConfig = Data.define(:command, :args) do
|
|
74
|
+
def initialize(command:, args: nil)
|
|
75
|
+
super
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def to_h
|
|
79
|
+
result = { command: command }
|
|
80
|
+
result[:args] = args if args
|
|
81
|
+
result
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Sandbox configuration for command execution (TypeScript SDK parity)
|
|
86
|
+
#
|
|
87
|
+
# @example Basic sandbox
|
|
88
|
+
# sandbox = SandboxSettings.new(enabled: true)
|
|
89
|
+
#
|
|
90
|
+
# @example With network config
|
|
91
|
+
# sandbox = SandboxSettings.new(
|
|
92
|
+
# enabled: true,
|
|
93
|
+
# auto_allow_bash_if_sandboxed: true,
|
|
94
|
+
# excluded_commands: ["docker"],
|
|
95
|
+
# network: SandboxNetworkConfig.new(allow_local_binding: true)
|
|
96
|
+
# )
|
|
97
|
+
#
|
|
98
|
+
# @example With custom ripgrep
|
|
99
|
+
# sandbox = SandboxSettings.new(
|
|
100
|
+
# enabled: true,
|
|
101
|
+
# ripgrep: SandboxRipgrepConfig.new(command: "/usr/local/bin/rg")
|
|
102
|
+
# )
|
|
103
|
+
#
|
|
104
|
+
SandboxSettings = Data.define(
|
|
105
|
+
:enabled,
|
|
106
|
+
:auto_allow_bash_if_sandboxed,
|
|
107
|
+
:excluded_commands,
|
|
108
|
+
:allow_unsandboxed_commands,
|
|
109
|
+
:network,
|
|
110
|
+
:ignore_violations,
|
|
111
|
+
:enable_weaker_nested_sandbox,
|
|
112
|
+
:ripgrep
|
|
113
|
+
) do
|
|
114
|
+
def initialize(
|
|
115
|
+
enabled: false,
|
|
116
|
+
auto_allow_bash_if_sandboxed: false,
|
|
117
|
+
excluded_commands: [],
|
|
118
|
+
allow_unsandboxed_commands: false,
|
|
119
|
+
network: nil,
|
|
120
|
+
ignore_violations: nil,
|
|
121
|
+
enable_weaker_nested_sandbox: false,
|
|
122
|
+
ripgrep: nil
|
|
123
|
+
)
|
|
124
|
+
super
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def to_h
|
|
128
|
+
result = { enabled: enabled }
|
|
129
|
+
result[:autoAllowBashIfSandboxed] = auto_allow_bash_if_sandboxed if auto_allow_bash_if_sandboxed
|
|
130
|
+
result[:excludedCommands] = excluded_commands unless excluded_commands.empty?
|
|
131
|
+
result[:allowUnsandboxedCommands] = allow_unsandboxed_commands if allow_unsandboxed_commands
|
|
132
|
+
result[:network] = network.to_h if network && !network.to_h.empty?
|
|
133
|
+
result[:ignoreViolations] = ignore_violations.to_h if ignore_violations && !ignore_violations.to_h.empty?
|
|
134
|
+
result[:enableWeakerNestedSandbox] = enable_weaker_nested_sandbox if enable_weaker_nested_sandbox
|
|
135
|
+
result[:ripgrep] = ripgrep.to_h if ripgrep
|
|
136
|
+
result
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|