n2b 0.4.0 → 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/base.rb CHANGED
@@ -14,99 +14,216 @@ module N2B
14
14
  end
15
15
  end
16
16
 
17
- def get_config( reconfigure: false)
17
+ def get_config(reconfigure: false, advanced_flow: false)
18
18
  config = load_config
19
19
  api_key = ENV['CLAUDE_API_KEY'] || config['access_key'] # This will be used as default or for existing configs
20
- model = config['model'] # Model will be handled per LLM
21
-
22
- if api_key.nil? || api_key == '' || config['llm'].nil? || reconfigure # Added config['llm'].nil? to force config if llm type isn't set
23
- print "choose a language model to use (1:claude, 2:openai, 3:gemini, 4:openrouter, 5:ollama) #{ config['llm'] }: "
24
- llm = $stdin.gets.chomp
25
- llm = config['llm'] if llm.empty? && !config['llm'].nil? # Keep current if input is empty and current exists
26
-
27
- unless ['claude', 'openai', 'gemini', 'openrouter', 'ollama', '1', '2', '3', '4', '5'].include?(llm)
28
- puts "Invalid language model. Choose from: claude, openai, gemini, openrouter, ollama, or 1-5"
29
- exit 1
30
- end
31
- llm = 'claude' if llm == '1'
32
- llm = 'openai' if llm == '2'
33
- llm = 'gemini' if llm == '3'
34
- llm = 'openrouter' if llm == '4'
35
- llm = 'ollama' if llm == '5'
36
-
37
- # Set default LLM if needed
38
- if llm.nil? || llm.empty?
39
- llm = 'claude'
40
- end
20
+ _model = config['model'] # Unused but kept for potential future use # Model will be handled per LLM
41
21
 
42
- config['llm'] = llm
22
+ # Determine if it's effectively a first-time setup for core LLM details
23
+ is_first_time_core_setup = config['llm'].nil?
43
24
 
44
- if llm == 'ollama'
45
- # Ollama specific configuration
25
+ if api_key.nil? || api_key == '' || config['llm'].nil? || reconfigure
26
+ puts "--- N2B Core LLM Configuration ---"
27
+ print "Choose a language model to use (1:claude, 2:openai, 3:gemini, 4:openrouter, 5:ollama) [current: #{config['llm']}]: "
28
+ llm_choice = $stdin.gets.chomp
29
+ llm_choice = config['llm'] if llm_choice.empty? && !config['llm'].nil? # Keep current if input is empty
30
+
31
+ unless ['claude', 'openai', 'gemini', 'openrouter', 'ollama', '1', '2', '3', '4', '5'].include?(llm_choice)
32
+ puts "Invalid language model selection. Defaulting to 'claude' or previous if available."
33
+ llm_choice = config['llm'] || 'claude' # Fallback
34
+ end
35
+
36
+ selected_llm = case llm_choice
37
+ when '1', 'claude' then 'claude'
38
+ when '2', 'openai' then 'openai'
39
+ when '3', 'gemini' then 'gemini'
40
+ when '4', 'openrouter' then 'openrouter'
41
+ when '5', 'ollama' then 'ollama'
42
+ else config['llm'] || 'claude' # Should not happen due to validation
43
+ end
44
+ config['llm'] = selected_llm
45
+
46
+ if selected_llm == 'ollama'
47
+ puts "\n--- Ollama Specific Configuration ---"
46
48
  puts "Ollama typically runs locally and doesn't require an API key."
47
49
 
48
50
  # Use new model configuration system
49
51
  current_model = config['model']
50
- model = N2B::ModelConfig.get_model_choice(llm, current_model)
52
+ model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model) # Renamed to model_choice to avoid conflict
53
+ config['model'] = model_choice
51
54
 
52
- # Configure Ollama API URL
53
55
  current_ollama_api_url = config['ollama_api_url'] || N2M::Llm::Ollama::DEFAULT_OLLAMA_API_URI
54
- print "Enter Ollama API base URL (default: #{current_ollama_api_url}): "
56
+ print "Enter Ollama API base URL [current: #{current_ollama_api_url}]: "
55
57
  ollama_api_url_input = $stdin.gets.chomp
56
58
  config['ollama_api_url'] = ollama_api_url_input.empty? ? current_ollama_api_url : ollama_api_url_input
57
59
  config.delete('access_key') # Remove access_key if switching to ollama
58
60
  else
59
- # Configuration for API key based LLMs (Claude, OpenAI, Gemini, OpenRouter)
60
- current_api_key = config['access_key'] # Use existing key from config as default
61
- print "Enter your #{llm} API key: #{ current_api_key.nil? || current_api_key.empty? ? '' : '(leave blank to keep the current key)' }"
61
+ # Configuration for API key based LLMs
62
+ puts "\n--- #{selected_llm.capitalize} Specific Configuration ---"
63
+ current_api_key = config['access_key']
64
+ print "Enter your #{selected_llm} API key #{ current_api_key.nil? || current_api_key.empty? ? '' : '[leave blank to keep current]' }: "
62
65
  api_key_input = $stdin.gets.chomp
63
66
  config['access_key'] = api_key_input if !api_key_input.empty?
64
67
  config['access_key'] = current_api_key if api_key_input.empty? && !current_api_key.nil? && !current_api_key.empty?
65
68
 
66
- # Ensure API key is present if not Ollama
67
69
  if config['access_key'].nil? || config['access_key'].empty?
68
- puts "API key is required for #{llm}."
70
+ puts "API key is required for #{selected_llm}."
69
71
  exit 1
70
72
  end
71
73
 
72
- # Use new model configuration system
73
74
  current_model = config['model']
74
- model = N2B::ModelConfig.get_model_choice(llm, current_model)
75
+ model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model) # Renamed
76
+ config['model'] = model_choice
75
77
 
76
- if llm == 'openrouter'
78
+ if selected_llm == 'openrouter'
77
79
  current_site_url = config['openrouter_site_url'] || ""
78
- print "Enter your OpenRouter Site URL (optional, for HTTP-Referer header, current: #{current_site_url}): "
80
+ print "Enter your OpenRouter Site URL (optional, for HTTP-Referer) [current: #{current_site_url}]: "
79
81
  openrouter_site_url_input = $stdin.gets.chomp
80
82
  config['openrouter_site_url'] = openrouter_site_url_input.empty? ? current_site_url : openrouter_site_url_input
81
83
 
82
84
  current_site_name = config['openrouter_site_name'] || ""
83
- print "Enter your OpenRouter Site Name (optional, for X-Title header, current: #{current_site_name}): "
85
+ print "Enter your OpenRouter Site Name (optional, for X-Title) [current: #{current_site_name}]: "
84
86
  openrouter_site_name_input = $stdin.gets.chomp
85
87
  config['openrouter_site_name'] = openrouter_site_name_input.empty? ? current_site_name : openrouter_site_name_input
86
88
  end
87
89
  end
88
90
 
89
- config['model'] = model # Store selected model for all types
91
+ # --- Advanced Configuration Flow ---
92
+ # Prompt for advanced settings if advanced_flow is true or if it's the first time setting up core LLM.
93
+ prompt_for_advanced = advanced_flow || is_first_time_core_setup
94
+
95
+ if prompt_for_advanced
96
+ puts "\n--- Advanced Settings ---"
97
+ print "Would you like to configure advanced settings (e.g., Jira integration, privacy)? (y/n) [default: n]: "
98
+ choice = $stdin.gets.chomp.downcase
99
+
100
+ if choice == 'y'
101
+ # Jira Configuration
102
+ puts "\n--- Jira Integration ---"
103
+ puts "You can generate a Jira API token here: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/"
104
+ config['jira'] ||= {}
105
+ print "Jira Domain (e.g., your-company.atlassian.net) [current: #{config['jira']['domain']}]: "
106
+ config['jira']['domain'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['domain'] : val }
107
+
108
+ print "Jira Email Address [current: #{config['jira']['email']}]: "
109
+ config['jira']['email'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['email'] : val }
110
+
111
+ print "Jira API Token #{config['jira']['api_key'] ? '[leave blank to keep current]' : ''}: "
112
+ api_token_input = $stdin.gets.chomp
113
+ config['jira']['api_key'] = api_token_input if !api_token_input.empty?
114
+
115
+ print "Default Jira Project Key (optional, e.g., MYPROJ) [current: #{config['jira']['default_project']}]: "
116
+ config['jira']['default_project'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['default_project'] : val }
117
+
118
+ # Privacy Settings
119
+ puts "\n--- Privacy Settings ---"
120
+ config['privacy'] ||= {}
121
+ puts "Allow sending shell command history to the LLM? (true/false) [current: #{config['privacy']['send_shell_history']}]"
122
+ config['privacy']['send_shell_history'] = $stdin.gets.chomp.then { |val| val.empty? ? config['privacy']['send_shell_history'] : (val.downcase == 'true') }
123
+
124
+ puts "Allow sending LLM interaction history (your prompts and LLM responses) to the LLM? (true/false) [current: #{config['privacy']['send_llm_history']}]"
125
+ config['privacy']['send_llm_history'] = $stdin.gets.chomp.then { |val| val.empty? ? config['privacy']['send_llm_history'] : (val.downcase == 'true') }
126
+
127
+ puts "Allow sending current directory to the LLM? (true/false) [current: #{config['privacy']['send_current_directory']}]"
128
+ config['privacy']['send_current_directory'] = $stdin.gets.chomp.then { |val| val.empty? ? config['privacy']['send_current_directory'] : (val.downcase == 'true') }
129
+
130
+ puts "Append n2b generated commands to your shell history file? (true/false) [current: #{config['append_to_shell_history']}]"
131
+ # Note: append_to_shell_history was previously outside 'privacy' hash. Standardizing it inside.
132
+ config['append_to_shell_history'] = $stdin.gets.chomp.then { |val| val.empty? ? config['append_to_shell_history'] : (val.downcase == 'true') }
133
+ config['privacy']['append_to_shell_history'] = config['append_to_shell_history'] # Also place under privacy for consistency
134
+
135
+ else # User chose 'n' for advanced settings
136
+ # Ensure Jira config is empty or defaults are cleared if user opts out of advanced
137
+ config['jira'] ||= {} # Ensure it exists
138
+ # If they opt out, we don't clear existing, just don't prompt.
139
+ # If it's a fresh setup and they opt out, these will remain empty/nil.
140
+
141
+ # Ensure privacy settings are initialized to defaults if not already set by advanced flow
142
+ config['privacy'] ||= {}
143
+ config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
144
+ config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true # Default true
145
+ config['privacy']['send_current_directory'] = config['privacy']['send_current_directory'] || true # Default true
146
+ config['append_to_shell_history'] = config['append_to_shell_history'] || false
147
+ config['privacy']['append_to_shell_history'] = config['append_to_shell_history']
148
+ end
149
+ else # Not prompting for advanced (neither advanced_flow nor first_time_core_setup)
150
+ # Ensure defaults for privacy if they don't exist from a previous config
151
+ config['jira'] ||= {} # Ensure Jira key exists
152
+ config['privacy'] ||= {}
153
+ config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
154
+ config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true
155
+ config['privacy']['send_current_directory'] = config['privacy']['send_current_directory'] || true
156
+ config['append_to_shell_history'] = config['append_to_shell_history'] || false
157
+ config['privacy']['append_to_shell_history'] = config['append_to_shell_history']
158
+ end
159
+
160
+ # Validate configuration before saving
161
+ validation_errors = validate_config(config)
162
+ if validation_errors.any?
163
+ puts "\n⚠️ Configuration validation warnings:"
164
+ validation_errors.each { |error| puts " - #{error}" }
165
+ puts "Configuration saved with warnings."
166
+ end
90
167
 
91
- # Ensure privacy settings are initialized if not present
92
- config['privacy'] ||= {}
93
- # Set defaults for any privacy settings that are nil
94
- config['privacy']['send_shell_history'] = false if config['privacy']['send_shell_history'].nil?
95
- config['privacy']['send_llm_history'] = true if config['privacy']['send_llm_history'].nil?
96
- config['privacy']['send_current_directory'] = true if config['privacy']['send_current_directory'].nil?
97
- config['append_to_shell_history'] = false if config['append_to_shell_history'].nil?
98
-
99
- puts "configure privacy settings directly in the config file #{CONFIG_FILE}"
100
- config['privacy'] ||= {}
101
- config['privacy']['send_shell_history'] = false
102
- config['privacy']['send_llm_history'] = true
103
- config['privacy']['send_current_directory'] =true
104
- config['append_to_shell_history'] = false
105
- puts "Current configuration: #{config['privacy']}"
168
+ puts "\nConfiguration saved to #{CONFIG_FILE}"
106
169
  FileUtils.mkdir_p(File.dirname(CONFIG_FILE)) unless File.exist?(File.dirname(CONFIG_FILE))
107
170
  File.write(CONFIG_FILE, config.to_yaml)
171
+ else
172
+ # If not reconfiguring, still ensure privacy and jira keys exist with defaults if missing
173
+ # This handles configs from before these settings were introduced
174
+ config['jira'] ||= {}
175
+ config['privacy'] ||= {}
176
+ config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
177
+ config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true
178
+ config['privacy']['send_current_directory'] = config['privacy']['send_current_directory'] || true
179
+ # append_to_shell_history was outside 'privacy' before, ensure it's correctly defaulted
180
+ # and also placed under 'privacy' for future consistency.
181
+ current_append_setting = config.key?('append_to_shell_history') ? config['append_to_shell_history'] : false
182
+ config['append_to_shell_history'] = current_append_setting
183
+ config['privacy']['append_to_shell_history'] = config['privacy']['append_to_shell_history'] || current_append_setting
108
184
  end
109
185
  config
110
186
  end
187
+
188
+ private
189
+
190
+ def validate_config(config)
191
+ errors = []
192
+
193
+ # Validate LLM configuration
194
+ if config['llm'].nil? || config['llm'].empty?
195
+ errors << "LLM provider not specified"
196
+ end
197
+
198
+ # Validate model name
199
+ if config['model'].nil? || config['model'].empty?
200
+ errors << "Model not specified"
201
+ elsif config['model'].length < 3
202
+ errors << "Model name '#{config['model']}' seems too short - might be invalid"
203
+ elsif %w[y n yes no true false].include?(config['model'].downcase)
204
+ errors << "Model name '#{config['model']}' appears to be a boolean response rather than a model name"
205
+ end
206
+
207
+ # Validate API key for non-Ollama providers
208
+ if config['llm'] != 'ollama' && (config['access_key'].nil? || config['access_key'].empty?)
209
+ errors << "API key missing for #{config['llm']} provider"
210
+ end
211
+
212
+ # Validate Jira configuration if present
213
+ if config['jira'] && !config['jira'].empty?
214
+ jira_config = config['jira']
215
+ if jira_config['domain'] && !jira_config['domain'].empty?
216
+ # If domain is set, email and api_key should also be set
217
+ if jira_config['email'].nil? || jira_config['email'].empty?
218
+ errors << "Jira email missing when domain is configured"
219
+ end
220
+ if jira_config['api_key'].nil? || jira_config['api_key'].empty?
221
+ errors << "Jira API key missing when domain is configured"
222
+ end
223
+ end
224
+ end
225
+
226
+ errors
227
+ end
111
228
  end
112
229
  end
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
@@ -646,7 +787,16 @@ JSON_INSTRUCTION
646
787
 
647
788
 
648
789
  def parse_options
649
- 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
+ }
650
800
 
651
801
  parser = OptionParser.new do |opts|
652
802
  opts.banner = "Usage: n2b [options] [natural language command]"
@@ -667,6 +817,18 @@ JSON_INSTRUCTION
667
817
  options[:requirements] = file
668
818
  end
669
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
+
670
832
  opts.on('-h', '--help', 'Print this help') do
671
833
  puts opts
672
834
  exit
@@ -675,6 +837,11 @@ JSON_INSTRUCTION
675
837
  opts.on('-c', '--config', 'Configure the API key and model') do
676
838
  options[:config] = true
677
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
678
845
  end
679
846
 
680
847
  begin
@@ -694,6 +861,27 @@ JSON_INSTRUCTION
694
861
  exit 1
695
862
  end
696
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
+
697
885
  options
698
886
  end
699
887
  end
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