agent-cli-runtime 0.1.1 → 0.2.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.
@@ -1,4 +1,11 @@
1
1
  module AgentCliRuntime
2
+ OPENCODE_OVERLAY_ENVIRONMENT_KEYS = %w[
3
+ XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME TMPDIR
4
+ OPENCODE_CONFIG OPENCODE_DISABLE_PROJECT_CONFIG
5
+ OPENCODE_DISABLE_CLAUDE_CODE OPENCODE_DISABLE_MODELS_FETCH
6
+ OPENCODE_DISABLE_AUTOUPDATE OPENCODE_PURE
7
+ ].freeze
8
+
2
9
  module Immutable
3
10
  module_function
4
11
 
@@ -14,6 +21,28 @@ module AgentCliRuntime
14
21
  def symbols(values)
15
22
  Array(values).map(&:to_sym).uniq.freeze
16
23
  end
24
+
25
+ def hash(value)
26
+ unless value.is_a?(Hash)
27
+ raise ArgumentError, "value must be a Hash"
28
+ end
29
+
30
+ value.each_with_object({}) do |(key, item), result|
31
+ result[string(key)] = deep(item)
32
+ end.freeze
33
+ end
34
+
35
+ def deep(value)
36
+ case value
37
+ when Hash then hash(value)
38
+ when Array then value.map { |item| deep(item) }.freeze
39
+ when String then string(value)
40
+ when Symbol, Numeric, TrueClass, FalseClass, NilClass then value
41
+ else
42
+ raise ArgumentError,
43
+ "unsupported immutable value #{value.class}"
44
+ end
45
+ end
17
46
  end
18
47
  private_constant :Immutable
19
48
 
@@ -52,6 +81,12 @@ module AgentCliRuntime
52
81
  end
53
82
  end
54
83
 
84
+ ExtractedFailure = Data.define(:kind, :message) do
85
+ def initialize(kind:, message:)
86
+ super(kind: kind.to_sym, message: Immutable.string(message))
87
+ end
88
+ end
89
+
55
90
  Request = Data.define(
56
91
  :profile, :prompt, :permission_mode, :permission_arguments,
57
92
  :add_dirs, :require_add_dirs,
@@ -173,4 +208,447 @@ module AgentCliRuntime
173
208
  )
174
209
  end
175
210
  end
211
+
212
+
213
+ Route = Data.define(:provider, :model) do
214
+ ROUTE_PATTERN = /\A(?<provider>[a-zA-Z0-9][a-zA-Z0-9._-]*)\/(?<model>[^\s\/][^\s]*)\z/
215
+
216
+ def initialize(provider:, model:)
217
+ normalized_provider = Immutable.string(provider)
218
+ normalized_model = Immutable.string(model)
219
+ route = "#{normalized_provider}/#{normalized_model}"
220
+ unless ROUTE_PATTERN.match?(route) && !normalized_model.include?("\0")
221
+ raise ArgumentError,
222
+ "OpenCode route must be a full provider/model value"
223
+ end
224
+
225
+ super(provider: normalized_provider, model: normalized_model)
226
+ end
227
+
228
+ def self.parse(value)
229
+ match = ROUTE_PATTERN.match(value.to_s)
230
+ unless match
231
+ raise ArgumentError,
232
+ "OpenCode route must be a full provider/model value"
233
+ end
234
+
235
+ new(provider: match[:provider], model: match[:model])
236
+ end
237
+
238
+ def to_s
239
+ "#{provider}/#{model}"
240
+ end
241
+ end
242
+
243
+ OpenCodePermissionPolicy = Data.define(:rules) do
244
+ ACTIONS = %w[allow ask deny].freeze
245
+
246
+ def initialize(rules = nil, **keywords)
247
+ value = rules || keywords[:rules] || keywords
248
+ normalized = Immutable.hash(value || {})
249
+ raise ArgumentError, "OpenCode permission policy cannot be empty" if normalized.empty?
250
+
251
+ validate_actions!(normalized)
252
+ super(rules: normalized)
253
+ end
254
+
255
+ private
256
+
257
+ def validate_actions!(value)
258
+ value.each_value do |entry|
259
+ if entry.is_a?(Hash)
260
+ validate_actions!(entry)
261
+ elsif !ACTIONS.include?(entry)
262
+ raise ArgumentError,
263
+ "OpenCode permission action must be allow, ask, or deny"
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ OpenCodePreparationRequest = Data.define(
270
+ :request, :working_directory, :invocation_root,
271
+ :configuration_path, :configuration,
272
+ :credential_environment_keys, :credential_file,
273
+ :permission_policy, :additional_read_roots,
274
+ :additional_write_roots, :edit_patterns, :bash_patterns, :plugins, :pure
275
+ ) do
276
+ def initialize(request:, working_directory:, invocation_root:,
277
+ configuration_path: nil, configuration: nil,
278
+ credential_environment_keys: [], credential_file: nil,
279
+ permission_policy: nil, additional_read_roots: [],
280
+ additional_write_roots: [], edit_patterns: [],
281
+ bash_patterns: [], plugins: [], pure: true)
282
+ unless request.is_a?(Request)
283
+ raise ArgumentError, "request must be an AgentCliRuntime::Request"
284
+ end
285
+ if configuration_path && configuration
286
+ raise ArgumentError,
287
+ "choose configuration_path or configuration, not both"
288
+ end
289
+ if permission_policy &&
290
+ !permission_policy.is_a?(OpenCodePermissionPolicy)
291
+ raise ArgumentError,
292
+ "permission_policy must be an OpenCodePermissionPolicy"
293
+ end
294
+
295
+ keys = Immutable.strings(credential_environment_keys)
296
+ invalid = keys.find { |key| !key.match?(/\A[A-Z][A-Z0-9_]*\z/) }
297
+ raise ArgumentError, "invalid credential environment key" if invalid
298
+ raise ArgumentError, "credential environment keys must be unique" if keys.uniq.length != keys.length
299
+ reserved = keys & OPENCODE_OVERLAY_ENVIRONMENT_KEYS
300
+ unless reserved.empty?
301
+ raise ArgumentError,
302
+ "credential environment keys cannot override the OpenCode overlay: #{reserved.join(', ')}"
303
+ end
304
+ normalized_bash_patterns = Immutable.strings(bash_patterns)
305
+ if normalized_bash_patterns.any?(&:empty?)
306
+ raise ArgumentError, "OpenCode bash patterns must be non-empty"
307
+ end
308
+
309
+ super(
310
+ request: request,
311
+ working_directory: Immutable.string(working_directory),
312
+ invocation_root: Immutable.string(invocation_root),
313
+ configuration_path:
314
+ configuration_path.nil? ? nil : Immutable.string(configuration_path),
315
+ configuration:
316
+ configuration.nil? ? nil : Immutable.hash(configuration),
317
+ credential_environment_keys: keys,
318
+ credential_file:
319
+ credential_file.nil? ? nil : Immutable.string(credential_file),
320
+ permission_policy: permission_policy,
321
+ additional_read_roots: Immutable.strings(additional_read_roots),
322
+ additional_write_roots: Immutable.strings(additional_write_roots),
323
+ edit_patterns: Immutable.strings(edit_patterns),
324
+ bash_patterns: normalized_bash_patterns,
325
+ plugins: Immutable.strings(plugins),
326
+ pure: pure != false
327
+ )
328
+ end
329
+ end
330
+
331
+ ProbeRequest = Data.define(
332
+ :profile, :route, :variant, :environment,
333
+ :credential_environment_keys, :credential_file_staged,
334
+ :configured_variants, :executable
335
+ ) do
336
+ def initialize(profile:, route:, variant: nil, environment: {},
337
+ credential_environment_keys: [],
338
+ credential_file_staged: false,
339
+ configured_variants: nil,
340
+ executable: nil)
341
+ parsed_route = route.is_a?(Route) ? route : Route.parse(route)
342
+ super(
343
+ profile: profile,
344
+ route: parsed_route,
345
+ variant: variant.nil? ? nil : Immutable.string(variant),
346
+ environment: Immutable.hash(environment),
347
+ credential_environment_keys:
348
+ Immutable.strings(credential_environment_keys),
349
+ credential_file_staged: credential_file_staged == true,
350
+ configured_variants:
351
+ configured_variants.nil? ? nil : Immutable.strings(configured_variants),
352
+ executable: executable.nil? ? nil : Immutable.string(executable)
353
+ )
354
+ end
355
+ end
356
+
357
+ RouteProbeResult = Data.define(
358
+ :provider, :ready, :installed, :executable, :version,
359
+ :minimum_version, :auth_configuration, :route,
360
+ :route_available, :available_variants,
361
+ :capability_evidence, :diagnostic
362
+ ) do
363
+ def initialize(provider:, ready:, installed:, executable:, version:,
364
+ minimum_version:, auth_configuration:, route:,
365
+ route_available:, available_variants:,
366
+ capability_evidence:, diagnostic: nil)
367
+ super(
368
+ provider: provider.to_sym,
369
+ ready: ready == true,
370
+ installed: installed == true,
371
+ executable: Immutable.string(executable),
372
+ version: version.nil? ? nil : Immutable.string(version),
373
+ minimum_version:
374
+ minimum_version.nil? ? nil : Immutable.string(minimum_version),
375
+ auth_configuration: auth_configuration,
376
+ route: route,
377
+ route_available: route_available == true,
378
+ available_variants: Immutable.strings(available_variants),
379
+ capability_evidence: Array(capability_evidence).freeze,
380
+ diagnostic:
381
+ diagnostic.nil? ? nil : Immutable.string(diagnostic)
382
+ )
383
+ end
384
+ end
385
+
386
+ class PreparedInvocation
387
+ attr_reader :invocation, :environment, :credential_environment_keys,
388
+ :invocation_root, :generated_paths, :configuration_path,
389
+ :requested_route, :configuration_source, :probe_result,
390
+ :executable
391
+
392
+ def initialize(invocation:, environment:, credential_environment_keys:,
393
+ invocation_root:, generated_paths:, configuration_path:,
394
+ requested_route:, configuration_source:, probe_result:,
395
+ cleanup:, executable: nil)
396
+ @invocation = invocation
397
+ @environment = Immutable.hash(environment)
398
+ @credential_environment_keys =
399
+ Immutable.strings(credential_environment_keys)
400
+ @invocation_root = Immutable.string(invocation_root)
401
+ @generated_paths = Immutable.strings(generated_paths)
402
+ @configuration_path = Immutable.string(configuration_path)
403
+ @requested_route = requested_route
404
+ @configuration_source =
405
+ configuration_source.nil? ? nil : Immutable.string(configuration_source)
406
+ @probe_result = probe_result
407
+ @executable = Immutable.string(executable || invocation.argv.fetch(0))
408
+ @cleanup = cleanup
409
+ freeze
410
+ end
411
+
412
+ def environment_for(env: ENV)
413
+ selected = credential_environment_keys.each_with_object({}) do |key, values|
414
+ value = env[key]
415
+ values[key] = value.to_s unless value.to_s.empty?
416
+ end
417
+ selected.merge(environment).freeze
418
+ end
419
+
420
+ def cleanup!
421
+ @cleanup.call
422
+ nil
423
+ end
424
+ end
425
+
426
+ TerminationEvidence = Data.define(
427
+ :exit_code, :timed_out, :cancelled, :signal
428
+ ) do
429
+ def initialize(exit_code:, timed_out: false, cancelled: false, signal: nil)
430
+ unless exit_code.nil? || exit_code.is_a?(Integer)
431
+ raise ArgumentError, "exit_code must be an Integer or nil"
432
+ end
433
+
434
+ super(
435
+ exit_code: exit_code,
436
+ timed_out: timed_out == true,
437
+ cancelled: cancelled == true,
438
+ signal: signal.nil? ? nil : Immutable.string(signal)
439
+ )
440
+ end
441
+
442
+ def success?
443
+ !timed_out && !cancelled && signal.nil? && exit_code == 0
444
+ end
445
+ end
446
+
447
+ CapturedResult = Data.define(
448
+ :stdout, :stderr, :termination, :inspection_output
449
+ ) do
450
+ def initialize(stdout:, stderr:, termination:, inspection_output: nil)
451
+ unless termination.is_a?(TerminationEvidence)
452
+ raise ArgumentError, "termination must be TerminationEvidence"
453
+ end
454
+
455
+ super(
456
+ stdout: Immutable.string(stdout),
457
+ stderr: Immutable.string(stderr),
458
+ termination: termination,
459
+ inspection_output:
460
+ inspection_output.nil? ? nil : Immutable.string(inspection_output)
461
+ )
462
+ end
463
+ end
464
+
465
+ NormalizedUsage = Data.define(
466
+ :input, :output, :cache_read, :cache_write, :reasoning,
467
+ :input_includes_cache_read, :input_includes_cache_write,
468
+ :output_includes_reasoning, :cost
469
+ ) do
470
+ def initialize(input: nil, output: nil, cache_read: nil, cache_write: nil,
471
+ reasoning: nil, input_includes_cache_read: nil,
472
+ input_includes_cache_write: nil,
473
+ output_includes_reasoning: nil,
474
+ provider_reported_cost: nil, cost: nil)
475
+ if !provider_reported_cost.nil? && !cost.nil? && provider_reported_cost != cost
476
+ raise ArgumentError, "provider_reported_cost and cost disagree"
477
+ end
478
+ super(
479
+ input: number(input, :input, integer: true),
480
+ output: number(output, :output, integer: true),
481
+ cache_read: number(cache_read, :cache_read, integer: true),
482
+ cache_write: number(cache_write, :cache_write, integer: true),
483
+ reasoning: number(reasoning, :reasoning, integer: true),
484
+ input_includes_cache_read:
485
+ boolean(input_includes_cache_read, :input_includes_cache_read),
486
+ input_includes_cache_write:
487
+ boolean(input_includes_cache_write, :input_includes_cache_write),
488
+ output_includes_reasoning:
489
+ boolean(output_includes_reasoning, :output_includes_reasoning),
490
+ cost: number(
491
+ provider_reported_cost.nil? ? cost : provider_reported_cost,
492
+ :provider_reported_cost,
493
+ integer: false
494
+ )
495
+ )
496
+ end
497
+
498
+ def cached
499
+ return nil if cache_read.nil? || cache_write.nil?
500
+
501
+ cache_read + cache_write
502
+ end
503
+
504
+ alias provider_reported_cost cost
505
+
506
+ private
507
+
508
+ def number(value, label, integer:)
509
+ return nil if value.nil?
510
+ valid = integer ? value.is_a?(Integer) : value.is_a?(Numeric)
511
+ valid &&= value.finite? if value.respond_to?(:finite?)
512
+ unless valid && value >= 0
513
+ raise ArgumentError, "#{label} must be a non-negative number or nil"
514
+ end
515
+
516
+ value
517
+ end
518
+
519
+
520
+ def boolean(value, label)
521
+ return nil if value.nil?
522
+ return value if value == true || value == false
523
+
524
+ raise ArgumentError, "#{label} must be true, false, or nil"
525
+ end
526
+ end
527
+
528
+ RouteIdentity = Data.define(:requested, :actual, :resolution_status) do
529
+ RESOLUTION_STATUSES = %i[unobserved matched resolved_differently].freeze
530
+
531
+ def initialize(requested:, actual: nil, resolution_status: nil)
532
+ requested_route = requested.is_a?(Route) ? requested : Route.parse(requested)
533
+ actual_route =
534
+ if actual.nil?
535
+ nil
536
+ elsif actual.is_a?(Route)
537
+ actual
538
+ else
539
+ Route.parse(actual)
540
+ end
541
+ status = resolution_status&.to_sym ||
542
+ (actual_route.nil? ? :unobserved :
543
+ (requested_route == actual_route ? :matched : :resolved_differently))
544
+ unless RESOLUTION_STATUSES.include?(status)
545
+ raise ArgumentError, "invalid route resolution status"
546
+ end
547
+
548
+ super(
549
+ requested: requested_route,
550
+ actual: actual_route,
551
+ resolution_status: status
552
+ )
553
+ end
554
+ end
555
+
556
+ ParsedRun = Data.define(
557
+ :session_id, :terminal_message_id, :terminal_reason,
558
+ :final_message, :final_message_truncated, :preliminary_usage, :unknown_events
559
+ ) do
560
+ def initialize(session_id:, terminal_message_id:, terminal_reason:,
561
+ final_message:, preliminary_usage:, unknown_events: [],
562
+ final_message_truncated: false)
563
+ unless preliminary_usage.is_a?(NormalizedUsage)
564
+ raise ArgumentError, "preliminary_usage must be NormalizedUsage"
565
+ end
566
+
567
+ super(
568
+ session_id: Immutable.string(session_id),
569
+ terminal_message_id: Immutable.string(terminal_message_id),
570
+ terminal_reason: Immutable.string(terminal_reason),
571
+ final_message: Immutable.string(final_message),
572
+ final_message_truncated: final_message_truncated == true,
573
+ preliminary_usage: preliminary_usage,
574
+ unknown_events: Immutable.strings(unknown_events)
575
+ )
576
+ end
577
+ end
578
+
579
+ InspectionCommand = Data.define(
580
+ :argv, :stdin_data, :environment, :credential_environment_keys,
581
+ :session_id, :message_id
582
+ ) do
583
+ def initialize(argv:, environment:, credential_environment_keys:,
584
+ session_id:, message_id:, stdin_data: nil)
585
+ super(
586
+ argv: Immutable.strings(argv),
587
+ stdin_data: stdin_data.nil? ? nil : Immutable.string(stdin_data),
588
+ environment: Immutable.hash(environment),
589
+ credential_environment_keys:
590
+ Immutable.strings(credential_environment_keys),
591
+ session_id: Immutable.string(session_id),
592
+ message_id: Immutable.string(message_id)
593
+ )
594
+ end
595
+
596
+ def environment_for(env: ENV)
597
+ selected = credential_environment_keys.each_with_object({}) do |key, values|
598
+ value = env[key]
599
+ values[key] = value.to_s unless value.to_s.empty?
600
+ end
601
+ selected.merge(environment).freeze
602
+ end
603
+ end
604
+
605
+ NormalizedOutcome = Data.define(
606
+ :provider, :launcher_identity, :kind, :termination,
607
+ :final_message, :final_message_truncated, :identity, :usage, :diagnostic,
608
+ :unknown_events, :session_id, :message_id
609
+ ) do
610
+ KINDS = %i[
611
+ completed authentication_failure configuration_failure cli_failure
612
+ malformed_output cancelled timed_out
613
+ ].freeze
614
+
615
+ def initialize(provider:, launcher_identity:, kind:, termination:,
616
+ final_message: nil, identity:, usage: nil, diagnostic: nil,
617
+ unknown_events: [], session_id: nil, message_id: nil,
618
+ final_message_truncated: false)
619
+ normalized_kind = kind.to_sym
620
+ unless KINDS.include?(normalized_kind)
621
+ raise ArgumentError, "invalid normalized outcome kind"
622
+ end
623
+ unless termination.is_a?(TerminationEvidence)
624
+ raise ArgumentError, "termination must be TerminationEvidence"
625
+ end
626
+ unless identity.is_a?(RouteIdentity)
627
+ raise ArgumentError, "identity must be RouteIdentity"
628
+ end
629
+ unless usage.nil? || usage.is_a?(NormalizedUsage)
630
+ raise ArgumentError, "usage must be NormalizedUsage or nil"
631
+ end
632
+
633
+ super(
634
+ provider: provider.to_sym,
635
+ launcher_identity: Immutable.string(launcher_identity),
636
+ kind: normalized_kind,
637
+ termination: termination,
638
+ final_message:
639
+ final_message.nil? ? nil : Immutable.string(final_message),
640
+ final_message_truncated: final_message_truncated == true,
641
+ identity: identity,
642
+ usage: usage,
643
+ diagnostic: diagnostic.nil? ? nil : Immutable.string(diagnostic),
644
+ unknown_events: Immutable.strings(unknown_events),
645
+ session_id: session_id.nil? ? nil : Immutable.string(session_id),
646
+ message_id: message_id.nil? ? nil : Immutable.string(message_id)
647
+ )
648
+ end
649
+
650
+ def completed?
651
+ kind == :completed
652
+ end
653
+ end
176
654
  end
@@ -1,3 +1,3 @@
1
1
  module AgentCliRuntime
2
- VERSION = "0.1.1".freeze
2
+ VERSION = "0.2.4".freeze
3
3
  end
@@ -8,10 +8,16 @@ end
8
8
 
9
9
  require "agent_cli_runtime/redactor"
10
10
  require "agent_cli_runtime/usage_extractors"
11
+ require "agent_cli_runtime/error_extractors"
12
+ require "agent_cli_runtime/opencode/result_parser"
11
13
  require "agent_cli_runtime/profile"
12
14
  require "agent_cli_runtime/profiles"
13
15
  require "agent_cli_runtime/probe"
14
16
  require "agent_cli_runtime/runtime"
17
+ require "agent_cli_runtime/opencode/probe"
18
+ require "agent_cli_runtime/opencode/permissions"
19
+ require "agent_cli_runtime/opencode/overlay"
20
+ require "agent_cli_runtime/opencode/inspection"
15
21
  require "agent_cli_runtime/cli"
16
22
 
17
23
  module AgentCliRuntime
@@ -21,8 +27,8 @@ module AgentCliRuntime
21
27
  Runtime.compile(request)
22
28
  end
23
29
 
24
- def prepare!(profile)
25
- Runtime.prepare!(profile)
30
+ def prepare!(profile, env: ENV)
31
+ Runtime.prepare!(profile, env:)
26
32
  end
27
33
 
28
34
  def require_capability!(profile, capability)
@@ -33,12 +39,32 @@ module AgentCliRuntime
33
39
  Runtime.extract_usage(profile, event)
34
40
  end
35
41
 
42
+ def extract_provider_error(profile, event)
43
+ Runtime.extract_provider_error(profile, event)
44
+ end
45
+
36
46
  def observe(profile, result)
37
47
  Runtime.observe(profile, result)
38
48
  end
39
49
 
50
+ def parse_run(profile, stdout:)
51
+ Runtime.parse_run(profile, stdout:)
52
+ end
53
+
54
+ def prepare_inspection(prepared, parsed_run)
55
+ OpenCode::Inspection.compile(prepared, parsed_run)
56
+ end
57
+
58
+ def normalize(profile, captured, requested_route:)
59
+ Runtime.normalize(profile, captured, requested_route:)
60
+ end
61
+
40
62
  def probe(profile, home: nil, env: ENV)
41
- Probe.call(profile, home: home, env: env)
63
+ if profile.is_a?(ProbeRequest)
64
+ OpenCode::Probe.call(profile, env: env)
65
+ else
66
+ Probe.call(profile, home: home, env: env)
67
+ end
42
68
  end
43
69
 
44
70
  def probe_all(home: nil, env: ENV)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agent-cli-runtime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Kuznetsov
@@ -64,10 +64,11 @@ dependencies:
64
64
  - !ruby/object:Gem::Version
65
65
  version: '1.0'
66
66
  description: |
67
- Agent CLI Runtime provides immutable profiles, invocation compilation,
68
- local prerequisite probes, capability evidence, usage extraction, and
69
- result normalization for Claude Code, Codex CLI, Pi, and Grok CLI. It does
70
- not spawn agents or claim live provider health, quota, or credential validity.
67
+ Agent CLI Runtime gives Ruby applications one stable integration layer for
68
+ local coding-agent CLIs. It builds provider-specific commands from a shared
69
+ request model, checks local versions and named capabilities, and normalizes
70
+ usage and results afterward. Centralized profiles, environment rules, and
71
+ parsers make agent providers easier to add, switch, and upgrade.
71
72
  email:
72
73
  - ivan@ikuznetsov.com
73
74
  executables:
@@ -82,7 +83,13 @@ files:
82
83
  - exe/agent-runtime
83
84
  - lib/agent_cli_runtime.rb
84
85
  - lib/agent_cli_runtime/cli.rb
86
+ - lib/agent_cli_runtime/error_extractors.rb
85
87
  - lib/agent_cli_runtime/errors.rb
88
+ - lib/agent_cli_runtime/opencode/inspection.rb
89
+ - lib/agent_cli_runtime/opencode/overlay.rb
90
+ - lib/agent_cli_runtime/opencode/permissions.rb
91
+ - lib/agent_cli_runtime/opencode/probe.rb
92
+ - lib/agent_cli_runtime/opencode/result_parser.rb
86
93
  - lib/agent_cli_runtime/probe.rb
87
94
  - lib/agent_cli_runtime/profile.rb
88
95
  - lib/agent_cli_runtime/profiles.rb
@@ -117,5 +124,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
117
124
  requirements: []
118
125
  rubygems_version: 3.6.9
119
126
  specification_version: 4
120
- summary: Provider-neutral contracts for installed agent CLIs
127
+ summary: One Ruby API for Claude Code, Codex CLI, Pi, Grok CLI, and OpenCode
121
128
  test_files: []