aicli 0.1.1 → 0.2.6

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.
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm'
4
+
5
+ module AiCli
6
+ module Helpers
7
+ module Llm
8
+ PROVIDERS = %w[openai anthropic].freeze
9
+
10
+ DEFAULT_MODELS = {
11
+ 'openai' => 'gpt-4o-mini',
12
+ 'anthropic' => 'claude-sonnet-4-6'
13
+ }.freeze
14
+
15
+ module_function
16
+
17
+ def configure!(config)
18
+ provider = normalize_provider(config['PROVIDER'])
19
+ ensure_credentials!(config, provider)
20
+
21
+ RubyLLM.configure do |c|
22
+ openai_key = config['OPENAI_KEY'].to_s
23
+ anthropic_key = config['ANTHROPIC_KEY'].to_s
24
+ endpoint = config['OPENAI_API_ENDPOINT'].to_s
25
+
26
+ c.openai_api_key = openai_key unless openai_key.empty?
27
+ c.anthropic_api_key = anthropic_key unless anthropic_key.empty?
28
+
29
+ if !endpoint.empty? && endpoint != 'https://api.openai.com/v1'
30
+ c.openai_api_base = endpoint.sub(%r{/+\z}, '')
31
+ end
32
+ end
33
+
34
+ provider
35
+ end
36
+
37
+ def build_chat(config)
38
+ provider, model = resolve_provider_and_model(config)
39
+ ensure_credentials!(config, provider)
40
+
41
+ configure!(config.merge('PROVIDER' => provider, 'MODEL' => model))
42
+
43
+ opts = { model: model, provider: provider.to_sym }
44
+ opts[:assume_model_exists] = true unless model_known?(model, provider)
45
+ RubyLLM.chat(**opts)
46
+ end
47
+
48
+ # Returns [provider, model], fixing mismatches against the RubyLLM registry.
49
+ def resolve_provider_and_model(config)
50
+ provider = normalize_provider(config['PROVIDER'])
51
+ model = config['MODEL'].to_s
52
+ model = default_model_for(provider) if model.empty?
53
+
54
+ model_provider = provider_for_model(model)
55
+ if model_provider && model_provider != provider
56
+ # Prefer the provider that actually owns the selected model.
57
+ provider = model_provider
58
+ elsif model_provider.nil? && !model_known?(model, provider)
59
+ # Unknown id for this provider: fall back to a safe default.
60
+ model = default_model_for(provider)
61
+ end
62
+
63
+ [provider, model]
64
+ end
65
+
66
+ def provider_for_model(model_id)
67
+ return nil if model_id.to_s.empty?
68
+
69
+ info = RubyLLM.models.find(model_id)
70
+ normalize_provider(info.provider)
71
+ rescue StandardError
72
+ # Try each known provider explicitly (aliases can be provider-specific).
73
+ PROVIDERS.each do |provider|
74
+ return provider if model_known?(model_id, provider)
75
+ end
76
+ nil
77
+ end
78
+
79
+ def list_chat_models(provider)
80
+ provider = normalize_provider(provider)
81
+
82
+ RubyLLM.models.chat_models
83
+ .by_provider(provider.to_sym)
84
+ .select { |m| text_chat_model?(m) }
85
+ .sort_by { |m| [m.name.to_s.downcase, m.id] }
86
+ .map { |m| { 'id' => m.id, 'name' => m.name.to_s } }
87
+ end
88
+
89
+ def default_model_for(provider)
90
+ DEFAULT_MODELS.fetch(normalize_provider(provider), DEFAULT_MODELS['openai'])
91
+ end
92
+
93
+ def normalize_provider(provider)
94
+ value = provider.to_s.downcase
95
+ PROVIDERS.include?(value) ? value : 'openai'
96
+ end
97
+
98
+ def ensure_credentials!(config, provider = nil)
99
+ provider = normalize_provider(provider || config['PROVIDER'])
100
+
101
+ case provider
102
+ when 'anthropic'
103
+ if config['ANTHROPIC_KEY'].to_s.empty?
104
+ raise KnownError,
105
+ "Please set your Anthropic API key via `#{Constants::COMMAND_NAME} config set ANTHROPIC_KEY=<your token>`"
106
+ end
107
+ else
108
+ if config['OPENAI_KEY'].to_s.empty?
109
+ raise KnownError,
110
+ "Please set your OpenAI API key via `#{Constants::COMMAND_NAME} config set OPENAI_KEY=<your token>`"
111
+ end
112
+ end
113
+ end
114
+
115
+ def model_known?(model, provider)
116
+ RubyLLM.models.find(model, provider.to_sym)
117
+ true
118
+ rescue StandardError
119
+ false
120
+ end
121
+
122
+ def text_chat_model?(model)
123
+ outputs = Array(model.modalities&.output).map(&:to_s)
124
+ return false unless outputs.include?('text')
125
+
126
+ !model.id.match?(/tts|transcribe|realtime|whisper|embedding|moderation|dall-e|imagen|search-preview|babbage|davinci|ada-|curie|instruct/i)
127
+ end
128
+ end
129
+ end
130
+ end
data/lib/aicli/prompt.rb CHANGED
@@ -14,14 +14,12 @@ module AiCli
14
14
  init_i18n
15
15
 
16
16
  config = Helpers::Config.get
17
- key = config['OPENAI_KEY']
18
- model = config['MODEL']
19
- api_endpoint = config['OPENAI_API_ENDPOINT']
20
17
  skip_explanation = silent_mode || config['SILENT_MODE']
21
18
 
22
19
  pastel = Pastel.new
23
20
  puts ''
24
- puts "┌ #{pastel.cyan(Helpers::Constants::PROJECT_NAME)}"
21
+ puts pastel.cyan(Helpers::Constants::PROJECT_NAME)
22
+ puts pastel.dim("#{config['PROVIDER']} / #{config['MODEL']}")
25
23
 
26
24
  the_prompt = use_prompt.nil? || use_prompt.strip.empty? ? ask_prompt : use_prompt
27
25
 
@@ -30,9 +28,7 @@ module AiCli
30
28
 
31
29
  result = Helpers::Completion.get_script_and_info(
32
30
  prompt: the_prompt,
33
- key: key,
34
- model: model,
35
- api_endpoint: api_endpoint
31
+ config: config
36
32
  )
37
33
 
38
34
  spinner.success(Helpers::I18n.t('Your script') + ':')
@@ -40,8 +36,8 @@ module AiCli
40
36
  script = result[:read_script].call(->(chunk) { print chunk })
41
37
  puts ''
42
38
  puts ''
43
- puts pastel.dim('•')
44
39
 
40
+ explanation_text = ''
45
41
  unless skip_explanation
46
42
  spinner = TTY::Spinner.new("[:spinner] #{Helpers::I18n.t('Getting explanation...')}", format: :dots)
47
43
  spinner.auto_spin
@@ -50,22 +46,21 @@ module AiCli
50
46
  if info.nil? || info.empty?
51
47
  explanation = Helpers::Completion.get_explanation(
52
48
  script: script,
53
- key: key,
54
- model: model,
55
- api_endpoint: api_endpoint
49
+ config: config
56
50
  )
57
51
  spinner.success(Helpers::I18n.t('Explanation') + ':')
58
52
  puts ''
59
- explanation[:read_explanation].call(->(chunk) { print chunk })
53
+ explanation_text = explanation[:read_explanation].call(->(chunk) { print chunk })
60
54
  puts ''
61
55
  puts ''
62
- puts pastel.dim('•')
63
56
  else
57
+ explanation_text = info
64
58
  spinner.success(Helpers::I18n.t('Explanation') + ':')
65
59
  end
66
60
  end
67
61
 
68
- run_or_revise_flow(script, key, model, api_endpoint, silent_mode)
62
+ append_prompt_context(the_prompt, script, explanation_text)
63
+ run_or_revise_flow(script, config, silent_mode)
69
64
  end
70
65
 
71
66
  def init_i18n
@@ -85,14 +80,32 @@ module AiCli
85
80
  end
86
81
 
87
82
  def ask_prompt(initial = nil)
88
- prompt = TTY::Prompt.new(interrupt: :exit)
89
- prompt.ask(Helpers::I18n.t('What would you like me to do?')) do |q|
90
- q.required true
91
- q.default initial || Helpers::I18n.t('Say hello')
92
- q.validate(/.+/, Helpers::I18n.t('Please enter a prompt.'))
83
+ prompt = TTY::Prompt.new(interrupt: :error)
84
+ Helpers::Context.seed_prompt_history(prompt)
85
+
86
+ loop do
87
+ begin
88
+ return prompt.ask(Helpers::I18n.t('What would you like me to do?')) do |q|
89
+ q.required true
90
+ q.default initial || Helpers::I18n.t('Say hello')
91
+ q.validate(/.+/, Helpers::I18n.t('Please enter a prompt.'))
92
+ end
93
+ rescue TTY::Reader::InputInterrupt
94
+ print "\r\e[2K"
95
+ end
93
96
  end
94
97
  end
95
98
 
99
+ def append_prompt_context(user_prompt, script, explanation)
100
+ assistant = +"```\n#{script}\n```"
101
+ assistant << "\n\n#{explanation}" unless explanation.to_s.strip.empty?
102
+
103
+ messages = Helpers::Context.load_messages
104
+ messages << { 'role' => 'user', 'content' => user_prompt.to_s }
105
+ messages << { 'role' => 'assistant', 'content' => assistant }
106
+ Helpers::Context.save_messages(messages)
107
+ end
108
+
96
109
  def ask_revision
97
110
  prompt = TTY::Prompt.new(interrupt: :exit)
98
111
  prompt.ask(Helpers::I18n.t('What would you like me to change in this script?')) do |q|
@@ -102,14 +115,13 @@ module AiCli
102
115
  end
103
116
 
104
117
  def run_script(script)
105
- pastel = Pastel.new
106
- puts "└ #{Helpers::I18n.t('Running')}: #{script}"
118
+ puts "#{Helpers::I18n.t('Running')}: #{script}"
107
119
  puts ''
108
120
  system(ENV['SHELL'] || 'bash', '-c', script)
109
121
  Helpers::ShellHistory.append(script)
110
122
  end
111
123
 
112
- def run_or_revise_flow(script, key, model, api_endpoint, silent_mode)
124
+ def run_or_revise_flow(script, config, silent_mode)
113
125
  prompt = TTY::Prompt.new(interrupt: :exit)
114
126
  empty_script = script.strip.empty?
115
127
 
@@ -132,18 +144,17 @@ module AiCli
132
144
  new_script = prompt.ask(Helpers::I18n.t('you can edit script here'), default: script)
133
145
  run_script(new_script) if new_script && !new_script.empty?
134
146
  when :revise
135
- revision_flow(script, key, model, api_endpoint, silent_mode)
147
+ revision_flow(script, config, silent_mode)
136
148
  when :copy
137
149
  Clipboard.copy(script)
138
- puts "└ #{Helpers::I18n.t('Copied to clipboard!')}"
150
+ puts Helpers::I18n.t('Copied to clipboard!')
139
151
  when :cancel
140
- puts "└ #{Helpers::I18n.t('Goodbye!')}"
152
+ puts Helpers::I18n.t('Goodbye!')
141
153
  exit 0
142
154
  end
143
155
  end
144
156
 
145
- def revision_flow(current_script, key, model, api_endpoint, silent_mode)
146
- pastel = Pastel.new
157
+ def revision_flow(current_script, config, silent_mode)
147
158
  revision = ask_revision
148
159
 
149
160
  spinner = TTY::Spinner.new("[:spinner] #{Helpers::I18n.t('Loading...')}", format: :dots)
@@ -152,9 +163,7 @@ module AiCli
152
163
  result = Helpers::Completion.get_revision(
153
164
  prompt: revision,
154
165
  code: current_script,
155
- key: key,
156
- model: model,
157
- api_endpoint: api_endpoint
166
+ config: config
158
167
  )
159
168
 
160
169
  spinner.success(Helpers::I18n.t('Your new script') + ':')
@@ -162,7 +171,6 @@ module AiCli
162
171
  script = result[:read_script].call(->(chunk) { print chunk })
163
172
  puts ''
164
173
  puts ''
165
- puts pastel.dim('•')
166
174
 
167
175
  unless silent_mode
168
176
  info_spinner = TTY::Spinner.new("[:spinner] #{Helpers::I18n.t('Getting explanation...')}", format: :dots)
@@ -170,9 +178,7 @@ module AiCli
170
178
 
171
179
  explanation = Helpers::Completion.get_explanation(
172
180
  script: script,
173
- key: key,
174
- model: model,
175
- api_endpoint: api_endpoint
181
+ config: config
176
182
  )
177
183
 
178
184
  info_spinner.success(Helpers::I18n.t('Explanation') + ':')
@@ -180,13 +186,13 @@ module AiCli
180
186
  explanation[:read_explanation].call(->(chunk) { print chunk })
181
187
  puts ''
182
188
  puts ''
183
- puts pastel.dim('•')
184
189
  end
185
190
 
186
- run_or_revise_flow(script, key, model, api_endpoint, silent_mode)
191
+ run_or_revise_flow(script, config, silent_mode)
187
192
  end
188
193
 
189
194
  private_class_method :init_i18n, :examples, :ask_prompt, :ask_revision,
190
- :run_script, :run_or_revise_flow, :revision_flow
195
+ :run_script, :run_or_revise_flow, :revision_flow,
196
+ :append_prompt_context
191
197
  end
192
198
  end
data/lib/aicli/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AiCli
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.6'
5
5
  end
data/lib/aicli.rb CHANGED
@@ -7,8 +7,10 @@ require_relative 'aicli/helpers/i18n'
7
7
  require_relative 'aicli/helpers/os_detect'
8
8
  require_relative 'aicli/helpers/strip_regex_patterns'
9
9
  require_relative 'aicli/helpers/shell_history'
10
+ require_relative 'aicli/helpers/llm'
10
11
  require_relative 'aicli/helpers/completion'
11
12
  require_relative 'aicli/helpers/config'
13
+ require_relative 'aicli/helpers/context'
12
14
  require_relative 'aicli/prompt'
13
15
  require_relative 'aicli/commands/chat'
14
16
  require_relative 'aicli/commands/config'
data/locales/en.yml CHANGED
@@ -8,14 +8,20 @@ Please set your OpenAI API key via `ai config set OPENAI_KEY=<your token>`: Plea
8
8
  set your OpenAI API key via `ai config set OPENAI_KEY=<your token>`
9
9
  Set config: Set config
10
10
  Enter your OpenAI API key: Enter your OpenAI API key
11
+ Enter your Anthropic API key: Enter your Anthropic API key
11
12
  "(not set)": "(not set)"
13
+ Provider: Provider
14
+ Pick a provider: Pick a provider
12
15
  OpenAI Key: OpenAI Key
16
+ Anthropic Key: Anthropic Key
13
17
  Please enter a key: Please enter a key
14
18
  OpenAI API Endpoint: OpenAI API Endpoint
15
19
  Enter your OpenAI API Endpoint: Enter your OpenAI API Endpoint
16
20
  Silent Mode: Silent Mode
17
21
  Enable silent mode?: Enable silent mode?
18
22
  Model: Model
23
+ Pick a model.: Pick a model.
24
+ No models found for this provider.: No models found for this provider.
19
25
  Enter the model you want to use: Enter the model you want to use
20
26
  Language: Language
21
27
  Enter the language you want to use: Enter the language you want to use
@@ -35,6 +41,7 @@ Loading...: Loading...
35
41
  Getting explanation...: Getting explanation...
36
42
  Explanation: Explanation
37
43
  Run this script?: Run this script?
44
+ Run it?: Run it?
38
45
  Revise this script?: Revise this script?
39
46
  'Yes': 'Yes'
40
47
  Lets go!: Lets go!
@@ -59,3 +66,6 @@ Please open a Bug report with the information above: Please open a Bug report wi
59
66
  the information above
60
67
  Prompt to run: Prompt to run
61
68
  You: You
69
+ Ask for a shell command. Type exit to quit.: Ask for a shell command. Type exit to quit.
70
+ Restored context: Restored context
71
+ messages: messages
data/locales/it.yml CHANGED
@@ -8,14 +8,20 @@ Please set your OpenAI API key via `ai config set OPENAI_KEY=<your token>`: Per
8
8
  imposta la tua chiave API OpenAI tramite `ai config set OPENAI_KEY=<your token>`
9
9
  Set config: Imposta la configurazione
10
10
  Enter your OpenAI API key: Inserisci la tua chiave API OpenAI
11
+ Enter your Anthropic API key: Inserisci la tua chiave API Anthropic
11
12
  "(not set)": "(non impostato)"
13
+ Provider: Provider
14
+ Pick a provider: Scegli un provider
12
15
  OpenAI Key: Chiave OpenAI
16
+ Anthropic Key: Chiave Anthropic
13
17
  Please enter a key: Per favore inserisci una chiave
14
18
  OpenAI API Endpoint: Endpoint API OpenAI
15
19
  Enter your OpenAI API Endpoint: Inserisci il tuo endpoint API OpenAI
16
20
  Silent Mode: Modalità silenziosa
17
21
  Enable silent mode?: Abilitare la modalità silenziosa?
18
22
  Model: Modello
23
+ Pick a model.: Scegli un modello.
24
+ No models found for this provider.: Nessun modello trovato per questo provider.
19
25
  Enter the model you want to use: Inserisci il modello che vuoi utilizzare
20
26
  Language: Lingua
21
27
  Enter the language you want to use: Inserisci la lingua che vuoi utilizzare
@@ -35,6 +41,7 @@ Loading...: Caricamento...
35
41
  Getting explanation...: Ottieni spiegazione...
36
42
  Explanation: Spiegazione
37
43
  Run this script?: Esegui questo script?
44
+ Run it?: Lo eseguo?
38
45
  Revise this script?: Rivedi questo script?
39
46
  'Yes': Si
40
47
  Lets go!: Vai!
@@ -58,3 +65,6 @@ Please open a Bug report with the information above: Apri un report di bug con l
58
65
  informazioni sopra
59
66
  Prompt to run: Prompt da eseguire
60
67
  You: Tu
68
+ Ask for a shell command. Type exit to quit.: Chiedi un comando shell. Digita exit per uscire.
69
+ Restored context: Contesto ripristinato
70
+ messages: messaggi
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aicli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antonio Molinari
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0.8'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ruby_llm
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.16'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.16'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: thor
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -102,8 +116,10 @@ files:
102
116
  - lib/aicli/helpers/completion.rb
103
117
  - lib/aicli/helpers/config.rb
104
118
  - lib/aicli/helpers/constants.rb
119
+ - lib/aicli/helpers/context.rb
105
120
  - lib/aicli/helpers/error.rb
106
121
  - lib/aicli/helpers/i18n.rb
122
+ - lib/aicli/helpers/llm.rb
107
123
  - lib/aicli/helpers/os_detect.rb
108
124
  - lib/aicli/helpers/shell_history.rb
109
125
  - lib/aicli/helpers/strip_regex_patterns.rb