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/bin/n2b-test-jira ADDED
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+ require 'base64'
7
+ require 'yaml'
8
+
9
+ # Jira API connection tester for N2B
10
+ # Tests authentication, permissions, and specific ticket access
11
+ class JiraConnectionTester
12
+ def initialize
13
+ @config_file = File.expand_path('~/.n2b/config.yml')
14
+ load_config
15
+ end
16
+
17
+ def load_config
18
+ unless File.exist?(@config_file)
19
+ puts "❌ Config file not found: #{@config_file}"
20
+ puts "Please run 'n2b --advanced-config' to set up Jira integration first."
21
+ exit 1
22
+ end
23
+
24
+ @config = YAML.load_file(@config_file)
25
+ @jira_config = @config['jira']
26
+
27
+ unless @jira_config && @jira_config['domain'] && @jira_config['email'] && @jira_config['api_key']
28
+ puts "❌ Jira configuration incomplete in #{@config_file}"
29
+ puts "Missing: domain, email, or api_key"
30
+ puts "Please run 'n2b --advanced-config' to configure Jira."
31
+ exit 1
32
+ end
33
+
34
+ # Handle domain that may or may not include protocol
35
+ domain = @jira_config['domain'].to_s.strip
36
+ if domain.start_with?('http://') || domain.start_with?('https://')
37
+ @base_url = "#{domain.chomp('/')}/rest/api/3"
38
+ else
39
+ @base_url = "https://#{domain.chomp('/')}/rest/api/3"
40
+ end
41
+
42
+ puts "✅ Config loaded successfully"
43
+ puts " Domain: #{@jira_config['domain']}"
44
+ puts " Email: #{@jira_config['email']}"
45
+ puts " API Key: #{@jira_config['api_key'][0..10]}..." # Show only first part for security
46
+ puts " Base URL: #{@base_url}"
47
+ puts
48
+ end
49
+
50
+ def test_connection
51
+ puts "🔍 Testing Jira API connection..."
52
+ puts "=" * 50
53
+
54
+ # Test 1: Basic API connectivity
55
+ test_basic_connectivity
56
+
57
+ # Test 2: Authentication
58
+ test_authentication
59
+
60
+ # Test 3: Permissions
61
+ test_permissions
62
+
63
+ # Test 4: Specific ticket access (if provided)
64
+ if ARGV[0]
65
+ test_ticket_access(ARGV[0])
66
+ else
67
+ puts "💡 To test specific ticket access, run: ruby test_jira_connection.rb TICKET-123"
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def test_basic_connectivity
74
+ puts "1️⃣ Testing basic connectivity to Jira..."
75
+
76
+ begin
77
+ uri = URI.parse(@base_url)
78
+ puts " Connecting to: #{uri.host}:#{uri.port}"
79
+
80
+ http = Net::HTTP.new(uri.host, uri.port)
81
+ http.use_ssl = (uri.scheme == 'https')
82
+ http.open_timeout = 10
83
+ http.read_timeout = 10
84
+
85
+ # Simple GET to the API root
86
+ request = Net::HTTP::Get.new('/rest/api/3/')
87
+ response = http.request(request)
88
+
89
+ puts " ✅ Connection successful (HTTP #{response.code})"
90
+ puts
91
+ rescue => e
92
+ puts " ❌ Connection failed: #{e.message}"
93
+ puts " Check your network connection and domain configuration."
94
+ exit 1
95
+ end
96
+ end
97
+
98
+ def test_authentication
99
+ puts "2️⃣ Testing authentication..."
100
+
101
+ begin
102
+ response = make_api_request('GET', '/myself')
103
+
104
+ if response['accountId']
105
+ puts " ✅ Authentication successful"
106
+ puts " Account ID: #{response['accountId']}"
107
+ puts " Display Name: #{response['displayName']}"
108
+ puts " Email: #{response['emailAddress']}"
109
+ puts
110
+ else
111
+ puts " ⚠️ Authentication response unexpected:"
112
+ puts " #{response}"
113
+ puts
114
+ end
115
+ rescue => e
116
+ puts " ❌ Authentication failed: #{e.message}"
117
+ puts " Check your email and API token."
118
+ puts " API token should be generated from: https://id.atlassian.com/manage-profile/security/api-tokens"
119
+ exit 1
120
+ end
121
+ end
122
+
123
+ def test_permissions
124
+ puts "3️⃣ Testing required permissions..."
125
+
126
+ begin
127
+ # Test 1: Project access (Browse Projects permission)
128
+ response = make_api_request('GET', '/project')
129
+
130
+ if response.is_a?(Array) && response.length > 0
131
+ puts " ✅ Browse Projects: Can access #{response.length} projects"
132
+ response.first(3).each do |project|
133
+ puts " - #{project['key']}: #{project['name']}"
134
+ end
135
+ puts " ... (showing first 3)" if response.length > 3
136
+
137
+ # Test 2: Issue access (Browse Issues permission)
138
+ test_project = response.first
139
+ puts " 🔍 Testing issue access in project: #{test_project['key']}"
140
+
141
+ begin
142
+ issues_response = make_api_request('GET', "/search?jql=project=#{test_project['key']}&maxResults=1")
143
+ if issues_response['issues'] && issues_response['issues'].length > 0
144
+ puts " ✅ Browse Issues: Can access issues in #{test_project['key']}"
145
+
146
+ # Test 3: Comment access (if we found an issue)
147
+ test_issue = issues_response['issues'].first
148
+ begin
149
+ comments_response = make_api_request('GET', "/issue/#{test_issue['key']}/comment")
150
+ puts " ✅ View Comments: Can access comments on #{test_issue['key']}"
151
+ rescue => e
152
+ puts " ❌ View Comments: Cannot access comments (#{e.message})"
153
+ end
154
+
155
+ # Test 4: Add comment permission (we won't actually add, just check the endpoint)
156
+ puts " ℹ️ Add Comments: Will be tested when actually posting comments"
157
+ else
158
+ puts " ⚠️ No issues found in #{test_project['key']} to test comment permissions"
159
+ end
160
+ rescue => e
161
+ puts " ❌ Browse Issues: Cannot search issues (#{e.message})"
162
+ end
163
+
164
+ puts
165
+ else
166
+ puts " ❌ Browse Projects: No projects accessible"
167
+ puts " Your API token needs 'Browse Projects' permission"
168
+ puts
169
+ end
170
+ rescue => e
171
+ puts " ❌ Permission test failed: #{e.message}"
172
+ puts " Your API token might not have sufficient permissions."
173
+ puts
174
+ end
175
+
176
+ puts " 📋 Required Jira Permissions for N2B:"
177
+ puts " • Browse Projects - to access project list"
178
+ puts " • Browse Issues - to read ticket details"
179
+ puts " • View Comments - to read ticket comments"
180
+ puts " • Add Comments - to post analysis results"
181
+ puts
182
+ end
183
+
184
+ def test_ticket_access(ticket_key)
185
+ puts "4️⃣ Testing access to specific ticket: #{ticket_key}..."
186
+
187
+ begin
188
+ # Test ticket access
189
+ response = make_api_request('GET', "/issue/#{ticket_key}")
190
+
191
+ puts " ✅ Ticket access successful"
192
+ puts " Key: #{response['key']}"
193
+ puts " Summary: #{response.dig('fields', 'summary')}"
194
+ puts " Status: #{response.dig('fields', 'status', 'name')}"
195
+ puts " Assignee: #{response.dig('fields', 'assignee', 'displayName') || 'Unassigned'}"
196
+ puts
197
+
198
+ # Test comments access
199
+ comments_response = make_api_request('GET', "/issue/#{ticket_key}/comment")
200
+ comment_count = comments_response.dig('comments')&.length || 0
201
+ puts " ✅ Comments access successful (#{comment_count} comments)"
202
+ puts
203
+
204
+ rescue => e
205
+ puts " ❌ Ticket access failed: #{e.message}"
206
+
207
+ if e.message.include?('404')
208
+ puts " Possible causes:"
209
+ puts " - Ticket doesn't exist"
210
+ puts " - You don't have permission to view this ticket"
211
+ puts " - Ticket is in a project you can't access"
212
+ elsif e.message.include?('401')
213
+ puts " Authentication issue - check your API token"
214
+ elsif e.message.include?('403')
215
+ puts " Permission denied - your account can't access this ticket"
216
+ end
217
+ puts
218
+ end
219
+ end
220
+
221
+ def make_api_request(method, path, body = nil)
222
+ full_url = "#{@base_url}#{path}"
223
+ uri = URI.parse(full_url)
224
+
225
+ http = Net::HTTP.new(uri.host, uri.port)
226
+ http.use_ssl = (uri.scheme == 'https')
227
+ http.read_timeout = 30
228
+ http.open_timeout = 10
229
+
230
+ request = case method.upcase
231
+ when 'GET'
232
+ Net::HTTP::Get.new(uri.request_uri)
233
+ when 'POST'
234
+ req = Net::HTTP::Post.new(uri.request_uri)
235
+ req.body = body.to_json if body
236
+ req
237
+ else
238
+ raise "Unsupported HTTP method: #{method}"
239
+ end
240
+
241
+ request['Authorization'] = "Basic #{Base64.strict_encode64("#{@jira_config['email']}:#{@jira_config['api_key']}")}"
242
+ request['Content-Type'] = 'application/json'
243
+ request['Accept'] = 'application/json'
244
+
245
+ response = http.request(request)
246
+
247
+ unless response.is_a?(Net::HTTPSuccess)
248
+ error_message = "Jira API Error: #{response.code} #{response.message}"
249
+ error_message += " - #{response.body}" if response.body && !response.body.empty?
250
+ raise error_message
251
+ end
252
+
253
+ response.body.empty? ? {} : JSON.parse(response.body)
254
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
255
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, Errno::ECONNREFUSED => e
256
+ raise "Jira API request failed: #{e.class} - #{e.message}"
257
+ end
258
+ end
259
+
260
+ # Run the test
261
+ if __FILE__ == $0
262
+ puts "🧪 Jira API Connection Tester"
263
+ puts "=" * 50
264
+ puts
265
+
266
+ tester = JiraConnectionTester.new
267
+ tester.test_connection
268
+
269
+ puts "🎉 Test completed!"
270
+ puts
271
+ puts "If all tests passed, your Jira integration should work correctly."
272
+ puts "If any tests failed, check the error messages above for guidance."
273
+ end
data/lib/n2b/base.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require_relative 'model_config'
2
+
1
3
  module N2B
2
4
  class Base
3
5
 
@@ -12,55 +14,216 @@ module N2B
12
14
  end
13
15
  end
14
16
 
15
- def get_config( reconfigure: false)
17
+ def get_config(reconfigure: false, advanced_flow: false)
16
18
  config = load_config
17
- api_key = ENV['CLAUDE_API_KEY'] || config['access_key']
18
- model = config['model'] || 'sonnet35'
19
-
20
- if api_key.nil? || api_key == '' || reconfigure
21
- print "choose a language model to use (1:claude, 2:openai, 3:gemini) #{ config['llm'] }: "
22
- llm = $stdin.gets.chomp
23
- llm = config['llm'] if llm.empty?
24
- unless ['claude', 'openai', 'gemini', '1', '2', '3'].include?(llm)
25
- puts "Invalid language model. Choose from: claude, openai, gemini"
26
- exit 1
19
+ api_key = ENV['CLAUDE_API_KEY'] || config['access_key'] # This will be used as default or for existing configs
20
+ _model = config['model'] # Unused but kept for potential future use # Model will be handled per LLM
21
+
22
+ # Determine if it's effectively a first-time setup for core LLM details
23
+ is_first_time_core_setup = config['llm'].nil?
24
+
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 ---"
48
+ puts "Ollama typically runs locally and doesn't require an API key."
49
+
50
+ # Use new model configuration system
51
+ current_model = config['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
54
+
55
+ current_ollama_api_url = config['ollama_api_url'] || N2M::Llm::Ollama::DEFAULT_OLLAMA_API_URI
56
+ print "Enter Ollama API base URL [current: #{current_ollama_api_url}]: "
57
+ ollama_api_url_input = $stdin.gets.chomp
58
+ config['ollama_api_url'] = ollama_api_url_input.empty? ? current_ollama_api_url : ollama_api_url_input
59
+ config.delete('access_key') # Remove access_key if switching to ollama
60
+ else
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]' }: "
65
+ api_key_input = $stdin.gets.chomp
66
+ config['access_key'] = api_key_input if !api_key_input.empty?
67
+ config['access_key'] = current_api_key if api_key_input.empty? && !current_api_key.nil? && !current_api_key.empty?
68
+
69
+ if config['access_key'].nil? || config['access_key'].empty?
70
+ puts "API key is required for #{selected_llm}."
71
+ exit 1
72
+ end
73
+
74
+ current_model = config['model']
75
+ model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model) # Renamed
76
+ config['model'] = model_choice
77
+
78
+ if selected_llm == 'openrouter'
79
+ current_site_url = config['openrouter_site_url'] || ""
80
+ print "Enter your OpenRouter Site URL (optional, for HTTP-Referer) [current: #{current_site_url}]: "
81
+ openrouter_site_url_input = $stdin.gets.chomp
82
+ config['openrouter_site_url'] = openrouter_site_url_input.empty? ? current_site_url : openrouter_site_url_input
83
+
84
+ current_site_name = config['openrouter_site_name'] || ""
85
+ print "Enter your OpenRouter Site Name (optional, for X-Title) [current: #{current_site_name}]: "
86
+ openrouter_site_name_input = $stdin.gets.chomp
87
+ config['openrouter_site_name'] = openrouter_site_name_input.empty? ? current_site_name : openrouter_site_name_input
88
+ end
89
+ end
90
+
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']
27
158
  end
28
- llm = 'claude' if llm == '1'
29
- llm = 'openai' if llm == '2'
30
- llm = 'gemini' if llm == '3'
31
- llm_class = case llm
32
- when 'openai'
33
- N2M::Llm::OpenAi
34
- when 'gemini'
35
- N2M::Llm::Gemini
36
- else
37
- N2M::Llm::Claude
38
- end
39
-
40
- print "Enter your #{llm} API key: #{ api_key.nil? || api_key.empty? ? '' : '(leave blank to keep the current key '+api_key[0..10]+'...)' }"
41
- api_key = $stdin.gets.chomp
42
- api_key = config['access_key'] if api_key.empty?
43
- print "Choose a model (#{ llm_class::MODELS.keys }, #{ llm_class::MODELS.keys.first } default): "
44
- model = $stdin.gets.chomp
45
- model = llm_class::MODELS.keys.first if model.empty?
46
- config['llm'] = llm
47
- config['access_key'] = api_key
48
- config['model'] = model
49
- unless llm_class::MODELS.keys.include?(model)
50
- puts "Invalid model. Choose from: #{llm_class::MODELS.keys.join(', ')}"
51
- exit 1
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."
52
166
  end
53
- puts "configure privacy settings directly in the config file #{CONFIG_FILE}"
54
- config['privacy'] ||= {}
55
- config['privacy']['send_shell_history'] = false
56
- config['privacy']['send_llm_history'] = true
57
- config['privacy']['send_current_directory'] =true
58
- config['append_to_shell_history'] = false
59
- puts "Current configuration: #{config['privacy']}"
167
+
168
+ puts "\nConfiguration saved to #{CONFIG_FILE}"
60
169
  FileUtils.mkdir_p(File.dirname(CONFIG_FILE)) unless File.exist?(File.dirname(CONFIG_FILE))
61
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
62
184
  end
63
185
  config
64
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
65
228
  end
66
229
  end