claude-agent-server 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE +21 -0
- data/README.md +213 -0
- data/config.ru +10 -0
- data/docs/openapi.json +773 -0
- data/exe/claude-agent-server +71 -0
- data/lib/claude_agent_server/app.rb +337 -0
- data/lib/claude_agent_server/config.rb +29 -0
- data/lib/claude_agent_server/errors.rb +51 -0
- data/lib/claude_agent_server/middleware/authentication.rb +57 -0
- data/lib/claude_agent_server/middleware/cors.rb +47 -0
- data/lib/claude_agent_server/middleware/error_handler.rb +46 -0
- data/lib/claude_agent_server/middleware/request_id.rb +22 -0
- data/lib/claude_agent_server/services/message_serializer.rb +165 -0
- data/lib/claude_agent_server/services/options_builder.rb +54 -0
- data/lib/claude_agent_server/services/query_executor.rb +27 -0
- data/lib/claude_agent_server/services/session_manager.rb +199 -0
- data/lib/claude_agent_server/services/sse_stream.rb +71 -0
- data/lib/claude_agent_server/version.rb +5 -0
- data/lib/claude_agent_server.rb +36 -0
- metadata +107 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'claude_agent_sdk'
|
|
4
|
+
|
|
5
|
+
module ClaudeAgentServer
|
|
6
|
+
module Services
|
|
7
|
+
module MessageSerializer
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def serialize(message)
|
|
11
|
+
case message
|
|
12
|
+
when ClaudeAgentSDK::UserMessage
|
|
13
|
+
serialize_user_message(message)
|
|
14
|
+
when ClaudeAgentSDK::AssistantMessage
|
|
15
|
+
serialize_assistant_message(message)
|
|
16
|
+
when ClaudeAgentSDK::ResultMessage
|
|
17
|
+
serialize_result_message(message)
|
|
18
|
+
when ClaudeAgentSDK::TaskStartedMessage
|
|
19
|
+
serialize_task_started_message(message)
|
|
20
|
+
when ClaudeAgentSDK::TaskProgressMessage
|
|
21
|
+
serialize_task_progress_message(message)
|
|
22
|
+
when ClaudeAgentSDK::TaskNotificationMessage
|
|
23
|
+
serialize_task_notification_message(message)
|
|
24
|
+
when ClaudeAgentSDK::SystemMessage
|
|
25
|
+
serialize_system_message(message)
|
|
26
|
+
when ClaudeAgentSDK::StreamEvent
|
|
27
|
+
serialize_stream_event(message)
|
|
28
|
+
when ClaudeAgentSDK::RateLimitEvent
|
|
29
|
+
serialize_rate_limit_event(message)
|
|
30
|
+
else
|
|
31
|
+
{ type: 'unknown', data: message.to_s }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def serialize_user_message(msg)
|
|
36
|
+
{
|
|
37
|
+
type: 'user',
|
|
38
|
+
content: serialize_content(msg.content),
|
|
39
|
+
uuid: msg.uuid,
|
|
40
|
+
parentToolUseId: msg.parent_tool_use_id
|
|
41
|
+
}.compact
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def serialize_assistant_message(msg)
|
|
45
|
+
{
|
|
46
|
+
type: 'assistant',
|
|
47
|
+
content: serialize_content(msg.content),
|
|
48
|
+
model: msg.model,
|
|
49
|
+
parentToolUseId: msg.parent_tool_use_id,
|
|
50
|
+
error: msg.error
|
|
51
|
+
}.compact
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def serialize_result_message(msg)
|
|
55
|
+
{
|
|
56
|
+
type: 'result',
|
|
57
|
+
subtype: msg.subtype,
|
|
58
|
+
durationMs: msg.duration_ms,
|
|
59
|
+
durationApiMs: msg.duration_api_ms,
|
|
60
|
+
isError: msg.is_error,
|
|
61
|
+
numTurns: msg.num_turns,
|
|
62
|
+
sessionId: msg.session_id,
|
|
63
|
+
stopReason: msg.stop_reason,
|
|
64
|
+
totalCostUsd: msg.total_cost_usd,
|
|
65
|
+
usage: msg.usage,
|
|
66
|
+
result: msg.result,
|
|
67
|
+
structuredOutput: msg.structured_output
|
|
68
|
+
}.compact
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def serialize_task_started_message(msg)
|
|
72
|
+
{
|
|
73
|
+
type: 'system',
|
|
74
|
+
subtype: 'task_started',
|
|
75
|
+
taskId: msg.task_id,
|
|
76
|
+
description: msg.description,
|
|
77
|
+
uuid: msg.uuid,
|
|
78
|
+
sessionId: msg.session_id,
|
|
79
|
+
toolUseId: msg.tool_use_id,
|
|
80
|
+
taskType: msg.task_type
|
|
81
|
+
}.compact
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def serialize_task_progress_message(msg)
|
|
85
|
+
{
|
|
86
|
+
type: 'system',
|
|
87
|
+
subtype: 'task_progress',
|
|
88
|
+
taskId: msg.task_id,
|
|
89
|
+
description: msg.description,
|
|
90
|
+
usage: msg.usage,
|
|
91
|
+
uuid: msg.uuid,
|
|
92
|
+
sessionId: msg.session_id,
|
|
93
|
+
toolUseId: msg.tool_use_id,
|
|
94
|
+
lastToolName: msg.last_tool_name
|
|
95
|
+
}.compact
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def serialize_task_notification_message(msg)
|
|
99
|
+
{
|
|
100
|
+
type: 'system',
|
|
101
|
+
subtype: 'task_notification',
|
|
102
|
+
taskId: msg.task_id,
|
|
103
|
+
status: msg.status,
|
|
104
|
+
outputFile: msg.output_file,
|
|
105
|
+
summary: msg.summary,
|
|
106
|
+
uuid: msg.uuid,
|
|
107
|
+
sessionId: msg.session_id,
|
|
108
|
+
toolUseId: msg.tool_use_id,
|
|
109
|
+
usage: msg.usage
|
|
110
|
+
}.compact
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def serialize_system_message(msg)
|
|
114
|
+
{
|
|
115
|
+
type: 'system',
|
|
116
|
+
subtype: msg.subtype,
|
|
117
|
+
data: msg.data
|
|
118
|
+
}.compact
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def serialize_stream_event(msg)
|
|
122
|
+
{
|
|
123
|
+
type: 'stream_event',
|
|
124
|
+
uuid: msg.uuid,
|
|
125
|
+
sessionId: msg.session_id,
|
|
126
|
+
event: msg.event,
|
|
127
|
+
parentToolUseId: msg.parent_tool_use_id
|
|
128
|
+
}.compact
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def serialize_rate_limit_event(msg)
|
|
132
|
+
{
|
|
133
|
+
type: 'rate_limit_event',
|
|
134
|
+
data: msg.data
|
|
135
|
+
}.compact
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def serialize_content(content)
|
|
139
|
+
return content.map { |block| serialize_content_block(block) } if content.is_a?(Array)
|
|
140
|
+
|
|
141
|
+
content
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def serialize_content_block(block)
|
|
145
|
+
case block
|
|
146
|
+
when ClaudeAgentSDK::TextBlock
|
|
147
|
+
{ type: 'text', text: block.text }
|
|
148
|
+
when ClaudeAgentSDK::ThinkingBlock
|
|
149
|
+
{ type: 'thinking', thinking: block.thinking, signature: block.signature }
|
|
150
|
+
when ClaudeAgentSDK::ToolUseBlock
|
|
151
|
+
{ type: 'tool_use', id: block.id, name: block.name, input: block.input }
|
|
152
|
+
when ClaudeAgentSDK::ToolResultBlock
|
|
153
|
+
result = { type: 'tool_result', toolUseId: block.tool_use_id }
|
|
154
|
+
result[:content] = block.content if block.content
|
|
155
|
+
result[:isError] = block.is_error unless block.is_error.nil?
|
|
156
|
+
result
|
|
157
|
+
when ClaudeAgentSDK::UnknownBlock
|
|
158
|
+
block.data
|
|
159
|
+
else
|
|
160
|
+
block
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'claude_agent_sdk'
|
|
4
|
+
|
|
5
|
+
module ClaudeAgentServer
|
|
6
|
+
module Services
|
|
7
|
+
module OptionsBuilder
|
|
8
|
+
# Map of camelCase JSON keys to snake_case ClaudeAgentOptions kwargs
|
|
9
|
+
CAMEL_TO_SNAKE = {
|
|
10
|
+
'allowedTools' => :allowed_tools,
|
|
11
|
+
'disallowedTools' => :disallowed_tools,
|
|
12
|
+
'systemPrompt' => :system_prompt,
|
|
13
|
+
'mcpServers' => :mcp_servers,
|
|
14
|
+
'permissionMode' => :permission_mode,
|
|
15
|
+
'continueConversation' => :continue_conversation,
|
|
16
|
+
'resume' => :resume,
|
|
17
|
+
'maxTurns' => :max_turns,
|
|
18
|
+
'model' => :model,
|
|
19
|
+
'cwd' => :cwd,
|
|
20
|
+
'cliPath' => :cli_path,
|
|
21
|
+
'env' => :env,
|
|
22
|
+
'maxBudgetUsd' => :max_budget_usd,
|
|
23
|
+
'maxThinkingTokens' => :max_thinking_tokens,
|
|
24
|
+
'fallbackModel' => :fallback_model,
|
|
25
|
+
'outputFormat' => :output_format,
|
|
26
|
+
'includePartialMessages' => :include_partial_messages,
|
|
27
|
+
'forkSession' => :fork_session,
|
|
28
|
+
'enableFileCheckpointing' => :enable_file_checkpointing,
|
|
29
|
+
'effort' => :effort,
|
|
30
|
+
'appendAllowedTools' => :append_allowed_tools
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
def build(params)
|
|
36
|
+
kwargs = {}
|
|
37
|
+
|
|
38
|
+
params.each do |key, value|
|
|
39
|
+
snake_key = CAMEL_TO_SNAKE[key.to_s] || key.to_s.gsub(/([A-Z])/, '_\1').downcase.to_sym
|
|
40
|
+
kwargs[snake_key] = value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Merge with server-level defaults
|
|
44
|
+
defaults = ClaudeAgentServer.config.default_sdk_options
|
|
45
|
+
merged = defaults.merge(kwargs)
|
|
46
|
+
|
|
47
|
+
# Force bypassPermissions for HTTP usage (no interactive terminal)
|
|
48
|
+
merged[:permission_mode] ||= 'bypassPermissions'
|
|
49
|
+
|
|
50
|
+
ClaudeAgentSDK::ClaudeAgentOptions.new(**merged)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'claude_agent_sdk'
|
|
4
|
+
|
|
5
|
+
module ClaudeAgentServer
|
|
6
|
+
module Services
|
|
7
|
+
module QueryExecutor
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def execute(prompt:, options:)
|
|
11
|
+
messages = []
|
|
12
|
+
|
|
13
|
+
ClaudeAgentSDK.query(prompt: prompt, options: options) do |message|
|
|
14
|
+
messages << MessageSerializer.serialize(message)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
messages
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def stream(prompt:, options:, &block)
|
|
21
|
+
ClaudeAgentSDK.query(prompt: prompt, options: options) do |message|
|
|
22
|
+
block.call(message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'claude_agent_sdk'
|
|
5
|
+
require 'async'
|
|
6
|
+
require 'async/queue'
|
|
7
|
+
|
|
8
|
+
module ClaudeAgentServer
|
|
9
|
+
module Services
|
|
10
|
+
# An indexed event wrapping an SDK message with a monotonic index
|
|
11
|
+
class SessionEvent
|
|
12
|
+
attr_reader :index, :message, :timestamp
|
|
13
|
+
|
|
14
|
+
def initialize(index:, message:)
|
|
15
|
+
@index = index
|
|
16
|
+
@message = message
|
|
17
|
+
@timestamp = Time.now
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class SessionEntry
|
|
22
|
+
attr_reader :id, :client, :created_at, :status, :events
|
|
23
|
+
attr_accessor :last_activity
|
|
24
|
+
|
|
25
|
+
def initialize(id:, client:)
|
|
26
|
+
@id = id
|
|
27
|
+
@client = client
|
|
28
|
+
@created_at = Time.now
|
|
29
|
+
@last_activity = Time.now
|
|
30
|
+
@status = :connected
|
|
31
|
+
@events = []
|
|
32
|
+
@next_index = 0
|
|
33
|
+
@subscribers = []
|
|
34
|
+
@mutex = Mutex.new
|
|
35
|
+
@reader_task = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def message_count
|
|
39
|
+
@events.size
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Retrieve events by offset and limit (for polling)
|
|
43
|
+
def get_events(offset: 0, limit: nil)
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
slice = @events[offset..] || []
|
|
46
|
+
limit ? slice.first(limit) : slice
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def subscribe(offset: 0, &block)
|
|
51
|
+
queue = Async::Queue.new
|
|
52
|
+
|
|
53
|
+
# Replay missed events from offset
|
|
54
|
+
@mutex.synchronize do
|
|
55
|
+
@subscribers << queue
|
|
56
|
+
@events[offset..]&.each { |evt| queue.enqueue(evt) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
loop do
|
|
61
|
+
event = queue.dequeue
|
|
62
|
+
break if event == :done
|
|
63
|
+
|
|
64
|
+
block.call(event)
|
|
65
|
+
end
|
|
66
|
+
ensure
|
|
67
|
+
@mutex.synchronize { @subscribers.delete(queue) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def broadcast(message)
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
event = SessionEvent.new(index: @next_index, message: message)
|
|
74
|
+
@next_index += 1
|
|
75
|
+
@events << event
|
|
76
|
+
@last_activity = Time.now
|
|
77
|
+
@subscribers.each { |q| q.enqueue(event) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def finish
|
|
82
|
+
@mutex.synchronize do
|
|
83
|
+
@status = :finished
|
|
84
|
+
@subscribers.each { |q| q.enqueue(:done) }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def start_message_reader
|
|
89
|
+
@reader_task = Thread.new do
|
|
90
|
+
Async do
|
|
91
|
+
client.receive_messages do |message|
|
|
92
|
+
broadcast(message)
|
|
93
|
+
break if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
94
|
+
end
|
|
95
|
+
rescue StandardError
|
|
96
|
+
# Reader ended (disconnect or error)
|
|
97
|
+
ensure
|
|
98
|
+
finish
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def disconnect
|
|
104
|
+
@status = :disconnected
|
|
105
|
+
@reader_task&.kill
|
|
106
|
+
@client.disconnect
|
|
107
|
+
@mutex.synchronize do
|
|
108
|
+
@subscribers.each { |q| q.enqueue(:done) }
|
|
109
|
+
@subscribers.clear
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class SessionManager
|
|
115
|
+
attr_reader :sessions
|
|
116
|
+
|
|
117
|
+
def initialize
|
|
118
|
+
@sessions = {}
|
|
119
|
+
@mutex = Mutex.new
|
|
120
|
+
@reaper_task = nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def create_session(options:, prompt: nil, id: nil)
|
|
124
|
+
config = ClaudeAgentServer.config
|
|
125
|
+
id ||= SecureRandom.uuid
|
|
126
|
+
|
|
127
|
+
@mutex.synchronize do
|
|
128
|
+
raise SessionAlreadyExistsError, "Session '#{id}' already exists" if @sessions.key?(id)
|
|
129
|
+
|
|
130
|
+
if @sessions.size >= config.max_sessions
|
|
131
|
+
raise SessionLimitError, "Maximum session limit (#{config.max_sessions}) reached"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
136
|
+
client.connect(prompt)
|
|
137
|
+
|
|
138
|
+
entry = SessionEntry.new(id: id, client: client)
|
|
139
|
+
@mutex.synchronize { @sessions[id] = entry }
|
|
140
|
+
|
|
141
|
+
entry.start_message_reader
|
|
142
|
+
entry
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def get_session(id)
|
|
146
|
+
@mutex.synchronize { @sessions[id] } || raise(SessionNotFoundError, "Session '#{id}' not found")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def destroy_session(id)
|
|
150
|
+
entry = @mutex.synchronize { @sessions.delete(id) }
|
|
151
|
+
raise SessionNotFoundError, "Session '#{id}' not found" unless entry
|
|
152
|
+
|
|
153
|
+
entry.disconnect
|
|
154
|
+
entry
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def list_sessions
|
|
158
|
+
@mutex.synchronize { @sessions.values.dup }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def start_reaper
|
|
162
|
+
@reaper_task = Async do |task|
|
|
163
|
+
loop do
|
|
164
|
+
task.sleep(60)
|
|
165
|
+
reap_expired_sessions
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def stop_reaper
|
|
171
|
+
@reaper_task&.stop
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def shutdown
|
|
175
|
+
stop_reaper
|
|
176
|
+
@mutex.synchronize do
|
|
177
|
+
@sessions.each_value(&:disconnect)
|
|
178
|
+
@sessions.clear
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def reap_expired_sessions
|
|
185
|
+
ttl = ClaudeAgentServer.config.session_ttl
|
|
186
|
+
now = Time.now
|
|
187
|
+
|
|
188
|
+
expired_ids = @mutex.synchronize do
|
|
189
|
+
@sessions.select { |_, entry| now - entry.last_activity > ttl }.keys
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
expired_ids.each do |id|
|
|
193
|
+
entry = @mutex.synchronize { @sessions.delete(id) }
|
|
194
|
+
entry&.disconnect
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module ClaudeAgentServer
|
|
6
|
+
module Services
|
|
7
|
+
module SseStream
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def stream_query(prompt:, options:)
|
|
11
|
+
index = 0
|
|
12
|
+
StreamBody.new do |stream|
|
|
13
|
+
QueryExecutor.stream(prompt: prompt, options: options) do |message|
|
|
14
|
+
serialized = MessageSerializer.serialize(message)
|
|
15
|
+
event_type = serialized[:type] || 'message'
|
|
16
|
+
stream.write(format_sse(event_type, serialized, id: index))
|
|
17
|
+
index += 1
|
|
18
|
+
end
|
|
19
|
+
stream.write(format_sse('done', { status: 'complete' }))
|
|
20
|
+
rescue IOError, Errno::EPIPE
|
|
21
|
+
# Client disconnected — exit cleanly
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stream_session(session_entry, last_event_id: nil)
|
|
26
|
+
offset = last_event_id ? last_event_id.to_i + 1 : 0
|
|
27
|
+
|
|
28
|
+
StreamBody.new do |stream|
|
|
29
|
+
session_entry.subscribe(offset: offset) do |event|
|
|
30
|
+
serialized = MessageSerializer.serialize(event.message)
|
|
31
|
+
event_type = serialized[:type] || 'message'
|
|
32
|
+
stream.write(format_sse(event_type, serialized, id: event.index))
|
|
33
|
+
end
|
|
34
|
+
stream.write(format_sse('done', { status: 'complete' }))
|
|
35
|
+
rescue IOError, Errno::EPIPE
|
|
36
|
+
# Client disconnected — session stays alive
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def format_sse(event, data, id: nil)
|
|
41
|
+
parts = []
|
|
42
|
+
parts << "id: #{id}" unless id.nil?
|
|
43
|
+
parts << "event: #{event}"
|
|
44
|
+
parts << "data: #{JSON.generate(data)}"
|
|
45
|
+
"#{parts.join("\n")}\n\n"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Rack 3 streaming body that yields chunks via a fiber
|
|
49
|
+
class StreamBody
|
|
50
|
+
def initialize(&block)
|
|
51
|
+
@block = block
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def each(&chunk_block)
|
|
55
|
+
stream = StreamWriter.new(chunk_block)
|
|
56
|
+
@block.call(stream)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class StreamWriter
|
|
61
|
+
def initialize(chunk_block)
|
|
62
|
+
@chunk_block = chunk_block
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def write(data)
|
|
66
|
+
@chunk_block.call(data)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'claude_agent_server/version'
|
|
4
|
+
require_relative 'claude_agent_server/errors'
|
|
5
|
+
require_relative 'claude_agent_server/config'
|
|
6
|
+
require_relative 'claude_agent_server/services/options_builder'
|
|
7
|
+
require_relative 'claude_agent_server/services/message_serializer'
|
|
8
|
+
require_relative 'claude_agent_server/services/query_executor'
|
|
9
|
+
require_relative 'claude_agent_server/services/sse_stream'
|
|
10
|
+
require_relative 'claude_agent_server/services/session_manager'
|
|
11
|
+
require_relative 'claude_agent_server/middleware/request_id'
|
|
12
|
+
require_relative 'claude_agent_server/middleware/error_handler'
|
|
13
|
+
require_relative 'claude_agent_server/middleware/authentication'
|
|
14
|
+
require_relative 'claude_agent_server/middleware/cors'
|
|
15
|
+
require_relative 'claude_agent_server/app'
|
|
16
|
+
|
|
17
|
+
module ClaudeAgentServer
|
|
18
|
+
@config = Config.new
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
attr_reader :config
|
|
22
|
+
|
|
23
|
+
def configure
|
|
24
|
+
yield @config if block_given?
|
|
25
|
+
@config
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset_config!
|
|
29
|
+
@config = Config.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def app
|
|
33
|
+
App.freeze.app
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: claude-agent-server
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Community Contributors
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-03-06 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: claude-agent-sdk
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.8'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.8'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: falcon
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.48'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.48'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: roda
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.85'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.85'
|
|
54
|
+
description: REST + SSE HTTP wrapper for claude-agent-sdk. Exposes Claude Code as
|
|
55
|
+
a network service with session management, streaming, and authentication.
|
|
56
|
+
email: []
|
|
57
|
+
executables:
|
|
58
|
+
- claude-agent-server
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- CHANGELOG.md
|
|
63
|
+
- LICENSE
|
|
64
|
+
- README.md
|
|
65
|
+
- config.ru
|
|
66
|
+
- docs/openapi.json
|
|
67
|
+
- exe/claude-agent-server
|
|
68
|
+
- lib/claude_agent_server.rb
|
|
69
|
+
- lib/claude_agent_server/app.rb
|
|
70
|
+
- lib/claude_agent_server/config.rb
|
|
71
|
+
- lib/claude_agent_server/errors.rb
|
|
72
|
+
- lib/claude_agent_server/middleware/authentication.rb
|
|
73
|
+
- lib/claude_agent_server/middleware/cors.rb
|
|
74
|
+
- lib/claude_agent_server/middleware/error_handler.rb
|
|
75
|
+
- lib/claude_agent_server/middleware/request_id.rb
|
|
76
|
+
- lib/claude_agent_server/services/message_serializer.rb
|
|
77
|
+
- lib/claude_agent_server/services/options_builder.rb
|
|
78
|
+
- lib/claude_agent_server/services/query_executor.rb
|
|
79
|
+
- lib/claude_agent_server/services/session_manager.rb
|
|
80
|
+
- lib/claude_agent_server/services/sse_stream.rb
|
|
81
|
+
- lib/claude_agent_server/version.rb
|
|
82
|
+
homepage: https://github.com/ya-luotao/claude-agent-server-ruby
|
|
83
|
+
licenses:
|
|
84
|
+
- MIT
|
|
85
|
+
metadata:
|
|
86
|
+
homepage_uri: https://github.com/ya-luotao/claude-agent-server-ruby
|
|
87
|
+
source_code_uri: https://github.com/ya-luotao/claude-agent-server-ruby
|
|
88
|
+
changelog_uri: https://github.com/ya-luotao/claude-agent-server-ruby/blob/main/CHANGELOG.md
|
|
89
|
+
rubygems_mfa_required: 'true'
|
|
90
|
+
rdoc_options: []
|
|
91
|
+
require_paths:
|
|
92
|
+
- lib
|
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: 3.2.0
|
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
requirements: []
|
|
104
|
+
rubygems_version: 3.6.2
|
|
105
|
+
specification_version: 4
|
|
106
|
+
summary: HTTP server wrapping the Claude Agent Ruby SDK
|
|
107
|
+
test_files: []
|