aia 0.9.12 → 0.9.14

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.
@@ -85,30 +85,163 @@ module AIA
85
85
  puts "===================="
86
86
  puts
87
87
 
88
- directives = self.methods(false).map(&:to_s).reject do |m|
89
- ['run', 'initialize', 'private?', 'descriptions', 'aliases', 'build_aliases'].include?(m)
90
- end.sort
91
-
92
- build_aliases(directives)
88
+ # Manual descriptions for all directives
89
+ directive_descriptions = {
90
+ # Configuration directives
91
+ 'config' => 'View or set configuration values',
92
+ 'model' => 'View or change the AI model',
93
+ 'temperature' => 'Set the temperature parameter for AI responses',
94
+ 'top_p' => 'Set the top_p parameter for AI responses',
95
+ 'clear' => 'Clear the conversation context',
96
+ 'review' => 'Display the current conversation context with checkpoint markers',
97
+ 'checkpoint' => 'Create a named checkpoint of the current context',
98
+ 'restore' => 'Restore context to a previous checkpoint',
99
+
100
+ # Utility directives
101
+ 'tools' => 'List available tools',
102
+ 'next' => 'Set the next prompt in the sequence',
103
+ 'pipeline' => 'Set or view the prompt workflow sequence',
104
+ 'terse' => 'Add instruction for concise responses',
105
+ 'robot' => 'Display ASCII robot art',
106
+
107
+ # Execution directives
108
+ 'ruby' => 'Execute Ruby code',
109
+ 'shell' => 'Execute shell commands',
110
+ 'say' => 'Use text-to-speech to speak the text',
111
+
112
+ # Web and File directives
113
+ 'webpage' => 'Fetch and include content from a webpage',
114
+ 'include' => 'Include content from a file',
115
+ 'include_file' => 'Include file content (internal use)',
116
+ 'included_files' => 'List files that have been included',
117
+ 'included_files=' => 'Set the list of included files',
118
+
119
+ # Model directives
120
+ 'available_models' => 'List all available AI models',
121
+ 'compare' => 'Compare responses from multiple models',
122
+ 'help' => 'Show this help message',
123
+
124
+ # Aliases (these get their descriptions from main directive)
125
+ 'cfg' => nil, # alias for config
126
+ 'temp' => nil, # alias for temperature
127
+ 'topp' => nil, # alias for top_p
128
+ 'context' => nil, # alias for review
129
+ 'cp' => nil, # alias for checkpoint
130
+ 'workflow' => nil, # alias for pipeline
131
+ 'rb' => nil, # alias for ruby
132
+ 'sh' => nil, # alias for shell
133
+ 'web' => nil, # alias for webpage
134
+ 'website' => nil, # alias for webpage
135
+ 'import' => nil, # alias for include
136
+ 'models' => nil, # alias for available_models
137
+ 'all_models' => nil, # alias for available_models
138
+ 'am' => nil, # alias for available_models
139
+ 'llms' => nil, # alias for available_models
140
+ 'available' => nil, # alias for available_models
141
+ 'cmp' => nil, # alias for compare
142
+ }
143
+
144
+ # Get all registered directive modules from the Registry
145
+ all_modules = [
146
+ AIA::Directives::WebAndFile,
147
+ AIA::Directives::Utility,
148
+ AIA::Directives::Configuration,
149
+ AIA::Directives::Execution,
150
+ AIA::Directives::Models
151
+ ]
152
+
153
+ all_directives = {}
154
+ excluded_methods = ['run', 'initialize', 'private?', 'descriptions', 'aliases', 'build_aliases',
155
+ 'desc', 'method_added', 'register_directive_module', 'process',
156
+ 'directive?', 'prefix_size']
157
+
158
+ # Collect directives from all modules
159
+ all_modules.each do |mod|
160
+ methods = mod.methods(false).map(&:to_s).reject { |m| excluded_methods.include?(m) }
161
+
162
+ methods.each do |method|
163
+ # Skip if this is an alias (has nil description)
164
+ next if directive_descriptions.key?(method) && directive_descriptions[method].nil?
165
+
166
+ description = directive_descriptions[method] || method.gsub('_', ' ').capitalize
167
+
168
+ all_directives[method] = {
169
+ module: mod.name.split('::').last,
170
+ description: description,
171
+ aliases: []
172
+ }
173
+ end
174
+ end
93
175
 
94
- directives.each do |directive|
95
- next unless descriptions[directive]
176
+ # Manually map known aliases
177
+ alias_mappings = {
178
+ 'config' => ['cfg'],
179
+ 'temperature' => ['temp'],
180
+ 'top_p' => ['topp'],
181
+ 'review' => ['context'],
182
+ 'checkpoint' => ['cp'],
183
+ 'pipeline' => ['workflow'],
184
+ 'ruby' => ['rb'],
185
+ 'shell' => ['sh'],
186
+ 'webpage' => ['web', 'website'],
187
+ 'include' => ['import'],
188
+ 'available_models' => ['models', 'all_models', 'am', 'llms', 'available'],
189
+ 'compare' => ['cmp']
190
+ }
191
+
192
+ # Apply alias mappings
193
+ alias_mappings.each do |directive, aliases|
194
+ if all_directives[directive]
195
+ all_directives[directive][:aliases] = aliases
196
+ end
197
+ end
96
198
 
97
- others = aliases[directive]
199
+ # Sort and display directives by category
200
+ categories = {
201
+ 'Configuration' => ['config', 'model', 'temperature', 'top_p', 'clear', 'review', 'checkpoint', 'restore'],
202
+ 'Utility' => ['tools', 'next', 'pipeline', 'terse', 'robot', 'help'],
203
+ 'Execution' => ['ruby', 'shell', 'say'],
204
+ 'Web & Files' => ['webpage', 'include'],
205
+ 'Models' => ['available_models', 'compare']
206
+ }
207
+
208
+ categories.each do |category, directives|
209
+ puts "#{category}:"
210
+ puts "-" * category.length
211
+
212
+ directives.each do |directive|
213
+ info = all_directives[directive]
214
+ next unless info
215
+
216
+ if info[:aliases] && !info[:aliases].empty?
217
+ with_prefix = info[:aliases].map { |m| PromptManager::Prompt::DIRECTIVE_SIGNAL + m }
218
+ alias_text = " (aliases: #{with_prefix.join(', ')})"
219
+ else
220
+ alias_text = ""
221
+ end
98
222
 
99
- if others.empty?
100
- others_line = ""
101
- else
102
- with_prefix = others.map { |m| PromptManager::Prompt::DIRECTIVE_SIGNAL + m }
103
- others_line = "\tAliases:#{with_prefix.join(' ')}\n"
223
+ puts " //#{directive}#{alias_text}"
224
+ puts " #{info[:description]}"
225
+ puts
104
226
  end
227
+ end
105
228
 
106
- puts <<~TEXT
107
- //#{directive} #{descriptions[directive]}
108
- #{others_line}
109
- TEXT
229
+ # Show any uncategorized directives
230
+ categorized = categories.values.flatten
231
+ uncategorized = all_directives.keys - categorized - ['include_file', 'included_files', 'included_files=']
232
+
233
+ if uncategorized.any?
234
+ puts "Other:"
235
+ puts "------"
236
+ uncategorized.sort.each do |directive|
237
+ info = all_directives[directive]
238
+ puts " //#{directive}"
239
+ puts " #{info[:description]}"
240
+ puts
241
+ end
110
242
  end
111
243
 
244
+ puts "\nTotal: #{all_directives.size} directives available"
112
245
  ""
113
246
  end
114
247
 
@@ -310,9 +310,15 @@ module AIA
310
310
  prompt_parts << ""
311
311
 
312
312
  results.each do |model_name, result|
313
- next if result.to_s.start_with?("Error with")
313
+ # Extract content from RubyLLM::Message if needed
314
+ content = if result.respond_to?(:content)
315
+ result.content
316
+ else
317
+ result.to_s
318
+ end
319
+ next if content.start_with?("Error with")
314
320
  prompt_parts << "#{model_name}:"
315
- prompt_parts << result.to_s
321
+ prompt_parts << content
316
322
  prompt_parts << ""
317
323
  end
318
324
 
@@ -321,13 +327,64 @@ module AIA
321
327
  end
322
328
 
323
329
  def format_individual_responses(results)
324
- output = []
330
+ # For metrics support, return a special structure if all results have token info
331
+ has_metrics = results.values.all? { |r| r.respond_to?(:input_tokens) && r.respond_to?(:output_tokens) }
332
+
333
+ if has_metrics && AIA.config.show_metrics
334
+ # Return structured data that preserves metrics for multi-model
335
+ format_multi_model_with_metrics(results)
336
+ else
337
+ # Original string formatting for non-metrics mode
338
+ output = []
339
+ results.each do |model_name, result|
340
+ output << "from: #{model_name}"
341
+ # Extract content from RubyLLM::Message if needed
342
+ content = if result.respond_to?(:content)
343
+ result.content
344
+ else
345
+ result.to_s
346
+ end
347
+ output << content
348
+ output << "" # Add blank line between results
349
+ end
350
+ output.join("\n")
351
+ end
352
+ end
353
+
354
+ def format_multi_model_with_metrics(results)
355
+ # Create a composite response that includes all model responses and metrics
356
+ formatted_content = []
357
+ metrics_data = []
358
+
325
359
  results.each do |model_name, result|
326
- output << "from: #{model_name}"
327
- output << result
328
- output << "" # Add blank line between results
360
+ formatted_content << "from: #{model_name}"
361
+ formatted_content << result.content
362
+ formatted_content << ""
363
+
364
+ # Collect metrics for each model
365
+ metrics_data << {
366
+ model_id: model_name,
367
+ input_tokens: result.input_tokens,
368
+ output_tokens: result.output_tokens
369
+ }
370
+ end
371
+
372
+ # Return a special MultiModelResponse that ChatProcessorService can handle
373
+ MultiModelResponse.new(formatted_content.join("\n"), metrics_data)
374
+ end
375
+
376
+ # Helper class to carry multi-model response with metrics
377
+ class MultiModelResponse
378
+ attr_reader :content, :metrics_list
379
+
380
+ def initialize(content, metrics_list)
381
+ @content = content
382
+ @metrics_list = metrics_list
383
+ end
384
+
385
+ def multi_model?
386
+ true
329
387
  end
330
- output.join("\n")
331
388
  end
332
389
 
333
390
 
@@ -480,7 +537,8 @@ module AIA
480
537
  chat_instance.ask(text_prompt, with: AIA.config.context_files)
481
538
  end
482
539
 
483
- response.content
540
+ # Return the full response object to preserve token information
541
+ response
484
542
  rescue StandardError => e
485
543
  e.message
486
544
  end
data/lib/aia/session.rb CHANGED
@@ -372,11 +372,34 @@ module AIA
372
372
  conversation = @context_manager.get_context
373
373
 
374
374
  @ui_presenter.display_thinking_animation
375
- response = @chat_processor.process_prompt(conversation)
375
+ response_data = @chat_processor.process_prompt(conversation)
376
+
377
+ # Handle new response format with metrics
378
+ if response_data.is_a?(Hash)
379
+ content = response_data[:content]
380
+ metrics = response_data[:metrics]
381
+ multi_metrics = response_data[:multi_metrics]
382
+ else
383
+ content = response_data
384
+ metrics = nil
385
+ multi_metrics = nil
386
+ end
376
387
 
377
- @ui_presenter.display_ai_response(response)
378
- @context_manager.add_to_context(role: "assistant", content: response)
379
- @chat_processor.speak(response)
388
+ @ui_presenter.display_ai_response(content)
389
+
390
+ # Display metrics if enabled and available (chat mode only)
391
+ if AIA.config.show_metrics
392
+ if multi_metrics
393
+ # Display metrics for each model in multi-model mode
394
+ @ui_presenter.display_multi_model_metrics(multi_metrics)
395
+ elsif metrics
396
+ # Display metrics for single model
397
+ @ui_presenter.display_token_metrics(metrics)
398
+ end
399
+ end
400
+
401
+ @context_manager.add_to_context(role: "assistant", content: content)
402
+ @chat_processor.speak(content)
380
403
 
381
404
  @ui_presenter.display_separator
382
405
  end
@@ -384,10 +407,12 @@ module AIA
384
407
 
385
408
  def process_chat_directive(follow_up_prompt)
386
409
  directive_output = @directive_processor.process(follow_up_prompt, @context_manager)
387
-
410
+
388
411
  return handle_clear_directive if follow_up_prompt.strip.start_with?("//clear")
412
+ return handle_checkpoint_directive(directive_output) if follow_up_prompt.strip.start_with?("//checkpoint")
413
+ return handle_restore_directive(directive_output) if follow_up_prompt.strip.start_with?("//restore")
389
414
  return handle_empty_directive_output if directive_output.nil? || directive_output.strip.empty?
390
-
415
+
391
416
  handle_successful_directive(follow_up_prompt, directive_output)
392
417
  end
393
418
 
@@ -415,6 +440,31 @@ module AIA
415
440
  nil
416
441
  end
417
442
 
443
+ def handle_checkpoint_directive(directive_output)
444
+ @ui_presenter.display_info(directive_output)
445
+ nil
446
+ end
447
+
448
+ def handle_restore_directive(directive_output)
449
+ # If the restore was successful, we also need to refresh the client's context
450
+ if directive_output.start_with?("Context restored")
451
+ # Try to clear and rebuild the client's context
452
+ if AIA.config.client && AIA.config.client.respond_to?(:clear_context)
453
+ AIA.config.client.clear_context
454
+ end
455
+
456
+ # Optionally reinitialize the client for a clean state
457
+ begin
458
+ AIA.config.client = AIA::RubyLLMAdapter.new
459
+ rescue => e
460
+ STDERR.puts "Error reinitializing client after restore: #{e.message}"
461
+ end
462
+ end
463
+
464
+ @ui_presenter.display_info(directive_output)
465
+ nil
466
+ end
467
+
418
468
  def handle_empty_directive_output
419
469
  nil
420
470
  end
@@ -117,5 +117,211 @@ module AIA
117
117
  yield
118
118
  end
119
119
  end
120
+
121
+ def display_token_metrics(metrics)
122
+ return unless metrics
123
+
124
+ output_lines = []
125
+ output_lines << "═" * 55
126
+ output_lines << "Model: #{metrics[:model_id]}"
127
+
128
+ if AIA.config.show_cost
129
+ output_lines.concat(format_metrics_with_cost(metrics))
130
+ else
131
+ output_lines.concat(format_metrics_basic(metrics))
132
+ end
133
+
134
+ output_lines << "═" * 55
135
+
136
+ # Output to STDOUT
137
+ output_lines.each { |line| puts line }
138
+
139
+ # Also write to file if configured
140
+ if AIA.config.out_file && !AIA.config.out_file.nil?
141
+ File.open(AIA.config.out_file, 'a') do |file|
142
+ output_lines.each { |line| file.puts line }
143
+ end
144
+ end
145
+ end
146
+
147
+ def display_multi_model_metrics(metrics_list)
148
+ return unless metrics_list && !metrics_list.empty?
149
+
150
+ output_lines = []
151
+
152
+ # Determine table width based on whether costs are shown
153
+ if AIA.config.show_cost
154
+ table_width = 80
155
+ else
156
+ table_width = 60
157
+ end
158
+
159
+ output_lines << "═" * table_width
160
+ output_lines << "Multi-Model Token Usage"
161
+ output_lines << "─" * table_width
162
+
163
+ # Build header row
164
+ if AIA.config.show_cost
165
+ output_lines << sprintf("%-20s %10s %10s %10s %12s %10s",
166
+ "Model", "Input", "Output", "Total", "Cost", "x1000")
167
+ output_lines << "─" * table_width
168
+ else
169
+ output_lines << sprintf("%-20s %10s %10s %10s",
170
+ "Model", "Input", "Output", "Total")
171
+ output_lines << "─" * table_width
172
+ end
173
+
174
+ # Process each model
175
+ total_input = 0
176
+ total_output = 0
177
+ total_cost = 0.0
178
+
179
+ metrics_list.each do |metrics|
180
+ model_name = metrics[:model_id]
181
+ # Truncate model name if too long
182
+ model_name = model_name[0..17] + ".." if model_name.length > 19
183
+
184
+ input_tokens = metrics[:input_tokens] || 0
185
+ output_tokens = metrics[:output_tokens] || 0
186
+ total_tokens = input_tokens + output_tokens
187
+
188
+ if AIA.config.show_cost
189
+ cost_data = calculate_cost(metrics)
190
+ if cost_data[:available]
191
+ cost_str = "$#{'%.5f' % cost_data[:total_cost]}"
192
+ x1000_str = "$#{'%.2f' % (cost_data[:total_cost] * 1000)}"
193
+ total_cost += cost_data[:total_cost]
194
+ else
195
+ cost_str = "N/A"
196
+ x1000_str = "N/A"
197
+ end
198
+
199
+ output_lines << sprintf("%-20s %10d %10d %10d %12s %10s",
200
+ model_name, input_tokens, output_tokens, total_tokens, cost_str, x1000_str)
201
+ else
202
+ output_lines << sprintf("%-20s %10d %10d %10d",
203
+ model_name, input_tokens, output_tokens, total_tokens)
204
+ end
205
+
206
+ total_input += input_tokens
207
+ total_output += output_tokens
208
+ end
209
+
210
+ # Display totals row
211
+ output_lines << "─" * table_width
212
+ total_tokens = total_input + total_output
213
+
214
+ if AIA.config.show_cost && total_cost > 0
215
+ cost_str = "$#{'%.5f' % total_cost}"
216
+ x1000_str = "$#{'%.2f' % (total_cost * 1000)}"
217
+ output_lines << sprintf("%-20s %10d %10d %10d %12s %10s",
218
+ "TOTAL", total_input, total_output, total_tokens, cost_str, x1000_str)
219
+ else
220
+ output_lines << sprintf("%-20s %10d %10d %10d",
221
+ "TOTAL", total_input, total_output, total_tokens)
222
+ end
223
+
224
+ output_lines << "═" * table_width
225
+
226
+ # Output to STDOUT
227
+ output_lines.each { |line| puts line }
228
+
229
+ # Also write to file if configured
230
+ if AIA.config.out_file && !AIA.config.out_file.nil?
231
+ File.open(AIA.config.out_file, 'a') do |file|
232
+ output_lines.each { |line| file.puts line }
233
+ end
234
+ end
235
+ end
236
+
237
+ private
238
+
239
+ def display_metrics_basic(metrics)
240
+ puts "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
241
+ puts "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
242
+
243
+ if metrics[:input_tokens] && metrics[:output_tokens]
244
+ total = metrics[:input_tokens] + metrics[:output_tokens]
245
+ puts "Total tokens: #{total}"
246
+ end
247
+ end
248
+
249
+ def format_metrics_basic(metrics)
250
+ lines = []
251
+ lines << "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
252
+ lines << "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
253
+
254
+ if metrics[:input_tokens] && metrics[:output_tokens]
255
+ total = metrics[:input_tokens] + metrics[:output_tokens]
256
+ lines << "Total tokens: #{total}"
257
+ end
258
+
259
+ lines
260
+ end
261
+
262
+ def display_metrics_with_cost(metrics)
263
+ cost_data = calculate_cost(metrics)
264
+
265
+ if cost_data[:available]
266
+ puts "Input tokens: #{metrics[:input_tokens]} ($#{'%.5f' % cost_data[:input_cost]})"
267
+ puts "Output tokens: #{metrics[:output_tokens]} ($#{'%.5f' % cost_data[:output_cost]})"
268
+ puts "Total cost: $#{'%.5f' % cost_data[:total_cost]}"
269
+ puts "Cost x1000: $#{'%.2f' % (cost_data[:total_cost] * 1000)}"
270
+ else
271
+ puts "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
272
+ puts "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
273
+ total = (metrics[:input_tokens] || 0) + (metrics[:output_tokens] || 0)
274
+ puts "Total tokens: #{total}"
275
+ puts "Cost: N/A (pricing not available)"
276
+ end
277
+ end
278
+
279
+ def format_metrics_with_cost(metrics)
280
+ lines = []
281
+ cost_data = calculate_cost(metrics)
282
+
283
+ if cost_data[:available]
284
+ lines << "Input tokens: #{metrics[:input_tokens]} ($#{'%.5f' % cost_data[:input_cost]})"
285
+ lines << "Output tokens: #{metrics[:output_tokens]} ($#{'%.5f' % cost_data[:output_cost]})"
286
+ lines << "Total cost: $#{'%.5f' % cost_data[:total_cost]}"
287
+ lines << "Cost x1000: $#{'%.2f' % (cost_data[:total_cost] * 1000)}"
288
+ else
289
+ lines << "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
290
+ lines << "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
291
+ total = (metrics[:input_tokens] || 0) + (metrics[:output_tokens] || 0)
292
+ lines << "Total tokens: #{total}"
293
+ lines << "Cost: N/A (pricing not available)"
294
+ end
295
+
296
+ lines
297
+ end
298
+
299
+ def calculate_cost(metrics)
300
+ return { available: false } unless metrics[:model_id] && metrics[:input_tokens] && metrics[:output_tokens]
301
+
302
+ # Look up model info from RubyLLM
303
+ begin
304
+ model_info = RubyLLM::Models.find(metrics[:model_id])
305
+ return { available: false } unless model_info
306
+
307
+ input_price = model_info.input_price_per_million
308
+ output_price = model_info.output_price_per_million
309
+
310
+ return { available: false } unless input_price && output_price
311
+
312
+ input_cost = metrics[:input_tokens] * input_price / 1_000_000.0
313
+ output_cost = metrics[:output_tokens] * output_price / 1_000_000.0
314
+ total_cost = input_cost + output_cost
315
+
316
+ {
317
+ available: true,
318
+ input_cost: input_cost,
319
+ output_cost: output_cost,
320
+ total_cost: total_cost
321
+ }
322
+ rescue StandardError => e
323
+ { available: false, error: e.message }
324
+ end
325
+ end
120
326
  end
121
327
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.12
4
+ version: 0.9.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: 0.5.7
74
+ version: 0.5.8
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: 0.5.7
81
+ version: 0.5.8
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: ruby_llm
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -177,20 +177,6 @@ dependencies:
177
177
  - - ">="
178
178
  - !ruby/object:Gem::Version
179
179
  version: '0'
180
- - !ruby/object:Gem::Dependency
181
- name: versionaire
182
- requirement: !ruby/object:Gem::Requirement
183
- requirements:
184
- - - ">="
185
- - !ruby/object:Gem::Version
186
- version: '0'
187
- type: :runtime
188
- prerelease: false
189
- version_requirements: !ruby/object:Gem::Requirement
190
- requirements:
191
- - - ">="
192
- - !ruby/object:Gem::Version
193
- version: '0'
194
180
  - !ruby/object:Gem::Dependency
195
181
  name: word_wrapper
196
182
  requirement: !ruby/object:Gem::Requirement
@@ -329,7 +315,6 @@ files:
329
315
  - LICENSE
330
316
  - README.md
331
317
  - Rakefile
332
- - _notes.txt
333
318
  - bin/aia
334
319
  - docs/advanced-prompting.md
335
320
  - docs/assets/images/aia.png
@@ -448,7 +433,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
448
433
  - !ruby/object:Gem::Version
449
434
  version: '0'
450
435
  requirements: []
451
- rubygems_version: 3.7.1
436
+ rubygems_version: 3.6.9
452
437
  specification_version: 4
453
438
  summary: Multi-model AI CLI with dynamic prompts, consensus responses, shell & Ruby
454
439
  integration, and seamless chat workflows.