n2b 0.2.4 โ 0.3.1
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/README.md +91 -20
- data/lib/n2b/cli.rb +497 -16
- data/lib/n2b/errors.rb +7 -0
- data/lib/n2b/llm/claude.rb +43 -5
- data/lib/n2b/llm/gemini.rb +38 -4
- data/lib/n2b/llm/open_ai.rb +33 -3
- data/lib/n2b/version.rb +1 -1
- data/lib/n2b.rb +2 -2
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 644455363c88bf95c4ab4f21121ed6b8f56a591fb32bd1a8fa9f8f36d981e515
|
4
|
+
data.tar.gz: f899199e26524f38f88e4416217616c048e588c1f54becb228261b07ffcda3d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b3cfcba02a3c41017737317df2d932e1ff053c2d470c529a3158ccba11696ccf09367369f4530de78fc7669cc26795a3dad8abe49a26da0f5a8f836758baf4d
|
7
|
+
data.tar.gz: f664e6b0e9099aecc969e7d46a71b7cf7da0bf1238e9fbf203f445bed9d7073c2a643cce09a3a50fc1ce666805505c7e8d6222817a207fd3b4ec9491c8fcd4e4
|
data/README.md
CHANGED
@@ -6,10 +6,15 @@ N2B (Natural Language to Bash & Ruby) is a Ruby gem that leverages AI to convert
|
|
6
6
|
|
7
7
|
## Features
|
8
8
|
|
9
|
-
- Convert natural language to bash commands
|
10
|
-
- Generate Ruby code from natural language instructions
|
11
|
-
- Analyze
|
12
|
-
-
|
9
|
+
- **๐ค Natural Language to Commands**: Convert natural language to bash commands
|
10
|
+
- **๐ Ruby Code Generation**: Generate Ruby code from natural language instructions
|
11
|
+
- **๐ AI-Powered Diff Analysis**: Analyze git/hg diffs with comprehensive code review
|
12
|
+
- **๐ Requirements Compliance**: Check if code changes meet specified requirements
|
13
|
+
- **๐งช Test Coverage Assessment**: Evaluate test coverage for code changes
|
14
|
+
- **๐ฟ Branch Comparison**: Compare changes against any branch (main/master/default)
|
15
|
+
- **๐ ๏ธ VCS Support**: Full support for both Git and Mercurial repositories
|
16
|
+
- **๐ Errbit Integration**: Analyze Errbit errors and generate detailed reports
|
17
|
+
- **๐ซ Scrum Tickets**: Create formatted Scrum tickets from errors
|
13
18
|
|
14
19
|
## Installation
|
15
20
|
|
@@ -56,29 +61,16 @@ n2rscrum "Create a user authentication system"
|
|
56
61
|
|
57
62
|
## Configuration
|
58
63
|
|
59
|
-
Create a config file at `~/.n2b/config.yml` with your API keys
|
64
|
+
Create a config file at `~/.n2b/config.yml` with your API keys:
|
60
65
|
|
61
|
-
```bash
|
62
|
-
export N2B_CONFIG_FILE=/path/to/your/config.yml
|
63
|
-
```
|
64
|
-
|
65
|
-
Example config file:
|
66
66
|
```yaml
|
67
|
-
llm: claude # or openai
|
67
|
+
llm: claude # or openai
|
68
68
|
claude:
|
69
69
|
key: your-anthropic-api-key
|
70
70
|
model: claude-3-opus-20240229 # or opus, haiku, sonnet
|
71
71
|
openai:
|
72
72
|
key: your-openai-api-key
|
73
73
|
model: gpt-4 # or gpt-3.5-turbo
|
74
|
-
gemini:
|
75
|
-
key: your-google-api-key
|
76
|
-
model: gemini-flash # uses gemini-2.0-flash model
|
77
|
-
```
|
78
|
-
|
79
|
-
You can also set the history file location using the `N2B_HISTORY_FILE` environment variable:
|
80
|
-
```bash
|
81
|
-
export N2B_HISTORY_FILE=/path/to/your/history
|
82
74
|
```
|
83
75
|
|
84
76
|
## Quick Example N2B
|
@@ -148,6 +140,9 @@ n2b [options] your natural language instruction
|
|
148
140
|
|
149
141
|
Options:
|
150
142
|
- `-x` or `--execute`: Execute the generated commands after confirmation
|
143
|
+
- `-d` or `--diff`: Analyze git/hg diff with AI-powered code review
|
144
|
+
- `-b` or `--branch [BRANCH]`: Compare against specific branch (auto-detects main/master/default)
|
145
|
+
- `-r` or `--requirements FILE`: Requirements file for compliance checking
|
151
146
|
- `-c` or `--config`: Reconfigure the tool
|
152
147
|
- `-h` or `--help`: Display help information
|
153
148
|
|
@@ -165,6 +160,82 @@ Examples:
|
|
165
160
|
|
166
161
|
```n2b -c ```
|
167
162
|
|
163
|
+
## ๐ AI-Powered Diff Analysis
|
164
|
+
|
165
|
+
N2B provides comprehensive AI-powered code review for your git and mercurial repositories.
|
166
|
+
|
167
|
+
### Basic Diff Analysis
|
168
|
+
|
169
|
+
```bash
|
170
|
+
# Analyze uncommitted changes
|
171
|
+
n2b --diff
|
172
|
+
|
173
|
+
# Analyze changes against specific branch
|
174
|
+
n2b --diff --branch main
|
175
|
+
n2b --diff --branch feature/auth
|
176
|
+
|
177
|
+
# Auto-detect default branch (main/master/default)
|
178
|
+
n2b --diff --branch
|
179
|
+
|
180
|
+
# Short form
|
181
|
+
n2b -d -b main
|
182
|
+
```
|
183
|
+
|
184
|
+
### Requirements Compliance Checking
|
185
|
+
|
186
|
+
```bash
|
187
|
+
# Check if changes meet requirements
|
188
|
+
n2b --diff --requirements requirements.md
|
189
|
+
n2b -d -r req.md
|
190
|
+
|
191
|
+
# Combine with branch comparison
|
192
|
+
n2b --diff --branch main --requirements requirements.md
|
193
|
+
```
|
194
|
+
|
195
|
+
### What You Get
|
196
|
+
|
197
|
+
The AI analysis provides:
|
198
|
+
|
199
|
+
- **๐ Summary**: Clear overview of what changed
|
200
|
+
- **๐จ Potential Errors**: Bugs, security issues, logic problems with exact file/line references
|
201
|
+
- **๐ก Suggested Improvements**: Code quality, performance, style recommendations
|
202
|
+
- **๐งช Test Coverage Assessment**: Evaluation of test completeness and quality
|
203
|
+
- **๐ Requirements Evaluation**: Compliance check with clear status indicators:
|
204
|
+
- โ
**IMPLEMENTED**: Requirement fully satisfied
|
205
|
+
- โ ๏ธ **PARTIALLY IMPLEMENTED**: Needs more work
|
206
|
+
- โ **NOT IMPLEMENTED**: Not addressed
|
207
|
+
- ๐ **UNCLEAR**: Cannot determine from diff
|
208
|
+
|
209
|
+
### Example Output
|
210
|
+
|
211
|
+
```
|
212
|
+
Code Diff Analysis:
|
213
|
+
-------------------
|
214
|
+
Summary:
|
215
|
+
Added user authentication with JWT tokens and password validation.
|
216
|
+
|
217
|
+
Potential Errors:
|
218
|
+
- lib/auth.rb line 42: Password validation allows weak passwords
|
219
|
+
- controllers/auth_controller.rb lines 15-20: Missing rate limiting for login attempts
|
220
|
+
|
221
|
+
Suggested Improvements:
|
222
|
+
- lib/auth.rb line 30: Consider using bcrypt for password hashing
|
223
|
+
- spec/auth_spec.rb: Add tests for edge cases and security scenarios
|
224
|
+
|
225
|
+
Test Coverage Assessment:
|
226
|
+
Good: Basic authentication flow is tested. Missing: No tests for password validation edge cases, JWT expiration handling, or security attack scenarios.
|
227
|
+
|
228
|
+
Requirements Evaluation:
|
229
|
+
โ
IMPLEMENTED: User login/logout functionality fully working
|
230
|
+
โ ๏ธ PARTIALLY IMPLEMENTED: Password strength requirements present but not comprehensive
|
231
|
+
โ NOT IMPLEMENTED: Two-factor authentication not addressed in this diff
|
232
|
+
-------------------
|
233
|
+
```
|
234
|
+
|
235
|
+
### Supported Version Control Systems
|
236
|
+
|
237
|
+
- **Git**: Full support with auto-detection of main/master branches
|
238
|
+
- **Mercurial (hg)**: Full support with auto-detection of default branch
|
168
239
|
|
169
240
|
n2r in ruby or rails console
|
170
241
|
n2r "your question", files:['file1.rb', 'file2.rb'], exception: AnError
|
@@ -253,4 +324,4 @@ The generated tickets include:
|
|
253
324
|
- Acceptance criteria
|
254
325
|
- Story point estimate
|
255
326
|
- Priority level
|
256
|
-
- Reference to the original Errbit URL
|
327
|
+
- Reference to the original Errbit URL# Test change
|
data/lib/n2b/cli.rb
CHANGED
@@ -11,12 +11,191 @@ module N2B
|
|
11
11
|
|
12
12
|
def execute
|
13
13
|
config = get_config(reconfigure: @options[:config])
|
14
|
-
|
15
|
-
|
14
|
+
user_input = @args.join(' ') # All remaining args form user input/prompt addition
|
15
|
+
|
16
|
+
if @options[:diff]
|
17
|
+
handle_diff_analysis(config)
|
18
|
+
elsif user_input.empty? # No input text after options
|
16
19
|
puts "Enter your natural language command:"
|
17
20
|
input_text = $stdin.gets.chomp
|
21
|
+
process_natural_language_command(input_text, config)
|
22
|
+
else # Natural language command
|
23
|
+
process_natural_language_command(user_input, config)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def handle_diff_analysis(config)
|
30
|
+
vcs_type = get_vcs_type
|
31
|
+
if vcs_type == :none
|
32
|
+
puts "Error: Not a git or hg repository."
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get requirements file from parsed options
|
37
|
+
requirements_filepath = @options[:requirements]
|
38
|
+
user_prompt_addition = @args.join(' ') # All remaining args are user prompt addition
|
39
|
+
|
40
|
+
requirements_content = nil
|
41
|
+
if requirements_filepath
|
42
|
+
unless File.exist?(requirements_filepath)
|
43
|
+
puts "Error: Requirements file not found: #{requirements_filepath}"
|
44
|
+
exit 1
|
45
|
+
end
|
46
|
+
requirements_content = File.read(requirements_filepath)
|
47
|
+
end
|
48
|
+
|
49
|
+
diff_output = execute_vcs_diff(vcs_type, @options[:branch])
|
50
|
+
analyze_diff(diff_output, config, user_prompt_addition, requirements_content)
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_vcs_type
|
54
|
+
if Dir.exist?(File.join(Dir.pwd, '.git'))
|
55
|
+
:git
|
56
|
+
elsif Dir.exist?(File.join(Dir.pwd, '.hg'))
|
57
|
+
:hg
|
58
|
+
else
|
59
|
+
:none
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def execute_vcs_diff(vcs_type, branch_option = nil)
|
64
|
+
case vcs_type
|
65
|
+
when :git
|
66
|
+
if branch_option
|
67
|
+
target_branch = branch_option == 'auto' ? detect_git_default_branch : branch_option
|
68
|
+
if target_branch
|
69
|
+
# Validate that the target branch exists
|
70
|
+
unless validate_git_branch_exists(target_branch)
|
71
|
+
puts "Error: Branch '#{target_branch}' does not exist."
|
72
|
+
puts "Available branches:"
|
73
|
+
puts `git branch -a`.lines.map(&:strip).reject(&:empty?)
|
74
|
+
exit 1
|
75
|
+
end
|
76
|
+
|
77
|
+
puts "Comparing current branch against '#{target_branch}'..."
|
78
|
+
`git diff #{target_branch}...HEAD`
|
79
|
+
else
|
80
|
+
puts "Could not detect default branch, falling back to HEAD diff..."
|
81
|
+
`git diff HEAD`
|
82
|
+
end
|
83
|
+
else
|
84
|
+
`git diff HEAD`
|
85
|
+
end
|
86
|
+
when :hg
|
87
|
+
if branch_option
|
88
|
+
target_branch = branch_option == 'auto' ? detect_hg_default_branch : branch_option
|
89
|
+
if target_branch
|
90
|
+
# Validate that the target branch exists
|
91
|
+
unless validate_hg_branch_exists(target_branch)
|
92
|
+
puts "Error: Branch '#{target_branch}' does not exist."
|
93
|
+
puts "Available branches:"
|
94
|
+
puts `hg branches`.lines.map(&:strip).reject(&:empty?)
|
95
|
+
exit 1
|
96
|
+
end
|
97
|
+
|
98
|
+
puts "Comparing current branch against '#{target_branch}'..."
|
99
|
+
`hg diff -r #{target_branch}`
|
100
|
+
else
|
101
|
+
puts "Could not detect default branch, falling back to standard diff..."
|
102
|
+
`hg diff`
|
103
|
+
end
|
104
|
+
else
|
105
|
+
`hg diff`
|
106
|
+
end
|
107
|
+
else
|
108
|
+
"" # Should not happen if get_vcs_type logic is correct and checked before calling
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def detect_git_default_branch
|
113
|
+
# Try multiple methods to detect the default branch
|
114
|
+
|
115
|
+
# Method 1: Check origin/HEAD symbolic ref
|
116
|
+
result = `git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null`.strip
|
117
|
+
if $?.success? && !result.empty?
|
118
|
+
return result.split('/').last
|
119
|
+
end
|
120
|
+
|
121
|
+
# Method 2: Check remote show origin
|
122
|
+
result = `git remote show origin 2>/dev/null | grep "HEAD branch"`.strip
|
123
|
+
if $?.success? && !result.empty?
|
124
|
+
match = result.match(/HEAD branch:\s*(\w+)/)
|
125
|
+
return match[1] if match
|
126
|
+
end
|
127
|
+
|
128
|
+
# Method 3: Check if common default branches exist
|
129
|
+
['main', 'master'].each do |branch|
|
130
|
+
result = `git rev-parse --verify origin/#{branch} 2>/dev/null`
|
131
|
+
if $?.success?
|
132
|
+
return branch
|
133
|
+
end
|
18
134
|
end
|
19
135
|
|
136
|
+
# Method 4: Fallback - check local branches
|
137
|
+
['main', 'master'].each do |branch|
|
138
|
+
result = `git rev-parse --verify #{branch} 2>/dev/null`
|
139
|
+
if $?.success?
|
140
|
+
return branch
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# If all else fails, return nil
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
def detect_hg_default_branch
|
149
|
+
# Method 1: Check current branch (if it's 'default', that's the main branch)
|
150
|
+
result = `hg branch 2>/dev/null`.strip
|
151
|
+
if $?.success? && result == 'default'
|
152
|
+
return 'default'
|
153
|
+
end
|
154
|
+
|
155
|
+
# Method 2: Look for 'default' branch in branch list
|
156
|
+
result = `hg branches 2>/dev/null`
|
157
|
+
if $?.success? && result.include?('default')
|
158
|
+
return 'default'
|
159
|
+
end
|
160
|
+
|
161
|
+
# Method 3: Check if there are any branches at all
|
162
|
+
result = `hg branches 2>/dev/null`.strip
|
163
|
+
if $?.success? && !result.empty?
|
164
|
+
# Get the first branch (usually the main one)
|
165
|
+
first_branch = result.lines.first&.split&.first
|
166
|
+
return first_branch if first_branch
|
167
|
+
end
|
168
|
+
|
169
|
+
# Fallback to 'default' (standard hg main branch name)
|
170
|
+
'default'
|
171
|
+
end
|
172
|
+
|
173
|
+
def validate_git_branch_exists(branch)
|
174
|
+
# Check if branch exists locally
|
175
|
+
result = `git rev-parse --verify #{branch} 2>/dev/null`
|
176
|
+
return true if $?.success?
|
177
|
+
|
178
|
+
# Check if branch exists on remote
|
179
|
+
result = `git rev-parse --verify origin/#{branch} 2>/dev/null`
|
180
|
+
return true if $?.success?
|
181
|
+
|
182
|
+
false
|
183
|
+
end
|
184
|
+
|
185
|
+
def validate_hg_branch_exists(branch)
|
186
|
+
# Check if branch exists in hg branches
|
187
|
+
result = `hg branches 2>/dev/null`
|
188
|
+
if $?.success?
|
189
|
+
return result.lines.any? { |line| line.strip.start_with?(branch) }
|
190
|
+
end
|
191
|
+
|
192
|
+
# If we can't list branches, assume it exists (hg is more permissive)
|
193
|
+
true
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
def process_natural_language_command(input_text, config)
|
20
199
|
bash_commands = call_llm(input_text, config)
|
21
200
|
|
22
201
|
puts "\nTranslated #{get_user_shell} Commands:"
|
@@ -24,10 +203,10 @@ module N2B
|
|
24
203
|
puts bash_commands['commands']
|
25
204
|
puts "------------------------"
|
26
205
|
if bash_commands['explanation']
|
27
|
-
puts "Explanation:"
|
206
|
+
puts "Explanation:"
|
28
207
|
puts bash_commands['explanation']
|
29
208
|
puts "------------------------"
|
30
|
-
end
|
209
|
+
end
|
31
210
|
|
32
211
|
if @options[:execute]
|
33
212
|
puts "Press Enter to execute these commands, or Ctrl+C to cancel."
|
@@ -38,9 +217,258 @@ module N2B
|
|
38
217
|
end
|
39
218
|
end
|
40
219
|
|
41
|
-
|
220
|
+
def build_diff_analysis_prompt(diff_output, user_prompt_addition = "", requirements_content = nil)
|
221
|
+
default_system_prompt = <<-SYSTEM_PROMPT.strip
|
222
|
+
You are a senior software developer reviewing a code diff.
|
223
|
+
Your task is to provide a constructive and detailed analysis of the changes.
|
224
|
+
Focus on identifying potential bugs, suggesting improvements in code quality, style, performance, and security.
|
225
|
+
Also, provide a concise summary of the changes.
|
42
226
|
|
43
|
-
|
227
|
+
IMPORTANT: When referring to specific issues or improvements, always include:
|
228
|
+
- The exact file path (e.g., "lib/n2b/cli.rb")
|
229
|
+
- The specific line numbers or line ranges (e.g., "line 42" or "lines 15-20")
|
230
|
+
- The exact code snippet you're referring to when possible
|
231
|
+
|
232
|
+
This helps users quickly locate and understand the issues you identify.
|
233
|
+
|
234
|
+
SPECIAL FOCUS ON TEST COVERAGE:
|
235
|
+
Pay special attention to whether the developer has provided adequate test coverage for the changes:
|
236
|
+
- Look for new test files or modifications to existing test files
|
237
|
+
- Check if new functionality has corresponding tests
|
238
|
+
- Evaluate if edge cases and error conditions are tested
|
239
|
+
- Assess if the tests are meaningful and comprehensive
|
240
|
+
- Note any missing test coverage that should be added
|
241
|
+
|
242
|
+
NOTE: In addition to the diff, you will also receive the current code context around the changed areas.
|
243
|
+
This provides better understanding of the surrounding code and helps with more accurate analysis.
|
244
|
+
The user may provide additional instructions or specific requirements below.
|
245
|
+
SYSTEM_PROMPT
|
246
|
+
|
247
|
+
user_instructions_section = ""
|
248
|
+
unless user_prompt_addition.to_s.strip.empty?
|
249
|
+
user_instructions_section = "User Instructions:\n#{user_prompt_addition.strip}\n\n"
|
250
|
+
end
|
251
|
+
|
252
|
+
requirements_section = ""
|
253
|
+
if requirements_content && !requirements_content.to_s.strip.empty?
|
254
|
+
requirements_section = <<-REQUIREMENTS_BLOCK
|
255
|
+
CRITICAL REQUIREMENTS EVALUATION:
|
256
|
+
You must carefully evaluate whether the code changes meet the following requirements from the ticket/task.
|
257
|
+
For each requirement, explicitly state whether it is:
|
258
|
+
- โ
IMPLEMENTED: The requirement is fully satisfied by the changes
|
259
|
+
- โ ๏ธ PARTIALLY IMPLEMENTED: The requirement is partially addressed but needs more work
|
260
|
+
- โ NOT IMPLEMENTED: The requirement is not addressed by these changes
|
261
|
+
- ๐ UNCLEAR: Cannot determine from the diff whether the requirement is met
|
262
|
+
|
263
|
+
--- BEGIN REQUIREMENTS ---
|
264
|
+
#{requirements_content.strip}
|
265
|
+
--- END REQUIREMENTS ---
|
266
|
+
|
267
|
+
REQUIREMENTS_BLOCK
|
268
|
+
end
|
269
|
+
|
270
|
+
analysis_intro = "Analyze the following diff based on the general instructions above and these specific requirements (if any):"
|
271
|
+
|
272
|
+
# Extract context around changed lines
|
273
|
+
context_sections = extract_code_context_from_diff(diff_output)
|
274
|
+
context_info = ""
|
275
|
+
unless context_sections.empty?
|
276
|
+
context_info = "\n\nCurrent Code Context (for better analysis):\n"
|
277
|
+
context_sections.each do |file_path, sections|
|
278
|
+
context_info += "\n--- #{file_path} ---\n"
|
279
|
+
sections.each do |section|
|
280
|
+
context_info += "Lines #{section[:start_line]}-#{section[:end_line]}:\n"
|
281
|
+
context_info += "```\n#{section[:content]}\n```\n\n"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
json_instruction = <<-JSON_INSTRUCTION.strip
|
287
|
+
CRITICAL: Return ONLY a valid JSON object with the keys "summary", "errors" (as a list of strings), "improvements" (as a list of strings), "test_coverage" (as a string), and "requirements_evaluation" (as a string, only if requirements were provided).
|
288
|
+
Do not include any explanatory text before or after the JSON.
|
289
|
+
Each error and improvement should include specific file paths and line numbers.
|
290
|
+
|
291
|
+
Example format:
|
292
|
+
{
|
293
|
+
"summary": "Brief description of the changes",
|
294
|
+
"errors": [
|
295
|
+
"lib/example.rb line 42: Potential null pointer exception when accessing user.name without checking if user is nil",
|
296
|
+
"src/main.js lines 15-20: Missing error handling for async operation"
|
297
|
+
],
|
298
|
+
"improvements": [
|
299
|
+
"lib/example.rb line 30: Consider using a constant for the magic number 42",
|
300
|
+
"src/utils.py lines 5-10: This method could be simplified using list comprehension"
|
301
|
+
],
|
302
|
+
"test_coverage": "Good: New functionality in lib/example.rb has corresponding tests in test/example_test.rb. Missing: No tests for error handling edge cases in the new validation method.",
|
303
|
+
"requirements_evaluation": "โ
IMPLEMENTED: User authentication feature is fully implemented in auth.rb. โ ๏ธ PARTIALLY IMPLEMENTED: Error handling is present but lacks specific error codes. โ NOT IMPLEMENTED: Email notifications are not addressed in this diff."
|
304
|
+
}
|
305
|
+
JSON_INSTRUCTION
|
306
|
+
|
307
|
+
full_prompt = [
|
308
|
+
default_system_prompt,
|
309
|
+
user_instructions_section,
|
310
|
+
requirements_section,
|
311
|
+
analysis_intro,
|
312
|
+
"Diff:\n```\n#{diff_output}\n```",
|
313
|
+
context_info,
|
314
|
+
json_instruction
|
315
|
+
].select { |s| s && !s.empty? }.join("\n\n") # Join non-empty sections with double newlines
|
316
|
+
|
317
|
+
full_prompt
|
318
|
+
end
|
319
|
+
|
320
|
+
def analyze_diff(diff_output, config, user_prompt_addition = "", requirements_content = nil)
|
321
|
+
prompt = build_diff_analysis_prompt(diff_output, user_prompt_addition, requirements_content)
|
322
|
+
analysis_json_str = call_llm_for_diff_analysis(prompt, config)
|
323
|
+
|
324
|
+
begin
|
325
|
+
# Try to extract JSON from response that might have text before it
|
326
|
+
json_content = extract_json_from_response(analysis_json_str)
|
327
|
+
analysis_result = JSON.parse(json_content)
|
328
|
+
|
329
|
+
puts "\nCode Diff Analysis:"
|
330
|
+
puts "-------------------"
|
331
|
+
puts "Summary:"
|
332
|
+
puts analysis_result['summary'] || "No summary provided."
|
333
|
+
puts "\nPotential Errors:"
|
334
|
+
errors_list = analysis_result['errors']
|
335
|
+
errors_list = [errors_list] if errors_list.is_a?(String) && !errors_list.empty?
|
336
|
+
errors_list = [] if errors_list.nil? || (errors_list.is_a?(String) && errors_list.empty?)
|
337
|
+
puts errors_list.any? ? errors_list.map{|err| "- #{err}"}.join("\n") : "No errors identified."
|
338
|
+
|
339
|
+
puts "\nSuggested Improvements:"
|
340
|
+
improvements_list = analysis_result['improvements']
|
341
|
+
improvements_list = [improvements_list] if improvements_list.is_a?(String) && !improvements_list.empty?
|
342
|
+
improvements_list = [] if improvements_list.nil? || (improvements_list.is_a?(String) && improvements_list.empty?)
|
343
|
+
puts improvements_list.any? ? improvements_list.map{|imp| "- #{imp}"}.join("\n") : "No improvements suggested."
|
344
|
+
|
345
|
+
puts "\nTest Coverage Assessment:"
|
346
|
+
test_coverage = analysis_result['test_coverage']
|
347
|
+
puts test_coverage && !test_coverage.to_s.strip.empty? ? test_coverage : "No test coverage assessment provided."
|
348
|
+
|
349
|
+
# Show requirements evaluation if requirements were provided
|
350
|
+
requirements_eval = analysis_result['requirements_evaluation']
|
351
|
+
if requirements_eval && !requirements_eval.to_s.strip.empty?
|
352
|
+
puts "\nRequirements Evaluation:"
|
353
|
+
puts requirements_eval
|
354
|
+
end
|
355
|
+
puts "-------------------"
|
356
|
+
rescue JSON::ParserError => e # Handles cases where the JSON string (even fallback) is malformed
|
357
|
+
puts "Critical Error: Failed to parse JSON response for diff analysis: #{e.message}"
|
358
|
+
puts "Raw response was: #{analysis_json_str}"
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def extract_json_from_response(response)
|
363
|
+
# First try to parse the response as-is
|
364
|
+
begin
|
365
|
+
JSON.parse(response)
|
366
|
+
return response
|
367
|
+
rescue JSON::ParserError
|
368
|
+
# If that fails, try to find JSON within the response
|
369
|
+
end
|
370
|
+
|
371
|
+
# Look for JSON object starting with { and ending with }
|
372
|
+
json_start = response.index('{')
|
373
|
+
return response unless json_start
|
374
|
+
|
375
|
+
# Find the matching closing brace
|
376
|
+
brace_count = 0
|
377
|
+
json_end = nil
|
378
|
+
(json_start...response.length).each do |i|
|
379
|
+
case response[i]
|
380
|
+
when '{'
|
381
|
+
brace_count += 1
|
382
|
+
when '}'
|
383
|
+
brace_count -= 1
|
384
|
+
if brace_count == 0
|
385
|
+
json_end = i
|
386
|
+
break
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
return response unless json_end
|
392
|
+
|
393
|
+
response[json_start..json_end]
|
394
|
+
end
|
395
|
+
|
396
|
+
def extract_code_context_from_diff(diff_output)
|
397
|
+
context_sections = {}
|
398
|
+
current_file = nil
|
399
|
+
|
400
|
+
diff_output.each_line do |line|
|
401
|
+
line = line.chomp
|
402
|
+
|
403
|
+
# Parse file headers (e.g., "diff --git a/lib/n2b/cli.rb b/lib/n2b/cli.rb")
|
404
|
+
if line.start_with?('diff --git')
|
405
|
+
# Extract file path from "diff --git a/path b/path"
|
406
|
+
match = line.match(/diff --git a\/(.+) b\/(.+)/)
|
407
|
+
current_file = match[2] if match # Use the "b/" path (new file)
|
408
|
+
elsif line.start_with?('+++')
|
409
|
+
# Alternative way to get file path from "+++ b/path"
|
410
|
+
match = line.match(/\+\+\+ b\/(.+)/)
|
411
|
+
current_file = match[1] if match
|
412
|
+
elsif line.start_with?('@@') && current_file
|
413
|
+
# Parse hunk header (e.g., "@@ -10,7 +10,8 @@")
|
414
|
+
match = line.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/)
|
415
|
+
if match
|
416
|
+
old_start = match[1].to_i
|
417
|
+
new_start = match[2].to_i
|
418
|
+
|
419
|
+
# Use the new file line numbers for context
|
420
|
+
context_start = [new_start - 5, 1].max # 5 lines before, but not less than 1
|
421
|
+
context_end = new_start + 10 # 10 lines after the start
|
422
|
+
|
423
|
+
# Read the actual file content
|
424
|
+
if File.exist?(current_file)
|
425
|
+
file_lines = File.readlines(current_file)
|
426
|
+
# Adjust end to not exceed file length
|
427
|
+
context_end = [context_end, file_lines.length].min
|
428
|
+
|
429
|
+
if context_start <= file_lines.length
|
430
|
+
context_content = file_lines[(context_start-1)...context_end].map.with_index do |content, idx|
|
431
|
+
line_num = context_start + idx
|
432
|
+
"#{line_num.to_s.rjust(4)}: #{content.rstrip}"
|
433
|
+
end.join("\n")
|
434
|
+
|
435
|
+
context_sections[current_file] ||= []
|
436
|
+
context_sections[current_file] << {
|
437
|
+
start_line: context_start,
|
438
|
+
end_line: context_end,
|
439
|
+
content: context_content
|
440
|
+
}
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
context_sections
|
448
|
+
end
|
449
|
+
|
450
|
+
def call_llm_for_diff_analysis(prompt, config)
|
451
|
+
begin
|
452
|
+
llm_service_name = config['llm']
|
453
|
+
llm = case llm_service_name
|
454
|
+
when 'openai'
|
455
|
+
N2M::Llm::OpenAi.new(config)
|
456
|
+
when 'claude'
|
457
|
+
N2M::Llm::Claude.new(config)
|
458
|
+
when 'gemini'
|
459
|
+
N2M::Llm::Gemini.new(config)
|
460
|
+
else
|
461
|
+
# Should not happen if config is validated, but as a safeguard:
|
462
|
+
raise N2B::Error, "Unsupported LLM service: #{llm_service_name}"
|
463
|
+
end
|
464
|
+
|
465
|
+
response_json_str = llm.analyze_code_diff(prompt) # Call the new dedicated method
|
466
|
+
response_json_str
|
467
|
+
rescue N2B::LlmApiError => e # This catches errors from analyze_code_diff
|
468
|
+
puts "Error communicating with the LLM: #{e.message}"
|
469
|
+
return '{"summary": "Error: Could not analyze diff due to LLM API error.", "errors": [], "improvements": []}'
|
470
|
+
end
|
471
|
+
end
|
44
472
|
|
45
473
|
def append_to_llm_history_file(commands)
|
46
474
|
File.open(HISTORY_FILE, 'a') do |file|
|
@@ -56,10 +484,11 @@ module N2B
|
|
56
484
|
end
|
57
485
|
|
58
486
|
def call_llm(prompt, config)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
487
|
+
begin # Added begin for LlmApiError rescue
|
488
|
+
llm = config['llm'] == 'openai' ? N2M::Llm::OpenAi.new(config) : N2M::Llm::Claude.new(config)
|
489
|
+
|
490
|
+
# This content is specific to bash command generation
|
491
|
+
content = <<-EOF
|
63
492
|
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.
|
64
493
|
#{' the user is in directory'+Dir.pwd if config['privacy']['send_current_directory']}.
|
65
494
|
#{' the user sent past requests to you and got these answers '+read_llm_history_file if config['privacy']['send_llm_history'] }
|
@@ -70,10 +499,33 @@ module N2B
|
|
70
499
|
EOF
|
71
500
|
|
72
501
|
|
73
|
-
|
502
|
+
response_json_str = llm.make_request(content)
|
74
503
|
|
75
|
-
append_to_llm_history_file("#{prompt}\n#{
|
76
|
-
|
504
|
+
append_to_llm_history_file("#{prompt}\n#{response_json_str}") # Storing the raw JSON string
|
505
|
+
# The original call_llm was expected to return a hash after JSON.parse,
|
506
|
+
# but it was actually returning the string. Let's assume it should return a parsed Hash.
|
507
|
+
# However, the calling method `process_natural_language_command` accesses it like `bash_commands['commands']`
|
508
|
+
# which implies it expects a Hash. Let's ensure call_llm returns a Hash.
|
509
|
+
# This internal JSON parsing is for the *content* of a successful LLM response.
|
510
|
+
# The LlmApiError for network/auth issues should be caught before this.
|
511
|
+
begin
|
512
|
+
# Check if response_json_str is already a Hash (parsed JSON)
|
513
|
+
if response_json_str.is_a?(Hash)
|
514
|
+
response_json_str
|
515
|
+
else
|
516
|
+
parsed_response = JSON.parse(response_json_str)
|
517
|
+
parsed_response
|
518
|
+
end
|
519
|
+
rescue JSON::ParserError => e
|
520
|
+
puts "Error parsing LLM response JSON for command generation: #{e.message}"
|
521
|
+
# This is a fallback for when the LLM response *content* is not valid JSON.
|
522
|
+
{ "commands" => ["echo 'Error: LLM returned invalid JSON content.'"], "explanation" => "The response from the language model was not valid JSON." }
|
523
|
+
end
|
524
|
+
rescue N2B::LlmApiError => e
|
525
|
+
puts "Error communicating with the LLM: #{e.message}"
|
526
|
+
# This is the fallback for LlmApiError (network, auth, etc.)
|
527
|
+
{ "commands" => ["echo 'LLM API error occurred. Please check your configuration and network.'"], "explanation" => "Failed to connect to the LLM." }
|
528
|
+
end
|
77
529
|
end
|
78
530
|
|
79
531
|
def get_user_shell
|
@@ -165,15 +617,27 @@ module N2B
|
|
165
617
|
|
166
618
|
|
167
619
|
def parse_options
|
168
|
-
options = { execute: false, config: nil }
|
620
|
+
options = { execute: false, config: nil, diff: false, requirements: nil, branch: nil }
|
169
621
|
|
170
|
-
OptionParser.new do |opts|
|
622
|
+
parser = OptionParser.new do |opts|
|
171
623
|
opts.banner = "Usage: n2b [options] [natural language command]"
|
172
624
|
|
173
625
|
opts.on('-x', '--execute', 'Execute the commands after confirmation') do
|
174
626
|
options[:execute] = true
|
175
627
|
end
|
176
628
|
|
629
|
+
opts.on('-d', '--diff', 'Analyze git/hg diff with AI') do
|
630
|
+
options[:diff] = true
|
631
|
+
end
|
632
|
+
|
633
|
+
opts.on('-b', '--branch [BRANCH]', 'Compare against branch (default: auto-detect main/master)') do |branch|
|
634
|
+
options[:branch] = branch || 'auto'
|
635
|
+
end
|
636
|
+
|
637
|
+
opts.on('-r', '--requirements FILE', 'Requirements file for diff analysis') do |file|
|
638
|
+
options[:requirements] = file
|
639
|
+
end
|
640
|
+
|
177
641
|
opts.on('-h', '--help', 'Print this help') do
|
178
642
|
puts opts
|
179
643
|
exit
|
@@ -182,7 +646,24 @@ module N2B
|
|
182
646
|
opts.on('-c', '--config', 'Configure the API key and model') do
|
183
647
|
options[:config] = true
|
184
648
|
end
|
185
|
-
end
|
649
|
+
end
|
650
|
+
|
651
|
+
begin
|
652
|
+
parser.parse!(@args)
|
653
|
+
rescue OptionParser::InvalidOption => e
|
654
|
+
puts "Error: #{e.message}"
|
655
|
+
puts ""
|
656
|
+
puts parser.help
|
657
|
+
exit 1
|
658
|
+
end
|
659
|
+
|
660
|
+
# Validate option combinations
|
661
|
+
if options[:branch] && !options[:diff]
|
662
|
+
puts "Error: --branch option can only be used with --diff"
|
663
|
+
puts ""
|
664
|
+
puts parser.help
|
665
|
+
exit 1
|
666
|
+
end
|
186
667
|
|
187
668
|
options
|
188
669
|
end
|
data/lib/n2b/errors.rb
ADDED
data/lib/n2b/llm/claude.rb
CHANGED
@@ -31,12 +31,12 @@ module N2M
|
|
31
31
|
end
|
32
32
|
# check for errors
|
33
33
|
if response.code != '200'
|
34
|
-
|
35
|
-
puts response.body
|
36
|
-
exit 1
|
34
|
+
raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
|
37
35
|
end
|
38
36
|
answer = JSON.parse(response.body)['content'].first['text']
|
39
37
|
begin
|
38
|
+
# The llm_response.json file is likely for debugging and can be kept or removed.
|
39
|
+
# For this refactoring, I'll keep it as it doesn't affect the error handling logic.
|
40
40
|
File.open('llm_response.json', 'w') do |f|
|
41
41
|
f.write(answer)
|
42
42
|
end
|
@@ -46,16 +46,54 @@ module N2M
|
|
46
46
|
# gsub all \n with \\n that are inside "
|
47
47
|
#
|
48
48
|
answer.gsub!(/"([^"]*)"/) { |match| match.gsub(/\n/, "\\n") }
|
49
|
+
# The llm_response.json file is likely for debugging and can be kept or removed.
|
49
50
|
File.open('llm_response.json', 'w') do |f|
|
50
51
|
f.write(answer)
|
51
52
|
end
|
52
53
|
answer = JSON.parse(answer)
|
53
54
|
rescue JSON::ParserError
|
54
|
-
|
55
|
-
|
55
|
+
# This specific JSON parsing error is about the LLM's *response content*, not an API error.
|
56
|
+
# It should probably be handled differently, but the subtask is about LlmApiError.
|
57
|
+
# For now, keeping existing behavior for this part.
|
58
|
+
puts "Error parsing JSON from LLM response: #{answer}" # Clarified error message
|
59
|
+
answer = { 'explanation' => answer} # Default fallback
|
56
60
|
end
|
57
61
|
answer
|
58
62
|
end
|
63
|
+
|
64
|
+
def analyze_code_diff(prompt_content)
|
65
|
+
# This method assumes prompt_content is the full, ready-to-send prompt
|
66
|
+
# including all instructions for the LLM (system message, diff, user additions, JSON format).
|
67
|
+
uri = URI.parse('https://api.anthropic.com/v1/messages')
|
68
|
+
request = Net::HTTP::Post.new(uri)
|
69
|
+
request.content_type = 'application/json'
|
70
|
+
request['X-API-Key'] = @config['access_key']
|
71
|
+
request['anthropic-version'] = '2023-06-01'
|
72
|
+
|
73
|
+
request.body = JSON.dump({
|
74
|
+
"model" => MODELS[@config['model']],
|
75
|
+
"max_tokens" => @config['max_tokens'] || 1024, # Allow overriding max_tokens from config
|
76
|
+
"messages" => [
|
77
|
+
{
|
78
|
+
"role" => "user", # The entire prompt is passed as a single user message
|
79
|
+
"content" => prompt_content
|
80
|
+
}
|
81
|
+
]
|
82
|
+
})
|
83
|
+
|
84
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
85
|
+
http.request(request)
|
86
|
+
end
|
87
|
+
|
88
|
+
if response.code != '200'
|
89
|
+
raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return the raw JSON string. CLI's call_llm_for_diff_analysis will handle parsing.
|
93
|
+
# The Claude API for messages returns the analysis in response.body['content'].first['text']
|
94
|
+
# which should itself be a JSON string as per our prompt's instructions.
|
95
|
+
JSON.parse(response.body)['content'].first['text']
|
96
|
+
end
|
59
97
|
end
|
60
98
|
end
|
61
99
|
end
|
data/lib/n2b/llm/gemini.rb
CHANGED
@@ -35,10 +35,7 @@ module N2M
|
|
35
35
|
|
36
36
|
# check for errors
|
37
37
|
if response.code != '200'
|
38
|
-
|
39
|
-
puts response.body
|
40
|
-
puts "Config: #{@config.inspect}"
|
41
|
-
exit 1
|
38
|
+
raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
|
42
39
|
end
|
43
40
|
|
44
41
|
parsed_response = JSON.parse(response.body)
|
@@ -64,6 +61,43 @@ module N2M
|
|
64
61
|
end
|
65
62
|
answer
|
66
63
|
end
|
64
|
+
|
65
|
+
def analyze_code_diff(prompt_content)
|
66
|
+
# This method assumes prompt_content is the full, ready-to-send prompt
|
67
|
+
# including all instructions for the LLM (system message, diff, user additions, JSON format).
|
68
|
+
model = MODELS[@config['model']] || 'gemini-flash' # Or a specific model for analysis if different
|
69
|
+
uri = URI.parse("#{API_URI}/#{model}:generateContent?key=#{@config['access_key']}")
|
70
|
+
|
71
|
+
request = Net::HTTP::Post.new(uri)
|
72
|
+
request.content_type = 'application/json'
|
73
|
+
|
74
|
+
request.body = JSON.dump({
|
75
|
+
"contents" => [{
|
76
|
+
"parts" => [{
|
77
|
+
"text" => prompt_content # The entire prompt is passed as text
|
78
|
+
}]
|
79
|
+
}],
|
80
|
+
# Gemini specific: Ensure JSON output if possible via generationConfig
|
81
|
+
# However, the primary method is instructing it within the prompt itself.
|
82
|
+
# "generationConfig": {
|
83
|
+
# "responseMimeType": "application/json", # This might be too restrictive or not always work as expected
|
84
|
+
# }
|
85
|
+
})
|
86
|
+
|
87
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
88
|
+
http.request(request)
|
89
|
+
end
|
90
|
+
|
91
|
+
if response.code != '200'
|
92
|
+
raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
|
93
|
+
end
|
94
|
+
|
95
|
+
parsed_response = JSON.parse(response.body)
|
96
|
+
# Return the raw JSON string. CLI's call_llm_for_diff_analysis will handle parsing.
|
97
|
+
# The Gemini API returns the analysis in parsed_response['candidates'].first['content']['parts'].first['text']
|
98
|
+
# which should itself be a JSON string as per our prompt's instructions.
|
99
|
+
parsed_response['candidates'].first['content']['parts'].first['text']
|
100
|
+
end
|
67
101
|
end
|
68
102
|
end
|
69
103
|
end
|
data/lib/n2b/llm/open_ai.rb
CHANGED
@@ -33,9 +33,7 @@ module N2M
|
|
33
33
|
|
34
34
|
# check for errors
|
35
35
|
if response.code != '200'
|
36
|
-
|
37
|
-
puts response.body
|
38
|
-
exit 1
|
36
|
+
raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
|
39
37
|
end
|
40
38
|
answer = JSON.parse(response.body)['choices'].first['message']['content']
|
41
39
|
begin
|
@@ -47,6 +45,38 @@ module N2M
|
|
47
45
|
end
|
48
46
|
answer
|
49
47
|
end
|
48
|
+
|
49
|
+
def analyze_code_diff(prompt_content)
|
50
|
+
# This method assumes prompt_content is the full, ready-to-send prompt
|
51
|
+
# including all instructions for the LLM (system message, diff, user additions, JSON format).
|
52
|
+
request = Net::HTTP::Post.new(API_URI)
|
53
|
+
request.content_type = 'application/json'
|
54
|
+
request['Authorization'] = "Bearer #{@config['access_key']}"
|
55
|
+
|
56
|
+
request.body = JSON.dump({
|
57
|
+
"model" => MODELS[@config['model']],
|
58
|
+
"response_format" => { "type" => "json_object" }, # Crucial for OpenAI to return JSON
|
59
|
+
"messages" => [
|
60
|
+
{
|
61
|
+
"role" => "user", # The entire prompt is passed as a single user message
|
62
|
+
"content" => prompt_content
|
63
|
+
}
|
64
|
+
],
|
65
|
+
"max_tokens" => @config['max_tokens'] || 1500 # Allow overriding, ensure it's enough for JSON
|
66
|
+
})
|
67
|
+
|
68
|
+
response = Net::HTTP.start(API_URI.hostname, API_URI.port, use_ssl: true) do |http|
|
69
|
+
http.request(request)
|
70
|
+
end
|
71
|
+
|
72
|
+
if response.code != '200'
|
73
|
+
raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
|
74
|
+
end
|
75
|
+
|
76
|
+
# Return the raw JSON string. CLI's call_llm_for_diff_analysis will handle parsing.
|
77
|
+
# OpenAI with json_object mode should return the JSON directly in 'choices'.first.message.content
|
78
|
+
JSON.parse(response.body)['choices'].first['message']['content']
|
79
|
+
end
|
50
80
|
end
|
51
81
|
end
|
52
82
|
end
|
data/lib/n2b/version.rb
CHANGED
data/lib/n2b.rb
CHANGED
@@ -9,12 +9,12 @@ require 'n2b/version'
|
|
9
9
|
require 'n2b/llm/claude'
|
10
10
|
require 'n2b/llm/open_ai'
|
11
11
|
require 'n2b/llm/gemini'
|
12
|
+
require 'n2b/errors' # Load custom errors
|
12
13
|
require 'n2b/base'
|
13
14
|
require 'n2b/cli'
|
14
15
|
|
15
16
|
require 'n2b/irb'
|
16
17
|
|
17
18
|
module N2B
|
18
|
-
|
19
|
-
|
19
|
+
# Error class is now defined in n2b/errors.rb
|
20
20
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: n2b
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Nothegger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-06-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -67,6 +67,7 @@ files:
|
|
67
67
|
- lib/n2b.rb
|
68
68
|
- lib/n2b/base.rb
|
69
69
|
- lib/n2b/cli.rb
|
70
|
+
- lib/n2b/errors.rb
|
70
71
|
- lib/n2b/irb.rb
|
71
72
|
- lib/n2b/llm/claude.rb
|
72
73
|
- lib/n2b/llm/gemini.rb
|
@@ -94,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
95
|
- !ruby/object:Gem::Version
|
95
96
|
version: '0'
|
96
97
|
requirements: []
|
97
|
-
rubygems_version: 3.5.
|
98
|
+
rubygems_version: 3.5.22
|
98
99
|
signing_key:
|
99
100
|
specification_version: 4
|
100
101
|
summary: Convert natural language to bash commands or ruby code and help with debugging.
|