opencode-ruby 0.0.1.alpha6 → 0.0.1.alpha9
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 +47 -1
- data/README.md +54 -9
- data/examples/conversation_recipe.rb +3 -2
- data/lib/opencode/client.rb +94 -40
- data/lib/opencode/version.rb +1 -1
- data/lib/opencode-ruby.rb +2 -0
- data/opencode-ruby.gemspec +3 -7
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0723db2b0d77a65c6718f0dcc49b67d298a62c33996169a697eb47c9436980a
|
|
4
|
+
data.tar.gz: c0446dbb4539a3432e6d0b71f4790b6c16377ac8aad32406c6f178b04c1dbf54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73205b52ce2f279b8daee9f7274f52d3ff2382cba757f8af2cfce0d2addb0e67c1bf7b684f6575b9161af53678bccfd0ff40c154afe2336b4202fff5f2085f7a
|
|
7
|
+
data.tar.gz: 98d7e60949dcad273ab8533c4652902084f0df117cb52ec522ee7a9cdf8bf35906093479634350b395ea1159d9d881e1b683c70a53fd7be5f5815d4d96cb0d84
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.1.alpha9 - 2026-07-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Parse SSE events with CRLF, CR-only, or LF framing; accept `data:` fields
|
|
8
|
+
with or without the optional leading space; join multiple `data:` fields;
|
|
9
|
+
ignore the stream's optional leading UTF-8 BOM and comment fields; and decode
|
|
10
|
+
correctly when a transport chunk splits any byte of the event framing.
|
|
11
|
+
- Keep request instrumentation free of query credentials and private workspace
|
|
12
|
+
paths.
|
|
13
|
+
- Load non-empty todo events and written-file artifacts in standalone Ruby
|
|
14
|
+
clients.
|
|
15
|
+
- Bound reconnects after an interactive prompt loses its event stream, handle
|
|
16
|
+
DNS failures as transient stream errors, and map non-404 4xx responses to
|
|
17
|
+
`BadRequestError` consistently.
|
|
18
|
+
- Retry the Rails recovery recipe with its replacement session and keep
|
|
19
|
+
exception details out of user-facing message content.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Test Ruby 3.2, 3.3, 3.4, and 4.0; pin every third-party CI and release
|
|
24
|
+
action to an exact reviewed commit; and use Ruby 4.0 for release builds.
|
|
25
|
+
- Fail the trusted-publishing job before release when the pushed tag does not
|
|
26
|
+
match `Opencode::VERSION`.
|
|
27
|
+
|
|
28
|
+
## 0.0.1.alpha8 - 2026-07-20
|
|
29
|
+
|
|
30
|
+
### Yanked
|
|
31
|
+
|
|
32
|
+
- Published from the unrepaired alpha8 source by mistake and yanked the same
|
|
33
|
+
day. Use alpha9, which contains the intended alpha8 fixes and release gates.
|
|
34
|
+
|
|
35
|
+
## 0.0.1.alpha7 - 2026-07-18
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Add an at-most-once `on_subscribed` hook to the lower-level
|
|
40
|
+
`Opencode::Client#stream_events` API. Higher-level orchestrators can now
|
|
41
|
+
wait for `server.connected` before submitting `prompt_async` without
|
|
42
|
+
abandoning their own Reply observers, persistence, or recovery pipeline.
|
|
43
|
+
- Propagate subscription-hook failures directly and never invoke the hook
|
|
44
|
+
again on SSE reconnect. Ambiguous prompt transport failures therefore
|
|
45
|
+
cannot silently become duplicate turns.
|
|
46
|
+
|
|
3
47
|
## 0.0.1.alpha6 - 2026-07-18
|
|
4
48
|
|
|
5
49
|
### Fixed
|
|
@@ -88,5 +132,7 @@ First public alpha. HTTP + SSE client for OpenCode REST API.
|
|
|
88
132
|
### Compatibility
|
|
89
133
|
|
|
90
134
|
- Ruby ≥ 3.2
|
|
91
|
-
- OpenCode
|
|
135
|
+
- OpenCode targeted the then-current 1.15 message-bus shape. This historical
|
|
136
|
+
target was not a blanket SemVer compatibility guarantee; use the README's
|
|
137
|
+
current compatibility evidence for deployment decisions.
|
|
92
138
|
- Runtime dependency: `activesupport (>= 6.1)` for `blank?`/`present?`/`presence`/`truncate`/`duplicable?`/`megabytes`. ActiveSupport is *not* Rails — it's a standalone helpers gem.
|
data/README.md
CHANGED
|
@@ -67,7 +67,9 @@ child = client.create_session(
|
|
|
67
67
|
|
|
68
68
|
Model strings use OpenCode's `provider/model` form; a preformatted model hash
|
|
69
69
|
with `providerID` and `id` keys is also accepted. These configured-session
|
|
70
|
-
fields
|
|
70
|
+
fields first appeared in OpenCode 1.16.1. That is an API-introduction note,
|
|
71
|
+
not evidence that every later server is compatible; use the
|
|
72
|
+
[certified compatibility evidence](#compatibility) for deployment choices.
|
|
71
73
|
|
|
72
74
|
### Streaming (the headline)
|
|
73
75
|
|
|
@@ -106,8 +108,9 @@ client.update_session(session_id, permissions: permission_rules)
|
|
|
106
108
|
|
|
107
109
|
OpenCode appends PATCHed permission rules and evaluates the last matching
|
|
108
110
|
rule. Hosts should send a complete ordered policy and fingerprint it so the
|
|
109
|
-
same policy is not appended on every turn. This endpoint
|
|
110
|
-
1.16.1
|
|
111
|
+
same policy is not appended on every turn. This endpoint first appeared in
|
|
112
|
+
OpenCode 1.16.1; that version floor describes endpoint availability, not
|
|
113
|
+
compatibility with every later server.
|
|
111
114
|
|
|
112
115
|
### Lower-level event firehose
|
|
113
116
|
|
|
@@ -119,6 +122,23 @@ client.stream_events(session_id: session_id) do |event|
|
|
|
119
122
|
end
|
|
120
123
|
```
|
|
121
124
|
|
|
125
|
+
Orchestrators that submit their own async prompt must do so through
|
|
126
|
+
`on_subscribed`; the callback runs only after the first `server.connected`
|
|
127
|
+
frame and at most once across automatic reconnects:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
client.stream_events(
|
|
131
|
+
session_id: session_id,
|
|
132
|
+
on_subscribed: -> { client.send_message_async(session_id, prompt) }
|
|
133
|
+
) do |event|
|
|
134
|
+
reply.apply(event)
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
If the callback raises, `stream_events` propagates that error and does not
|
|
139
|
+
retry it. This is intentional: a timed-out prompt response is ambiguous, so
|
|
140
|
+
reposting could duplicate the model turn and its cost.
|
|
141
|
+
|
|
122
142
|
### Interactive prompts
|
|
123
143
|
|
|
124
144
|
When the agent uses the `question` or `permission` tools, opencode emits `question.asked` / `permission.asked` events. Answer them via:
|
|
@@ -182,8 +202,29 @@ Want every OpenCode endpoint auto-generated from the OpenAPI spec? Use [`opencod
|
|
|
182
202
|
## Compatibility
|
|
183
203
|
|
|
184
204
|
- Ruby ≥ 3.2
|
|
185
|
-
-
|
|
186
|
-
- Runtime dependency: `
|
|
205
|
+
- Runtime dependency: `activesupport (>= 6.1, < 9.0)` — *not* Rails. ActiveSupport is a standalone helpers gem (`blank?`, `present?`, `presence`, `truncate`, etc.).
|
|
206
|
+
- Runtime dependency: `marcel (~> 1.0)` for artifact MIME type detection.
|
|
207
|
+
|
|
208
|
+
OpenCode server compatibility is evidence-based, not an open-ended SemVer
|
|
209
|
+
promise. OpenCode's HTTP, SSE, and runtime behavior can change independently
|
|
210
|
+
of a numeric version floor, so this gem does not infer compatibility from a
|
|
211
|
+
constraint such as `>= 1.15`.
|
|
212
|
+
|
|
213
|
+
Use the [OpenCode compatibility corpus](https://github.com/ajaynomics/opencode-compat)
|
|
214
|
+
as the source of current evidence. Its
|
|
215
|
+
[image matrix](https://github.com/ajaynomics/opencode-compat/blob/main/manifests/image-matrix.json)
|
|
216
|
+
records the exact gem commit and immutable OpenCode OCI digest exercised by
|
|
217
|
+
the `ruby-rest-sse` profile. Its
|
|
218
|
+
[runtime tuples](https://github.com/ajaynomics/opencode-compat/blob/main/manifests/runtime-tuples.json)
|
|
219
|
+
add the exact consumer commit and canary evidence needed for promotion and
|
|
220
|
+
rollback. A version label or image tag is provenance only, never the execution
|
|
221
|
+
coordinate.
|
|
222
|
+
|
|
223
|
+
A combination absent from passing evidence is **unverified**, not necessarily
|
|
224
|
+
incompatible. Run the corpus against the exact gem, image digest, and consumer
|
|
225
|
+
commit before adopting or promoting it. See the
|
|
226
|
+
[certification policy](https://github.com/ajaynomics/opencode-compat/blob/main/docs/certification.md)
|
|
227
|
+
for the full process.
|
|
187
228
|
|
|
188
229
|
## Development
|
|
189
230
|
|
|
@@ -196,10 +237,14 @@ The smoke suite covers Client end-to-end against WebMock-stubbed OpenCode
|
|
|
196
237
|
endpoints, including subscription-before-prompt ordering and
|
|
197
238
|
reconnect-without-repost.
|
|
198
239
|
|
|
199
|
-
|
|
200
|
-
`
|
|
201
|
-
|
|
202
|
-
|
|
240
|
+
The mistakenly published `0.0.1.alpha8` package contained unrepaired source and
|
|
241
|
+
was yanked; use `0.0.1.alpha9` or later. The repository contains a tag-triggered
|
|
242
|
+
`release.yml` workflow intended for RubyGems trusted publishing, but its
|
|
243
|
+
trusted-publisher registration is not confirmed for alpha9. A `v*` tag push
|
|
244
|
+
therefore does not currently guarantee publication. Before a future release,
|
|
245
|
+
verify the RubyGems registry result explicitly. Once the workflow is registered
|
|
246
|
+
as a trusted publisher for the `release` environment, it can build, attest, and
|
|
247
|
+
publish without a long-lived RubyGems API key.
|
|
203
248
|
|
|
204
249
|
## License
|
|
205
250
|
|
|
@@ -128,12 +128,13 @@ class GenerateAssistantReplyJob < ApplicationJob
|
|
|
128
128
|
end
|
|
129
129
|
rescue Opencode::SessionNotFoundError, Opencode::StaleSessionError
|
|
130
130
|
raise if attempted_recreate
|
|
131
|
-
message.conversation.recreate_opencode_session!(client)
|
|
131
|
+
session_id = message.conversation.recreate_opencode_session!(client)
|
|
132
132
|
attempted_recreate = true
|
|
133
133
|
retry
|
|
134
134
|
end
|
|
135
135
|
rescue StandardError => e
|
|
136
|
-
|
|
136
|
+
Rails.logger.error(e.full_message)
|
|
137
|
+
message&.update!(status: :errored, content: "An error occurred. Please try again.")
|
|
137
138
|
end
|
|
138
139
|
|
|
139
140
|
private
|
data/lib/opencode/client.rb
CHANGED
|
@@ -230,18 +230,12 @@ module Opencode
|
|
|
230
230
|
request = Net::HTTP::Get.new(uri)
|
|
231
231
|
add_auth_header(request)
|
|
232
232
|
|
|
233
|
-
response = Opencode::Instrumentation.instrument("opencode.request", method: request.method, path:
|
|
233
|
+
response = Opencode::Instrumentation.instrument("opencode.request", method: request.method, path: uri.path) do
|
|
234
234
|
http_client.request(request)
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
return [] if response.body.blank?
|
|
242
|
-
JSON.parse(response.body, symbolize_names: true)
|
|
243
|
-
rescue JSON::ParserError => e
|
|
244
|
-
raise ServerError, "list_questions returned invalid JSON: #{e.message}"
|
|
237
|
+
body = handle_response(response)
|
|
238
|
+
body.is_a?(Array) ? body : []
|
|
245
239
|
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
|
|
246
240
|
raise TimeoutError, "OpenCode timeout after #{@timeout}s: #{e.message}"
|
|
247
241
|
rescue Errno::ECONNREFUSED, SocketError => e
|
|
@@ -255,6 +249,10 @@ module Opencode
|
|
|
255
249
|
end
|
|
256
250
|
|
|
257
251
|
MAX_SSE_BUFFER = 1_048_576 # 1 MB — safety valve against pathological server responses
|
|
252
|
+
# Keep CRLF atomic so it cannot backtrack into CR + LF and look like a
|
|
253
|
+
# blank line. SSE also permits bare CR and LF line endings.
|
|
254
|
+
SSE_EVENT_BOUNDARY = /(?>\r\n|[\r\n]){2}/
|
|
255
|
+
UTF8_BOM = "\xEF\xBB\xBF".b.freeze
|
|
258
256
|
SSE_RECONNECT_DELAY = 0.1
|
|
259
257
|
TRANSIENT_SSE_ERRORS = [
|
|
260
258
|
EOFError,
|
|
@@ -263,7 +261,8 @@ module Opencode
|
|
|
263
261
|
Net::ReadTimeout,
|
|
264
262
|
Errno::ECONNREFUSED,
|
|
265
263
|
Errno::ECONNRESET,
|
|
266
|
-
Errno::EPIPE
|
|
264
|
+
Errno::EPIPE,
|
|
265
|
+
SocketError
|
|
267
266
|
].freeze
|
|
268
267
|
|
|
269
268
|
# Opens SSE connection to GET /event, yields parsed events filtered by session_id.
|
|
@@ -285,6 +284,13 @@ module Opencode
|
|
|
285
284
|
# without nuking real reasoning. Callers that know their agent is
|
|
286
285
|
# short-prompt + fast can pass a lower value.
|
|
287
286
|
#
|
|
287
|
+
# on_subscribed: optional callable invoked at most once, after the first
|
|
288
|
+
# `server.connected` frame proves the SSE response body is flowing. This
|
|
289
|
+
# is the safe place for higher-level orchestrators to submit prompt_async;
|
|
290
|
+
# reconnects wait for their own connected frame but never invoke it again.
|
|
291
|
+
# A raised callback error is propagated directly and is never treated as
|
|
292
|
+
# a reconnectable SSE transport failure.
|
|
293
|
+
#
|
|
288
294
|
# idle_stream_timeout: seconds to wait BETWEEN meaningful events once
|
|
289
295
|
# the session has started producing them. Default nil = no check
|
|
290
296
|
# (preserves the overall `timeout` ceiling behavior). Opt-in heartbeat
|
|
@@ -298,7 +304,7 @@ module Opencode
|
|
|
298
304
|
# translate into a user-visible error / retry affordance.
|
|
299
305
|
def stream_events(session_id:, timeout: 600, first_event_timeout: 120,
|
|
300
306
|
idle_stream_timeout: nil,
|
|
301
|
-
reply: nil, on_activity_tick: nil, &block)
|
|
307
|
+
reply: nil, on_activity_tick: nil, on_subscribed: nil, &block)
|
|
302
308
|
consume_event_stream(
|
|
303
309
|
session_id: session_id,
|
|
304
310
|
timeout: timeout,
|
|
@@ -306,6 +312,7 @@ module Opencode
|
|
|
306
312
|
idle_stream_timeout: idle_stream_timeout,
|
|
307
313
|
reply: reply,
|
|
308
314
|
on_activity_tick: on_activity_tick,
|
|
315
|
+
on_subscribed: on_subscribed,
|
|
309
316
|
&block
|
|
310
317
|
)
|
|
311
318
|
end
|
|
@@ -318,10 +325,11 @@ module Opencode
|
|
|
318
325
|
first_event_deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + first_event_timeout
|
|
319
326
|
received_session_event = false
|
|
320
327
|
last_meaningful_event_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
328
|
+
subscription_callback_attempted = on_subscribed.nil?
|
|
321
329
|
|
|
322
330
|
loop do
|
|
323
331
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
324
|
-
deadline =
|
|
332
|
+
deadline = check_deadline(now, deadline, timeout)
|
|
325
333
|
|
|
326
334
|
# NOTE: first_event_deadline is *not* suspension-eligible. If the agent
|
|
327
335
|
# never gets started we want to fail fast — a session that's blocked on
|
|
@@ -348,9 +356,11 @@ module Opencode
|
|
|
348
356
|
http.read_timeout = 30
|
|
349
357
|
|
|
350
358
|
subscription_callback_error = nil
|
|
359
|
+
event_callback_error = nil
|
|
351
360
|
subscription_ready = on_subscribed.nil?
|
|
352
361
|
begin
|
|
353
362
|
buffer = String.new
|
|
363
|
+
first_sse_event = true
|
|
354
364
|
|
|
355
365
|
http.request(request) do |response|
|
|
356
366
|
unless response.is_a?(Net::HTTPSuccess)
|
|
@@ -377,9 +387,14 @@ module Opencode
|
|
|
377
387
|
raise ServerError, "SSE buffer exceeded #{MAX_SSE_BUFFER} bytes"
|
|
378
388
|
end
|
|
379
389
|
|
|
380
|
-
while (
|
|
381
|
-
raw_event = buffer.slice!(0,
|
|
382
|
-
event = parse_sse_event(
|
|
390
|
+
while (boundary = buffer.match(SSE_EVENT_BOUNDARY))
|
|
391
|
+
raw_event = buffer.slice!(0, boundary.end(0))
|
|
392
|
+
event = parse_sse_event(
|
|
393
|
+
raw_event,
|
|
394
|
+
session_id,
|
|
395
|
+
allow_bom: first_sse_event
|
|
396
|
+
)
|
|
397
|
+
first_sse_event = false
|
|
383
398
|
next unless event
|
|
384
399
|
|
|
385
400
|
unless subscription_ready
|
|
@@ -390,14 +405,22 @@ module Opencode
|
|
|
390
405
|
# available readiness handshake before prompting.
|
|
391
406
|
next unless event[:type] == "server.connected"
|
|
392
407
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
#
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
|
|
400
|
-
|
|
408
|
+
turn_started = false
|
|
409
|
+
unless subscription_callback_attempted
|
|
410
|
+
# Mark the attempt before invoking the callback. A timeout
|
|
411
|
+
# can mean the prompt reached OpenCode even though its HTTP
|
|
412
|
+
# response did not reach us; retrying would duplicate the
|
|
413
|
+
# turn. The caller receives the original error instead.
|
|
414
|
+
subscription_callback_attempted = true
|
|
415
|
+
begin
|
|
416
|
+
turn_started = on_subscribed.call
|
|
417
|
+
rescue StandardError => error
|
|
418
|
+
# Prompt submission happens inside the open SSE response.
|
|
419
|
+
# Do not mistake its transport failure for an SSE disconnect
|
|
420
|
+
# and hide it behind a reconnect/first-event timeout.
|
|
421
|
+
subscription_callback_error = error
|
|
422
|
+
raise
|
|
423
|
+
end
|
|
401
424
|
end
|
|
402
425
|
if turn_started
|
|
403
426
|
# Before this fix stream_events began only after the prompt
|
|
@@ -421,14 +444,19 @@ module Opencode
|
|
|
421
444
|
# that's the whole point: a healthy long wait (user thinking
|
|
422
445
|
# for 30 minutes) keeps the container warm via heartbeats so
|
|
423
446
|
# the reaper doesn't kill it mid-wait.
|
|
424
|
-
|
|
425
|
-
|
|
447
|
+
begin
|
|
448
|
+
on_activity_tick&.call(event)
|
|
449
|
+
block.call(event)
|
|
450
|
+
rescue StandardError => error
|
|
451
|
+
event_callback_error = error
|
|
452
|
+
raise
|
|
453
|
+
end
|
|
426
454
|
return if terminal_session_event?(event)
|
|
427
455
|
end
|
|
428
456
|
end
|
|
429
457
|
end
|
|
430
458
|
rescue *TRANSIENT_SSE_ERRORS
|
|
431
|
-
raise if subscription_callback_error
|
|
459
|
+
raise if subscription_callback_error || event_callback_error
|
|
432
460
|
|
|
433
461
|
# Treat transport-level SSE disconnects like clean EOF: reconnect
|
|
434
462
|
# until an idle session event, the overall timeout, or first-event
|
|
@@ -487,6 +515,11 @@ module Opencode
|
|
|
487
515
|
# the prompt resolves. Otherwise apply the normal deadline check.
|
|
488
516
|
def check_deadline_or_suspend(now, deadline, timeout, reply)
|
|
489
517
|
return now + timeout if reply&.prompt_blocked?
|
|
518
|
+
|
|
519
|
+
check_deadline(now, deadline, timeout)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def check_deadline(now, deadline, timeout)
|
|
490
523
|
raise TimeoutError, "SSE stream timed out after #{timeout}s" if now > deadline
|
|
491
524
|
|
|
492
525
|
deadline
|
|
@@ -601,7 +634,7 @@ module Opencode
|
|
|
601
634
|
add_auth_header(request)
|
|
602
635
|
|
|
603
636
|
response = nil
|
|
604
|
-
result = Opencode::Instrumentation.instrument("opencode.request", method: request.method, path: request.path) do
|
|
637
|
+
result = Opencode::Instrumentation.instrument("opencode.request", method: request.method, path: request.uri.path) do
|
|
605
638
|
response = http_client.request(request)
|
|
606
639
|
handle_response(response)
|
|
607
640
|
end
|
|
@@ -622,11 +655,22 @@ module Opencode
|
|
|
622
655
|
end
|
|
623
656
|
end
|
|
624
657
|
|
|
625
|
-
def parse_sse_event(raw, session_id)
|
|
626
|
-
|
|
627
|
-
|
|
658
|
+
def parse_sse_event(raw, session_id, allow_bom: false)
|
|
659
|
+
if allow_bom && raw.b.start_with?(UTF8_BOM)
|
|
660
|
+
raw = raw.byteslice(UTF8_BOM.bytesize..)
|
|
661
|
+
end
|
|
628
662
|
|
|
629
|
-
|
|
663
|
+
data = raw.split(/\r\n|\r|\n/, -1).filter_map do |line|
|
|
664
|
+
next if line.start_with?(":")
|
|
665
|
+
|
|
666
|
+
field, value = line.split(":", 2)
|
|
667
|
+
next unless field == "data"
|
|
668
|
+
|
|
669
|
+
(value || "").delete_prefix(" ")
|
|
670
|
+
end
|
|
671
|
+
return nil if data.empty?
|
|
672
|
+
|
|
673
|
+
json = JSON.parse(data.join("\n"), symbolize_names: true)
|
|
630
674
|
|
|
631
675
|
event_session = json.dig(:properties, :sessionID) ||
|
|
632
676
|
json.dig(:properties, :info, :sessionID) ||
|
|
@@ -642,23 +686,30 @@ module Opencode
|
|
|
642
686
|
end
|
|
643
687
|
|
|
644
688
|
def handle_response(response)
|
|
645
|
-
|
|
689
|
+
status = response.code.to_i
|
|
690
|
+
return {} if status == 204
|
|
646
691
|
|
|
647
|
-
body =
|
|
648
|
-
JSON.parse(response.body, symbolize_names: true)
|
|
649
|
-
else
|
|
650
|
-
{}
|
|
651
|
-
end
|
|
692
|
+
body = parse_response_body(response, status)
|
|
652
693
|
|
|
653
|
-
case
|
|
694
|
+
case status
|
|
654
695
|
when 200..299 then body
|
|
655
|
-
when 400 then raise BadRequestError.new(error_message(body, "Bad request"), response: body)
|
|
656
696
|
when 404 then raise SessionNotFoundError.new(error_message(body, "Session not found"), response: body)
|
|
697
|
+
when 400..499 then raise BadRequestError.new(error_message(body, "Bad request"), response: body)
|
|
657
698
|
when 500..599 then raise ServerError.new(error_message(body, "Server error"), response: body)
|
|
658
699
|
else raise Error.new("Unexpected response: #{response.code}", response: body)
|
|
659
700
|
end
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def parse_response_body(response, status)
|
|
704
|
+
return {} if response.body.blank?
|
|
705
|
+
|
|
706
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
660
707
|
rescue JSON::ParserError
|
|
661
|
-
|
|
708
|
+
if status.between?(200, 299)
|
|
709
|
+
raise ServerError.new("Invalid JSON from OpenCode (HTTP #{response.code}): #{response.body&.truncate(200)}")
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
{ message: response.body.to_s.truncate(200) }
|
|
662
713
|
end
|
|
663
714
|
|
|
664
715
|
# OpenCode HTTP error bodies use a wrapped shape: { name:, data: { message:, kind?: } }.
|
|
@@ -666,6 +717,9 @@ module Opencode
|
|
|
666
717
|
# `body[:message]` is no longer populated for errors — only `body[:data][:message]`.
|
|
667
718
|
# We read both to keep older mock servers working in tests.
|
|
668
719
|
def error_message(body, fallback)
|
|
720
|
+
return body.truncate(200) if body.is_a?(String) && body.present?
|
|
721
|
+
return fallback unless body.is_a?(Hash)
|
|
722
|
+
|
|
669
723
|
body.dig(:data, :message) || body[:message] || fallback
|
|
670
724
|
end
|
|
671
725
|
end
|
data/lib/opencode/version.rb
CHANGED
data/lib/opencode-ruby.rb
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
# non-Rails apps.
|
|
7
7
|
require "active_support/core_ext/object/blank" # provides blank?, present?, presence
|
|
8
8
|
require "active_support/core_ext/object/duplicable"
|
|
9
|
+
require "active_support/core_ext/hash/keys" # provides Hash#deep_stringify_keys
|
|
9
10
|
require "active_support/core_ext/string/filters" # provides String#truncate
|
|
10
11
|
require "active_support/core_ext/numeric/bytes" # provides Integer#megabytes
|
|
12
|
+
require "marcel"
|
|
11
13
|
|
|
12
14
|
require_relative "opencode/version"
|
|
13
15
|
require_relative "opencode/error"
|
data/opencode-ruby.gemspec
CHANGED
|
@@ -30,14 +30,10 @@ Gem::Specification.new do |spec|
|
|
|
30
30
|
%w[README.md LICENSE CHANGELOG.md opencode-ruby.gemspec]
|
|
31
31
|
spec.require_paths = ["lib"]
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# `truncate`/`duplicable?` helpers used in this gem's code. It does NOT
|
|
36
|
-
# pull in ActiveRecord, ActionView, ActionController, Turbo, or any other
|
|
37
|
-
# Rails-only piece. Most Ruby apps in the wild already have ActiveSupport
|
|
38
|
-
# transitively via another gem; in the rare case yours doesn't, ~250 LOC
|
|
39
|
-
# of core_ext is added when this gem installs.
|
|
33
|
+
# ActiveSupport supplies the small set of core extensions used by the
|
|
34
|
+
# client without pulling in Rails. Marcel identifies artifact content types.
|
|
40
35
|
spec.add_runtime_dependency "activesupport", ">= 6.1", "< 9.0"
|
|
36
|
+
spec.add_runtime_dependency "marcel", "~> 1.0"
|
|
41
37
|
|
|
42
38
|
spec.add_development_dependency "minitest", "~> 5.20"
|
|
43
39
|
spec.add_development_dependency "rake", "~> 13.0"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: opencode-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.1.
|
|
4
|
+
version: 0.0.1.alpha9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ajay Krishnan
|
|
@@ -29,6 +29,20 @@ dependencies:
|
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '9.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: marcel
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.0'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '1.0'
|
|
32
46
|
- !ruby/object:Gem::Dependency
|
|
33
47
|
name: minitest
|
|
34
48
|
requirement: !ruby/object:Gem::Requirement
|