claude_code 0.0.16 → 0.0.18

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.
@@ -7,6 +7,7 @@ require_relative '../lib/claude_code'
7
7
  def ninja_test(prompt)
8
8
  ClaudeCode.quick_mcp_query(
9
9
  prompt,
10
+ model: 'sonnet',
10
11
  server_name: 'ninja',
11
12
  server_url: 'https://mcp-creator-ninja-v1-4-0.mcp.soy/',
12
13
  tools: 'about'
@@ -43,18 +44,16 @@ def quick_claude(prompt, model: nil)
43
44
  model: model,
44
45
  max_turns: 1
45
46
  )
46
-
47
+
47
48
  ClaudeCode.query(
48
49
  prompt: prompt,
49
50
  options: options,
50
- cli_path: "/Users/admin/.claude/local/claude"
51
+ cli_path: '/Users/admin/.claude/local/claude'
51
52
  ).each do |msg|
52
- if msg.is_a?(ClaudeCode::AssistantMessage)
53
- msg.content.each do |block|
54
- if block.is_a?(ClaudeCode::TextBlock)
55
- puts block.text
56
- end
57
- end
53
+ next unless msg.is_a?(ClaudeCode::AssistantMessage)
54
+
55
+ msg.content.each do |block|
56
+ puts block.text if block.is_a?(ClaudeCode::TextBlock)
58
57
  end
59
58
  end
60
59
  end
@@ -65,15 +64,13 @@ def stream_claude(prompt, model: nil)
65
64
  ClaudeCode.stream_query(
66
65
  prompt: prompt,
67
66
  options: ClaudeCode::ClaudeCodeOptions.new(model: model, max_turns: 1),
68
- cli_path: "/Users/admin/.claude/local/claude"
67
+ cli_path: '/Users/admin/.claude/local/claude'
69
68
  ) do |msg, index|
70
69
  timestamp = Time.now - start_time
71
70
  case msg
72
71
  when ClaudeCode::AssistantMessage
73
72
  msg.content.each do |block|
74
- if block.is_a?(ClaudeCode::TextBlock)
75
- puts "[#{format('%.2f', timestamp)}s] #{block.text}"
76
- end
73
+ puts "[#{format('%.2f', timestamp)}s] #{block.text}" if block.is_a?(ClaudeCode::TextBlock)
77
74
  end
78
75
  when ClaudeCode::ResultMessage
79
76
  puts "[#{format('%.2f', timestamp)}s] 💰 $#{format('%.6f', msg.total_cost_usd || 0)}"
@@ -86,21 +83,19 @@ def auto_stream(prompt, model: nil)
86
83
  ClaudeCode.stream_query(
87
84
  prompt: prompt,
88
85
  options: ClaudeCode::ClaudeCodeOptions.new(model: model, max_turns: 1),
89
- cli_path: "/Users/admin/.claude/local/claude"
86
+ cli_path: '/Users/admin/.claude/local/claude'
90
87
  )
91
88
  end
92
89
 
93
90
  # Continue the most recent conversation
94
91
  def continue_chat(prompt = nil)
95
92
  last_session_id = nil
96
-
93
+
97
94
  ClaudeCode.continue_conversation(prompt) do |msg|
98
95
  case msg
99
96
  when ClaudeCode::AssistantMessage
100
97
  msg.content.each do |block|
101
- if block.is_a?(ClaudeCode::TextBlock)
102
- puts "💬 #{block.text}"
103
- end
98
+ puts "💬 #{block.text}" if block.is_a?(ClaudeCode::TextBlock)
104
99
  end
105
100
  when ClaudeCode::ResultMessage
106
101
  last_session_id = msg.session_id
@@ -108,7 +103,7 @@ def continue_chat(prompt = nil)
108
103
  puts "💰 $#{format('%.6f', msg.total_cost_usd || 0)}"
109
104
  end
110
105
  end
111
-
106
+
112
107
  last_session_id
113
108
  end
114
109
 
@@ -118,9 +113,7 @@ def resume_chat(session_id, prompt = nil)
118
113
  case msg
119
114
  when ClaudeCode::AssistantMessage
120
115
  msg.content.each do |block|
121
- if block.is_a?(ClaudeCode::TextBlock)
122
- puts "💬 #{block.text}"
123
- end
116
+ puts "💬 #{block.text}" if block.is_a?(ClaudeCode::TextBlock)
124
117
  end
125
118
  when ClaudeCode::ResultMessage
126
119
  puts "📋 Session: #{msg.session_id}"
@@ -141,28 +134,28 @@ def resume_last(prompt = nil)
141
134
  if $last_session_id
142
135
  resume_chat($last_session_id, prompt)
143
136
  else
144
- puts "❌ No saved session ID. Use save_session(id) first."
137
+ puts '❌ No saved session ID. Use save_session(id) first.'
145
138
  end
146
139
  end
147
140
 
148
- puts "🚀 Ruby Claude Code SDK with MCP, Streaming, and Conversation helpers loaded!"
141
+ puts '🚀 Ruby Claude Code SDK with MCP, Streaming, and Conversation helpers loaded!'
149
142
  puts
150
- puts "Basic commands:"
143
+ puts 'Basic commands:'
151
144
  puts " quick_claude('What is Ruby?')"
152
145
  puts " ninja_test('Tell me about yourself')"
153
146
  puts
154
- puts "Streaming commands:"
147
+ puts 'Streaming commands:'
155
148
  puts " stream_claude('Explain Ruby blocks')"
156
149
  puts " auto_stream('Count to 5')"
157
150
  puts
158
- puts "Conversation commands:"
151
+ puts 'Conversation commands:'
159
152
  puts " continue_chat('Follow up question')"
160
153
  puts " resume_chat('session-id', 'New prompt')"
161
154
  puts " save_session('session-id')"
162
155
  puts " resume_last('New prompt')"
163
156
  puts
164
- puts "Advanced:"
157
+ puts 'Advanced:'
165
158
  puts " quick_claude('Explain arrays', model: 'sonnet')"
166
159
  puts " test_mcp('prompt', 'server_name', 'server_url', 'tool_name')"
167
160
  puts
168
- puts "💡 Remember to set ANTHROPIC_API_KEY environment variable!"
161
+ puts '💡 Remember to set ANTHROPIC_API_KEY environment variable!'
@@ -10,42 +10,38 @@ module ClaudeCode
10
10
  # Client setup
11
11
  end
12
12
 
13
- def process_query(prompt: nil, messages: nil, options:, cli_path: nil, mcp_servers: {})
13
+ def process_query(options:, prompt: nil, messages: nil, cli_path: nil, mcp_servers: {})
14
14
  if messages
15
15
  # Handle streaming JSON input
16
- transport = SubprocessCLITransport.new(prompt: "", options: options, cli_path: cli_path)
16
+ transport = SubprocessCLITransport.new(prompt: '', options: options, cli_path: cli_path)
17
17
  transport.connect
18
-
18
+
19
19
  # Send messages
20
20
  transport.send_messages(messages)
21
-
21
+
22
22
  # Return enumerator for responses
23
23
  return Enumerator.new do |yielder|
24
- begin
25
- transport.receive_messages do |data|
26
- message = parse_message(data)
27
- yielder << message if message
28
- end
29
- ensure
30
- transport.disconnect
24
+ transport.receive_messages do |data|
25
+ message = parse_message(data)
26
+ yielder << message if message
31
27
  end
28
+ ensure
29
+ transport.disconnect
32
30
  end
33
31
  end
34
-
32
+
35
33
  transport = SubprocessCLITransport.new(prompt: prompt, options: options, cli_path: cli_path)
36
-
34
+
37
35
  transport.connect
38
-
36
+
39
37
  # Return lazy enumerator that streams messages as they arrive
40
38
  Enumerator.new do |yielder|
41
- begin
42
- transport.receive_messages do |data|
43
- message = parse_message(data)
44
- yielder << message if message
45
- end
46
- ensure
47
- transport.disconnect
39
+ transport.receive_messages do |data|
40
+ message = parse_message(data)
41
+ yielder << message if message
48
42
  end
43
+ ensure
44
+ transport.disconnect
49
45
  end
50
46
  end
51
47
 
@@ -110,7 +106,7 @@ module ClaudeCode
110
106
  end
111
107
 
112
108
  class SubprocessCLITransport
113
- MAX_BUFFER_SIZE = 1024 * 1024 # 1MB
109
+ MAX_BUFFER_SIZE = 1024 * 1024 * 50 # 50MB
114
110
 
115
111
  def initialize(prompt:, options:, cli_path: nil)
116
112
  @prompt = prompt
@@ -127,9 +123,10 @@ module ClaudeCode
127
123
  # Use provided CLI path if valid
128
124
  if cli_path
129
125
  return cli_path if File.executable?(cli_path)
126
+
130
127
  raise CLINotFoundError.new("CLI not found at specified path: #{cli_path}", cli_path: cli_path)
131
128
  end
132
-
129
+
133
130
  # Try PATH first using cross-platform which
134
131
  cli = which('claude')
135
132
  return cli if cli
@@ -198,25 +195,19 @@ module ClaudeCode
198
195
  def build_environment
199
196
  # Start with current environment
200
197
  env = ENV.to_h
201
-
198
+
202
199
  # Set SDK entrypoint identifier
203
200
  env['CLAUDE_CODE_ENTRYPOINT'] = 'sdk-ruby'
204
-
201
+
205
202
  # Ensure ANTHROPIC_API_KEY is available if set
206
203
  # This allows the CLI to authenticate with Anthropic's API
207
- if ENV['ANTHROPIC_API_KEY']
208
- env['ANTHROPIC_API_KEY'] = ENV['ANTHROPIC_API_KEY']
209
- end
210
-
204
+ env['ANTHROPIC_API_KEY'] = ENV['ANTHROPIC_API_KEY'] if ENV['ANTHROPIC_API_KEY']
205
+
211
206
  # Support for other authentication methods
212
- if ENV['CLAUDE_CODE_USE_BEDROCK']
213
- env['CLAUDE_CODE_USE_BEDROCK'] = ENV['CLAUDE_CODE_USE_BEDROCK']
214
- end
215
-
216
- if ENV['CLAUDE_CODE_USE_VERTEX']
217
- env['CLAUDE_CODE_USE_VERTEX'] = ENV['CLAUDE_CODE_USE_VERTEX']
218
- end
219
-
207
+ env['CLAUDE_CODE_USE_BEDROCK'] = ENV['CLAUDE_CODE_USE_BEDROCK'] if ENV['CLAUDE_CODE_USE_BEDROCK']
208
+
209
+ env['CLAUDE_CODE_USE_VERTEX'] = ENV['CLAUDE_CODE_USE_VERTEX'] if ENV['CLAUDE_CODE_USE_VERTEX']
210
+
220
211
  env
221
212
  end
222
213
 
@@ -234,26 +225,24 @@ module ClaudeCode
234
225
  cmd.concat(['--max-turns', @options.max_turns.to_s]) if @options.max_turns
235
226
  cmd.concat(['--disallowedTools', @options.disallowed_tools.join(',')]) unless @options.disallowed_tools.empty?
236
227
  cmd.concat(['--model', @options.model]) if @options.model
237
- cmd.concat(['--permission-prompt-tool', @options.permission_prompt_tool_name]) if @options.permission_prompt_tool_name
228
+ if @options.permission_prompt_tool_name
229
+ cmd.concat(['--permission-prompt-tool',
230
+ @options.permission_prompt_tool_name])
231
+ end
238
232
  cmd.concat(['--permission-mode', @options.permission_mode]) if @options.permission_mode
239
233
  cmd << '--continue' if @options.continue_conversation
240
234
  cmd.concat(['--resume', @options.resume]) if @options.resume
241
235
 
242
236
  unless @options.mcp_servers.empty?
243
- mcp_config = { 'mcpServers' => @options.mcp_servers.transform_values { |config|
244
- config.respond_to?(:to_h) ? config.to_h : config
245
- } }
237
+ mcp_config = { 'mcpServers' => @options.mcp_servers.transform_values do |config|
238
+ config.respond_to?(:to_h) ? config.to_h : config
239
+ end }
246
240
  cmd.concat(['--mcp-config', JSON.generate(mcp_config)])
247
241
  end
248
242
 
249
- # For streaming JSON input, we use --print mode and send JSON via stdin
250
- # For regular input, we use --print with the prompt
251
- if @options.input_format == 'stream-json'
252
- cmd << '--print'
253
- else
254
- cmd.concat(['--print', @prompt])
255
- end
256
-
243
+ # Always use --print flag (prompt will be sent via stdin)
244
+ cmd << '--print'
245
+
257
246
  cmd
258
247
  end
259
248
 
@@ -262,36 +251,39 @@ module ClaudeCode
262
251
 
263
252
  # Find CLI if not already set
264
253
  @cli_path ||= find_cli
265
-
254
+
266
255
  cmd = build_command
267
256
  puts "Debug: Connecting with command: #{cmd.join(' ')}" if ENV['DEBUG_CLAUDE_SDK']
268
-
257
+
269
258
  begin
270
259
  env = build_environment
271
-
260
+
272
261
  if @cwd
273
262
  @stdin, @stdout, @stderr, @process = Open3.popen3(env, *cmd, chdir: @cwd)
274
263
  else
275
264
  @stdin, @stdout, @stderr, @process = Open3.popen3(env, *cmd)
276
265
  end
277
-
266
+
278
267
  # Handle different input modes
279
268
  if @options.input_format == 'stream-json'
280
269
  # Keep stdin open for streaming JSON input
281
- puts "Debug: Keeping stdin open for streaming JSON input" if ENV['DEBUG_CLAUDE_SDK']
270
+ puts 'Debug: Keeping stdin open for streaming JSON input' if ENV['DEBUG_CLAUDE_SDK']
282
271
  else
283
- # Close stdin for regular prompt mode
272
+ # Write prompt to stdin and close
273
+ if @prompt && !@prompt.empty?
274
+ puts "Debug: Writing prompt to stdin (#{@prompt.length} chars)" if ENV['DEBUG_CLAUDE_SDK']
275
+ @stdin.write(@prompt)
276
+ @stdin.flush
277
+ end
284
278
  @stdin.close
285
279
  end
286
-
280
+
287
281
  puts "Debug: Process started with PID #{@process.pid}" if ENV['DEBUG_CLAUDE_SDK']
288
-
289
282
  rescue Errno::ENOENT => e
290
- if @cwd && !Dir.exist?(@cwd)
291
- raise CLIConnectionError.new("Working directory does not exist: #{@cwd}")
292
- end
283
+ raise CLIConnectionError.new("Working directory does not exist: #{@cwd}") if @cwd && !Dir.exist?(@cwd)
284
+
293
285
  raise CLINotFoundError.new("Claude Code not found at: #{@cli_path}")
294
- rescue => e
286
+ rescue StandardError => e
295
287
  raise CLIConnectionError.new("Failed to start Claude Code: #{e.class} - #{e.message}")
296
288
  end
297
289
  end
@@ -303,7 +295,7 @@ module ClaudeCode
303
295
  # Try to terminate gracefully
304
296
  if @process.alive?
305
297
  Process.kill('INT', @process.pid)
306
-
298
+
307
299
  # Wait for process to exit with timeout
308
300
  begin
309
301
  require 'timeout'
@@ -314,7 +306,11 @@ module ClaudeCode
314
306
  # Force kill if it doesn't exit gracefully
315
307
  begin
316
308
  Process.kill('KILL', @process.pid) if @process.alive?
317
- @process.join rescue nil
309
+ begin
310
+ @process.join
311
+ rescue StandardError
312
+ nil
313
+ end
318
314
  rescue Errno::ESRCH
319
315
  # Process already gone
320
316
  end
@@ -323,9 +319,21 @@ module ClaudeCode
323
319
  rescue Errno::ESRCH, Errno::ECHILD
324
320
  # Process already gone
325
321
  ensure
326
- @stdin&.close rescue nil
327
- @stdout&.close rescue nil
328
- @stderr&.close rescue nil
322
+ begin
323
+ @stdin&.close
324
+ rescue StandardError
325
+ nil
326
+ end
327
+ begin
328
+ @stdout&.close
329
+ rescue StandardError
330
+ nil
331
+ end
332
+ begin
333
+ @stderr&.close
334
+ rescue StandardError
335
+ nil
336
+ end
329
337
  @stdin = nil
330
338
  @stdout = nil
331
339
  @stderr = nil
@@ -334,17 +342,17 @@ module ClaudeCode
334
342
  end
335
343
 
336
344
  def receive_messages
337
- raise CLIConnectionError.new("Not connected") unless @process && @stdout
345
+ raise CLIConnectionError.new('Not connected') unless @process && @stdout
346
+
347
+ json_buffer = ''
338
348
 
339
- json_buffer = ""
340
-
341
349
  begin
342
350
  @stdout.each_line do |line|
343
351
  line = line.strip
344
352
  next if line.empty?
345
353
 
346
354
  json_lines = line.split("\n")
347
-
355
+
348
356
  json_lines.each do |json_line|
349
357
  json_line = json_line.strip
350
358
  next if json_line.empty?
@@ -352,7 +360,7 @@ module ClaudeCode
352
360
  json_buffer += json_line
353
361
 
354
362
  if json_buffer.length > MAX_BUFFER_SIZE
355
- json_buffer = ""
363
+ json_buffer = ''
356
364
  raise CLIJSONDecodeError.new(
357
365
  "JSON message exceeded maximum buffer size of #{MAX_BUFFER_SIZE} bytes",
358
366
  StandardError.new("Buffer size #{json_buffer.length} exceeds limit #{MAX_BUFFER_SIZE}")
@@ -361,13 +369,12 @@ module ClaudeCode
361
369
 
362
370
  begin
363
371
  data = JSON.parse(json_buffer)
364
- json_buffer = ""
372
+ json_buffer = ''
365
373
  yield data
366
374
  rescue JSON::ParserError => e
367
375
  # For single-line JSON, if parsing fails, it's an error
368
- if json_buffer.include?("\n") || json_buffer.length > 1000
369
- raise CLIJSONDecodeError.new(json_buffer, e)
370
- end
376
+ raise CLIJSONDecodeError.new(json_buffer, e) if json_buffer.include?("\n") || json_buffer.length > 10_000
377
+
371
378
  # Otherwise continue accumulating
372
379
  next
373
380
  end
@@ -384,21 +391,44 @@ module ClaudeCode
384
391
  data = JSON.parse(json_buffer)
385
392
  yield data
386
393
  rescue JSON::ParserError => e
387
- raise CLIJSONDecodeError.new(json_buffer, StandardError.new("Incomplete JSON at end of stream"))
394
+ raise CLIJSONDecodeError.new(json_buffer, StandardError.new('Incomplete JSON at end of stream'))
388
395
  end
389
396
  end
390
397
 
391
398
  # Check for errors
392
399
  exit_code = @process.value.exitstatus if @process
393
400
  stderr_output = @stderr.read if @stderr
401
+
402
+ return unless exit_code && exit_code != 0
403
+
404
+ # Build helpful error message
405
+ error_message = "Command failed with exit code #{exit_code}"
394
406
 
395
- if exit_code && exit_code != 0
396
- raise ProcessError.new(
397
- "Command failed with exit code #{exit_code}",
398
- exit_code: exit_code,
399
- stderr: stderr_output
400
- )
407
+ if stderr_output.nil? || stderr_output.strip.empty?
408
+ error_message += "\n\nNo error output from Claude CLI. Common causes:"
409
+ error_message += "\n- Invalid or missing ANTHROPIC_API_KEY"
410
+ error_message += "\n- MCP server connection failed"
411
+ error_message += "\n- Network connectivity issues"
412
+ error_message += "\n- Invalid model name or options"
413
+
414
+ # Include debug info if available
415
+ if ENV['DEBUG_CLAUDE_SDK']
416
+ error_message += "\n\nDebug info:"
417
+ error_message += "\n- CLI path: #{@cli_path}"
418
+ error_message += "\n- Working directory: #{@cwd || 'current'}"
419
+ error_message += "\n- JSON buffer (last #{[json_buffer.length, 200].min} chars): #{json_buffer[-200..-1]}" if json_buffer && !json_buffer.empty?
420
+ end
421
+
422
+ error_message += "\n\nTry enabling debug mode with ENV['DEBUG_CLAUDE_SDK'] = '1' for more details"
423
+ else
424
+ error_message += "\nError output: #{stderr_output}"
401
425
  end
426
+
427
+ raise ProcessError.new(
428
+ error_message,
429
+ exit_code: exit_code,
430
+ stderr: stderr_output
431
+ )
402
432
  end
403
433
 
404
434
  def connected?
@@ -407,31 +437,31 @@ module ClaudeCode
407
437
 
408
438
  # Send a JSON message via stdin for streaming input mode
409
439
  def send_message(message)
410
- raise CLIConnectionError.new("Not connected to CLI") unless @stdin
411
-
440
+ raise CLIConnectionError.new('Not connected to CLI') unless @stdin
441
+
412
442
  json_line = message.to_json + "\n"
413
443
  puts "Debug: Sending JSON message: #{json_line.strip}" if ENV['DEBUG_CLAUDE_SDK']
414
-
444
+
415
445
  begin
416
446
  @stdin.write(json_line)
417
447
  @stdin.flush
418
448
  rescue Errno::EPIPE
419
449
  # Pipe is broken, process has terminated
420
- raise CLIConnectionError.new("CLI process terminated unexpectedly")
450
+ raise CLIConnectionError.new('CLI process terminated unexpectedly')
421
451
  end
422
452
  end
423
453
 
424
454
  # Send multiple messages and close stdin to signal end of input
425
455
  def send_messages(messages)
426
- raise CLIConnectionError.new("Not connected to CLI") unless @stdin
427
-
456
+ raise CLIConnectionError.new('Not connected to CLI') unless @stdin
457
+
428
458
  messages.each do |message|
429
459
  send_message(message)
430
460
  end
431
-
461
+
432
462
  # Close stdin to signal end of input stream
433
463
  @stdin.close
434
464
  @stdin = nil
435
465
  end
436
466
  end
437
- end
467
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeCode
4
- VERSION = "0.0.16"
4
+ VERSION = "0.0.18"
5
5
  end
data/lib/claude_code.rb CHANGED
@@ -10,6 +10,11 @@ require_relative 'claude_code/errors'
10
10
  require_relative 'claude_code/client'
11
11
 
12
12
  module ClaudeCode
13
+ # Check if API key is configured
14
+ def self.api_key_configured?
15
+ !ENV['ANTHROPIC_API_KEY'].nil? && !ENV['ANTHROPIC_API_KEY'].strip.empty?
16
+ end
17
+
13
18
  # Main query method - supports both positional and keyword arguments
14
19
  def self.query(prompt_or_args = nil, prompt: nil, options: nil, cli_path: nil, mcp_servers: {}, &block)
15
20
  # Handle positional argument for backward compatibility
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude_code
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.16
4
+ version: 0.0.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Your Name
@@ -168,6 +168,7 @@ files:
168
168
  - README.md
169
169
  - Rakefile
170
170
  - USAGE_EXAMPLES.md
171
+ - claude_code-0.0.16.gem
171
172
  - claude_code.gemspec
172
173
  - docs/README.md
173
174
  - docs/mcp_integration.md