n2b 0.3.1 → 0.5.0

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
@@ -1,4 +1,6 @@
1
- module N2B
1
+ require_relative 'jira_client' # For N2B::JiraClient
2
+
3
+ module N2B
2
4
  class CLI < Base
3
5
  def self.run(args)
4
6
  new(args).execute
@@ -10,7 +12,8 @@ module N2B
10
12
  end
11
13
 
12
14
  def execute
13
- config = get_config(reconfigure: @options[:config])
15
+ # Pass advanced_config flag to get_config
16
+ config = get_config(reconfigure: @options[:config], advanced_flow: @options[:advanced_config])
14
17
  user_input = @args.join(' ') # All remaining args form user input/prompt addition
15
18
 
16
19
  if @options[:diff]
@@ -37,17 +40,113 @@ module N2B
37
40
  requirements_filepath = @options[:requirements]
38
41
  user_prompt_addition = @args.join(' ') # All remaining args are user prompt addition
39
42
 
40
- requirements_content = nil
41
- if requirements_filepath
42
- unless File.exist?(requirements_filepath)
43
+ # Jira Ticket Information
44
+ jira_ticket = @options[:jira_ticket]
45
+ jira_update_flag = @options[:jira_update] # true, false, or nil
46
+
47
+ requirements_content = nil # Initialize requirements_content
48
+
49
+ if jira_ticket
50
+ puts "Jira ticket specified: #{jira_ticket}"
51
+ if config['jira'] && config['jira']['domain'] && config['jira']['email'] && config['jira']['api_key']
52
+ begin
53
+ jira_client = N2B::JiraClient.new(config) # Pass the whole config
54
+ puts "Fetching Jira ticket details..."
55
+ # If a requirements file is also provided, the Jira ticket will take precedence.
56
+ # Or, we could append/prepend. For now, Jira overwrites.
57
+ requirements_content = jira_client.fetch_ticket(jira_ticket)
58
+ puts "Successfully fetched Jira ticket details."
59
+ # The fetched content is now in requirements_content and will be passed to analyze_diff
60
+ rescue N2B::JiraClient::JiraApiError => e
61
+ puts "Error fetching Jira ticket: #{e.message}"
62
+ puts "Proceeding with diff analysis without Jira ticket details."
63
+ rescue ArgumentError => e # Catches missing Jira config in JiraClient.new
64
+ puts "Jira configuration error: #{e.message}"
65
+ puts "Please ensure Jira is configured correctly using 'n2b -c'."
66
+ puts "Proceeding with diff analysis without Jira ticket details."
67
+ rescue StandardError => e
68
+ puts "An unexpected error occurred while fetching Jira ticket: #{e.message}"
69
+ puts "Proceeding with diff analysis without Jira ticket details."
70
+ end
71
+ else
72
+ puts "Jira configuration is missing or incomplete in N2B settings."
73
+ puts "Please configure Jira using 'n2b -c' to fetch ticket details."
74
+ puts "Proceeding with diff analysis without Jira ticket details."
75
+ end
76
+ # Handling of jira_update_flag can be done elsewhere, e.g., after analysis
77
+ if jira_update_flag == true
78
+ puts "Note: Jira ticket update (--jira-update) is flagged."
79
+ # Actual update logic will be separate
80
+ elsif jira_update_flag == false
81
+ puts "Note: Jira ticket will not be updated (--jira-no-update)."
82
+ end
83
+ end
84
+
85
+ # Load requirements from file if no Jira ticket was fetched or if specifically desired even with Jira.
86
+ # Current logic: Jira fetch, if successful, populates requirements_content.
87
+ # If Jira not specified, or fetch failed, try to load from file.
88
+ if requirements_content.nil? && requirements_filepath
89
+ if File.exist?(requirements_filepath)
90
+ puts "Loading requirements from file: #{requirements_filepath}"
91
+ requirements_content = File.read(requirements_filepath)
92
+ else
43
93
  puts "Error: Requirements file not found: #{requirements_filepath}"
44
- exit 1
94
+ # Decide if to exit or proceed. For now, proceed.
95
+ puts "Proceeding with diff analysis without file-based requirements."
45
96
  end
46
- requirements_content = File.read(requirements_filepath)
97
+ elsif requirements_content && requirements_filepath
98
+ puts "Note: Both Jira ticket and requirements file were provided. Using Jira ticket content for analysis."
47
99
  end
48
100
 
49
101
  diff_output = execute_vcs_diff(vcs_type, @options[:branch])
50
- analyze_diff(diff_output, config, user_prompt_addition, requirements_content)
102
+ analysis_result = analyze_diff(diff_output, config, user_prompt_addition, requirements_content) # Store the result
103
+
104
+ # --- Jira Update Logic ---
105
+ if jira_ticket && analysis_result && !analysis_result.empty?
106
+ # Check if Jira config is valid for updating
107
+ if config['jira'] && config['jira']['domain'] && config['jira']['email'] && config['jira']['api_key']
108
+ jira_comment_data = format_analysis_for_jira(analysis_result)
109
+ proceed_with_update = false
110
+
111
+ if jira_update_flag == true # --jira-update used
112
+ proceed_with_update = true
113
+ elsif jira_update_flag.nil? # Neither --jira-update nor --jira-no-update used
114
+ puts "\nWould you like to update Jira ticket #{jira_ticket} with this analysis? (y/n)"
115
+ user_choice = $stdin.gets.chomp.downcase
116
+ proceed_with_update = user_choice == 'y'
117
+ end # If jira_update_flag is false, proceed_with_update remains false
118
+
119
+ if proceed_with_update
120
+ begin
121
+ # Re-instantiate JiraClient or use an existing one if available and in scope
122
+ # For safety and simplicity here, re-instantiate with current config.
123
+ update_jira_client = N2B::JiraClient.new(config)
124
+ puts "Updating Jira ticket #{jira_ticket}..."
125
+ if update_jira_client.update_ticket(jira_ticket, jira_comment_data)
126
+ puts "Jira ticket #{jira_ticket} updated successfully."
127
+ else
128
+ # update_ticket currently returns true/false, but might raise error for http issues
129
+ puts "Failed to update Jira ticket #{jira_ticket}. The client did not report an error, but the update may not have completed."
130
+ end
131
+ rescue N2B::JiraClient::JiraApiError => e
132
+ puts "Error updating Jira ticket: #{e.message}"
133
+ rescue ArgumentError => e # From JiraClient.new if config is suddenly invalid
134
+ puts "Jira configuration error before update: #{e.message}"
135
+ rescue StandardError => e
136
+ puts "An unexpected error occurred while updating Jira ticket: #{e.message}"
137
+ end
138
+ else
139
+ puts "Jira ticket update skipped."
140
+ end
141
+ else
142
+ puts "Jira configuration is missing or incomplete. Cannot proceed with Jira update."
143
+ end
144
+ elsif jira_ticket && (analysis_result.nil? || analysis_result.empty?)
145
+ puts "Skipping Jira update as analysis result was empty or not generated."
146
+ end
147
+ # --- End of Jira Update Logic ---
148
+
149
+ analysis_result # Return analysis_result from handle_diff_analysis
51
150
  end
52
151
 
53
152
  def get_vcs_type
@@ -172,11 +271,11 @@ module N2B
172
271
 
173
272
  def validate_git_branch_exists(branch)
174
273
  # Check if branch exists locally
175
- result = `git rev-parse --verify #{branch} 2>/dev/null`
274
+ _result = `git rev-parse --verify #{branch} 2>/dev/null`
176
275
  return true if $?.success?
177
276
 
178
277
  # Check if branch exists on remote
179
- result = `git rev-parse --verify origin/#{branch} 2>/dev/null`
278
+ _result = `git rev-parse --verify origin/#{branch} 2>/dev/null`
180
279
  return true if $?.success?
181
280
 
182
281
  false
@@ -284,20 +383,29 @@ REQUIREMENTS_BLOCK
284
383
  end
285
384
 
286
385
  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).
386
+ CRITICAL: Return ONLY a valid JSON object.
288
387
  Do not include any explanatory text before or after the JSON.
289
388
  Each error and improvement should include specific file paths and line numbers.
290
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
+
291
398
  Example format:
292
399
  {
293
- "summary": "Brief description of the changes",
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.",
294
402
  "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"
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."
297
405
  ],
298
406
  "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"
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."
301
409
  ],
302
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.",
303
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."
@@ -352,13 +460,46 @@ JSON_INSTRUCTION
352
460
  puts "\nRequirements Evaluation:"
353
461
  puts requirements_eval
354
462
  end
463
+
464
+ puts "\nTicket Implementation Summary:"
465
+ impl_summary = analysis_result['ticket_implementation_summary']
466
+ puts impl_summary && !impl_summary.to_s.strip.empty? ? impl_summary : "No implementation summary provided."
467
+
355
468
  puts "-------------------"
469
+ return analysis_result # Return the parsed hash
356
470
  rescue JSON::ParserError => e # Handles cases where the JSON string (even fallback) is malformed
357
471
  puts "Critical Error: Failed to parse JSON response for diff analysis: #{e.message}"
358
472
  puts "Raw response was: #{analysis_json_str}"
473
+ return {} # Return empty hash on parsing error
359
474
  end
360
475
  end
361
476
 
477
+ private # Make sure new helper is private
478
+
479
+ def format_analysis_for_jira(analysis_result)
480
+ return "No analysis result available." if analysis_result.nil? || analysis_result.empty?
481
+
482
+ # Return structured data for ADF formatting
483
+ {
484
+ implementation_summary: analysis_result['ticket_implementation_summary']&.strip,
485
+ technical_summary: analysis_result['summary']&.strip,
486
+ issues: format_issues_for_adf(analysis_result['errors']),
487
+ improvements: format_improvements_for_adf(analysis_result['improvements']),
488
+ test_coverage: analysis_result['test_coverage']&.strip,
489
+ requirements_evaluation: analysis_result['requirements_evaluation']&.strip
490
+ }
491
+ end
492
+
493
+ def format_issues_for_adf(errors)
494
+ return [] unless errors.is_a?(Array) && errors.any?
495
+ errors.map(&:strip).reject(&:empty?)
496
+ end
497
+
498
+ def format_improvements_for_adf(improvements)
499
+ return [] unless improvements.is_a?(Array) && improvements.any?
500
+ improvements.map(&:strip).reject(&:empty?)
501
+ end
502
+
362
503
  def extract_json_from_response(response)
363
504
  # First try to parse the response as-is
364
505
  begin
@@ -413,7 +554,7 @@ JSON_INSTRUCTION
413
554
  # Parse hunk header (e.g., "@@ -10,7 +10,8 @@")
414
555
  match = line.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/)
415
556
  if match
416
- old_start = match[1].to_i
557
+ _old_start = match[1].to_i
417
558
  new_start = match[2].to_i
418
559
 
419
560
  # Use the new file line numbers for context
@@ -457,6 +598,10 @@ JSON_INSTRUCTION
457
598
  N2M::Llm::Claude.new(config)
458
599
  when 'gemini'
459
600
  N2M::Llm::Gemini.new(config)
601
+ when 'openrouter'
602
+ N2M::Llm::OpenRouter.new(config)
603
+ when 'ollama'
604
+ N2M::Llm::Ollama.new(config)
460
605
  else
461
606
  # Should not happen if config is validated, but as a safeguard:
462
607
  raise N2B::Error, "Unsupported LLM service: #{llm_service_name}"
@@ -466,6 +611,13 @@ JSON_INSTRUCTION
466
611
  response_json_str
467
612
  rescue N2B::LlmApiError => e # This catches errors from analyze_code_diff
468
613
  puts "Error communicating with the LLM: #{e.message}"
614
+
615
+ # Check if it might be a model-related error
616
+ if e.message.include?('model') || e.message.include?('Model') || e.message.include?('invalid') || e.message.include?('not found')
617
+ puts "\nThis might be due to an invalid or unsupported model configuration."
618
+ puts "Run 'n2b -c' to reconfigure your model settings."
619
+ end
620
+
469
621
  return '{"summary": "Error: Could not analyze diff due to LLM API error.", "errors": [], "improvements": []}'
470
622
  end
471
623
  end
@@ -485,7 +637,23 @@ JSON_INSTRUCTION
485
637
 
486
638
  def call_llm(prompt, config)
487
639
  begin # Added begin for LlmApiError rescue
488
- llm = config['llm'] == 'openai' ? N2M::Llm::OpenAi.new(config) : N2M::Llm::Claude.new(config)
640
+ llm_service_name = config['llm']
641
+ llm = case llm_service_name
642
+ when 'openai'
643
+ N2M::Llm::OpenAi.new(config)
644
+ when 'claude'
645
+ N2M::Llm::Claude.new(config)
646
+ when 'gemini'
647
+ N2M::Llm::Gemini.new(config)
648
+ when 'openrouter'
649
+ N2M::Llm::OpenRouter.new(config)
650
+ when 'ollama'
651
+ N2M::Llm::Ollama.new(config)
652
+ else
653
+ # Fallback or error, though config validation should prevent this
654
+ puts "Warning: Unsupported LLM service '#{llm_service_name}' configured. Falling back to Claude."
655
+ N2M::Llm::Claude.new(config)
656
+ end
489
657
 
490
658
  # This content is specific to bash command generation
491
659
  content = <<-EOF
@@ -508,24 +676,26 @@ JSON_INSTRUCTION
508
676
  # which implies it expects a Hash. Let's ensure call_llm returns a Hash.
509
677
  # This internal JSON parsing is for the *content* of a successful LLM response.
510
678
  # 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
679
+ begin
516
680
  parsed_response = JSON.parse(response_json_str)
517
681
  parsed_response
682
+ 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." }
518
686
  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
687
+ rescue N2B::LlmApiError => e
688
+ puts "Error communicating with the LLM: #{e.message}"
689
+
690
+ # Check if it might be a model-related error
691
+ if e.message.include?('model') || e.message.include?('Model') || e.message.include?('invalid') || e.message.include?('not found')
692
+ puts "\nThis might be due to an invalid or unsupported model configuration."
693
+ puts "Run 'n2b -c' to reconfigure your model settings."
694
+ end
695
+
696
+ # This is the fallback for LlmApiError (network, auth, etc.)
697
+ { "commands" => ["echo 'LLM API error occurred. Please check your configuration and network.'"], "explanation" => "Failed to connect to the LLM." }
698
+ end
529
699
  end
530
700
 
531
701
  def get_user_shell
@@ -617,7 +787,16 @@ JSON_INSTRUCTION
617
787
 
618
788
 
619
789
  def parse_options
620
- options = { execute: false, config: nil, diff: false, requirements: nil, branch: nil }
790
+ options = {
791
+ execute: false,
792
+ config: nil,
793
+ diff: false,
794
+ requirements: nil,
795
+ branch: nil,
796
+ jira_ticket: nil,
797
+ jira_update: nil, # Using nil as default, true for --jira-update, false for --jira-no-update
798
+ advanced_config: false # New option for advanced configuration flow
799
+ }
621
800
 
622
801
  parser = OptionParser.new do |opts|
623
802
  opts.banner = "Usage: n2b [options] [natural language command]"
@@ -638,6 +817,18 @@ JSON_INSTRUCTION
638
817
  options[:requirements] = file
639
818
  end
640
819
 
820
+ opts.on('-j', '--jira JIRA_ID_OR_URL', 'Jira ticket ID or URL for context or update') do |jira|
821
+ options[:jira_ticket] = jira
822
+ end
823
+
824
+ opts.on('--jira-update', 'Update the linked Jira ticket (requires -j)') do
825
+ options[:jira_update] = true
826
+ end
827
+
828
+ opts.on('--jira-no-update', 'Do not update the linked Jira ticket (requires -j)') do
829
+ options[:jira_update] = false
830
+ end
831
+
641
832
  opts.on('-h', '--help', 'Print this help') do
642
833
  puts opts
643
834
  exit
@@ -646,6 +837,11 @@ JSON_INSTRUCTION
646
837
  opts.on('-c', '--config', 'Configure the API key and model') do
647
838
  options[:config] = true
648
839
  end
840
+
841
+ opts.on('--advanced-config', 'Access advanced configuration options including Jira and privacy settings') do
842
+ options[:advanced_config] = true
843
+ options[:config] = true # Forcing config mode if advanced is chosen
844
+ end
649
845
  end
650
846
 
651
847
  begin
@@ -665,6 +861,27 @@ JSON_INSTRUCTION
665
861
  exit 1
666
862
  end
667
863
 
864
+ if options[:jira_update] == true && options[:jira_ticket].nil?
865
+ puts "Error: --jira-update option requires a Jira ticket to be specified with -j or --jira."
866
+ puts ""
867
+ puts parser.help
868
+ exit 1
869
+ end
870
+
871
+ if options[:jira_update] == false && options[:jira_ticket].nil?
872
+ puts "Error: --jira-no-update option requires a Jira ticket to be specified with -j or --jira."
873
+ puts ""
874
+ puts parser.help
875
+ exit 1
876
+ end
877
+
878
+ if options[:jira_update] == true && options[:jira_update] == false
879
+ puts "Error: --jira-update and --jira-no-update are mutually exclusive."
880
+ puts ""
881
+ puts parser.help
882
+ exit 1
883
+ end
884
+
668
885
  options
669
886
  end
670
887
  end
@@ -0,0 +1,47 @@
1
+ # N2B Model Configuration
2
+ # Format: display_name -> api_model_name
3
+ # Users can select from suggested models or enter custom model names
4
+
5
+ claude:
6
+ suggested:
7
+ haiku: "claude-3-haiku-20240307"
8
+ sonnet: "claude-3-sonnet-20240229"
9
+ sonnet35: "claude-3-5-sonnet-20240620"
10
+ sonnet37: "claude-3-7-sonnet-20250219"
11
+ sonnet40: "claude-sonnet-4-20250514"
12
+ default: "sonnet"
13
+
14
+ openai:
15
+ suggested:
16
+ gpt-4o: "gpt-4o"
17
+ gpt-4o-mini: "gpt-4o-mini"
18
+ o3: "o3"
19
+ o3-mini: "o3-mini"
20
+ o3-mini-high: "o3-mini-high"
21
+ o4: "o4"
22
+ o4-mini: "o4-mini"
23
+ o4-mini-high: "o4-mini-high"
24
+ default: "gpt-4o-mini"
25
+
26
+ gemini:
27
+ suggested:
28
+ gemini-2.5-flash: "gemini-2.5-flash-preview-05-20"
29
+ gemini-2.5-pro: "gemini-2.5-pro-preview-05-06"
30
+ default: "gemini-2.5-flash"
31
+
32
+ openrouter:
33
+ suggested:
34
+ deepseek-v3: "deepseek-v3-0324"
35
+ deepseek-r1-llama-8b: "deepseek-r1-distill-llama-8b"
36
+ llama-3.3-70b: "llama-3.3-70b-instruct"
37
+ llama-3.3-8b: "llama-3.3-8b-instruct"
38
+ wayfinder-large: "wayfinder-large-70b-llama-3.3"
39
+ default: "deepseek-v3"
40
+
41
+ ollama:
42
+ suggested:
43
+ llama3: "llama3"
44
+ mistral: "mistral"
45
+ codellama: "codellama"
46
+ qwen: "qwen2.5"
47
+ default: "llama3"
data/lib/n2b/irb.rb CHANGED
@@ -991,7 +991,7 @@ module N2B
991
991
 
992
992
  # If we still can't fix it, return a formatted version of the original
993
993
  "# Auto-formatted Ticket\n\n```\n#{original_response.inspect}\n```"
994
- rescue => e
994
+ rescue => _e
995
995
  # If anything goes wrong in the fixing process, return a readable version of the original
996
996
  if original_response.is_a?(Hash)
997
997
  # Try to create a readable markdown from the hash