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.
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
- default_system_prompt = <<-SYSTEM_PROMPT.strip
321
- You are a senior software developer reviewing a code diff.
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
- json_instruction = <<-JSON_INSTRUCTION.strip
386
- CRITICAL: Return ONLY a valid JSON object.
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
- response_json_str = llm.analyze_code_diff(prompt) # Call the new dedicated method
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
- response_json_str = llm.make_request(content)
671
-
672
- append_to_llm_history_file("#{prompt}\n#{response_json_str}") # Storing the raw JSON string
673
- # The original call_llm was expected to return a hash after JSON.parse,
674
- # but it was actually returning the string. Let's assume it should return a parsed Hash.
675
- # However, the calling method `process_natural_language_command` accesses it like `bash_commands['commands']`
676
- # which implies it expects a Hash. Let's ensure call_llm returns a Hash.
677
- # This internal JSON parsing is for the *content* of a successful LLM response.
678
- # The LlmApiError for network/auth issues should be caught before this.
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(response_json_str)
681
- parsed_response
633
+ parsed_response = JSON.parse(response_str)
682
634
  rescue JSON::ParserError => e
683
- puts "Error parsing LLM response JSON for command generation: #{e.message}"
684
- # This is a fallback for when the LLM response *content* is not valid JSON.
685
- { "commands" => ["echo 'Error: LLM returned invalid JSON content.'"], "explanation" => "The response from the language model was not valid JSON." }
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