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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf304be008b27bf4336085ec39f75fc8ac5207c6e17d442d5129a366795629eb
4
- data.tar.gz: f436a7dc4a59f2840b023d7e406aa5fa2aa02dee7ebb10f48c982efaaf489546
3
+ metadata.gz: 644455363c88bf95c4ab4f21121ed6b8f56a591fb32bd1a8fa9f8f36d981e515
4
+ data.tar.gz: f899199e26524f38f88e4416217616c048e588c1f54becb228261b07ffcda3d1
5
5
  SHA512:
6
- metadata.gz: 770f49a5df340f3f9eae3f9072eafcbd9e8de6e2a5a227da3a96c67ebb22ca998411d1098921a375a9203fe446593ac8b15cfc3b6c6e6d13503a48957fed29c9
7
- data.tar.gz: 88720e0bae3adf499830681214c142ea96787f3fbae01fcfe276593ce3860f65127783293cf2c1c993725747ad54165e260473f84d172fb290490165cd587708
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 Errbit errors and generate detailed reports
12
- - Create formatted Scrum tickets from errors
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. You can also use a custom config file by setting the `N2B_CONFIG_FILE` environment variable:
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, gemini
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
- input_text = @args.join(' ')
15
- if input_text.empty?
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
- private
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
- llm = config['llm'] == 'openai' ? N2M::Llm::OpenAi.new(config) : N2M::Llm::Claude.new(config)
61
-
62
- content = <<-EOF
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
- answer = llm.make_request(content)
502
+ response_json_str = llm.make_request(content)
74
503
 
75
- append_to_llm_history_file("#{prompt}\n#{answer}")
76
- answer
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.parse!(@args)
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
@@ -0,0 +1,7 @@
1
+ module N2B
2
+ class Error < StandardError
3
+ end
4
+
5
+ class LlmApiError < Error
6
+ end
7
+ end
@@ -31,12 +31,12 @@ module N2M
31
31
  end
32
32
  # check for errors
33
33
  if response.code != '200'
34
- puts "Error: #{response.code} #{response.message}"
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
- puts "Error parsing JSON: #{answer}"
55
- answer = { 'explanation' => answer}
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
@@ -35,10 +35,7 @@ module N2M
35
35
 
36
36
  # check for errors
37
37
  if response.code != '200'
38
- puts "Error: #{response.code} #{response.message}"
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
@@ -33,9 +33,7 @@ module N2M
33
33
 
34
34
  # check for errors
35
35
  if response.code != '200'
36
- puts "Error: #{response.code} #{response.message}"
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
@@ -1,4 +1,4 @@
1
1
  # lib/n2b/version.rb
2
2
  module N2B
3
- VERSION = "0.2.4"
3
+ VERSION = "0.3.1"
4
4
  end
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
- class Error < StandardError; end
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.2.4
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-03-21 00:00:00.000000000 Z
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.3
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.