ask-acp 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 847f6fef76788fc8ddb79a41f6d071ed5f9c62ea05e191b713a628afe0325614
4
+ data.tar.gz: 81692ef719aff5a322b5fa9ae34dffad897d93f1b7c43acc661e369bdb573858
5
+ SHA512:
6
+ metadata.gz: 36000307696429f9d664b2d0e3d6e750ac146089b7f7dcf8d94fafd7392dc332e58973f824fc19d182d13ed7391abb1f89a6153c7e5a27d800c100f71fac26e8
7
+ data.tar.gz: e6ec8427722c7444cb2e9b1d2a06538d924f49530bf48795b719233d9372cf0a5e298c40ac4a807afd0e22883e4059b5a3368bf9964af8e6f618965b561a0c72
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+
6
+ module Ask
7
+ module ACP
8
+ # ACP client that connects to a coding agent over stdio using JSON-RPC 2.0.
9
+ #
10
+ # Spawns the agent CLI as a subprocess and communicates via stdin/stdout.
11
+ # Supports all ACP methods: initialize, session/new, session/prompt, etc.
12
+ #
13
+ # @example
14
+ # client = Ask::ACP::Client.new(command: ["codex", "acp"])
15
+ # client.start
16
+ # result = client.initialize!(name: "askoda", version: "0.1.0")
17
+ # session = client.session_new(cwd: "/tmp")
18
+ # client.session_prompt(session[:id], "Hello") { |event| puts event }
19
+ # client.stop
20
+ class Client
21
+ attr_reader :command, :running
22
+
23
+ def initialize(command:, request_timeout: 30.0)
24
+ @command = command
25
+ @request_timeout = request_timeout
26
+ @stdin = nil
27
+ @next_id = 1
28
+ @pending = {}
29
+ @event_handlers = []
30
+ @running = false
31
+ @mutex = Mutex.new
32
+ @initialized = false
33
+ end
34
+
35
+ # Start the agent subprocess and begin reading stdout.
36
+ def start
37
+ @mutex.synchronize do
38
+ return if @running
39
+ @stdin, stdout, stderr, @wait_thr = Open3.popen3(*@command)
40
+
41
+ @stdout_thread = Thread.new(stdout) do |io|
42
+ io.each_line do |line|
43
+ line = line.strip
44
+ next if line.empty?
45
+ handle_message(Protocol.parse(line))
46
+ end
47
+ end
48
+
49
+ # Drain stderr to prevent deadlocks
50
+ Thread.new(stderr) { |io| io.each_line { |_| } }
51
+
52
+ @running = true
53
+ end
54
+ end
55
+
56
+ # Stop the agent subprocess.
57
+ def stop
58
+ @mutex.synchronize do
59
+ return unless @running
60
+ @running = false
61
+ @initialized = false
62
+ @stdin&.close rescue nil
63
+ @stdout_thread&.join(3) rescue nil
64
+ Process.kill("TERM", @wait_thr.pid) rescue nil
65
+ @wait_thr&.join(5) rescue nil
66
+ @stdin = nil
67
+ @pending.each_value { |f| f[:error] = Error.new("process exited"); f[:done] = true; f[:condition].signal }
68
+ @pending.clear
69
+ end
70
+ end
71
+
72
+ def running?
73
+ @running && @wait_thr&.alive?
74
+ end
75
+
76
+ # Register a handler for incoming notifications (client methods from agent).
77
+ def on_notification(&handler)
78
+ @mutex.synchronize { @event_handlers << handler }
79
+ end
80
+
81
+ # ── ACP Methods ──
82
+
83
+ # Initialize handshake. Must be called first.
84
+ def initialize!(client_name:, client_version:, capabilities: {})
85
+ result = request(Protocol::AGENT_METHODS[:initialize], {
86
+ protocolVersion: Protocol::PROTOCOL_VERSION,
87
+ clientInfo: { name: client_name, version: client_version },
88
+ capabilities: capabilities
89
+ })
90
+ @initialized = true
91
+ result
92
+ end
93
+
94
+ # Authenticate with the agent (if required).
95
+ def authenticate(token:, scheme: "bearer")
96
+ request(Protocol::AGENT_METHODS[:authenticate], {
97
+ credentials: { scheme: scheme, token: token }
98
+ })
99
+ end
100
+
101
+ # Create a new session in the given working directory.
102
+ # Returns { id:, status:, created_at: }.
103
+ def session_new(cwd: ".", model: nil, tools: nil)
104
+ ensure_initialized!
105
+ params = { cwd: cwd, mcpServers: [] }
106
+ params[:model] = model if model
107
+ result = request(Protocol::AGENT_METHODS[:session_new], params)
108
+ normalize_session(result)
109
+ end
110
+
111
+ # Load an existing session by ID.
112
+ def session_load(session_id)
113
+ ensure_initialized!
114
+ result = request(Protocol::AGENT_METHODS[:session_load], { sessionId: session_id })
115
+ normalize_session(result)
116
+ end
117
+
118
+ # List sessions.
119
+ def session_list(cwd: nil)
120
+ ensure_initialized!
121
+ params = {}
122
+ params[:cwd] = cwd if cwd
123
+ result = request(Protocol::AGENT_METHODS[:session_list], params)
124
+ (result["sessions"] || result[:sessions] || []).map { |s| normalize_session(s) }
125
+ end
126
+
127
+ # Fork a session from an existing one.
128
+ def session_fork(session_id)
129
+ ensure_initialized!
130
+ result = request(Protocol::AGENT_METHODS[:session_fork], { sessionId: session_id })
131
+ normalize_session(result)
132
+ end
133
+
134
+ # Resume a session.
135
+ def session_resume(session_id)
136
+ ensure_initialized!
137
+ result = request(Protocol::AGENT_METHODS[:session_resume], { sessionId: session_id })
138
+ normalize_session(result)
139
+ end
140
+
141
+ # Close a session.
142
+ def session_close(session_id)
143
+ ensure_initialized!
144
+ request(Protocol::AGENT_METHODS[:session_close], { sessionId: session_id })
145
+ end
146
+
147
+ # Send a prompt to a session and stream events via the block.
148
+ # The prompt is automatically wrapped in a ContentBlock array if given
149
+ # as a plain string.
150
+ #
151
+ # @yield [Hash] event with :method and :params
152
+ # @return [Hash] the final session/prompt response
153
+ def session_prompt(session_id, prompt, timeout: nil, &block)
154
+ ensure_initialized!
155
+ prompt_blocks = prompt.is_a?(Array) ? prompt : [{ type: "text", text: prompt.to_s }]
156
+ params = { sessionId: session_id, prompt: prompt_blocks }
157
+ if block
158
+ # Register a temporary handler for streaming events
159
+ handler = ->(msg) { block.call(method: msg["method"], params: msg["params"] || {}) if msg["method"] }
160
+ on_notification(&handler)
161
+ begin
162
+ request(Protocol::AGENT_METHODS[:session_prompt], params, timeout: timeout)
163
+ ensure
164
+ @mutex.synchronize { @event_handlers.delete(handler) }
165
+ end
166
+ else
167
+ request(Protocol::AGENT_METHODS[:session_prompt], params, timeout: timeout)
168
+ end
169
+ end
170
+
171
+ # Cancel the current prompt execution.
172
+ def session_cancel(session_id)
173
+ ensure_initialized!
174
+ request(Protocol::AGENT_METHODS[:session_cancel], { sessionId: session_id })
175
+ end
176
+
177
+ # Set a config option on a session.
178
+ def session_set_config_option(session_id, key, value)
179
+ ensure_initialized!
180
+ request(Protocol::AGENT_METHODS[:session_set_config_option], {
181
+ sessionId: session_id, key: key, value: value
182
+ })
183
+ end
184
+
185
+ # Set the mode on a session.
186
+ def session_set_mode(session_id, mode)
187
+ ensure_initialized!
188
+ request(Protocol::AGENT_METHODS[:session_set_mode], {
189
+ sessionId: session_id, mode: mode
190
+ })
191
+ end
192
+
193
+ # Set the model on a session.
194
+ def session_set_model(session_id, provider:, model:)
195
+ ensure_initialized!
196
+ request(Protocol::AGENT_METHODS[:session_set_model], {
197
+ sessionId: session_id, provider: provider, model: model
198
+ })
199
+ end
200
+
201
+ # Send a JSON-RPC request and wait for the response.
202
+ def request(method, params = nil, timeout: nil)
203
+ ensure_running!
204
+ future = { done: false, result: nil, error: nil, condition: ConditionVariable.new }
205
+
206
+ @mutex.synchronize do
207
+ id = @next_id
208
+ @next_id += 1
209
+ @pending[id] = future
210
+ write(Protocol.build_request(method, params, id: id))
211
+ end
212
+
213
+ timeout ||= @request_timeout
214
+ deadline = Time.now + timeout
215
+
216
+ @mutex.synchronize do
217
+ until future[:done]
218
+ remaining = deadline - Time.now
219
+ raise TimeoutError, "Request timed out after #{timeout}s" if remaining <= 0
220
+ future[:condition].wait(@mutex, remaining)
221
+ end
222
+ end
223
+
224
+ raise future[:error] if future[:error]
225
+ future[:result]
226
+ end
227
+
228
+ private
229
+
230
+ def ensure_running!
231
+ raise Error, "ACP client not started. Call #start first." unless running?
232
+ end
233
+
234
+ def ensure_initialized!
235
+ ensure_running!
236
+ raise Error, "ACP not initialized. Call #initialize! first." unless @initialized
237
+ end
238
+
239
+ def write(msg)
240
+ @stdin&.puts(JSON.generate(msg))
241
+ @stdin&.flush
242
+ end
243
+
244
+ def handle_message(msg)
245
+ return unless msg
246
+
247
+ # Response to a pending request
248
+ if msg.key?("id")
249
+ rid = msg["id"]
250
+ future = nil
251
+ @mutex.synchronize { future = @pending.delete(rid) }
252
+
253
+ if future
254
+ @mutex.synchronize do
255
+ if msg.key?("error")
256
+ err = msg["error"]
257
+ future[:error] = Error.new("[#{err["code"]}] #{err["message"]}")
258
+ elsif msg.key?("result")
259
+ future[:result] = msg["result"]
260
+ else
261
+ future[:error] = Error.new("No result or error")
262
+ end
263
+ future[:done] = true
264
+ future[:condition].signal
265
+ end
266
+ return
267
+ end
268
+ end
269
+
270
+ # Notification or event (no id or unmatched id)
271
+ dispatch_event(msg)
272
+ end
273
+
274
+ def dispatch_event(msg)
275
+ @event_handlers.dup.each { |h| h.call(msg) rescue nil }
276
+ end
277
+
278
+ def normalize_session(result)
279
+ s = result["session"] || result[:session] || result
280
+ {
281
+ id: s["id"] || s["sessionId"],
282
+ status: s["status"] || "unknown",
283
+ created_at: s["createdAt"] || s["created_at"]
284
+ }
285
+ end
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ask
6
+ module ACP
7
+ # Protocol constants and message helpers for the Agent Client Protocol (ACP).
8
+ #
9
+ # ACP uses JSON-RPC 2.0 over stdio (newline-delimited JSON).
10
+ # Protocol version: 1 (v0.11.3 schema)
11
+ module Protocol
12
+ PROTOCOL_VERSION = 1
13
+
14
+ # Agent methods (client → agent)
15
+ AGENT_METHODS = {
16
+ initialize: "initialize",
17
+ authenticate: "authenticate",
18
+ logout: "logout",
19
+ session_cancel: "session/cancel",
20
+ session_close: "session/close",
21
+ session_fork: "session/fork",
22
+ session_list: "session/list",
23
+ session_load: "session/load",
24
+ session_new: "session/new",
25
+ session_prompt: "session/prompt",
26
+ session_resume: "session/resume",
27
+ session_set_config_option: "session/set_config_option",
28
+ session_set_mode: "session/set_mode",
29
+ session_set_model: "session/set_model"
30
+ }.freeze
31
+
32
+ # Client methods (agent → client, for tool execution and elicitation)
33
+ CLIENT_METHODS = {
34
+ fs_read_text_file: "fs/read_text_file",
35
+ fs_write_text_file: "fs/write_text_file",
36
+ session_elicitation: "session/elicitation",
37
+ session_elicitation_complete: "session/elicitation/complete",
38
+ session_request_permission: "session/request_permission",
39
+ session_update: "session/update",
40
+ terminal_create: "terminal/create",
41
+ terminal_kill: "terminal/kill",
42
+ terminal_output: "terminal/output",
43
+ terminal_release: "terminal/release",
44
+ terminal_wait_for_exit: "terminal/wait_for_exit"
45
+ }.freeze
46
+
47
+ # Prompt response event types (streamed from session/prompt)
48
+ PROMPT_EVENTS = {
49
+ text: "text",
50
+ tool_use: "tool_use",
51
+ tool_result: "tool_result",
52
+ turn_complete: "turn_complete",
53
+ turn_failed: "turn_failed",
54
+ session_update: "session/update",
55
+ permission_request: "session/request_permission",
56
+ elicitation: "session/elicitation"
57
+ }.freeze
58
+
59
+ STATUSES = {
60
+ completed: "completed",
61
+ failed: "failed",
62
+ cancelled: "cancelled",
63
+ in_progress: "in_progress"
64
+ }.freeze
65
+
66
+ # Build a JSON-RPC 2.0 request.
67
+ def self.build_request(method, params = nil, id: nil)
68
+ msg = { jsonrpc: "2.0", method: method }
69
+ msg[:id] = id
70
+ msg[:params] = params if params
71
+ msg
72
+ end
73
+
74
+ # Build a JSON-RPC 2.0 notification (no id).
75
+ def self.build_notification(method, params = nil)
76
+ msg = { jsonrpc: "2.0", method: method }
77
+ msg[:params] = params if params
78
+ msg
79
+ end
80
+
81
+ # Build a JSON-RPC 2.0 success response.
82
+ def self.build_response(id, result)
83
+ { jsonrpc: "2.0", id: id, result: result }
84
+ end
85
+
86
+ # Build a JSON-RPC 2.0 error response.
87
+ def self.build_error(id, code, message, data: nil)
88
+ err = { code: code, message: message }
89
+ err[:data] = data if data
90
+ { jsonrpc: "2.0", id: id, error: err }
91
+ end
92
+
93
+ # Parse a JSON-RPC message from a line.
94
+ # Returns a hash with string keys, or nil if invalid.
95
+ def self.parse(line)
96
+ return nil if line.nil? || line.strip.empty?
97
+ JSON.parse(line)
98
+ rescue JSON::ParserError
99
+ nil
100
+ end
101
+
102
+ # Serialize a message to a JSON line (with newline).
103
+ def self.serialize(msg)
104
+ JSON.generate(msg) + "\n"
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ask
6
+ module ACP
7
+ # ACP client that replays pre-recorded interactions from a fixture file.
8
+ #
9
+ # Like VCR for stdio — no subprocess, instant response, deterministic.
10
+ # Records are newline-delimited JSON with format:
11
+ # {"request": {...}} ← request sent to agent
12
+ # {"response": {...}} ← response from agent (or mock)
13
+ # {"notification": {...}} ← async notification from agent
14
+ #
15
+ # @example
16
+ # client = Ask::ACP::ReplayClient.new(fixture: "test/fixtures/opencode_session.jsonl")
17
+ # client.start
18
+ # client.initialize!(client_name: "test", client_version: "0.1.0")
19
+ class ReplayClient
20
+ attr_reader :fixture_path, :running
21
+
22
+ def initialize(fixture_path:)
23
+ @fixture_path = fixture_path
24
+ @events = []
25
+ @index = 0
26
+ @running = false
27
+ @initialized = false
28
+ @event_handlers = []
29
+ @pending = {}
30
+ @next_id = 1
31
+ @mutex = Mutex.new
32
+ end
33
+
34
+ def start
35
+ load_fixture
36
+ @running = true
37
+ end
38
+
39
+ def stop
40
+ @running = false
41
+ @initialized = false
42
+ end
43
+
44
+ def running?
45
+ @running
46
+ end
47
+
48
+ def on_notification(&handler)
49
+ @mutex.synchronize { @event_handlers << handler }
50
+ end
51
+
52
+ # ── ACP Methods (same interface as Client) ──
53
+
54
+ def initialize!(client_name:, client_version:, capabilities: {})
55
+ @initialized = true
56
+ make_request("initialize", {
57
+ protocolVersion: 1, clientInfo: { name: client_name, version: client_version }, capabilities: capabilities
58
+ })
59
+ end
60
+
61
+ def session_new(cwd: ".", model: nil)
62
+ ensure_initialized!
63
+ params = { cwd: cwd, mcpServers: [] }
64
+ params[:model] = model if model
65
+ normalize_session(make_request(Protocol::AGENT_METHODS[:session_new], params))
66
+ end
67
+
68
+ def session_load(session_id)
69
+ ensure_initialized!
70
+ result = make_request(Protocol::AGENT_METHODS[:session_load], { sessionId: session_id })
71
+ normalize_session(result)
72
+ end
73
+
74
+ def session_list(cwd: nil)
75
+ ensure_initialized!
76
+ params = {}
77
+ params[:cwd] = cwd if cwd
78
+ result = make_request(Protocol::AGENT_METHODS[:session_list], params)
79
+ (result["sessions"] || result[:sessions] || []).map { |s| normalize_session(s) }
80
+ end
81
+
82
+ def session_resume(session_id)
83
+ ensure_initialized!
84
+ result = make_request(Protocol::AGENT_METHODS[:session_resume], { sessionId: session_id })
85
+ normalize_session(result)
86
+ end
87
+
88
+ def session_close(session_id)
89
+ ensure_initialized!
90
+ make_request(Protocol::AGENT_METHODS[:session_close], { sessionId: session_id })
91
+ end
92
+
93
+ def session_prompt(session_id, prompt, timeout: nil, &block)
94
+ ensure_initialized!
95
+ prompt_blocks = prompt.is_a?(Array) ? prompt : [{ type: "text", text: prompt.to_s }]
96
+ params = { sessionId: session_id, prompt: prompt_blocks }
97
+
98
+ # Dispatch any notifications before the response
99
+ while @index < @events.length
100
+ event = @events[@index]
101
+ break if event.key?("response")
102
+ if event.key?("notification")
103
+ dispatch_event(event["notification"])
104
+ end
105
+ @index += 1
106
+ end
107
+
108
+ # Get the response
109
+ event = @events[@index]
110
+ @index += 1
111
+ if event && event["response"]
112
+ result = event["response"]["result"]
113
+ error = event["response"]["error"]
114
+ raise Error.new("[#{error["code"]}] #{error["message"]}") if error
115
+ result
116
+ else
117
+ { "status" => "completed" }
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def ensure_initialized!
124
+ raise Error, "ReplayClient not started. Call #start first." unless @running
125
+ raise Error, "Not initialized. Call #initialize! first." unless @initialized
126
+ end
127
+
128
+ def load_fixture
129
+ @events = []
130
+ File.readlines(@fixture_path).each do |line|
131
+ line = line.strip
132
+ next if line.empty?
133
+ @events << JSON.parse(line)
134
+ end
135
+ @index = 0
136
+ rescue => e
137
+ raise Error, "Failed to load fixture #{@fixture_path}: #{e.message}"
138
+ end
139
+
140
+ def make_request(method, params)
141
+ # Look for the next response event matching this method
142
+ while @index < @events.length
143
+ event = @events[@index]
144
+ @index += 1
145
+
146
+ if event["response"]
147
+ result = event["response"]["result"]
148
+ error = event["response"]["error"]
149
+ raise Error.new("[#{error["code"]}] #{error["message"]}") if error
150
+ return result
151
+ end
152
+ end
153
+ {}
154
+ end
155
+
156
+ def dispatch_event(msg)
157
+ @event_handlers.dup.each { |h| h.call(msg) rescue nil }
158
+ end
159
+
160
+ def normalize_session(result)
161
+ s = result["session"] || result[:session] || result
162
+ {
163
+ id: s["id"] || s["sessionId"],
164
+ status: s["status"] || "unknown",
165
+ created_at: s["createdAt"] || s["created_at"]
166
+ }
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ask
6
+ module ACP
7
+ # ACP server that lets a Ruby agent speak the Agent Client Protocol over stdio.
8
+ #
9
+ # Handles the JSON-RPC 2.0 transport, method dispatch, and streaming responses.
10
+ # Subclass and implement the method handlers you need.
11
+ #
12
+ # @example
13
+ # class MyAgent < Ask::ACP::Server
14
+ # def handle_initialize(params)
15
+ # { protocolVersion: 1, serverInfo: { name: "my-agent", version: "0.1.0" } }
16
+ # end
17
+ #
18
+ # def handle_session_new(params)
19
+ # { session: { id: SecureRandom.uuid, status: "running" } }
20
+ # end
21
+ # end
22
+ #
23
+ # MyAgent.new.run
24
+ class Server
25
+ def initialize
26
+ @methods = {}
27
+ register_defaults
28
+ end
29
+
30
+ # Start reading from stdin and processing requests.
31
+ def run
32
+ $stdin.each_line do |line|
33
+ line = line.strip
34
+ next if line.empty?
35
+ handle(Protocol.parse(line))
36
+ end
37
+ rescue => e
38
+ $stderr.puts "ACP server error: #{e.message}"
39
+ end
40
+
41
+ private
42
+
43
+ def register_defaults
44
+ register("initialize") do |params|
45
+ {
46
+ protocolVersion: Protocol::PROTOCOL_VERSION,
47
+ capabilities: {},
48
+ serverInfo: { name: "ask-acp", version: VERSION }
49
+ }
50
+ end
51
+
52
+ register("session/new") do |params|
53
+ handle_session_new(params)
54
+ end
55
+
56
+ register("session/load") do |params|
57
+ handle_session_load(params)
58
+ end
59
+
60
+ register("session/list") do |params|
61
+ handle_session_list(params)
62
+ end
63
+
64
+ register("session/resume") do |params|
65
+ handle_session_resume(params)
66
+ end
67
+
68
+ register("session/close") do |params|
69
+ handle_session_close(params)
70
+ end
71
+
72
+ register("session/prompt") do |params|
73
+ handle_session_prompt(params)
74
+ end
75
+
76
+ register("session/cancel") do |params|
77
+ handle_session_cancel(params)
78
+ end
79
+ end
80
+
81
+ # Default handlers — override in subclasses.
82
+
83
+ def handle_initialize(params)
84
+ { protocolVersion: Protocol::PROTOCOL_VERSION, capabilities: {}, serverInfo: { name: "ask-acp", version: VERSION } }
85
+ end
86
+
87
+ def handle_session_new(params)
88
+ require "securerandom"
89
+ id = SecureRandom.uuid
90
+ { session: { id: id, status: "running" } }
91
+ end
92
+
93
+ def handle_session_load(params)
94
+ { session: { id: params["sessionId"], status: "running" } }
95
+ end
96
+
97
+ def handle_session_list(params)
98
+ { sessions: [] }
99
+ end
100
+
101
+ def handle_session_resume(params)
102
+ { session: { id: params["sessionId"], status: "running" } }
103
+ end
104
+
105
+ def handle_session_close(params)
106
+ {}
107
+ end
108
+
109
+ def handle_session_prompt(params)
110
+ # Default: stream back a completion event immediately
111
+ send_text_delta(params["sessionId"], "Hello from ACP server!")
112
+ send_event("turn_complete", { sessionId: params["sessionId"] })
113
+ { status: "completed" }
114
+ end
115
+
116
+ def handle_session_cancel(params)
117
+ {}
118
+ end
119
+
120
+ def register(method, &handler)
121
+ @methods[method] = handler
122
+ end
123
+
124
+ def handle(msg)
125
+ return unless msg
126
+
127
+ if msg.key?("id")
128
+ id = msg["id"]
129
+ method = msg["method"]
130
+ params = msg["params"] || {}
131
+
132
+ handler = @methods[method]
133
+ if handler
134
+ begin
135
+ result = handler.call(params)
136
+ write(Protocol.build_response(id, result))
137
+ rescue => e
138
+ write(Protocol.build_error(id, -32_600, e.message))
139
+ end
140
+ else
141
+ write(Protocol.build_error(id, -32_601, "Unknown method: #{method}"))
142
+ end
143
+ end
144
+ end
145
+
146
+ def write(msg)
147
+ $stdout.puts(JSON.generate(msg))
148
+ $stdout.flush
149
+ end
150
+
151
+ def send_event(method, params)
152
+ write(Protocol.build_notification(method, params))
153
+ end
154
+
155
+ def send_text_delta(session_id, content)
156
+ send_event("text", { sessionId: session_id, content: content })
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module ACP
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/ask/acp.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "acp/version"
4
+ require_relative "acp/protocol"
5
+ require_relative "acp/client"
6
+ require_relative "acp/replay_client"
7
+ require_relative "acp/server"
8
+
9
+ module Ask
10
+ module ACP
11
+ class Error < StandardError; end
12
+ class TimeoutError < Error; end
13
+ end
14
+ end
data/lib/ask-acp.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ask/acp"
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ask-acp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaka Ruto
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.25'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.25'
40
+ - !ruby/object:Gem::Dependency
41
+ name: mocha
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.1'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: simplecov
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.22'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.22'
82
+ description: Ruby implementation of the Agent Client Protocol (ACP) — a JSON-RPC standard
83
+ for agent-editor communication. Includes client (connect to ACP agents), server
84
+ (host Ruby agents via ACP), and transport layer.
85
+ email:
86
+ - kaka@myrrlabs.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - lib/ask-acp.rb
92
+ - lib/ask/acp.rb
93
+ - lib/ask/acp/client.rb
94
+ - lib/ask/acp/protocol.rb
95
+ - lib/ask/acp/replay_client.rb
96
+ - lib/ask/acp/server.rb
97
+ - lib/ask/acp/version.rb
98
+ homepage: https://github.com/ask-rb/ask-acp
99
+ licenses:
100
+ - MIT
101
+ metadata: {}
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '3.2'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 4.0.3
117
+ specification_version: 4
118
+ summary: Agent Client Protocol for Ruby
119
+ test_files: []