claude_agent 0.7.12 → 0.7.13

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/rules/testing.md +51 -10
  3. data/.claude/settings.json +1 -0
  4. data/ARCHITECTURE.md +237 -0
  5. data/CHANGELOG.md +45 -0
  6. data/CLAUDE.md +2 -0
  7. data/README.md +46 -1
  8. data/Rakefile +17 -0
  9. data/SPEC.md +214 -125
  10. data/lib/claude_agent/client/commands.rb +225 -0
  11. data/lib/claude_agent/client.rb +4 -204
  12. data/lib/claude_agent/content_blocks/generic_block.rb +39 -0
  13. data/lib/claude_agent/content_blocks/image_content_block.rb +54 -0
  14. data/lib/claude_agent/content_blocks/server_tool_result_block.rb +22 -0
  15. data/lib/claude_agent/content_blocks/server_tool_use_block.rb +48 -0
  16. data/lib/claude_agent/content_blocks/text_block.rb +19 -0
  17. data/lib/claude_agent/content_blocks/thinking_block.rb +19 -0
  18. data/lib/claude_agent/content_blocks/tool_result_block.rb +25 -0
  19. data/lib/claude_agent/content_blocks/tool_use_block.rb +134 -0
  20. data/lib/claude_agent/content_blocks.rb +8 -335
  21. data/lib/claude_agent/control_protocol/commands.rb +304 -0
  22. data/lib/claude_agent/control_protocol/lifecycle.rb +113 -0
  23. data/lib/claude_agent/control_protocol/messaging.rb +166 -0
  24. data/lib/claude_agent/control_protocol/primitives.rb +168 -0
  25. data/lib/claude_agent/control_protocol/request_handling.rb +231 -0
  26. data/lib/claude_agent/control_protocol.rb +27 -882
  27. data/lib/claude_agent/event_handler.rb +1 -0
  28. data/lib/claude_agent/get_session_info.rb +86 -0
  29. data/lib/claude_agent/hooks.rb +23 -2
  30. data/lib/claude_agent/list_sessions.rb +22 -13
  31. data/lib/claude_agent/message_parser.rb +26 -4
  32. data/lib/claude_agent/messages/conversation.rb +138 -0
  33. data/lib/claude_agent/messages/generic.rb +39 -0
  34. data/lib/claude_agent/messages/hook_lifecycle.rb +158 -0
  35. data/lib/claude_agent/messages/result.rb +80 -0
  36. data/lib/claude_agent/messages/streaming.rb +84 -0
  37. data/lib/claude_agent/messages/system.rb +67 -0
  38. data/lib/claude_agent/messages/task_lifecycle.rb +240 -0
  39. data/lib/claude_agent/messages/tool_lifecycle.rb +95 -0
  40. data/lib/claude_agent/messages.rb +11 -829
  41. data/lib/claude_agent/options/serializer.rb +194 -0
  42. data/lib/claude_agent/options.rb +11 -176
  43. data/lib/claude_agent/sandbox_settings.rb +3 -0
  44. data/lib/claude_agent/session.rb +0 -204
  45. data/lib/claude_agent/session_mutations.rb +148 -0
  46. data/lib/claude_agent/types/mcp.rb +30 -0
  47. data/lib/claude_agent/types/models.rb +146 -0
  48. data/lib/claude_agent/types/operations.rb +38 -0
  49. data/lib/claude_agent/types/sessions.rb +50 -0
  50. data/lib/claude_agent/types/tools.rb +32 -0
  51. data/lib/claude_agent/types.rb +6 -264
  52. data/lib/claude_agent/v2_session.rb +207 -0
  53. data/lib/claude_agent/version.rb +1 -1
  54. data/lib/claude_agent.rb +37 -3
  55. data/sig/claude_agent.rbs +144 -13
  56. metadata +33 -1
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module ClaudeAgent
6
+ # Tool use request block
7
+ #
8
+ # @example
9
+ # block = ToolUseBlock.new(id: "tool_123", name: "Read", input: {file_path: "/tmp/file"})
10
+ # block.input[:file_path] # => "/tmp/file"
11
+ # block.name # => "Read"
12
+ #
13
+ ToolUseBlock = Data.define(:id, :name, :input) do
14
+ def type
15
+ :tool_use
16
+ end
17
+
18
+ def to_h
19
+ { type: "tool_use", id: id, name: name, input: input }
20
+ end
21
+
22
+ # Returns the file path for file-based tools, nil otherwise.
23
+ # @return [String, nil]
24
+ def file_path
25
+ case name
26
+ when "Read", "Write", "Edit"
27
+ input[:file_path]
28
+ when "NotebookEdit"
29
+ input[:notebook_path]
30
+ end
31
+ end
32
+
33
+ # One-line human-readable label for the tool call.
34
+ # @return [String]
35
+ def display_label
36
+ case name
37
+ when "Read", "Write", "Edit", "NotebookEdit"
38
+ path = file_path
39
+ path ? "#{name} #{shorten_path(path)}" : name
40
+ when "Bash"
41
+ cmd = input[:command]
42
+ cmd ? "Bash: #{truncate(cmd, 50)}" : "Bash"
43
+ when "Grep"
44
+ pattern = input[:pattern]
45
+ pattern ? "Grep: #{pattern}" : "Grep"
46
+ when "Glob"
47
+ pattern = input[:pattern]
48
+ pattern ? "Glob: #{pattern}" : "Glob"
49
+ when "WebFetch"
50
+ host = extract_host(input[:url])
51
+ host ? "WebFetch: #{host}" : "WebFetch"
52
+ when "WebSearch"
53
+ query = input[:query]
54
+ query ? "WebSearch: #{truncate(query, 50)}" : "WebSearch"
55
+ when "Task"
56
+ desc = input[:description]
57
+ desc ? "Task: #{truncate(desc, 50)}" : "Task"
58
+ else
59
+ name
60
+ end
61
+ end
62
+
63
+ # Detailed summary of the tool call, truncated to max chars.
64
+ # @param max [Integer] maximum length before truncation
65
+ # @return [String]
66
+ def summary(max: 60)
67
+ text = case name
68
+ when "Read"
69
+ path = file_path
70
+ path ? "Read: #{path}" : "Read"
71
+ when "Write"
72
+ path = file_path
73
+ if path
74
+ size = content_size(input[:content])
75
+ "Write: #{path} (#{size})"
76
+ else
77
+ "Write"
78
+ end
79
+ when "Edit"
80
+ path = file_path
81
+ if path
82
+ old = input[:old_string]
83
+ lines = old ? old.count("\n") + 1 : 0
84
+ "Edit: #{path} replacing #{lines} line(s)"
85
+ else
86
+ "Edit"
87
+ end
88
+ when "Bash"
89
+ cmd = input[:command]
90
+ cmd ? "Bash: #{cmd}" : "Bash"
91
+ when "Grep"
92
+ pattern = input[:pattern]
93
+ path = input[:path]
94
+ glob = input[:glob]
95
+ parts = [ "Grep: #{pattern}" ]
96
+ parts << "in #{path}" if path
97
+ parts << "(#{glob})" if glob
98
+ parts.join(" ")
99
+ when "NotebookEdit"
100
+ path = file_path
101
+ path ? "NotebookEdit: #{path}" : "NotebookEdit"
102
+ else
103
+ "#{name}: #{input.inspect}"
104
+ end
105
+
106
+ truncate(text, max)
107
+ end
108
+
109
+ private
110
+
111
+ def shorten_path(path)
112
+ parts = path.to_s.split("/")
113
+ parts.length > 2 ? parts.last(2).join("/") : path.to_s
114
+ end
115
+
116
+ def truncate(str, max)
117
+ return str if str.length <= max
118
+ "#{str[0, max]}..."
119
+ end
120
+
121
+ def extract_host(url)
122
+ return nil if url.nil?
123
+ URI.parse(url.to_s).host
124
+ rescue URI::InvalidURIError
125
+ nil
126
+ end
127
+
128
+ def content_size(content)
129
+ return "empty" if content.nil? || content.empty?
130
+ lines = content.count("\n") + 1
131
+ lines == 1 ? "1 line" : "#{lines} lines"
132
+ end
133
+ end
134
+ end
@@ -1,342 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "uri"
3
+ require_relative "content_blocks/text_block"
4
+ require_relative "content_blocks/thinking_block"
5
+ require_relative "content_blocks/tool_use_block"
6
+ require_relative "content_blocks/tool_result_block"
7
+ require_relative "content_blocks/server_tool_use_block"
8
+ require_relative "content_blocks/server_tool_result_block"
9
+ require_relative "content_blocks/image_content_block"
10
+ require_relative "content_blocks/generic_block"
4
11
 
5
12
  module ClaudeAgent
6
- # Text content block
7
- #
8
- # @example
9
- # block = TextBlock.new(text: "Hello, world!")
10
- # block.text # => "Hello, world!"
11
- #
12
- TextBlock = Data.define(:text) do
13
- def type
14
- :text
15
- end
16
-
17
- def to_h
18
- { type: "text", text: text }
19
- end
20
- end
21
-
22
- # Extended thinking content block
23
- #
24
- # @example
25
- # block = ThinkingBlock.new(thinking: "Let me consider...", signature: "abc123")
26
- # block.thinking # => "Let me consider..."
27
- #
28
- ThinkingBlock = Data.define(:thinking, :signature) do
29
- def type
30
- :thinking
31
- end
32
-
33
- def to_h
34
- { type: "thinking", thinking: thinking, signature: signature }
35
- end
36
- end
37
-
38
- # Tool use request block
39
- #
40
- # @example
41
- # block = ToolUseBlock.new(id: "tool_123", name: "Read", input: {file_path: "/tmp/file"})
42
- # block.input[:file_path] # => "/tmp/file"
43
- # block.name # => "Read"
44
- #
45
- ToolUseBlock = Data.define(:id, :name, :input) do
46
- def type
47
- :tool_use
48
- end
49
-
50
- def to_h
51
- { type: "tool_use", id: id, name: name, input: input }
52
- end
53
-
54
- # Returns the file path for file-based tools, nil otherwise.
55
- # @return [String, nil]
56
- def file_path
57
- case name
58
- when "Read", "Write", "Edit"
59
- input[:file_path]
60
- when "NotebookEdit"
61
- input[:notebook_path]
62
- end
63
- end
64
-
65
- # One-line human-readable label for the tool call.
66
- # @return [String]
67
- def display_label
68
- case name
69
- when "Read", "Write", "Edit", "NotebookEdit"
70
- path = file_path
71
- path ? "#{name} #{shorten_path(path)}" : name
72
- when "Bash"
73
- cmd = input[:command]
74
- cmd ? "Bash: #{truncate(cmd, 50)}" : "Bash"
75
- when "Grep"
76
- pattern = input[:pattern]
77
- pattern ? "Grep: #{pattern}" : "Grep"
78
- when "Glob"
79
- pattern = input[:pattern]
80
- pattern ? "Glob: #{pattern}" : "Glob"
81
- when "WebFetch"
82
- host = extract_host(input[:url])
83
- host ? "WebFetch: #{host}" : "WebFetch"
84
- when "WebSearch"
85
- query = input[:query]
86
- query ? "WebSearch: #{truncate(query, 50)}" : "WebSearch"
87
- when "Task"
88
- desc = input[:description]
89
- desc ? "Task: #{truncate(desc, 50)}" : "Task"
90
- else
91
- name
92
- end
93
- end
94
-
95
- # Detailed summary of the tool call, truncated to max chars.
96
- # @param max [Integer] maximum length before truncation
97
- # @return [String]
98
- def summary(max: 60)
99
- text = case name
100
- when "Read"
101
- path = file_path
102
- path ? "Read: #{path}" : "Read"
103
- when "Write"
104
- path = file_path
105
- if path
106
- size = content_size(input[:content])
107
- "Write: #{path} (#{size})"
108
- else
109
- "Write"
110
- end
111
- when "Edit"
112
- path = file_path
113
- if path
114
- old = input[:old_string]
115
- lines = old ? old.count("\n") + 1 : 0
116
- "Edit: #{path} replacing #{lines} line(s)"
117
- else
118
- "Edit"
119
- end
120
- when "Bash"
121
- cmd = input[:command]
122
- cmd ? "Bash: #{cmd}" : "Bash"
123
- when "Grep"
124
- pattern = input[:pattern]
125
- path = input[:path]
126
- glob = input[:glob]
127
- parts = [ "Grep: #{pattern}" ]
128
- parts << "in #{path}" if path
129
- parts << "(#{glob})" if glob
130
- parts.join(" ")
131
- when "NotebookEdit"
132
- path = file_path
133
- path ? "NotebookEdit: #{path}" : "NotebookEdit"
134
- else
135
- "#{name}: #{input.inspect}"
136
- end
137
-
138
- truncate(text, max)
139
- end
140
-
141
- private
142
-
143
- def shorten_path(path)
144
- parts = path.to_s.split("/")
145
- parts.length > 2 ? parts.last(2).join("/") : path.to_s
146
- end
147
-
148
- def truncate(str, max)
149
- return str if str.length <= max
150
- "#{str[0, max]}..."
151
- end
152
-
153
- def extract_host(url)
154
- return nil if url.nil?
155
- URI.parse(url.to_s).host
156
- rescue URI::InvalidURIError
157
- nil
158
- end
159
-
160
- def content_size(content)
161
- return "empty" if content.nil? || content.empty?
162
- lines = content.count("\n") + 1
163
- lines == 1 ? "1 line" : "#{lines} lines"
164
- end
165
- end
166
-
167
- # Tool result block
168
- #
169
- # @example
170
- # block = ToolResultBlock.new(tool_use_id: "tool_123", content: "file contents", is_error: false)
171
- #
172
- ToolResultBlock = Data.define(:tool_use_id, :content, :is_error) do
173
- def initialize(tool_use_id:, content: nil, is_error: nil)
174
- super
175
- end
176
-
177
- def type
178
- :tool_result
179
- end
180
-
181
- def to_h
182
- h = { type: "tool_result", tool_use_id: tool_use_id }
183
- h[:content] = content unless content.nil?
184
- h[:is_error] = is_error unless is_error.nil?
185
- h
186
- end
187
- end
188
-
189
- # Server tool use block (for MCP servers)
190
- #
191
- ServerToolUseBlock = Data.define(:id, :name, :input, :server_name) do
192
- def type
193
- :server_tool_use
194
- end
195
-
196
- def to_h
197
- { type: "server_tool_use", id: id, name: name, input: input, server_name: server_name }
198
- end
199
-
200
- # Returns the file path for file-based tools, nil otherwise.
201
- # @return [String, nil]
202
- def file_path
203
- case name
204
- when "Read", "Write", "Edit"
205
- input[:file_path]
206
- when "NotebookEdit"
207
- input[:notebook_path]
208
- end
209
- end
210
-
211
- # One-line human-readable label with server context.
212
- # @return [String]
213
- def display_label
214
- server_name ? "#{server_name}/#{name}" : name
215
- end
216
-
217
- # Detailed summary with server context, truncated to max chars.
218
- # @param max [Integer] maximum length before truncation
219
- # @return [String]
220
- def summary(max: 60)
221
- label = display_label
222
- text = "#{label}: #{input.inspect}"
223
- truncate(text, max)
224
- end
225
-
226
- private
227
-
228
- def truncate(str, max)
229
- return str if str.length <= max
230
- "#{str[0, max]}..."
231
- end
232
- end
233
-
234
- # Server tool result block
235
- #
236
- ServerToolResultBlock = Data.define(:tool_use_id, :content, :is_error, :server_name) do
237
- def initialize(tool_use_id:, server_name:, content: nil, is_error: nil)
238
- super
239
- end
240
-
241
- def type
242
- :server_tool_result
243
- end
244
-
245
- def to_h
246
- h = { type: "server_tool_result", tool_use_id: tool_use_id, server_name: server_name }
247
- h[:content] = content unless content.nil?
248
- h[:is_error] = is_error unless is_error.nil?
249
- h
250
- end
251
- end
252
-
253
- # Image content block (TypeScript SDK parity)
254
- #
255
- # Supports both base64-encoded image data and URL sources.
256
- #
257
- # @example Base64 image
258
- # block = ImageContentBlock.new(
259
- # source: { type: "base64", media_type: "image/png", data: "..." }
260
- # )
261
- # block.source_type # => "base64"
262
- # block.media_type # => "image/png"
263
- #
264
- # @example URL image
265
- # block = ImageContentBlock.new(
266
- # source: { type: "url", url: "https://example.com/image.png" }
267
- # )
268
- # block.url # => "https://example.com/image.png"
269
- #
270
- ImageContentBlock = Data.define(:source) do
271
- def type
272
- :image
273
- end
274
-
275
- # Get the media type if available
276
- # @return [String, nil]
277
- def media_type
278
- source.is_a?(Hash) ? source[:media_type] : nil
279
- end
280
-
281
- # Get the base64 data if available
282
- # @return [String, nil]
283
- def data
284
- source.is_a?(Hash) ? source[:data] : nil
285
- end
286
-
287
- # Get the URL if this is a URL-sourced image
288
- # @return [String, nil]
289
- def url
290
- source.is_a?(Hash) ? source[:url] : nil
291
- end
292
-
293
- # Get the source type (base64 or url)
294
- # @return [String, nil]
295
- def source_type
296
- source.is_a?(Hash) ? source[:type] : nil
297
- end
298
-
299
- def to_h
300
- { type: "image", source: source }
301
- end
302
- end
303
-
304
- # Generic content block for unknown/future block types
305
- #
306
- # Wraps unrecognized content block types so they can be inspected
307
- # without losing type information. Supports dynamic field access via
308
- # `[]` and `method_missing`.
309
- #
310
- # @example
311
- # block = GenericBlock.new(block_type: "citation", raw: { text: "ref", url: "https://example.com" })
312
- # block.type # => :citation
313
- # block[:text] # => "ref"
314
- # block.url # => "https://example.com"
315
- # block.to_h # => { text: "ref", url: "https://example.com" }
316
- #
317
- GenericBlock = Data.define(:block_type, :raw) do
318
- def type
319
- block_type&.to_sym || :unknown
320
- end
321
-
322
- def to_h
323
- raw
324
- end
325
-
326
- def [](key)
327
- raw[key]
328
- end
329
-
330
- def respond_to_missing?(name, include_private = false)
331
- raw.key?(name) || super
332
- end
333
-
334
- def method_missing(name, *args)
335
- return raw[name] if args.empty? && raw.key?(name)
336
- super
337
- end
338
- end
339
-
340
13
  # All content block types
341
14
  CONTENT_BLOCK_TYPES = [
342
15
  TextBlock,