n2b 0.7.1 → 2.1.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 +291 -118
- data/bin/n2b-test-github +22 -0
- data/lib/n2b/base.rb +346 -55
- data/lib/n2b/cli.rb +60 -404
- data/lib/n2b/config/models.yml +19 -3
- data/lib/n2b/github_client.rb +391 -0
- data/lib/n2b/jira_client.rb +238 -38
- data/lib/n2b/llm/claude.rb +1 -1
- data/lib/n2b/llm/gemini.rb +7 -2
- data/lib/n2b/llm/open_ai.rb +1 -1
- data/lib/n2b/llm/vertex_ai.rb +225 -0
- data/lib/n2b/merge_cli.rb +1774 -136
- data/lib/n2b/message_utils.rb +59 -0
- data/lib/n2b/model_config.rb +8 -0
- data/lib/n2b/templates/diff_system_prompt.txt +40 -20
- data/lib/n2b/templates/github_comment.txt +67 -0
- data/lib/n2b/templates/jira_comment.txt +7 -0
- data/lib/n2b/templates/merge_conflict_prompt.txt +2 -2
- data/lib/n2b/version.rb +1 -1
- data/lib/n2b.rb +1 -0
- metadata +21 -2
data/lib/n2b/base.rb
CHANGED
@@ -3,12 +3,36 @@ require_relative 'model_config'
|
|
3
3
|
module N2B
|
4
4
|
class Base
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
def self.config_file
|
7
|
+
# Bulletproof test environment detection
|
8
|
+
if test_environment?
|
9
|
+
File.expand_path('~/.n2b_test/config.yml')
|
10
|
+
else
|
11
|
+
ENV['N2B_CONFIG_FILE'] || File.expand_path('~/.n2b/config.yml')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.history_file
|
16
|
+
if test_environment?
|
17
|
+
File.expand_path('~/.n2b_test/history')
|
18
|
+
else
|
19
|
+
ENV['N2B_HISTORY_FILE'] || File.expand_path('~/.n2b/history')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.test_environment?
|
24
|
+
# Multiple ways to detect test environment for maximum safety
|
25
|
+
ENV['RAILS_ENV'] == 'test' ||
|
26
|
+
ENV['RACK_ENV'] == 'test' ||
|
27
|
+
ENV['N2B_TEST_MODE'] == 'true' ||
|
28
|
+
$PROGRAM_NAME.include?('rake') ||
|
29
|
+
$PROGRAM_NAME.include?('test') ||
|
30
|
+
caller.any? { |line| line.include?('test/') || line.include?('minitest') || line.include?('rake') }
|
31
|
+
end
|
8
32
|
|
9
33
|
def load_config
|
10
|
-
if File.exist?(
|
11
|
-
YAML.load_file(
|
34
|
+
if File.exist?(self.class.config_file)
|
35
|
+
YAML.load_file(self.class.config_file)
|
12
36
|
else
|
13
37
|
{ }
|
14
38
|
end
|
@@ -22,13 +46,19 @@ module N2B
|
|
22
46
|
# Determine if it's effectively a first-time setup for core LLM details
|
23
47
|
is_first_time_core_setup = config['llm'].nil?
|
24
48
|
|
25
|
-
|
49
|
+
# Check if configuration is incomplete based on provider type
|
50
|
+
config_incomplete = config['llm'].nil? || reconfigure ||
|
51
|
+
(config['llm'] == 'vertexai' && (config['vertex_credential_file'].nil? || config['vertex_credential_file'].empty?)) ||
|
52
|
+
(config['llm'] == 'ollama' && false) || # Ollama doesn't require API key, so never incomplete due to missing key
|
53
|
+
(['claude', 'openai', 'gemini', 'openrouter'].include?(config['llm']) && (api_key.nil? || api_key == ''))
|
54
|
+
|
55
|
+
if config_incomplete
|
26
56
|
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']}]: "
|
57
|
+
print "Choose a language model to use (1:claude, 2:openai, 3:gemini (API Key), 4:openrouter, 5:ollama, 6:vertexai (Credential File)) [current: #{config['llm']}]: "
|
28
58
|
llm_choice = $stdin.gets.chomp
|
29
59
|
llm_choice = config['llm'] if llm_choice.empty? && !config['llm'].nil? # Keep current if input is empty
|
30
60
|
|
31
|
-
unless ['claude', 'openai', 'gemini', 'openrouter', 'ollama', '1', '2', '3', '4', '5'].include?(llm_choice)
|
61
|
+
unless ['claude', 'openai', 'gemini', 'openrouter', 'ollama', 'vertexai', '1', '2', '3', '4', '5', '6'].include?(llm_choice)
|
32
62
|
puts "Invalid language model selection. Defaulting to 'claude' or previous if available."
|
33
63
|
llm_choice = config['llm'] || 'claude' # Fallback
|
34
64
|
end
|
@@ -36,10 +66,11 @@ module N2B
|
|
36
66
|
selected_llm = case llm_choice
|
37
67
|
when '1', 'claude' then 'claude'
|
38
68
|
when '2', 'openai' then 'openai'
|
39
|
-
when '3', 'gemini' then 'gemini'
|
69
|
+
when '3', 'gemini' then 'gemini' # This is now explicitly API Key based Gemini
|
40
70
|
when '4', 'openrouter' then 'openrouter'
|
41
71
|
when '5', 'ollama' then 'ollama'
|
42
|
-
|
72
|
+
when '6', 'vertexai' then 'vertexai' # New Vertex AI (credential file)
|
73
|
+
else config['llm'] || 'claude'
|
43
74
|
end
|
44
75
|
config['llm'] = selected_llm
|
45
76
|
|
@@ -47,18 +78,76 @@ module N2B
|
|
47
78
|
puts "\n--- Ollama Specific Configuration ---"
|
48
79
|
puts "Ollama typically runs locally and doesn't require an API key."
|
49
80
|
|
50
|
-
# Use new model configuration system
|
51
81
|
current_model = config['model']
|
52
|
-
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
|
82
|
+
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
|
53
83
|
config['model'] = model_choice
|
54
84
|
|
55
85
|
current_ollama_api_url = config['ollama_api_url'] || N2M::Llm::Ollama::DEFAULT_OLLAMA_API_URI
|
56
86
|
print "Enter Ollama API base URL [current: #{current_ollama_api_url}]: "
|
57
87
|
ollama_api_url_input = $stdin.gets.chomp
|
58
88
|
config['ollama_api_url'] = ollama_api_url_input.empty? ? current_ollama_api_url : ollama_api_url_input
|
59
|
-
|
60
|
-
|
61
|
-
#
|
89
|
+
|
90
|
+
config.delete('access_key')
|
91
|
+
config.delete('gemini_credential_file') # old key
|
92
|
+
config.delete('vertex_credential_file') # new key for Vertex
|
93
|
+
|
94
|
+
elsif selected_llm == 'vertexai'
|
95
|
+
puts "\n--- Vertex AI (Credential File) Specific Configuration ---"
|
96
|
+
current_vertex_credential_file = config['vertex_credential_file']
|
97
|
+
print "Enter your Google Cloud credential file path for Vertex AI #{current_vertex_credential_file ? '[leave blank to keep current]' : ''}: "
|
98
|
+
vertex_credential_file_input = $stdin.gets.chomp
|
99
|
+
if !vertex_credential_file_input.empty?
|
100
|
+
config['vertex_credential_file'] = vertex_credential_file_input
|
101
|
+
elsif current_vertex_credential_file
|
102
|
+
config['vertex_credential_file'] = current_vertex_credential_file
|
103
|
+
else
|
104
|
+
config['vertex_credential_file'] = nil
|
105
|
+
end
|
106
|
+
|
107
|
+
# Ask for Vertex AI region
|
108
|
+
current_vertex_location = config['vertex_location'] || 'us-central1'
|
109
|
+
puts "\nVertex AI Region Selection:"
|
110
|
+
puts "Common regions:"
|
111
|
+
puts " 1. us-central1 (Iowa, USA) [default]"
|
112
|
+
puts " 2. us-east1 (South Carolina, USA)"
|
113
|
+
puts " 3. us-west1 (Oregon, USA)"
|
114
|
+
puts " 4. europe-west1 (Belgium)"
|
115
|
+
puts " 5. europe-west4 (Netherlands)"
|
116
|
+
puts " 6. asia-northeast1 (Tokyo, Japan)"
|
117
|
+
puts " 7. asia-southeast1 (Singapore)"
|
118
|
+
puts " 8. custom (enter your own region)"
|
119
|
+
print "Choose region (1-8) or enter region name [current: #{current_vertex_location}]: "
|
120
|
+
|
121
|
+
region_choice = $stdin.gets.chomp
|
122
|
+
if region_choice.empty?
|
123
|
+
config['vertex_location'] = current_vertex_location
|
124
|
+
else
|
125
|
+
case region_choice
|
126
|
+
when '1' then config['vertex_location'] = 'us-central1'
|
127
|
+
when '2' then config['vertex_location'] = 'us-east1'
|
128
|
+
when '3' then config['vertex_location'] = 'us-west1'
|
129
|
+
when '4' then config['vertex_location'] = 'europe-west1'
|
130
|
+
when '5' then config['vertex_location'] = 'europe-west4'
|
131
|
+
when '6' then config['vertex_location'] = 'asia-northeast1'
|
132
|
+
when '7' then config['vertex_location'] = 'asia-southeast1'
|
133
|
+
when '8'
|
134
|
+
print "Enter custom region (e.g., europe-west2): "
|
135
|
+
custom_region = $stdin.gets.chomp
|
136
|
+
config['vertex_location'] = custom_region.empty? ? current_vertex_location : custom_region
|
137
|
+
else
|
138
|
+
# Treat as direct region input
|
139
|
+
config['vertex_location'] = region_choice
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
config.delete('access_key')
|
144
|
+
config.delete('ollama_api_url')
|
145
|
+
config.delete('gemini_credential_file') # old key
|
146
|
+
|
147
|
+
current_model = config['model']
|
148
|
+
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
|
149
|
+
config['model'] = model_choice
|
150
|
+
else # API Key based LLMs: claude, openai, gemini (API Key), openrouter
|
62
151
|
puts "\n--- #{selected_llm.capitalize} Specific Configuration ---"
|
63
152
|
current_api_key = config['access_key']
|
64
153
|
print "Enter your #{selected_llm} API key #{ current_api_key.nil? || current_api_key.empty? ? '' : '[leave blank to keep current]' }: "
|
@@ -70,9 +159,12 @@ module N2B
|
|
70
159
|
puts "API key is required for #{selected_llm}."
|
71
160
|
exit 1
|
72
161
|
end
|
162
|
+
config.delete('vertex_credential_file') # new key for Vertex
|
163
|
+
config.delete('ollama_api_url')
|
164
|
+
config.delete('gemini_credential_file') # old key
|
73
165
|
|
74
166
|
current_model = config['model']
|
75
|
-
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
|
167
|
+
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
|
76
168
|
config['model'] = model_choice
|
77
169
|
|
78
170
|
if selected_llm == 'openrouter'
|
@@ -94,26 +186,46 @@ module N2B
|
|
94
186
|
|
95
187
|
if prompt_for_advanced
|
96
188
|
puts "\n--- Advanced Settings ---"
|
97
|
-
print "Would you like to configure advanced settings (e.g., Jira integration, privacy)? (y/n) [default: n]: "
|
189
|
+
print "Would you like to configure advanced settings (e.g., Jira or GitHub integration, privacy)? (y/n) [default: n]: "
|
98
190
|
choice = $stdin.gets.chomp.downcase
|
99
191
|
|
100
192
|
if choice == 'y'
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
193
|
+
current_tracker = config['issue_tracker'] || 'none'
|
194
|
+
print "\nSelect issue tracker to integrate (none, jira, github) [current: #{current_tracker}]: "
|
195
|
+
tracker_choice = $stdin.gets.chomp.downcase
|
196
|
+
tracker_choice = current_tracker if tracker_choice.empty?
|
197
|
+
config['issue_tracker'] = tracker_choice
|
198
|
+
|
199
|
+
case tracker_choice
|
200
|
+
when 'jira'
|
201
|
+
puts "\n--- Jira Integration ---"
|
202
|
+
puts "You can generate a Jira API token here: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/"
|
203
|
+
config['jira'] ||= {}
|
204
|
+
print "Jira Domain (e.g., your-company.atlassian.net) [current: #{config['jira']['domain']}]: "
|
205
|
+
config['jira']['domain'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['domain'] : val }
|
206
|
+
|
207
|
+
print "Jira Email Address [current: #{config['jira']['email']}]: "
|
208
|
+
config['jira']['email'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['email'] : val }
|
209
|
+
|
210
|
+
print "Jira API Token #{config['jira']['api_key'] ? '[leave blank to keep current]' : ''}: "
|
211
|
+
api_token_input = $stdin.gets.chomp
|
212
|
+
config['jira']['api_key'] = api_token_input if !api_token_input.empty?
|
213
|
+
|
214
|
+
print "Default Jira Project Key (optional, e.g., MYPROJ) [current: #{config['jira']['default_project']}]: "
|
215
|
+
config['jira']['default_project'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['default_project'] : val }
|
216
|
+
when 'github'
|
217
|
+
puts "\n--- GitHub Integration ---"
|
218
|
+
config['github'] ||= {}
|
219
|
+
print "GitHub Repository (owner/repo) [current: #{config['github']['repo']}]: "
|
220
|
+
config['github']['repo'] = $stdin.gets.chomp.then { |val| val.empty? ? config['github']['repo'] : val }
|
221
|
+
|
222
|
+
print "GitHub Access Token #{config['github']['access_token'] ? '[leave blank to keep current]' : ''}: "
|
223
|
+
token_input = $stdin.gets.chomp
|
224
|
+
config['github']['access_token'] = token_input if !token_input.empty?
|
225
|
+
else
|
226
|
+
config['jira'] ||= {}
|
227
|
+
config['github'] ||= {}
|
228
|
+
end
|
117
229
|
|
118
230
|
# Privacy Settings
|
119
231
|
puts "\n--- Privacy Settings ---"
|
@@ -132,9 +244,13 @@ module N2B
|
|
132
244
|
config['append_to_shell_history'] = $stdin.gets.chomp.then { |val| val.empty? ? config['append_to_shell_history'] : (val.downcase == 'true') }
|
133
245
|
config['privacy']['append_to_shell_history'] = config['append_to_shell_history'] # Also place under privacy for consistency
|
134
246
|
|
247
|
+
# Editor Configuration
|
248
|
+
prompt_for_editor_config(config)
|
249
|
+
|
135
250
|
else # User chose 'n' for advanced settings
|
136
|
-
|
137
|
-
config['
|
251
|
+
config['jira'] ||= {}
|
252
|
+
config['github'] ||= {}
|
253
|
+
config['issue_tracker'] ||= 'none'
|
138
254
|
# If they opt out, we don't clear existing, just don't prompt.
|
139
255
|
# If it's a fresh setup and they opt out, these will remain empty/nil.
|
140
256
|
|
@@ -148,7 +264,9 @@ module N2B
|
|
148
264
|
end
|
149
265
|
else # Not prompting for advanced (neither advanced_flow nor first_time_core_setup)
|
150
266
|
# Ensure defaults for privacy if they don't exist from a previous config
|
151
|
-
config['jira'] ||= {}
|
267
|
+
config['jira'] ||= {}
|
268
|
+
config['github'] ||= {}
|
269
|
+
config['issue_tracker'] ||= 'none'
|
152
270
|
config['privacy'] ||= {}
|
153
271
|
config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
|
154
272
|
config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true
|
@@ -157,6 +275,12 @@ module N2B
|
|
157
275
|
config['privacy']['append_to_shell_history'] = config['append_to_shell_history']
|
158
276
|
end
|
159
277
|
|
278
|
+
# Editor Configuration
|
279
|
+
config['editor'] ||= {}
|
280
|
+
config['editor']['command'] ||= nil # or 'nano', 'vi'
|
281
|
+
config['editor']['type'] ||= nil # 'text_editor' or 'diff_tool'
|
282
|
+
config['editor']['configured'] ||= false
|
283
|
+
|
160
284
|
# Validate configuration before saving
|
161
285
|
validation_errors = validate_config(config)
|
162
286
|
if validation_errors.any?
|
@@ -165,13 +289,15 @@ module N2B
|
|
165
289
|
puts "Configuration saved with warnings."
|
166
290
|
end
|
167
291
|
|
168
|
-
puts "\nConfiguration saved to #{
|
169
|
-
FileUtils.mkdir_p(File.dirname(
|
170
|
-
File.write(
|
292
|
+
puts "\nConfiguration saved to #{self.class.config_file}"
|
293
|
+
FileUtils.mkdir_p(File.dirname(self.class.config_file)) unless File.exist?(File.dirname(self.class.config_file))
|
294
|
+
File.write(self.class.config_file, config.to_yaml)
|
171
295
|
else
|
172
296
|
# If not reconfiguring, still ensure privacy and jira keys exist with defaults if missing
|
173
297
|
# This handles configs from before these settings were introduced
|
174
298
|
config['jira'] ||= {}
|
299
|
+
config['github'] ||= {}
|
300
|
+
config['issue_tracker'] ||= 'none'
|
175
301
|
config['privacy'] ||= {}
|
176
302
|
config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
|
177
303
|
config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true
|
@@ -181,46 +307,211 @@ module N2B
|
|
181
307
|
current_append_setting = config.key?('append_to_shell_history') ? config['append_to_shell_history'] : false
|
182
308
|
config['append_to_shell_history'] = current_append_setting
|
183
309
|
config['privacy']['append_to_shell_history'] = config['privacy']['append_to_shell_history'] || current_append_setting
|
310
|
+
|
311
|
+
# Ensure editor config is initialized if missing (for older configs)
|
312
|
+
config['editor'] ||= {}
|
313
|
+
config['editor']['command'] ||= nil
|
314
|
+
config['editor']['type'] ||= nil
|
315
|
+
config['editor']['configured'] ||= false
|
184
316
|
end
|
185
317
|
config
|
186
318
|
end
|
187
319
|
|
188
320
|
private
|
189
321
|
|
322
|
+
def command_exists?(command)
|
323
|
+
# Check for Windows or Unix-like systems for the correct command
|
324
|
+
null_device = RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ ? 'NUL' : '/dev/null'
|
325
|
+
system("which #{command} > #{null_device} 2>&1") || system("where #{command} > #{null_device} 2>&1")
|
326
|
+
end
|
327
|
+
|
328
|
+
def prompt_for_editor_config(config)
|
329
|
+
puts "\n--- Editor Configuration ---"
|
330
|
+
current_editor_command = config.dig('editor', 'command')
|
331
|
+
current_editor_type = config.dig('editor', 'type')
|
332
|
+
current_status = if current_editor_command
|
333
|
+
"Current: #{current_editor_command} (#{current_editor_type || 'not set'})"
|
334
|
+
else
|
335
|
+
"Current: Not configured"
|
336
|
+
end
|
337
|
+
puts current_status
|
338
|
+
|
339
|
+
known_editors = [
|
340
|
+
{ name: 'nano', command: 'nano', description: 'Simple terminal editor', type: 'text_editor' },
|
341
|
+
{ name: 'vim', command: 'vim', description: 'Powerful terminal editor', type: 'text_editor' },
|
342
|
+
{ name: 'vi', command: 'vi', description: 'Standard Unix terminal editor', type: 'text_editor' },
|
343
|
+
{ name: 'code', command: 'code', description: 'Visual Studio Code (requires "code" in PATH)', type: 'text_editor' },
|
344
|
+
{ name: 'subl', command: 'subl', description: 'Sublime Text (requires "subl" in PATH)', type: 'text_editor' },
|
345
|
+
{ name: 'meld', command: 'meld', description: 'Visual diff and merge tool', type: 'diff_tool' },
|
346
|
+
{ name: 'kdiff3', command: 'kdiff3', description: 'Visual diff and merge tool (KDE)', type: 'diff_tool' },
|
347
|
+
{ name: 'opendiff', command: 'opendiff', description: 'File comparison tool (macOS)', type: 'diff_tool' },
|
348
|
+
{ name: 'vimdiff', command: 'vimdiff', description: 'Diff tool using Vim', type: 'diff_tool' }
|
349
|
+
]
|
350
|
+
|
351
|
+
available_editors = known_editors.select { |editor| command_exists?(editor[:command]) }
|
352
|
+
|
353
|
+
if available_editors.empty?
|
354
|
+
puts "No standard editors detected automatically."
|
355
|
+
else
|
356
|
+
puts "Choose your preferred editor/diff tool:"
|
357
|
+
available_editors.each_with_index do |editor, index|
|
358
|
+
puts "#{index + 1}. #{editor[:name]} (#{editor[:description]})"
|
359
|
+
end
|
360
|
+
puts "#{available_editors.length + 1}. Custom (enter your own command)"
|
361
|
+
print "Enter choice (1-#{available_editors.length + 1}) or leave blank to skip: "
|
362
|
+
end
|
363
|
+
|
364
|
+
choice_input = $stdin.gets.chomp
|
365
|
+
return if choice_input.empty? && current_editor_command # Skip if already configured and user wants to skip
|
366
|
+
|
367
|
+
choice = choice_input.to_i
|
368
|
+
|
369
|
+
selected_editor = nil
|
370
|
+
custom_command = nil
|
371
|
+
|
372
|
+
if choice > 0 && choice <= available_editors.length
|
373
|
+
selected_editor = available_editors[choice - 1]
|
374
|
+
config['editor']['command'] = selected_editor[:command]
|
375
|
+
config['editor']['type'] = selected_editor[:type]
|
376
|
+
config['editor']['configured'] = true
|
377
|
+
puts "✓ Using #{selected_editor[:name]} as your editor/diff tool."
|
378
|
+
elsif choice == available_editors.length + 1 || (available_editors.empty? && !choice_input.empty?)
|
379
|
+
print "Enter custom editor command: "
|
380
|
+
custom_command = $stdin.gets.chomp
|
381
|
+
if custom_command.empty?
|
382
|
+
puts "No command entered. Editor configuration skipped."
|
383
|
+
return
|
384
|
+
end
|
385
|
+
|
386
|
+
print "Is this a 'text_editor' or a 'diff_tool'? (text_editor/diff_tool): "
|
387
|
+
custom_type = $stdin.gets.chomp.downcase
|
388
|
+
unless ['text_editor', 'diff_tool'].include?(custom_type)
|
389
|
+
puts "Invalid type. Defaulting to 'text_editor'."
|
390
|
+
custom_type = 'text_editor'
|
391
|
+
end
|
392
|
+
config['editor']['command'] = custom_command
|
393
|
+
config['editor']['type'] = custom_type
|
394
|
+
config['editor']['configured'] = true
|
395
|
+
puts "✓ Using custom command '#{custom_command}' (#{custom_type}) as your editor/diff tool."
|
396
|
+
else
|
397
|
+
puts "Invalid choice. Editor configuration skipped."
|
398
|
+
# Keep existing config if invalid choice, or clear if they wanted to change but failed?
|
399
|
+
# For now, just skipping and keeping whatever was there.
|
400
|
+
return
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
190
404
|
def validate_config(config)
|
191
405
|
errors = []
|
192
406
|
|
193
|
-
# Validate LLM
|
407
|
+
# Validate LLM provider selection
|
194
408
|
if config['llm'].nil? || config['llm'].empty?
|
195
409
|
errors << "LLM provider not specified"
|
196
410
|
end
|
197
411
|
|
198
|
-
# Validate model name
|
412
|
+
# Validate model name (existing logic can likely stay as is)
|
199
413
|
if config['model'].nil? || config['model'].empty?
|
200
414
|
errors << "Model not specified"
|
201
|
-
elsif config['model'].length < 3
|
415
|
+
elsif config['model'].length < 3 # Example of an existing check
|
202
416
|
errors << "Model name '#{config['model']}' seems too short - might be invalid"
|
203
|
-
elsif %w[y n yes no true false].include?(config['model'].downcase)
|
417
|
+
elsif %w[y n yes no true false].include?(config['model'].downcase) # Example
|
204
418
|
errors << "Model name '#{config['model']}' appears to be a boolean response rather than a model name"
|
205
419
|
end
|
206
420
|
|
207
|
-
#
|
208
|
-
|
209
|
-
|
210
|
-
|
421
|
+
# Provider-specific validation
|
422
|
+
case config['llm']
|
423
|
+
when 'gemini' # API Key based Gemini
|
424
|
+
if config['access_key'].nil? || config['access_key'].empty?
|
425
|
+
errors << "API key missing for gemini provider"
|
426
|
+
end
|
427
|
+
if config['vertex_credential_file'] && !config['vertex_credential_file'].empty?
|
428
|
+
errors << "Vertex AI credential file should not be present when gemini (API Key) provider is selected"
|
429
|
+
end
|
430
|
+
# Ensure old gemini_credential_file key is also not present
|
431
|
+
if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
|
432
|
+
errors << "Old gemini_credential_file key should not be present. Use 'gemini' (API key) or 'vertexai' (credential file)."
|
433
|
+
end
|
211
434
|
|
212
|
-
#
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
#
|
217
|
-
|
218
|
-
errors << "
|
435
|
+
when 'vertexai' # Credential File based Vertex AI
|
436
|
+
if config['vertex_credential_file'].nil? || config['vertex_credential_file'].empty?
|
437
|
+
errors << "Credential file path for Vertex AI not provided"
|
438
|
+
else
|
439
|
+
# Ensure File.exist? is only called if the path is not nil and not empty
|
440
|
+
unless File.exist?(config['vertex_credential_file'])
|
441
|
+
errors << "Credential file missing or invalid at #{config['vertex_credential_file']} for Vertex AI"
|
219
442
|
end
|
220
|
-
|
221
|
-
|
443
|
+
end
|
444
|
+
if config['access_key'] && !config['access_key'].empty?
|
445
|
+
errors << "API key (access_key) should not be present when Vertex AI provider is selected"
|
446
|
+
end
|
447
|
+
# Ensure old gemini_credential_file key is also not present
|
448
|
+
if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
|
449
|
+
errors << "Old gemini_credential_file key should not be present when Vertex AI is selected."
|
450
|
+
end
|
451
|
+
|
452
|
+
when 'ollama'
|
453
|
+
# No specific key/credential validation for Ollama here, but ensure others are not present
|
454
|
+
if config['access_key'] && !config['access_key'].empty?
|
455
|
+
errors << "API key (access_key) should not be present when Ollama provider is selected"
|
456
|
+
end
|
457
|
+
if config['vertex_credential_file'] && !config['vertex_credential_file'].empty?
|
458
|
+
errors << "Vertex AI credential file should not be present when Ollama provider is selected"
|
459
|
+
end
|
460
|
+
if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
|
461
|
+
errors << "Old gemini_credential_file key should not be present when Ollama is selected."
|
462
|
+
end
|
463
|
+
# Ollama specific validations like ollama_api_url could be here if needed
|
464
|
+
|
465
|
+
when 'claude', 'openai', 'openrouter' # Other API key based providers
|
466
|
+
if config['access_key'].nil? || config['access_key'].empty?
|
467
|
+
errors << "API key missing for #{config['llm']} provider"
|
468
|
+
end
|
469
|
+
if config['vertex_credential_file'] && !config['vertex_credential_file'].empty?
|
470
|
+
errors << "Vertex AI credential file should not be present for #{config['llm']} provider"
|
471
|
+
end
|
472
|
+
if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
|
473
|
+
errors << "Old gemini_credential_file key should not be present for #{config['llm']} provider."
|
474
|
+
end
|
475
|
+
# else
|
476
|
+
# This case means llm is nil or an unknown provider, already handled by the first check.
|
477
|
+
end
|
478
|
+
|
479
|
+
# Validate editor configuration (optional, so more like warnings or info)
|
480
|
+
# Example: Check if command is set if configured is true
|
481
|
+
# if config['editor'] && config['editor']['configured'] && (config['editor']['command'].nil? || config['editor']['command'].empty?)
|
482
|
+
# errors << "Editor is marked as configured, but no command is set."
|
483
|
+
# end
|
484
|
+
|
485
|
+
tracker = config['issue_tracker'] || 'none'
|
486
|
+
case tracker
|
487
|
+
when 'jira'
|
488
|
+
if config['jira'] && !config['jira'].empty?
|
489
|
+
jira_config = config['jira']
|
490
|
+
if jira_config['domain'] && !jira_config['domain'].empty?
|
491
|
+
if jira_config['email'].nil? || jira_config['email'].empty?
|
492
|
+
errors << "Jira email missing when domain is configured"
|
493
|
+
end
|
494
|
+
if jira_config['api_key'].nil? || jira_config['api_key'].empty?
|
495
|
+
errors << "Jira API key missing when domain is configured"
|
496
|
+
end
|
497
|
+
else
|
498
|
+
errors << "Jira domain missing when issue_tracker is set to 'jira'"
|
222
499
|
end
|
500
|
+
else
|
501
|
+
errors << "Jira configuration missing when issue_tracker is set to 'jira'"
|
223
502
|
end
|
503
|
+
when 'github'
|
504
|
+
if config['github'] && !config['github'].empty?
|
505
|
+
gh = config['github']
|
506
|
+
errors << "GitHub repository missing when issue_tracker is set to 'github'" if gh['repo'].nil? || gh['repo'].empty?
|
507
|
+
errors << "GitHub access token missing when issue_tracker is set to 'github'" if gh['access_token'].nil? || gh['access_token'].empty?
|
508
|
+
else
|
509
|
+
errors << "GitHub configuration missing when issue_tracker is set to 'github'"
|
510
|
+
end
|
511
|
+
when 'none'
|
512
|
+
# No validation needed for 'none' tracker
|
513
|
+
else
|
514
|
+
errors << "Invalid issue_tracker '#{tracker}' - must be 'jira', 'github', or 'none'"
|
224
515
|
end
|
225
516
|
|
226
517
|
errors
|