n2b 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +90 -0
- data/bin/n2b +5 -0
- data/lib/n2b/version.rb +4 -0
- data/lib/n2b.rb +276 -0
- 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
data/lib/n2b/version.rb
ADDED
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: []
|