aicli 0.1.1 → 0.2.2
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 +22 -0
- data/README.md +46 -12
- data/lib/aicli/cli.rb +2 -2
- data/lib/aicli/commands/chat.rb +19 -18
- data/lib/aicli/commands/update.rb +65 -3
- data/lib/aicli/helpers/completion.rb +111 -189
- data/lib/aicli/helpers/config.rb +69 -12
- data/lib/aicli/helpers/llm.rb +130 -0
- data/lib/aicli/prompt.rb +10 -21
- data/lib/aicli/version.rb +1 -1
- data/lib/aicli.rb +1 -0
- data/locales/en.yml +6 -0
- data/locales/it.yml +6 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49c27ca0316a3ca251344ea8a63589d603d67a6a89e77287b6615f9314650ed1
|
|
4
|
+
data.tar.gz: c44be18b27d259f0fd8f93f2133246775d042ab09576be64dc8138353d53d095
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: becb34f20ed09c6d7de4f68c72b81aa6ae83cb72bf158e401c35bdabc291a2339d2fa16d845abf6934c826d1be0cbb50f48f45e9bec08a7c554a3e79bf8ba5c3
|
|
7
|
+
data.tar.gz: 9dc02259b8ea17da0d3a73aaade312cc88a0c234b9c6418014ac840025f8b440c9c735ac605165b898b00fe1e53e37f14365c60943652b6561e8ffa0b603f0ad
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## 0.2.2
|
|
2
|
+
|
|
3
|
+
- Improve `ai update`: compare installed version with RubyGems, install latest explicitly, clearer status messages
|
|
4
|
+
|
|
5
|
+
## 0.2.1
|
|
6
|
+
|
|
7
|
+
- Fix provider/model sync so Anthropic models are not sent to OpenAI
|
|
8
|
+
- Show active `provider / model` on each run
|
|
9
|
+
- Clearer error when a model is unavailable for the API key
|
|
10
|
+
- `config set MODEL=...` infers PROVIDER from the RubyLLM registry
|
|
11
|
+
|
|
12
|
+
## 0.2.0
|
|
13
|
+
|
|
14
|
+
- Add Anthropic provider alongside OpenAI
|
|
15
|
+
- Route all LLM calls through RubyLLM
|
|
16
|
+
- Choose provider and model in `aicli config` (model list from RubyLLM registry)
|
|
17
|
+
- Config keys: `PROVIDER`, `ANTHROPIC_KEY` (plus existing `OPENAI_KEY` / `MODEL`)
|
|
18
|
+
|
|
19
|
+
## 0.1.0
|
|
20
|
+
|
|
21
|
+
- Initial RubyGems release as `aicli`
|
|
22
|
+
|
|
1
23
|
## 1.0.12
|
|
2
24
|
|
|
3
25
|
- Bug fixes
|
data/README.md
CHANGED
|
@@ -38,16 +38,25 @@ Ruby port by [Antonio Molinari](https://github.com/magnum), inspired by the orig
|
|
|
38
38
|
gem build aicli.gemspec && gem install ./aicli-*.gem
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
2.
|
|
41
|
+
2. Choose a provider and set the matching API key.
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
3. Set the key so aicli can use it:
|
|
43
|
+
**OpenAI** (default) — key from [OpenAI](https://platform.openai.com/account/api-keys):
|
|
46
44
|
|
|
47
45
|
```sh
|
|
46
|
+
aicli config set PROVIDER=openai
|
|
48
47
|
aicli config set OPENAI_KEY=<your token>
|
|
49
48
|
```
|
|
50
49
|
|
|
50
|
+
**Anthropic** — key from [Anthropic](https://console.anthropic.com/settings/keys):
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
aicli config set PROVIDER=anthropic
|
|
54
|
+
aicli config set ANTHROPIC_KEY=<your token>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or use the interactive UI (`aicli config`) to pick provider, key, and model.
|
|
58
|
+
Model lists come from the [RubyLLM](https://rubyllm.com/models/) registry.
|
|
59
|
+
|
|
51
60
|
This will create a `.aicli` file in your home directory.
|
|
52
61
|
|
|
53
62
|
## Usage
|
|
@@ -94,7 +103,20 @@ Or save the preference:
|
|
|
94
103
|
aicli config set SILENT_MODE=true
|
|
95
104
|
```
|
|
96
105
|
|
|
97
|
-
###
|
|
106
|
+
### Provider and model
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
aicli config set PROVIDER=openai # or anthropic
|
|
110
|
+
aicli config set MODEL=gpt-4o-mini # provider-specific model id
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Defaults: `gpt-4o-mini` (OpenAI), `claude-sonnet-4-6` (Anthropic).
|
|
114
|
+
|
|
115
|
+
LLM calls go through [RubyLLM](https://rubyllm.com/), so chat streaming and model metadata stay provider-agnostic.
|
|
116
|
+
|
|
117
|
+
### Custom OpenAI API endpoint
|
|
118
|
+
|
|
119
|
+
Useful for OpenAI-compatible proxies (ignored for Anthropic):
|
|
98
120
|
|
|
99
121
|
```sh
|
|
100
122
|
aicli config set OPENAI_API_ENDPOINT=<your proxy endpoint>
|
|
@@ -148,20 +170,32 @@ aicli update
|
|
|
148
170
|
|
|
149
171
|
## Development
|
|
150
172
|
|
|
173
|
+
Run the local checkout (not the installed gem) with Bundler from this directory:
|
|
174
|
+
|
|
151
175
|
```sh
|
|
152
176
|
bundle install
|
|
153
|
-
|
|
154
|
-
bundle exec bin/
|
|
155
|
-
bundle exec bin/
|
|
177
|
+
|
|
178
|
+
bundle exec bin/ai --help
|
|
179
|
+
bundle exec bin/ai config
|
|
180
|
+
bundle exec bin/ai chat
|
|
181
|
+
bundle exec bin/ai list all log files
|
|
156
182
|
```
|
|
157
183
|
|
|
158
|
-
|
|
184
|
+
`bin/aicli` is equivalent to `bin/ai`. `bundle exec` loads dependencies from the Gemfile and code from `lib/`.
|
|
159
185
|
|
|
160
|
-
|
|
186
|
+
Example config for local testing:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
bundle exec bin/ai config set PROVIDER=openai
|
|
190
|
+
bundle exec bin/ai config set OPENAI_KEY=<your token>
|
|
191
|
+
# or: PROVIDER=anthropic + ANTHROPIC_KEY=<your token>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Common Issues
|
|
161
195
|
|
|
162
|
-
|
|
196
|
+
### Rate limit / quota errors
|
|
163
197
|
|
|
164
|
-
|
|
198
|
+
Usually billing or quota on the configured provider (OpenAI or Anthropic). Check that provider’s console billing page and that the matching API key is set in `~/.aicli`.
|
|
165
199
|
|
|
166
200
|
## Motivation
|
|
167
201
|
|
data/lib/aicli/cli.rb
CHANGED
data/lib/aicli/commands/chat.rb
CHANGED
|
@@ -11,16 +11,14 @@ module AiCli
|
|
|
11
11
|
|
|
12
12
|
def run
|
|
13
13
|
config = Helpers::Config.get
|
|
14
|
-
|
|
15
|
-
model = config['MODEL']
|
|
16
|
-
api_endpoint = config['OPENAI_API_ENDPOINT']
|
|
17
|
-
chat_history = []
|
|
14
|
+
chat = Helpers::Completion.start_chat(config)
|
|
18
15
|
|
|
19
16
|
pastel = Pastel.new
|
|
20
17
|
prompt = TTY::Prompt.new(interrupt: :exit)
|
|
21
18
|
|
|
22
19
|
puts ''
|
|
23
20
|
puts "┌ #{Helpers::I18n.t('Starting new conversation')}"
|
|
21
|
+
puts pastel.dim(" #{config['PROVIDER']} / #{config['MODEL']}")
|
|
24
22
|
|
|
25
23
|
loop do
|
|
26
24
|
user_prompt = prompt.ask(pastel.cyan("#{Helpers::I18n.t('You')}:")) do |q|
|
|
@@ -35,22 +33,25 @@ module AiCli
|
|
|
35
33
|
|
|
36
34
|
spinner = TTY::Spinner.new("[:spinner] #{Helpers::I18n.t('THINKING...')}", format: :dots)
|
|
37
35
|
spinner.auto_spin
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
started = false
|
|
37
|
+
|
|
38
|
+
Helpers::Completion.stream_chat_message(
|
|
39
|
+
chat,
|
|
40
|
+
user_prompt,
|
|
41
|
+
writer: lambda { |chunk|
|
|
42
|
+
unless started
|
|
43
|
+
spinner.success(pastel.green('aicli:'))
|
|
44
|
+
puts ''
|
|
45
|
+
started = true
|
|
46
|
+
end
|
|
47
|
+
print chunk
|
|
48
|
+
}
|
|
46
49
|
)
|
|
47
|
-
enumerator = stream_lines.each
|
|
48
|
-
read_response = Helpers::Completion.read_data(enumerator)
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
unless started
|
|
52
|
+
spinner.success(pastel.green('aicli:'))
|
|
53
|
+
puts ''
|
|
54
|
+
end
|
|
54
55
|
puts ''
|
|
55
56
|
puts ''
|
|
56
57
|
end
|
|
@@ -1,21 +1,83 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
3
6
|
require 'pastel'
|
|
4
7
|
|
|
5
8
|
module AiCli
|
|
6
9
|
module Commands
|
|
7
10
|
module Update
|
|
11
|
+
RUBYGEMS_GEM_URI = 'https://rubygems.org/api/v1/gems/aicli.json'
|
|
12
|
+
|
|
8
13
|
module_function
|
|
9
14
|
|
|
10
15
|
def run
|
|
11
16
|
pastel = Pastel.new
|
|
17
|
+
current = Gem::Version.new(AiCli::VERSION)
|
|
18
|
+
|
|
19
|
+
puts ''
|
|
20
|
+
puts pastel.dim("Current version: #{current}")
|
|
21
|
+
|
|
22
|
+
latest_str = fetch_latest_version
|
|
23
|
+
latest = Gem::Version.new(latest_str)
|
|
24
|
+
puts pastel.dim("Latest on RubyGems: #{latest}")
|
|
12
25
|
puts ''
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
|
|
27
|
+
if latest < current
|
|
28
|
+
puts pastel.yellow(
|
|
29
|
+
"Installed #{current} is newer than RubyGems #{latest}. Skipping update."
|
|
30
|
+
)
|
|
31
|
+
puts pastel.dim('Publish a new version, then run `ai update` again.')
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if latest == current
|
|
36
|
+
puts pastel.green("Already up to date (#{current}).")
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
command = ['gem', 'install', 'aicli', '-v', latest_str]
|
|
41
|
+
puts pastel.dim("#{Helpers::I18n.t('Running')}: #{command.join(' ')}")
|
|
15
42
|
puts ''
|
|
16
|
-
|
|
43
|
+
|
|
44
|
+
ok = system(*command)
|
|
17
45
|
puts ''
|
|
46
|
+
|
|
47
|
+
unless ok
|
|
48
|
+
raise Helpers::KnownError,
|
|
49
|
+
'Failed to update aicli. Try manually: gem install aicli'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
puts pastel.green("Updated aicli to #{latest}.")
|
|
53
|
+
puts pastel.dim('Open a new shell if `ai version` still shows the old number.')
|
|
54
|
+
rescue Helpers::KnownError => e
|
|
55
|
+
puts "\n#{pastel.red('✖')} #{e.message}"
|
|
56
|
+
exit 1
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
puts "\n#{pastel.red('✖')} Could not check RubyGems for updates: #{e.message}"
|
|
59
|
+
Helpers::Error.handle_cli_error(e)
|
|
60
|
+
exit 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fetch_latest_version
|
|
64
|
+
uri = URI(RUBYGEMS_GEM_URI)
|
|
65
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
66
|
+
http.open_timeout = 10
|
|
67
|
+
http.read_timeout = 10
|
|
68
|
+
http.get(uri.request_uri)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
72
|
+
raise "RubyGems returned HTTP #{response.code}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
version = JSON.parse(response.body)['version'].to_s
|
|
76
|
+
raise 'RubyGems response missing version' if version.empty?
|
|
77
|
+
|
|
78
|
+
version
|
|
18
79
|
end
|
|
80
|
+
private_class_method :fetch_latest_version
|
|
19
81
|
end
|
|
20
82
|
end
|
|
21
83
|
end
|
|
@@ -1,219 +1,66 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'net/http'
|
|
5
|
-
require 'uri'
|
|
6
|
-
require 'thread'
|
|
7
|
-
|
|
8
3
|
module AiCli
|
|
9
4
|
module Helpers
|
|
10
5
|
module Completion
|
|
11
6
|
EXPLAIN_IN_SECOND_REQUEST = true
|
|
12
|
-
SHELL_CODE_EXCLUSIONS = [/```[a-zA-Z]*\n
|
|
7
|
+
SHELL_CODE_EXCLUSIONS = [/```[a-zA-Z]*\n?/i, /```[a-zA-Z]*/i, "\n"].freeze
|
|
13
8
|
|
|
14
9
|
module_function
|
|
15
10
|
|
|
16
|
-
def get_script_and_info(prompt:,
|
|
17
|
-
stream = generate_completion(
|
|
18
|
-
prompt: full_prompt(prompt),
|
|
19
|
-
key: key,
|
|
20
|
-
model: model,
|
|
21
|
-
api_endpoint: api_endpoint
|
|
22
|
-
)
|
|
23
|
-
enumerator = stream.each
|
|
24
|
-
|
|
11
|
+
def get_script_and_info(prompt:, config:)
|
|
25
12
|
{
|
|
26
|
-
read_script:
|
|
27
|
-
read_info:
|
|
13
|
+
read_script: stream_prompt(full_prompt(prompt), config: config, exclusions: SHELL_CODE_EXCLUSIONS),
|
|
14
|
+
read_info: ->(_writer) { '' }
|
|
28
15
|
}
|
|
29
16
|
end
|
|
30
17
|
|
|
31
|
-
def get_explanation(script:,
|
|
32
|
-
|
|
33
|
-
prompt: explanation_prompt(script),
|
|
34
|
-
key: key,
|
|
35
|
-
model: model,
|
|
36
|
-
api_endpoint: api_endpoint
|
|
37
|
-
)
|
|
38
|
-
{ read_explanation: read_data(stream.each) }
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def get_revision(prompt:, code:, key:, api_endpoint:, model: nil)
|
|
42
|
-
stream = generate_completion(
|
|
43
|
-
prompt: revision_prompt(prompt, code),
|
|
44
|
-
key: key,
|
|
45
|
-
model: model,
|
|
46
|
-
api_endpoint: api_endpoint
|
|
47
|
-
)
|
|
48
|
-
{ read_script: read_data(stream.each, *SHELL_CODE_EXCLUSIONS) }
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def get_models(key, api_endpoint)
|
|
52
|
-
uri = URI.join(ensure_trailing_slash(api_endpoint), 'models')
|
|
53
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
54
|
-
http.use_ssl = uri.scheme == 'https'
|
|
55
|
-
|
|
56
|
-
request = Net::HTTP::Get.new(uri)
|
|
57
|
-
request['Authorization'] = "Bearer #{key}"
|
|
58
|
-
request['Content-Type'] = 'application/json'
|
|
59
|
-
|
|
60
|
-
response = http.request(request)
|
|
61
|
-
raise KnownError, "Failed to list models: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
62
|
-
|
|
63
|
-
data = JSON.parse(response.body)
|
|
64
|
-
data.fetch('data', []).select { |m| m['object'] == 'model' }
|
|
18
|
+
def get_explanation(script:, config:)
|
|
19
|
+
{ read_explanation: stream_prompt(explanation_prompt(script), config: config) }
|
|
65
20
|
end
|
|
66
21
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
uri = URI.join(ensure_trailing_slash(api_endpoint), 'chat/completions')
|
|
76
|
-
body = {
|
|
77
|
-
model: model || 'gpt-4o-mini',
|
|
78
|
-
messages: messages,
|
|
79
|
-
n: [number, 10].min,
|
|
80
|
-
stream: true
|
|
22
|
+
def get_revision(prompt:, code:, config:)
|
|
23
|
+
{
|
|
24
|
+
read_script: stream_prompt(
|
|
25
|
+
revision_prompt(prompt, code),
|
|
26
|
+
config: config,
|
|
27
|
+
exclusions: SHELL_CODE_EXCLUSIONS
|
|
28
|
+
)
|
|
81
29
|
}
|
|
30
|
+
end
|
|
82
31
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Thread.new do
|
|
87
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
88
|
-
http.use_ssl = uri.scheme == 'https'
|
|
89
|
-
http.read_timeout = 120
|
|
90
|
-
|
|
91
|
-
request = Net::HTTP::Post.new(uri)
|
|
92
|
-
request['Authorization'] = "Bearer #{key}"
|
|
93
|
-
request['Content-Type'] = 'application/json'
|
|
94
|
-
request['Accept'] = 'text/event-stream'
|
|
95
|
-
request.body = JSON.generate(body)
|
|
96
|
-
|
|
97
|
-
http.request(request) do |response|
|
|
98
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
99
|
-
error_box << [:api, response.code.to_i, response.body]
|
|
100
|
-
queue << :done
|
|
101
|
-
next
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
buffer = +''
|
|
105
|
-
response.read_body do |chunk|
|
|
106
|
-
buffer << chunk
|
|
107
|
-
while (eol = buffer.index("\n"))
|
|
108
|
-
line = buffer.slice!(0..eol).strip
|
|
109
|
-
queue << line if line.start_with?('data: ')
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
queue << :done
|
|
114
|
-
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
|
115
|
-
error_box << [:network, uri.host, e.message]
|
|
116
|
-
queue << :done
|
|
117
|
-
rescue StandardError => e
|
|
118
|
-
error_box << [:other, e]
|
|
119
|
-
queue << :done
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
Enumerator.new do |yielder|
|
|
123
|
-
loop do
|
|
124
|
-
item = queue.pop
|
|
125
|
-
break if item == :done
|
|
126
|
-
|
|
127
|
-
yielder << item
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
if (err = error_box.first)
|
|
131
|
-
case err[0]
|
|
132
|
-
when :api
|
|
133
|
-
handle_api_error(err[1], err[2])
|
|
134
|
-
when :network
|
|
135
|
-
raise KnownError,
|
|
136
|
-
"Error connecting to #{err[1]}. Are you connected to the internet? (#{err[2]})"
|
|
137
|
-
when :other
|
|
138
|
-
raise err[1]
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|
|
32
|
+
def get_models(provider)
|
|
33
|
+
Llm.list_chat_models(provider)
|
|
142
34
|
end
|
|
143
35
|
|
|
144
|
-
|
|
36
|
+
# Streams a single-turn completion. Returns a callable that takes a writer.
|
|
37
|
+
def stream_prompt(prompt, config:, exclusions: [])
|
|
145
38
|
lambda do |writer|
|
|
146
|
-
|
|
147
|
-
data_start = false
|
|
148
|
-
buffer = +''
|
|
149
|
-
excluded_prefix = excluded.first
|
|
150
|
-
|
|
151
|
-
loop do
|
|
152
|
-
chunk = enumerator.next
|
|
153
|
-
payloads = chunk.to_s.split("\n\n")
|
|
154
|
-
|
|
155
|
-
payloads.each do |payload|
|
|
156
|
-
return data if payload.include?('[DONE]')
|
|
157
|
-
|
|
158
|
-
next unless payload.start_with?('data:')
|
|
159
|
-
|
|
160
|
-
content = parse_content(payload)
|
|
161
|
-
|
|
162
|
-
unless data_start
|
|
163
|
-
buffer << content
|
|
164
|
-
if excluded_prefix.nil? || buffer.match?(excluded_prefix)
|
|
165
|
-
data_start = true
|
|
166
|
-
buffer = +''
|
|
167
|
-
break if excluded_prefix
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
next unless data_start && !content.empty?
|
|
172
|
-
|
|
173
|
-
cleaned = StripRegexPatterns.call(content, excluded)
|
|
174
|
-
data << cleaned
|
|
175
|
-
writer.call(cleaned)
|
|
176
|
-
end
|
|
177
|
-
rescue StopIteration
|
|
178
|
-
break
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
data
|
|
39
|
+
stream_to_writer(prompt, config: config, exclusions: exclusions, writer: writer)
|
|
182
40
|
end
|
|
183
41
|
end
|
|
184
42
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
delta.dig('choices', 0, 'delta', 'content') || ''
|
|
189
|
-
rescue JSON::ParserError => e
|
|
190
|
-
"Error with JSON.parse and #{payload}.\n#{e}"
|
|
43
|
+
# Multi-turn chat helper used by `aicli chat`.
|
|
44
|
+
def start_chat(config)
|
|
45
|
+
Llm.build_chat(config)
|
|
191
46
|
end
|
|
192
47
|
|
|
193
|
-
def
|
|
194
|
-
|
|
195
|
-
JSON.pretty_generate(JSON.parse(body))
|
|
196
|
-
rescue StandardError
|
|
197
|
-
body.to_s
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
if status == 429
|
|
201
|
-
raise KnownError, <<~MSG
|
|
202
|
-
Request to OpenAI failed with status 429. This is due to incorrect billing setup or excessive quota usage. Please follow this guide to fix it: https://help.openai.com/en/articles/6891831-error-code-429-you-exceeded-your-current-quota-please-check-your-plan-and-billing-details
|
|
48
|
+
def stream_chat_message(chat, message, writer:)
|
|
49
|
+
data = +''
|
|
203
50
|
|
|
204
|
-
|
|
51
|
+
begin
|
|
52
|
+
chat.ask(message) do |chunk|
|
|
53
|
+
content = chunk.content.to_s
|
|
54
|
+
next if content.empty?
|
|
205
55
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
56
|
+
data << content
|
|
57
|
+
writer.call(content)
|
|
58
|
+
end
|
|
59
|
+
rescue RubyLLM::Error, RubyLLM::ConfigurationError => e
|
|
60
|
+
handle_ruby_llm_error(e)
|
|
210
61
|
end
|
|
211
62
|
|
|
212
|
-
|
|
213
|
-
Request to OpenAI failed with status #{status}:
|
|
214
|
-
|
|
215
|
-
#{message_string}
|
|
216
|
-
MSG
|
|
63
|
+
data
|
|
217
64
|
end
|
|
218
65
|
|
|
219
66
|
def explanation_prompt(script)
|
|
@@ -267,9 +114,84 @@ module AiCli
|
|
|
267
114
|
PROMPT
|
|
268
115
|
end
|
|
269
116
|
|
|
270
|
-
def
|
|
271
|
-
|
|
117
|
+
def stream_to_writer(prompt, config:, exclusions:, writer:)
|
|
118
|
+
chat = Llm.build_chat(config)
|
|
119
|
+
data = +''
|
|
120
|
+
data_start = exclusions.empty?
|
|
121
|
+
buffer = +''
|
|
122
|
+
excluded_prefix = exclusions.first
|
|
123
|
+
|
|
124
|
+
begin
|
|
125
|
+
chat.ask(prompt) do |chunk|
|
|
126
|
+
content = chunk.content.to_s
|
|
127
|
+
next if content.empty?
|
|
128
|
+
|
|
129
|
+
unless data_start
|
|
130
|
+
buffer << content
|
|
131
|
+
next unless excluded_prefix.nil? || buffer.match?(excluded_prefix)
|
|
132
|
+
|
|
133
|
+
data_start = true
|
|
134
|
+
remainder = StripRegexPatterns.call(buffer, exclusions)
|
|
135
|
+
buffer = +''
|
|
136
|
+
next if remainder.empty?
|
|
137
|
+
|
|
138
|
+
data << remainder
|
|
139
|
+
writer.call(remainder)
|
|
140
|
+
next
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
cleaned = StripRegexPatterns.call(content, exclusions)
|
|
144
|
+
next if cleaned.empty?
|
|
145
|
+
|
|
146
|
+
data << cleaned
|
|
147
|
+
writer.call(cleaned)
|
|
148
|
+
end
|
|
149
|
+
rescue RubyLLM::Error, RubyLLM::ConfigurationError => e
|
|
150
|
+
handle_ruby_llm_error(e)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
data
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def handle_ruby_llm_error(error)
|
|
157
|
+
message = error.message.to_s
|
|
158
|
+
|
|
159
|
+
if error.is_a?(RubyLLM::RateLimitError) || error.is_a?(RubyLLM::PaymentRequiredError) ||
|
|
160
|
+
message.match?(/rate.?limit|quota|billing/i)
|
|
161
|
+
raise KnownError, <<~MSG
|
|
162
|
+
Request to the LLM provider failed (rate limit / quota).
|
|
163
|
+
|
|
164
|
+
Check your plan and billing details for the configured provider, then try again.
|
|
165
|
+
|
|
166
|
+
Full message:
|
|
167
|
+
|
|
168
|
+
#{message}
|
|
169
|
+
MSG
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if message.match?(/model_not_found|does not exist|do not have access/i)
|
|
173
|
+
raise KnownError, <<~MSG
|
|
174
|
+
The configured model is not available for this API key/provider.
|
|
175
|
+
|
|
176
|
+
Fix with:
|
|
177
|
+
#{Constants::COMMAND_NAME} config
|
|
178
|
+
or:
|
|
179
|
+
#{Constants::COMMAND_NAME} config set MODEL=#{Llm.default_model_for('anthropic')}
|
|
180
|
+
#{Constants::COMMAND_NAME} config set PROVIDER=anthropic
|
|
181
|
+
|
|
182
|
+
Full message:
|
|
183
|
+
|
|
184
|
+
#{message}
|
|
185
|
+
MSG
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
raise KnownError, <<~MSG
|
|
189
|
+
Request to the LLM provider failed:
|
|
190
|
+
|
|
191
|
+
#{message}
|
|
192
|
+
MSG
|
|
272
193
|
end
|
|
194
|
+
private_class_method :stream_to_writer, :handle_ruby_llm_error
|
|
273
195
|
end
|
|
274
196
|
end
|
|
275
197
|
end
|
data/lib/aicli/helpers/config.rb
CHANGED
|
@@ -11,15 +11,17 @@ module AiCli
|
|
|
11
11
|
LEGACY_CONFIG_PATH = File.join(Dir.home, '.ai-shell')
|
|
12
12
|
|
|
13
13
|
CONFIG_PARSERS = {
|
|
14
|
+
'PROVIDER' => lambda { |provider|
|
|
15
|
+
Llm.normalize_provider(provider)
|
|
16
|
+
},
|
|
14
17
|
'OPENAI_KEY' => lambda { |key|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
key
|
|
18
|
+
key.to_s
|
|
19
|
+
},
|
|
20
|
+
'ANTHROPIC_KEY' => lambda { |key|
|
|
21
|
+
key.to_s
|
|
20
22
|
},
|
|
21
23
|
'MODEL' => lambda { |model|
|
|
22
|
-
model.
|
|
24
|
+
model.to_s
|
|
23
25
|
},
|
|
24
26
|
'SILENT_MODE' => lambda { |mode|
|
|
25
27
|
mode.to_s.downcase == 'true'
|
|
@@ -43,21 +45,48 @@ module AiCli
|
|
|
43
45
|
parsed[key] = parser.call(value)
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
raw_model = cli_config&.dig('MODEL') || config['MODEL']
|
|
49
|
+
if raw_model.nil? || raw_model.to_s.empty?
|
|
50
|
+
parsed['MODEL'] = Llm.default_model_for(parsed['PROVIDER'])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
provider, model = Llm.resolve_provider_and_model(parsed)
|
|
54
|
+
parsed['PROVIDER'] = provider
|
|
55
|
+
parsed['MODEL'] = model
|
|
46
56
|
parsed
|
|
47
57
|
end
|
|
48
58
|
|
|
49
59
|
def set(key_values)
|
|
50
60
|
config = read_config_file
|
|
61
|
+
updates = {}
|
|
51
62
|
|
|
52
63
|
key_values.each do |key, value|
|
|
53
64
|
unless CONFIG_PARSERS.key?(key)
|
|
54
65
|
raise KnownError, "#{I18n.t('Invalid config property')}: #{key}"
|
|
55
66
|
end
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
updates[key] = CONFIG_PARSERS[key].call(value).to_s
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Keep PROVIDER and MODEL aligned with the RubyLLM registry.
|
|
72
|
+
if updates.key?('MODEL') && !updates['MODEL'].empty?
|
|
73
|
+
model_provider = Llm.provider_for_model(updates['MODEL'])
|
|
74
|
+
if model_provider
|
|
75
|
+
updates['PROVIDER'] = model_provider unless updates.key?('PROVIDER')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if updates.key?('PROVIDER')
|
|
80
|
+
provider = Llm.normalize_provider(updates['PROVIDER'])
|
|
81
|
+
updates['PROVIDER'] = provider
|
|
82
|
+
next_model = updates['MODEL'] || config['MODEL']
|
|
83
|
+
known_ids = Completion.get_models(provider).map { |m| m['id'] }
|
|
84
|
+
if next_model.to_s.empty? || !known_ids.include?(next_model)
|
|
85
|
+
updates['MODEL'] = Llm.default_model_for(provider) unless updates.key?('MODEL') && known_ids.include?(updates['MODEL'])
|
|
86
|
+
end
|
|
59
87
|
end
|
|
60
88
|
|
|
89
|
+
updates.each { |key, value| config[key] = value }
|
|
61
90
|
File.write(CONFIG_PATH, stringify_ini(config))
|
|
62
91
|
end
|
|
63
92
|
|
|
@@ -72,8 +101,12 @@ module AiCli
|
|
|
72
101
|
loop do
|
|
73
102
|
config = get
|
|
74
103
|
choice = prompt.select("#{I18n.t('Set config')}:") do |menu|
|
|
104
|
+
menu.choice "#{I18n.t('Provider')} (#{display_hint(config, 'PROVIDER')})",
|
|
105
|
+
'PROVIDER'
|
|
75
106
|
menu.choice "#{I18n.t('OpenAI Key')} (#{display_hint(config, 'OPENAI_KEY') { |v| "sk-...#{v[-3..]}" }})",
|
|
76
107
|
'OPENAI_KEY'
|
|
108
|
+
menu.choice "#{I18n.t('Anthropic Key')} (#{display_hint(config, 'ANTHROPIC_KEY') { |v| "...#{v[-3..]}" }})",
|
|
109
|
+
'ANTHROPIC_KEY'
|
|
77
110
|
menu.choice "#{I18n.t('OpenAI API Endpoint')} (#{display_hint(config, 'OPENAI_API_ENDPOINT')})",
|
|
78
111
|
'OPENAI_API_ENDPOINT'
|
|
79
112
|
menu.choice "#{I18n.t('Silent Mode')} (#{display_hint(config, 'SILENT_MODE')})",
|
|
@@ -86,11 +119,27 @@ module AiCli
|
|
|
86
119
|
end
|
|
87
120
|
|
|
88
121
|
case choice
|
|
122
|
+
when 'PROVIDER'
|
|
123
|
+
provider = prompt.select(I18n.t('Pick a provider')) do |menu|
|
|
124
|
+
Llm::PROVIDERS.each { |p| menu.choice p, p }
|
|
125
|
+
end
|
|
126
|
+
updates = [['PROVIDER', provider]]
|
|
127
|
+
current_model = get['MODEL']
|
|
128
|
+
known_ids = Completion.get_models(provider).map { |m| m['id'] }
|
|
129
|
+
unless known_ids.include?(current_model)
|
|
130
|
+
updates << ['MODEL', Llm.default_model_for(provider)]
|
|
131
|
+
end
|
|
132
|
+
set(updates)
|
|
89
133
|
when 'OPENAI_KEY'
|
|
90
134
|
key = prompt.ask(I18n.t('Enter your OpenAI API key')) do |q|
|
|
91
135
|
q.required true
|
|
92
136
|
end
|
|
93
137
|
set([['OPENAI_KEY', key]])
|
|
138
|
+
when 'ANTHROPIC_KEY'
|
|
139
|
+
key = prompt.ask(I18n.t('Enter your Anthropic API key')) do |q|
|
|
140
|
+
q.required true
|
|
141
|
+
end
|
|
142
|
+
set([['ANTHROPIC_KEY', key]])
|
|
94
143
|
when 'OPENAI_API_ENDPOINT'
|
|
95
144
|
api_endpoint = prompt.ask(I18n.t('Enter your OpenAI API Endpoint'))
|
|
96
145
|
set([['OPENAI_API_ENDPOINT', api_endpoint]]) if api_endpoint
|
|
@@ -99,9 +148,17 @@ module AiCli
|
|
|
99
148
|
set([['SILENT_MODE', silent ? 'true' : 'false']])
|
|
100
149
|
when 'MODEL'
|
|
101
150
|
cfg = get
|
|
102
|
-
models = Completion.get_models(cfg['
|
|
103
|
-
|
|
104
|
-
|
|
151
|
+
models = Completion.get_models(cfg['PROVIDER'])
|
|
152
|
+
if models.empty?
|
|
153
|
+
puts pastel.yellow(I18n.t('No models found for this provider.'))
|
|
154
|
+
next
|
|
155
|
+
end
|
|
156
|
+
model = prompt.select(I18n.t('Pick a model.')) do |menu|
|
|
157
|
+
menu.default cfg['MODEL'] if models.any? { |m| m['id'] == cfg['MODEL'] }
|
|
158
|
+
models.each do |m|
|
|
159
|
+
label = m['name'].empty? || m['name'] == m['id'] ? m['id'] : "#{m['name']} (#{m['id']})"
|
|
160
|
+
menu.choice label, m['id']
|
|
161
|
+
end
|
|
105
162
|
end
|
|
106
163
|
set([['MODEL', model]])
|
|
107
164
|
when 'LANGUAGE'
|
|
@@ -149,7 +206,7 @@ module AiCli
|
|
|
149
206
|
end
|
|
150
207
|
|
|
151
208
|
def display_hint(config, key)
|
|
152
|
-
if config.key?(key) && !config[key].nil?
|
|
209
|
+
if config.key?(key) && !config[key].nil? && !(config[key].respond_to?(:empty?) && config[key].empty?)
|
|
153
210
|
value = config[key]
|
|
154
211
|
block_given? ? yield(value) : value.to_s
|
|
155
212
|
else
|
|
@@ -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
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
|
-
|
|
34
|
-
model: model,
|
|
35
|
-
api_endpoint: api_endpoint
|
|
31
|
+
config: config
|
|
36
32
|
)
|
|
37
33
|
|
|
38
34
|
spinner.success(Helpers::I18n.t('Your script') + ':')
|
|
@@ -50,9 +46,7 @@ module AiCli
|
|
|
50
46
|
if info.nil? || info.empty?
|
|
51
47
|
explanation = Helpers::Completion.get_explanation(
|
|
52
48
|
script: script,
|
|
53
|
-
|
|
54
|
-
model: model,
|
|
55
|
-
api_endpoint: api_endpoint
|
|
49
|
+
config: config
|
|
56
50
|
)
|
|
57
51
|
spinner.success(Helpers::I18n.t('Explanation') + ':')
|
|
58
52
|
puts ''
|
|
@@ -65,7 +59,7 @@ module AiCli
|
|
|
65
59
|
end
|
|
66
60
|
end
|
|
67
61
|
|
|
68
|
-
run_or_revise_flow(script,
|
|
62
|
+
run_or_revise_flow(script, config, silent_mode)
|
|
69
63
|
end
|
|
70
64
|
|
|
71
65
|
def init_i18n
|
|
@@ -102,14 +96,13 @@ module AiCli
|
|
|
102
96
|
end
|
|
103
97
|
|
|
104
98
|
def run_script(script)
|
|
105
|
-
pastel = Pastel.new
|
|
106
99
|
puts "└ #{Helpers::I18n.t('Running')}: #{script}"
|
|
107
100
|
puts ''
|
|
108
101
|
system(ENV['SHELL'] || 'bash', '-c', script)
|
|
109
102
|
Helpers::ShellHistory.append(script)
|
|
110
103
|
end
|
|
111
104
|
|
|
112
|
-
def run_or_revise_flow(script,
|
|
105
|
+
def run_or_revise_flow(script, config, silent_mode)
|
|
113
106
|
prompt = TTY::Prompt.new(interrupt: :exit)
|
|
114
107
|
empty_script = script.strip.empty?
|
|
115
108
|
|
|
@@ -132,7 +125,7 @@ module AiCli
|
|
|
132
125
|
new_script = prompt.ask(Helpers::I18n.t('you can edit script here'), default: script)
|
|
133
126
|
run_script(new_script) if new_script && !new_script.empty?
|
|
134
127
|
when :revise
|
|
135
|
-
revision_flow(script,
|
|
128
|
+
revision_flow(script, config, silent_mode)
|
|
136
129
|
when :copy
|
|
137
130
|
Clipboard.copy(script)
|
|
138
131
|
puts "└ #{Helpers::I18n.t('Copied to clipboard!')}"
|
|
@@ -142,7 +135,7 @@ module AiCli
|
|
|
142
135
|
end
|
|
143
136
|
end
|
|
144
137
|
|
|
145
|
-
def revision_flow(current_script,
|
|
138
|
+
def revision_flow(current_script, config, silent_mode)
|
|
146
139
|
pastel = Pastel.new
|
|
147
140
|
revision = ask_revision
|
|
148
141
|
|
|
@@ -152,9 +145,7 @@ module AiCli
|
|
|
152
145
|
result = Helpers::Completion.get_revision(
|
|
153
146
|
prompt: revision,
|
|
154
147
|
code: current_script,
|
|
155
|
-
|
|
156
|
-
model: model,
|
|
157
|
-
api_endpoint: api_endpoint
|
|
148
|
+
config: config
|
|
158
149
|
)
|
|
159
150
|
|
|
160
151
|
spinner.success(Helpers::I18n.t('Your new script') + ':')
|
|
@@ -170,9 +161,7 @@ module AiCli
|
|
|
170
161
|
|
|
171
162
|
explanation = Helpers::Completion.get_explanation(
|
|
172
163
|
script: script,
|
|
173
|
-
|
|
174
|
-
model: model,
|
|
175
|
-
api_endpoint: api_endpoint
|
|
164
|
+
config: config
|
|
176
165
|
)
|
|
177
166
|
|
|
178
167
|
info_spinner.success(Helpers::I18n.t('Explanation') + ':')
|
|
@@ -183,7 +172,7 @@ module AiCli
|
|
|
183
172
|
puts pastel.dim('•')
|
|
184
173
|
end
|
|
185
174
|
|
|
186
|
-
run_or_revise_flow(script,
|
|
175
|
+
run_or_revise_flow(script, config, silent_mode)
|
|
187
176
|
end
|
|
188
177
|
|
|
189
178
|
private_class_method :init_i18n, :examples, :ask_prompt, :ask_revision,
|
data/lib/aicli/version.rb
CHANGED
data/lib/aicli.rb
CHANGED
|
@@ -7,6 +7,7 @@ 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'
|
|
12
13
|
require_relative 'aicli/prompt'
|
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
|
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
|
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.
|
|
4
|
+
version: 0.2.2
|
|
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
|
|
@@ -104,6 +118,7 @@ files:
|
|
|
104
118
|
- lib/aicli/helpers/constants.rb
|
|
105
119
|
- lib/aicli/helpers/error.rb
|
|
106
120
|
- lib/aicli/helpers/i18n.rb
|
|
121
|
+
- lib/aicli/helpers/llm.rb
|
|
107
122
|
- lib/aicli/helpers/os_detect.rb
|
|
108
123
|
- lib/aicli/helpers/shell_history.rb
|
|
109
124
|
- lib/aicli/helpers/strip_regex_patterns.rb
|