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.
data/README.md ADDED
@@ -0,0 +1,679 @@
1
+ # ClaudeAgent
2
+
3
+ A Ruby SDK for building AI-powered applications with [Claude Code](https://docs.anthropic.com/en/docs/claude-code). This library wraps the Claude Code CLI, providing both simple one-shot queries and interactive bidirectional sessions.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby 3.2+ (uses `Data.define`)
8
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code/getting-started) v2.0.0+
9
+
10
+ ## Installation
11
+
12
+ Add to your Gemfile:
13
+
14
+ ```ruby
15
+ gem "claude_agent"
16
+ ```
17
+
18
+ Then run:
19
+
20
+ ```bash
21
+ bundle install
22
+ ```
23
+
24
+ Or install directly:
25
+
26
+ ```bash
27
+ gem install claude_agent
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ### One-Shot Query
33
+
34
+ The simplest way to use Claude:
35
+
36
+ ```ruby
37
+ require "claude_agent"
38
+
39
+ ClaudeAgent.query(prompt: "What is the capital of France?").each do |message|
40
+ case message
41
+ when ClaudeAgent::AssistantMessage
42
+ puts message.text
43
+ when ClaudeAgent::ResultMessage
44
+ puts "Cost: $#{message.total_cost_usd}"
45
+ end
46
+ end
47
+ ```
48
+
49
+ ### Interactive Client
50
+
51
+ For multi-turn conversations:
52
+
53
+ ```ruby
54
+ require "claude_agent"
55
+
56
+ ClaudeAgent::Client.open do |client|
57
+ client.query("Remember the number 42")
58
+ client.receive_response.each { |msg| } # Process first response
59
+
60
+ client.query("What number did I ask you to remember?")
61
+ client.receive_response.each do |msg|
62
+ puts msg.text if msg.is_a?(ClaudeAgent::AssistantMessage)
63
+ end
64
+ end
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ Use `ClaudeAgent::Options` to customize behavior:
70
+
71
+ ```ruby
72
+ options = ClaudeAgent::Options.new(
73
+ # Model selection
74
+ model: "claude-sonnet-4-5-20250514",
75
+ fallback_model: "claude-haiku-3-5-20241022",
76
+
77
+ # Conversation limits
78
+ max_turns: 10,
79
+ max_budget_usd: 1.0,
80
+ max_thinking_tokens: 10000,
81
+
82
+ # System prompt
83
+ system_prompt: "You are a helpful coding assistant.",
84
+ append_system_prompt: "Always be concise.",
85
+
86
+ # Tool configuration
87
+ tools: ["Read", "Write", "Bash"],
88
+ allowed_tools: ["Read"],
89
+ disallowed_tools: ["Write"],
90
+
91
+ # Permission modes: "default", "acceptEdits", "plan", "delegate", "dontAsk", "bypassPermissions"
92
+ permission_mode: "acceptEdits",
93
+
94
+ # Working directory for file operations
95
+ cwd: "/path/to/project",
96
+ add_dirs: ["/additional/path"],
97
+
98
+ # Session management
99
+ resume: "session-id",
100
+ continue_conversation: true,
101
+ fork_session: true,
102
+ persist_session: true, # Default: true
103
+
104
+ # Structured output
105
+ output_format: {
106
+ type: "object",
107
+ properties: { answer: { type: "string" } }
108
+ }
109
+ )
110
+
111
+ ClaudeAgent.query(prompt: "Help me refactor this code", options: options)
112
+ ```
113
+
114
+ ### Tools Preset
115
+
116
+ Use a preset tool configuration:
117
+
118
+ ```ruby
119
+ # Using ToolsPreset class
120
+ options = ClaudeAgent::Options.new(
121
+ tools: ClaudeAgent::ToolsPreset.new(preset: "claude_code")
122
+ )
123
+
124
+ # Or as a Hash
125
+ options = ClaudeAgent::Options.new(
126
+ tools: { type: "preset", preset: "claude_code" }
127
+ )
128
+ ```
129
+
130
+ ### Sandbox Settings
131
+
132
+ Configure sandboxed command execution:
133
+
134
+ ```ruby
135
+ sandbox = ClaudeAgent::SandboxSettings.new(
136
+ enabled: true,
137
+ auto_allow_bash_if_sandboxed: true,
138
+ excluded_commands: ["docker", "kubectl"],
139
+ network: ClaudeAgent::SandboxNetworkConfig.new(
140
+ allowed_domains: ["api.example.com"],
141
+ allow_local_binding: true
142
+ ),
143
+ ripgrep: ClaudeAgent::SandboxRipgrepConfig.new(
144
+ command: "/usr/local/bin/rg"
145
+ )
146
+ )
147
+
148
+ options = ClaudeAgent::Options.new(sandbox: sandbox)
149
+ ```
150
+
151
+ ### Custom Agents
152
+
153
+ Define custom subagents:
154
+
155
+ ```ruby
156
+ agents = {
157
+ "test-runner" => ClaudeAgent::AgentDefinition.new(
158
+ description: "Runs tests and reports results",
159
+ prompt: "You are a test runner. Execute tests and report failures clearly.",
160
+ tools: ["Read", "Bash"],
161
+ model: "haiku"
162
+ )
163
+ }
164
+
165
+ options = ClaudeAgent::Options.new(agents: agents)
166
+ ```
167
+
168
+ ## Message Types
169
+
170
+ The SDK provides strongly-typed message classes:
171
+
172
+ ### AssistantMessage
173
+
174
+ Claude's responses:
175
+
176
+ ```ruby
177
+ message.text # Combined text content
178
+ message.thinking # Extended thinking content (if enabled)
179
+ message.model # Model that generated the response
180
+ message.uuid # Message UUID
181
+ message.session_id # Session identifier
182
+ message.tool_uses # Array of ToolUseBlock if Claude wants to use tools
183
+ message.has_tool_use? # Check if tools are being used
184
+ ```
185
+
186
+ ### ResultMessage
187
+
188
+ Final message with usage statistics:
189
+
190
+ ```ruby
191
+ result.session_id # Session identifier
192
+ result.num_turns # Number of conversation turns
193
+ result.duration_ms # Total duration in milliseconds
194
+ result.total_cost_usd # API cost in USD
195
+ result.usage # Token usage breakdown
196
+ result.model_usage # Per-model usage breakdown
197
+ result.is_error # Whether the session ended in error
198
+ result.success? # Convenience method
199
+ result.error? # Convenience method
200
+ result.errors # Array of error messages (if any)
201
+ result.permission_denials # Array of SDKPermissionDenial (if any)
202
+ ```
203
+
204
+ ### SystemMessage
205
+
206
+ Internal system events:
207
+
208
+ ```ruby
209
+ system_msg.subtype # e.g., "init"
210
+ system_msg.data # Event-specific data
211
+ ```
212
+
213
+ ### StreamEvent
214
+
215
+ Real-time streaming events:
216
+
217
+ ```ruby
218
+ event.uuid # Event UUID
219
+ event.session_id # Session identifier
220
+ event.event_type # Type of stream event
221
+ event.event # Raw event data
222
+ ```
223
+
224
+ ### CompactBoundaryMessage
225
+
226
+ Conversation compaction marker:
227
+
228
+ ```ruby
229
+ boundary.uuid # Message UUID
230
+ boundary.session_id # Session identifier
231
+ boundary.trigger # "manual" or "auto"
232
+ boundary.pre_tokens # Token count before compaction
233
+ ```
234
+
235
+ ### StatusMessage
236
+
237
+ Session status updates:
238
+
239
+ ```ruby
240
+ status.uuid # Message UUID
241
+ status.session_id # Session identifier
242
+ status.status # e.g., "compacting"
243
+ ```
244
+
245
+ ### ToolProgressMessage
246
+
247
+ Long-running tool progress:
248
+
249
+ ```ruby
250
+ progress.tool_use_id # Tool use ID
251
+ progress.tool_name # Tool name
252
+ progress.elapsed_time_seconds # Time elapsed
253
+ ```
254
+
255
+ ### HookResponseMessage
256
+
257
+ Hook execution output:
258
+
259
+ ```ruby
260
+ hook_response.hook_name # Hook name
261
+ hook_response.hook_event # Hook event type
262
+ hook_response.stdout # Hook stdout
263
+ hook_response.stderr # Hook stderr
264
+ hook_response.exit_code # Exit code
265
+ ```
266
+
267
+ ### AuthStatusMessage
268
+
269
+ Authentication status during login:
270
+
271
+ ```ruby
272
+ auth.is_authenticating # Whether auth is in progress
273
+ auth.output # Auth output messages
274
+ auth.error # Error message (if any)
275
+ ```
276
+
277
+ ## Content Blocks
278
+
279
+ Assistant messages contain content blocks:
280
+
281
+ ```ruby
282
+ message.content.each do |block|
283
+ case block
284
+ when ClaudeAgent::TextBlock
285
+ puts block.text
286
+ when ClaudeAgent::ThinkingBlock
287
+ puts "Thinking: #{block.thinking}"
288
+ when ClaudeAgent::ToolUseBlock
289
+ puts "Tool: #{block.name}, ID: #{block.id}, Input: #{block.input}"
290
+ when ClaudeAgent::ToolResultBlock
291
+ puts "Result for #{block.tool_use_id}: #{block.content}"
292
+ when ClaudeAgent::ServerToolUseBlock
293
+ puts "MCP Tool: #{block.name} from #{block.server_name}"
294
+ when ClaudeAgent::ServerToolResultBlock
295
+ puts "MCP Result from #{block.server_name}"
296
+ when ClaudeAgent::ImageContentBlock
297
+ puts "Image: #{block.media_type}, source: #{block.source_type}"
298
+ end
299
+ end
300
+ ```
301
+
302
+ ## MCP Tools
303
+
304
+ Create in-process MCP tools that Claude can use:
305
+
306
+ ```ruby
307
+ # Define a tool
308
+ calculator = ClaudeAgent::MCP::Tool.new(
309
+ name: "add",
310
+ description: "Add two numbers together",
311
+ schema: { a: Float, b: Float }
312
+ ) do |args|
313
+ args["a"] + args["b"]
314
+ end
315
+
316
+ # Create a server with tools
317
+ server = ClaudeAgent::MCP::Server.new(
318
+ name: "calculator",
319
+ tools: [calculator]
320
+ )
321
+
322
+ # Use with options (SDK MCP servers)
323
+ options = ClaudeAgent::Options.new(
324
+ mcp_servers: {
325
+ "calculator" => { type: "sdk", instance: server }
326
+ }
327
+ )
328
+
329
+ ClaudeAgent.query(
330
+ prompt: "What is 25 + 17?",
331
+ options: options
332
+ )
333
+ ```
334
+
335
+ ### External MCP Servers
336
+
337
+ Configure external MCP servers:
338
+
339
+ ```ruby
340
+ options = ClaudeAgent::Options.new(
341
+ mcp_servers: {
342
+ "filesystem" => {
343
+ type: "stdio",
344
+ command: "npx",
345
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
346
+ }
347
+ }
348
+ )
349
+ ```
350
+
351
+ ### Tool Schema
352
+
353
+ Define schemas using Ruby types or JSON Schema:
354
+
355
+ ```ruby
356
+ # Ruby types (converted automatically)
357
+ schema: {
358
+ name: String,
359
+ age: Integer,
360
+ score: Float,
361
+ active: TrueClass, # boolean
362
+ tags: Array,
363
+ metadata: Hash
364
+ }
365
+
366
+ # Or use JSON Schema directly
367
+ schema: {
368
+ type: "object",
369
+ properties: {
370
+ name: { type: "string", description: "User's name" },
371
+ age: { type: "integer", minimum: 0 }
372
+ },
373
+ required: ["name"]
374
+ }
375
+ ```
376
+
377
+ ### Tool Return Values
378
+
379
+ Tools can return various formats:
380
+
381
+ ```ruby
382
+ # Simple string
383
+ ClaudeAgent::MCP::Tool.new(name: "greet", description: "Greet") do |args|
384
+ "Hello, #{args['name']}!"
385
+ end
386
+
387
+ # Number (converted to string)
388
+ ClaudeAgent::MCP::Tool.new(name: "add", description: "Add") do |args|
389
+ args["a"] + args["b"]
390
+ end
391
+
392
+ # Custom MCP content
393
+ ClaudeAgent::MCP::Tool.new(name: "fancy", description: "Fancy") do |args|
394
+ { content: [{ type: "text", text: "Custom response" }] }
395
+ end
396
+ ```
397
+
398
+ ## Hooks
399
+
400
+ Intercept tool usage and other events with hooks:
401
+
402
+ ```ruby
403
+ options = ClaudeAgent::Options.new(
404
+ hooks: {
405
+ "PreToolUse" => [
406
+ ClaudeAgent::HookMatcher.new(
407
+ matcher: "Bash|Write", # Match specific tools
408
+ callbacks: [
409
+ ->(input, context) {
410
+ puts "Tool: #{input.tool_name}"
411
+ puts "Input: #{input.tool_input}"
412
+ puts "Tool Use ID: #{input.tool_use_id}"
413
+ { continue_: true } # Allow the tool to proceed
414
+ }
415
+ ]
416
+ )
417
+ ],
418
+ "PostToolUse" => [
419
+ ClaudeAgent::HookMatcher.new(
420
+ matcher: /^mcp__/, # Regex matching
421
+ callbacks: [
422
+ ->(input, context) {
423
+ puts "Result: #{input.tool_response}"
424
+ { continue_: true }
425
+ }
426
+ ]
427
+ )
428
+ ]
429
+ }
430
+ )
431
+ ```
432
+
433
+ ### Hook Events
434
+
435
+ All available hook events:
436
+
437
+ - `PreToolUse` - Before tool execution
438
+ - `PostToolUse` - After successful tool execution
439
+ - `PostToolUseFailure` - After tool execution failure
440
+ - `Notification` - System notifications
441
+ - `UserPromptSubmit` - When user submits a prompt
442
+ - `SessionStart` - When session starts
443
+ - `SessionEnd` - When session ends
444
+ - `Stop` - When agent stops
445
+ - `SubagentStart` - When subagent starts
446
+ - `SubagentStop` - When subagent stops
447
+ - `PreCompact` - Before conversation compaction
448
+ - `PermissionRequest` - When permission is requested
449
+
450
+ ### Hook Input Types
451
+
452
+ | Event | Input Type | Key Fields |
453
+ |--------------------|---------------------------|---------------------------------------------------------|
454
+ | PreToolUse | `PreToolUseInput` | tool_name, tool_input, tool_use_id |
455
+ | PostToolUse | `PostToolUseInput` | tool_name, tool_input, tool_response, tool_use_id |
456
+ | PostToolUseFailure | `PostToolUseFailureInput` | tool_name, tool_input, error, tool_use_id, is_interrupt |
457
+ | Notification | `NotificationInput` | message, title, notification_type |
458
+ | UserPromptSubmit | `UserPromptSubmitInput` | prompt |
459
+ | SessionStart | `SessionStartInput` | source, agent_type |
460
+ | SessionEnd | `SessionEndInput` | reason |
461
+ | Stop | `StopInput` | stop_hook_active |
462
+ | SubagentStart | `SubagentStartInput` | agent_id, agent_type |
463
+ | SubagentStop | `SubagentStopInput` | stop_hook_active, agent_id, agent_transcript_path |
464
+ | PreCompact | `PreCompactInput` | trigger, custom_instructions |
465
+ | PermissionRequest | `PermissionRequestInput` | tool_name, tool_input, permission_suggestions |
466
+
467
+ All hook inputs inherit from `BaseHookInput` with: `hook_event_name`, `session_id`, `transcript_path`, `cwd`, `permission_mode`.
468
+
469
+ ## Permissions
470
+
471
+ Control tool permissions programmatically:
472
+
473
+ ```ruby
474
+ options = ClaudeAgent::Options.new(
475
+ can_use_tool: ->(tool_name, tool_input, context) {
476
+ # Context includes: permission_suggestions, blocked_path, decision_reason, tool_use_id, agent_id
477
+
478
+ # Allow all read operations
479
+ if tool_name == "Read"
480
+ ClaudeAgent::PermissionResultAllow.new
481
+ # Deny writes to sensitive paths
482
+ elsif tool_name == "Write" && tool_input["file_path"].include?(".env")
483
+ ClaudeAgent::PermissionResultDeny.new(
484
+ message: "Cannot modify .env files",
485
+ interrupt: true
486
+ )
487
+ else
488
+ ClaudeAgent::PermissionResultAllow.new
489
+ end
490
+ }
491
+ )
492
+ ```
493
+
494
+ ### Permission Results
495
+
496
+ ```ruby
497
+ # Allow with optional modifications
498
+ ClaudeAgent::PermissionResultAllow.new(
499
+ updated_input: { file_path: "/safe/path" }, # Modify tool input
500
+ updated_permissions: [...] # Update permission rules
501
+ )
502
+
503
+ # Deny
504
+ ClaudeAgent::PermissionResultDeny.new(
505
+ message: "Operation not allowed",
506
+ interrupt: true # Stop the agent
507
+ )
508
+ ```
509
+
510
+ ### Permission Updates
511
+
512
+ ```ruby
513
+ update = ClaudeAgent::PermissionUpdate.new(
514
+ type: "addRules", # addRules, replaceRules, removeRules, setMode, addDirectories, removeDirectories
515
+ rules: [
516
+ ClaudeAgent::PermissionRuleValue.new(tool_name: "Read", rule_content: "/**")
517
+ ],
518
+ behavior: "allow",
519
+ destination: "session" # userSettings, projectSettings, localSettings, session, cliArg
520
+ )
521
+ ```
522
+
523
+ ## Error Handling
524
+
525
+ The SDK provides specific error types:
526
+
527
+ ```ruby
528
+ begin
529
+ ClaudeAgent.query(prompt: "Hello")
530
+ rescue ClaudeAgent::CLINotFoundError
531
+ puts "Claude Code CLI not installed"
532
+ rescue ClaudeAgent::CLIVersionError => e
533
+ puts "CLI version too old: #{e.message}"
534
+ puts "Required: #{e.required_version}, Actual: #{e.actual_version}"
535
+ rescue ClaudeAgent::CLIConnectionError => e
536
+ puts "Connection failed: #{e.message}"
537
+ rescue ClaudeAgent::ProcessError => e
538
+ puts "Process error: #{e.message}, exit code: #{e.exit_code}"
539
+ rescue ClaudeAgent::TimeoutError => e
540
+ puts "Timeout: #{e.message}"
541
+ rescue ClaudeAgent::JSONDecodeError => e
542
+ puts "Invalid JSON response"
543
+ rescue ClaudeAgent::MessageParseError => e
544
+ puts "Could not parse message"
545
+ rescue ClaudeAgent::AbortError => e
546
+ puts "Operation aborted"
547
+ end
548
+ ```
549
+
550
+ ## Client API
551
+
552
+ For fine-grained control:
553
+
554
+ ```ruby
555
+ client = ClaudeAgent::Client.new(options: options)
556
+
557
+ # Connect to CLI
558
+ client.connect
559
+
560
+ # Send queries
561
+ client.query("First question")
562
+ client.receive_response.each { |msg| process(msg) }
563
+
564
+ client.query("Follow-up question")
565
+ client.receive_response.each { |msg| process(msg) }
566
+
567
+ # Control methods
568
+ client.interrupt # Cancel current operation
569
+ client.set_model("claude-opus-4-5-20251101") # Change model
570
+ client.set_permission_mode("acceptEdits") # Change permissions
571
+ client.set_max_thinking_tokens(5000) # Change thinking limit
572
+
573
+ # File checkpointing (requires enable_file_checkpointing: true)
574
+ result = client.rewind_files("user-message-uuid", dry_run: true)
575
+ puts "Can rewind: #{result.can_rewind}"
576
+ puts "Files changed: #{result.files_changed}"
577
+
578
+ # Dynamic MCP server management
579
+ result = client.set_mcp_servers({
580
+ "my-server" => { type: "stdio", command: "node", args: ["server.js"] }
581
+ })
582
+ puts "Added: #{result.added}, Removed: #{result.removed}"
583
+
584
+ # Query capabilities
585
+ client.supported_commands.each { |cmd| puts "#{cmd.name}: #{cmd.description}" }
586
+ client.supported_models.each { |model| puts "#{model.value}: #{model.display_name}" }
587
+ client.mcp_server_status.each { |s| puts "#{s.name}: #{s.status}" }
588
+ puts client.account_info.email
589
+
590
+ # Disconnect
591
+ client.disconnect
592
+ ```
593
+
594
+ ## Types Reference
595
+
596
+ ### Return Types
597
+
598
+ | Type | Purpose |
599
+ |-----------------------|----------------------------------------------------------------------------------|
600
+ | `SlashCommand` | Available slash commands (name, description, argument_hint) |
601
+ | `ModelInfo` | Available models (value, display_name, description) |
602
+ | `McpServerStatus` | MCP server status (name, status, server_info) |
603
+ | `AccountInfo` | Account information (email, organization, subscription_type) |
604
+ | `ModelUsage` | Per-model usage stats (input_tokens, output_tokens, cost_usd) |
605
+ | `McpSetServersResult` | Result of set_mcp_servers (added, removed, errors) |
606
+ | `RewindFilesResult` | Result of rewind_files (can_rewind, error, files_changed, insertions, deletions) |
607
+ | `SDKPermissionDenial` | Permission denial info (tool_name, tool_use_id, tool_input) |
608
+
609
+ ## Environment Variables
610
+
611
+ The SDK sets these automatically:
612
+
613
+ - `CLAUDE_CODE_ENTRYPOINT=sdk-rb`
614
+ - `CLAUDE_AGENT_SDK_VERSION=<version>`
615
+
616
+ Skip version checking (for development):
617
+
618
+ ```bash
619
+ export CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK=true
620
+ ```
621
+
622
+ ## Development
623
+
624
+ ```bash
625
+ # Install dependencies
626
+ bin/setup
627
+
628
+ # Run unit tests
629
+ bundle exec rake test
630
+
631
+ # Run integration tests (requires Claude Code CLI v2.0.0+)
632
+ bundle exec rake test_integration
633
+
634
+ # Run all tests
635
+ bundle exec rake test_all
636
+
637
+ # Validate RBS type signatures
638
+ bundle exec rake rbs
639
+
640
+ # Linting
641
+ bundle exec rubocop
642
+
643
+ # Interactive console
644
+ bin/console
645
+
646
+ # Binstubs for convenience
647
+ bin/test # Unit tests only
648
+ bin/test-integration # Integration tests
649
+ bin/test-all # All tests
650
+ bin/rbs-validate # Validate RBS signatures
651
+ bin/release 1.2.0 # Release a new version
652
+ ```
653
+
654
+ ## Architecture
655
+
656
+ ```
657
+ ClaudeAgent.query() / ClaudeAgent::Client
658
+
659
+
660
+ ┌──────────────────────────┐
661
+ │ Control Protocol │ Request/response routing
662
+ │ - Hooks │ Permission callbacks
663
+ │ - MCP bridging │ Tool interception
664
+ └──────────┬───────────────┘
665
+
666
+
667
+ ┌──────────────────────────┐
668
+ │ Subprocess Transport │ JSON Lines protocol
669
+ │ - stdin/stdout │ Process management
670
+ │ - stderr handling │
671
+ └──────────┬───────────────┘
672
+
673
+
674
+ Claude Code CLI
675
+ ```
676
+
677
+ ## License
678
+
679
+ MIT License - see [LICENSE.txt](LICENSE.txt)