aicli 0.2.2 → 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 +17 -0
- data/README.md +3 -3
- data/lib/aicli/commands/chat.rb +91 -31
- data/lib/aicli/helpers/completion.rb +35 -1
- data/lib/aicli/helpers/config.rb +34 -3
- data/lib/aicli/helpers/context.rb +94 -0
- data/lib/aicli/prompt.rb +34 -17
- data/lib/aicli/version.rb +1 -1
- data/lib/aicli.rb +1 -0
- data/locales/en.yml +4 -0
- data/locales/it.yml +4 -0
- metadata +2 -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,20 @@
|
|
|
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
|
+
|
|
1
18
|
## 0.2.2
|
|
2
19
|
|
|
3
20
|
- Improve `ai update`: compare installed version with RubyGems, install latest explicitly, clearer status messages
|
data/README.md
CHANGED
|
@@ -57,7 +57,7 @@ Ruby port by [Antonio Molinari](https://github.com/magnum), inspired by the orig
|
|
|
57
57
|
Or use the interactive UI (`aicli config`) to pick provider, key, and model.
|
|
58
58
|
Model lists come from the [RubyLLM](https://rubyllm.com/models/) registry.
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Config lives in `~/.aicli/config`. Chat/prompt history is stored in `~/.aicli/context` (last 40 messages) and reloaded on the next run.
|
|
61
61
|
|
|
62
62
|
## Usage
|
|
63
63
|
|
|
@@ -89,7 +89,7 @@ aicli 'what is my ip address'
|
|
|
89
89
|
aicli chat
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
|
|
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.
|
|
93
93
|
|
|
94
94
|
### Silent mode (skip explanations)
|
|
95
95
|
|
|
@@ -195,7 +195,7 @@ bundle exec bin/ai config set OPENAI_KEY=<your token>
|
|
|
195
195
|
|
|
196
196
|
### Rate limit / quota errors
|
|
197
197
|
|
|
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`.
|
|
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`.
|
|
199
199
|
|
|
200
200
|
## Motivation
|
|
201
201
|
|
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'
|
|
@@ -12,50 +13,109 @@ module AiCli
|
|
|
12
13
|
def run
|
|
13
14
|
config = Helpers::Config.get
|
|
14
15
|
chat = Helpers::Completion.start_chat(config)
|
|
16
|
+
restored = Helpers::Context.load_messages
|
|
17
|
+
Helpers::Context.apply_to_chat(chat) if restored.any?
|
|
15
18
|
|
|
16
19
|
pastel = Pastel.new
|
|
17
|
-
prompt = TTY::Prompt.new(interrupt: :
|
|
20
|
+
prompt = TTY::Prompt.new(interrupt: :error)
|
|
21
|
+
Helpers::Context.seed_prompt_history(prompt)
|
|
18
22
|
|
|
19
23
|
puts ''
|
|
20
|
-
puts "
|
|
21
|
-
puts pastel.dim(" #{config['PROVIDER']} / #{config['MODEL']}")
|
|
24
|
+
puts "#{config['PROVIDER']} / #{config['MODEL']} #{pastel.dim('Ctrl+d to quit')}"
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
q.required true
|
|
26
|
-
q.validate(/.+/, Helpers::I18n.t('Please enter a prompt.'))
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
if user_prompt.nil? || user_prompt == 'exit'
|
|
30
|
-
puts "└ #{Helpers::I18n.t('Goodbye!')}"
|
|
31
|
-
exit 0
|
|
32
|
-
end
|
|
26
|
+
catch(:chat_quit) do
|
|
27
|
+
prompt.on(:keyctrl_d) { throw :chat_quit }
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
chat,
|
|
40
|
-
user_prompt,
|
|
41
|
-
writer: lambda { |chunk|
|
|
42
|
-
unless started
|
|
43
|
-
spinner.success(pastel.green('aicli:'))
|
|
44
|
-
puts ''
|
|
45
|
-
started = true
|
|
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.'))
|
|
46
34
|
end
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
)
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
unless started
|
|
63
|
+
spinner.success(pastel.green('aicli:'))
|
|
64
|
+
puts ''
|
|
65
|
+
end
|
|
53
66
|
puts ''
|
|
67
|
+
puts ''
|
|
68
|
+
|
|
69
|
+
Helpers::Context.save_from_chat(chat)
|
|
70
|
+
offer_to_run_commands(response, pastel)
|
|
54
71
|
end
|
|
55
|
-
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Helpers::Context.save_from_chat(chat)
|
|
75
|
+
puts ''
|
|
76
|
+
puts Helpers::I18n.t('Goodbye!')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def offer_to_run_commands(response, pastel)
|
|
80
|
+
commands = Helpers::Completion.extract_commands(response)
|
|
81
|
+
return if commands.empty?
|
|
82
|
+
|
|
83
|
+
commands.each do |command|
|
|
84
|
+
puts pastel.dim(command)
|
|
85
|
+
next unless ask_to_run?
|
|
86
|
+
|
|
87
|
+
run_command(command)
|
|
56
88
|
puts ''
|
|
57
89
|
end
|
|
58
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
|
|
59
119
|
end
|
|
60
120
|
end
|
|
61
121
|
end
|
|
@@ -42,7 +42,41 @@ module AiCli
|
|
|
42
42
|
|
|
43
43
|
# Multi-turn chat helper used by `aicli chat`.
|
|
44
44
|
def start_chat(config)
|
|
45
|
-
Llm.build_chat(config)
|
|
45
|
+
chat = Llm.build_chat(config)
|
|
46
|
+
chat.with_instructions(chat_system_prompt)
|
|
47
|
+
chat
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def chat_system_prompt
|
|
51
|
+
<<~PROMPT
|
|
52
|
+
You are aicli, a terminal assistant. The user is asking for shell commands.
|
|
53
|
+
|
|
54
|
+
Target OS: #{OsDetect.operating_system_name}
|
|
55
|
+
Target shell: #{OsDetect.detect_shell}
|
|
56
|
+
|
|
57
|
+
Whenever you suggest a command the user can run, put the exact runnable command
|
|
58
|
+
alone inside a markdown fenced code block, for example:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
ls -la
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Prefer a single command. You may briefly explain outside the fence.
|
|
65
|
+
Do not wrap non-runnable prose in fences.
|
|
66
|
+
PROMPT
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_commands(text)
|
|
70
|
+
return [] if text.nil? || text.empty?
|
|
71
|
+
|
|
72
|
+
blocks = text.scan(/```(?:[a-zA-Z0-9_+-]*)\s*\n?(.*?)```/m)
|
|
73
|
+
.flatten
|
|
74
|
+
.map { |block| block.strip.gsub(/\A`+|`+\z/, '') }
|
|
75
|
+
.map(&:strip)
|
|
76
|
+
.reject(&:empty?)
|
|
77
|
+
|
|
78
|
+
# Prefer the last fenced command if several were suggested.
|
|
79
|
+
blocks
|
|
46
80
|
end
|
|
47
81
|
|
|
48
82
|
def stream_chat_message(chat, message, writer:)
|
data/lib/aicli/helpers/config.rb
CHANGED
|
@@ -7,7 +7,9 @@ require 'tty-prompt'
|
|
|
7
7
|
module AiCli
|
|
8
8
|
module Helpers
|
|
9
9
|
module Config
|
|
10
|
-
|
|
10
|
+
HOME_DIR = File.join(Dir.home, '.aicli')
|
|
11
|
+
CONFIG_PATH = File.join(HOME_DIR, 'config')
|
|
12
|
+
LEGACY_FLAT_CONFIG_PATH = File.join(Dir.home, '.aicli')
|
|
11
13
|
LEGACY_CONFIG_PATH = File.join(Dir.home, '.ai-shell')
|
|
12
14
|
|
|
13
15
|
CONFIG_PARSERS = {
|
|
@@ -36,6 +38,32 @@ module AiCli
|
|
|
36
38
|
|
|
37
39
|
module_function
|
|
38
40
|
|
|
41
|
+
def home_dir
|
|
42
|
+
ensure_home!
|
|
43
|
+
HOME_DIR
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ensure_home!
|
|
47
|
+
if File.file?(LEGACY_FLAT_CONFIG_PATH) && !File.directory?(LEGACY_FLAT_CONFIG_PATH)
|
|
48
|
+
migrate_flat_config!
|
|
49
|
+
else
|
|
50
|
+
FileUtils.mkdir_p(HOME_DIR)
|
|
51
|
+
end
|
|
52
|
+
HOME_DIR
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def migrate_flat_config!
|
|
56
|
+
backup = "#{LEGACY_FLAT_CONFIG_PATH}.migrating.#{Process.pid}"
|
|
57
|
+
File.rename(LEGACY_FLAT_CONFIG_PATH, backup)
|
|
58
|
+
FileUtils.mkdir_p(HOME_DIR)
|
|
59
|
+
FileUtils.mv(backup, CONFIG_PATH)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
FileUtils.mkdir_p(HOME_DIR)
|
|
62
|
+
if File.exist?(backup)
|
|
63
|
+
FileUtils.mv(backup, CONFIG_PATH) unless File.exist?(CONFIG_PATH)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
39
67
|
def get(cli_config = nil)
|
|
40
68
|
config = read_config_file
|
|
41
69
|
parsed = {}
|
|
@@ -87,6 +115,7 @@ module AiCli
|
|
|
87
115
|
end
|
|
88
116
|
|
|
89
117
|
updates.each { |key, value| config[key] = value }
|
|
118
|
+
ensure_home!
|
|
90
119
|
File.write(CONFIG_PATH, stringify_ini(config))
|
|
91
120
|
end
|
|
92
121
|
|
|
@@ -178,9 +207,11 @@ module AiCli
|
|
|
178
207
|
end
|
|
179
208
|
|
|
180
209
|
def read_config_file
|
|
181
|
-
|
|
210
|
+
ensure_home!
|
|
211
|
+
|
|
212
|
+
path = if File.file?(CONFIG_PATH)
|
|
182
213
|
CONFIG_PATH
|
|
183
|
-
elsif File.
|
|
214
|
+
elsif File.file?(LEGACY_CONFIG_PATH)
|
|
184
215
|
LEGACY_CONFIG_PATH
|
|
185
216
|
end
|
|
186
217
|
return {} unless path
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module AiCli
|
|
8
|
+
module Helpers
|
|
9
|
+
module Context
|
|
10
|
+
# Keep the last N user/assistant messages across sessions.
|
|
11
|
+
MAX_MESSAGES = 40
|
|
12
|
+
CONTEXT_FILENAME = 'context'
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def path
|
|
17
|
+
File.join(Config.home_dir, CONTEXT_FILENAME)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def load_messages
|
|
21
|
+
Config.ensure_home!
|
|
22
|
+
return [] unless File.file?(path)
|
|
23
|
+
|
|
24
|
+
data = JSON.parse(File.read(path))
|
|
25
|
+
Array(data['messages'])
|
|
26
|
+
.select { |m| m.is_a?(Hash) && %w[user assistant].include?(m['role'].to_s) }
|
|
27
|
+
.map { |m| { 'role' => m['role'].to_s, 'content' => m['content'].to_s } }
|
|
28
|
+
.reject { |m| m['content'].strip.empty? }
|
|
29
|
+
.last(MAX_MESSAGES)
|
|
30
|
+
rescue StandardError
|
|
31
|
+
[]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def save_messages(messages)
|
|
35
|
+
Config.ensure_home!
|
|
36
|
+
normalized = Array(messages)
|
|
37
|
+
.map { |m| normalize_message(m) }
|
|
38
|
+
.compact
|
|
39
|
+
.last(MAX_MESSAGES)
|
|
40
|
+
|
|
41
|
+
payload = {
|
|
42
|
+
'version' => 1,
|
|
43
|
+
'updated_at' => Time.now.utc.iso8601,
|
|
44
|
+
'messages' => normalized
|
|
45
|
+
}
|
|
46
|
+
File.write(path, "#{JSON.pretty_generate(payload)}\n")
|
|
47
|
+
rescue StandardError
|
|
48
|
+
# Ignore persistence failures; chat should still work.
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def save_from_chat(chat)
|
|
52
|
+
save_messages(chat.messages)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def apply_to_chat(chat)
|
|
56
|
+
load_messages.each do |message|
|
|
57
|
+
chat.add_message(role: message['role'].to_sym, content: message['content'])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def user_prompts
|
|
62
|
+
load_messages.select { |m| m['role'] == 'user' }.map { |m| m['content'] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def seed_prompt_history(prompt)
|
|
66
|
+
return unless prompt.respond_to?(:reader)
|
|
67
|
+
|
|
68
|
+
user_prompts.each do |line|
|
|
69
|
+
prompt.reader.add_to_history(line)
|
|
70
|
+
end
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def normalize_message(message)
|
|
76
|
+
if message.respond_to?(:role) && message.respond_to?(:content)
|
|
77
|
+
role = message.role.to_s
|
|
78
|
+
content = message.content.to_s
|
|
79
|
+
elsif message.is_a?(Hash)
|
|
80
|
+
role = (message[:role] || message['role']).to_s
|
|
81
|
+
content = (message[:content] || message['content']).to_s
|
|
82
|
+
else
|
|
83
|
+
return nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
return nil unless %w[user assistant].include?(role)
|
|
87
|
+
return nil if content.strip.empty?
|
|
88
|
+
|
|
89
|
+
{ 'role' => role, 'content' => content }
|
|
90
|
+
end
|
|
91
|
+
private_class_method :normalize_message
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/aicli/prompt.rb
CHANGED
|
@@ -18,8 +18,8 @@ module AiCli
|
|
|
18
18
|
|
|
19
19
|
pastel = Pastel.new
|
|
20
20
|
puts ''
|
|
21
|
-
puts
|
|
22
|
-
puts pastel.dim("
|
|
21
|
+
puts pastel.cyan(Helpers::Constants::PROJECT_NAME)
|
|
22
|
+
puts pastel.dim("#{config['PROVIDER']} / #{config['MODEL']}")
|
|
23
23
|
|
|
24
24
|
the_prompt = use_prompt.nil? || use_prompt.strip.empty? ? ask_prompt : use_prompt
|
|
25
25
|
|
|
@@ -36,8 +36,8 @@ module AiCli
|
|
|
36
36
|
script = result[:read_script].call(->(chunk) { print chunk })
|
|
37
37
|
puts ''
|
|
38
38
|
puts ''
|
|
39
|
-
puts pastel.dim('•')
|
|
40
39
|
|
|
40
|
+
explanation_text = ''
|
|
41
41
|
unless skip_explanation
|
|
42
42
|
spinner = TTY::Spinner.new("[:spinner] #{Helpers::I18n.t('Getting explanation...')}", format: :dots)
|
|
43
43
|
spinner.auto_spin
|
|
@@ -50,15 +50,16 @@ module AiCli
|
|
|
50
50
|
)
|
|
51
51
|
spinner.success(Helpers::I18n.t('Explanation') + ':')
|
|
52
52
|
puts ''
|
|
53
|
-
explanation[:read_explanation].call(->(chunk) { print chunk })
|
|
53
|
+
explanation_text = explanation[:read_explanation].call(->(chunk) { print chunk })
|
|
54
54
|
puts ''
|
|
55
55
|
puts ''
|
|
56
|
-
puts pastel.dim('•')
|
|
57
56
|
else
|
|
57
|
+
explanation_text = info
|
|
58
58
|
spinner.success(Helpers::I18n.t('Explanation') + ':')
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
append_prompt_context(the_prompt, script, explanation_text)
|
|
62
63
|
run_or_revise_flow(script, config, silent_mode)
|
|
63
64
|
end
|
|
64
65
|
|
|
@@ -79,14 +80,32 @@ module AiCli
|
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
def ask_prompt(initial = nil)
|
|
82
|
-
prompt = TTY::Prompt.new(interrupt: :
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
87
96
|
end
|
|
88
97
|
end
|
|
89
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
|
+
|
|
90
109
|
def ask_revision
|
|
91
110
|
prompt = TTY::Prompt.new(interrupt: :exit)
|
|
92
111
|
prompt.ask(Helpers::I18n.t('What would you like me to change in this script?')) do |q|
|
|
@@ -96,7 +115,7 @@ module AiCli
|
|
|
96
115
|
end
|
|
97
116
|
|
|
98
117
|
def run_script(script)
|
|
99
|
-
puts "
|
|
118
|
+
puts "#{Helpers::I18n.t('Running')}: #{script}"
|
|
100
119
|
puts ''
|
|
101
120
|
system(ENV['SHELL'] || 'bash', '-c', script)
|
|
102
121
|
Helpers::ShellHistory.append(script)
|
|
@@ -128,15 +147,14 @@ module AiCli
|
|
|
128
147
|
revision_flow(script, config, silent_mode)
|
|
129
148
|
when :copy
|
|
130
149
|
Clipboard.copy(script)
|
|
131
|
-
puts
|
|
150
|
+
puts Helpers::I18n.t('Copied to clipboard!')
|
|
132
151
|
when :cancel
|
|
133
|
-
puts
|
|
152
|
+
puts Helpers::I18n.t('Goodbye!')
|
|
134
153
|
exit 0
|
|
135
154
|
end
|
|
136
155
|
end
|
|
137
156
|
|
|
138
157
|
def revision_flow(current_script, config, silent_mode)
|
|
139
|
-
pastel = Pastel.new
|
|
140
158
|
revision = ask_revision
|
|
141
159
|
|
|
142
160
|
spinner = TTY::Spinner.new("[:spinner] #{Helpers::I18n.t('Loading...')}", format: :dots)
|
|
@@ -153,7 +171,6 @@ module AiCli
|
|
|
153
171
|
script = result[:read_script].call(->(chunk) { print chunk })
|
|
154
172
|
puts ''
|
|
155
173
|
puts ''
|
|
156
|
-
puts pastel.dim('•')
|
|
157
174
|
|
|
158
175
|
unless silent_mode
|
|
159
176
|
info_spinner = TTY::Spinner.new("[:spinner] #{Helpers::I18n.t('Getting explanation...')}", format: :dots)
|
|
@@ -169,13 +186,13 @@ module AiCli
|
|
|
169
186
|
explanation[:read_explanation].call(->(chunk) { print chunk })
|
|
170
187
|
puts ''
|
|
171
188
|
puts ''
|
|
172
|
-
puts pastel.dim('•')
|
|
173
189
|
end
|
|
174
190
|
|
|
175
191
|
run_or_revise_flow(script, config, silent_mode)
|
|
176
192
|
end
|
|
177
193
|
|
|
178
194
|
private_class_method :init_i18n, :examples, :ask_prompt, :ask_revision,
|
|
179
|
-
:run_script, :run_or_revise_flow, :revision_flow
|
|
195
|
+
:run_script, :run_or_revise_flow, :revision_flow,
|
|
196
|
+
:append_prompt_context
|
|
180
197
|
end
|
|
181
198
|
end
|
data/lib/aicli/version.rb
CHANGED
data/lib/aicli.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative 'aicli/helpers/shell_history'
|
|
|
10
10
|
require_relative 'aicli/helpers/llm'
|
|
11
11
|
require_relative 'aicli/helpers/completion'
|
|
12
12
|
require_relative 'aicli/helpers/config'
|
|
13
|
+
require_relative 'aicli/helpers/context'
|
|
13
14
|
require_relative 'aicli/prompt'
|
|
14
15
|
require_relative 'aicli/commands/chat'
|
|
15
16
|
require_relative 'aicli/commands/config'
|
data/locales/en.yml
CHANGED
|
@@ -41,6 +41,7 @@ Loading...: Loading...
|
|
|
41
41
|
Getting explanation...: Getting explanation...
|
|
42
42
|
Explanation: Explanation
|
|
43
43
|
Run this script?: Run this script?
|
|
44
|
+
Run it?: Run it?
|
|
44
45
|
Revise this script?: Revise this script?
|
|
45
46
|
'Yes': 'Yes'
|
|
46
47
|
Lets go!: Lets go!
|
|
@@ -65,3 +66,6 @@ Please open a Bug report with the information above: Please open a Bug report wi
|
|
|
65
66
|
the information above
|
|
66
67
|
Prompt to run: Prompt to run
|
|
67
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
|
@@ -41,6 +41,7 @@ Loading...: Caricamento...
|
|
|
41
41
|
Getting explanation...: Ottieni spiegazione...
|
|
42
42
|
Explanation: Spiegazione
|
|
43
43
|
Run this script?: Esegui questo script?
|
|
44
|
+
Run it?: Lo eseguo?
|
|
44
45
|
Revise this script?: Rivedi questo script?
|
|
45
46
|
'Yes': Si
|
|
46
47
|
Lets go!: Vai!
|
|
@@ -64,3 +65,6 @@ Please open a Bug report with the information above: Apri un report di bug con l
|
|
|
64
65
|
informazioni sopra
|
|
65
66
|
Prompt to run: Prompt da eseguire
|
|
66
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.2.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antonio Molinari
|
|
@@ -116,6 +116,7 @@ files:
|
|
|
116
116
|
- lib/aicli/helpers/completion.rb
|
|
117
117
|
- lib/aicli/helpers/config.rb
|
|
118
118
|
- lib/aicli/helpers/constants.rb
|
|
119
|
+
- lib/aicli/helpers/context.rb
|
|
119
120
|
- lib/aicli/helpers/error.rb
|
|
120
121
|
- lib/aicli/helpers/i18n.rb
|
|
121
122
|
- lib/aicli/helpers/llm.rb
|