agent-cli-runtime 0.2.0 → 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.
@@ -46,12 +46,23 @@ module AgentCliRuntime
46
46
  end
47
47
  argv.concat(request.trusted_cli_arguments)
48
48
  argv.concat(profile.output_format_flags) if request.include_output_format
49
- argv << (prompt_style == :stdin ? "-" : request.prompt) unless
50
- prompt_style == :headless_flag_value
49
+ stdin_data =
50
+ case prompt_style
51
+ when :positional
52
+ argv << request.prompt
53
+ nil
54
+ when :headless_flag_value
55
+ nil
56
+ when :stdin
57
+ argv << "-"
58
+ request.prompt
59
+ when :piped_stdin
60
+ request.prompt
61
+ end
51
62
 
52
63
  CompiledInvocation.new(
53
64
  argv: request.command_prefix + argv,
54
- stdin_data: prompt_style == :stdin ? request.prompt : nil,
65
+ stdin_data:,
55
66
  provider: profile.name,
56
67
  launcher_identity: profile.launcher_identity,
57
68
  capability_evidence: evidence
@@ -107,6 +118,51 @@ module AgentCliRuntime
107
118
  end
108
119
  end
109
120
 
121
+ # A provider failure seen mid-stream, normalized to
122
+ # {kind:, provider:, status_code:, message:}, or nil when the event is
123
+ # clean. Callers use this to tell a provider-side stop (quota, credit
124
+ # ceiling, rate limit, model output truncation) apart from an agent that
125
+ # genuinely produced nothing: several CLIs report the former on the stream
126
+ # and still exit zero.
127
+ def extract_provider_error(profile, event)
128
+ resolved = Profiles.resolve(profile)
129
+ extracted = resolved.extract_error_event(event)
130
+ return nil if extracted.nil?
131
+
132
+ kind, text = normalize_extracted_error(extracted)
133
+ return nil if text.nil? || text.strip.empty?
134
+
135
+ {
136
+ kind: kind,
137
+ provider: resolved.name,
138
+ status_code: status_code_from(text),
139
+ message: Redactor.diagnostic(text)
140
+ }.freeze
141
+ end
142
+
143
+ def normalize_extracted_error(extracted)
144
+ return [ extracted.kind, extracted.message ] if extracted.is_a?(ExtractedFailure)
145
+ return [ failure_kind_from(extracted), extracted ] if extracted.is_a?(String)
146
+
147
+ [ nil, nil ]
148
+ end
149
+ private_class_method :normalize_extracted_error
150
+
151
+ def failure_kind_from(text)
152
+ status = status_code_from(text)
153
+ return :provider_limit if status == 402
154
+ return :rate_limited if status == 429
155
+
156
+ normalized = text.to_s.downcase
157
+ return :rate_limited if normalized.match?(/\brate[\s_-]*limit(?:ed|s)?\b/)
158
+ return :provider_limit if normalized.match?(
159
+ /\b(?:quota|billing|credits?|tokens?[\s_-]*limit|usage[\s_-]*limit)\b/
160
+ )
161
+
162
+ :provider_error
163
+ end
164
+ private_class_method :failure_kind_from
165
+
110
166
  def observe(profile, result)
111
167
  resolved = Profiles.resolve(profile)
112
168
  raw = result.is_a?(Hash) ? result : {}
@@ -267,22 +323,57 @@ module AgentCliRuntime
267
323
  end
268
324
  private_class_method :tool_csv
269
325
 
326
+ # Providers prefix the status onto the text ("402: {...}") or carry it in
327
+ # the embedded payload. Either is enough to classify without matching on
328
+ # human-readable wording, which differs per provider and changes freely.
329
+ def status_code_from(text)
330
+ value = text[/\A\s*(\d{3})\s*:/, 1] ||
331
+ text[/"(?:code|http_status)"\s*:\s*(\d{3})\b/, 1]
332
+ code = value.to_i
333
+ code.between?(100, 599) ? code : nil
334
+ end
335
+ private_class_method :status_code_from
336
+
270
337
  def normalize_usage(usage)
271
338
  return nil unless usage.is_a?(Hash)
272
339
 
273
- input = usage.key?(:input) ? usage[:input] : usage["input"]
274
- output = usage.key?(:output) ? usage[:output] : usage["output"]
275
- cached = usage.key?(:cached) ? usage[:cached] : usage["cached"]
276
- model = usage.key?(:model) ? usage[:model] : usage["model"]
340
+ value = ->(key) { usage.key?(key) ? usage[key] : usage[key.to_s] }
277
341
  {
278
- input: [ input.to_i, 0 ].max,
279
- output: [ output.to_i, 0 ].max,
280
- cached: [ cached.to_i, 0 ].max,
281
- model: model&.to_s&.dup&.freeze
342
+ input: normalized_count(value.call(:input)),
343
+ output: normalized_count(value.call(:output)),
344
+ cached: normalized_count(value.call(:cached)),
345
+ cache_read: normalized_count(value.call(:cache_read)),
346
+ cache_write: normalized_count(value.call(:cache_write)),
347
+ reasoning: normalized_count(value.call(:reasoning)),
348
+ input_includes_cache_read: normalized_boolean(value.call(:input_includes_cache_read)),
349
+ input_includes_cache_write: normalized_boolean(value.call(:input_includes_cache_write)),
350
+ output_includes_reasoning: normalized_boolean(value.call(:output_includes_reasoning)),
351
+ model: value.call(:model)&.to_s&.dup&.freeze,
352
+ # Carried as a float: this is money, and normalized_count would round
353
+ # a fraction-of-a-cent charge to zero.
354
+ provider_reported_cost: normalized_cost(value.call(:provider_reported_cost))
282
355
  }.freeze
283
356
  end
284
357
  private_class_method :normalize_usage
285
358
 
359
+ def normalized_cost(value)
360
+ return nil unless value.is_a?(Numeric)
361
+
362
+ cost = value.to_f
363
+ cost if cost >= 0 && cost.finite?
364
+ end
365
+ private_class_method :normalized_cost
366
+
367
+ def normalized_count(value)
368
+ value.nil? ? nil : [ value.to_i, 0 ].max
369
+ end
370
+ private_class_method :normalized_count
371
+
372
+ def normalized_boolean(value)
373
+ value if value == true || value == false
374
+ end
375
+ private_class_method :normalized_boolean
376
+
286
377
  def unsupported!(profile, capability, diagnostic)
287
378
  evidence = unsupported_evidence(profile, capability, diagnostic)
288
379
  raise UnsupportedCapability.new(
@@ -7,9 +7,9 @@ module AgentCliRuntime
7
7
 
8
8
  usage = usage_hash(event)
9
9
  if event["type"] == "result" && usage
10
- usage_result(event, usage, model_from(event))
10
+ usage_result(event, usage, model_from(event), provider: :claude)
11
11
  elsif event["type"] == "stream_event" && usage
12
- usage_result(event, usage, model_from(event))
12
+ usage_result(event, usage, model_from(event), provider: :claude)
13
13
  end
14
14
  end
15
15
 
@@ -17,21 +17,21 @@ module AgentCliRuntime
17
17
  next nil unless event.is_a?(Hash)
18
18
 
19
19
  usage = usage_hash(event)
20
- usage_result(event, usage, model_from(event)) if usage
20
+ usage_result(event, usage, model_from(event), provider: :codex) if usage
21
21
  end
22
22
 
23
23
  PI = lambda do |event|
24
24
  next nil unless event.is_a?(Hash)
25
25
 
26
26
  usage = usage_hash(event)
27
- usage_result(event, usage, model_from(event)) if usage
27
+ usage_result(event, usage, model_from(event), provider: :pi) if usage
28
28
  end
29
29
 
30
30
  GROK = lambda do |event|
31
31
  next nil unless event.is_a?(Hash)
32
32
 
33
33
  usage = usage_hash(event)
34
- usage_result(event, usage, model_from(event)) if usage
34
+ usage_result(event, usage, model_from(event), provider: :grok) if usage
35
35
  end
36
36
 
37
37
  def usage_hash(event)
@@ -40,6 +40,9 @@ module AgentCliRuntime
40
40
  event["token_usage"],
41
41
  event.dig("event", "usage"),
42
42
  event.dig("event", "message", "usage"),
43
+ # pi reports usage on the assistant message itself, not under an
44
+ # "event" envelope. Without this every pi run is unmetered.
45
+ event.dig("message", "usage"),
43
46
  event.dig("info", "usage"),
44
47
  event.dig("info", "total_token_usage"),
45
48
  event.dig("response", "usage"),
@@ -48,42 +51,126 @@ module AgentCliRuntime
48
51
  candidates.find { |value| value.is_a?(Hash) }
49
52
  end
50
53
 
51
- def usage_result(event, usage, model)
54
+ def usage_result(event, usage, model, provider:)
55
+ cache_read = cache_read_tokens(usage)
56
+ cache_write = cache_write_tokens(usage)
57
+ aggregate_cached = aggregate_cached_tokens(usage)
58
+ reasoning = optional_token_count(
59
+ usage, "reasoning_tokens", "reasoningTokens"
60
+ )
61
+ reasoning ||= optional_token_count(
62
+ nested_hash(usage, "completion_tokens_details"),
63
+ "reasoning_tokens", "reasoningTokens"
64
+ )
52
65
  {
53
- input: token_count(
54
- usage, "input_tokens", "inputTokens", "prompt_tokens", "promptTokens"
66
+ # The bare "input"/"output" spellings are pi's and are matched last,
67
+ # so a provider using an explicit *_tokens key keeps its own reading.
68
+ input: optional_token_count(
69
+ usage, "input_tokens", "inputTokens", "prompt_tokens", "promptTokens",
70
+ "input"
55
71
  ),
56
- output: token_count(
72
+ output: optional_token_count(
57
73
  usage, "output_tokens", "outputTokens", "completion_tokens",
58
- "completionTokens"
74
+ "completionTokens", "output"
59
75
  ),
60
- cached: cached_tokens(usage),
61
- model: model || model_from_usage(usage) || model_from(event)
76
+ cached: aggregate_cached || complete_cached(cache_read, cache_write),
77
+ cache_read: cache_read,
78
+ cache_write: cache_write,
79
+ reasoning: reasoning,
80
+ input_includes_cache_read:
81
+ inclusion_or_inferred(
82
+ usage, "input_includes_cache_read",
83
+ inferred_input_cache_inclusion(provider, cache_read)
84
+ ),
85
+ input_includes_cache_write:
86
+ inclusion_or_inferred(
87
+ usage, "input_includes_cache_write",
88
+ inferred_input_cache_inclusion(provider, cache_write)
89
+ ),
90
+ output_includes_reasoning:
91
+ inclusion_or_inferred(
92
+ usage, "output_includes_reasoning",
93
+ inferred_reasoning_inclusion(provider, reasoning)
94
+ ),
95
+ model: model || model_from_usage(usage) || model_from(event),
96
+ provider_reported_cost: reported_cost(usage)
62
97
  }
63
98
  end
64
99
 
65
- def cached_tokens(usage)
66
- direct = token_count(
100
+ # A charge the provider itself reported for this turn, in USD. This is an
101
+ # observed amount, not an estimate from the pricing catalog, and it is the
102
+ # only cost available for a model the catalog does not carry — an
103
+ # OpenRouter-routed model, for one. OpenRouter sends a per-category
104
+ # breakdown carrying a total; other providers may send a scalar.
105
+ def reported_cost(usage)
106
+ value = usage["cost"] || usage["total_cost"] || usage["totalCost"]
107
+ value = value["total"] || value["total_cost"] if value.is_a?(Hash)
108
+ return nil unless value.is_a?(Numeric) || value.is_a?(String)
109
+
110
+ cost = begin
111
+ Float(value)
112
+ rescue ArgumentError, TypeError
113
+ nil
114
+ end
115
+ cost if cost && cost >= 0 && cost.finite?
116
+ end
117
+
118
+ def aggregate_cached_tokens(usage)
119
+ optional_token_count(
67
120
  usage, "cached", "cached_tokens", "cachedTokens",
68
121
  "cached_input_tokens", "cachedInputTokens"
69
122
  )
70
- return direct if direct.positive?
123
+ end
124
+
125
+ def cache_read_tokens(usage)
126
+ optional_token_count(
127
+ usage, "cache_read_input_tokens", "cacheReadInputTokens"
128
+ ) || optional_token_count(
129
+ nested_hash(usage, "prompt_tokens_details"), "cached_tokens", "cachedTokens"
130
+ ) || optional_token_count(
131
+ nested_hash(usage, "input_tokens_details"), "cached_tokens", "cachedTokens"
132
+ ) || optional_token_count(usage, "cacheRead")
133
+ end
134
+
135
+ def cache_write_tokens(usage)
136
+ optional_token_count(
137
+ usage, "cache_creation_input_tokens", "cacheCreationInputTokens",
138
+ "cache_write_input_tokens", "cacheWriteInputTokens", "cacheWrite"
139
+ )
140
+ end
71
141
 
72
- token_count(usage, "cache_read_input_tokens", "cacheReadInputTokens") +
73
- token_count(usage, "cache_creation_input_tokens", "cacheCreationInputTokens") +
74
- token_count(
75
- nested_hash(usage, "prompt_tokens_details"), "cached_tokens", "cachedTokens"
76
- ) +
77
- token_count(
78
- nested_hash(usage, "input_tokens_details"), "cached_tokens", "cachedTokens"
79
- )
142
+ def complete_cached(cache_read, cache_write)
143
+ return nil if cache_read.nil? || cache_write.nil?
144
+
145
+ cache_read + cache_write
80
146
  end
81
147
 
82
- def token_count(hash, *keys)
83
- return 0 unless hash.is_a?(Hash)
148
+ def inferred_input_cache_inclusion(provider, value)
149
+ return nil if value.nil?
150
+ return false if provider == :claude
151
+ return true if provider == :codex
152
+
153
+ nil
154
+ end
155
+
156
+ def inferred_reasoning_inclusion(provider, value)
157
+ return nil if value.nil?
158
+
159
+ provider == :codex ? true : nil
160
+ end
161
+
162
+ def inclusion_or_inferred(hash, key, inferred)
163
+ return inferred unless hash.is_a?(Hash) && hash.key?(key)
164
+
165
+ value = hash[key]
166
+ value if value == true || value == false
167
+ end
168
+
169
+ def optional_token_count(hash, *keys)
170
+ return nil unless hash.is_a?(Hash)
84
171
 
85
172
  key = keys.find { |candidate| hash.key?(candidate) }
86
- key ? hash[key].to_i : 0
173
+ key ? hash[key].to_i : nil
87
174
  end
88
175
 
89
176
  def nested_hash(hash, key)
@@ -92,11 +179,6 @@ module AgentCliRuntime
92
179
  end
93
180
 
94
181
  def model_from(event)
95
- model_usage = event["modelUsage"]
96
- if model_usage.is_a?(Hash) && !model_usage.empty?
97
- return model_usage.keys.first.to_s
98
- end
99
-
100
182
  candidates = [
101
183
  event["model"],
102
184
  event.dig("message", "model"),
@@ -104,7 +186,12 @@ module AgentCliRuntime
104
186
  event.dig("response", "model"),
105
187
  event.dig("item", "model")
106
188
  ]
107
- candidates.find { |value| !value.to_s.empty? }&.to_s
189
+ explicit = candidates.find { |value| !value.to_s.empty? }
190
+ return explicit.to_s if explicit
191
+
192
+ # Run totals may include helper models; their order is not agent identity.
193
+ model_usage = event["modelUsage"]
194
+ model_usage.keys.first.to_s if model_usage.is_a?(Hash) && model_usage.size == 1
108
195
  end
109
196
 
110
197
  def model_from_usage(usage)
@@ -81,6 +81,12 @@ module AgentCliRuntime
81
81
  end
82
82
  end
83
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
+
84
90
  Request = Data.define(
85
91
  :profile, :prompt, :permission_mode, :permission_arguments,
86
92
  :add_dirs, :require_add_dirs,
@@ -265,13 +271,14 @@ module AgentCliRuntime
265
271
  :configuration_path, :configuration,
266
272
  :credential_environment_keys, :credential_file,
267
273
  :permission_policy, :additional_read_roots,
268
- :additional_write_roots, :edit_patterns, :plugins, :pure
274
+ :additional_write_roots, :edit_patterns, :bash_patterns, :plugins, :pure
269
275
  ) do
270
276
  def initialize(request:, working_directory:, invocation_root:,
271
277
  configuration_path: nil, configuration: nil,
272
278
  credential_environment_keys: [], credential_file: nil,
273
279
  permission_policy: nil, additional_read_roots: [],
274
- additional_write_roots: [], edit_patterns: [], plugins: [], pure: true)
280
+ additional_write_roots: [], edit_patterns: [],
281
+ bash_patterns: [], plugins: [], pure: true)
275
282
  unless request.is_a?(Request)
276
283
  raise ArgumentError, "request must be an AgentCliRuntime::Request"
277
284
  end
@@ -294,6 +301,10 @@ module AgentCliRuntime
294
301
  raise ArgumentError,
295
302
  "credential environment keys cannot override the OpenCode overlay: #{reserved.join(', ')}"
296
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
297
308
 
298
309
  super(
299
310
  request: request,
@@ -310,6 +321,7 @@ module AgentCliRuntime
310
321
  additional_read_roots: Immutable.strings(additional_read_roots),
311
322
  additional_write_roots: Immutable.strings(additional_write_roots),
312
323
  edit_patterns: Immutable.strings(edit_patterns),
324
+ bash_patterns: normalized_bash_patterns,
313
325
  plugins: Immutable.strings(plugins),
314
326
  pure: pure != false
315
327
  )
@@ -318,11 +330,14 @@ module AgentCliRuntime
318
330
 
319
331
  ProbeRequest = Data.define(
320
332
  :profile, :route, :variant, :environment,
321
- :credential_environment_keys, :credential_file_staged
333
+ :credential_environment_keys, :credential_file_staged,
334
+ :configured_variants, :executable
322
335
  ) do
323
336
  def initialize(profile:, route:, variant: nil, environment: {},
324
337
  credential_environment_keys: [],
325
- credential_file_staged: false)
338
+ credential_file_staged: false,
339
+ configured_variants: nil,
340
+ executable: nil)
326
341
  parsed_route = route.is_a?(Route) ? route : Route.parse(route)
327
342
  super(
328
343
  profile: profile,
@@ -331,7 +346,10 @@ module AgentCliRuntime
331
346
  environment: Immutable.hash(environment),
332
347
  credential_environment_keys:
333
348
  Immutable.strings(credential_environment_keys),
334
- credential_file_staged: credential_file_staged == true
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)
335
353
  )
336
354
  end
337
355
  end
@@ -445,17 +463,35 @@ module AgentCliRuntime
445
463
  end
446
464
 
447
465
  NormalizedUsage = Data.define(
448
- :input, :output, :cache_read, :cache_write, :reasoning, :cost
466
+ :input, :output, :cache_read, :cache_write, :reasoning,
467
+ :input_includes_cache_read, :input_includes_cache_write,
468
+ :output_includes_reasoning, :cost
449
469
  ) do
450
470
  def initialize(input: nil, output: nil, cache_read: nil, cache_write: nil,
451
- reasoning: nil, cost: 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
452
478
  super(
453
479
  input: number(input, :input, integer: true),
454
480
  output: number(output, :output, integer: true),
455
481
  cache_read: number(cache_read, :cache_read, integer: true),
456
482
  cache_write: number(cache_write, :cache_write, integer: true),
457
483
  reasoning: number(reasoning, :reasoning, integer: true),
458
- cost: number(cost, :cost, integer: false)
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
+ )
459
495
  )
460
496
  end
461
497
 
@@ -465,6 +501,8 @@ module AgentCliRuntime
465
501
  cache_read + cache_write
466
502
  end
467
503
 
504
+ alias provider_reported_cost cost
505
+
468
506
  private
469
507
 
470
508
  def number(value, label, integer:)
@@ -477,6 +515,14 @@ module AgentCliRuntime
477
515
 
478
516
  value
479
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
480
526
  end
481
527
 
482
528
  RouteIdentity = Data.define(:requested, :actual, :resolution_status) do
@@ -1,3 +1,3 @@
1
1
  module AgentCliRuntime
2
- VERSION = "0.2.0".freeze
2
+ VERSION = "0.2.4".freeze
3
3
  end
@@ -8,12 +8,14 @@ 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"
11
12
  require "agent_cli_runtime/opencode/result_parser"
12
13
  require "agent_cli_runtime/profile"
13
14
  require "agent_cli_runtime/profiles"
14
15
  require "agent_cli_runtime/probe"
15
16
  require "agent_cli_runtime/runtime"
16
17
  require "agent_cli_runtime/opencode/probe"
18
+ require "agent_cli_runtime/opencode/permissions"
17
19
  require "agent_cli_runtime/opencode/overlay"
18
20
  require "agent_cli_runtime/opencode/inspection"
19
21
  require "agent_cli_runtime/cli"
@@ -37,6 +39,10 @@ module AgentCliRuntime
37
39
  Runtime.extract_usage(profile, event)
38
40
  end
39
41
 
42
+ def extract_provider_error(profile, event)
43
+ Runtime.extract_provider_error(profile, event)
44
+ end
45
+
40
46
  def observe(profile, result)
41
47
  Runtime.observe(profile, result)
42
48
  end
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.2.0
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Kuznetsov
@@ -64,11 +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, Grok CLI, and
70
- OpenCode. It does not spawn agents or claim live provider health, quota,
71
- 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.
72
72
  email:
73
73
  - ivan@ikuznetsov.com
74
74
  executables:
@@ -83,9 +83,11 @@ files:
83
83
  - exe/agent-runtime
84
84
  - lib/agent_cli_runtime.rb
85
85
  - lib/agent_cli_runtime/cli.rb
86
+ - lib/agent_cli_runtime/error_extractors.rb
86
87
  - lib/agent_cli_runtime/errors.rb
87
88
  - lib/agent_cli_runtime/opencode/inspection.rb
88
89
  - lib/agent_cli_runtime/opencode/overlay.rb
90
+ - lib/agent_cli_runtime/opencode/permissions.rb
89
91
  - lib/agent_cli_runtime/opencode/probe.rb
90
92
  - lib/agent_cli_runtime/opencode/result_parser.rb
91
93
  - lib/agent_cli_runtime/probe.rb
@@ -122,5 +124,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
124
  requirements: []
123
125
  rubygems_version: 3.6.9
124
126
  specification_version: 4
125
- summary: Provider-neutral contracts for installed agent CLIs
127
+ summary: One Ruby API for Claude Code, Codex CLI, Pi, Grok CLI, and OpenCode
126
128
  test_files: []