Class: N2B::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/n2b/base.rb

Direct Known Subclasses

CLI, MergeCLI

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.config_fileObject



6
7
8
9
10
11
12
13
# File 'lib/n2b/base.rb', line 6

def self.config_file
  # Bulletproof test environment detection
  if test_environment?
    File.expand_path('~/.n2b_test/config.yml')
  else
    ENV['N2B_CONFIG_FILE'] || File.expand_path('~/.n2b/config.yml')
  end
end

.history_fileObject



15
16
17
18
19
20
21
# File 'lib/n2b/base.rb', line 15

def self.history_file
  if test_environment?
    File.expand_path('~/.n2b_test/history')
  else
    ENV['N2B_HISTORY_FILE'] || File.expand_path('~/.n2b/history')
  end
end

.test_environment?Boolean

Returns:

  • (Boolean)


23
24
25
26
27
28
29
30
31
# File 'lib/n2b/base.rb', line 23

def self.test_environment?
  # Multiple ways to detect test environment for maximum safety
  ENV['RAILS_ENV'] == 'test' ||
  ENV['RACK_ENV'] == 'test' ||
  ENV['N2B_TEST_MODE'] == 'true' ||
  $PROGRAM_NAME.include?('rake') ||
  $PROGRAM_NAME.include?('test') ||
  caller.any? { |line| line.include?('test/') || line.include?('minitest') || line.include?('rake') }
end

Instance Method Details

#get_config(reconfigure: false, advanced_flow: false) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/n2b/base.rb', line 41

def get_config(reconfigure: false, advanced_flow: false)
  config = load_config
  api_key = ENV['CLAUDE_API_KEY'] || config['access_key'] # This will be used as default or for existing configs
  _model = config['model'] # Unused but kept for potential future use # Model will be handled per LLM

  # Determine if it's effectively a first-time setup for core LLM details
  is_first_time_core_setup = config['llm'].nil?

  if api_key.nil? || api_key == '' || config['llm'].nil? || reconfigure
    puts "--- N2B Core LLM Configuration ---"
    print "Choose a language model to use (1:claude, 2:openai, 3:gemini, 4:openrouter, 5:ollama) [current: #{config['llm']}]: "
    llm_choice = $stdin.gets.chomp
    llm_choice = config['llm'] if llm_choice.empty? && !config['llm'].nil? # Keep current if input is empty

    unless ['claude', 'openai', 'gemini', 'openrouter', 'ollama', '1', '2', '3', '4', '5'].include?(llm_choice)
      puts "Invalid language model selection. Defaulting to 'claude' or previous if available."
      llm_choice = config['llm'] || 'claude' # Fallback
    end

    selected_llm = case llm_choice
                   when '1', 'claude' then 'claude'
                   when '2', 'openai' then 'openai'
                   when '3', 'gemini' then 'gemini'
                   when '4', 'openrouter' then 'openrouter'
                   when '5', 'ollama' then 'ollama'
                   else config['llm'] || 'claude' # Should not happen due to validation
                   end
    config['llm'] = selected_llm

    if selected_llm == 'ollama'
      puts "\n--- Ollama Specific Configuration ---"
      puts "Ollama typically runs locally and doesn't require an API key."

      # Use new model configuration system
      current_model = config['model']
      model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model) # Renamed to model_choice to avoid conflict
      config['model'] = model_choice

      current_ollama_api_url = config['ollama_api_url'] || N2M::Llm::Ollama::DEFAULT_OLLAMA_API_URI
      print "Enter Ollama API base URL [current: #{current_ollama_api_url}]: "
      ollama_api_url_input = $stdin.gets.chomp
      config['ollama_api_url'] = ollama_api_url_input.empty? ? current_ollama_api_url : ollama_api_url_input
      config.delete('access_key') # Remove access_key if switching to ollama
    else
      # Configuration for API key based LLMs
      puts "\n--- #{selected_llm.capitalize} Specific Configuration ---"
      current_api_key = config['access_key']
      print "Enter your #{selected_llm} API key #{ current_api_key.nil? || current_api_key.empty? ? '' : '[leave blank to keep current]' }: "
      api_key_input = $stdin.gets.chomp
      config['access_key'] = api_key_input if !api_key_input.empty?
      config['access_key'] = current_api_key if api_key_input.empty? && !current_api_key.nil? && !current_api_key.empty?

      if config['access_key'].nil? || config['access_key'].empty?
        puts "API key is required for #{selected_llm}."
        exit 1
      end

      current_model = config['model']
      model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model) # Renamed
      config['model'] = model_choice

      if selected_llm == 'openrouter'
        current_site_url = config['openrouter_site_url'] || ""
        print "Enter your OpenRouter Site URL (optional, for HTTP-Referer) [current: #{current_site_url}]: "
        openrouter_site_url_input = $stdin.gets.chomp
        config['openrouter_site_url'] = openrouter_site_url_input.empty? ? current_site_url : openrouter_site_url_input

        current_site_name = config['openrouter_site_name'] || ""
        print "Enter your OpenRouter Site Name (optional, for X-Title) [current: #{current_site_name}]: "
        openrouter_site_name_input = $stdin.gets.chomp
        config['openrouter_site_name'] = openrouter_site_name_input.empty? ? current_site_name : openrouter_site_name_input
      end
    end

    # --- Advanced Configuration Flow ---
    # Prompt for advanced settings if advanced_flow is true or if it's the first time setting up core LLM.
    prompt_for_advanced = advanced_flow || is_first_time_core_setup

    if prompt_for_advanced
      puts "\n--- Advanced Settings ---"
      print "Would you like to configure advanced settings (e.g., Jira or GitHub integration, privacy)? (y/n) [default: n]: "
      choice = $stdin.gets.chomp.downcase

      if choice == 'y'
        current_tracker = config['issue_tracker'] || 'none'
        print "\nSelect issue tracker to integrate (none, jira, github) [current: #{current_tracker}]: "
        tracker_choice = $stdin.gets.chomp.downcase
        tracker_choice = current_tracker if tracker_choice.empty?
        config['issue_tracker'] = tracker_choice

        case tracker_choice
        when 'jira'
          puts "\n--- Jira Integration ---"
          puts "You can generate a Jira API token here: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/"
          config['jira'] ||= {}
          print "Jira Domain (e.g., your-company.atlassian.net) [current: #{config['jira']['domain']}]: "
          config['jira']['domain'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['domain'] : val }

          print "Jira Email Address [current: #{config['jira']['email']}]: "
          config['jira']['email'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['email'] : val }

          print "Jira API Token #{config['jira']['api_key'] ? '[leave blank to keep current]' : ''}: "
          api_token_input = $stdin.gets.chomp
          config['jira']['api_key'] = api_token_input if !api_token_input.empty?

          print "Default Jira Project Key (optional, e.g., MYPROJ) [current: #{config['jira']['default_project']}]: "
          config['jira']['default_project'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['default_project'] : val }
        when 'github'
          puts "\n--- GitHub Integration ---"
          config['github'] ||= {}
          print "GitHub Repository (owner/repo) [current: #{config['github']['repo']}]: "
          config['github']['repo'] = $stdin.gets.chomp.then { |val| val.empty? ? config['github']['repo'] : val }

          print "GitHub Access Token #{config['github']['access_token'] ? '[leave blank to keep current]' : ''}: "
          token_input = $stdin.gets.chomp
          config['github']['access_token'] = token_input if !token_input.empty?
        else
          config['jira'] ||= {}
          config['github'] ||= {}
        end

        # Privacy Settings
        puts "\n--- Privacy Settings ---"
        config['privacy'] ||= {}
        puts "Allow sending shell command history to the LLM? (true/false) [current: #{config['privacy']['send_shell_history']}]"
        config['privacy']['send_shell_history'] = $stdin.gets.chomp.then { |val| val.empty? ? config['privacy']['send_shell_history'] : (val.downcase == 'true') }

        puts "Allow sending LLM interaction history (your prompts and LLM responses) to the LLM? (true/false) [current: #{config['privacy']['send_llm_history']}]"
        config['privacy']['send_llm_history'] = $stdin.gets.chomp.then { |val| val.empty? ? config['privacy']['send_llm_history'] : (val.downcase == 'true') }

        puts "Allow sending current directory to the LLM? (true/false) [current: #{config['privacy']['send_current_directory']}]"
        config['privacy']['send_current_directory'] = $stdin.gets.chomp.then { |val| val.empty? ? config['privacy']['send_current_directory'] : (val.downcase == 'true') }

        puts "Append n2b generated commands to your shell history file? (true/false) [current: #{config['append_to_shell_history']}]"
        # Note: append_to_shell_history was previously outside 'privacy' hash. Standardizing it inside.
        config['append_to_shell_history'] = $stdin.gets.chomp.then { |val| val.empty? ? config['append_to_shell_history'] : (val.downcase == 'true') }
        config['privacy']['append_to_shell_history'] = config['append_to_shell_history'] # Also place under privacy for consistency

        # Editor Configuration
        prompt_for_editor_config(config)

      else # User chose 'n' for advanced settings
        config['jira'] ||= {}
        config['github'] ||= {}
        config['issue_tracker'] ||= 'none'
        # If they opt out, we don't clear existing, just don't prompt.
        # If it's a fresh setup and they opt out, these will remain empty/nil.

        # Ensure privacy settings are initialized to defaults if not already set by advanced flow
        config['privacy'] ||= {}
        config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
        config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true # Default true
        config['privacy']['send_current_directory'] = config['privacy']['send_current_directory'] || true # Default true
        config['append_to_shell_history'] = config['append_to_shell_history'] || false
        config['privacy']['append_to_shell_history'] = config['append_to_shell_history']
      end
    else # Not prompting for advanced (neither advanced_flow nor first_time_core_setup)
      # Ensure defaults for privacy if they don't exist from a previous config
      config['jira'] ||= {}
      config['github'] ||= {}
      config['issue_tracker'] ||= 'none'
      config['privacy'] ||= {}
      config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
      config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true
      config['privacy']['send_current_directory'] = config['privacy']['send_current_directory'] || true
      config['append_to_shell_history'] = config['append_to_shell_history'] || false
      config['privacy']['append_to_shell_history'] = config['append_to_shell_history']
    end

    # Editor Configuration
    config['editor'] ||= {}
    config['editor']['command'] ||= nil # or 'nano', 'vi'
    config['editor']['type'] ||= nil # 'text_editor' or 'diff_tool'
    config['editor']['configured'] ||= false

    # Validate configuration before saving
    validation_errors = validate_config(config)
    if validation_errors.any?
      puts "\n⚠️  Configuration validation warnings:"
      validation_errors.each { |error| puts "  - #{error}" }
      puts "Configuration saved with warnings."
    end

    puts "\nConfiguration saved to #{self.class.config_file}"
    FileUtils.mkdir_p(File.dirname(self.class.config_file)) unless File.exist?(File.dirname(self.class.config_file))
    File.write(self.class.config_file, config.to_yaml)
  else
    # If not reconfiguring, still ensure privacy and jira keys exist with defaults if missing
    # This handles configs from before these settings were introduced
    config['jira'] ||= {}
    config['github'] ||= {}
    config['issue_tracker'] ||= 'none'
    config['privacy'] ||= {}
    config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
    config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true
    config['privacy']['send_current_directory'] = config['privacy']['send_current_directory'] || true
    # append_to_shell_history was outside 'privacy' before, ensure it's correctly defaulted
    # and also placed under 'privacy' for future consistency.
    current_append_setting = config.key?('append_to_shell_history') ? config['append_to_shell_history'] : false
    config['append_to_shell_history'] = current_append_setting
    config['privacy']['append_to_shell_history'] = config['privacy']['append_to_shell_history'] || current_append_setting

    # Ensure editor config is initialized if missing (for older configs)
    config['editor'] ||= {}
    config['editor']['command'] ||= nil
    config['editor']['type'] ||= nil
    config['editor']['configured'] ||= false
  end
  config
end

#load_configObject



33
34
35
36
37
38
39
# File 'lib/n2b/base.rb', line 33

def load_config
  if File.exist?(self.class.config_file)
    YAML.load_file(self.class.config_file)
  else
    { }
  end
end