n2b 0.5.0 → 0.7.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 +374 -23
- data/bin/n2b-diff +5 -0
- data/bin/n2b-test-jira +17 -14
- data/lib/n2b/cli.rb +137 -72
- data/lib/n2b/jira_client.rb +430 -70
- data/lib/n2b/llm/claude.rb +6 -19
- data/lib/n2b/llm/gemini.rb +8 -7
- data/lib/n2b/llm/ollama.rb +1 -1
- data/lib/n2b/llm/open_ai.rb +26 -20
- data/lib/n2b/merge_cli.rb +694 -0
- data/lib/n2b/merge_conflict_parser.rb +70 -0
- data/lib/n2b/template_engine.rb +105 -0
- data/lib/n2b/templates/diff_json_instruction.txt +27 -0
- data/lib/n2b/templates/diff_system_prompt.txt +23 -0
- data/lib/n2b/templates/jira_comment.txt +67 -0
- data/lib/n2b/templates/merge_conflict_prompt.txt +20 -0
- data/lib/n2b/version.rb +1 -1
- data/lib/n2b.rb +3 -0
- metadata +34 -7
data/lib/n2b/cli.rb
CHANGED
@@ -316,32 +316,9 @@ module N2B
|
|
316
316
|
end
|
317
317
|
end
|
318
318
|
|
319
|
-
def build_diff_analysis_prompt(diff_output, user_prompt_addition = "", requirements_content = nil)
|
320
|
-
|
321
|
-
|
322
|
-
Your task is to provide a constructive and detailed analysis of the changes.
|
323
|
-
Focus on identifying potential bugs, suggesting improvements in code quality, style, performance, and security.
|
324
|
-
Also, provide a concise summary of the changes.
|
325
|
-
|
326
|
-
IMPORTANT: When referring to specific issues or improvements, always include:
|
327
|
-
- The exact file path (e.g., "lib/n2b/cli.rb")
|
328
|
-
- The specific line numbers or line ranges (e.g., "line 42" or "lines 15-20")
|
329
|
-
- The exact code snippet you're referring to when possible
|
330
|
-
|
331
|
-
This helps users quickly locate and understand the issues you identify.
|
332
|
-
|
333
|
-
SPECIAL FOCUS ON TEST COVERAGE:
|
334
|
-
Pay special attention to whether the developer has provided adequate test coverage for the changes:
|
335
|
-
- Look for new test files or modifications to existing test files
|
336
|
-
- Check if new functionality has corresponding tests
|
337
|
-
- Evaluate if edge cases and error conditions are tested
|
338
|
-
- Assess if the tests are meaningful and comprehensive
|
339
|
-
- Note any missing test coverage that should be added
|
340
|
-
|
341
|
-
NOTE: In addition to the diff, you will also receive the current code context around the changed areas.
|
342
|
-
This provides better understanding of the surrounding code and helps with more accurate analysis.
|
343
|
-
The user may provide additional instructions or specific requirements below.
|
344
|
-
SYSTEM_PROMPT
|
319
|
+
def build_diff_analysis_prompt(diff_output, user_prompt_addition = "", requirements_content = nil, config = {})
|
320
|
+
default_system_prompt_path = resolve_template_path('diff_system_prompt', config)
|
321
|
+
default_system_prompt = File.read(default_system_prompt_path).strip
|
345
322
|
|
346
323
|
user_instructions_section = ""
|
347
324
|
unless user_prompt_addition.to_s.strip.empty?
|
@@ -382,35 +359,8 @@ REQUIREMENTS_BLOCK
|
|
382
359
|
end
|
383
360
|
end
|
384
361
|
|
385
|
-
|
386
|
-
|
387
|
-
Do not include any explanatory text before or after the JSON.
|
388
|
-
Each error and improvement should include specific file paths and line numbers.
|
389
|
-
|
390
|
-
The JSON object must contain the following keys:
|
391
|
-
- "summary": (string) Brief overall description of the changes.
|
392
|
-
- "ticket_implementation_summary": (string) A concise summary of what was implemented or achieved in relation to the ticket's goals, based *only* on the provided diff. This is for developer status updates and Jira comments.
|
393
|
-
- "errors": (list of strings) Potential bugs or issues found.
|
394
|
-
- "improvements": (list of strings) Suggestions for code quality, style, performance, or security.
|
395
|
-
- "test_coverage": (string) Assessment of test coverage for the changes.
|
396
|
-
- "requirements_evaluation": (string, include only if requirements were provided in the prompt) Evaluation of how the changes meet the provided requirements.
|
397
|
-
|
398
|
-
Example format:
|
399
|
-
{
|
400
|
-
"summary": "Refactored the user authentication module and added password complexity checks.",
|
401
|
-
"ticket_implementation_summary": "Implemented the core logic for user password updates and strengthened security by adding complexity validation as per the ticket's primary goal. Some UI elements are pending.",
|
402
|
-
"errors": [
|
403
|
-
"lib/example.rb line 42: Potential null pointer exception when accessing user.name without checking if user is nil.",
|
404
|
-
"src/main.js lines 15-20: Missing error handling for async operation."
|
405
|
-
],
|
406
|
-
"improvements": [
|
407
|
-
"lib/example.rb line 30: Consider using a constant for the magic number 42.",
|
408
|
-
"src/utils.py lines 5-10: This method could be simplified using list comprehension."
|
409
|
-
],
|
410
|
-
"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.",
|
411
|
-
"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."
|
412
|
-
}
|
413
|
-
JSON_INSTRUCTION
|
362
|
+
json_instruction_path = resolve_template_path('diff_json_instruction', config)
|
363
|
+
json_instruction = File.read(json_instruction_path).strip
|
414
364
|
|
415
365
|
full_prompt = [
|
416
366
|
default_system_prompt,
|
@@ -426,7 +376,7 @@ JSON_INSTRUCTION
|
|
426
376
|
end
|
427
377
|
|
428
378
|
def analyze_diff(diff_output, config, user_prompt_addition = "", requirements_content = nil)
|
429
|
-
prompt = build_diff_analysis_prompt(diff_output, user_prompt_addition, requirements_content)
|
379
|
+
prompt = build_diff_analysis_prompt(diff_output, user_prompt_addition, requirements_content, config)
|
430
380
|
analysis_json_str = call_llm_for_diff_analysis(prompt, config)
|
431
381
|
|
432
382
|
begin
|
@@ -607,7 +557,8 @@ JSON_INSTRUCTION
|
|
607
557
|
raise N2B::Error, "Unsupported LLM service: #{llm_service_name}"
|
608
558
|
end
|
609
559
|
|
610
|
-
|
560
|
+
puts "🔍 AI is analyzing your code diff..."
|
561
|
+
response_json_str = analyze_diff_with_spinner(llm, prompt)
|
611
562
|
response_json_str
|
612
563
|
rescue N2B::LlmApiError => e # This catches errors from analyze_code_diff
|
613
564
|
puts "Error communicating with the LLM: #{e.message}"
|
@@ -667,23 +618,37 @@ JSON_INSTRUCTION
|
|
667
618
|
EOF
|
668
619
|
|
669
620
|
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
#
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
621
|
+
puts "🤖 AI is generating commands..."
|
622
|
+
response = make_request_with_spinner(llm, content)
|
623
|
+
|
624
|
+
# Handle both Hash (from JSON mode providers) and String responses
|
625
|
+
if response.is_a?(Hash)
|
626
|
+
# Already parsed by the LLM provider
|
627
|
+
parsed_response = response
|
628
|
+
response_str = response.to_json # For history logging
|
629
|
+
else
|
630
|
+
# String response that needs parsing
|
631
|
+
response_str = response
|
679
632
|
begin
|
680
|
-
parsed_response = JSON.parse(
|
681
|
-
parsed_response
|
633
|
+
parsed_response = JSON.parse(response_str)
|
682
634
|
rescue JSON::ParserError => e
|
683
|
-
puts "
|
684
|
-
|
685
|
-
|
635
|
+
puts "⚠️ Invalid JSON detected, attempting automatic repair..."
|
636
|
+
repaired_response = attempt_json_repair_for_commands(response_str, llm)
|
637
|
+
|
638
|
+
if repaired_response
|
639
|
+
puts "✅ JSON repair successful!"
|
640
|
+
parsed_response = repaired_response
|
641
|
+
else
|
642
|
+
puts "❌ JSON repair failed"
|
643
|
+
puts "Error parsing LLM response JSON for command generation: #{e.message}"
|
644
|
+
# This is a fallback for when the LLM response *content* is not valid JSON.
|
645
|
+
parsed_response = { "commands" => ["echo 'Error: LLM returned invalid JSON content.'"], "explanation" => "The response from the language model was not valid JSON." }
|
646
|
+
end
|
686
647
|
end
|
648
|
+
end
|
649
|
+
|
650
|
+
append_to_llm_history_file("#{prompt}\n#{response_str}") # Storing the response for history
|
651
|
+
parsed_response
|
687
652
|
rescue N2B::LlmApiError => e
|
688
653
|
puts "Error communicating with the LLM: #{e.message}"
|
689
654
|
|
@@ -784,7 +749,102 @@ JSON_INSTRUCTION
|
|
784
749
|
end
|
785
750
|
system("history -r") # Attempt to reload history in current session
|
786
751
|
end
|
787
|
-
|
752
|
+
|
753
|
+
def resolve_template_path(template_key, config)
|
754
|
+
user_path = config.dig('templates', template_key) if config.is_a?(Hash)
|
755
|
+
return user_path if user_path && File.exist?(user_path)
|
756
|
+
|
757
|
+
File.expand_path(File.join(__dir__, 'templates', "#{template_key}.txt"))
|
758
|
+
end
|
759
|
+
|
760
|
+
def make_request_with_spinner(llm, content)
|
761
|
+
spinner_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
762
|
+
spinner_thread = Thread.new do
|
763
|
+
i = 0
|
764
|
+
while true
|
765
|
+
print "\r⠿ #{spinner_chars[i % spinner_chars.length]} Processing..."
|
766
|
+
$stdout.flush
|
767
|
+
sleep(0.1)
|
768
|
+
i += 1
|
769
|
+
end
|
770
|
+
end
|
771
|
+
|
772
|
+
begin
|
773
|
+
result = llm.make_request(content)
|
774
|
+
spinner_thread.kill
|
775
|
+
print "\r#{' ' * 25}\r" # Clear the spinner line
|
776
|
+
puts "✅ Commands generated!"
|
777
|
+
result
|
778
|
+
rescue => e
|
779
|
+
spinner_thread.kill
|
780
|
+
print "\r#{' ' * 25}\r" # Clear the spinner line
|
781
|
+
raise e
|
782
|
+
end
|
783
|
+
end
|
784
|
+
|
785
|
+
def analyze_diff_with_spinner(llm, prompt)
|
786
|
+
spinner_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
787
|
+
spinner_thread = Thread.new do
|
788
|
+
i = 0
|
789
|
+
while true
|
790
|
+
print "\r🔍 #{spinner_chars[i % spinner_chars.length]} Analyzing diff..."
|
791
|
+
$stdout.flush
|
792
|
+
sleep(0.1)
|
793
|
+
i += 1
|
794
|
+
end
|
795
|
+
end
|
796
|
+
|
797
|
+
begin
|
798
|
+
result = llm.analyze_code_diff(prompt)
|
799
|
+
spinner_thread.kill
|
800
|
+
print "\r#{' ' * 30}\r" # Clear the spinner line
|
801
|
+
puts "✅ Diff analysis complete!"
|
802
|
+
result
|
803
|
+
rescue => e
|
804
|
+
spinner_thread.kill
|
805
|
+
print "\r#{' ' * 30}\r" # Clear the spinner line
|
806
|
+
raise e
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
def attempt_json_repair_for_commands(malformed_response, llm)
|
811
|
+
repair_prompt = <<~PROMPT
|
812
|
+
The following response was supposed to be valid JSON with keys "commands" (array) and "explanation" (string), but it has formatting issues. Please fix it and return ONLY the corrected JSON:
|
813
|
+
|
814
|
+
Original response:
|
815
|
+
#{malformed_response}
|
816
|
+
|
817
|
+
Requirements:
|
818
|
+
- Must be valid JSON
|
819
|
+
- Must have "commands" key with array of command strings
|
820
|
+
- Must have "explanation" key with explanation text
|
821
|
+
- Return ONLY the JSON, no other text
|
822
|
+
|
823
|
+
Fixed JSON:
|
824
|
+
PROMPT
|
825
|
+
|
826
|
+
begin
|
827
|
+
puts "🔧 Asking AI to fix the JSON..."
|
828
|
+
repaired_json_str = llm.make_request(repair_prompt)
|
829
|
+
|
830
|
+
# Handle both Hash and String responses
|
831
|
+
if repaired_json_str.is_a?(Hash)
|
832
|
+
repaired_response = repaired_json_str
|
833
|
+
else
|
834
|
+
repaired_response = JSON.parse(repaired_json_str)
|
835
|
+
end
|
836
|
+
|
837
|
+
# Validate the repaired response structure
|
838
|
+
if repaired_response.is_a?(Hash) && repaired_response.key?('commands') && repaired_response.key?('explanation')
|
839
|
+
return repaired_response
|
840
|
+
else
|
841
|
+
return nil
|
842
|
+
end
|
843
|
+
rescue JSON::ParserError, StandardError
|
844
|
+
return nil
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
788
848
|
|
789
849
|
def parse_options
|
790
850
|
options = {
|
@@ -834,6 +894,11 @@ JSON_INSTRUCTION
|
|
834
894
|
exit
|
835
895
|
end
|
836
896
|
|
897
|
+
opts.on('-v', '--version', 'Show version') do
|
898
|
+
puts "n2b version #{N2B::VERSION}"
|
899
|
+
exit
|
900
|
+
end
|
901
|
+
|
837
902
|
opts.on('-c', '--config', 'Configure the API key and model') do
|
838
903
|
options[:config] = true
|
839
904
|
end
|