aicli 0.3.1 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8134338cc00872e289ea3980bc6367cb8c6231ba5c09879a80cc3ff5d8ebf32d
4
- data.tar.gz: 4c53b4eb29d4f93cb2dba0709a7121219945a37aebf6edb45db288dba373002b
3
+ metadata.gz: cf98faf6cbdbc3b6ec1597895520ab51c1a2c4d8f31e0ef80360497d57d6fdd6
4
+ data.tar.gz: 531e7e3941c6de76d4dbca4f284f2ba6f3017216398173c605448e8c4a036934
5
5
  SHA512:
6
- metadata.gz: 0b89932ed8a62cce86bd1feb3e6da8e3026fe8448ae3a11a55bfdffa7180addee78cbfeeaad8ef19a556e865fb016c2277edfb31448bb6675606919cef528da4
7
- data.tar.gz: ae512d71dfdd1c65fb2d5d18130180eb6d6f5d00673b030743e5cf3cc195e87e2db6856816601d9b617ad8e250e5ed3223b749b2032480ea08a2e30c32d2f64f
6
+ metadata.gz: 1b9259c9ac632dcb4bb5324db52260a112b58ea087faf2d28a36f22c7099d234d6188ad406e6791e746b2d994d397d4d61b39a24f8940b1dc54f9b17af3adf85
7
+ data.tar.gz: 348882fccb22e28fcb3970e1a2f603b512ab6ca149e1d339debdb3a3feb29facd4133cfb34c29000334bc10121d6f4fb3432bebdef862fb61b3b4a3c4e916446
data/README.md CHANGED
@@ -1,194 +1,55 @@
1
1
  # aicli
2
2
 
3
- Natural language to live shell commands OpenAI & Anthropic.
3
+ Natural language shell commands. OpenAI or Anthropic.
4
4
 
5
- Chat with the model and run suggested commands in your shell in one seamless flow.
6
-
7
- The gem is named **aicli**; the shell command is **`ai`** (also available as `aicli`).
5
+ Install: `gem install aicli` (Ruby 3.0+). Command: **`ai`**.
8
6
 
9
7
  ## Setup
10
8
 
11
- > Requires Ruby 3.0+
12
-
13
- 1. Install the gem:
14
-
15
- ```sh
16
- gem install aicli
17
- ```
18
-
19
- Or from this repository:
20
-
21
- ```sh
22
- bundle install
23
- bundle exec rake install
24
- # or
25
- gem build aicli.gemspec && gem install ./aicli-*.gem
26
- ```
27
-
28
- 2. Choose a provider and set the matching API key.
29
-
30
- **OpenAI** (default) — key from [OpenAI](https://platform.openai.com/account/api-keys):
31
-
32
- ```sh
33
- ai config set PROVIDER=openai
34
- ai config set OPENAI_KEY=<your token>
35
- ```
36
-
37
- **Anthropic** — key from [Anthropic](https://console.anthropic.com/settings/keys):
38
-
39
- ```sh
40
- ai config set PROVIDER=anthropic
41
- ai config set ANTHROPIC_KEY=<your token>
42
- ```
43
-
44
- Or use the interactive UI (`ai config`) to pick provider, key, and model.
45
- Model lists come from the [RubyLLM](https://rubyllm.com/models/) registry.
46
-
47
- Config lives in `~/.aicli/config`. Chat/prompt history is stored in `~/.aicli/context` (last 40 messages) and reloaded on the next run.
48
-
49
- 3. **Shell history** (optional): when you run a suggested command, store that command in your shell history instead of the original `ai ...` line (so ↑ recalls `for f in *.mp4; do ...` rather than `ai bash ...`):
50
-
51
- ```sh
52
- ai config shell
53
- ```
54
-
55
- Then add the printed `source` line to `~/.zshrc` or `~/.bashrc` and restart your shell (or `source` the file).
56
-
57
- ## Usage
58
-
59
- ```bash
60
- ai <prompt>
61
- ```
62
-
63
- For example:
64
-
65
- ```bash
66
- ai list all log files
67
- ```
68
-
69
- Then you will get an output where you can choose to run the suggested command, revise it via a prompt, edit it, copy it, or cancel.
70
-
71
- ### Special characters
72
-
73
- Some shells handle characters like `?` or `*` specially. Wrap the prompt in quotes if needed:
74
-
75
- ```bash
76
- ai 'what is my ip address'
77
- ```
78
-
79
- ### Chat mode
80
-
81
- ```bash
82
- ai chat
83
- ```
84
-
85
- 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.
86
-
87
- ### Silent mode (skip explanations)
88
-
89
- ```bash
90
- ai -s list all log files
91
- ```
92
-
93
- Or save the preference:
94
-
95
- ```bash
96
- ai config set SILENT_MODE=true
97
- ```
98
-
99
- ### Provider and model
100
-
101
9
  ```sh
102
- ai config set PROVIDER=openai # or anthropic
103
- ai config set MODEL=gpt-4o-mini # provider-specific model id
10
+ ai config set PROVIDER=openai
11
+ ai config set OPENAI_KEY=<token>
104
12
  ```
105
13
 
106
- Defaults: `gpt-4o-mini` (OpenAI), `claude-sonnet-4-6` (Anthropic).
107
-
108
- LLM calls go through [RubyLLM](https://rubyllm.com/), so chat streaming and model metadata stay provider-agnostic.
109
-
110
- ### Custom OpenAI API endpoint
14
+ Or `ai config` for the interactive setup. Config: `~/.aicli/config`.
111
15
 
112
- Useful for OpenAI-compatible proxies (ignored for Anthropic):
16
+ Anthropic:
113
17
 
114
18
  ```sh
115
- ai config set OPENAI_API_ENDPOINT=<your proxy endpoint>
19
+ ai config set PROVIDER=anthropic
20
+ ai config set ANTHROPIC_KEY=<token>
116
21
  ```
117
22
 
118
- Default: `https://api.openai.com/v1`
119
-
120
- ### Set Language
121
-
122
- | Language | Key |
123
- | ------------------- | ------- |
124
- | English | en |
125
- | Simplified Chinese | zh-Hans |
126
- | Traditional Chinese | zh-Hant |
127
- | Spanish | es |
128
- | Japanese | jp |
129
- | Korean | ko |
130
- | French | fr |
131
- | German | de |
132
- | Russian | ru |
133
- | Ukrainian | uk |
134
- | Vietnamese | vi |
135
- | Arabic | ar |
136
- | Portuguese | pt |
137
- | Turkish | tr |
138
- | Indonesian | id |
139
- | Italian | it |
23
+ Shell history (optional — ↑ recalls the run command, not `ai ...`):
140
24
 
141
25
  ```sh
142
- ai config set LANGUAGE=zh-Hans
26
+ ai config shell
27
+ source ~/.zshrc
143
28
  ```
144
29
 
145
- ### Config UI
30
+ ## Usage
146
31
 
147
- ```bash
148
- ai config
32
+ ```sh
33
+ ai list all log files
34
+ ai 'what is my ip address' # quote special chars
35
+ ai chat # multi-turn; Ctrl+d to quit
36
+ ai -s list log files # skip Explain in menu
149
37
  ```
150
38
 
151
- ### Upgrading
152
-
153
- ```bash
154
- ai --version
39
+ ```sh
40
+ ai config set SILENT_MODE=true
41
+ ai config set MODEL=gpt-4o-mini
42
+ ai config set LANGUAGE=it
155
43
  ai update
156
- # or: gem update aicli
157
44
  ```
158
45
 
159
46
  ## Development
160
47
 
161
- Run the local checkout (not the installed gem) with Bundler from this directory:
162
-
163
48
  ```sh
164
49
  bundle install
165
-
166
- bundle exec bin/ai --help
167
- bundle exec bin/ai config
168
- bundle exec bin/ai chat
169
50
  bundle exec bin/ai list all log files
170
51
  ```
171
52
 
172
- `bundle exec` loads dependencies from the Gemfile and code from `lib/`. `bin/aicli` is an alias of `bin/ai`.
173
-
174
- Example config for local testing:
175
-
176
- ```sh
177
- bundle exec bin/ai config set PROVIDER=openai
178
- bundle exec bin/ai config set OPENAI_KEY=<your token>
179
- # or: PROVIDER=anthropic + ANTHROPIC_KEY=<your token>
180
- ```
181
-
182
- ## Common Issues
183
-
184
- ### Rate limit / quota errors
185
-
186
- 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`.
187
-
188
- ## Motivation
189
-
190
- I am not a bash wizard, and am dying for access to the copilot CLI, and got impatient.
191
-
192
- ## Credit
53
+ ## License
193
54
 
194
- Started from [BuilderIO/ai-shell](https://github.com/BuilderIO/ai-shell) (MIT; copyright Builder.io 2023).
55
+ [MIT](LICENSE)
data/lib/aicli/cli.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
- require 'pastel'
5
4
 
6
5
  module AiCli
7
6
  class CLI < Thor
8
- KNOWN_COMMANDS = %w[config chat update help version tree].freeze
7
+ KNOWN_COMMANDS = %w[config chat update help version].freeze
9
8
 
10
9
  def self.exit_on_failure?
11
10
  true
@@ -13,8 +12,6 @@ module AiCli
13
12
 
14
13
  def self.start(given_args = ARGV, config = {})
15
14
  args = given_args.dup
16
-
17
- # Extract global flags that may appear before the prompt text
18
15
  silent = args.delete('--silent') || args.delete('-s')
19
16
  version_flag = args.delete('--version') || args.delete('-v')
20
17
  help_flag = args.delete('--help') || args.delete('-h')
@@ -29,7 +26,6 @@ module AiCli
29
26
  return
30
27
  end
31
28
 
32
- # Prompt flag: -p / --prompt
33
29
  prompt_from_flag = nil
34
30
  if (idx = args.index('-p') || args.index('--prompt'))
35
31
  prompt_from_flag = args[idx + 1]
@@ -38,51 +34,43 @@ module AiCli
38
34
 
39
35
  first = args.first
40
36
 
41
- if first.nil? || !KNOWN_COMMANDS.include?(first)
42
- # Treat remaining args as the natural-language prompt
37
+ unless first.nil? || KNOWN_COMMANDS.include?(first)
43
38
  prompt_text = prompt_from_flag || args.join(' ')
44
39
  invoke_prompt(prompt_text, silent: !silent.nil?)
45
40
  return
46
41
  end
47
42
 
48
- # Rebuild argv for Thor subcommands, preserving silent if relevant
49
- thor_args = args
50
- thor_args = ['--silent'] + thor_args if silent && first == 'chat'
43
+ thor_args = silent && first == 'chat' ? ['--silent'] + args : args
51
44
  super(thor_args, config)
52
45
  end
53
46
 
54
47
  def self.invoke_prompt(prompt_text, silent: false)
55
- pastel = Pastel.new
56
-
57
- begin
58
- config = Helpers::Config.get
59
- Helpers::I18n.set_language(config['LANGUAGE'])
60
- rescue StandardError
61
- Helpers::I18n.set_language('en')
62
- end
63
-
48
+ Helpers::I18n.load_from_config!
64
49
  Prompt.run(use_prompt: prompt_text, silent_mode: silent)
50
+ rescue Interrupt
51
+ puts Helpers::I18n.t('Goodbye!')
52
+ exit 0
65
53
  rescue Helpers::KnownError, StandardError => e
66
- puts "\n#{pastel.red('✖')} #{e.message}"
54
+ puts "\n#{Helpers::Theme.pastel.red('✖')} #{e.message}"
67
55
  Helpers::Error.handle_cli_error(e)
68
56
  exit 1
69
57
  end
70
58
 
71
59
  desc 'config [MODE] [KEY=VALUE...]', 'Configure the CLI'
72
60
  def config(mode = nil, *key_values)
73
- init_i18n
61
+ Helpers::I18n.load_from_config!
74
62
  Commands::Config.run(mode, *key_values)
75
63
  end
76
64
 
77
65
  desc 'chat', 'Start a new chat session'
78
66
  def chat
79
- init_i18n
67
+ Helpers::I18n.load_from_config!
80
68
  Commands::Chat.run
81
69
  end
82
70
 
83
71
  desc 'update', 'Update the aicli gem to the latest version'
84
72
  def update
85
- init_i18n
73
+ Helpers::I18n.load_from_config!
86
74
  Commands::Update.run
87
75
  end
88
76
 
@@ -91,14 +79,5 @@ module AiCli
91
79
  def version
92
80
  puts AiCli::VERSION
93
81
  end
94
-
95
- private
96
-
97
- def init_i18n
98
- config = Helpers::Config.get
99
- Helpers::I18n.set_language(config['LANGUAGE'])
100
- rescue StandardError
101
- Helpers::I18n.set_language('en')
102
- end
103
82
  end
104
83
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'io/console'
4
- require 'pastel'
5
- require 'tty-prompt'
6
4
  require 'tty-spinner'
7
5
 
8
6
  module AiCli
@@ -16,31 +14,27 @@ module AiCli
16
14
  restored = Helpers::Context.load_messages
17
15
  Helpers::Context.apply_to_chat(chat) if restored.any?
18
16
 
19
- pastel = Pastel.new
20
- prompt = TTY::Prompt.new(interrupt: :error)
17
+ prompt = Helpers::Theme.prompt(interrupt: :error)
21
18
  Helpers::Context.seed_prompt_history(prompt)
22
19
 
23
20
  puts ''
24
- puts "#{config['PROVIDER']} / #{config['MODEL']} #{pastel.dim('Ctrl+d to quit')}"
21
+ puts "#{Helpers::Theme.status(config)} #{Helpers::Theme.dim('Ctrl+d to quit')}"
25
22
 
26
23
  catch(:chat_quit) do
27
24
  prompt.on(:keyctrl_d) { throw :chat_quit }
28
25
 
29
26
  loop do
30
27
  begin
31
- user_prompt = prompt.ask(pastel.cyan("#{Helpers::I18n.t('You')}:")) do |q|
28
+ user_prompt = prompt.ask(Helpers::Theme.accent("#{Helpers::I18n.t('You')}:")) do |q|
32
29
  q.required true
33
30
  q.validate(/.+/, Helpers::I18n.t('Please enter a prompt.'))
34
31
  end
35
32
  rescue TTY::Reader::InputInterrupt
36
- # Ctrl+C clears the current input line and re-prompts.
37
33
  print "\r\e[2K"
38
34
  next
39
35
  end
40
36
 
41
- if user_prompt.nil? || user_prompt.strip.downcase == 'exit'
42
- throw :chat_quit
43
- end
37
+ throw :chat_quit if user_prompt.nil? || user_prompt.strip.downcase == 'exit'
44
38
 
45
39
  spinner = TTY::Spinner.new("[:spinner] #{Helpers::I18n.t('THINKING...')}", format: :dots)
46
40
  spinner.auto_spin
@@ -51,7 +45,7 @@ module AiCli
51
45
  user_prompt,
52
46
  writer: lambda { |chunk|
53
47
  unless started
54
- spinner.success(pastel.green('ai:'))
48
+ spinner.success(Helpers::Theme.pastel.green('ai:'))
55
49
  puts ''
56
50
  started = true
57
51
  end
@@ -60,14 +54,13 @@ module AiCli
60
54
  )
61
55
 
62
56
  unless started
63
- spinner.success(pastel.green('ai:'))
57
+ spinner.success(Helpers::Theme.pastel.green('ai:'))
64
58
  puts ''
65
59
  end
66
- puts ''
67
- puts ''
60
+ puts "\n\n"
68
61
 
69
62
  Helpers::Context.save_from_chat(chat)
70
- offer_to_run_commands(response, pastel)
63
+ offer_to_run_commands(response)
71
64
  end
72
65
  end
73
66
 
@@ -76,20 +69,16 @@ module AiCli
76
69
  puts Helpers::I18n.t('Goodbye!')
77
70
  end
78
71
 
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)
72
+ def offer_to_run_commands(response)
73
+ Helpers::Completion.extract_commands(response).each do |command|
74
+ puts Helpers::Theme.dim(command)
85
75
  next unless ask_to_run?
86
76
 
87
- run_command(command)
88
- puts ''
77
+ puts "#{Helpers::I18n.t('Running')}: #{command}\n\n"
78
+ Helpers::ShellExec.run(command)
89
79
  end
90
80
  end
91
81
 
92
- # Only explicit `y` runs the command. Enter, Esc, n, Ctrl-C → skip.
93
82
  def ask_to_run?
94
83
  tty = File.open('/dev/tty', 'r+')
95
84
  tty.print "#{Helpers::I18n.t('Run it?')} (y/n) "
@@ -98,24 +87,13 @@ module AiCli
98
87
  tty.puts
99
88
  char.to_s.downcase == 'y'
100
89
  rescue Interrupt
101
- begin
102
- tty&.puts
103
- rescue StandardError
104
- puts
105
- end
90
+ tty&.puts
106
91
  false
107
92
  ensure
108
93
  tty&.close
109
94
  end
110
95
 
111
- def run_command(command)
112
- puts "#{Helpers::I18n.t('Running')}: #{command}"
113
- puts ''
114
- system(ENV['SHELL'] || 'bash', '-c', command)
115
- Helpers::ShellHistory.record_executed(command)
116
- end
117
-
118
- private_class_method :offer_to_run_commands, :ask_to_run?, :run_command
96
+ private_class_method :offer_to_run_commands, :ask_to_run?
119
97
  end
120
98
  end
121
99
  end
@@ -17,9 +17,13 @@ module AiCli
17
17
 
18
18
  case mode
19
19
  when 'shell'
20
- Helpers::ShellHistory.install_shell_integration!
20
+ result = Helpers::ShellHistory.activate_shell_integration!
21
21
  puts pastel.green('✔') + ' ' + Helpers::I18n.t('Shell integration installed.')
22
- hint = Helpers::ShellHistory.shell_integration_hint
22
+ hint = Helpers::ShellHistory.shell_integration_hint(
23
+ activated: result[:activated],
24
+ rc_path: result[:rc_path],
25
+ source_path: result[:source_path]
26
+ )
23
27
  puts hint unless hint.empty?
24
28
  when 'get', 'set'
25
29
  if key_values.empty?
@@ -30,7 +34,7 @@ module AiCli
30
34
  if mode == 'get'
31
35
  config = Helpers::Config.get
32
36
  key_values.each do |key|
33
- if Helpers::Config.has_own?(config, key)
37
+ if config.key?(key)
34
38
  puts "#{key}=#{config[key]}"
35
39
  else
36
40
  raise Helpers::KnownError, "#{Helpers::I18n.t('Invalid config property')}: #{key}"