claude_agent 0.7.3 → 0.7.4
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 +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +61 -0
- data/Rakefile +1 -1
- data/SPEC.md +33 -36
- data/lib/claude_agent/client.rb +8 -0
- data/lib/claude_agent/control_protocol.rb +41 -39
- data/lib/claude_agent/logging.rb +102 -0
- data/lib/claude_agent/mcp/server.rb +10 -1
- data/lib/claude_agent/message_parser.rb +11 -0
- data/lib/claude_agent/options.rb +16 -3
- data/lib/claude_agent/permissions.rb +1 -1
- data/lib/claude_agent/query.rb +22 -33
- data/lib/claude_agent/transport/subprocess.rb +13 -0
- data/lib/claude_agent/version.rb +1 -1
- data/lib/claude_agent.rb +1 -0
- data/sig/claude_agent.rbs +28 -2
- data/sig/manifest.yaml +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d8b88e03dcf883a968958f700a7dd765fbdae8553934b45e967594ad81f9bba
|
|
4
|
+
data.tar.gz: fd16c0e625ed0df2c548c39378e167af73a21c2fa6d7b61cacc351d9cbde78d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4c7ba1adad712135d2934405b758324f2477e71b7914769deb07ba07fe4a025ca11f2c04a0ec46e3d2fe6954cf02288097ef914eb62c7874aca229aa4cd9c6c
|
|
7
|
+
data.tar.gz: c608d6c49ffb80c0c0aa96a4dca2f042f280060a73f473e271b1b0d099a368239fedf3194264f82f825fc5d8d76dfa4986c44563929f3a83f09773af96f84654
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.4] - 2026-02-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Configurable logging via `ClaudeAgent.logger` (module-level) and `Options#logger` (per-query)
|
|
14
|
+
- `NullLogger` default for zero overhead when logging is not configured
|
|
15
|
+
- `ClaudeAgent.debug!` convenience method for quick stderr debug logging
|
|
16
|
+
- Backward-compatible with `CLAUDE_AGENT_DEBUG` env var
|
|
17
|
+
- Log points across transport, control protocol, message parser, MCP server, query, and client
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- `can_use_tool` callback now works without hooks or MCP servers configured
|
|
21
|
+
- `PermissionResultAllow` and `PermissionResultDeny` (`Data.define` types) are now correctly recognized in `handle_can_use_tool` instead of silently falling through to allow
|
|
22
|
+
- `normalize_hook_response` now handles `Data.define` return types from hook callbacks
|
|
23
|
+
- Allow responses without explicit `updated_input` now fall back to the original input (Python SDK parity)
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Always use streaming mode with control protocol initialization (Python/TypeScript SDK parity)
|
|
27
|
+
- Removes fragile conditional gate on hooks/MCP/can_use_tool
|
|
28
|
+
- `send_initialize` handshake is now always sent in streaming mode
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- Auto-set `permission_prompt_tool_name` to `"stdio"` when `can_use_tool` is configured (Python/TypeScript SDK parity)
|
|
32
|
+
|
|
10
33
|
## [0.7.3] - 2026-02-06
|
|
11
34
|
|
|
12
35
|
### Added
|
data/README.md
CHANGED
|
@@ -818,6 +818,61 @@ session = ClaudeAgent.unstable_v2_create_session(options)
|
|
|
818
818
|
| `RewindFilesResult` | Result of rewind_files (can_rewind, error, files_changed, insertions, deletions) |
|
|
819
819
|
| `SDKPermissionDenial` | Permission denial info (tool_name, tool_use_id, tool_input) |
|
|
820
820
|
|
|
821
|
+
## Logging
|
|
822
|
+
|
|
823
|
+
The SDK includes optional logging with zero overhead when disabled. All log output is silent by default.
|
|
824
|
+
|
|
825
|
+
### Quick Debug
|
|
826
|
+
|
|
827
|
+
```ruby
|
|
828
|
+
# Enable debug logging to stderr
|
|
829
|
+
ClaudeAgent.debug!
|
|
830
|
+
|
|
831
|
+
# Or to a file
|
|
832
|
+
ClaudeAgent.debug!(output: File.open("claude_agent.log", "a"))
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### Custom Logger
|
|
836
|
+
|
|
837
|
+
Set any `Logger`-compatible instance at the module level:
|
|
838
|
+
|
|
839
|
+
```ruby
|
|
840
|
+
ClaudeAgent.logger = Logger.new($stderr, level: :info)
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### Per-Query Logger
|
|
844
|
+
|
|
845
|
+
Override the module-level logger for a specific query or client:
|
|
846
|
+
|
|
847
|
+
```ruby
|
|
848
|
+
my_logger = Logger.new("query.log", level: :debug)
|
|
849
|
+
|
|
850
|
+
ClaudeAgent.query(prompt: "Hello", options: ClaudeAgent::Options.new(logger: my_logger))
|
|
851
|
+
|
|
852
|
+
# Or with Client
|
|
853
|
+
client = ClaudeAgent::Client.new(options: ClaudeAgent::Options.new(logger: my_logger))
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
### Log Output
|
|
857
|
+
|
|
858
|
+
When enabled, the SDK logs events across transport, protocol, parsing, MCP, and query layers:
|
|
859
|
+
|
|
860
|
+
```
|
|
861
|
+
[ClaudeAgent] [12:00:00.123] INFO -- transport: Process spawned (pid=12345)
|
|
862
|
+
[ClaudeAgent] [12:00:00.456] DEBUG -- protocol: Sending control request: initialize (req_1_abc)
|
|
863
|
+
[ClaudeAgent] [12:00:01.789] INFO -- protocol: Permission decision for Bash: allow
|
|
864
|
+
[ClaudeAgent] [12:00:02.100] INFO -- query: Query complete (3.45s, cost=$0.012)
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### Log Levels
|
|
868
|
+
|
|
869
|
+
| Level | What's Logged |
|
|
870
|
+
|-------|---------------|
|
|
871
|
+
| ERROR | Control request failures, unknown message types |
|
|
872
|
+
| WARN | Force kills, JSON parse errors during buffering, unknown MCP tools |
|
|
873
|
+
| INFO | Process spawn/close, protocol start/stop, permission decisions, tool calls, query start/completion with timing |
|
|
874
|
+
| DEBUG | Full commands, message types received, control request/response routing, reader thread lifecycle |
|
|
875
|
+
|
|
821
876
|
## Environment Variables
|
|
822
877
|
|
|
823
878
|
The SDK sets these automatically:
|
|
@@ -825,6 +880,12 @@ The SDK sets these automatically:
|
|
|
825
880
|
- `CLAUDE_CODE_ENTRYPOINT=sdk-rb`
|
|
826
881
|
- `CLAUDE_AGENT_SDK_VERSION=<version>`
|
|
827
882
|
|
|
883
|
+
Enable debug logging via environment variable:
|
|
884
|
+
|
|
885
|
+
```bash
|
|
886
|
+
export CLAUDE_AGENT_DEBUG=1
|
|
887
|
+
```
|
|
888
|
+
|
|
828
889
|
Skip version checking (for development):
|
|
829
890
|
|
|
830
891
|
```bash
|
data/Rakefile
CHANGED
|
@@ -43,7 +43,7 @@ RuboCop::RakeTask.new
|
|
|
43
43
|
namespace :rbs do
|
|
44
44
|
desc "Validate RBS signatures (syntax + type resolution)"
|
|
45
45
|
task :validate do
|
|
46
|
-
sh "bundle exec rbs -I sig validate"
|
|
46
|
+
sh "bundle exec rbs -r logger -I sig validate"
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
desc "Parse RBS files (syntax check only, faster)"
|
data/SPEC.md
CHANGED
|
@@ -7,7 +7,7 @@ This document provides a comprehensive specification of the Claude Agent SDK, co
|
|
|
7
7
|
- Python SDK: v0.1.31 from GitHub (commit 4b19642)
|
|
8
8
|
- Ruby SDK: This repository
|
|
9
9
|
|
|
10
|
-
**Last Updated:** 2026-02-
|
|
10
|
+
**Last Updated:** 2026-02-07
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -37,7 +37,7 @@ Configuration options for SDK queries and clients.
|
|
|
37
37
|
| `model` | ✅ | ✅ | ✅ | Claude model identifier |
|
|
38
38
|
| `fallbackModel` | ✅ | ✅ | ✅ | Fallback if primary fails |
|
|
39
39
|
| `systemPrompt` | ✅ | ✅ | ✅ | String or preset object |
|
|
40
|
-
| `appendSystemPrompt` | ✅ |
|
|
40
|
+
| `appendSystemPrompt` | ✅ | ✅ | ✅ | Append to system prompt (via preset) |
|
|
41
41
|
| `tools` | ✅ | ✅ | ✅ | Array or preset |
|
|
42
42
|
| `allowedTools` | ✅ | ✅ | ✅ | Auto-allowed tools |
|
|
43
43
|
| `disallowedTools` | ✅ | ✅ | ✅ | Blocked tools |
|
|
@@ -249,23 +249,23 @@ Event hooks for intercepting and modifying SDK behavior.
|
|
|
249
249
|
|
|
250
250
|
### Hook Events
|
|
251
251
|
|
|
252
|
-
| Event | TypeScript | Python | Ruby | Notes
|
|
253
|
-
|
|
254
|
-
| `PreToolUse` | ✅ | ✅ | ✅ | Before tool execution
|
|
255
|
-
| `PostToolUse` | ✅ | ✅ | ✅ | After tool execution
|
|
256
|
-
| `PostToolUseFailure` | ✅ | ✅ | ✅ | After tool failure
|
|
257
|
-
| `Notification` | ✅ | ✅ | ✅ | System notifications
|
|
258
|
-
| `UserPromptSubmit` | ✅ | ✅ | ✅ | User message submitted
|
|
259
|
-
| `SessionStart` | ✅ | ❌ | ✅ | Session starts
|
|
260
|
-
| `SessionEnd` | ✅ | ❌ | ✅ | Session ends
|
|
261
|
-
| `Stop` | ✅ | ✅ | ✅ | Agent stops
|
|
262
|
-
| `SubagentStart` | ✅ | ✅ | ✅ | Subagent starts
|
|
263
|
-
| `SubagentStop` | ✅ | ✅ | ✅ | Subagent stops
|
|
264
|
-
| `PreCompact` | ✅ | ✅ | ✅ | Before compaction
|
|
265
|
-
| `PermissionRequest` | ✅ | ✅ | ✅ | Permission requested
|
|
266
|
-
| `Setup` | ✅ | ❌ | ✅ | Initial setup/maintenance
|
|
267
|
-
| `TeammateIdle` | ✅ | ❌ | ✅ | Teammate idle (v0.2.33)
|
|
268
|
-
| `TaskCompleted` | ✅ | ❌ | ✅ | Task completed (v0.2.33)
|
|
252
|
+
| Event | TypeScript | Python | Ruby | Notes |
|
|
253
|
+
|----------------------|:----------:|:------:|:----:|-----------------------------------|
|
|
254
|
+
| `PreToolUse` | ✅ | ✅ | ✅ | Before tool execution |
|
|
255
|
+
| `PostToolUse` | ✅ | ✅ | ✅ | After tool execution |
|
|
256
|
+
| `PostToolUseFailure` | ✅ | ✅ | ✅ | After tool failure (Py v0.1.26) |
|
|
257
|
+
| `Notification` | ✅ | ✅ | ✅ | System notifications (Py v0.1.29) |
|
|
258
|
+
| `UserPromptSubmit` | ✅ | ✅ | ✅ | User message submitted |
|
|
259
|
+
| `SessionStart` | ✅ | ❌ | ✅ | Session starts |
|
|
260
|
+
| `SessionEnd` | ✅ | ❌ | ✅ | Session ends |
|
|
261
|
+
| `Stop` | ✅ | ✅ | ✅ | Agent stops |
|
|
262
|
+
| `SubagentStart` | ✅ | ✅ | ✅ | Subagent starts (Py v0.1.29) |
|
|
263
|
+
| `SubagentStop` | ✅ | ✅ | ✅ | Subagent stops |
|
|
264
|
+
| `PreCompact` | ✅ | ✅ | ✅ | Before compaction |
|
|
265
|
+
| `PermissionRequest` | ✅ | ✅ | ✅ | Permission requested (Py v0.1.29) |
|
|
266
|
+
| `Setup` | ✅ | ❌ | ✅ | Initial setup/maintenance |
|
|
267
|
+
| `TeammateIdle` | ✅ | ❌ | ✅ | Teammate idle (v0.2.33) |
|
|
268
|
+
| `TaskCompleted` | ✅ | ❌ | ✅ | Task completed (v0.2.33) |
|
|
269
269
|
|
|
270
270
|
### Hook Input Types
|
|
271
271
|
|
|
@@ -631,16 +631,16 @@ Public API surface for SDK clients.
|
|
|
631
631
|
|
|
632
632
|
### Client Class
|
|
633
633
|
|
|
634
|
-
| Feature | TypeScript | Python | Ruby | Notes
|
|
635
|
-
|
|
636
|
-
| Multi-turn client | ❌ | ✅ `ClaudeSDKClient` | ✅ `ClaudeAgent::Client` | Interactive sessions
|
|
637
|
-
| `connect()` | N/A | ✅ | ✅ | Start session
|
|
638
|
-
| `disconnect()` | N/A | ✅ | ✅ | End session
|
|
639
|
-
| `send_message()` | N/A | ✅ | ✅ | Send user message
|
|
640
|
-
| `receive_response()` | N/A | ✅ | ✅ | Receive until result
|
|
641
|
-
| `stream_input()` | N/A | ❌ | ✅ | Stream input messages
|
|
642
|
-
| `abort!()` | N/A | ❌ | ✅ | Abort operations
|
|
643
|
-
| Control methods | N/A | Partial | ✅ | interrupt, setPermissionMode, setModel, rewindFiles (Python); all
|
|
634
|
+
| Feature | TypeScript | Python | Ruby | Notes |
|
|
635
|
+
|----------------------|:----------:|:-------------------:|:-----------------------:|-------------------------------------------------------------------------------------|
|
|
636
|
+
| Multi-turn client | ❌ | ✅ `ClaudeSDKClient` | ✅ `ClaudeAgent::Client` | Interactive sessions |
|
|
637
|
+
| `connect()` | N/A | ✅ | ✅ | Start session |
|
|
638
|
+
| `disconnect()` | N/A | ✅ | ✅ | End session |
|
|
639
|
+
| `send_message()` | N/A | ✅ | ✅ | Send user message |
|
|
640
|
+
| `receive_response()` | N/A | ✅ | ✅ | Receive until result |
|
|
641
|
+
| `stream_input()` | N/A | ❌ | ✅ | Stream input messages |
|
|
642
|
+
| `abort!()` | N/A | ❌ | ✅ | Abort operations |
|
|
643
|
+
| Control methods | N/A | Partial | ✅ | interrupt, setPermissionMode, setModel, rewindFiles, mcpStatus (Python); all (Ruby) |
|
|
644
644
|
|
|
645
645
|
### Transport
|
|
646
646
|
|
|
@@ -670,16 +670,13 @@ Public API surface for SDK clients.
|
|
|
670
670
|
- `executable`/`executableArgs` are JS-specific (`node`/`bun`/`deno`)
|
|
671
671
|
|
|
672
672
|
### Python SDK
|
|
673
|
-
- Full source available
|
|
674
|
-
-
|
|
675
|
-
- Query supports: `interrupt()`, `set_permission_mode()`, `set_model()`, `rewind_files()`, `stream_input()`, `close()`, `get_mcp_status()`
|
|
676
|
-
- Client supports: `interrupt()`, `set_permission_mode()`, `set_model()`, `rewind_files()`, `get_mcp_status()`, `get_server_info()`
|
|
673
|
+
- Full source available with `Transport` abstract class
|
|
674
|
+
- Partial control protocol: query and client support interrupt, setPermissionMode, setModel, rewindFiles, mcpStatus
|
|
677
675
|
- Missing hooks: SessionStart, SessionEnd, Setup, TeammateIdle, TaskCompleted
|
|
678
676
|
- Missing permission modes: `delegate`, `dontAsk`
|
|
679
677
|
- Missing options: `allowDangerouslySkipPermissions`, `persistSession`, `resumeSessionAt`, `sessionId`, `strictMcpConfig`, `init`/`initOnly`/`maintenance`, `debug`/`debugFile`
|
|
680
|
-
- `ToolPermissionContext` missing `blockedPath`, `decisionReason`, `toolUseID`, `agentID`
|
|
681
|
-
- Has
|
|
682
|
-
- Has `create_sdk_mcp_server`, `tool()` helper, and MCP tool annotations (added in v0.1.30/v0.1.31)
|
|
678
|
+
- `ToolPermissionContext` missing `blockedPath`, `decisionReason`, `toolUseID`, `agentID`, `description`
|
|
679
|
+
- Has SDK MCP server support with `tool()` helper and annotations
|
|
683
680
|
|
|
684
681
|
### Ruby SDK (This Repository)
|
|
685
682
|
- Full TypeScript SDK v0.2.34 feature parity (v0.2.34 contains no new SDK features beyond a Claude Code version bump)
|
data/lib/claude_agent/client.rb
CHANGED
|
@@ -72,9 +72,11 @@ module ClaudeAgent
|
|
|
72
72
|
|
|
73
73
|
ENV["CLAUDE_CODE_ENTRYPOINT"] = "sdk-rb-client"
|
|
74
74
|
|
|
75
|
+
logger.info("client") { "Connecting" }
|
|
75
76
|
@protocol = ControlProtocol.new(transport: @transport, options: @options)
|
|
76
77
|
@server_info = @protocol.start(streaming: true)
|
|
77
78
|
@connected = true
|
|
79
|
+
logger.info("client") { "Connected" }
|
|
78
80
|
|
|
79
81
|
send_message(prompt) if prompt
|
|
80
82
|
end
|
|
@@ -85,6 +87,7 @@ module ClaudeAgent
|
|
|
85
87
|
def disconnect
|
|
86
88
|
return unless @connected
|
|
87
89
|
|
|
90
|
+
logger.info("client") { "Disconnecting" }
|
|
88
91
|
@protocol&.stop
|
|
89
92
|
@protocol = nil
|
|
90
93
|
@connected = false
|
|
@@ -105,6 +108,7 @@ module ClaudeAgent
|
|
|
105
108
|
# @return [void]
|
|
106
109
|
def send_message(content, session_id: "default", uuid: nil)
|
|
107
110
|
require_connection!
|
|
111
|
+
logger.debug("client") { "Sending message (session=#{session_id})" }
|
|
108
112
|
@protocol.send_user_message(content, session_id: session_id, uuid: uuid)
|
|
109
113
|
end
|
|
110
114
|
|
|
@@ -336,6 +340,10 @@ module ClaudeAgent
|
|
|
336
340
|
|
|
337
341
|
private
|
|
338
342
|
|
|
343
|
+
def logger
|
|
344
|
+
@options.effective_logger
|
|
345
|
+
end
|
|
346
|
+
|
|
339
347
|
def require_connection!
|
|
340
348
|
raise CLIConnectionError, "Not connected" unless @connected
|
|
341
349
|
end
|
|
@@ -30,7 +30,7 @@ module ClaudeAgent
|
|
|
30
30
|
def initialize(transport:, options: nil)
|
|
31
31
|
@transport = transport
|
|
32
32
|
@options = options || Options.new
|
|
33
|
-
@parser = MessageParser.new
|
|
33
|
+
@parser = MessageParser.new(logger: @options.effective_logger)
|
|
34
34
|
@server_info = nil
|
|
35
35
|
|
|
36
36
|
# Control protocol state
|
|
@@ -57,15 +57,18 @@ module ClaudeAgent
|
|
|
57
57
|
# @param prompt [String, nil] Initial prompt for non-streaming mode
|
|
58
58
|
# @return [Hash, nil] Server info from initialization
|
|
59
59
|
def start(streaming: true, prompt: nil)
|
|
60
|
+
logger.info("protocol") { "Starting control protocol (streaming=#{streaming})" }
|
|
60
61
|
@transport.connect(streaming: streaming, prompt: prompt)
|
|
61
62
|
@running = true
|
|
62
63
|
|
|
63
64
|
# Start background reader thread
|
|
64
65
|
@reader_thread = Thread.new { reader_loop }
|
|
66
|
+
logger.debug("protocol") { "Reader thread started" }
|
|
65
67
|
|
|
66
|
-
#
|
|
67
|
-
if streaming
|
|
68
|
+
# Always send initialize in streaming mode (Python/TypeScript SDK parity)
|
|
69
|
+
if streaming
|
|
68
70
|
@server_info = send_initialize
|
|
71
|
+
logger.info("protocol") { "Initialize complete" }
|
|
69
72
|
end
|
|
70
73
|
|
|
71
74
|
@server_info
|
|
@@ -74,6 +77,7 @@ module ClaudeAgent
|
|
|
74
77
|
# Stop the control protocol
|
|
75
78
|
# @return [void]
|
|
76
79
|
def stop
|
|
80
|
+
logger.info("protocol") { "Stopping control protocol" }
|
|
77
81
|
@running = false
|
|
78
82
|
@transport.end_input
|
|
79
83
|
@reader_thread&.join(5)
|
|
@@ -143,8 +147,7 @@ module ClaudeAgent
|
|
|
143
147
|
# Re-raise abort errors
|
|
144
148
|
raise
|
|
145
149
|
rescue => e
|
|
146
|
-
|
|
147
|
-
warn "[ClaudeAgent] Message parse error: #{e.message}" if ENV["CLAUDE_AGENT_DEBUG"]
|
|
150
|
+
logger.warn("protocol") { "Message parse error: #{e.message}" }
|
|
148
151
|
end
|
|
149
152
|
end
|
|
150
153
|
end
|
|
@@ -457,16 +460,9 @@ module ClaudeAgent
|
|
|
457
460
|
#
|
|
458
461
|
def set_mcp_servers(servers)
|
|
459
462
|
# Convert servers hash to format expected by CLI
|
|
460
|
-
servers_config = servers.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
next nil if config[:type] == "sdk" || config["type"] == "sdk"
|
|
464
|
-
|
|
465
|
-
config
|
|
466
|
-
else
|
|
467
|
-
config
|
|
468
|
-
end
|
|
469
|
-
end.compact
|
|
463
|
+
servers_config = servers.reject do |_, config|
|
|
464
|
+
config.is_a?(Hash) && (config[:type] == "sdk" || config["type"] == "sdk")
|
|
465
|
+
end
|
|
470
466
|
|
|
471
467
|
response = send_control_request(subtype: "mcp_set_servers", servers: servers_config)
|
|
472
468
|
|
|
@@ -479,6 +475,10 @@ module ClaudeAgent
|
|
|
479
475
|
|
|
480
476
|
private
|
|
481
477
|
|
|
478
|
+
def logger
|
|
479
|
+
@options.effective_logger
|
|
480
|
+
end
|
|
481
|
+
|
|
482
482
|
# Background thread that reads messages and routes them
|
|
483
483
|
def reader_loop
|
|
484
484
|
@transport.read_messages do |raw|
|
|
@@ -491,19 +491,22 @@ module ClaudeAgent
|
|
|
491
491
|
break unless @running
|
|
492
492
|
|
|
493
493
|
if raw["type"] == "control_request"
|
|
494
|
+
logger.debug("protocol") { "Control request received: #{raw.dig("request", "subtype")}" }
|
|
494
495
|
handle_control_request(raw)
|
|
495
496
|
elsif raw["type"] == "control_response"
|
|
497
|
+
logger.debug("protocol") { "Control response received: #{raw.dig("response", "request_id")}" }
|
|
496
498
|
handle_control_response(raw)
|
|
497
499
|
else
|
|
498
500
|
# SDK message - queue for consumer
|
|
501
|
+
logger.debug("protocol") { "Queued message: #{raw["type"]}" }
|
|
499
502
|
@message_queue.push(raw)
|
|
500
503
|
end
|
|
501
504
|
end
|
|
502
505
|
rescue IOError, Errno::EPIPE
|
|
503
|
-
|
|
506
|
+
logger.debug("protocol") { "Reader thread exiting: transport closed" }
|
|
504
507
|
@running = false
|
|
505
508
|
rescue AbortError
|
|
506
|
-
|
|
509
|
+
logger.debug("protocol") { "Reader thread exiting: abort signal" }
|
|
507
510
|
@running = false
|
|
508
511
|
end
|
|
509
512
|
|
|
@@ -572,7 +575,10 @@ module ClaudeAgent
|
|
|
572
575
|
# @param request [Hash] Request data
|
|
573
576
|
# @return [Hash] Response
|
|
574
577
|
def handle_can_use_tool(request)
|
|
575
|
-
|
|
578
|
+
unless options.can_use_tool
|
|
579
|
+
logger.info("protocol") { "Permission decision for #{request["tool_name"]}: allow (no callback)" }
|
|
580
|
+
return { behavior: "allow" }
|
|
581
|
+
end
|
|
576
582
|
|
|
577
583
|
tool_name = request["tool_name"]
|
|
578
584
|
input = request["input"] || {}
|
|
@@ -587,27 +593,14 @@ module ClaudeAgent
|
|
|
587
593
|
|
|
588
594
|
result = options.can_use_tool.call(tool_name, input, context)
|
|
589
595
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if result[:updated_permissions]
|
|
596
|
-
response[:updatedPermissions] = result[:updated_permissions].map do |p|
|
|
597
|
-
p.respond_to?(:to_h) ? p.to_h : p
|
|
598
|
-
end
|
|
599
|
-
end
|
|
600
|
-
response
|
|
601
|
-
else
|
|
602
|
-
{
|
|
603
|
-
behavior: "deny",
|
|
604
|
-
message: result[:message] || "",
|
|
605
|
-
interrupt: result[:interrupt] || false
|
|
606
|
-
}
|
|
607
|
-
end
|
|
608
|
-
else
|
|
609
|
-
{ behavior: "allow" }
|
|
596
|
+
normalized = result.to_h
|
|
597
|
+
logger.info("protocol") { "Permission decision for #{tool_name}: #{normalized[:behavior]}" }
|
|
598
|
+
|
|
599
|
+
if normalized[:behavior] == "allow" && !normalized.key?(:updatedInput)
|
|
600
|
+
normalized[:updatedInput] = input
|
|
610
601
|
end
|
|
602
|
+
|
|
603
|
+
normalized
|
|
611
604
|
end
|
|
612
605
|
|
|
613
606
|
# Handle hook callback request
|
|
@@ -619,7 +612,11 @@ module ClaudeAgent
|
|
|
619
612
|
tool_use_id = request["tool_use_id"]
|
|
620
613
|
|
|
621
614
|
callback = @hook_callbacks[callback_id]
|
|
622
|
-
|
|
615
|
+
unless callback
|
|
616
|
+
logger.debug("protocol") { "Hook callback not found: #{callback_id}" }
|
|
617
|
+
return {}
|
|
618
|
+
end
|
|
619
|
+
logger.debug("protocol") { "Hook callback: #{callback_id}" }
|
|
623
620
|
|
|
624
621
|
context = { tool_use_id: tool_use_id }
|
|
625
622
|
result = callback.call(input, context)
|
|
@@ -634,6 +631,7 @@ module ClaudeAgent
|
|
|
634
631
|
def handle_mcp_message(request)
|
|
635
632
|
server_name = request["server_name"]
|
|
636
633
|
message = request["message"]
|
|
634
|
+
logger.debug("protocol") { "MCP message for #{server_name}: #{message["method"]}" }
|
|
637
635
|
|
|
638
636
|
# Find SDK MCP server
|
|
639
637
|
server_config = options.mcp_servers[server_name]
|
|
@@ -667,6 +665,8 @@ module ClaudeAgent
|
|
|
667
665
|
# @param result [Hash] Raw result from callback
|
|
668
666
|
# @return [Hash] Normalized response
|
|
669
667
|
def normalize_hook_response(result)
|
|
668
|
+
result = result.to_h
|
|
669
|
+
|
|
670
670
|
response = HOOK_RESPONSE_KEYS.each_with_object({}) do |(ruby_key, json_key), acc|
|
|
671
671
|
acc[json_key] = result[ruby_key] if result.key?(ruby_key)
|
|
672
672
|
end
|
|
@@ -713,6 +713,7 @@ module ClaudeAgent
|
|
|
713
713
|
@abort_signal&.check!
|
|
714
714
|
|
|
715
715
|
request_id = generate_request_id
|
|
716
|
+
logger.debug("protocol") { "Sending control request: #{subtype} (#{request_id})" }
|
|
716
717
|
|
|
717
718
|
request = {
|
|
718
719
|
type: "control_request",
|
|
@@ -749,6 +750,7 @@ module ClaudeAgent
|
|
|
749
750
|
end
|
|
750
751
|
|
|
751
752
|
if response["subtype"] == "error"
|
|
753
|
+
logger.error("protocol") { "Control request failed: #{subtype} - #{response["error"]}" }
|
|
752
754
|
raise Error, response["error"] || "Unknown error"
|
|
753
755
|
end
|
|
754
756
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module ClaudeAgent
|
|
6
|
+
# Null logger that discards all output with zero overhead.
|
|
7
|
+
#
|
|
8
|
+
# All log methods return +true+ immediately without performing any I/O.
|
|
9
|
+
# This is the default logger, ensuring logging adds no cost when not configured.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# logger = ClaudeAgent::NullLogger.new
|
|
13
|
+
# logger.info("transport") { "This is discarded" } # => true
|
|
14
|
+
#
|
|
15
|
+
class NullLogger < Logger
|
|
16
|
+
def initialize
|
|
17
|
+
super(File::NULL)
|
|
18
|
+
@level = Logger::DEBUG
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add(_severity = nil, _message = nil, _progname = nil)
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def debug(...) = true
|
|
26
|
+
def info(...) = true
|
|
27
|
+
def warn(...) = true
|
|
28
|
+
def error(...) = true
|
|
29
|
+
def fatal(...) = true
|
|
30
|
+
def unknown(...) = true
|
|
31
|
+
|
|
32
|
+
def debug? = false
|
|
33
|
+
def info? = false
|
|
34
|
+
def warn? = false
|
|
35
|
+
def error? = false
|
|
36
|
+
def fatal? = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Compact log formatter with gem name tag.
|
|
40
|
+
#
|
|
41
|
+
# Output format:
|
|
42
|
+
# [ClaudeAgent] [12:00:00.123] DEBUG -- transport: Spawning CLI
|
|
43
|
+
#
|
|
44
|
+
LOG_FORMATTER = proc do |severity, time, progname, msg|
|
|
45
|
+
"[ClaudeAgent] [#{time.strftime("%H:%M:%S.%L")}] #{severity.ljust(5)} -- #{progname}: #{msg}\n"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# Module-level logger used by all components unless overridden per-query.
|
|
50
|
+
#
|
|
51
|
+
# Defaults to {NullLogger} for zero overhead. Set to any +Logger+-compatible
|
|
52
|
+
# instance to enable logging.
|
|
53
|
+
#
|
|
54
|
+
# @return [Logger]
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# ClaudeAgent.logger = Logger.new($stderr, level: :info)
|
|
58
|
+
#
|
|
59
|
+
def logger
|
|
60
|
+
@logger ||= default_logger
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Set the module-level logger.
|
|
64
|
+
#
|
|
65
|
+
# @param logger [Logger] A Logger-compatible instance
|
|
66
|
+
# @return [Logger]
|
|
67
|
+
def logger=(logger)
|
|
68
|
+
@logger = logger
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Enable debug-level logging to stderr (or a custom output).
|
|
72
|
+
#
|
|
73
|
+
# Convenience method for quick debugging. Creates a +Logger+ with
|
|
74
|
+
# a compact formatter and DEBUG level.
|
|
75
|
+
#
|
|
76
|
+
# @param output [IO] Output destination (default: +$stderr+)
|
|
77
|
+
# @return [Logger] The configured logger
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# ClaudeAgent.debug!
|
|
81
|
+
# ClaudeAgent.debug!(output: $stdout)
|
|
82
|
+
# ClaudeAgent.debug!(output: File.open("debug.log", "a"))
|
|
83
|
+
#
|
|
84
|
+
def debug!(output: $stderr)
|
|
85
|
+
self.logger = Logger.new(output, level: Logger::DEBUG).tap do |l|
|
|
86
|
+
l.formatter = LOG_FORMATTER
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def default_logger
|
|
93
|
+
if ENV["CLAUDE_AGENT_DEBUG"]
|
|
94
|
+
Logger.new($stderr, level: Logger::DEBUG).tap do |l|
|
|
95
|
+
l.formatter = LOG_FORMATTER
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
NullLogger.new
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -34,9 +34,11 @@ module ClaudeAgent
|
|
|
34
34
|
|
|
35
35
|
# @param name [String] Server name
|
|
36
36
|
# @param tools [Array<Tool>] Tools to expose
|
|
37
|
-
|
|
37
|
+
# @param logger [Logger, nil] Optional logger instance
|
|
38
|
+
def initialize(name:, tools: [], logger: nil)
|
|
38
39
|
@name = name.to_s
|
|
39
40
|
@tools = {}
|
|
41
|
+
@logger = logger
|
|
40
42
|
tools.each { |tool| add_tool(tool) }
|
|
41
43
|
end
|
|
42
44
|
|
|
@@ -61,6 +63,7 @@ module ClaudeAgent
|
|
|
61
63
|
method = message["method"]
|
|
62
64
|
params = message["params"] || {}
|
|
63
65
|
id = message["id"]
|
|
66
|
+
logger.debug("mcp.#{@name}") { "Handling: #{method}" }
|
|
64
67
|
|
|
65
68
|
result = case method
|
|
66
69
|
when "initialize"
|
|
@@ -114,15 +117,21 @@ module ClaudeAgent
|
|
|
114
117
|
|
|
115
118
|
tool = @tools[tool_name]
|
|
116
119
|
unless tool
|
|
120
|
+
logger.warn("mcp.#{@name}") { "Unknown tool: #{tool_name}" }
|
|
117
121
|
return {
|
|
118
122
|
content: [ { type: "text", text: "Unknown tool: #{tool_name}" } ],
|
|
119
123
|
isError: true
|
|
120
124
|
}
|
|
121
125
|
end
|
|
122
126
|
|
|
127
|
+
logger.info("mcp.#{@name}") { "Tool call: #{tool_name}" }
|
|
123
128
|
tool.call(arguments)
|
|
124
129
|
end
|
|
125
130
|
|
|
131
|
+
def logger
|
|
132
|
+
@logger || ClaudeAgent.logger
|
|
133
|
+
end
|
|
134
|
+
|
|
126
135
|
def jsonrpc_response(id, result)
|
|
127
136
|
{
|
|
128
137
|
jsonrpc: "2.0",
|
|
@@ -8,6 +8,11 @@ module ClaudeAgent
|
|
|
8
8
|
# message = parser.parse({"type" => "assistant", "message" => {...}})
|
|
9
9
|
#
|
|
10
10
|
class MessageParser
|
|
11
|
+
# @param logger [Logger, nil] Optional logger instance
|
|
12
|
+
def initialize(logger: nil)
|
|
13
|
+
@logger = logger
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
# Parse a raw message hash into a typed message object
|
|
12
17
|
#
|
|
13
18
|
# @param raw [Hash] Raw message from CLI
|
|
@@ -15,6 +20,7 @@ module ClaudeAgent
|
|
|
15
20
|
# @raise [MessageParseError] If message cannot be parsed
|
|
16
21
|
def parse(raw)
|
|
17
22
|
type = raw["type"]
|
|
23
|
+
logger.debug("parser") { "Parsing message: #{type}" }
|
|
18
24
|
|
|
19
25
|
case type
|
|
20
26
|
when "user"
|
|
@@ -52,12 +58,17 @@ module ClaudeAgent
|
|
|
52
58
|
when "tool_use_summary"
|
|
53
59
|
parse_tool_use_summary_message(raw)
|
|
54
60
|
else
|
|
61
|
+
logger.error("parser") { "Unknown message type: #{type}" }
|
|
55
62
|
raise MessageParseError.new("Unknown message type: #{type}", raw_message: raw)
|
|
56
63
|
end
|
|
57
64
|
end
|
|
58
65
|
|
|
59
66
|
private
|
|
60
67
|
|
|
68
|
+
def logger
|
|
69
|
+
@logger || ClaudeAgent.logger
|
|
70
|
+
end
|
|
71
|
+
|
|
61
72
|
# Fetch a value from a hash, trying both snake_case and camelCase keys
|
|
62
73
|
# @param raw [Hash] The hash to fetch from
|
|
63
74
|
# @param snake_key [Symbol, String] The snake_case key to try
|
data/lib/claude_agent/options.rb
CHANGED
|
@@ -62,6 +62,7 @@ module ClaudeAgent
|
|
|
62
62
|
abort_controller spawn_claude_code_process
|
|
63
63
|
init init_only maintenance
|
|
64
64
|
debug debug_file
|
|
65
|
+
logger
|
|
65
66
|
].freeze
|
|
66
67
|
|
|
67
68
|
attr_accessor(*ATTRIBUTES)
|
|
@@ -127,6 +128,12 @@ module ClaudeAgent
|
|
|
127
128
|
abort_controller&.signal
|
|
128
129
|
end
|
|
129
130
|
|
|
131
|
+
# Resolved logger: per-instance override or module-level default
|
|
132
|
+
# @return [Logger]
|
|
133
|
+
def effective_logger
|
|
134
|
+
@logger || ClaudeAgent.logger
|
|
135
|
+
end
|
|
136
|
+
|
|
130
137
|
private
|
|
131
138
|
|
|
132
139
|
# --- CLI Argument Builders ---
|
|
@@ -207,8 +214,7 @@ module ClaudeAgent
|
|
|
207
214
|
[].tap do |args|
|
|
208
215
|
args.push("--settings", settings) if settings
|
|
209
216
|
if sandbox
|
|
210
|
-
|
|
211
|
-
args.push("--sandbox", JSON.generate(sandbox_json))
|
|
217
|
+
args.push("--sandbox", JSON.generate(sandbox.to_h))
|
|
212
218
|
end
|
|
213
219
|
end
|
|
214
220
|
end
|
|
@@ -234,7 +240,7 @@ module ClaudeAgent
|
|
|
234
240
|
args.push("--json-schema", JSON.generate(output_format)) if output_format
|
|
235
241
|
args.push("--include-partial-messages") if include_partial_messages
|
|
236
242
|
if agents
|
|
237
|
-
agents_hash = agents.transform_values
|
|
243
|
+
agents_hash = agents.transform_values(&:to_h)
|
|
238
244
|
args.push("--agents", JSON.generate(agents_hash))
|
|
239
245
|
end
|
|
240
246
|
end
|
|
@@ -280,6 +286,13 @@ module ClaudeAgent
|
|
|
280
286
|
raise ConfigurationError, "can_use_tool must be callable (Proc, Lambda, or object responding to #call)"
|
|
281
287
|
end
|
|
282
288
|
|
|
289
|
+
# Auto-set permission_prompt_tool_name to "stdio" when can_use_tool is configured
|
|
290
|
+
# (Python/TypeScript SDK parity) so the CLI routes permission prompts through the
|
|
291
|
+
# control protocol instead of interactive terminal prompts
|
|
292
|
+
if can_use_tool && !permission_prompt_tool_name
|
|
293
|
+
@permission_prompt_tool_name = "stdio"
|
|
294
|
+
end
|
|
295
|
+
|
|
283
296
|
if max_turns && (!max_turns.is_a?(Integer) || max_turns < 1)
|
|
284
297
|
raise ConfigurationError, "max_turns must be a positive integer"
|
|
285
298
|
end
|
|
@@ -21,7 +21,7 @@ module ClaudeAgent
|
|
|
21
21
|
def to_h
|
|
22
22
|
h = { behavior: "allow" }
|
|
23
23
|
h[:updatedInput] = updated_input if updated_input
|
|
24
|
-
h[:updatedPermissions] = updated_permissions&.map
|
|
24
|
+
h[:updatedPermissions] = updated_permissions&.map(&:to_h) if updated_permissions
|
|
25
25
|
h[:toolUseID] = tool_use_id if tool_use_id
|
|
26
26
|
h
|
|
27
27
|
end
|
data/lib/claude_agent/query.rb
CHANGED
|
@@ -85,46 +85,35 @@ module ClaudeAgent
|
|
|
85
85
|
Enumerator.new do |yielder|
|
|
86
86
|
# Set entrypoint environment variable
|
|
87
87
|
ENV["CLAUDE_CODE_ENTRYPOINT"] = "sdk-rb"
|
|
88
|
+
query_logger = options.effective_logger
|
|
89
|
+
query_logger.info("query") { "Starting query" }
|
|
90
|
+
query_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
88
91
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# Register abort handler if abort controller is provided
|
|
97
|
-
if options.abort_signal
|
|
98
|
-
options.abort_signal.on_abort do
|
|
99
|
-
protocol.abort! rescue nil
|
|
100
|
-
end
|
|
92
|
+
# Always use streaming mode with control protocol (Python/TypeScript SDK parity)
|
|
93
|
+
protocol = ControlProtocol.new(transport: transport, options: options)
|
|
94
|
+
begin
|
|
95
|
+
# Register abort handler if abort controller is provided
|
|
96
|
+
if options.abort_signal
|
|
97
|
+
options.abort_signal.on_abort do
|
|
98
|
+
protocol.abort! rescue nil
|
|
101
99
|
end
|
|
100
|
+
end
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
transport.end_input unless options.has_hooks? || options.has_sdk_mcp_servers?
|
|
102
|
+
protocol.start(streaming: true)
|
|
103
|
+
protocol.send_user_message(prompt)
|
|
106
104
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
end
|
|
111
|
-
ensure
|
|
112
|
-
protocol.stop
|
|
113
|
-
end
|
|
114
|
-
else
|
|
115
|
-
# Simple mode - just send prompt and read responses
|
|
116
|
-
parser = MessageParser.new
|
|
117
|
-
begin
|
|
118
|
-
transport.connect(streaming: false, prompt: prompt)
|
|
105
|
+
protocol.each_message do |message|
|
|
106
|
+
query_logger.debug("query") { "Received: #{message.class.name.split("::").last}" }
|
|
107
|
+
yielder << message
|
|
119
108
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
break
|
|
109
|
+
if message.is_a?(ResultMessage)
|
|
110
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - query_start
|
|
111
|
+
query_logger.info("query") { "Query complete (#{elapsed.round(2)}s, cost=$#{message.total_cost_usd || "?"})" }
|
|
112
|
+
break
|
|
124
113
|
end
|
|
125
|
-
ensure
|
|
126
|
-
transport.close
|
|
127
114
|
end
|
|
115
|
+
ensure
|
|
116
|
+
protocol.stop
|
|
128
117
|
end
|
|
129
118
|
end
|
|
130
119
|
end
|
|
@@ -88,6 +88,9 @@ module ClaudeAgent
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
@connected = true
|
|
91
|
+
logger.info("transport") { "Process spawned (pid=#{@wait_thread&.pid || @process&.pid rescue "?"})" }
|
|
92
|
+
logger.debug("transport") { "Command: #{cmd.join(" ")}" }
|
|
93
|
+
logger.debug("transport") { "Working dir: #{working_directory}" }
|
|
91
94
|
|
|
92
95
|
# Always start stderr reader to prevent pipe buffer from filling up
|
|
93
96
|
start_stderr_reader if @stderr
|
|
@@ -115,6 +118,7 @@ module ClaudeAgent
|
|
|
115
118
|
raise CLIConnectionError, "Not connected" unless @connected
|
|
116
119
|
raise CLIConnectionError, "stdin closed" unless @stdin && !@stdin.closed?
|
|
117
120
|
|
|
121
|
+
logger.debug("transport") { "Write: #{data.bytesize} bytes" }
|
|
118
122
|
@mutex.synchronize do
|
|
119
123
|
@stdin.write(data)
|
|
120
124
|
@stdin.write("\n") unless data.end_with?("\n")
|
|
@@ -139,8 +143,10 @@ module ClaudeAgent
|
|
|
139
143
|
|
|
140
144
|
begin
|
|
141
145
|
message = JSON.parse(line)
|
|
146
|
+
logger.debug("transport") { "Received: #{message["type"] || "unknown"}" }
|
|
142
147
|
yield message
|
|
143
148
|
rescue JSON::ParserError
|
|
149
|
+
logger.warn("transport") { "JSON parse error, buffering (#{@buffer.bytesize} bytes)" }
|
|
144
150
|
# Buffer partial JSON (in case of split lines)
|
|
145
151
|
@buffer << line
|
|
146
152
|
if @buffer.bytesize > @max_buffer_size
|
|
@@ -192,6 +198,7 @@ module ClaudeAgent
|
|
|
192
198
|
@connected = false
|
|
193
199
|
@stdin = @stdout = @stderr = @wait_thread = nil
|
|
194
200
|
|
|
201
|
+
logger.info("transport") { "Transport closed (exit_status=#{exit_status.inspect})" }
|
|
195
202
|
exit_status
|
|
196
203
|
end
|
|
197
204
|
|
|
@@ -264,6 +271,7 @@ module ClaudeAgent
|
|
|
264
271
|
# Force kill the CLI process (SIGKILL)
|
|
265
272
|
# @return [void]
|
|
266
273
|
def kill
|
|
274
|
+
logger.warn("transport") { "Force killing CLI process" }
|
|
267
275
|
# Use custom process kill if available
|
|
268
276
|
if @process&.respond_to?(:kill)
|
|
269
277
|
@mutex.synchronize { @killed = true }
|
|
@@ -302,6 +310,10 @@ module ClaudeAgent
|
|
|
302
310
|
paths.find { |p| !p.empty? && File.executable?(p) } || "claude"
|
|
303
311
|
end
|
|
304
312
|
|
|
313
|
+
def logger
|
|
314
|
+
@options.effective_logger
|
|
315
|
+
end
|
|
316
|
+
|
|
305
317
|
def check_cli_version!
|
|
306
318
|
version_output = `#{@cli_path} -v 2>&1`.strip
|
|
307
319
|
# Parse version like "claude 2.1.0" or just "2.1.0"
|
|
@@ -312,6 +324,7 @@ module ClaudeAgent
|
|
|
312
324
|
end
|
|
313
325
|
|
|
314
326
|
found_version = version_match[1]
|
|
327
|
+
logger.debug("transport") { "CLI version: #{found_version}" }
|
|
315
328
|
unless version_satisfies?(found_version, MINIMUM_CLI_VERSION)
|
|
316
329
|
raise CLIVersionError.new(found_version)
|
|
317
330
|
end
|
data/lib/claude_agent/version.rb
CHANGED
data/lib/claude_agent.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "active_support/core_ext/string/inflections"
|
|
|
4
4
|
require "active_support/core_ext/hash/keys"
|
|
5
5
|
|
|
6
6
|
require_relative "claude_agent/version"
|
|
7
|
+
require_relative "claude_agent/logging"
|
|
7
8
|
require_relative "claude_agent/errors"
|
|
8
9
|
require_relative "claude_agent/types" # TypeScript SDK parity types
|
|
9
10
|
require_relative "claude_agent/sandbox_settings" # Sandbox configuration types
|
data/sig/claude_agent.rbs
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
module ClaudeAgent
|
|
2
2
|
VERSION: String
|
|
3
3
|
|
|
4
|
+
# Null logger that discards all output with zero overhead
|
|
5
|
+
class NullLogger < Logger
|
|
6
|
+
def initialize: () -> void
|
|
7
|
+
def add: (?Integer? severity, ?untyped message, ?String? progname) -> true
|
|
8
|
+
def debug: (*untyped) -> true
|
|
9
|
+
def info: (*untyped) -> true
|
|
10
|
+
def warn: (*untyped) -> true
|
|
11
|
+
def error: (*untyped) -> true
|
|
12
|
+
def fatal: (*untyped) -> true
|
|
13
|
+
def unknown: (*untyped) -> true
|
|
14
|
+
def debug?: () -> false
|
|
15
|
+
def info?: () -> false
|
|
16
|
+
def warn?: () -> false
|
|
17
|
+
def error?: () -> false
|
|
18
|
+
def fatal?: () -> false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Module-level logger
|
|
22
|
+
def self.logger: () -> Logger
|
|
23
|
+
def self.logger=: (Logger logger) -> Logger
|
|
24
|
+
def self.debug!: (?output: IO) -> Logger
|
|
25
|
+
|
|
4
26
|
# One-shot query to Claude Code CLI
|
|
5
27
|
def self.query: (
|
|
6
28
|
prompt: String,
|
|
@@ -316,6 +338,9 @@ module ClaudeAgent
|
|
|
316
338
|
attr_accessor debug: bool
|
|
317
339
|
attr_accessor debug_file: String?
|
|
318
340
|
|
|
341
|
+
# Logging
|
|
342
|
+
attr_accessor logger: Logger?
|
|
343
|
+
|
|
319
344
|
# Abort control (TypeScript SDK parity)
|
|
320
345
|
attr_accessor abort_controller: AbortController?
|
|
321
346
|
|
|
@@ -328,6 +353,7 @@ module ClaudeAgent
|
|
|
328
353
|
def has_hooks?: () -> bool
|
|
329
354
|
def has_sdk_mcp_servers?: () -> bool
|
|
330
355
|
def abort_signal: () -> AbortSignal?
|
|
356
|
+
def effective_logger: () -> Logger
|
|
331
357
|
end
|
|
332
358
|
|
|
333
359
|
# Content block types
|
|
@@ -646,7 +672,7 @@ module ClaudeAgent
|
|
|
646
672
|
|
|
647
673
|
# Message parser
|
|
648
674
|
class MessageParser
|
|
649
|
-
def initialize: () -> void
|
|
675
|
+
def initialize: (?logger: Logger?) -> void
|
|
650
676
|
def parse: (Hash[String, untyped] raw) -> message
|
|
651
677
|
end
|
|
652
678
|
|
|
@@ -1029,7 +1055,7 @@ module ClaudeAgent
|
|
|
1029
1055
|
attr_reader name: String
|
|
1030
1056
|
attr_reader tools: Hash[String, Tool]
|
|
1031
1057
|
|
|
1032
|
-
def initialize: (name: String, ?tools: Array[Tool]) -> void
|
|
1058
|
+
def initialize: (name: String, ?tools: Array[Tool], ?logger: Logger?) -> void
|
|
1033
1059
|
def add_tool: (Tool tool) -> void
|
|
1034
1060
|
def remove_tool: (String name) -> Tool?
|
|
1035
1061
|
def handle_message: (Hash[String, untyped] message) -> Hash[Symbol, untyped]?
|
data/sig/manifest.yaml
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thomas Carr
|
|
@@ -56,6 +56,7 @@ files:
|
|
|
56
56
|
- lib/claude_agent/control_protocol.rb
|
|
57
57
|
- lib/claude_agent/errors.rb
|
|
58
58
|
- lib/claude_agent/hooks.rb
|
|
59
|
+
- lib/claude_agent/logging.rb
|
|
59
60
|
- lib/claude_agent/mcp/server.rb
|
|
60
61
|
- lib/claude_agent/mcp/tool.rb
|
|
61
62
|
- lib/claude_agent/message_parser.rb
|