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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/README.md +48 -14
- data/lib/aicli/cli.rb +2 -2
- data/lib/aicli/commands/chat.rb +91 -30
- data/lib/aicli/commands/update.rb +65 -3
- data/lib/aicli/helpers/completion.rb +138 -182
- data/lib/aicli/helpers/config.rb +103 -15
- data/lib/aicli/helpers/context.rb +94 -0
- data/lib/aicli/helpers/llm.rb +130 -0
- data/lib/aicli/prompt.rb +43 -37
- data/lib/aicli/version.rb +1 -1
- data/lib/aicli.rb +2 -0
- data/locales/en.yml +10 -0
- data/locales/it.yml +10 -0
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9657d0162a9e2e70e856d1d6fd798880a300eb8c2dd8e284b2d3abe9e7964e9a
|
|
4
|
+
data.tar.gz: 73885b2b870ba4c450df673e7ee21f9eadd456106703b2023be0678b1b43aa78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e395efe484bc84474cfc1e0d50600e07a1b836b6431dfda5f1786f7357289c7c113292df41ac7ff54bd11187f4a55f4ddb940b0be2af3bb235dc2dc82aa3466b
|
|
7
|
+
data.tar.gz: aa611ebff07502eef7886872083511cff09fb7e7613fd61294eb64c0403eb2012f083c5091c19f807d310ed842f4f48195ca7729cfc803bc4e924d4dddb04728
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
## 0.2.6
|
|
2
|
+
|
|
3
|
+
- Ctrl+C clears the current input line instead of exiting chat/prompt
|
|
4
|
+
|
|
5
|
+
## 0.2.5
|
|
6
|
+
|
|
7
|
+
- Fix chat `Run it? (y/n)` input (read from `/dev/tty`; only `y` executes)
|
|
8
|
+
|
|
9
|
+
## 0.2.4
|
|
10
|
+
|
|
11
|
+
- Persist last 40 chat/prompt messages in `~/.aicli/context` and reload on next run
|
|
12
|
+
- Migrate flat `~/.aicli` config file to `~/.aicli/config`
|
|
13
|
+
|
|
14
|
+
## 0.2.3
|
|
15
|
+
|
|
16
|
+
- In `ai chat`, detect suggested shell commands and ask to run them, then continue the conversation
|
|
17
|
+
|
|
18
|
+
## 0.2.2
|
|
19
|
+
|
|
20
|
+
- Improve `ai update`: compare installed version with RubyGems, install latest explicitly, clearer status messages
|
|
21
|
+
|
|
22
|
+
## 0.2.1
|
|
23
|
+
|
|
24
|
+
- Fix provider/model sync so Anthropic models are not sent to OpenAI
|
|
25
|
+
- Show active `provider / model` on each run
|
|
26
|
+
- Clearer error when a model is unavailable for the API key
|
|
27
|
+
- `config set MODEL=...` infers PROVIDER from the RubyLLM registry
|
|
28
|
+
|
|
29
|
+
## 0.2.0
|
|
30
|
+
|
|
31
|
+
- Add Anthropic provider alongside OpenAI
|
|
32
|
+
- Route all LLM calls through RubyLLM
|
|
33
|
+
- Choose provider and model in `aicli config` (model list from RubyLLM registry)
|
|
34
|
+
- Config keys: `PROVIDER`, `ANTHROPIC_KEY` (plus existing `OPENAI_KEY` / `MODEL`)
|
|
35
|
+
|
|
36
|
+
## 0.1.0
|
|
37
|
+
|
|
38
|
+
- Initial RubyGems release as `aicli`
|
|
39
|
+
|
|
1
40
|
## 1.0.12
|
|
2
41
|
|
|
3
42
|
- Bug fixes
|
data/README.md
CHANGED
|
@@ -38,17 +38,26 @@ 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
|
|
|
51
|
-
|
|
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
|
+
|
|
60
|
+
Config lives in `~/.aicli/config`. Chat/prompt history is stored in `~/.aicli/context` (last 40 messages) and reloaded on the next run.
|
|
52
61
|
|
|
53
62
|
## Usage
|
|
54
63
|
|
|
@@ -80,7 +89,7 @@ aicli 'what is my ip address'
|
|
|
80
89
|
aicli chat
|
|
81
90
|
```
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
Ask for shell commands in a multi-turn conversation. When the assistant suggests a command in a code fence, you are asked whether to run it; after it runs (or you decline), chat continues. Press `Ctrl+d` to quit.
|
|
84
93
|
|
|
85
94
|
### Silent mode (skip explanations)
|
|
86
95
|
|
|
@@ -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/config`.
|
|
165
199
|
|
|
166
200
|
## Motivation
|
|
167
201
|
|
data/lib/aicli/cli.rb
CHANGED
data/lib/aicli/commands/chat.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'io/console'
|
|
3
4
|
require 'pastel'
|
|
4
5
|
require 'tty-prompt'
|
|
5
6
|
require 'tty-spinner'
|
|
@@ -11,50 +12,110 @@ module AiCli
|
|
|
11
12
|
|
|
12
13
|
def run
|
|
13
14
|
config = Helpers::Config.get
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
chat_history = []
|
|
15
|
+
chat = Helpers::Completion.start_chat(config)
|
|
16
|
+
restored = Helpers::Context.load_messages
|
|
17
|
+
Helpers::Context.apply_to_chat(chat) if restored.any?
|
|
18
18
|
|
|
19
19
|
pastel = Pastel.new
|
|
20
|
-
prompt = TTY::Prompt.new(interrupt: :
|
|
20
|
+
prompt = TTY::Prompt.new(interrupt: :error)
|
|
21
|
+
Helpers::Context.seed_prompt_history(prompt)
|
|
21
22
|
|
|
22
23
|
puts ''
|
|
23
|
-
puts "
|
|
24
|
+
puts "#{config['PROVIDER']} / #{config['MODEL']} #{pastel.dim('Ctrl+d to quit')}"
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
catch(:chat_quit) do
|
|
27
|
+
prompt.on(:keyctrl_d) { throw :chat_quit }
|
|
28
|
+
|
|
29
|
+
loop do
|
|
30
|
+
begin
|
|
31
|
+
user_prompt = prompt.ask(pastel.cyan("#{Helpers::I18n.t('You')}:")) do |q|
|
|
32
|
+
q.required true
|
|
33
|
+
q.validate(/.+/, Helpers::I18n.t('Please enter a prompt.'))
|
|
34
|
+
end
|
|
35
|
+
rescue TTY::Reader::InputInterrupt
|
|
36
|
+
# Ctrl+C clears the current input line and re-prompts.
|
|
37
|
+
print "\r\e[2K"
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if user_prompt.nil? || user_prompt.strip.downcase == 'exit'
|
|
42
|
+
throw :chat_quit
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
spinner = TTY::Spinner.new("[:spinner] #{Helpers::I18n.t('THINKING...')}", format: :dots)
|
|
46
|
+
spinner.auto_spin
|
|
47
|
+
started = false
|
|
48
|
+
|
|
49
|
+
response = Helpers::Completion.stream_chat_message(
|
|
50
|
+
chat,
|
|
51
|
+
user_prompt,
|
|
52
|
+
writer: lambda { |chunk|
|
|
53
|
+
unless started
|
|
54
|
+
spinner.success(pastel.green('aicli:'))
|
|
55
|
+
puts ''
|
|
56
|
+
started = true
|
|
57
|
+
end
|
|
58
|
+
print chunk
|
|
59
|
+
}
|
|
60
|
+
)
|
|
30
61
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
unless started
|
|
63
|
+
spinner.success(pastel.green('aicli:'))
|
|
64
|
+
puts ''
|
|
65
|
+
end
|
|
66
|
+
puts ''
|
|
67
|
+
puts ''
|
|
68
|
+
|
|
69
|
+
Helpers::Context.save_from_chat(chat)
|
|
70
|
+
offer_to_run_commands(response, pastel)
|
|
34
71
|
end
|
|
72
|
+
end
|
|
35
73
|
|
|
36
|
-
|
|
37
|
-
|
|
74
|
+
Helpers::Context.save_from_chat(chat)
|
|
75
|
+
puts ''
|
|
76
|
+
puts Helpers::I18n.t('Goodbye!')
|
|
77
|
+
end
|
|
38
78
|
|
|
39
|
-
|
|
79
|
+
def offer_to_run_commands(response, pastel)
|
|
80
|
+
commands = Helpers::Completion.extract_commands(response)
|
|
81
|
+
return if commands.empty?
|
|
40
82
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
model: model,
|
|
45
|
-
api_endpoint: api_endpoint
|
|
46
|
-
)
|
|
47
|
-
enumerator = stream_lines.each
|
|
48
|
-
read_response = Helpers::Completion.read_data(enumerator)
|
|
83
|
+
commands.each do |command|
|
|
84
|
+
puts pastel.dim(command)
|
|
85
|
+
next unless ask_to_run?
|
|
49
86
|
|
|
50
|
-
|
|
51
|
-
puts ''
|
|
52
|
-
full_response = read_response.call(->(chunk) { print chunk })
|
|
53
|
-
chat_history << { 'role' => 'assistant', 'content' => full_response }
|
|
54
|
-
puts ''
|
|
87
|
+
run_command(command)
|
|
55
88
|
puts ''
|
|
56
89
|
end
|
|
57
90
|
end
|
|
91
|
+
|
|
92
|
+
# Only explicit `y` runs the command. Enter, Esc, n, Ctrl-C → skip.
|
|
93
|
+
def ask_to_run?
|
|
94
|
+
tty = File.open('/dev/tty', 'r+')
|
|
95
|
+
tty.print "#{Helpers::I18n.t('Run it?')} (y/n) "
|
|
96
|
+
tty.flush
|
|
97
|
+
char = tty.getch
|
|
98
|
+
tty.puts
|
|
99
|
+
char.to_s.downcase == 'y'
|
|
100
|
+
rescue Interrupt
|
|
101
|
+
begin
|
|
102
|
+
tty&.puts
|
|
103
|
+
rescue StandardError
|
|
104
|
+
puts
|
|
105
|
+
end
|
|
106
|
+
false
|
|
107
|
+
ensure
|
|
108
|
+
tty&.close
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def run_command(command)
|
|
112
|
+
puts "#{Helpers::I18n.t('Running')}: #{command}"
|
|
113
|
+
puts ''
|
|
114
|
+
system(ENV['SHELL'] || 'bash', '-c', command)
|
|
115
|
+
Helpers::ShellHistory.append(command)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private_class_method :offer_to_run_commands, :ask_to_run?, :run_command
|
|
58
119
|
end
|
|
59
120
|
end
|
|
60
121
|
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
|