n2b 0.5.1 → 2.0.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/lib/n2b/cli.rb CHANGED
@@ -1,5 +1,3 @@
1
- require_relative 'jira_client' # For N2B::JiraClient
2
-
3
1
  module N2B
4
2
  class CLI < Base
5
3
  def self.run(args)
@@ -16,281 +14,27 @@ module N2B
16
14
  config = get_config(reconfigure: @options[:config], advanced_flow: @options[:advanced_config])
17
15
  user_input = @args.join(' ') # All remaining args form user input/prompt addition
18
16
 
19
- if @options[:diff]
20
- handle_diff_analysis(config)
21
- elsif user_input.empty? # No input text after options
22
- puts "Enter your natural language command:"
23
- input_text = $stdin.gets.chomp
24
- process_natural_language_command(input_text, config)
25
- else # Natural language command
17
+ # Diff functionality has been removed from N2B::CLI
18
+ # if @options[:diff]
19
+ # handle_diff_analysis(config)
20
+ # els
21
+ if user_input.empty? # No input text after options
22
+ # If config mode was chosen, it's handled by get_config.
23
+ # If not, and no input, prompt for it.
24
+ unless @options[:config] # Don't prompt if only -c or --advanced-config was used
25
+ puts "Enter your natural language command (or type 'exit' or 'quit'):"
26
+ input_text = $stdin.gets.chomp
27
+ exit if ['exit', 'quit'].include?(input_text.downcase)
28
+ process_natural_language_command(input_text, config)
29
+ end
30
+ else # Natural language command provided as argument
26
31
  process_natural_language_command(user_input, config)
27
32
  end
28
33
  end
29
34
 
30
35
  protected
31
36
 
32
- def handle_diff_analysis(config)
33
- vcs_type = get_vcs_type
34
- if vcs_type == :none
35
- puts "Error: Not a git or hg repository."
36
- exit 1
37
- end
38
-
39
- # Get requirements file from parsed options
40
- requirements_filepath = @options[:requirements]
41
- user_prompt_addition = @args.join(' ') # All remaining args are user prompt addition
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
93
- puts "Error: Requirements file not found: #{requirements_filepath}"
94
- # Decide if to exit or proceed. For now, proceed.
95
- puts "Proceeding with diff analysis without file-based requirements."
96
- end
97
- elsif requirements_content && requirements_filepath
98
- puts "Note: Both Jira ticket and requirements file were provided. Using Jira ticket content for analysis."
99
- end
100
-
101
- diff_output = execute_vcs_diff(vcs_type, @options[:branch])
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
150
- end
151
-
152
- def get_vcs_type
153
- if Dir.exist?(File.join(Dir.pwd, '.git'))
154
- :git
155
- elsif Dir.exist?(File.join(Dir.pwd, '.hg'))
156
- :hg
157
- else
158
- :none
159
- end
160
- end
161
-
162
- def execute_vcs_diff(vcs_type, branch_option = nil)
163
- case vcs_type
164
- when :git
165
- if branch_option
166
- target_branch = branch_option == 'auto' ? detect_git_default_branch : branch_option
167
- if target_branch
168
- # Validate that the target branch exists
169
- unless validate_git_branch_exists(target_branch)
170
- puts "Error: Branch '#{target_branch}' does not exist."
171
- puts "Available branches:"
172
- puts `git branch -a`.lines.map(&:strip).reject(&:empty?)
173
- exit 1
174
- end
175
-
176
- puts "Comparing current branch against '#{target_branch}'..."
177
- `git diff #{target_branch}...HEAD`
178
- else
179
- puts "Could not detect default branch, falling back to HEAD diff..."
180
- `git diff HEAD`
181
- end
182
- else
183
- `git diff HEAD`
184
- end
185
- when :hg
186
- if branch_option
187
- target_branch = branch_option == 'auto' ? detect_hg_default_branch : branch_option
188
- if target_branch
189
- # Validate that the target branch exists
190
- unless validate_hg_branch_exists(target_branch)
191
- puts "Error: Branch '#{target_branch}' does not exist."
192
- puts "Available branches:"
193
- puts `hg branches`.lines.map(&:strip).reject(&:empty?)
194
- exit 1
195
- end
196
-
197
- puts "Comparing current branch against '#{target_branch}'..."
198
- `hg diff -r #{target_branch}`
199
- else
200
- puts "Could not detect default branch, falling back to standard diff..."
201
- `hg diff`
202
- end
203
- else
204
- `hg diff`
205
- end
206
- else
207
- "" # Should not happen if get_vcs_type logic is correct and checked before calling
208
- end
209
- end
210
-
211
- def detect_git_default_branch
212
- # Try multiple methods to detect the default branch
213
-
214
- # Method 1: Check origin/HEAD symbolic ref
215
- result = `git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null`.strip
216
- if $?.success? && !result.empty?
217
- return result.split('/').last
218
- end
219
-
220
- # Method 2: Check remote show origin
221
- result = `git remote show origin 2>/dev/null | grep "HEAD branch"`.strip
222
- if $?.success? && !result.empty?
223
- match = result.match(/HEAD branch:\s*(\w+)/)
224
- return match[1] if match
225
- end
226
-
227
- # Method 3: Check if common default branches exist
228
- ['main', 'master'].each do |branch|
229
- result = `git rev-parse --verify origin/#{branch} 2>/dev/null`
230
- if $?.success?
231
- return branch
232
- end
233
- end
234
-
235
- # Method 4: Fallback - check local branches
236
- ['main', 'master'].each do |branch|
237
- result = `git rev-parse --verify #{branch} 2>/dev/null`
238
- if $?.success?
239
- return branch
240
- end
241
- end
242
-
243
- # If all else fails, return nil
244
- nil
245
- end
246
-
247
- def detect_hg_default_branch
248
- # Method 1: Check current branch (if it's 'default', that's the main branch)
249
- result = `hg branch 2>/dev/null`.strip
250
- if $?.success? && result == 'default'
251
- return 'default'
252
- end
253
-
254
- # Method 2: Look for 'default' branch in branch list
255
- result = `hg branches 2>/dev/null`
256
- if $?.success? && result.include?('default')
257
- return 'default'
258
- end
259
-
260
- # Method 3: Check if there are any branches at all
261
- result = `hg branches 2>/dev/null`.strip
262
- if $?.success? && !result.empty?
263
- # Get the first branch (usually the main one)
264
- first_branch = result.lines.first&.split&.first
265
- return first_branch if first_branch
266
- end
267
-
268
- # Fallback to 'default' (standard hg main branch name)
269
- 'default'
270
- end
271
-
272
- def validate_git_branch_exists(branch)
273
- # Check if branch exists locally
274
- _result = `git rev-parse --verify #{branch} 2>/dev/null`
275
- return true if $?.success?
276
-
277
- # Check if branch exists on remote
278
- _result = `git rev-parse --verify origin/#{branch} 2>/dev/null`
279
- return true if $?.success?
280
-
281
- false
282
- end
283
-
284
- def validate_hg_branch_exists(branch)
285
- # Check if branch exists in hg branches
286
- result = `hg branches 2>/dev/null`
287
- if $?.success?
288
- return result.lines.any? { |line| line.strip.start_with?(branch) }
289
- end
290
-
291
- # If we can't list branches, assume it exists (hg is more permissive)
292
- true
293
- end
37
+ # All diff-related methods like handle_diff_analysis, get_vcs_type, etc., are removed.
294
38
 
295
39
  private
296
40
 
@@ -316,32 +60,9 @@ module N2B
316
60
  end
317
61
  end
318
62
 
319
- def build_diff_analysis_prompt(diff_output, user_prompt_addition = "", requirements_content = nil)
320
- default_system_prompt = <<-SYSTEM_PROMPT.strip
321
- You are a senior software developer reviewing a code diff.
322
- Your task is to provide a constructive and detailed analysis of the changes.
323
- Focus on identifying potential bugs, suggesting improvements in code quality, style, performance, and security.
324
- Also, provide a concise summary of the changes.
325
-
326
- IMPORTANT: When referring to specific issues or improvements, always include:
327
- - The exact file path (e.g., "lib/n2b/cli.rb")
328
- - The specific line numbers or line ranges (e.g., "line 42" or "lines 15-20")
329
- - The exact code snippet you're referring to when possible
330
-
331
- This helps users quickly locate and understand the issues you identify.
332
-
333
- SPECIAL FOCUS ON TEST COVERAGE:
334
- Pay special attention to whether the developer has provided adequate test coverage for the changes:
335
- - Look for new test files or modifications to existing test files
336
- - Check if new functionality has corresponding tests
337
- - Evaluate if edge cases and error conditions are tested
338
- - Assess if the tests are meaningful and comprehensive
339
- - Note any missing test coverage that should be added
340
-
341
- NOTE: In addition to the diff, you will also receive the current code context around the changed areas.
342
- This provides better understanding of the surrounding code and helps with more accurate analysis.
343
- The user may provide additional instructions or specific requirements below.
344
- SYSTEM_PROMPT
63
+ def build_diff_analysis_prompt(diff_output, user_prompt_addition = "", requirements_content = nil, config = {})
64
+ default_system_prompt_path = resolve_template_path('diff_system_prompt', config)
65
+ default_system_prompt = File.read(default_system_prompt_path).strip
345
66
 
346
67
  user_instructions_section = ""
347
68
  unless user_prompt_addition.to_s.strip.empty?
@@ -382,35 +103,8 @@ REQUIREMENTS_BLOCK
382
103
  end
383
104
  end
384
105
 
385
- json_instruction = <<-JSON_INSTRUCTION.strip
386
- CRITICAL: Return ONLY a valid JSON object.
387
- Do not include any explanatory text before or after the JSON.
388
- Each error and improvement should include specific file paths and line numbers.
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
-
398
- Example format:
399
- {
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.",
402
- "errors": [
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."
405
- ],
406
- "improvements": [
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."
409
- ],
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.",
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."
412
- }
413
- JSON_INSTRUCTION
106
+ json_instruction_path = resolve_template_path('diff_json_instruction', config)
107
+ json_instruction = File.read(json_instruction_path).strip
414
108
 
415
109
  full_prompt = [
416
110
  default_system_prompt,
@@ -426,7 +120,7 @@ JSON_INSTRUCTION
426
120
  end
427
121
 
428
122
  def analyze_diff(diff_output, config, user_prompt_addition = "", requirements_content = nil)
429
- prompt = build_diff_analysis_prompt(diff_output, user_prompt_addition, requirements_content)
123
+ prompt = build_diff_analysis_prompt(diff_output, user_prompt_addition, requirements_content, config)
430
124
  analysis_json_str = call_llm_for_diff_analysis(prompt, config)
431
125
 
432
126
  begin
@@ -500,6 +194,19 @@ JSON_INSTRUCTION
500
194
  improvements.map(&:strip).reject(&:empty?)
501
195
  end
502
196
 
197
+ def format_analysis_for_github(analysis_result)
198
+ return "No analysis result available." if analysis_result.nil? || analysis_result.empty?
199
+
200
+ {
201
+ implementation_summary: analysis_result['ticket_implementation_summary']&.strip,
202
+ technical_summary: analysis_result['summary']&.strip,
203
+ issues: format_issues_for_adf(analysis_result['errors']),
204
+ improvements: format_improvements_for_adf(analysis_result['improvements']),
205
+ test_coverage: analysis_result['test_coverage']&.strip,
206
+ requirements_evaluation: analysis_result['requirements_evaluation']&.strip
207
+ }
208
+ end
209
+
503
210
  def extract_json_from_response(response)
504
211
  # First try to parse the response as-is
505
212
  begin
@@ -588,40 +295,6 @@ JSON_INSTRUCTION
588
295
  context_sections
589
296
  end
590
297
 
591
- def call_llm_for_diff_analysis(prompt, config)
592
- begin
593
- llm_service_name = config['llm']
594
- llm = case llm_service_name
595
- when 'openai'
596
- N2M::Llm::OpenAi.new(config)
597
- when 'claude'
598
- N2M::Llm::Claude.new(config)
599
- when 'gemini'
600
- N2M::Llm::Gemini.new(config)
601
- when 'openrouter'
602
- N2M::Llm::OpenRouter.new(config)
603
- when 'ollama'
604
- N2M::Llm::Ollama.new(config)
605
- else
606
- # Should not happen if config is validated, but as a safeguard:
607
- raise N2B::Error, "Unsupported LLM service: #{llm_service_name}"
608
- end
609
-
610
- response_json_str = llm.analyze_code_diff(prompt) # Call the new dedicated method
611
- response_json_str
612
- rescue N2B::LlmApiError => e # This catches errors from analyze_code_diff
613
- puts "Error communicating with the LLM: #{e.message}"
614
-
615
- # Check if it might be a model-related error
616
- if e.message.include?('model') || e.message.include?('Model') || e.message.include?('invalid') || e.message.include?('not found')
617
- puts "\nThis might be due to an invalid or unsupported model configuration."
618
- puts "Run 'n2b -c' to reconfigure your model settings."
619
- end
620
-
621
- return '{"summary": "Error: Could not analyze diff due to LLM API error.", "errors": [], "improvements": []}'
622
- end
623
- end
624
-
625
298
  def append_to_llm_history_file(commands)
626
299
  File.open(HISTORY_FILE, 'a') do |file|
627
300
  file.puts(commands)
@@ -640,19 +313,19 @@ JSON_INSTRUCTION
640
313
  llm_service_name = config['llm']
641
314
  llm = case llm_service_name
642
315
  when 'openai'
643
- N2M::Llm::OpenAi.new(config)
316
+ N2B::Llm::OpenAi.new(config)
644
317
  when 'claude'
645
- N2M::Llm::Claude.new(config)
318
+ N2B::Llm::Claude.new(config)
646
319
  when 'gemini'
647
- N2M::Llm::Gemini.new(config)
320
+ N2B::Llm::Gemini.new(config)
648
321
  when 'openrouter'
649
- N2M::Llm::OpenRouter.new(config)
322
+ N2B::Llm::OpenRouter.new(config)
650
323
  when 'ollama'
651
- N2M::Llm::Ollama.new(config)
324
+ N2B::Llm::Ollama.new(config)
652
325
  else
653
326
  # Fallback or error, though config validation should prevent this
654
327
  puts "Warning: Unsupported LLM service '#{llm_service_name}' configured. Falling back to Claude."
655
- N2M::Llm::Claude.new(config)
328
+ N2B::Llm::Claude.new(config)
656
329
  end
657
330
 
658
331
  # This content is specific to bash command generation
@@ -667,23 +340,37 @@ JSON_INSTRUCTION
667
340
  EOF
668
341
 
669
342
 
670
- response_json_str = llm.make_request(content)
671
-
672
- append_to_llm_history_file("#{prompt}\n#{response_json_str}") # Storing the raw JSON string
673
- # The original call_llm was expected to return a hash after JSON.parse,
674
- # but it was actually returning the string. Let's assume it should return a parsed Hash.
675
- # However, the calling method `process_natural_language_command` accesses it like `bash_commands['commands']`
676
- # which implies it expects a Hash. Let's ensure call_llm returns a Hash.
677
- # This internal JSON parsing is for the *content* of a successful LLM response.
678
- # The LlmApiError for network/auth issues should be caught before this.
343
+ puts "🤖 AI is generating commands..."
344
+ response = make_request_with_spinner(llm, content)
345
+
346
+ # Handle both Hash (from JSON mode providers) and String responses
347
+ if response.is_a?(Hash)
348
+ # Already parsed by the LLM provider
349
+ parsed_response = response
350
+ response_str = response.to_json # For history logging
351
+ else
352
+ # String response that needs parsing
353
+ response_str = response
679
354
  begin
680
- parsed_response = JSON.parse(response_json_str)
681
- parsed_response
355
+ parsed_response = JSON.parse(response_str)
682
356
  rescue JSON::ParserError => e
683
- puts "Error parsing LLM response JSON for command generation: #{e.message}"
684
- # This is a fallback for when the LLM response *content* is not valid JSON.
685
- { "commands" => ["echo 'Error: LLM returned invalid JSON content.'"], "explanation" => "The response from the language model was not valid JSON." }
357
+ puts "⚠️ Invalid JSON detected, attempting automatic repair..."
358
+ repaired_response = attempt_json_repair_for_commands(response_str, llm)
359
+
360
+ if repaired_response
361
+ puts "✅ JSON repair successful!"
362
+ parsed_response = repaired_response
363
+ else
364
+ puts "❌ JSON repair failed"
365
+ puts "Error parsing LLM response JSON for command generation: #{e.message}"
366
+ # This is a fallback for when the LLM response *content* is not valid JSON.
367
+ parsed_response = { "commands" => ["echo 'Error: LLM returned invalid JSON content.'"], "explanation" => "The response from the language model was not valid JSON." }
368
+ end
686
369
  end
370
+ end
371
+
372
+ append_to_llm_history_file("#{prompt}\n#{response_str}") # Storing the response for history
373
+ parsed_response
687
374
  rescue N2B::LlmApiError => e
688
375
  puts "Error communicating with the LLM: #{e.message}"
689
376
 
@@ -784,63 +471,116 @@ JSON_INSTRUCTION
784
471
  end
785
472
  system("history -r") # Attempt to reload history in current session
786
473
  end
787
-
474
+
475
+ def resolve_template_path(template_key, config)
476
+ user_path = config.dig('templates', template_key) if config.is_a?(Hash)
477
+ return user_path if user_path && File.exist?(user_path)
478
+
479
+ File.expand_path(File.join(__dir__, 'templates', "#{template_key}.txt"))
480
+ end
481
+
482
+ def make_request_with_spinner(llm, content)
483
+ spinner_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
484
+ spinner_thread = Thread.new do
485
+ i = 0
486
+ while true
487
+ print "\r⠿ #{spinner_chars[i % spinner_chars.length]} Processing..."
488
+ $stdout.flush
489
+ sleep(0.1)
490
+ i += 1
491
+ end
492
+ end
493
+
494
+ begin
495
+ result = llm.make_request(content)
496
+ spinner_thread.kill
497
+ print "\r#{' ' * 25}\r" # Clear the spinner line
498
+ puts "✅ Commands generated!"
499
+ result
500
+ rescue => e
501
+ spinner_thread.kill
502
+ print "\r#{' ' * 25}\r" # Clear the spinner line
503
+ raise e
504
+ end
505
+ end
506
+
507
+ def attempt_json_repair_for_commands(malformed_response, llm)
508
+ repair_prompt = <<~PROMPT
509
+ The following response was supposed to be valid JSON with keys "commands" (array) and "explanation" (string), but it has formatting issues. Please fix it and return ONLY the corrected JSON:
510
+
511
+ Original response:
512
+ #{malformed_response}
513
+
514
+ Requirements:
515
+ - Must be valid JSON
516
+ - Must have "commands" key with array of command strings
517
+ - Must have "explanation" key with explanation text
518
+ - Return ONLY the JSON, no other text
519
+
520
+ Fixed JSON:
521
+ PROMPT
522
+
523
+ begin
524
+ puts "🔧 Asking AI to fix the JSON..."
525
+ repaired_json_str = llm.make_request(repair_prompt)
526
+
527
+ # Handle both Hash and String responses
528
+ if repaired_json_str.is_a?(Hash)
529
+ repaired_response = repaired_json_str
530
+ else
531
+ repaired_response = JSON.parse(repaired_json_str)
532
+ end
533
+
534
+ # Validate the repaired response structure
535
+ if repaired_response.is_a?(Hash) && repaired_response.key?('commands') && repaired_response.key?('explanation')
536
+ return repaired_response
537
+ else
538
+ return nil
539
+ end
540
+ rescue JSON::ParserError, StandardError
541
+ return nil
542
+ end
543
+ end
544
+
788
545
 
789
546
  def parse_options
790
547
  options = {
791
548
  execute: false,
792
549
  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
550
+ # diff: false, # Removed
551
+ # requirements: nil, # Removed
552
+ # branch: nil, # Removed
553
+ # jira_ticket: nil, # Removed
554
+ # jira_update: nil, # Removed
555
+ advanced_config: false
799
556
  }
800
557
 
801
558
  parser = OptionParser.new do |opts|
802
559
  opts.banner = "Usage: n2b [options] [natural language command]"
803
560
 
804
- opts.on('-x', '--execute', 'Execute the commands after confirmation') do
561
+ opts.on('-x', '--execute', 'Execute the translated commands after confirmation.') do
805
562
  options[:execute] = true
806
563
  end
807
564
 
808
- opts.on('-d', '--diff', 'Analyze git/hg diff with AI') do
809
- options[:diff] = true
810
- end
811
-
812
- opts.on('-b', '--branch [BRANCH]', 'Compare against branch (default: auto-detect main/master)') do |branch|
813
- options[:branch] = branch || 'auto'
814
- end
815
-
816
- opts.on('-r', '--requirements FILE', 'Requirements file for diff analysis') do |file|
817
- options[:requirements] = file
818
- end
565
+ # Removed options: -d, --diff, -b, --branch, -r, --requirements, -j, --jira, --jira-update, --jira-no-update
819
566
 
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
567
+ opts.on('-c', '--config', 'Configure N2B (API key, model, privacy settings, etc.).') do
568
+ options[:config] = true
826
569
  end
827
570
 
828
- opts.on('--jira-no-update', 'Do not update the linked Jira ticket (requires -j)') do
829
- options[:jira_update] = false
571
+ opts.on('--advanced-config', 'Access advanced configuration options.') do
572
+ options[:advanced_config] = true
573
+ options[:config] = true # --advanced-config implies -c
830
574
  end
831
575
 
832
- opts.on('-h', '--help', 'Print this help') do
576
+ opts.on_tail('-h', '--help', 'Show this help message.') do
833
577
  puts opts
834
578
  exit
835
579
  end
836
580
 
837
- opts.on('-c', '--config', 'Configure the API key and model') do
838
- options[:config] = true
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
581
+ opts.on_tail('-v', '--version', 'Show version.') do
582
+ puts "n2b version #{N2B::VERSION}" # Assuming N2B::VERSION is defined
583
+ exit
844
584
  end
845
585
  end
846
586
 
@@ -851,36 +591,14 @@ JSON_INSTRUCTION
851
591
  puts ""
852
592
  puts parser.help
853
593
  exit 1
854
- end
855
-
856
- # Validate option combinations
857
- if options[:branch] && !options[:diff]
858
- puts "Error: --branch option can only be used with --diff"
859
- puts ""
860
- puts parser.help
861
- exit 1
862
- end
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."
594
+ rescue OptionParser::MissingArgument => e
595
+ puts "Error: #{e.message}"
873
596
  puts ""
874
597
  puts parser.help
875
598
  exit 1
876
599
  end
877
600
 
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
601
+ # Removed validation logic for diff/jira related options
884
602
 
885
603
  options
886
604
  end