claude_code 0.0.14
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rspec_status +114 -0
- data/.rubocop.yml +51 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +37 -0
- data/LICENSE +21 -0
- data/README.md +288 -0
- data/Rakefile +32 -0
- data/USAGE_EXAMPLES.md +428 -0
- data/claude_code.gemspec +47 -0
- data/docs/README.md +254 -0
- data/docs/mcp_integration.md +404 -0
- data/docs/streaming.md +316 -0
- data/examples/authentication_examples.rb +133 -0
- data/examples/basic_usage.rb +73 -0
- data/examples/conversation_resuming.rb +96 -0
- data/examples/irb_helpers.rb +168 -0
- data/examples/jsonl_cli_equivalent.rb +108 -0
- data/examples/mcp_examples.rb +166 -0
- data/examples/model_examples.rb +111 -0
- data/examples/quick_start.rb +75 -0
- data/examples/rails_sidekiq_example.rb +336 -0
- data/examples/streaming_examples.rb +195 -0
- data/examples/streaming_json_input.rb +171 -0
- data/hello.txt +1 -0
- data/lib/claude_code/client.rb +437 -0
- data/lib/claude_code/errors.rb +48 -0
- data/lib/claude_code/types.rb +237 -0
- data/lib/claude_code/version.rb +5 -0
- data/lib/claude_code.rb +265 -0
- metadata +219 -0
@@ -0,0 +1,237 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClaudeCode
|
4
|
+
# Permission modes
|
5
|
+
PERMISSION_MODES = %w[default acceptEdits bypassPermissions].freeze
|
6
|
+
|
7
|
+
# Content block types
|
8
|
+
class TextBlock
|
9
|
+
attr_reader :text
|
10
|
+
|
11
|
+
def initialize(text)
|
12
|
+
@text = text
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ToolUseBlock
|
17
|
+
attr_reader :id, :name, :input
|
18
|
+
|
19
|
+
def initialize(id:, name:, input:)
|
20
|
+
@id = id
|
21
|
+
@name = name
|
22
|
+
@input = input
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class ToolResultBlock
|
27
|
+
attr_reader :tool_use_id, :content, :is_error
|
28
|
+
|
29
|
+
def initialize(tool_use_id:, content: nil, is_error: nil)
|
30
|
+
@tool_use_id = tool_use_id
|
31
|
+
@content = content
|
32
|
+
@is_error = is_error
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Base message class
|
37
|
+
class BaseMessage
|
38
|
+
end
|
39
|
+
|
40
|
+
# Message types
|
41
|
+
class UserMessage < BaseMessage
|
42
|
+
attr_reader :content
|
43
|
+
|
44
|
+
def initialize(content)
|
45
|
+
@content = content
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class AssistantMessage < BaseMessage
|
50
|
+
attr_reader :content
|
51
|
+
|
52
|
+
def initialize(content)
|
53
|
+
@content = content
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class SystemMessage < BaseMessage
|
58
|
+
attr_reader :subtype, :data
|
59
|
+
|
60
|
+
def initialize(subtype:, data:)
|
61
|
+
@subtype = subtype
|
62
|
+
@data = data
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class ResultMessage < BaseMessage
|
67
|
+
attr_reader :subtype, :duration_ms, :duration_api_ms, :is_error, :num_turns,
|
68
|
+
:session_id, :total_cost_usd, :usage, :result
|
69
|
+
|
70
|
+
def initialize(subtype:, duration_ms:, duration_api_ms: nil, is_error: false, num_turns:,
|
71
|
+
session_id:, total_cost_usd: nil, usage: nil, result: nil)
|
72
|
+
@subtype = subtype
|
73
|
+
@duration_ms = duration_ms
|
74
|
+
@duration_api_ms = duration_api_ms
|
75
|
+
@is_error = is_error
|
76
|
+
@num_turns = num_turns
|
77
|
+
@session_id = session_id
|
78
|
+
@total_cost_usd = total_cost_usd
|
79
|
+
@usage = usage
|
80
|
+
@result = result
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# MCP Server configurations
|
85
|
+
class McpStdioServerConfig
|
86
|
+
attr_reader :command, :args, :env, :type
|
87
|
+
|
88
|
+
def initialize(command:, args: [], env: {}, type: 'stdio')
|
89
|
+
@command = command
|
90
|
+
@args = args
|
91
|
+
@env = env
|
92
|
+
@type = type
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_h
|
96
|
+
{
|
97
|
+
type: @type,
|
98
|
+
command: @command,
|
99
|
+
args: @args,
|
100
|
+
env: @env
|
101
|
+
}
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class McpSSEServerConfig
|
106
|
+
attr_reader :url, :headers, :type
|
107
|
+
|
108
|
+
def initialize(url:, headers: {})
|
109
|
+
@url = url
|
110
|
+
@headers = headers
|
111
|
+
@type = 'sse'
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_h
|
115
|
+
{
|
116
|
+
type: @type,
|
117
|
+
url: @url,
|
118
|
+
headers: @headers
|
119
|
+
}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class McpHttpServerConfig
|
124
|
+
attr_reader :url, :headers, :type
|
125
|
+
|
126
|
+
def initialize(url:, headers: {})
|
127
|
+
@url = url
|
128
|
+
@headers = headers
|
129
|
+
@type = 'http'
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_h
|
133
|
+
{
|
134
|
+
type: @type,
|
135
|
+
url: @url,
|
136
|
+
headers: @headers
|
137
|
+
}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# JSONL message helpers for streaming input
|
142
|
+
module JSONLHelpers
|
143
|
+
# Create a user message in the format expected by Claude CLI
|
144
|
+
def self.create_user_message(text)
|
145
|
+
{
|
146
|
+
'type' => 'user',
|
147
|
+
'message' => {
|
148
|
+
'role' => 'user',
|
149
|
+
'content' => [
|
150
|
+
{
|
151
|
+
'type' => 'text',
|
152
|
+
'text' => text
|
153
|
+
}
|
154
|
+
]
|
155
|
+
}
|
156
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
# Format multiple messages as JSONL
|
160
|
+
def self.format_messages_as_jsonl(messages)
|
161
|
+
messages.map { |msg| msg.to_json }.join("\n")
|
162
|
+
end
|
163
|
+
|
164
|
+
# Create a multi-turn conversation as JSONL messages
|
165
|
+
def self.create_conversation(*turns)
|
166
|
+
turns.compact.reject(&:empty?).map { |turn| create_user_message(turn) }
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Query options
|
171
|
+
class ClaudeCodeOptions
|
172
|
+
attr_reader :allowed_tools, :max_thinking_tokens, :system_prompt, :append_system_prompt,
|
173
|
+
:mcp_tools, :mcp_servers, :permission_mode, :continue_conversation, :resume,
|
174
|
+
:resume_conversation_id, :max_turns, :disallowed_tools, :model, :permission_prompt_tool_name, :cwd,
|
175
|
+
:input_format, :output_format
|
176
|
+
|
177
|
+
def initialize(
|
178
|
+
allowed_tools: [],
|
179
|
+
max_thinking_tokens: 8000,
|
180
|
+
system_prompt: nil,
|
181
|
+
append_system_prompt: nil,
|
182
|
+
mcp_tools: [],
|
183
|
+
mcp_servers: {},
|
184
|
+
permission_mode: nil,
|
185
|
+
continue_conversation: false,
|
186
|
+
resume: nil,
|
187
|
+
resume_conversation_id: nil,
|
188
|
+
max_turns: nil,
|
189
|
+
disallowed_tools: [],
|
190
|
+
model: nil,
|
191
|
+
permission_prompt_tool_name: nil,
|
192
|
+
cwd: nil,
|
193
|
+
input_format: nil,
|
194
|
+
output_format: nil
|
195
|
+
)
|
196
|
+
@allowed_tools = allowed_tools
|
197
|
+
@max_thinking_tokens = max_thinking_tokens
|
198
|
+
@system_prompt = system_prompt
|
199
|
+
@append_system_prompt = append_system_prompt
|
200
|
+
@mcp_tools = mcp_tools
|
201
|
+
@mcp_servers = mcp_servers
|
202
|
+
@permission_mode = permission_mode
|
203
|
+
@continue_conversation = continue_conversation
|
204
|
+
@resume = resume
|
205
|
+
@resume_conversation_id = resume_conversation_id
|
206
|
+
@max_turns = max_turns
|
207
|
+
@disallowed_tools = disallowed_tools
|
208
|
+
@model = model
|
209
|
+
@permission_prompt_tool_name = permission_prompt_tool_name
|
210
|
+
@cwd = cwd
|
211
|
+
@input_format = input_format
|
212
|
+
@output_format = output_format
|
213
|
+
end
|
214
|
+
|
215
|
+
def to_h
|
216
|
+
{
|
217
|
+
allowed_tools: @allowed_tools,
|
218
|
+
max_thinking_tokens: @max_thinking_tokens,
|
219
|
+
system_prompt: @system_prompt,
|
220
|
+
append_system_prompt: @append_system_prompt,
|
221
|
+
mcp_tools: @mcp_tools,
|
222
|
+
mcp_servers: @mcp_servers.transform_values { |config| config.respond_to?(:to_h) ? config.to_h : config },
|
223
|
+
permission_mode: @permission_mode,
|
224
|
+
continue_conversation: @continue_conversation,
|
225
|
+
resume: @resume,
|
226
|
+
resume_conversation_id: @resume_conversation_id,
|
227
|
+
max_turns: @max_turns,
|
228
|
+
disallowed_tools: @disallowed_tools,
|
229
|
+
model: @model,
|
230
|
+
permission_prompt_tool_name: @permission_prompt_tool_name,
|
231
|
+
cwd: @cwd&.to_s,
|
232
|
+
input_format: @input_format,
|
233
|
+
output_format: @output_format
|
234
|
+
}.compact
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
data/lib/claude_code.rb
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'open3'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
require_relative 'claude_code/version'
|
8
|
+
require_relative 'claude_code/types'
|
9
|
+
require_relative 'claude_code/errors'
|
10
|
+
require_relative 'claude_code/client'
|
11
|
+
|
12
|
+
module ClaudeCode
|
13
|
+
# Main query method - supports both positional and keyword arguments
|
14
|
+
def self.query(prompt_or_args = nil, prompt: nil, options: nil, cli_path: nil, mcp_servers: {}, &block)
|
15
|
+
# Handle positional argument for backward compatibility
|
16
|
+
if prompt_or_args.is_a?(String)
|
17
|
+
prompt = prompt_or_args
|
18
|
+
elsif prompt_or_args.is_a?(Hash)
|
19
|
+
# Extract from hash if all args passed as first parameter
|
20
|
+
prompt = prompt_or_args[:prompt] || prompt
|
21
|
+
options = prompt_or_args[:options] || options
|
22
|
+
cli_path = prompt_or_args[:cli_path] || cli_path
|
23
|
+
mcp_servers = prompt_or_args[:mcp_servers] || mcp_servers
|
24
|
+
end
|
25
|
+
options ||= ClaudeCodeOptions.new
|
26
|
+
|
27
|
+
# Merge MCP servers if provided as a separate parameter
|
28
|
+
unless mcp_servers.empty?
|
29
|
+
options = ClaudeCodeOptions.new(
|
30
|
+
allowed_tools: options.allowed_tools,
|
31
|
+
max_thinking_tokens: options.max_thinking_tokens,
|
32
|
+
system_prompt: options.system_prompt,
|
33
|
+
append_system_prompt: options.append_system_prompt,
|
34
|
+
mcp_tools: options.mcp_tools,
|
35
|
+
mcp_servers: options.mcp_servers.merge(mcp_servers),
|
36
|
+
permission_mode: options.permission_mode,
|
37
|
+
continue_conversation: options.continue_conversation,
|
38
|
+
resume: options.resume,
|
39
|
+
max_turns: options.max_turns,
|
40
|
+
disallowed_tools: options.disallowed_tools,
|
41
|
+
model: options.model,
|
42
|
+
permission_prompt_tool_name: options.permission_prompt_tool_name,
|
43
|
+
cwd: options.cwd
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
ENV['CLAUDE_CODE_ENTRYPOINT'] = 'sdk-ruby'
|
48
|
+
|
49
|
+
client = Client.new
|
50
|
+
result = client.process_query(prompt: prompt, options: options, cli_path: cli_path, mcp_servers: mcp_servers)
|
51
|
+
|
52
|
+
if block_given?
|
53
|
+
result.each { |message| yield message }
|
54
|
+
else
|
55
|
+
result
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Convenience method for adding MCP servers
|
60
|
+
def self.add_mcp_server(name, config)
|
61
|
+
{ name => config }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Ultra-convenient method for quick MCP queries
|
65
|
+
def self.quick_mcp_query(prompt, server_name:, server_url:, tools:, **options)
|
66
|
+
cli_path = options.delete(:cli_path)
|
67
|
+
|
68
|
+
mcp_servers = add_mcp_server(server_name, server_url)
|
69
|
+
|
70
|
+
# Ensure tools are in the correct format
|
71
|
+
allowed_tools = Array(tools).map { |tool|
|
72
|
+
tool.start_with?("mcp__") ? tool : "mcp__#{server_name}__#{tool}"
|
73
|
+
}
|
74
|
+
|
75
|
+
opts = ClaudeCodeOptions.new(
|
76
|
+
allowed_tools: allowed_tools,
|
77
|
+
max_turns: options[:max_turns] || 1,
|
78
|
+
system_prompt: options[:system_prompt] || "You are helpful. Use the available MCP tools to answer questions.",
|
79
|
+
**options.slice(:model, :permission_mode, :cwd)
|
80
|
+
)
|
81
|
+
|
82
|
+
query(
|
83
|
+
prompt: prompt,
|
84
|
+
options: opts,
|
85
|
+
cli_path: cli_path,
|
86
|
+
mcp_servers: mcp_servers
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Continue the most recent conversation
|
91
|
+
def self.continue_conversation(prompt = nil, options: nil, cli_path: nil, mcp_servers: {})
|
92
|
+
options ||= ClaudeCodeOptions.new
|
93
|
+
|
94
|
+
# Set continue_conversation to true
|
95
|
+
continue_options = ClaudeCodeOptions.new(
|
96
|
+
allowed_tools: options.allowed_tools,
|
97
|
+
max_thinking_tokens: options.max_thinking_tokens,
|
98
|
+
system_prompt: options.system_prompt,
|
99
|
+
append_system_prompt: options.append_system_prompt,
|
100
|
+
mcp_tools: options.mcp_tools,
|
101
|
+
mcp_servers: options.mcp_servers,
|
102
|
+
permission_mode: options.permission_mode,
|
103
|
+
continue_conversation: true,
|
104
|
+
resume: options.resume,
|
105
|
+
max_turns: options.max_turns,
|
106
|
+
disallowed_tools: options.disallowed_tools,
|
107
|
+
model: options.model,
|
108
|
+
permission_prompt_tool_name: options.permission_prompt_tool_name,
|
109
|
+
cwd: options.cwd
|
110
|
+
)
|
111
|
+
|
112
|
+
query(
|
113
|
+
prompt: prompt || "",
|
114
|
+
options: continue_options,
|
115
|
+
cli_path: cli_path,
|
116
|
+
mcp_servers: mcp_servers
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Resume a specific conversation by session ID
|
121
|
+
def self.resume_conversation(session_id, prompt = nil, options: nil, cli_path: nil, mcp_servers: {})
|
122
|
+
options ||= ClaudeCodeOptions.new
|
123
|
+
|
124
|
+
# Set resume with the session ID
|
125
|
+
resume_options = ClaudeCodeOptions.new(
|
126
|
+
allowed_tools: options.allowed_tools,
|
127
|
+
max_thinking_tokens: options.max_thinking_tokens,
|
128
|
+
system_prompt: options.system_prompt,
|
129
|
+
append_system_prompt: options.append_system_prompt,
|
130
|
+
mcp_tools: options.mcp_tools,
|
131
|
+
mcp_servers: options.mcp_servers,
|
132
|
+
permission_mode: options.permission_mode,
|
133
|
+
continue_conversation: options.continue_conversation,
|
134
|
+
resume_conversation_id: session_id,
|
135
|
+
max_turns: options.max_turns,
|
136
|
+
disallowed_tools: options.disallowed_tools,
|
137
|
+
model: options.model,
|
138
|
+
permission_prompt_tool_name: options.permission_prompt_tool_name,
|
139
|
+
cwd: options.cwd
|
140
|
+
)
|
141
|
+
|
142
|
+
query(
|
143
|
+
prompt: prompt || "",
|
144
|
+
options: resume_options,
|
145
|
+
cli_path: cli_path,
|
146
|
+
mcp_servers: mcp_servers
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Query with streaming JSON input (multiple turns via JSONL)
|
151
|
+
def self.stream_json_query(messages, options: nil, cli_path: nil, mcp_servers: {}, &block)
|
152
|
+
options ||= ClaudeCodeOptions.new
|
153
|
+
|
154
|
+
# Set input_format to stream-json
|
155
|
+
stream_options = ClaudeCodeOptions.new(
|
156
|
+
allowed_tools: options.allowed_tools,
|
157
|
+
max_thinking_tokens: options.max_thinking_tokens,
|
158
|
+
system_prompt: options.system_prompt,
|
159
|
+
append_system_prompt: options.append_system_prompt,
|
160
|
+
mcp_tools: options.mcp_tools,
|
161
|
+
mcp_servers: options.mcp_servers,
|
162
|
+
permission_mode: options.permission_mode,
|
163
|
+
continue_conversation: options.continue_conversation,
|
164
|
+
resume_conversation_id: options.resume_conversation_id,
|
165
|
+
max_turns: options.max_turns,
|
166
|
+
disallowed_tools: options.disallowed_tools,
|
167
|
+
model: options.model,
|
168
|
+
permission_prompt_tool_name: options.permission_prompt_tool_name,
|
169
|
+
cwd: options.cwd,
|
170
|
+
input_format: 'stream-json'
|
171
|
+
)
|
172
|
+
|
173
|
+
# Use the client to process the query
|
174
|
+
client = Client.new
|
175
|
+
enumerator = client.process_query(messages: messages, options: stream_options, cli_path: cli_path, mcp_servers: mcp_servers)
|
176
|
+
|
177
|
+
if block_given?
|
178
|
+
enumerator.each { |message| yield message }
|
179
|
+
else
|
180
|
+
enumerator
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Helper method to parse CLI messages for streaming JSON input
|
185
|
+
def self.parse_cli_message(data)
|
186
|
+
case data['type']
|
187
|
+
when 'user'
|
188
|
+
UserMessage.new(data.dig('message', 'content'))
|
189
|
+
when 'assistant'
|
190
|
+
content_blocks = parse_content_blocks(data.dig('message', 'content') || [])
|
191
|
+
AssistantMessage.new(content_blocks)
|
192
|
+
when 'system'
|
193
|
+
SystemMessage.new(subtype: data['subtype'], data: data)
|
194
|
+
when 'result'
|
195
|
+
ResultMessage.new(
|
196
|
+
subtype: data['subtype'],
|
197
|
+
duration_ms: data['duration_ms'],
|
198
|
+
duration_api_ms: data['duration_api_ms'],
|
199
|
+
is_error: data['is_error'],
|
200
|
+
num_turns: data['num_turns'],
|
201
|
+
session_id: data['session_id'],
|
202
|
+
total_cost_usd: data['total_cost_usd'],
|
203
|
+
usage: data['usage'],
|
204
|
+
result: data['result']
|
205
|
+
)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Helper method to parse content blocks
|
210
|
+
def self.parse_content_blocks(blocks)
|
211
|
+
blocks.map do |block|
|
212
|
+
case block['type']
|
213
|
+
when 'text'
|
214
|
+
TextBlock.new(block['text'])
|
215
|
+
when 'tool_use'
|
216
|
+
ToolUseBlock.new(id: block['id'], name: block['name'], input: block['input'])
|
217
|
+
when 'tool_result'
|
218
|
+
ToolResultBlock.new(
|
219
|
+
tool_use_id: block['tool_use_id'],
|
220
|
+
content: block['content'],
|
221
|
+
is_error: block['is_error']
|
222
|
+
)
|
223
|
+
end
|
224
|
+
end.compact
|
225
|
+
end
|
226
|
+
|
227
|
+
# Streaming helper that prints messages as they arrive
|
228
|
+
def self.stream_query(prompt:, options: nil, cli_path: nil, mcp_servers: {}, &block)
|
229
|
+
query(
|
230
|
+
prompt: prompt,
|
231
|
+
options: options,
|
232
|
+
cli_path: cli_path,
|
233
|
+
mcp_servers: mcp_servers
|
234
|
+
).each_with_index do |message, index|
|
235
|
+
if block_given?
|
236
|
+
yield message, index
|
237
|
+
else
|
238
|
+
# Use auto_format_message for consistent formatting
|
239
|
+
auto_format_message(message)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Auto-format message for pretty printing
|
245
|
+
def self.auto_format_message(message)
|
246
|
+
case message
|
247
|
+
when SystemMessage
|
248
|
+
puts "🔧 System: #{message.subtype}" if message.subtype != "init"
|
249
|
+
when AssistantMessage
|
250
|
+
message.content.each do |block|
|
251
|
+
case block
|
252
|
+
when TextBlock
|
253
|
+
puts "💬 #{block.text}"
|
254
|
+
when ToolUseBlock
|
255
|
+
puts "🔧 #{block.name}"
|
256
|
+
when ToolResultBlock
|
257
|
+
puts "📤 #{block.content}"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
when ResultMessage
|
261
|
+
puts "✅ Cost: $#{format('%.6f', message.total_cost_usd || 0)}"
|
262
|
+
end
|
263
|
+
$stdout.flush
|
264
|
+
end
|
265
|
+
end
|