claude_code_sdk 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.
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "open3"
5
+ require "json"
6
+ require "timeout"
7
+
8
+ puts "=== Debug Process Example ==="
9
+
10
+ cli_path = `which claude`.chomp
11
+ puts "CLI path: #{cli_path}"
12
+
13
+ args = ["--print", "--output-format", "stream-json", "--verbose", "What is 2 + 2?"]
14
+ puts "Command: #{cli_path} #{args.join(" ")}"
15
+
16
+ ENV["ANTHROPIC_API_KEY"] =
17
+ "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"
18
+
19
+ stdin, stdout, stderr, wait_thread = Open3.popen3(cli_path, *args)
20
+
21
+ puts "\nProcess started, PID: #{wait_thread.pid}"
22
+
23
+ # Read stdout in a thread
24
+ stdout_thread = Thread.new do
25
+ puts "\nSTDOUT:"
26
+ stdout.each_line do |line|
27
+ puts " > #{line}"
28
+ end
29
+ end
30
+
31
+ # Read stderr in a thread
32
+ stderr_thread = Thread.new do
33
+ puts "\nSTDERR:"
34
+ stderr.each_line do |line|
35
+ puts " ! #{line}"
36
+ end
37
+ end
38
+
39
+ # Wait for threads with timeout
40
+ begin
41
+ Timeout.timeout(5) do
42
+ stdout_thread.join
43
+ stderr_thread.join
44
+ end
45
+ rescue Timeout::Error
46
+ puts "\n[Timeout after 5 seconds]"
47
+ end
48
+
49
+ # Check process status
50
+ if wait_thread.alive?
51
+ puts "\nProcess still alive, killing..."
52
+ Process.kill("TERM", wait_thread.pid)
53
+ wait_thread.join(1)
54
+ end
55
+
56
+ puts "\nExit status: #{wait_thread.value.exitstatus}"
57
+
58
+ stdin.close
59
+ stdout.close
60
+ stderr.close
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add the lib directory to the load path for local development
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+ require "claude_code_sdk"
7
+
8
+ # Enable debug output
9
+ puts "=== Debug Query Example ==="
10
+
11
+ # Create a simple transport to see what's happening
12
+ transport = ClaudeCodeSDK::Transport::SubprocessCLI.new(
13
+ prompt: "What is 2 + 2?",
14
+ options: ClaudeCodeSDK::Options.new
15
+ )
16
+
17
+ begin
18
+ puts "Connecting to CLI..."
19
+ transport.connect
20
+ puts "Connected!"
21
+
22
+ puts "\nReceiving messages..."
23
+ transport.receive_messages do |raw_message|
24
+ puts "Raw message: #{raw_message.inspect}"
25
+ end
26
+ rescue StandardError => e
27
+ puts "Error: #{e.class} - #{e.message}"
28
+ puts e.backtrace.first(5).join("\n")
29
+ ensure
30
+ puts "\nDisconnecting..."
31
+ transport.disconnect
32
+ end
33
+
34
+ puts "Done!"
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add the lib directory to the load path for local development
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+ require "claude_code_sdk"
7
+
8
+ # Simple query with block
9
+ puts "=== Simple Query Example ==="
10
+ ClaudeCodeSDK.query("What is 2 + 2?") do |message|
11
+ case message
12
+ when ClaudeCodeSDK::AssistantMessage
13
+ message.content.each do |block|
14
+ puts block.text if block.is_a?(ClaudeCodeSDK::Content::TextBlock)
15
+ end
16
+ when ClaudeCodeSDK::SystemMessage
17
+ puts "[System] #{message.title}: #{message.message}"
18
+ end
19
+ end
20
+
21
+ puts "\n=== Using ask method ==="
22
+ response = ClaudeCodeSDK.ask("What is the capital of France?")
23
+ puts response
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Test using backticks
5
+ require "English"
6
+ ENV["ANTHROPIC_API_KEY"] =
7
+ "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"
8
+
9
+ command = 'claude --print --output-format json "What is 2 + 2?"'
10
+ puts "Running: #{command}"
11
+
12
+ output = `#{command}`
13
+ puts "Exit status: #{$CHILD_STATUS.exitstatus}"
14
+ puts "Output: #{output}"
15
+
16
+ require "json"
17
+ data = JSON.parse(output)
18
+ puts "Result: #{data["result"]}"
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add the lib directory to the load path for local development
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+ require "claude_code_sdk"
7
+
8
+ puts "=== Simple Tools Example ==="
9
+
10
+ # Test without cwd to see if that's the issue
11
+ options = ClaudeCodeSDK::Options.new(
12
+ allowed_tools: %w[read_file write_file list_files]
13
+ )
14
+
15
+ ClaudeCodeSDK.query("List the Ruby files in the current directory", options) do |message|
16
+ case message
17
+ when ClaudeCodeSDK::AssistantMessage
18
+ message.content.each do |block|
19
+ case block
20
+ when ClaudeCodeSDK::Content::TextBlock
21
+ puts block.text
22
+ when ClaudeCodeSDK::Content::ToolUseBlock
23
+ puts "[Tool Use] #{block.name}: #{block.input.inspect}"
24
+ end
25
+ end
26
+ when ClaudeCodeSDK::SystemMessage
27
+ puts "[System] #{message.title}"
28
+ when ClaudeCodeSDK::ResultMessage
29
+ puts "[Result] Status: #{message.status}"
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add the lib directory to the load path for local development
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+ require "claude_code_sdk"
7
+
8
+ puts "=== Simple Tools Example (Fixed) ==="
9
+
10
+ # Create options without cwd to avoid CLI parsing issues
11
+ options = ClaudeCodeSDK::Options.new(
12
+ allowed_tools: %w[read_file write_file list_files]
13
+ # NOTE: Removing cwd for now as it causes CLI parsing issues
14
+ )
15
+
16
+ ClaudeCodeSDK.query("Help me understand what Ruby files are in this project", options) do |message|
17
+ case message
18
+ when ClaudeCodeSDK::AssistantMessage
19
+ message.content.each do |block|
20
+ case block
21
+ when ClaudeCodeSDK::Content::TextBlock
22
+ puts block.text
23
+ when ClaudeCodeSDK::Content::ToolUseBlock
24
+ puts "[Tool Use] #{block.name}: #{block.input.inspect}"
25
+ end
26
+ end
27
+ when ClaudeCodeSDK::SystemMessage
28
+ puts "[System] #{message.title}"
29
+ when ClaudeCodeSDK::ResultMessage
30
+ puts "[Result] Status: #{message.status}"
31
+ puts " Cost: $#{message.cost[:usd]}" if message.cost && message.cost[:usd]
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add the lib directory to the load path for local development
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+ require "claude_code_sdk"
7
+
8
+ # Example showing real-time streaming of responses
9
+ puts "=== Streaming Response Example ==="
10
+
11
+ # Configure global defaults
12
+ ClaudeCodeSDK.configure do |config|
13
+ config.default_system_prompt = "You are a Ruby programming expert"
14
+ config.default_cwd = Dir.pwd
15
+ end
16
+
17
+ # Track message types
18
+ message_counts = Hash.new(0)
19
+
20
+ ClaudeCodeSDK.query("Write a Ruby function to calculate fibonacci numbers") do |message|
21
+ message_counts[message.class.name] += 1
22
+
23
+ case message
24
+ when ClaudeCodeSDK::UserMessage
25
+ puts "\n[You] #{message.text}"
26
+ when ClaudeCodeSDK::AssistantMessage
27
+ print "\n[Claude] "
28
+ message.content.each do |block|
29
+ print block.text if block.is_a?(ClaudeCodeSDK::Content::TextBlock)
30
+ end
31
+ when ClaudeCodeSDK::SystemMessage
32
+ puts "\n[System] #{message.title}: #{message.message}"
33
+ when ClaudeCodeSDK::ResultMessage
34
+ puts "\n\n[Conversation Complete]"
35
+ puts "Status: #{message.status}"
36
+ puts "Messages received:"
37
+ message_counts.each { |type, count| puts " #{type}: #{count}" }
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add the lib directory to the load path for local development
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+ require "claude_code_sdk"
7
+
8
+ puts "=== Testing Ruby SDK ==="
9
+ puts "SDK Version: #{ClaudeCodeSDK::VERSION}"
10
+ puts "Environment: #{ENV.fetch("CLAUDE_CODE_SDK", nil)}"
11
+
12
+ # Test Options creation
13
+ options = ClaudeCodeSDK::Options.new(
14
+ system_prompt: "Test prompt",
15
+ allowed_tools: %w[read_file write_file],
16
+ max_thinking_tokens: 5000,
17
+ cwd: "/tmp"
18
+ )
19
+
20
+ puts "\nOptions created successfully:"
21
+ puts " System prompt: #{options.system_prompt}"
22
+ puts " Allowed tools: #{options.allowed_tools.join(", ")}"
23
+ puts " Max thinking tokens: #{options.max_thinking_tokens}"
24
+ puts " CWD: #{options.cwd}"
25
+
26
+ puts "\nCLI args: #{options.to_cli_args.join(" ")}"
27
+
28
+ # Test message creation
29
+ user_msg = ClaudeCodeSDK::UserMessage.new(id: "1", text: "Hello")
30
+ puts "\nUser message created: #{user_msg.text}"
31
+
32
+ assistant_msg = ClaudeCodeSDK::AssistantMessage.new(
33
+ id: "2",
34
+ content: [
35
+ ClaudeCodeSDK::Content::TextBlock.new(text: "Hi there!"),
36
+ ClaudeCodeSDK::Content::ToolUseBlock.new(id: "tool1", name: "read_file", input: { path: "test.txt" })
37
+ ]
38
+ )
39
+ puts "Assistant message created with #{assistant_msg.content.length} content blocks"
40
+
41
+ # Test error creation
42
+ error = ClaudeCodeSDK::CLINotFoundError.new("Test error", cli_path: "/usr/bin/claude")
43
+ puts "\nError created: #{error.message}"
44
+
45
+ puts "\n✅ Ruby SDK is working correctly!"
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add the lib directory to the load path for local development
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+ require "claude_code_sdk"
7
+
8
+ # Query with tool permissions
9
+ puts "=== Query with Tools Example ==="
10
+
11
+ options = ClaudeCodeSDK::Options.new(
12
+ system_prompt: "You are a helpful coding assistant",
13
+ allowed_tools: %w[read_file write_file list_files],
14
+ max_thinking_tokens: 10_000,
15
+ cwd: Dir.pwd
16
+ )
17
+
18
+ ClaudeCodeSDK.query("List the files in the current directory", options) do |message|
19
+ case message
20
+ when ClaudeCodeSDK::AssistantMessage
21
+ message.content.each do |block|
22
+ case block
23
+ when ClaudeCodeSDK::Content::TextBlock
24
+ puts block.text
25
+ when ClaudeCodeSDK::Content::ToolUseBlock
26
+ puts "[Tool Use] #{block.name}: #{block.input}"
27
+ end
28
+ end
29
+ when ClaudeCodeSDK::SystemMessage
30
+ puts "[System] #{message.title}"
31
+ when ClaudeCodeSDK::ResultMessage
32
+ puts "[Result] Status: #{message.status}"
33
+ puts " Cost: $#{message.cost[:usd]}" if message.cost
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module ClaudeCodeSDK
6
+ class Configuration
7
+ include Singleton
8
+
9
+ attr_accessor :default_system_prompt, :default_cwd,
10
+ :default_permission_mode, :cli_path
11
+
12
+ def initialize
13
+ @default_system_prompt = nil
14
+ @default_cwd = nil
15
+ @default_permission_mode = PermissionMode::DEFAULT
16
+ @cli_path = nil
17
+ end
18
+
19
+ def to_options
20
+ Options.new(
21
+ system_prompt: default_system_prompt,
22
+ cwd: default_cwd,
23
+ permission_mode: default_permission_mode
24
+ )
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeCodeSDK
4
+ # Base error class for all SDK errors
5
+ class Error < StandardError; end
6
+
7
+ # Raised when unable to connect to Claude Code CLI
8
+ class CLIConnectionError < Error; end
9
+
10
+ # Raised when Claude Code CLI is not found
11
+ class CLINotFoundError < CLIConnectionError
12
+ attr_reader :cli_path
13
+
14
+ def initialize(message = "Claude Code not found", cli_path: nil)
15
+ @cli_path = cli_path
16
+ super(cli_path ? "#{message}: #{cli_path}" : message)
17
+ end
18
+ end
19
+
20
+ # Raised when the CLI process fails
21
+ class ProcessError < Error
22
+ attr_reader :exit_code, :stderr
23
+
24
+ def initialize(message, exit_code: nil, stderr: nil)
25
+ @exit_code = exit_code
26
+ @stderr = stderr
27
+
28
+ full_message = message
29
+ full_message += " (exit code: #{exit_code})" if exit_code
30
+ full_message += "\nError output: #{stderr}" if stderr && !stderr.empty?
31
+
32
+ super(full_message)
33
+ end
34
+ end
35
+
36
+ # Raised when JSON parsing fails
37
+ class CLIJSONDecodeError < Error
38
+ attr_reader :line
39
+
40
+ def initialize(message, line: nil)
41
+ @line = line
42
+ super(line ? "#{message}\nLine: #{line}" : message)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeCodeSDK
4
+ module Internal
5
+ class Client
6
+ def initialize
7
+ @transport = nil
8
+ end
9
+
10
+ def query(prompt:, options: nil, &block)
11
+ # Use SubprocessCLI for streaming JSON support
12
+ require_relative "../transport/subprocess_cli"
13
+ @transport = Transport::SubprocessCLI.new(prompt: prompt, options: options)
14
+
15
+ begin
16
+ @transport.connect
17
+
18
+ @transport.receive_messages do |raw_message|
19
+ message = parse_message(raw_message)
20
+ block.call(message) if message
21
+ end
22
+ ensure
23
+ @transport&.disconnect
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def parse_message(raw)
30
+ case raw[:type]
31
+ when "user"
32
+ UserMessage.new(
33
+ id: raw[:id] || "user-#{Time.now.to_f}",
34
+ text: raw[:text] || raw[:prompt]
35
+ )
36
+ when "assistant"
37
+ # Handle the actual CLI format
38
+ if raw[:message]
39
+ msg = raw[:message]
40
+ # Extract thinking from content blocks
41
+ thinking_content = extract_thinking_from_content(msg[:content] || [])
42
+ # Parse remaining content blocks
43
+ content_blocks = parse_content_blocks(msg[:content] || [])
44
+
45
+ AssistantMessage.new(
46
+ id: msg[:id],
47
+ content: content_blocks,
48
+ thinking: thinking_content
49
+ )
50
+ else
51
+ # Extract thinking from content blocks
52
+ thinking_content = extract_thinking_from_content(raw[:content] || [])
53
+ # Parse remaining content blocks
54
+ content_blocks = parse_content_blocks(raw[:content] || [])
55
+
56
+ AssistantMessage.new(
57
+ id: raw[:id] || "assistant-#{Time.now.to_f}",
58
+ content: content_blocks,
59
+ thinking: thinking_content
60
+ )
61
+ end
62
+ when "system"
63
+ # System messages from CLI have different structure
64
+ SystemMessage.new(
65
+ id: raw[:id] || "system-#{Time.now.to_f}",
66
+ title: raw[:subtype] || "system",
67
+ message: raw.to_json
68
+ )
69
+ when "result"
70
+ ResultMessage.new(
71
+ id: raw[:id] || "result-#{Time.now.to_f}",
72
+ status: raw[:subtype] || raw[:status],
73
+ cost: raw[:total_cost_usd] ? { usd: raw[:total_cost_usd] } : raw[:cost],
74
+ usage: raw[:usage]
75
+ )
76
+ when "thinking"
77
+ # Handle thinking messages from the CLI
78
+ content = raw[:content] || raw[:text] || raw[:message] || ""
79
+ ::ClaudeCodeSDK::ThinkingMessage.new(
80
+ id: raw[:id] || "thinking-#{Time.now.to_f}",
81
+ content: content
82
+ )
83
+ else
84
+ # Unknown message type, skip
85
+ nil
86
+ end
87
+ end
88
+
89
+ def extract_thinking_from_content(content)
90
+ thinking_blocks = content.select { |block| block[:type] == "thinking" }
91
+ return nil if thinking_blocks.empty?
92
+
93
+ # Combine all thinking blocks
94
+ thinking_blocks.map { |block| block[:thinking] }.join("\n")
95
+ end
96
+
97
+ def parse_content_blocks(content)
98
+ content.map do |block|
99
+ case block[:type]
100
+ when "text"
101
+ Content::TextBlock.new(text: block[:text])
102
+ when "tool_use"
103
+ Content::ToolUseBlock.new(
104
+ id: block[:id],
105
+ name: block[:name],
106
+ input: block[:input]
107
+ )
108
+ when "tool_result"
109
+ Content::ToolResultBlock.new(
110
+ tool_use_id: block[:tool_use_id],
111
+ output: block[:output],
112
+ is_error: block[:is_error] || false
113
+ )
114
+ when "thinking"
115
+ # Skip thinking blocks as they're handled separately
116
+ nil
117
+ end
118
+ end.compact
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeCodeSDK
4
+ # Base message class
5
+ class Message
6
+ attr_reader :id, :type
7
+
8
+ def initialize(id:, type:)
9
+ @id = id
10
+ @type = type
11
+ end
12
+ end
13
+
14
+ # User input message
15
+ class UserMessage < Message
16
+ attr_reader :text
17
+
18
+ def initialize(id:, text:)
19
+ super(id: id, type: "user")
20
+ @text = text
21
+ end
22
+ end
23
+
24
+ # Assistant response message
25
+ class AssistantMessage < Message
26
+ attr_reader :content, :thinking
27
+
28
+ def initialize(id:, content:, thinking: nil)
29
+ super(id: id, type: "assistant")
30
+ @content = content # Array of content blocks
31
+ @thinking = thinking # Optional thinking content
32
+ end
33
+ end
34
+
35
+ # System message
36
+ class SystemMessage < Message
37
+ attr_reader :title, :message
38
+
39
+ def initialize(id:, title:, message:)
40
+ super(id: id, type: "system")
41
+ @title = title
42
+ @message = message
43
+ end
44
+ end
45
+
46
+ # Final result message
47
+ class ResultMessage < Message
48
+ attr_reader :status, :cost, :usage
49
+
50
+ def initialize(id:, status:, cost: nil, usage: nil)
51
+ super(id: id, type: "result")
52
+ @status = status
53
+ @cost = cost
54
+ @usage = usage
55
+ end
56
+ end
57
+
58
+ # Thinking message (Claude's internal reasoning)
59
+ class ThinkingMessage < Message
60
+ attr_reader :content
61
+
62
+ def initialize(id:, content:)
63
+ super(id: id, type: "thinking")
64
+ @content = content
65
+ end
66
+ end
67
+
68
+ # Content blocks
69
+ module Content
70
+ class TextBlock
71
+ attr_reader :type, :text
72
+
73
+ def initialize(text:)
74
+ @type = "text"
75
+ @text = text
76
+ end
77
+ end
78
+
79
+ class ToolUseBlock
80
+ attr_reader :type, :id, :name, :input
81
+
82
+ def initialize(id:, name:, input:)
83
+ @type = "tool_use"
84
+ @id = id
85
+ @name = name
86
+ @input = input
87
+ end
88
+ end
89
+
90
+ class ToolResultBlock
91
+ attr_reader :type, :tool_use_id, :output, :is_error
92
+
93
+ def initialize(tool_use_id:, output:, is_error: false)
94
+ @type = "tool_result"
95
+ @tool_use_id = tool_use_id
96
+ @output = output
97
+ @is_error = is_error
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeCodeSDK
4
+ module Transport
5
+ # Abstract base class for transports
6
+ class Base
7
+ def connect
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def disconnect
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def connected?
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def receive_messages(&block)
20
+ raise NotImplementedError
21
+ end
22
+ end
23
+ end
24
+ end