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.
- checksums.yaml +4 -4
- data/README.md +250 -21
- data/bin/n2b-test-jira +273 -0
- data/lib/n2b/base.rb +171 -54
- data/lib/n2b/cli.rb +206 -18
- data/lib/n2b/irb.rb +1 -1
- data/lib/n2b/jira_client.rb +753 -0
- data/lib/n2b/model_config.rb +33 -6
- data/lib/n2b/version.rb +1 -1
- metadata +4 -2
data/lib/n2b/base.rb
CHANGED
@@ -14,99 +14,216 @@ module N2B
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
def get_config(
|
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
|
-
|
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
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
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
|
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
|
60
|
-
|
61
|
-
|
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 #{
|
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
|
-
|
75
|
+
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model) # Renamed
|
76
|
+
config['model'] = model_choice
|
75
77
|
|
76
|
-
if
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
94
|
+
# Decide if to exit or proceed. For now, proceed.
|
95
|
+
puts "Proceeding with diff analysis without file-based requirements."
|
45
96
|
end
|
46
|
-
|
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
|
-
|
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
|
-
|
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
|
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": "
|
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
|
-
|
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 = {
|
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 =>
|
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
|