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
@@ -42,6 +42,7 @@ module ClaudeAgent
42
42
  status tool_progress hook_response auth_status task_notification
43
43
  hook_started hook_progress tool_use_summary task_started
44
44
  task_progress rate_limit_event prompt_suggestion files_persisted
45
+ elicitation_complete local_command_output
45
46
  ].freeze
46
47
 
47
48
  # Decomposed events — extracted content from rich message types
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeAgent
4
+ # Looks up a single session by ID and returns its metadata.
5
+ #
6
+ # Reuses ListSessions parsing logic to read session files from disk.
7
+ # Searches project directories for the session file by UUID.
8
+ #
9
+ # @example
10
+ # info = ClaudeAgent.get_session_info("abc-123-...")
11
+ # puts info.summary if info
12
+ #
13
+ module GetSessionInfo
14
+ module_function
15
+
16
+ # Look up a single session by ID.
17
+ #
18
+ # @param session_id [String] UUID of the session to look up
19
+ # @param dir [String, nil] Project directory to scope the search.
20
+ # When nil, searches all projects.
21
+ # @return [SessionInfo, nil] Session metadata or nil if not found
22
+ def call(session_id, dir: nil)
23
+ unless SessionPaths::UUID_PATTERN.match?(session_id.to_s)
24
+ raise ArgumentError, "Invalid session_id: #{session_id}. Must be a valid UUID."
25
+ end
26
+
27
+ filename = "#{session_id}.jsonl"
28
+
29
+ if dir
30
+ find_in_directory(session_id, filename, dir)
31
+ else
32
+ find_in_all_projects(session_id, filename)
33
+ end
34
+ end
35
+
36
+ # --- Private Helpers ---
37
+
38
+ def find_in_directory(session_id, filename, dir)
39
+ resolved = SessionPaths.realpath(dir)
40
+ project_dir = SessionPaths.find_project_dir(resolved)
41
+
42
+ if project_dir
43
+ result = try_parse(project_dir, filename, session_id)
44
+ return result if result
45
+ end
46
+
47
+ # Check worktrees
48
+ worktrees = SessionPaths.git_worktrees(resolved)
49
+ worktrees.each do |wt|
50
+ wt_project_dir = SessionPaths.find_project_dir(wt)
51
+ next unless wt_project_dir
52
+
53
+ result = try_parse(wt_project_dir, filename, session_id)
54
+ return result if result
55
+ end
56
+
57
+ nil
58
+ end
59
+ private_class_method :find_in_directory
60
+
61
+ def find_in_all_projects(session_id, filename)
62
+ base = SessionPaths.projects_dir
63
+ return nil unless File.directory?(base)
64
+
65
+ Dir.entries(base).each do |entry|
66
+ next if entry.start_with?(".")
67
+ dir_path = File.join(base, entry)
68
+ next unless File.directory?(dir_path)
69
+
70
+ result = try_parse(dir_path, filename, session_id)
71
+ return result if result
72
+ end
73
+
74
+ nil
75
+ end
76
+ private_class_method :find_in_all_projects
77
+
78
+ def try_parse(project_dir, filename, session_id)
79
+ path = File.join(project_dir, filename)
80
+ return nil unless File.exist?(path)
81
+
82
+ ListSessions.send(:parse_session_file, path, session_id)
83
+ end
84
+ private_class_method :try_parse
85
+ end
86
+ end
@@ -18,9 +18,12 @@ module ClaudeAgent
18
18
  Setup
19
19
  TeammateIdle
20
20
  TaskCompleted
21
+ Elicitation
22
+ ElicitationResult
21
23
  ConfigChange
22
24
  WorktreeCreate
23
25
  WorktreeRemove
26
+ InstructionsLoaded
24
27
  ].freeze
25
28
 
26
29
  # Matcher configuration for hooks
@@ -71,14 +74,16 @@ module ClaudeAgent
71
74
  # hook_event_name/base field inheritance.
72
75
  #
73
76
  class BaseHookInput
74
- attr_reader :hook_event_name, :session_id, :transcript_path, :cwd, :permission_mode
77
+ attr_reader :hook_event_name, :session_id, :transcript_path, :cwd, :permission_mode, :agent_id, :agent_type
75
78
 
76
- def initialize(hook_event_name:, session_id: nil, transcript_path: nil, cwd: nil, permission_mode: nil, **kwargs)
79
+ def initialize(hook_event_name:, session_id: nil, transcript_path: nil, cwd: nil, permission_mode: nil, agent_id: nil, agent_type: nil, **kwargs)
77
80
  @hook_event_name = hook_event_name
78
81
  @session_id = session_id
79
82
  @transcript_path = transcript_path
80
83
  @cwd = cwd
81
84
  @permission_mode = permission_mode
85
+ @agent_id = agent_id
86
+ @agent_type = agent_type
82
87
  end
83
88
 
84
89
  # Define a hook input subclass declaratively
@@ -196,6 +201,14 @@ module ClaudeAgent
196
201
  required: [ :task_id, :task_subject ],
197
202
  optional: { task_description: nil, teammate_name: nil, team_name: nil }
198
203
 
204
+ BaseHookInput.define_input "Elicitation",
205
+ required: [ :mcp_server_name, :message ],
206
+ optional: { mode: nil, url: nil, elicitation_id: nil, requested_schema: nil }
207
+
208
+ BaseHookInput.define_input "ElicitationResult",
209
+ required: [ :mcp_server_name, :action ],
210
+ optional: { elicitation_id: nil, mode: nil, content: nil }
211
+
199
212
  BaseHookInput.define_input "ConfigChange",
200
213
  required: [ :source ],
201
214
  optional: { file_path: nil },
@@ -206,4 +219,12 @@ module ClaudeAgent
206
219
 
207
220
  BaseHookInput.define_input "WorktreeRemove",
208
221
  required: [ :worktree_path ]
222
+
223
+ BaseHookInput.define_input "InstructionsLoaded",
224
+ required: [ :file_path, :memory_type, :load_reason ],
225
+ optional: { globs: nil, trigger_file_path: nil, parent_file_path: nil },
226
+ constants: {
227
+ MEMORY_TYPES: %w[User Project Local Managed].freeze,
228
+ LOAD_REASONS: %w[session_start nested_traversal path_glob_match include].freeze
229
+ }
209
230
  end
@@ -31,12 +31,14 @@ module ClaudeAgent
31
31
  # @param dir [String, nil] Directory to scope sessions to (with worktree support).
32
32
  # When nil, returns sessions from all projects.
33
33
  # @param limit [Integer, nil] Maximum number of sessions to return.
34
+ # @param include_worktrees [Boolean] When dir is in a git repo, include sessions
35
+ # from all git worktree paths. Defaults to true.
34
36
  # @return [Array<SessionInfo>] Sessions sorted by last_modified descending.
35
- def call(dir: nil, limit: nil)
37
+ def call(dir: nil, limit: nil, offset: nil, include_worktrees: true)
36
38
  if dir
37
- list_for_directory(dir, limit)
39
+ list_for_directory(dir, limit, offset: offset, include_worktrees: include_worktrees)
38
40
  else
39
- list_all(limit)
41
+ list_all(limit, offset: offset)
40
42
  end
41
43
  end
42
44
 
@@ -243,6 +245,9 @@ module ClaudeAgent
243
245
  first_prompt = extract_first_prompt(head)
244
246
  git_branch = extract_last(tail, "gitBranch") || extract_first(head, "gitBranch")
245
247
  cwd = extract_first(head, "cwd")
248
+ tag = extract_last(tail, "tag")
249
+ timestamp_str = extract_first(head, "timestamp")
250
+ created_at = timestamp_str&.to_i
246
251
 
247
252
  # Build summary: customTitle > last summary > firstPrompt > "(session)"
248
253
  summary_from_file = extract_last(tail, "summary")
@@ -256,7 +261,9 @@ module ClaudeAgent
256
261
  custom_title: custom_title,
257
262
  first_prompt: first_prompt,
258
263
  git_branch: git_branch,
259
- cwd: cwd
264
+ cwd: cwd,
265
+ tag: tag,
266
+ created_at: created_at
260
267
  )
261
268
  end
262
269
 
@@ -288,21 +295,22 @@ module ClaudeAgent
288
295
 
289
296
  # --- Listing Modes ---
290
297
 
291
- # List sessions for a specific directory, including worktree siblings.
298
+ # List sessions for a specific directory, optionally including worktree siblings.
292
299
  # Matches TypeScript's bM function.
293
300
  #
294
301
  # @param dir [String]
295
302
  # @param limit [Integer, nil]
303
+ # @param include_worktrees [Boolean] Whether to include worktree sessions
296
304
  # @return [Array<SessionInfo>]
297
- def list_for_directory(dir, limit)
305
+ def list_for_directory(dir, limit, offset: nil, include_worktrees: true)
298
306
  resolved = SessionPaths.realpath(dir)
299
- worktrees = SessionPaths.git_worktrees(resolved)
307
+ worktrees = include_worktrees ? SessionPaths.git_worktrees(resolved) : []
300
308
 
301
- # Simple case: not in a worktree (or single worktree)
309
+ # Simple case: not in a worktree (or single worktree) or worktrees disabled
302
310
  if worktrees.length <= 1
303
311
  project_dir = SessionPaths.find_project_dir(resolved)
304
312
  return [] unless project_dir
305
- return sort_and_limit(scan_project_dir(project_dir), limit)
313
+ return sort_and_limit(scan_project_dir(project_dir), limit, offset: offset)
306
314
  end
307
315
 
308
316
  # Complex case: multiple worktrees - scan all related project directories
@@ -341,7 +349,7 @@ module ClaudeAgent
341
349
  end
342
350
  end
343
351
 
344
- sort_and_limit(deduplicate(all_sessions), limit)
352
+ sort_and_limit(deduplicate(all_sessions), limit, offset: offset)
345
353
  end
346
354
 
347
355
  # List sessions from all project directories.
@@ -349,7 +357,7 @@ module ClaudeAgent
349
357
  #
350
358
  # @param limit [Integer, nil]
351
359
  # @return [Array<SessionInfo>]
352
- def list_all(limit)
360
+ def list_all(limit, offset: nil)
353
361
  base = SessionPaths.projects_dir
354
362
  return [] unless File.directory?(base)
355
363
 
@@ -362,7 +370,7 @@ module ClaudeAgent
362
370
  all_sessions.concat(scan_project_dir(dir_path))
363
371
  end
364
372
 
365
- sort_and_limit(deduplicate(all_sessions), limit)
373
+ sort_and_limit(deduplicate(all_sessions), limit, offset: offset)
366
374
  end
367
375
 
368
376
  # --- Helpers ---
@@ -389,8 +397,9 @@ module ClaudeAgent
389
397
  # @param sessions [Array<SessionInfo>]
390
398
  # @param limit [Integer, nil]
391
399
  # @return [Array<SessionInfo>]
392
- def sort_and_limit(sessions, limit)
400
+ def sort_and_limit(sessions, limit, offset: nil)
393
401
  sorted = sessions.sort_by { |s| -s.last_modified }
402
+ sorted = sorted.drop(offset) if offset
394
403
  limit ? sorted.first(limit) : sorted
395
404
  end
396
405
  end
@@ -83,7 +83,9 @@ module ClaudeAgent
83
83
  register "system:hook_progress", :parse_hook_progress_message
84
84
  register "system:files_persisted", :parse_files_persisted_event
85
85
  register "system:task_started", :parse_task_started_message
86
- register "system:task_progress", :parse_task_progress_message
86
+ register "system:task_progress", :parse_task_progress_message
87
+ register "system:elicitation_complete", :parse_elicitation_complete_message
88
+ register "system:local_command_output", :parse_local_command_output_message
87
89
 
88
90
  private
89
91
 
@@ -167,7 +169,8 @@ module ClaudeAgent
167
169
  errors: raw[:errors],
168
170
  permission_denials: permission_denials,
169
171
  model_usage: raw[:model_usage],
170
- stop_reason: raw[:stop_reason]
172
+ stop_reason: raw[:stop_reason],
173
+ fast_mode_state: raw[:fast_mode_state]
171
174
  )
172
175
  end
173
176
 
@@ -368,7 +371,8 @@ module ClaudeAgent
368
371
  task_id: raw[:task_id] || "",
369
372
  tool_use_id: raw[:tool_use_id],
370
373
  description: raw[:description],
371
- task_type: raw[:task_type]
374
+ task_type: raw[:task_type],
375
+ prompt: raw[:prompt]
372
376
  )
373
377
  end
374
378
 
@@ -380,7 +384,25 @@ module ClaudeAgent
380
384
  tool_use_id: raw[:tool_use_id],
381
385
  description: raw[:description] || "",
382
386
  usage: raw[:usage],
383
- last_tool_name: raw[:last_tool_name]
387
+ last_tool_name: raw[:last_tool_name],
388
+ summary: raw[:summary]
389
+ )
390
+ end
391
+
392
+ def parse_elicitation_complete_message(raw)
393
+ ElicitationCompleteMessage.new(
394
+ uuid: raw[:uuid] || "",
395
+ session_id: raw[:session_id] || "",
396
+ mcp_server_name: raw[:mcp_server_name] || "",
397
+ elicitation_id: raw[:elicitation_id] || ""
398
+ )
399
+ end
400
+
401
+ def parse_local_command_output_message(raw)
402
+ LocalCommandOutputMessage.new(
403
+ uuid: raw[:uuid] || "",
404
+ session_id: raw[:session_id] || "",
405
+ content: raw[:content] || ""
384
406
  )
385
407
  end
386
408
 
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeAgent
4
+ # User message sent to Claude
5
+ #
6
+ # @example
7
+ # msg = UserMessage.new(content: "Hello!", uuid: "abc-123", session_id: "session-abc")
8
+ #
9
+ UserMessage = Data.define(:content, :uuid, :session_id, :parent_tool_use_id) do
10
+ def initialize(content:, uuid: nil, session_id: nil, parent_tool_use_id: nil)
11
+ super
12
+ end
13
+
14
+ def type
15
+ :user
16
+ end
17
+
18
+ # Get text content if content is a string
19
+ # @return [String, nil]
20
+ def text
21
+ content.is_a?(String) ? content : nil
22
+ end
23
+
24
+ # Check if this is a replayed message
25
+ # @return [Boolean]
26
+ def replay?
27
+ false
28
+ end
29
+ end
30
+
31
+ # User message replay (TypeScript SDK parity)
32
+ #
33
+ # Sent when resuming a session with existing conversation history.
34
+ # These messages represent replayed user messages from a previous session.
35
+ #
36
+ # @example
37
+ # msg = UserMessageReplay.new(
38
+ # content: "Hello!",
39
+ # uuid: "abc-123",
40
+ # session_id: "session-abc",
41
+ # is_replay: true
42
+ # )
43
+ # msg.replay? # => true
44
+ #
45
+ UserMessageReplay = Data.define(
46
+ :content,
47
+ :uuid,
48
+ :session_id,
49
+ :parent_tool_use_id,
50
+ :is_replay,
51
+ :is_synthetic,
52
+ :tool_use_result
53
+ ) do
54
+ def initialize(
55
+ content:,
56
+ uuid: nil,
57
+ session_id: nil,
58
+ parent_tool_use_id: nil,
59
+ is_replay: true,
60
+ is_synthetic: nil,
61
+ tool_use_result: nil
62
+ )
63
+ super
64
+ end
65
+
66
+ def type
67
+ :user
68
+ end
69
+
70
+ # Get text content if content is a string
71
+ # @return [String, nil]
72
+ def text
73
+ content.is_a?(String) ? content : nil
74
+ end
75
+
76
+ # Check if this is a replayed message
77
+ # @return [Boolean]
78
+ def replay?
79
+ is_replay == true
80
+ end
81
+
82
+ # Check if this is a synthetic message (system-generated)
83
+ # @return [Boolean]
84
+ def synthetic?
85
+ is_synthetic == true
86
+ end
87
+ end
88
+
89
+ # Assistant message from Claude
90
+ #
91
+ # @example
92
+ # msg = AssistantMessage.new(
93
+ # content: [TextBlock.new(text: "Hello!")],
94
+ # model: "claude-sonnet-4-5-20250514",
95
+ # uuid: "msg-123",
96
+ # session_id: "session-abc"
97
+ # )
98
+ #
99
+ AssistantMessage = Data.define(:content, :model, :uuid, :session_id, :error, :parent_tool_use_id) do
100
+ def initialize(content:, model:, uuid: nil, session_id: nil, error: nil, parent_tool_use_id: nil)
101
+ super
102
+ end
103
+
104
+ def type
105
+ :assistant
106
+ end
107
+
108
+ # Get all text content concatenated
109
+ # @return [String]
110
+ def text
111
+ content
112
+ .select { |block| block.is_a?(TextBlock) }
113
+ .map(&:text)
114
+ .join
115
+ end
116
+
117
+ # Get all thinking content concatenated
118
+ # @return [String]
119
+ def thinking
120
+ content
121
+ .select { |block| block.is_a?(ThinkingBlock) }
122
+ .map(&:thinking)
123
+ .join
124
+ end
125
+
126
+ # Get all tool use blocks
127
+ # @return [Array<ToolUseBlock>]
128
+ def tool_uses
129
+ content.select { |block| block.is_a?(ToolUseBlock) }
130
+ end
131
+
132
+ # Check if assistant wants to use a tool
133
+ # @return [Boolean]
134
+ def has_tool_use?
135
+ content.any? { |block| block.is_a?(ToolUseBlock) }
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeAgent
4
+ # Generic message for unknown/future protocol types
5
+ #
6
+ # Wraps unrecognized top-level message types so they can be inspected
7
+ # without crashing the application. Supports dynamic field access via
8
+ # `[]` and `method_missing`.
9
+ #
10
+ # @example
11
+ # msg = GenericMessage.new(message_type: "fancy_new", raw: { data: "hello" })
12
+ # msg.type # => :fancy_new
13
+ # msg[:data] # => "hello"
14
+ # msg.data # => "hello"
15
+ # msg.to_h # => { data: "hello" }
16
+ #
17
+ GenericMessage = Data.define(:message_type, :raw) do
18
+ def type
19
+ message_type&.to_sym || :unknown
20
+ end
21
+
22
+ def to_h
23
+ raw
24
+ end
25
+
26
+ def [](key)
27
+ raw[key]
28
+ end
29
+
30
+ def respond_to_missing?(name, include_private = false)
31
+ raw.key?(name) || super
32
+ end
33
+
34
+ def method_missing(name, *args)
35
+ return raw[name] if args.empty? && raw.key?(name)
36
+ super
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeAgent
4
+ # Hook started message (TypeScript SDK parity)
5
+ #
6
+ # Sent when a hook execution starts.
7
+ #
8
+ # @example
9
+ # msg = HookStartedMessage.new(
10
+ # uuid: "msg-123",
11
+ # session_id: "session-abc",
12
+ # hook_id: "hook-456",
13
+ # hook_name: "my-hook",
14
+ # hook_event: "PreToolUse"
15
+ # )
16
+ #
17
+ HookStartedMessage = Data.define(
18
+ :uuid,
19
+ :session_id,
20
+ :hook_id,
21
+ :hook_name,
22
+ :hook_event
23
+ ) do
24
+ def initialize(
25
+ uuid:,
26
+ session_id:,
27
+ hook_id:,
28
+ hook_name:,
29
+ hook_event:
30
+ )
31
+ super
32
+ end
33
+
34
+ def type
35
+ :hook_started
36
+ end
37
+ end
38
+
39
+ # Hook progress message (TypeScript SDK parity)
40
+ #
41
+ # Reports progress during hook execution.
42
+ #
43
+ # @example
44
+ # msg = HookProgressMessage.new(
45
+ # uuid: "msg-123",
46
+ # session_id: "session-abc",
47
+ # hook_id: "hook-456",
48
+ # hook_name: "my-hook",
49
+ # hook_event: "PreToolUse",
50
+ # stdout: "Hook output so far...",
51
+ # stderr: "",
52
+ # output: "Combined output"
53
+ # )
54
+ #
55
+ HookProgressMessage = Data.define(
56
+ :uuid,
57
+ :session_id,
58
+ :hook_id,
59
+ :hook_name,
60
+ :hook_event,
61
+ :stdout,
62
+ :stderr,
63
+ :output
64
+ ) do
65
+ def initialize(
66
+ uuid:,
67
+ session_id:,
68
+ hook_id:,
69
+ hook_name:,
70
+ hook_event:,
71
+ stdout: "",
72
+ stderr: "",
73
+ output: ""
74
+ )
75
+ super
76
+ end
77
+
78
+ def type
79
+ :hook_progress
80
+ end
81
+ end
82
+
83
+ # Hook response message (TypeScript SDK parity)
84
+ #
85
+ # Contains output from hook executions.
86
+ #
87
+ # @example
88
+ # msg = HookResponseMessage.new(
89
+ # uuid: "msg-123",
90
+ # session_id: "session-abc",
91
+ # hook_id: "hook-456",
92
+ # hook_name: "my-hook",
93
+ # hook_event: "PreToolUse",
94
+ # stdout: "Hook output",
95
+ # stderr: "",
96
+ # output: "Combined output",
97
+ # exit_code: 0,
98
+ # outcome: "success"
99
+ # )
100
+ # msg.success? # => true
101
+ # msg.error? # => false
102
+ # msg.cancelled? # => false
103
+ #
104
+ # Outcome values:
105
+ # - "success" - Hook completed successfully
106
+ # - "error" - Hook encountered an error
107
+ # - "cancelled" - Hook was cancelled
108
+ #
109
+ HookResponseMessage = Data.define(
110
+ :uuid,
111
+ :session_id,
112
+ :hook_id,
113
+ :hook_name,
114
+ :hook_event,
115
+ :stdout,
116
+ :stderr,
117
+ :output,
118
+ :exit_code,
119
+ :outcome
120
+ ) do
121
+ def initialize(
122
+ uuid:,
123
+ session_id:,
124
+ hook_id: nil,
125
+ hook_name:,
126
+ hook_event:,
127
+ stdout: "",
128
+ stderr: "",
129
+ output: "",
130
+ exit_code: nil,
131
+ outcome: nil
132
+ )
133
+ super
134
+ end
135
+
136
+ def type
137
+ :hook_response
138
+ end
139
+
140
+ # Check if hook completed successfully
141
+ # @return [Boolean]
142
+ def success?
143
+ outcome == "success"
144
+ end
145
+
146
+ # Check if hook encountered an error
147
+ # @return [Boolean]
148
+ def error?
149
+ outcome == "error"
150
+ end
151
+
152
+ # Check if hook was cancelled
153
+ # @return [Boolean]
154
+ def cancelled?
155
+ outcome == "cancelled"
156
+ end
157
+ end
158
+ end