claude_agent 0.7.14 → 0.7.16

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/rules/conventions.md +66 -16
  3. data/CHANGELOG.md +20 -0
  4. data/CLAUDE.md +24 -4
  5. data/README.md +52 -1529
  6. data/SPEC.md +56 -29
  7. data/docs/architecture.md +339 -0
  8. data/docs/client.md +526 -0
  9. data/docs/configuration.md +571 -0
  10. data/docs/conversations.md +461 -0
  11. data/docs/errors.md +127 -0
  12. data/docs/events.md +225 -0
  13. data/docs/getting-started.md +310 -0
  14. data/docs/hooks.md +380 -0
  15. data/docs/logging.md +96 -0
  16. data/docs/mcp.md +308 -0
  17. data/docs/messages.md +871 -0
  18. data/docs/permissions.md +611 -0
  19. data/docs/queries.md +227 -0
  20. data/docs/sessions.md +335 -0
  21. data/lib/claude_agent/abort_controller.rb +24 -0
  22. data/lib/claude_agent/client/commands.rb +32 -0
  23. data/lib/claude_agent/client.rb +10 -4
  24. data/lib/claude_agent/configuration.rb +129 -0
  25. data/lib/claude_agent/control_protocol/commands.rb +28 -0
  26. data/lib/claude_agent/conversation.rb +37 -4
  27. data/lib/claude_agent/errors.rb +21 -4
  28. data/lib/claude_agent/event_handler.rb +14 -0
  29. data/lib/claude_agent/fork_session.rb +117 -0
  30. data/lib/claude_agent/hook_registry.rb +110 -0
  31. data/lib/claude_agent/hooks.rb +4 -0
  32. data/lib/claude_agent/mcp/server.rb +22 -0
  33. data/lib/claude_agent/mcp/tool.rb +24 -3
  34. data/lib/claude_agent/message.rb +93 -0
  35. data/lib/claude_agent/messages/streaming.rb +37 -0
  36. data/lib/claude_agent/options.rb +10 -0
  37. data/lib/claude_agent/permission_policy.rb +174 -0
  38. data/lib/claude_agent/permission_request.rb +17 -0
  39. data/lib/claude_agent/session.rb +100 -11
  40. data/lib/claude_agent/session_paths.rb +5 -2
  41. data/lib/claude_agent/turn_result.rb +20 -2
  42. data/lib/claude_agent/types/sessions.rb +8 -0
  43. data/lib/claude_agent/version.rb +1 -1
  44. data/lib/claude_agent.rb +187 -0
  45. data/sig/claude_agent.rbs +38 -1
  46. metadata +20 -1
data/docs/client.md ADDED
@@ -0,0 +1,526 @@
1
+ # Client
2
+
3
+ The `Client` class provides fine-grained, bidirectional control over a persistent Claude Code CLI connection. It supports multi-turn conversations, streaming, interrupts, dynamic model and permission changes, file checkpointing, and MCP server management.
4
+
5
+ > **Most users should prefer the higher-level APIs.** `ClaudeAgent.ask` handles one-shot queries with global configuration. `ClaudeAgent.chat` and `Conversation` manage multi-turn state, auto-connection, event callbacks, tool activity tracking, and cleanup. Reach for `Client` only when you need direct control over connection lifecycle, split send/receive, or protocol-level commands that `Conversation` does not expose.
6
+
7
+ ## Overview
8
+
9
+ `Client` wraps a `ControlProtocol` over a `Transport::Subprocess`, giving you:
10
+
11
+ - Persistent connection with explicit connect/disconnect
12
+ - Multiple conversation turns over a single CLI process
13
+ - Streaming message delivery via enumerators or blocks
14
+ - Typed event handlers that persist across turns
15
+ - Control commands: model switching, permission changes, file rewind, MCP management
16
+ - Abort/interrupt support with partial result recovery
17
+ - Cumulative usage tracking across all turns
18
+ - Asynchronous permission queue for UI-driven approval flows
19
+
20
+ ## Creating and Connecting
21
+
22
+ ### Constructor
23
+
24
+ ```ruby
25
+ client = ClaudeAgent::Client.new(
26
+ options: ClaudeAgent::Options.new(model: "opus", max_turns: 10),
27
+ transport: nil # defaults to Transport::Subprocess
28
+ )
29
+ ```
30
+
31
+ Both parameters are optional. When omitted, `options` defaults to a bare `Options.new` and `transport` defaults to a `Transport::Subprocess` built from those options.
32
+
33
+ ### Connecting
34
+
35
+ Call `connect` to start the CLI subprocess and perform the protocol handshake. An optional `prompt` sends an initial message immediately after connection.
36
+
37
+ ```ruby
38
+ client.connect
39
+ client.connect(prompt: "You are a helpful coding assistant")
40
+ ```
41
+
42
+ Raises `CLIConnectionError` if the client is already connected.
43
+
44
+ ### Block form
45
+
46
+ `Client.open` connects, yields the client, and guarantees disconnection:
47
+
48
+ ```ruby
49
+ ClaudeAgent::Client.open(
50
+ options: ClaudeAgent::Options.new(model: "opus"),
51
+ prompt: "Hello"
52
+ ) do |client|
53
+ client.send_message("Fix the bug")
54
+ client.receive_response.each { |msg| puts msg }
55
+ end
56
+ # client is automatically disconnected here
57
+ ```
58
+
59
+ ## Sending and Receiving
60
+
61
+ ### send_and_receive
62
+
63
+ The primary method for a complete turn. Sends a message and blocks until a `ResultMessage` arrives, accumulating everything into a `TurnResult`. Dispatches registered event handlers as messages flow through.
64
+
65
+ ```ruby
66
+ turn = client.send_and_receive("Fix the bug in auth.rb")
67
+ puts turn.text
68
+ puts "Cost: $#{turn.cost}"
69
+ puts "Tools used: #{turn.tool_uses.map(&:display_label).join(", ")}"
70
+ ```
71
+
72
+ With a streaming block:
73
+
74
+ ```ruby
75
+ turn = client.send_and_receive("Fix the bug") do |msg|
76
+ case msg
77
+ when ClaudeAgent::AssistantMessage
78
+ print msg.text
79
+ end
80
+ end
81
+ ```
82
+
83
+ Parameters:
84
+
85
+ | Parameter | Type | Default | Description |
86
+ |---------------|-------------------|-------------|--------------------------------------|
87
+ | `content` | `String`, `Array` | required | Message content |
88
+ | `session_id:` | `String` | `"default"` | Session ID for multi-session support |
89
+ | `uuid:` | `String`, `nil` | `nil` | Message UUID for file checkpointing |
90
+
91
+ Returns a `TurnResult`. See the [Queries](queries.md) doc for `TurnResult` accessors.
92
+
93
+ ### Split send/receive
94
+
95
+ For finer control, separate the send and receive steps.
96
+
97
+ **send_message** queues a message to the CLI without waiting for a response:
98
+
99
+ ```ruby
100
+ client.send_message("Hello")
101
+ client.send_message("Follow up", session_id: "session-2", uuid: "msg-uuid-1")
102
+ ```
103
+
104
+ **query** is an alias for `send_message`:
105
+
106
+ ```ruby
107
+ client.query("Hello")
108
+ ```
109
+
110
+ **receive_turn** blocks until a `ResultMessage` arrives, returning a `TurnResult`. It dispatches event handlers and resets turn-level handler state afterward.
111
+
112
+ ```ruby
113
+ client.send_message("Fix the bug")
114
+ turn = client.receive_turn
115
+ puts turn.text
116
+ ```
117
+
118
+ With a block:
119
+
120
+ ```ruby
121
+ turn = client.receive_turn do |msg|
122
+ print msg.text if msg.is_a?(ClaudeAgent::AssistantMessage)
123
+ end
124
+ ```
125
+
126
+ **receive_response** returns an `Enumerator` of messages for the current turn (until a `ResultMessage`). No event dispatch or `TurnResult` accumulation -- you process each message yourself.
127
+
128
+ ```ruby
129
+ client.send_message("Hello")
130
+ client.receive_response.each do |msg|
131
+ case msg
132
+ when ClaudeAgent::AssistantMessage
133
+ print msg.text
134
+ when ClaudeAgent::ResultMessage
135
+ puts "\nDone: #{msg.total_cost_usd}"
136
+ end
137
+ end
138
+ ```
139
+
140
+ **receive_messages** returns an `Enumerator` over all messages until the connection closes (not just one turn):
141
+
142
+ ```ruby
143
+ client.receive_messages.each { |msg| handle(msg) }
144
+ ```
145
+
146
+ ## Event Handlers
147
+
148
+ Register typed callbacks that fire automatically during `receive_turn` and `send_and_receive`. Handlers persist across turns -- register once, and they fire on every subsequent turn.
149
+
150
+ ### Registering handlers
151
+
152
+ Use `on` with a symbol, or the `on_*` convenience methods:
153
+
154
+ ```ruby
155
+ client.on(:text) { |text| print text }
156
+ client.on(:tool_use) { |tool| puts "Using: #{tool.display_label}" }
157
+ client.on(:result) { |result| puts "Cost: $#{result.total_cost_usd}" }
158
+
159
+ # Equivalent convenience methods:
160
+ client.on_text { |text| print text }
161
+ client.on_tool_use { |tool| puts "Using: #{tool.display_label}" }
162
+ client.on_result { |result| puts "Cost: $#{result.total_cost_usd}" }
163
+ ```
164
+
165
+ ### Chaining
166
+
167
+ All registration methods return `self`, so they chain:
168
+
169
+ ```ruby
170
+ client
171
+ .on(:text) { |text| print text }
172
+ .on(:tool_use) { |tool| show_spinner(tool) }
173
+ .on(:result) { |r| puts "\nDone!" }
174
+ ```
175
+
176
+ ### Event hierarchy
177
+
178
+ Events fire in three layers for each message:
179
+
180
+ 1. **Catch-all** -- `:message` fires for every message
181
+ 2. **Type-based** -- the message's type fires (e.g., `:assistant`, `:stream_event`, `:status`)
182
+ 3. **Decomposed** -- convenience events extracted from rich content types
183
+
184
+ **Decomposed events:**
185
+
186
+ | Event | Argument | Source |
187
+ |----------------|--------------------------------------------|--------------------------------------------------------------|
188
+ | `:text` | `String` | Text from `AssistantMessage` |
189
+ | `:thinking` | `String` | Thinking from `AssistantMessage` |
190
+ | `:tool_use` | `ToolUseBlock` or `ServerToolUseBlock` | Tool call from `AssistantMessage` |
191
+ | `:tool_result` | `ToolResultBlock`, `ToolUseBlock` or `nil` | Tool result from `UserMessage`, paired with original request |
192
+
193
+ **Type-based events:**
194
+
195
+ `:user`, `:assistant`, `:system`, `:result`, `:stream_event`, `:compact_boundary`, `:status`, `:tool_progress`, `:hook_response`, `:auth_status`, `:task_notification`, `:hook_started`, `:hook_progress`, `:tool_use_summary`, `:task_started`, `:task_progress`, `:rate_limit_event`, `:prompt_suggestion`, `:files_persisted`, `:elicitation_complete`, `:local_command_output`
196
+
197
+ ## Control Methods
198
+
199
+ These methods send control commands to the running CLI process. All require an active connection.
200
+
201
+ ### set_model
202
+
203
+ Change the model for subsequent turns:
204
+
205
+ ```ruby
206
+ client.set_model("claude-sonnet-4-5-20250514")
207
+ client.set_model(nil) # revert to default
208
+ ```
209
+
210
+ ### set_permission_mode
211
+
212
+ Change the permission mode:
213
+
214
+ ```ruby
215
+ client.set_permission_mode("acceptEdits")
216
+ ```
217
+
218
+ ### set_max_thinking_tokens
219
+
220
+ Set or reset the maximum thinking tokens:
221
+
222
+ ```ruby
223
+ client.set_max_thinking_tokens(10_000)
224
+ client.set_max_thinking_tokens(nil) # reset to default
225
+ ```
226
+
227
+ ### stop_task
228
+
229
+ Stop a running background task by its ID (from `task_notification` events):
230
+
231
+ ```ruby
232
+ client.stop_task("task-123")
233
+ ```
234
+
235
+ ### apply_flag_settings
236
+
237
+ Merge settings into the flag settings layer:
238
+
239
+ ```ruby
240
+ client.apply_flag_settings({ "model" => "claude-sonnet-4-5-20250514" })
241
+ ```
242
+
243
+ ## File Checkpointing
244
+
245
+ When `enable_file_checkpointing: true` is set in Options and UUIDs are passed with messages, you can rewind files to the state at a specific user message.
246
+
247
+ ### rewind_files
248
+
249
+ ```ruby
250
+ result = client.rewind_files("user-message-uuid")
251
+ result.can_rewind # => true
252
+ result.files_changed # => ["src/foo.rb", "src/bar.rb"]
253
+ result.insertions # => 10
254
+ result.deletions # => 5
255
+ result.error # => nil
256
+ ```
257
+
258
+ Dry-run mode previews changes without modifying files:
259
+
260
+ ```ruby
261
+ result = client.rewind_files("user-message-uuid", dry_run: true)
262
+ ```
263
+
264
+ Returns a `RewindFilesResult` with fields: `can_rewind`, `error`, `files_changed`, `insertions`, `deletions`.
265
+
266
+ ## Dynamic MCP Management
267
+
268
+ Manage MCP (Model Context Protocol) servers on a live connection without restarting.
269
+
270
+ ### set_mcp_servers
271
+
272
+ Replace the set of dynamically-added MCP servers. New servers are connected; removed servers are disconnected.
273
+
274
+ ```ruby
275
+ result = client.set_mcp_servers({
276
+ "my-server" => { type: "stdio", command: "node", args: ["server.js"] }
277
+ })
278
+ result.added # => ["my-server"]
279
+ result.removed # => ["old-server"]
280
+ result.errors # => {} or {"server2" => "Connection failed"}
281
+ ```
282
+
283
+ Returns a `McpSetServersResult` with fields: `added`, `removed`, `errors`.
284
+
285
+ ### mcp_reconnect
286
+
287
+ Reconnect to a disconnected or errored MCP server:
288
+
289
+ ```ruby
290
+ client.mcp_reconnect("my-server")
291
+ ```
292
+
293
+ ### mcp_toggle
294
+
295
+ Enable or disable a server without removing its configuration:
296
+
297
+ ```ruby
298
+ client.mcp_toggle("my-server", enabled: false)
299
+ client.mcp_toggle("my-server", enabled: true)
300
+ ```
301
+
302
+ ### mcp_authenticate
303
+
304
+ Initiate OAuth authentication for a remote MCP server:
305
+
306
+ ```ruby
307
+ client.mcp_authenticate("my-remote-server")
308
+ ```
309
+
310
+ ### mcp_clear_auth
311
+
312
+ Clear stored authentication credentials for an MCP server:
313
+
314
+ ```ruby
315
+ client.mcp_clear_auth("my-remote-server")
316
+ ```
317
+
318
+ ## Query Capabilities
319
+
320
+ Query the CLI for available resources.
321
+
322
+ ### supported_commands
323
+
324
+ ```ruby
325
+ commands = client.supported_commands
326
+ # => [#<SlashCommand name="commit" description="Create a commit" argument_hint="[message]">, ...]
327
+ ```
328
+
329
+ Returns `Array<SlashCommand>`.
330
+
331
+ ### supported_models
332
+
333
+ ```ruby
334
+ models = client.supported_models
335
+ models.each { |m| puts "#{m.value}: #{m.display_name}" }
336
+ ```
337
+
338
+ Returns `Array<ModelInfo>`.
339
+
340
+ ### supported_agents
341
+
342
+ ```ruby
343
+ agents = client.supported_agents
344
+ agents.each { |a| puts "#{a.name}: #{a.description}" }
345
+ ```
346
+
347
+ Returns `Array<AgentInfo>`.
348
+
349
+ ### mcp_server_status
350
+
351
+ ```ruby
352
+ statuses = client.mcp_server_status
353
+ statuses.each { |s| puts "#{s.name}: #{s.status}" }
354
+ ```
355
+
356
+ Returns `Array<McpServerStatus>`. Status values: `"connected"`, `"failed"`, `"needs-auth"`, `"pending"`.
357
+
358
+ ### account_info
359
+
360
+ ```ruby
361
+ info = client.account_info
362
+ puts "#{info.email} (#{info.organization})"
363
+ ```
364
+
365
+ Returns an `AccountInfo` with fields: `email`, `organization`, `subscription_type`, `token_source`, `api_key_source`.
366
+
367
+ ## Permission Queue
368
+
369
+ When `permission_queue: true` is set in Options (or `can_use_tool` defers a request), the CLI routes tool permission prompts through a thread-safe queue instead of requiring synchronous callback resolution. This is useful for UI-driven applications where a separate thread or event loop handles approval dialogs.
370
+
371
+ ### pending_permission
372
+
373
+ Non-blocking poll for the next pending request. Returns `nil` if the queue is empty.
374
+
375
+ ```ruby
376
+ if request = client.pending_permission
377
+ puts "Tool: #{request.tool_name}"
378
+ puts "Input: #{request.input}"
379
+ puts "Label: #{request.display_label}"
380
+
381
+ request.allow!
382
+ # or: request.deny!(message: "Not allowed in production")
383
+ end
384
+ ```
385
+
386
+ ### pending_permissions?
387
+
388
+ Check whether any requests are waiting:
389
+
390
+ ```ruby
391
+ client.pending_permissions? # => true/false
392
+ ```
393
+
394
+ ### permission_queue
395
+
396
+ Direct access to the underlying `PermissionQueue` for blocking waits or batch draining:
397
+
398
+ ```ruby
399
+ # Blocking wait (with optional timeout)
400
+ request = client.permission_queue.pop(timeout: 30)
401
+
402
+ # Drain all pending (used during cleanup)
403
+ client.permission_queue.drain!(reason: "Shutting down")
404
+ ```
405
+
406
+ `PermissionRequest` methods:
407
+
408
+ | Method | Description |
409
+ |------------------------------------------------|-----------------------------------------------------------------|
410
+ | `allow!(updated_input:, updated_permissions:)` | Allow execution, optionally modifying input or permission rules |
411
+ | `deny!(message:, interrupt:)` | Deny execution with a reason; optionally interrupt the agent |
412
+ | `tool_name` | Name of the tool requesting permission |
413
+ | `input` | Tool input hash |
414
+ | `display_label` | Human-readable label (e.g., `"Read(path: /tmp/file.txt)"`) |
415
+ | `summary(max:)` | Detailed summary, truncated to `max` characters |
416
+ | `pending?` / `resolved?` | Resolution state |
417
+ | `created_at` | Timestamp of the request |
418
+
419
+ ## Cumulative Usage
420
+
421
+ The client tracks token usage, cost, and timing across all turns.
422
+
423
+ ```ruby
424
+ usage = client.cumulative_usage
425
+ puts "Input tokens: #{usage.input_tokens}"
426
+ puts "Output tokens: #{usage.output_tokens}"
427
+ puts "Cache read: #{usage.cache_read_input_tokens}"
428
+ puts "Cache created: #{usage.cache_creation_input_tokens}"
429
+ puts "Total cost: $#{usage.total_cost_usd}"
430
+ puts "Turns: #{usage.num_turns}"
431
+ puts "Duration: #{usage.duration_ms}ms"
432
+ puts "API duration: #{usage.duration_api_ms}ms"
433
+ ```
434
+
435
+ Returns a `CumulativeUsage` instance. Token counts are summed across turns. Cost and turn count reflect session-cumulative values from the CLI.
436
+
437
+ ## Streaming Input
438
+
439
+ Send multiple messages from an enumerable source.
440
+
441
+ ### Without block (send only)
442
+
443
+ ```ruby
444
+ client.stream_input(["Hello", "How are you?"])
445
+ client.receive_response.each { |msg| puts msg }
446
+ ```
447
+
448
+ ### With block (concurrent send/receive)
449
+
450
+ Messages are sent in a background thread while responses are yielded to the block:
451
+
452
+ ```ruby
453
+ client.stream_input(["Hello", "Follow up"], session_id: "default") do |msg|
454
+ case msg
455
+ when ClaudeAgent::AssistantMessage
456
+ puts msg.text
457
+ when ClaudeAgent::ResultMessage
458
+ puts "Done!"
459
+ end
460
+ end
461
+ ```
462
+
463
+ ## Abort and Interrupt
464
+
465
+ ### interrupt
466
+
467
+ Send an interrupt signal to the CLI, stopping the current generation:
468
+
469
+ ```ruby
470
+ client.interrupt
471
+ ```
472
+
473
+ ### abort!
474
+
475
+ Abort all pending operations. Triggers the abort controller (if configured), drains the permission queue, and terminates the transport.
476
+
477
+ ```ruby
478
+ client.abort!
479
+ client.abort!("User cancelled")
480
+ ```
481
+
482
+ ### AbortController
483
+
484
+ For cross-thread cancellation, configure an `AbortController` on Options:
485
+
486
+ ```ruby
487
+ controller = ClaudeAgent::AbortController.new
488
+
489
+ options = ClaudeAgent::Options.new(abort_controller: controller)
490
+ client = ClaudeAgent::Client.new(options: options)
491
+ client.connect
492
+
493
+ # In another thread:
494
+ Thread.new { sleep(5); controller.abort("Timeout") }
495
+
496
+ begin
497
+ turn = client.send_and_receive("Long running task")
498
+ rescue ClaudeAgent::AbortError => e
499
+ partial = e.partial_turn
500
+ puts partial.text # text accumulated before abort
501
+ puts partial.tool_uses # tools that ran before abort
502
+ end
503
+ ```
504
+
505
+ The `AbortSignal` is thread-safe and supports callbacks:
506
+
507
+ ```ruby
508
+ controller.signal.on_abort { |reason| puts "Aborted: #{reason}" }
509
+ controller.signal.aborted? # => false
510
+ controller.abort("Done")
511
+ controller.signal.aborted? # => true
512
+ controller.signal.reason # => "Done"
513
+ ```
514
+
515
+ Call `controller.reset!` to reuse the controller for another turn. `Conversation` does this automatically.
516
+
517
+ ## Disconnect
518
+
519
+ ```ruby
520
+ client.disconnect
521
+ client.connected? # => false
522
+ ```
523
+
524
+ Disconnecting drains the permission queue (denying all pending requests with "Client disconnected"), stops the protocol, and terminates the CLI subprocess. Calling `disconnect` on an already-disconnected client is a no-op.
525
+
526
+ `Client.open` calls `disconnect` automatically in its `ensure` block.