aicli 0.3.0 → 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: 3502a13ce44daa62a15e1e9bfe0bb20d372bca46b782f35dae8be05dac9dba1f
4
- data.tar.gz: 7d4462f9b8bd94263aaf5da5ec87d35406f2b814842ad6217b0db0434ff027b1
3
+ metadata.gz: cf98faf6cbdbc3b6ec1597895520ab51c1a2c4d8f31e0ef80360497d57d6fdd6
4
+ data.tar.gz: 531e7e3941c6de76d4dbca4f284f2ba6f3017216398173c605448e8c4a036934
5
5
  SHA512:
6
- metadata.gz: 3f6c788b8aa265951ac9dd7ad859659c8c695f57c0760e68b4f2d8683bd11070c78c714836a077e8c9cc161790e77899f0fdc83823b4a2c9657725a6340d4536
7
- data.tar.gz: 0ac353b46b92efc2a4fc887e0732d767017c313bd54c0ff8dddfdf8300ffa2eba386c3c15843aa4bbfd99b0c5c17d94616cdf36c162e02838bf118711023ae62
6
+ metadata.gz: 1b9259c9ac632dcb4bb5324db52260a112b58ea087faf2d28a36f22c7099d234d6188ad406e6791e746b2d994d397d4d61b39a24f8940b1dc54f9b17af3adf85
7
+ data.tar.gz: 348882fccb22e28fcb3970e1a2f603b512ab6ca149e1d339debdb3a3feb29facd4133cfb34c29000334bc10121d6f4fb3432bebdef862fb61b3b4a3c4e916446
data/README.md CHANGED
@@ -1,186 +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
- ## Usage
50
-
51
- ```bash
52
- ai <prompt>
53
- ```
54
-
55
- For example:
56
-
57
- ```bash
58
- ai list all log files
59
- ```
60
-
61
- 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.
62
-
63
- ### Special characters
64
-
65
- Some shells handle characters like `?` or `*` specially. Wrap the prompt in quotes if needed:
66
-
67
- ```bash
68
- ai 'what is my ip address'
69
- ```
70
-
71
- ### Chat mode
72
-
73
- ```bash
74
- ai chat
75
- ```
76
-
77
- 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.
78
-
79
- ### Silent mode (skip explanations)
80
-
81
- ```bash
82
- ai -s list all log files
83
- ```
84
-
85
- Or save the preference:
86
-
87
- ```bash
88
- ai config set SILENT_MODE=true
89
- ```
90
-
91
- ### Provider and model
92
-
93
9
  ```sh
94
- ai config set PROVIDER=openai # or anthropic
95
- ai config set MODEL=gpt-4o-mini # provider-specific model id
10
+ ai config set PROVIDER=openai
11
+ ai config set OPENAI_KEY=<token>
96
12
  ```
97
13
 
98
- Defaults: `gpt-4o-mini` (OpenAI), `claude-sonnet-4-6` (Anthropic).
99
-
100
- LLM calls go through [RubyLLM](https://rubyllm.com/), so chat streaming and model metadata stay provider-agnostic.
14
+ Or `ai config` for the interactive setup. Config: `~/.aicli/config`.
101
15
 
102
- ### Custom OpenAI API endpoint
103
-
104
- Useful for OpenAI-compatible proxies (ignored for Anthropic):
16
+ Anthropic:
105
17
 
106
18
  ```sh
107
- ai config set OPENAI_API_ENDPOINT=<your proxy endpoint>
19
+ ai config set PROVIDER=anthropic
20
+ ai config set ANTHROPIC_KEY=<token>
108
21
  ```
109
22
 
110
- Default: `https://api.openai.com/v1`
111
-
112
- ### Set Language
113
-
114
- | Language | Key |
115
- | ------------------- | ------- |
116
- | English | en |
117
- | Simplified Chinese | zh-Hans |
118
- | Traditional Chinese | zh-Hant |
119
- | Spanish | es |
120
- | Japanese | jp |
121
- | Korean | ko |
122
- | French | fr |
123
- | German | de |
124
- | Russian | ru |
125
- | Ukrainian | uk |
126
- | Vietnamese | vi |
127
- | Arabic | ar |
128
- | Portuguese | pt |
129
- | Turkish | tr |
130
- | Indonesian | id |
131
- | Italian | it |
23
+ Shell history (optional — ↑ recalls the run command, not `ai ...`):
132
24
 
133
25
  ```sh
134
- ai config set LANGUAGE=zh-Hans
26
+ ai config shell
27
+ source ~/.zshrc
135
28
  ```
136
29
 
137
- ### Config UI
30
+ ## Usage
138
31
 
139
- ```bash
140
- 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
141
37
  ```
142
38
 
143
- ### Upgrading
144
-
145
- ```bash
146
- 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
147
43
  ai update
148
- # or: gem update aicli
149
44
  ```
150
45
 
151
46
  ## Development
152
47
 
153
- Run the local checkout (not the installed gem) with Bundler from this directory:
154
-
155
48
  ```sh
156
49
  bundle install
157
-
158
- bundle exec bin/ai --help
159
- bundle exec bin/ai config
160
- bundle exec bin/ai chat
161
50
  bundle exec bin/ai list all log files
162
51
  ```
163
52
 
164
- `bundle exec` loads dependencies from the Gemfile and code from `lib/`. `bin/aicli` is an alias of `bin/ai`.
165
-
166
- Example config for local testing:
167
-
168
- ```sh
169
- bundle exec bin/ai config set PROVIDER=openai
170
- bundle exec bin/ai config set OPENAI_KEY=<your token>
171
- # or: PROVIDER=anthropic + ANTHROPIC_KEY=<your token>
172
- ```
173
-
174
- ## Common Issues
175
-
176
- ### Rate limit / quota errors
177
-
178
- 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`.
179
-
180
- ## Motivation
181
-
182
- I am not a bash wizard, and am dying for access to the copilot CLI, and got impatient.
183
-
184
- ## Credit
53
+ ## License
185
54
 
186
- 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.append(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
@@ -15,27 +15,38 @@ module AiCli
15
15
  return
16
16
  end
17
17
 
18
- if key_values.empty?
19
- puts "#{Helpers::I18n.t('Error')}: #{Helpers::I18n.t('Missing required parameter')} \"key=value\"\n"
20
- exit 1
21
- end
22
-
23
18
  case mode
24
- when 'get'
25
- config = Helpers::Config.get
26
- key_values.each do |key|
27
- if Helpers::Config.has_own?(config, key)
28
- puts "#{key}=#{config[key]}"
29
- else
30
- raise Helpers::KnownError, "#{Helpers::I18n.t('Invalid config property')}: #{key}"
31
- end
19
+ when 'shell'
20
+ result = Helpers::ShellHistory.activate_shell_integration!
21
+ puts pastel.green('✔') + ' ' + Helpers::I18n.t('Shell integration installed.')
22
+ hint = Helpers::ShellHistory.shell_integration_hint(
23
+ activated: result[:activated],
24
+ rc_path: result[:rc_path],
25
+ source_path: result[:source_path]
26
+ )
27
+ puts hint unless hint.empty?
28
+ when 'get', 'set'
29
+ if key_values.empty?
30
+ puts "#{Helpers::I18n.t('Error')}: #{Helpers::I18n.t('Missing required parameter')} \"key=value\"\n"
31
+ exit 1
32
32
  end
33
- when 'set'
34
- pairs = key_values.map do |kv|
35
- k, v = kv.split('=', 2)
36
- [k, v]
33
+
34
+ if mode == 'get'
35
+ config = Helpers::Config.get
36
+ key_values.each do |key|
37
+ if config.key?(key)
38
+ puts "#{key}=#{config[key]}"
39
+ else
40
+ raise Helpers::KnownError, "#{Helpers::I18n.t('Invalid config property')}: #{key}"
41
+ end
42
+ end
43
+ else
44
+ pairs = key_values.map do |kv|
45
+ k, v = kv.split('=', 2)
46
+ [k, v]
47
+ end
48
+ Helpers::Config.set(pairs)
37
49
  end
38
- Helpers::Config.set(pairs)
39
50
  else
40
51
  raise Helpers::KnownError, "#{Helpers::I18n.t('Invalid mode')}: #{mode}"
41
52
  end