n2b 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +90 -0
  3. data/bin/n2b +5 -0
  4. data/lib/n2b/version.rb +4 -0
  5. data/lib/n2b.rb +276 -0
  6. metadata +91 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c3de539be7363cf775fc09b14001ae6775670075543030ba46d1b8c176e3ec7e
4
+ data.tar.gz: c01a0bca5a0df44a9736c78c574d9f9c0ba056012d188c6b8a1386bcad48d6cb
5
+ SHA512:
6
+ metadata.gz: 2c815152121062403a7f5985c3cad091caaaa04f5774f467debae307747072fb293343e7d25fa008aed916e1b2c40c3ef08436af25f21a6b6a9dc0e6d7fe9aae
7
+ data.tar.gz: a144fd904d8c7b37d28eb56e2f6977275fa4107a70da126ea936e727271149bc9146e6ce1103b2dab3cc03ee896186027e220c332d02f1508435f96d8d1530f1
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # N2B: Natural Language to Bash Commands Converter
2
+
3
+ N2B (Natural to Bash) is a Ruby gem that converts natural language instructions into executable shell commands using the Claude AI API. It's designed to help users quickly generate shell commands without needing to remember exact syntax.
4
+
5
+ ## Features
6
+
7
+ - Convert natural language to shell commands
8
+ - Support for multiple Claude AI models (Haiku, Sonnet, Sonnet 3.5)
9
+ - Option to execute generated commands directly
10
+ - Configurable privacy settings
11
+ - Shell history integration
12
+ - Command history tracking for improved context
13
+
14
+ ## Installation
15
+
16
+ Install the gem by running:
17
+ gem install n2b
18
+
19
+ ## Configuration
20
+
21
+ Before using n2b, you need to configure it with your Claude API key and preferences. Run:
22
+ n2b -c
23
+
24
+ This will prompt you to enter:
25
+ - Your Claude API key
26
+ - Preferred Claude model (haiku, sonnet, or sonnet35)
27
+ - Privacy settings (whether to send shell history, past requests, current directory)
28
+ - Whether to append generated commands to your shell history
29
+
30
+ Configuration is stored in `~/.n2b/config.yml`.
31
+
32
+ ## Usage
33
+
34
+ Basic usage:
35
+
36
+ n2b [options] your natural language instruction
37
+
38
+ Options:
39
+ - `-x` or `--execute`: Execute the generated commands after confirmation
40
+ - `-c` or `--config`: Reconfigure the tool
41
+ - `-h` or `--help`: Display help information
42
+
43
+ Examples:
44
+
45
+ 1. Generate commands without executing:
46
+
47
+ n2b list all PDF files in the current directory
48
+
49
+ 2. Generate and execute commands:
50
+
51
+ n2b -x create a new directory named 'project' and initialize a git repository in it
52
+
53
+ 3. Reconfigure the tool:
54
+
55
+ n2b -c
56
+
57
+ ## How It Works
58
+
59
+ 1. N2B takes your natural language input and sends it to the Claude AI API.
60
+ 2. The AI generates appropriate shell commands based on your input and configured shell.
61
+ 3. N2B displays the generated commands and explanations (if any).
62
+ 4. If the execute option is used, N2B will prompt for confirmation before running the commands.
63
+ 5. Optionally, commands are added to your shell history for future reference.
64
+
65
+ ## Privacy
66
+
67
+ N2B allows you to configure what information is sent to the Claude API:
68
+ - Shell history
69
+ - Past n2b requests and responses
70
+ - Current working directory
71
+
72
+ You can adjust these settings during configuration.
73
+
74
+ ## Limitations
75
+
76
+ - The quality of generated commands depends on the Claude AI model's capabilities.
77
+ - Complex or ambiguous instructions might not always produce the desired results.
78
+ - Always review generated commands before execution, especially when using the `-x` option.
79
+
80
+ ## Contributing
81
+
82
+ Contributions are welcome! Please feel free to submit a Pull Request.
83
+
84
+ ## License
85
+
86
+ This project is licensed under the MIT License.
87
+
88
+ ## Support
89
+
90
+ If you encounter any issues or have questions, please file an issue on the GitHub repository.
data/bin/n2b ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "n2b"
4
+
5
+ N2B::CLI.run(ARGV)
@@ -0,0 +1,4 @@
1
+ # lib/n2b/version.rb
2
+ module N2B
3
+ VERSION = "0.1.0"
4
+ end
data/lib/n2b.rb ADDED
@@ -0,0 +1,276 @@
1
+ # lib/n2b.rb
2
+ require "n2b/version"
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+ require 'optparse'
7
+ require 'yaml'
8
+ require 'fileutils'
9
+
10
+ module N2B
11
+ class Error < StandardError; end
12
+
13
+ CONFIG_FILE = File.expand_path('~/.n2b/config.yml')
14
+ HISTORY_FILE = File.expand_path('~/.n2b/history')
15
+ MODELS = { 'haiku' => 'claude-3-haiku-20240307', 'sonnet' => 'claude-3-sonnet-20240229', 'sonnet35' => 'claude-3-5-sonnet-20240620' }
16
+
17
+ class CLI
18
+ def self.run(args)
19
+ new(args).execute
20
+ end
21
+
22
+ def initialize(args)
23
+ @args = args
24
+ @options = parse_options
25
+ end
26
+
27
+ def execute
28
+ config = get_config(reconfigure: @options[:config])
29
+ input_text = @args.join(' ')
30
+ if input_text.empty?
31
+ puts "Enter your natural language command:"
32
+ input_text = $stdin.gets.chomp
33
+ end
34
+
35
+ bash_commands = call_llm(input_text, config)
36
+
37
+ puts "\nTranslated #{get_user_shell} Commands:"
38
+ puts "------------------------"
39
+ puts bash_commands['commands']
40
+ puts "------------------------"
41
+ if bash_commands['explanation']
42
+ puts "Explanation:"
43
+ puts bash_commands['explanation']
44
+ puts "------------------------"
45
+ end
46
+
47
+ if @options[:execute]
48
+ puts "Press Enter to execute these commands, or Ctrl+C to cancel."
49
+ $stdin.gets
50
+ system(bash_commands['commands'].join("\n"))
51
+ else
52
+ add_to_shell_history(bash_commands['commands'].join("\n")) if config['append_to_shell_history']
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def load_config
59
+ if File.exist?(CONFIG_FILE)
60
+ YAML.load_file(CONFIG_FILE)
61
+ else
62
+ { }
63
+ end
64
+ end
65
+
66
+ def get_config( reconfigure: false)
67
+ config = load_config
68
+ api_key = ENV['CLAUDE_API_KEY'] || config['access_key']
69
+ model = config['model'] || 'sonnet35'
70
+ if api_key.nil? || api_key == '' || reconfigure
71
+ print "Enter your Claude API key: #{ api_key.nil? || api_key.empty? ? '' : '(leave blank to keep the current key '+api_key[0..10]+'...)' }"
72
+ api_key = $stdin.gets.chomp
73
+ api_key = config['access_key'] if api_key.empty?
74
+ print "Choose a model (haiku, sonnet, sonnet35 (default)): "
75
+ model = $stdin.gets.chomp
76
+ model = 'sonnet35' if model.empty?
77
+ config['llm'] ||= 'anthropic'
78
+ config['access_key'] = api_key
79
+ config['model'] = model
80
+ unless MODELS.keys.include?(model)
81
+ puts "Invalid model. Choose from: #{MODELS.keys.join(', ')}"
82
+ exit 1
83
+ end
84
+
85
+ config['privacy'] ||= {}
86
+ print "Do you want to send your shell history to Claude? (y/n): "
87
+ config['privacy']['send_shell_history'] = $stdin.gets.chomp == 'y'
88
+ print "Do you want to send your past requests and answers to Claude? (y/n): "
89
+ config['privacy']['send_llm_history'] = $stdin.gets.chomp == 'y'
90
+ print "Do you want to send your current directory to Claude? (y/n): "
91
+ config['privacy']['send_current_directory'] = $stdin.gets.chomp == 'y'
92
+ print "Do you want to append the commands to your shell history? (y/n): "
93
+ config['append_to_shell_history'] = $stdin.gets.chomp == 'y'
94
+
95
+ FileUtils.mkdir_p(File.dirname(CONFIG_FILE)) unless File.exist?(File.dirname(CONFIG_FILE))
96
+ File.open(CONFIG_FILE, 'w+') do |f|
97
+ f.write(config.to_yaml )
98
+ end
99
+ end
100
+
101
+ config
102
+ end
103
+
104
+ def append_to_llm_history_file(commands)
105
+ File.open(HISTORY_FILE, 'a') do |file|
106
+ file.puts(commands)
107
+ end
108
+ end
109
+
110
+ def read_llm_history_file
111
+ history = File.read(HISTORY_FILE) if File.exist?(HISTORY_FILE)
112
+ history || ''
113
+ # limit to 20 most recent commands
114
+ history.split("\n").last(20).join("\n")
115
+ end
116
+
117
+ def call_llm(prompt, config)
118
+ uri = URI.parse('https://api.anthropic.com/v1/messages')
119
+ request = Net::HTTP::Post.new(uri)
120
+ request.content_type = 'application/json'
121
+ request['X-API-Key'] = config['access_key']
122
+ request['anthropic-version'] = '2023-06-01'
123
+ content = <<-EOF
124
+ Translate the following natural language command to bash commands: #{prompt}\n\nProvide only the #{get_user_shell} commands for #{ get_user_os }. the commands should be separated by newlines.
125
+ #{' the user is in directory'+Dir.pwd if config['privacy']['send_current_directory']}.
126
+ #{' the user sent past requests to you and got these answers '+read_llm_history_file if config['privacy']['send_llm_history'] }
127
+ #{ "The user has this history for his shell. "+read_shell_history if config['privacy']['send_shell_history'] }
128
+ he is using #{File.basename(get_user_shell)} shell."
129
+ answer only with a valid json object with the key 'commands' and the value as a list of bash commands plus any additional information you want to provide in explanation.
130
+ { "commands": [ "echo 'Hello, World!'" ], "explanation": "This command prints 'Hello, World!' to the terminal."}
131
+ EOF
132
+
133
+ request.body = JSON.dump({
134
+ "model" => MODELS[config['model']],
135
+ "max_tokens" => 1024,
136
+ "messages" => [
137
+ {
138
+ "role" => "user",
139
+ "content" => content
140
+ }
141
+ ]
142
+ })
143
+
144
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
145
+ http.request(request)
146
+ end
147
+ # check for errors
148
+ if response.code != '200'
149
+ puts "Error: #{response.code} #{response.message}"
150
+ puts response.body
151
+ exit 1
152
+ end
153
+ answer = JSON.parse(response.body)['content'].first['text']
154
+ begin
155
+ # removee everything before the first { and after the last }
156
+ answer = answer.sub(/.*\{(.*)\}.*/m, '{\1}')
157
+ answer = JSON.parse(answer)
158
+ rescue JSON::ParserError
159
+ answer = { 'commands' => answer.split("\n"), explanation: answer}
160
+ end
161
+ append_to_llm_history_file("#{prompt}\n#{answer}")
162
+ answer
163
+ end
164
+
165
+ def get_user_shell
166
+ ENV['SHELL'] || `getent passwd #{ENV['USER']}`.split(':')[6]
167
+ end
168
+
169
+ def get_user_os
170
+ case RbConfig::CONFIG['host_os']
171
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
172
+ :windows
173
+ when /darwin|mac os/
174
+ :macos
175
+ when /linux/
176
+ :linux
177
+ when /solaris|bsd/
178
+ :unix
179
+ else
180
+ :unknown
181
+ end
182
+ end
183
+
184
+ def find_history_file(shell)
185
+ case shell
186
+ when 'zsh'
187
+ [
188
+ ENV['HISTFILE'],
189
+ File.expand_path('~/.zsh_history'),
190
+ File.expand_path('~/.zhistory')
191
+ ].find { |f| f && File.exist?(f) }
192
+ when 'bash'
193
+ [
194
+ ENV['HISTFILE'],
195
+ File.expand_path('~/.bash_history')
196
+ ].find { |f| f && File.exist?(f) }
197
+ else
198
+ nil
199
+ end
200
+ end
201
+
202
+ def read_shell_history()
203
+ shell = File.basename(get_user_shell)
204
+ history_file = find_history_file(shell)
205
+ return '' unless history_file
206
+
207
+ File.read(history_file)
208
+ end
209
+
210
+ def add_to_shell_history(commands)
211
+ shell = File.basename(get_user_shell)
212
+ history_file = find_history_file(shell)
213
+
214
+ unless history_file
215
+ puts "Could not find history file for #{shell}. Cannot add commands to history."
216
+ return
217
+ end
218
+
219
+ case shell
220
+ when 'zsh'
221
+ add_to_zsh_history(commands, history_file)
222
+ when 'bash'
223
+ add_to_bash_history(commands, history_file)
224
+ else
225
+ puts "Unsupported shell: #{shell}. Cannot add commands to history."
226
+ return
227
+ end
228
+
229
+ puts "Commands have been added to your #{shell} history file: #{history_file}"
230
+ puts "You may need to start a new shell session or reload your history to see the changes. #{ shell == 'zsh' ? 'For example, run `fc -R` in your zsh session.' : 'history -r for bash' }"
231
+ puts "Then you can access them using the up arrow key or Ctrl+R for reverse search."
232
+ end
233
+
234
+ def add_to_zsh_history(commands, history_file)
235
+ File.open(history_file, 'a') do |file|
236
+ commands.each_line do |cmd|
237
+ timestamp = Time.now.to_i
238
+ file.puts(": #{timestamp}:0;#{cmd.strip}")
239
+ end
240
+ end
241
+ end
242
+
243
+ def add_to_bash_history(commands, history_file)
244
+ File.open(history_file, 'a') do |file|
245
+ commands.each_line do |cmd|
246
+ file.puts(cmd.strip)
247
+ end
248
+ end
249
+ system("history -r") # Attempt to reload history in current session
250
+ end
251
+
252
+
253
+ def parse_options
254
+ options = { execute: false, config: nil }
255
+
256
+ OptionParser.new do |opts|
257
+ opts.banner = "Usage: n2b [options] [natural language command]"
258
+
259
+ opts.on('-x', '--execute', 'Execute the commands after confirmation') do
260
+ options[:execute] = true
261
+ end
262
+
263
+ opts.on('-h', '--help', 'Print this help') do
264
+ puts opts
265
+ exit
266
+ end
267
+
268
+ opts.on('-c', '--config', 'Configure the API key and model') do
269
+ options[:config] = true
270
+ end
271
+ end.parse!(@args)
272
+
273
+ options
274
+ end
275
+ end
276
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: n2b
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stefan Nothegger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-07-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ description: A tool to convert natural language instructions to bash commands using
56
+ Claude API
57
+ email:
58
+ - stefan@kaproblem.com
59
+ executables:
60
+ - n2b
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - README.md
65
+ - bin/n2b
66
+ - lib/n2b.rb
67
+ - lib/n2b/version.rb
68
+ homepage: https://github.com/stefan-kp/n2b
69
+ licenses:
70
+ - MIT
71
+ metadata: {}
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubygems_version: 3.4.2
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: Convert natural language to bash commands
91
+ test_files: []