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.
- checksums.yaml +4 -4
- data/README.md +349 -31
- data/bin/n2b-test-jira +273 -0
- data/lib/n2b/base.rb +205 -42
- data/lib/n2b/cli.rb +251 -34
- data/lib/n2b/config/models.yml +47 -0
- data/lib/n2b/irb.rb +1 -1
- data/lib/n2b/jira_client.rb +753 -0
- data/lib/n2b/llm/claude.rb +14 -3
- data/lib/n2b/llm/gemini.rb +13 -5
- data/lib/n2b/llm/ollama.rb +129 -0
- data/lib/n2b/llm/open_ai.rb +13 -3
- data/lib/n2b/llm/open_router.rb +116 -0
- data/lib/n2b/model_config.rb +139 -0
- data/lib/n2b/version.rb +1 -1
- metadata +10 -4
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(
|
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
|
-
|
19
|
-
|
20
|
-
if
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
54
|
-
|
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
|