gitarsenal-cli 1.9.25 → 1.9.26

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.
@@ -2,362 +2,185 @@ import os
2
2
  import re
3
3
  import json
4
4
  import requests
5
- import openai
6
- import anthropic
7
5
  import time
8
6
  import getpass
7
+ from pathlib import Path
9
8
 
10
9
 
11
10
  def get_stored_credentials():
12
11
  """Load stored credentials from ~/.gitarsenal/credentials.json"""
13
- import json
14
- from pathlib import Path
15
-
16
12
  try:
17
13
  credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
18
14
  if credentials_file.exists():
19
15
  with open(credentials_file, 'r') as f:
20
- credentials = json.load(f)
21
- return credentials
22
- else:
23
- return {}
16
+ return json.load(f)
17
+ return {}
24
18
  except Exception as e:
25
19
  print(f"⚠️ Error loading stored credentials: {e}")
26
20
  return {}
27
21
 
22
+
28
23
  def generate_auth_context(stored_credentials):
29
- """Generate simple authentication context for the OpenAI prompt"""
24
+ """Generate authentication context for the LLM prompt"""
30
25
  if not stored_credentials:
31
26
  return "No stored credentials available."
32
27
 
33
28
  auth_context = "Available stored credentials (use actual values in commands):\n"
34
-
35
29
  for key, value in stored_credentials.items():
36
- # Mask the actual value for security in logs, but provide the real value
37
30
  masked_value = value[:8] + "..." if len(value) > 8 else "***"
38
31
  auth_context += f"- {key}: {masked_value} (actual value: {value})\n"
39
32
 
40
33
  return auth_context
41
34
 
35
+
42
36
  def get_current_debug_model():
43
37
  """Get the currently configured debugging model preference"""
44
- return os.environ.get("GITARSENAL_DEBUG_MODEL", "anthropic")
38
+ return os.environ.get("GITARSENAL_DEBUG_MODEL", "openai")
45
39
 
46
- def call_llm_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
47
- """Unified function to call LLM for debugging - routes to OpenAI, Anthropic, or OpenRouter based on configuration"""
48
- current_model = get_current_debug_model()
49
-
50
- print(f"🔍 DEBUG: Using {current_model.upper()} for debugging...")
51
-
52
- if current_model == "anthropic":
53
- # Try to get Anthropic API key if not provided
54
- if not api_key:
55
- # First try environment variable
56
- api_key = os.environ.get("ANTHROPIC_API_KEY")
57
-
58
- # If not in environment, try to fetch from server using fetch_modal_tokens
59
- if not api_key:
60
- try:
61
- from fetch_modal_tokens import get_tokens
62
- _, _, _, api_key = get_tokens()
63
- except Exception as e:
64
- print(f"⚠️ Error fetching Anthropic API key from server: {e}")
65
-
66
- # Then try credentials manager
67
- if not api_key:
68
- try:
69
- from credentials_manager import CredentialsManager
70
- credentials_manager = CredentialsManager()
71
- api_key = credentials_manager.get_anthropic_api_key()
72
- except Exception as e:
73
- print(f"⚠️ Error getting Anthropic API key from credentials manager: {e}")
74
-
75
- return call_anthropic_for_debug(command, error_output, api_key, current_dir, sandbox)
76
- elif current_model == "openrouter":
77
- # Try to get OpenRouter API key if not provided
78
- if not api_key:
79
- # First try environment variable
80
- api_key = os.environ.get("OPENROUTER_API_KEY")
81
-
82
- # If not in environment, try to fetch from server using fetch_modal_tokens
83
- if not api_key:
84
- try:
85
- from fetch_modal_tokens import get_tokens
86
- # Assuming OpenRouter key is the 5th token in the tuple
87
- tokens = get_tokens()
88
- if len(tokens) >= 5:
89
- api_key = tokens[4]
90
- except Exception as e:
91
- print(f"⚠️ Error fetching OpenRouter API key from server: {e}")
92
-
93
- # Then try credentials manager
94
- if not api_key:
95
- try:
96
- from credentials_manager import CredentialsManager
97
- credentials_manager = CredentialsManager()
98
- api_key = credentials_manager.get_openrouter_api_key()
99
- except Exception as e:
100
- print(f"⚠️ Error getting OpenRouter API key from credentials manager: {e}")
101
-
102
- return call_openrouter_for_debug(command, error_output, api_key, current_dir, sandbox)
103
- else:
104
- # Default to OpenAI
105
- # Try to get OpenAI API key if not provided
106
- if not api_key:
107
- # First try environment variable
108
- api_key = os.environ.get("OPENAI_API_KEY")
109
-
110
- # If not in environment, try to fetch from server using fetch_modal_tokens
111
- if not api_key:
112
- try:
113
- from fetch_modal_tokens import get_tokens
114
- _, _, api_key, _ = get_tokens()
115
- except Exception as e:
116
- print(f"⚠️ Error fetching OpenAI API key from server: {e}")
117
-
118
- # Then try credentials manager
119
- if not api_key:
120
- try:
121
- from credentials_manager import CredentialsManager
122
- credentials_manager = CredentialsManager()
123
- api_key = credentials_manager.get_openai_api_key()
124
- except Exception as e:
125
- print(f"⚠️ Error getting OpenAI API key from credentials manager: {e}")
126
-
127
- return call_openai_for_debug(command, error_output, api_key, current_dir, sandbox)
128
40
 
129
- def call_openai_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
130
- """Call OpenAI to debug a failed command and suggest a fix"""
131
- print("\n🔍 DEBUG: Starting LLM debugging...")
132
- print(f"🔍 DEBUG: Command: {command}")
133
- print(f"🔍 DEBUG: Error output length: {len(error_output) if error_output else 0}")
134
- print(f"🔍 DEBUG: Current directory: {current_dir}")
135
- print(f"🔍 DEBUG: Sandbox available: {sandbox is not None}")
41
+ def _to_str(maybe_bytes):
42
+ """Convert bytes to string safely"""
43
+ try:
44
+ return maybe_bytes.decode('utf-8') if isinstance(maybe_bytes, (bytes, bytearray)) else maybe_bytes
45
+ except UnicodeDecodeError:
46
+ if isinstance(maybe_bytes, (bytes, bytearray)):
47
+ return maybe_bytes.decode('utf-8', errors='replace')
48
+ return str(maybe_bytes)
49
+ except Exception:
50
+ return str(maybe_bytes)
51
+
52
+
53
+ def get_api_key(provider):
54
+ """Get API key for the specified provider from multiple sources"""
55
+ env_var_map = {
56
+ "openai": "OPENAI_API_KEY",
57
+ "anthropic": "ANTHROPIC_API_KEY",
58
+ "openrouter": "OPENROUTER_API_KEY"
59
+ }
136
60
 
137
- # Define _to_str function locally to avoid NameError
138
- def _to_str(maybe_bytes):
139
- try:
140
- return (maybe_bytes.decode('utf-8') if isinstance(maybe_bytes, (bytes, bytearray)) else maybe_bytes)
141
- except UnicodeDecodeError:
142
- # Handle non-UTF-8 bytes by replacing invalid characters
143
- if isinstance(maybe_bytes, (bytes, bytearray)):
144
- return maybe_bytes.decode('utf-8', errors='replace')
145
- else:
146
- return str(maybe_bytes)
147
- except Exception:
148
- # Last resort fallback
149
- return str(maybe_bytes)
150
-
151
- # Skip debugging for certain commands that commonly return non-zero exit codes
152
- # but aren't actually errors (like test commands)
153
- if command.strip().startswith("test "):
154
- print("🔍 Skipping debugging for test command - non-zero exit code is expected behavior")
155
- return None
156
-
157
- # Validate error_output - if it's empty, we can't debug effectively
158
- if not error_output or not error_output.strip():
159
- print("⚠️ Error output is empty. Cannot effectively debug the command.")
160
- print("⚠️ Skipping OpenAI debugging due to lack of error information.")
161
- return None
61
+ key_file_map = {
62
+ "openai": "openai_key",
63
+ "anthropic": "anthropic_key",
64
+ "openrouter": "openrouter_key"
65
+ }
162
66
 
163
- # Try to get API key from multiple sources
164
- if not api_key:
165
- print("🔍 DEBUG: No API key provided, searching for one...")
166
-
167
- # First try environment variable
168
- api_key = os.environ.get("OPENAI_API_KEY")
169
- print(f"🔍 DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
170
- if api_key:
171
- print(f"🔍 DEBUG: Environment API key value: {api_key}")
172
-
173
- # If not in environment, try to fetch from server using fetch_modal_tokens
174
- if not api_key:
175
- try:
176
- print("🔍 DEBUG: Trying to fetch API key from server...")
177
- from fetch_modal_tokens import get_tokens
178
- _, _, api_key, _ = get_tokens()
179
- if api_key:
180
- # Set in environment for this session
181
- os.environ["OPENAI_API_KEY"] = api_key
182
- else:
183
- print("⚠️ Could not fetch OpenAI API key from server")
184
- except Exception as e:
185
- print(f"⚠️ Error fetching API key from server: {e}")
186
-
187
- # Store the API key in a persistent file if found
188
- if api_key:
189
- try:
190
- os.makedirs(os.path.expanduser("~/.gitarsenal"), exist_ok=True)
191
- with open(os.path.expanduser("~/.gitarsenal/openai_key"), "w") as f:
192
- f.write(api_key)
193
- print("✅ Saved OpenAI API key for future use")
194
- except Exception as e:
195
- print(f"⚠️ Could not save API key: {e}")
196
-
197
- # Try to load from saved file if not in environment
198
- if not api_key:
199
- try:
200
- key_file = os.path.expanduser("~/.gitarsenal/openai_key")
201
- print(f"🔍 DEBUG: Checking for saved API key at: {key_file}")
202
- if os.path.exists(key_file):
203
- with open(key_file, "r") as f:
204
- api_key = f.read().strip()
205
- if api_key:
206
- print("✅ Loaded OpenAI API key from saved file")
207
- print(f"🔍 DEBUG: API key from file: {api_key}")
208
- print(f"🔍 DEBUG: API key length: {len(api_key)}")
209
- # Also set in environment for this session
210
- os.environ["OPENAI_API_KEY"] = api_key
211
- else:
212
- print("🔍 DEBUG: Saved file exists but is empty")
213
- else:
214
- print("🔍 DEBUG: No saved API key file found")
215
- except Exception as e:
216
- print(f"⚠️ Could not load saved API key: {e}")
217
-
218
- # Then try credentials manager
219
- if not api_key:
220
- print("🔍 DEBUG: Trying credentials manager...")
221
- try:
222
- from credentials_manager import CredentialsManager
223
- credentials_manager = CredentialsManager()
224
- api_key = credentials_manager.get_openai_api_key()
225
- if api_key:
226
- print(f"🔍 DEBUG: API key from credentials manager: Found")
227
- print(f"🔍 DEBUG: Credentials manager API key value: {api_key}")
228
- # Set in environment for this session
229
- os.environ["OPENAI_API_KEY"] = api_key
230
- else:
231
- print(f"🔍 DEBUG: API key from credentials manager: Not found")
232
- except ImportError as e:
233
- print(f"🔍 DEBUG: Credentials manager not available: {e}")
234
- # Fall back to direct input if credentials_manager is not available
235
- pass
236
-
237
- # Finally, prompt the user if still no API key
238
- if not api_key:
239
- print("🔍 DEBUG: No API key found in any source, prompting user...")
240
- print("\n" + "="*60)
241
- print("🔑 OPENAI API KEY REQUIRED FOR DEBUGGING")
242
- print("="*60)
243
- print("To debug failed commands, an OpenAI API key is needed.")
244
- print("📝 Please paste your OpenAI API key below:")
245
- print(" (Your input will be hidden for security)")
246
- print("-" * 60)
247
-
248
- try:
249
- api_key = getpass.getpass("OpenAI API Key: ").strip()
250
- if not api_key:
251
- print("❌ No API key provided. Skipping debugging.")
252
- return None
253
- print("✅ API key received successfully!")
254
- print(f"🔍 DEBUG: User-provided API key: {api_key}")
255
- # Save the API key to environment for future use in this session
256
- os.environ["OPENAI_API_KEY"] = api_key
257
- except KeyboardInterrupt:
258
- print("\n❌ API key input cancelled by user.")
259
- return None
260
- except Exception as e:
261
- print(f"❌ Error getting API key: {e}")
262
- return None
67
+ token_index_map = {
68
+ "openai": 2,
69
+ "anthropic": 3,
70
+ "openrouter": 4
71
+ }
263
72
 
264
- # If we still don't have an API key, we can't proceed
265
- if not api_key:
266
- print("❌ No OpenAI API key available. Cannot perform LLM debugging.")
267
- print("💡 To enable LLM debugging, set the OPENAI_API_KEY environment variable")
73
+ env_var = env_var_map.get(provider)
74
+ if not env_var:
268
75
  return None
269
76
 
270
- # print(f"✅ OpenAI API key available (length: {len(api_key)})")
77
+ # Try environment variable first
78
+ api_key = os.environ.get(env_var)
79
+ if api_key:
80
+ return api_key
271
81
 
272
- # Gather additional context to help with debugging
273
- directory_context = ""
82
+ # Try fetch from server
83
+ try:
84
+ from fetch_modal_tokens import get_tokens
85
+ tokens = get_tokens()
86
+ token_index = token_index_map.get(provider)
87
+ if token_index is not None and len(tokens) > token_index:
88
+ api_key = tokens[token_index]
89
+ if api_key:
90
+ os.environ[env_var] = api_key
91
+ return api_key
92
+ except Exception:
93
+ pass
94
+
95
+ # Try credentials manager
96
+ try:
97
+ from credentials_manager import CredentialsManager
98
+ credentials_manager = CredentialsManager()
99
+ method_name = f"get_{provider}_api_key"
100
+ if hasattr(credentials_manager, method_name):
101
+ api_key = getattr(credentials_manager, method_name)()
102
+ if api_key:
103
+ os.environ[env_var] = api_key
104
+ return api_key
105
+ except Exception:
106
+ pass
107
+
108
+ # Try saved file
109
+ try:
110
+ key_file = Path.home() / ".gitarsenal" / key_file_map[provider]
111
+ if key_file.exists():
112
+ api_key = key_file.read_text().strip()
113
+ if api_key:
114
+ os.environ[env_var] = api_key
115
+ return api_key
116
+ except Exception:
117
+ pass
118
+
119
+ return None
120
+
121
+
122
+ def save_api_key(provider, api_key):
123
+ """Save API key to persistent file"""
124
+ key_file_map = {
125
+ "openai": "openai_key",
126
+ "anthropic": "anthropic_key",
127
+ "openrouter": "openrouter_key"
128
+ }
129
+
130
+ try:
131
+ gitarsenal_dir = Path.home() / ".gitarsenal"
132
+ gitarsenal_dir.mkdir(exist_ok=True)
133
+ key_file = gitarsenal_dir / key_file_map[provider]
134
+ key_file.write_text(api_key)
135
+ except Exception as e:
136
+ print(f"⚠️ Could not save {provider} API key: {e}")
137
+
138
+
139
+ def gather_context(sandbox, current_dir):
140
+ """Gather system and directory context for debugging"""
274
141
  system_info = ""
275
- command_history = ""
142
+ directory_context = ""
276
143
  file_context = ""
277
144
 
278
- if sandbox:
279
- try:
280
- print("🔍 Getting system information for better debugging...")
281
-
282
- # Get OS information
283
- os_info_cmd = """
284
- echo "OS Information:"
285
- cat /etc/os-release 2>/dev/null || echo "OS release info not available"
286
- echo -e "\nKernel Information:"
287
- uname -a
288
- echo -e "\nPython Information:"
289
- python --version
290
- pip --version
291
- echo -e "\nPackage Manager:"
292
- which apt 2>/dev/null && echo "apt available" || echo "apt not available"
293
- which yum 2>/dev/null && echo "yum available" || echo "yum not available"
294
- which dnf 2>/dev/null && echo "dnf available" || echo "dnf not available"
295
- which apk 2>/dev/null && echo "apk available" || echo "apk not available"
296
- echo -e "\nEnvironment Variables:"
297
- env | grep -E "^(PATH|PYTHON|VIRTUAL_ENV|HOME|USER|SHELL|LANG)" || echo "No relevant env vars found"
298
- """
299
-
300
- os_result = sandbox.exec("bash", "-c", os_info_cmd)
301
- os_output = ""
302
- for line in os_result.stdout:
303
- os_output += _to_str(line)
304
- os_result.wait()
305
-
306
- system_info = f"""
307
- System Information:
308
- {os_output}
309
- """
310
- print("✅ System information gathered successfully")
311
- except Exception as e:
312
- print(f"⚠️ Error getting system information: {e}")
313
- system_info = "System information not available\n"
145
+ if not sandbox:
146
+ return system_info, directory_context, file_context
314
147
 
315
- if current_dir and sandbox:
148
+ # Get system information
149
+ try:
150
+ os_info_cmd = """
151
+ echo "OS Information:"
152
+ cat /etc/os-release 2>/dev/null || echo "OS release info not available"
153
+ echo -e "\nKernel Information:"
154
+ uname -a
155
+ echo -e "\nPython Information:"
156
+ python --version
157
+ pip --version
158
+ """
159
+
160
+ os_result = sandbox.exec("bash", "-c", os_info_cmd)
161
+ os_output = ""
162
+ for line in os_result.stdout:
163
+ os_output += _to_str(line)
164
+ os_result.wait()
165
+
166
+ system_info = f"\nSystem Information:\n{os_output}"
167
+ except Exception:
168
+ system_info = "System information not available\n"
169
+
170
+ # Get directory context
171
+ if current_dir:
316
172
  try:
317
- # print("🔍 Getting directory context for better debugging...")
318
-
319
- # Get current directory contents
320
173
  ls_result = sandbox.exec("bash", "-c", "ls -la")
321
174
  ls_output = ""
322
175
  for line in ls_result.stdout:
323
176
  ls_output += _to_str(line)
324
177
  ls_result.wait()
325
178
 
326
- # Get parent directory contents
327
- parent_result = sandbox.exec("bash", "-c", "ls -la ../")
328
- parent_ls = ""
329
- for line in parent_result.stdout:
330
- parent_ls += _to_str(line)
331
- parent_result.wait()
332
-
333
- directory_context = f"""
334
- Current directory contents:
335
- {ls_output}
336
-
337
- Parent directory contents:
338
- {parent_ls}
339
- """
340
- print("✅ Directory context gathered successfully")
179
+ directory_context = f"\nCurrent directory contents:\n{ls_output}"
341
180
 
342
- # Check for relevant files that might provide additional context
343
- # For example, if error mentions a specific file, try to get its content
181
+ # Check for common config files
182
+ common_config_files = ["package.json", "requirements.txt", "pyproject.toml", "setup.py"]
344
183
  relevant_files = []
345
- error_files = re.findall(r'(?:No such file or directory|cannot open|not found): ([^\s:]+)', error_output)
346
- if error_files:
347
- for file_path in error_files:
348
- # Clean up the file path
349
- file_path = file_path.strip("'\"")
350
- if not os.path.isabs(file_path):
351
- file_path = os.path.join(current_dir, file_path)
352
-
353
- # Try to get the parent directory if the file doesn't exist
354
- if '/' in file_path:
355
- parent_file_dir = os.path.dirname(file_path)
356
- relevant_files.append(parent_file_dir)
357
-
358
- # Look for package.json, requirements.txt, etc.
359
- common_config_files = ["package.json", "requirements.txt", "pyproject.toml", "setup.py",
360
- "Pipfile", "Dockerfile", "docker-compose.yml", "Makefile"]
361
184
 
362
185
  for config_file in common_config_files:
363
186
  check_cmd = f"test -f {current_dir}/{config_file}"
@@ -366,65 +189,32 @@ Parent directory contents:
366
189
  if check_result.returncode == 0:
367
190
  relevant_files.append(f"{current_dir}/{config_file}")
368
191
 
369
- # Get content of relevant files
192
+ # Get content of relevant files (limit to 2)
370
193
  if relevant_files:
371
194
  file_context = "\nRelevant file contents:\n"
372
- for file_path in relevant_files[:2]: # Limit to 2 files to avoid too much context
195
+ for file_path in relevant_files[:2]:
373
196
  try:
374
- file_check_cmd = f"test -f {file_path}"
375
- file_check = sandbox.exec("bash", "-c", file_check_cmd)
376
- file_check.wait()
197
+ cat_result = sandbox.exec("bash", "-c", f"cat {file_path}")
198
+ file_content = ""
199
+ for line in cat_result.stdout:
200
+ file_content += _to_str(line)
201
+ cat_result.wait()
377
202
 
378
- if file_check.returncode == 0:
379
- # It's a file, get its content
380
- cat_cmd = f"cat {file_path}"
381
- cat_result = sandbox.exec("bash", "-c", cat_cmd)
382
- file_content = ""
383
- for line in cat_result.stdout:
384
- file_content += _to_str(line)
385
- cat_result.wait()
386
-
387
- # Truncate if too long
388
- if len(file_content) > 1000:
389
- file_content = file_content[:1000] + "\n... (truncated)"
390
-
391
- file_context += f"\n--- {file_path} ---\n{file_content}\n"
392
- else:
393
- # It's a directory, list its contents
394
- ls_cmd = f"ls -la {file_path}"
395
- ls_dir_result = sandbox.exec("bash", "-c", ls_cmd)
396
- dir_content = ""
397
- for line in ls_dir_result.stdout:
398
- dir_content += _to_str(line)
399
- ls_dir_result.wait()
400
-
401
- file_context += f"\n--- Directory: {file_path} ---\n{dir_content}\n"
402
- except Exception as e:
403
- print(f"⚠️ Error getting content of {file_path}: {e}")
404
-
405
- # print(f"✅ Additional file context gathered from {len(relevant_files)} relevant files")
406
-
407
- except Exception as e:
408
- print(f"⚠️ Error getting directory context: {e}")
203
+ if len(file_content) > 1000:
204
+ file_content = file_content[:1000] + "\n... (truncated)"
205
+
206
+ file_context += f"\n--- {file_path} ---\n{file_content}\n"
207
+ except Exception:
208
+ continue
209
+ except Exception:
409
210
  directory_context = f"\nCurrent directory: {current_dir}\n"
410
211
 
411
- # Prepare the API request
412
- headers = {
413
- "Content-Type": "application/json",
414
- "Authorization": f"Bearer {api_key}"
415
- }
212
+ return system_info, directory_context, file_context
416
213
 
417
- stored_credentials = get_stored_credentials()
418
- auth_context = generate_auth_context(stored_credentials)
419
-
420
- # Create a prompt for the LLM
421
- print("\n" + "="*60)
422
- print("DEBUG: ERROR_OUTPUT SENT TO LLM:")
423
- print("="*60)
424
- print(f"{error_output}")
425
- print("="*60 + "\n")
426
-
427
- prompt = f"""
214
+
215
+ def create_debug_prompt(command, error_output, system_info, directory_context, file_context, auth_context):
216
+ """Create the debugging prompt for LLM"""
217
+ return f"""
428
218
  I'm trying to run the following command in a Linux environment:
429
219
 
430
220
  ```
@@ -465,1193 +255,300 @@ IMPORTANT GUIDELINES:
465
255
  - Analyze the error to determine what type of authentication is needed
466
256
  - ALWAYS use the actual credential values from the AVAILABLE CREDENTIALS section above (NOT placeholders)
467
257
  - Look for the specific API key or token needed in the auth_context and use its exact value
468
- - Common patterns:
469
- * wandb errors: use wandb login with the actual WANDB_API_KEY value from auth_context
470
- * huggingface errors: use huggingface-cli login with the actual HF_TOKEN or HUGGINGFACE_TOKEN value from auth_context
471
- * github errors: configure git credentials with the actual GITHUB_TOKEN value from auth_context
472
- * kaggle errors: create ~/.kaggle/kaggle.json with the actual KAGGLE_USERNAME and KAGGLE_KEY values from auth_context
473
- * API errors: export the appropriate API key as environment variable using the actual value from auth_context
474
258
 
475
259
  5. Environment variable exports:
476
260
  - Use export commands for API keys that need to be in environment
477
261
  - ALWAYS use the actual credential values from auth_context, never use placeholders like "YOUR_API_KEY"
478
- - Example: export OPENAI_API_KEY="sk-..." (using the actual key from auth_context)
479
-
480
- 6. CRITICAL: When using any API key, token, or credential:
481
- - Find the exact value in the AVAILABLE CREDENTIALS section
482
- - Use that exact value in your command
483
- - Do not use generic placeholders or dummy values
484
- - The auth_context contains real, usable credentials
485
262
 
486
- 7. For Git SSH authentication failures:
263
+ 6. For Git SSH authentication failures:
487
264
  - If the error contains "Host key verification failed" or "Could not read from remote repository"
488
265
  - ALWAYS convert SSH URLs to HTTPS URLs for public repositories
489
266
  - Replace git@github.com:username/repo.git with https://github.com/username/repo.git
490
- - This works for public repositories without authentication
491
- - Example: git clone https://github.com/xg-chu/ARTalk.git
492
267
 
493
268
  Do not provide any explanations, just the exact command to run.
494
269
  """
270
+
271
+
272
+ def extract_command_from_response(response_text):
273
+ """Extract the actual command from LLM response"""
274
+ fix_command = response_text.strip()
495
275
 
496
- # Prepare the API request payload
497
- # print("🔍 DEBUG: Preparing API request...")
498
-
499
- # Try to use GPT-4 first, but fall back to other models if needed
500
- models_to_try = [
501
- "gpt-4.1-mini", # First choice: GPT-4o (most widely available)
502
- ]
503
-
504
- # Check if we have a preferred model in environment
505
- preferred_model = os.environ.get("OPENAI_MODEL")
506
- if preferred_model:
507
- # Insert the preferred model at the beginning of the list
508
- models_to_try.insert(0, preferred_model)
509
- # print(f"✅ Using preferred model from environment: {preferred_model}")
510
-
511
- # Remove duplicates while preserving order
512
- models_to_try = list(dict.fromkeys(models_to_try))
513
- # print(f"🔍 DEBUG: Models to try: {models_to_try}")
276
+ # Extract from code blocks
277
+ if "```" in fix_command:
278
+ code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
279
+ if code_blocks:
280
+ fix_command = code_blocks[0].strip()
514
281
 
515
- # Function to make the API call with a specific model
516
- def try_api_call(model_name, retries=2, backoff_factor=1.5):
517
- # print(f"🔍 DEBUG: Attempting API call with model: {model_name}")
518
- # print(f"🔍 DEBUG: API key available: {'Yes' if api_key else 'No'}")
519
- # if api_key:
520
- # print(f"🔍 DEBUG: API key length: {len(api_key)}")
521
- # print(f"🔍 DEBUG: API key starts with: {api_key[:10]}...")
522
-
523
- payload = {
524
- "model": model_name,
525
- "messages": [
526
- {"role": "system", "content": "You are a debugging assistant. Provide only the terminal command to fix the issue. Analyze the issue first, understand why it's happening, then provide the command to fix it. For file not found errors, first search for the file using 'find . -name filename -type f' and navigate to the directory if found. For missing packages, use appropriate package managers (pip, apt-get, npm). For Git SSH authentication failures, always convert SSH URLs to HTTPS URLs (git@github.com:user/repo.git -> https://github.com/user/repo.git). For authentication, suggest login commands with placeholders."},
527
- {"role": "user", "content": prompt}
528
- ],
529
- "temperature": 0.2,
530
- "max_tokens": 300
531
- }
282
+ # If multi-line, try to find the actual command
283
+ if len(fix_command.split('\n')) > 1:
284
+ command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
285
+ 'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
286
+ 'curl', 'wget', 'docker', 'make', 'conda', 'uv', 'poetry']
532
287
 
533
- print(f"🔍 DEBUG: Payload prepared, prompt length: {len(prompt)}")
288
+ command_lines = [line.strip() for line in fix_command.split('\n')
289
+ if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
534
290
 
535
- # Add specific handling for common errors
536
- last_error = None
537
- for attempt in range(retries + 1):
538
- try:
539
- if attempt > 0:
540
- # Exponential backoff
541
- wait_time = backoff_factor * (2 ** (attempt - 1))
542
- print(f"⏱️ Retrying in {wait_time:.1f} seconds... (attempt {attempt+1}/{retries+1})")
543
- time.sleep(wait_time)
544
-
545
- print(f"🤖 Calling OpenAI with {model_name} model to debug the failed command...")
546
- print(f"🔍 DEBUG: Making POST request to OpenAI API...")
547
- response = requests.post(
548
- "https://api.openai.com/v1/chat/completions",
549
- headers=headers,
550
- json=payload,
551
- timeout=45 # Increased timeout for reliability
552
- )
553
-
554
- print(f"🔍 DEBUG: Response received, status code: {response.status_code}")
555
-
556
- # Handle specific status codes
557
- if response.status_code == 200:
558
- print(f"🔍 DEBUG: Success! Response length: {len(response.text)}")
559
- return response.json(), None
560
- elif response.status_code == 401:
561
- error_msg = "Authentication error: Invalid API key"
562
- print(f"❌ {error_msg}")
563
- print(f"🔍 DEBUG: Response text: {response.text}")
564
- # Don't retry auth errors
565
- return None, error_msg
566
- elif response.status_code == 429:
567
- error_msg = "Rate limit exceeded or quota reached"
568
- print(f"⚠️ {error_msg}")
569
- print(f"🔍 DEBUG: Response text: {response.text}")
570
- # Always retry rate limit errors with increasing backoff
571
- last_error = error_msg
572
- continue
573
- elif response.status_code == 500:
574
- error_msg = "OpenAI server error"
575
- print(f"⚠️ {error_msg}")
576
- print(f"🔍 DEBUG: Response text: {response.text}")
577
- # Retry server errors
578
- last_error = error_msg
579
- continue
580
- else:
581
- error_msg = f"Status code: {response.status_code}, Response: {response.text}"
582
- print(f"⚠️ OpenAI API error: {error_msg}")
583
- print(f"🔍 DEBUG: Full response text: {response.text}")
584
- last_error = error_msg
585
- # Only retry if we have attempts left
586
- if attempt < retries:
587
- continue
588
- return None, error_msg
589
- except requests.exceptions.Timeout:
590
- error_msg = "Request timed out"
591
- # print(f"⚠️ {error_msg}")
592
- # print(f"🔍 DEBUG: Timeout after 45 seconds")
593
- last_error = error_msg
594
- # Always retry timeouts
595
- continue
596
- except requests.exceptions.ConnectionError:
597
- error_msg = "Connection error"
598
- print(f"⚠️ {error_msg}")
599
- print(f"🔍 DEBUG: Connection failed to api.openai.com")
600
- last_error = error_msg
601
- # Always retry connection errors
602
- continue
603
- except Exception as e:
604
- error_msg = str(e)
605
- print(f"⚠️ Unexpected error: {error_msg}")
606
- print(f"🔍 DEBUG: Exception type: {type(e).__name__}")
607
- print(f"🔍 DEBUG: Exception details: {str(e)}")
608
- last_error = error_msg
609
- # Only retry if we have attempts left
610
- if attempt < retries:
611
- continue
612
- return None, error_msg
613
-
614
- # If we get here, all retries failed
615
- return None, last_error
616
-
617
- # Try each model in sequence until one works
618
- result = None
619
- last_error = None
620
-
621
- for model in models_to_try:
622
- result, error = try_api_call(model)
623
- if result:
624
- # print(f"✅ Successfully got response from {model}")
625
- break
291
+ if command_lines:
292
+ fix_command = command_lines[0]
626
293
  else:
627
- print(f"⚠️ Failed to get response from {model}: {error}")
628
- last_error = error
629
-
630
- if not result:
631
- print(f"❌ All model attempts failed. Last error: {last_error}")
632
- return None
633
-
634
- # Process the response
635
- try:
636
- print(f"🔍 DEBUG: Processing OpenAI response...")
637
- # print(f"🔍 DEBUG: Response structure: {list(result.keys())}")
638
- print(f"🔍 DEBUG: Choices count: {len(result.get('choices', []))}")
639
-
640
- fix_command = result["choices"][0]["message"]["content"].strip()
641
- print(f"🔍 DEBUG: Raw response content: {fix_command}")
642
-
643
- # Save the original response for debugging
644
- original_response = fix_command
645
-
646
- # Extract just the command if it's wrapped in backticks or explanation
647
- if "```" in fix_command:
648
- # Extract content between backticks
649
- import re
650
- code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
651
- if code_blocks:
652
- fix_command = code_blocks[0].strip()
653
- print(f"✅ Extracted command from code block: {fix_command}")
654
-
655
- # If the response still has explanatory text, try to extract just the command
656
- if len(fix_command.split('\n')) > 1:
657
- # First try to find lines that look like commands (start with common command prefixes)
658
- command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
659
- 'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
660
- 'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
661
- 'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
662
-
663
- # Check for lines that start with common command prefixes
294
+ # Try shell patterns
295
+ shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
664
296
  command_lines = [line.strip() for line in fix_command.split('\n')
665
- if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
297
+ if any(pattern in line for pattern in shell_patterns)]
666
298
 
667
299
  if command_lines:
668
- # Use the first command line found
669
300
  fix_command = command_lines[0]
670
- print(f"✅ Identified command by prefix: {fix_command}")
671
301
  else:
672
- # Try to find lines that look like commands (contain common shell patterns)
673
- shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
674
- command_lines = [line.strip() for line in fix_command.split('\n')
675
- if any(pattern in line for pattern in shell_patterns)]
676
-
677
- if command_lines:
678
- # Use the first command line found
679
- fix_command = command_lines[0]
680
- print(f"✅ Identified command by shell pattern: {fix_command}")
681
- else:
682
- # Fall back to the shortest non-empty line as it's likely the command
683
- lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
684
- if lines:
685
- # Exclude very short lines that are likely not commands
686
- valid_lines = [line for line in lines if len(line) > 5]
687
- if valid_lines:
688
- fix_command = min(valid_lines, key=len)
689
- else:
690
- fix_command = min(lines, key=len)
691
- print(f"✅ Selected shortest line as command: {fix_command}")
692
-
693
- # Clean up the command - remove any trailing periods or quotes
694
- fix_command = fix_command.rstrip('.;"\'')
695
-
696
- # Remove common prefixes that LLMs sometimes add
697
- prefixes_to_remove = [
698
- "Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
699
- "You should run: ", "You can run: ", "You need to run: "
700
- ]
701
- for prefix in prefixes_to_remove:
702
- if fix_command.startswith(prefix):
703
- fix_command = fix_command[len(prefix):].strip()
704
- print(f"✅ Removed prefix: {prefix}")
705
- break
706
-
707
- # If the command is still multi-line or very long, it might not be a valid command
708
- if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
709
- print("⚠️ Extracted command appears invalid (multi-line or too long)")
710
- print("🔍 Original response from LLM:")
711
- print("-" * 60)
712
- print(original_response)
713
- print("-" * 60)
714
- print("⚠️ Using best guess for command")
715
-
716
- print(f"🔧 Suggested fix: {fix_command}")
717
- print(f"🔍 DEBUG: Returning fix command: {fix_command}")
718
- return fix_command
719
- except Exception as e:
720
- print(f"❌ Error processing OpenAI response: {e}")
721
- print(f"🔍 DEBUG: Exception type: {type(e).__name__}")
722
- print(f"🔍 DEBUG: Exception details: {str(e)}")
723
- return None
724
-
725
- def call_openai_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
726
- """Call OpenAI to debug multiple failed commands and suggest fixes for all of them at once"""
727
- print("\n🔍 DEBUG: Starting batch LLM debugging...")
728
- print(f"🔍 DEBUG: Analyzing {len(failed_commands)} failed commands")
729
-
730
- if not failed_commands:
731
- print("⚠️ No failed commands to analyze")
732
- return []
733
-
734
- if not api_key:
735
- print("❌ No OpenAI API key provided for batch debugging")
736
- return []
737
-
738
- # Prepare context for batch analysis
739
- context_parts = []
740
- context_parts.append(f"Current directory: {current_dir}")
741
- context_parts.append(f"Sandbox available: {sandbox is not None}")
742
-
743
- # Add failed commands with their errors
744
- for i, failed_cmd in enumerate(failed_commands, 1):
745
- cmd_type = failed_cmd.get('type', 'main')
746
- original_cmd = failed_cmd.get('original_command', '')
747
- cmd_text = failed_cmd['command']
748
- stderr = failed_cmd.get('stderr', '')
749
- stdout = failed_cmd.get('stdout', '')
750
-
751
- context_parts.append(f"\n--- Failed Command {i} ({cmd_type}) ---")
752
- context_parts.append(f"Command: {cmd_text}")
753
- if original_cmd and original_cmd != cmd_text:
754
- context_parts.append(f"Original Command: {original_cmd}")
755
- if stderr:
756
- context_parts.append(f"Error Output: {stderr}")
757
- if stdout:
758
- context_parts.append(f"Standard Output: {stdout}")
302
+ # Use shortest non-empty line
303
+ lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
304
+ if lines:
305
+ valid_lines = [line for line in lines if len(line) > 5]
306
+ fix_command = min(valid_lines, key=len) if valid_lines else min(lines, key=len)
307
+
308
+ # Clean up the command
309
+ fix_command = fix_command.rstrip('.;"\'')
310
+
311
+ # Remove common prefixes
312
+ prefixes_to_remove = [
313
+ "Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
314
+ "You should run: ", "You can run: ", "You need to run: "
315
+ ]
316
+ for prefix in prefixes_to_remove:
317
+ if fix_command.startswith(prefix):
318
+ fix_command = fix_command[len(prefix):].strip()
319
+ break
759
320
 
760
- # Create the prompt for batch analysis
761
- prompt = f"""You are a debugging assistant analyzing multiple failed commands.
321
+ return fix_command
762
322
 
763
- Context:
764
- {chr(10).join(context_parts)}
765
-
766
- Please analyze each failed command and provide a fix command for each one. For each failed command, respond with:
767
-
768
- FIX_COMMAND_{i}: <the fix command>
769
- REASON_{i}: <brief explanation of why the original command failed and how the fix addresses it>
770
323
 
771
- Guidelines:
772
- - For file not found errors, first search for the file using 'find . -name filename -type f'
773
- - For missing packages, use appropriate package managers (pip, apt-get, npm)
774
- - For Git SSH authentication failures, convert SSH URLs to HTTPS URLs
775
- - For permission errors, suggest commands with sudo if appropriate
776
- - For network issues, suggest retry commands or alternative URLs
777
- - Keep each fix command simple and focused on the specific error
324
+ def make_api_request(provider, api_key, prompt, retries=2):
325
+ """Make API request to the specified provider"""
326
+ if provider == "openai":
327
+ return make_openai_request(api_key, prompt, retries)
328
+ elif provider == "anthropic":
329
+ return make_anthropic_request(api_key, prompt, retries)
330
+ elif provider == "openrouter":
331
+ return make_openrouter_request(api_key, prompt, retries)
332
+ else:
333
+ return None
778
334
 
779
- Provide fixes for all {len(failed_commands)} failed commands:"""
780
335
 
781
- # Make the API call
336
+ def make_openai_request(api_key, prompt, retries=2):
337
+ """Make request to OpenAI API"""
782
338
  headers = {
783
- "Authorization": f"Bearer {api_key}",
784
- "Content-Type": "application/json"
339
+ "Content-Type": "application/json",
340
+ "Authorization": f"Bearer {api_key}"
785
341
  }
786
342
 
787
343
  payload = {
788
- "model": "gpt-4o-mini", # Use a more capable model for batch analysis
344
+ "model": os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
789
345
  "messages": [
790
- {"role": "system", "content": "You are a debugging assistant. Analyze failed commands and provide specific fix commands. Return only the fix commands and reasons in the specified format."},
346
+ {"role": "system", "content": "You are a debugging assistant. Provide only the terminal command to fix the issue."},
791
347
  {"role": "user", "content": prompt}
792
348
  ],
793
- "temperature": 0.1,
794
- "max_tokens": 1000
349
+ "temperature": 0.2,
350
+ "max_tokens": 300
795
351
  }
796
352
 
797
- try:
798
- print(f"🤖 Calling OpenAI for batch debugging of {len(failed_commands)} commands...")
799
- response = requests.post(
800
- "https://api.openai.com/v1/chat/completions",
801
- headers=headers,
802
- json=payload,
803
- timeout=60
804
- )
805
-
806
- if response.status_code == 200:
807
- result = response.json()
808
- content = result['choices'][0]['message']['content']
809
- print(f"✅ Batch analysis completed")
810
-
811
- # Parse the response to extract fix commands
812
- fixes = []
813
- for i in range(1, len(failed_commands) + 1):
814
- fix_pattern = f"FIX_COMMAND_{i}: (.+)"
815
- reason_pattern = f"REASON_{i}: (.+)"
816
-
817
- fix_match = re.search(fix_pattern, content, re.MULTILINE)
818
- reason_match = re.search(reason_pattern, content, re.MULTILINE)
819
-
820
- if fix_match:
821
- fix_command = fix_match.group(1).strip()
822
- reason = reason_match.group(1).strip() if reason_match else "LLM suggested fix"
823
-
824
- # Clean up the fix command
825
- if fix_command.startswith('`') and fix_command.endswith('`'):
826
- fix_command = fix_command[1:-1]
827
-
828
- fixes.append({
829
- 'original_command': failed_commands[i-1]['command'],
830
- 'fix_command': fix_command,
831
- 'reason': reason,
832
- 'command_index': i-1
833
- })
834
-
835
- print(f"🔧 Generated {len(fixes)} fix commands from batch analysis")
836
- return fixes
837
- else:
838
- print(f"❌ OpenAI API error: {response.status_code} - {response.text}")
839
- return []
840
-
841
- except Exception as e:
842
- print(f"❌ Error during batch debugging: {e}")
843
- return []
844
-
845
- def call_anthropic_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
846
- """Call Anthropic Claude to debug a failed command and suggest a fix"""
847
- print("\n🔍 DEBUG: Starting Anthropic Claude debugging...")
848
- print(f"🔍 DEBUG: Command: {command}")
849
- print(f"🔍 DEBUG: Error output length: {len(error_output) if error_output else 0}")
850
- print(f"🔍 DEBUG: Current directory: {current_dir}")
851
- print(f"🔍 DEBUG: Sandbox available: {sandbox is not None}")
852
-
853
- # Define _to_str function locally to avoid NameError
854
- def _to_str(maybe_bytes):
353
+ for attempt in range(retries + 1):
855
354
  try:
856
- return (maybe_bytes.decode('utf-8') if isinstance(maybe_bytes, (bytes, bytearray)) else maybe_bytes)
857
- except UnicodeDecodeError:
858
- # Handle non-UTF-8 bytes by replacing invalid characters
859
- if isinstance(maybe_bytes, (bytes, bytearray)):
860
- return maybe_bytes.decode('utf-8', errors='replace')
355
+ if attempt > 0:
356
+ time.sleep(1.5 * (2 ** (attempt - 1)))
357
+
358
+ response = requests.post(
359
+ "https://api.openai.com/v1/chat/completions",
360
+ headers=headers,
361
+ json=payload,
362
+ timeout=45
363
+ )
364
+
365
+ if response.status_code == 200:
366
+ result = response.json()
367
+ return result["choices"][0]["message"]["content"]
368
+ elif response.status_code == 401:
369
+ print("❌ Invalid OpenAI API key")
370
+ return None
371
+ elif response.status_code in [429, 500]:
372
+ continue # Retry
861
373
  else:
862
- return str(maybe_bytes)
863
- except Exception:
864
- # Last resort fallback
865
- return str(maybe_bytes)
866
-
867
- # Skip debugging for certain commands that commonly return non-zero exit codes
868
- # but aren't actually errors (like test commands)
869
- if command.strip().startswith("test "):
870
- print("🔍 Skipping debugging for test command - non-zero exit code is expected behavior")
871
- return None
872
-
873
- # Validate error_output - if it's empty, we can't debug effectively
874
- if not error_output or not error_output.strip():
875
- print("⚠️ Error output is empty. Cannot effectively debug the command.")
876
- print("⚠️ Skipping Anthropic debugging due to lack of error information.")
877
- return None
878
-
879
- # Try to get API key from multiple sources
880
- if not api_key:
881
- print("🔍 DEBUG: No Anthropic API key provided, searching for one...")
882
-
883
- # First try environment variable
884
- api_key = os.environ.get("ANTHROPIC_API_KEY")
885
- print(f"🔍 DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
886
- if api_key:
887
- print(f"🔍 DEBUG: Environment API key value: {api_key}")
888
-
889
- # If not in environment, try to fetch from server using fetch_modal_tokens
890
- if not api_key:
891
- try:
892
- print("🔍 DEBUG: Trying to fetch API key from server...")
893
- from fetch_modal_tokens import get_tokens
894
- _, _, _, api_key = get_tokens()
895
- if api_key:
896
- # Set in environment for this session
897
- os.environ["ANTHROPIC_API_KEY"] = api_key
898
- else:
899
- print("⚠️ Could not fetch Anthropic API key from server")
900
- except Exception as e:
901
- print(f"⚠️ Error fetching API key from server: {e}")
902
-
903
- # Then try credentials manager
904
- if not api_key:
905
- print("🔍 DEBUG: Trying credentials manager...")
906
- try:
907
- from credentials_manager import CredentialsManager
908
- credentials_manager = CredentialsManager()
909
- api_key = credentials_manager.get_anthropic_api_key()
910
- if api_key:
911
- print(f"🔍 DEBUG: API key from credentials manager: Found")
912
- print(f"🔍 DEBUG: Credentials manager API key value: {api_key}")
913
- # Set in environment for this session
914
- os.environ["ANTHROPIC_API_KEY"] = api_key
915
- else:
916
- print("⚠️ Could not fetch Anthropic API key from credentials manager")
917
- except Exception as e:
918
- print(f"⚠️ Error fetching API key from credentials manager: {e}")
919
-
920
- # Store the API key in a persistent file if found
921
- if api_key:
922
- try:
923
- os.makedirs(os.path.expanduser("~/.gitarsenal"), exist_ok=True)
924
- with open(os.path.expanduser("~/.gitarsenal/anthropic_key"), "w") as f:
925
- f.write(api_key)
926
- print("✅ Saved Anthropic API key for future use")
927
- except Exception as e:
928
- print(f"⚠️ Could not save API key: {e}")
929
-
930
- # Try to load from saved file if not in environment
931
- if not api_key:
932
- try:
933
- key_file = os.path.expanduser("~/.gitarsenal/anthropic_key")
934
- print(f"🔍 DEBUG: Checking for saved API key at: {key_file}")
935
- if os.path.exists(key_file):
936
- with open(key_file, "r") as f:
937
- api_key = f.read().strip()
938
- if api_key:
939
- print("✅ Loaded Anthropic API key from saved file")
940
- print(f"🔍 DEBUG: API key from file: {api_key}")
941
- print(f"🔍 DEBUG: API key length: {len(api_key)}")
942
- # Also set in environment for this session
943
- os.environ["ANTHROPIC_API_KEY"] = api_key
944
- else:
945
- print("🔍 DEBUG: Saved file exists but is empty")
946
- else:
947
- print("🔍 DEBUG: No saved API key file found")
948
- except Exception as e:
949
- print(f"⚠️ Could not load saved API key: {e}")
950
-
951
- if not api_key:
952
- print("❌ No Anthropic API key available for debugging")
953
- return None
374
+ print(f"⚠️ OpenAI API error: {response.status_code}")
375
+ return None
376
+
377
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
378
+ continue # Retry
379
+ except Exception as e:
380
+ print(f"⚠️ OpenAI request error: {e}")
381
+ return None
954
382
 
955
- # Prepare the prompt for debugging
956
- error_str = _to_str(error_output)
957
- prompt = f"""You are a debugging assistant. Provide only the terminal command to fix the issue.
958
-
959
- Context:
960
- - Current directory: {current_dir}
961
- - Sandbox available: {sandbox is not None}
962
- - Failed command: {command}
963
- - Error output: {error_str}
964
-
965
- Analyze the issue first, understand why it's happening, then provide the command to fix it.
966
-
967
- Guidelines:
968
- - For file not found errors, first search for the file using 'find . -name filename -type f' and navigate to the directory if found
969
- - For missing packages, use appropriate package managers (pip, apt-get, npm)
970
- - For Git SSH authentication failures, always convert SSH URLs to HTTPS URLs (git@github.com:user/repo.git -> https://github.com/user/repo.git)
971
- - For authentication, suggest login commands with placeholders
972
- - For permission errors, suggest commands with sudo if appropriate
973
- - For network issues, suggest retry commands or alternative URLs
383
+ return None
974
384
 
975
- Return only the command to fix the issue, nothing else."""
976
385
 
977
- # Set up headers for Anthropic API
386
+ def make_anthropic_request(api_key, prompt, retries=2):
387
+ """Make request to Anthropic API"""
978
388
  headers = {
979
389
  "x-api-key": api_key,
980
390
  "anthropic-version": "2023-06-01",
981
391
  "content-type": "application/json"
982
392
  }
983
393
 
984
- # Models to try in order of preference
985
- models_to_try = ["claude-sonnet-4-20250514"]
394
+ payload = {
395
+ "model": "claude-sonnet-4-20250514",
396
+ "max_tokens": 300,
397
+ "messages": [{"role": "user", "content": prompt}]
398
+ }
986
399
 
987
- def try_api_call(model_name, retries=2, backoff_factor=1.5):
988
- payload = {
989
- "model": model_name,
990
- "max_tokens": 300,
991
- "messages": [
992
- {"role": "user", "content": prompt}
993
- ]
994
- }
995
-
996
- print(f"🔍 DEBUG: Payload prepared, prompt length: {len(prompt)}")
997
-
998
- # Add specific handling for common errors
999
- last_error = None
1000
- for attempt in range(retries + 1):
1001
- try:
1002
- if attempt > 0:
1003
- # Exponential backoff
1004
- wait_time = backoff_factor * (2 ** (attempt - 1))
1005
- print(f"⏱️ Retrying in {wait_time:.1f} seconds... (attempt {attempt+1}/{retries+1})")
1006
- time.sleep(wait_time)
1007
-
1008
- print(f"🤖 Calling Anthropic Claude with {model_name} model to debug the failed command...")
1009
- print(f"🔍 DEBUG: Making POST request to Anthropic API...")
1010
- response = requests.post(
1011
- "https://api.anthropic.com/v1/messages",
1012
- headers=headers,
1013
- json=payload,
1014
- timeout=45 # Increased timeout for reliability
1015
- )
1016
-
1017
- print(f"🔍 DEBUG: Response received, status code: {response.status_code}")
400
+ for attempt in range(retries + 1):
401
+ try:
402
+ if attempt > 0:
403
+ time.sleep(1.5 * (2 ** (attempt - 1)))
404
+
405
+ response = requests.post(
406
+ "https://api.anthropic.com/v1/messages",
407
+ headers=headers,
408
+ json=payload,
409
+ timeout=45
410
+ )
411
+
412
+ if response.status_code == 200:
413
+ result = response.json()
414
+ return result["content"][0]["text"]
415
+ elif response.status_code == 401:
416
+ print("❌ Invalid Anthropic API key")
417
+ return None
418
+ elif response.status_code in [429, 500]:
419
+ continue # Retry
420
+ else:
421
+ print(f"⚠️ Anthropic API error: {response.status_code}")
422
+ return None
1018
423
 
1019
- # Handle specific status codes
1020
- if response.status_code == 200:
1021
- print(f"🔍 DEBUG: Success! Response length: {len(response.text)}")
1022
- return response.json(), None
1023
- elif response.status_code == 401:
1024
- error_msg = "Authentication error: Invalid API key"
1025
- print(f"❌ {error_msg}")
1026
- print(f"🔍 DEBUG: Response text: {response.text}")
1027
- # Don't retry auth errors
1028
- return None, error_msg
1029
- elif response.status_code == 429:
1030
- error_msg = "Rate limit exceeded or quota reached"
1031
- print(f"⚠️ {error_msg}")
1032
- print(f"🔍 DEBUG: Response text: {response.text}")
1033
- # Always retry rate limit errors with increasing backoff
1034
- last_error = error_msg
1035
- continue
1036
- elif response.status_code == 500:
1037
- error_msg = "Anthropic server error"
1038
- print(f"⚠️ {error_msg}")
1039
- print(f"🔍 DEBUG: Response text: {response.text}")
1040
- # Retry server errors
1041
- last_error = error_msg
1042
- continue
1043
- else:
1044
- error_msg = f"Status code: {response.status_code}, Response: {response.text}"
1045
- print(f"⚠️ Anthropic API error: {error_msg}")
1046
- print(f"🔍 DEBUG: Full response text: {response.text}")
1047
- last_error = error_msg
1048
- # Only retry if we have attempts left
1049
- if attempt < retries:
1050
- continue
1051
- return None, error_msg
1052
- except requests.exceptions.Timeout:
1053
- error_msg = "Request timed out"
1054
- last_error = error_msg
1055
- # Always retry timeouts
1056
- continue
1057
- except requests.exceptions.ConnectionError:
1058
- error_msg = "Connection error"
1059
- print(f"⚠️ {error_msg}")
1060
- print(f"🔍 DEBUG: Connection failed to api.anthropic.com")
1061
- last_error = error_msg
1062
- # Always retry connection errors
1063
- continue
1064
- except Exception as e:
1065
- error_msg = str(e)
1066
- print(f"⚠️ Unexpected error: {error_msg}")
1067
- print(f"🔍 DEBUG: Exception type: {type(e).__name__}")
1068
- print(f"🔍 DEBUG: Exception details: {str(e)}")
1069
- last_error = error_msg
1070
- # Only retry if we have attempts left
1071
- if attempt < retries:
1072
- continue
1073
- return None, error_msg
1074
-
1075
- # If we get here, all retries failed
1076
- return None, last_error
1077
-
1078
- # Try each model in sequence until one works
1079
- result = None
1080
- last_error = None
424
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
425
+ continue # Retry
426
+ except Exception as e:
427
+ print(f"⚠️ Anthropic request error: {e}")
428
+ return None
1081
429
 
1082
- for model in models_to_try:
1083
- result, error = try_api_call(model)
1084
- if result:
1085
- break
1086
- else:
1087
- print(f"⚠️ Failed to get response from {model}: {error}")
1088
- last_error = error
430
+ return None
431
+
432
+
433
+ def make_openrouter_request(api_key, prompt, retries=2):
434
+ """Make request to OpenRouter API"""
435
+ headers = {
436
+ "x-api-key": api_key,
437
+ "content-type": "application/json"
438
+ }
1089
439
 
1090
- if not result:
1091
- print(f"❌ All model attempts failed. Last error: {last_error}")
1092
- return None
440
+ payload = {
441
+ "model": "openai/gpt-4o-mini",
442
+ "max_tokens": 300,
443
+ "messages": [{"role": "user", "content": prompt}]
444
+ }
1093
445
 
1094
- # Process the response
1095
- try:
1096
- print(f"🔍 DEBUG: Processing Anthropic response...")
1097
- print(f"🔍 DEBUG: Choices count: {len(result.get('content', []))}")
1098
-
1099
- fix_command = result["content"][0]["text"].strip()
1100
- print(f"🔍 DEBUG: Raw response content: {fix_command}")
1101
-
1102
- # Save the original response for debugging
1103
- original_response = fix_command
1104
-
1105
- # Extract just the command if it's wrapped in backticks or explanation
1106
- if "```" in fix_command:
1107
- # Extract content between backticks
1108
- import re
1109
- code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
1110
- if code_blocks:
1111
- fix_command = code_blocks[0].strip()
1112
- print(f"✅ Extracted command from code block: {fix_command}")
1113
-
1114
- # If the response still has explanatory text, try to extract just the command
1115
- if len(fix_command.split('\n')) > 1:
1116
- # First try to find lines that look like commands (start with common command prefixes)
1117
- command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
1118
- 'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
1119
- 'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
1120
- 'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
1121
-
1122
- # Check for lines that start with common command prefixes
1123
- command_lines = [line.strip() for line in fix_command.split('\n')
1124
- if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
1125
-
1126
- if command_lines:
1127
- # Use the first command line found
1128
- fix_command = command_lines[0]
1129
- print(f"✅ Identified command by prefix: {fix_command}")
446
+ for attempt in range(retries + 1):
447
+ try:
448
+ if attempt > 0:
449
+ time.sleep(1.5 * (2 ** (attempt - 1)))
450
+
451
+ response = requests.post(
452
+ "https://openrouter.ai/api/v1/chat/completions",
453
+ headers=headers,
454
+ json=payload,
455
+ timeout=45
456
+ )
457
+
458
+ if response.status_code == 200:
459
+ result = response.json()
460
+ return result["choices"][0]["message"]["content"]
461
+ elif response.status_code == 401:
462
+ print("❌ Invalid OpenRouter API key")
463
+ return None
464
+ elif response.status_code in [429, 500]:
465
+ continue # Retry
1130
466
  else:
1131
- # Try to find lines that look like commands (contain common shell patterns)
1132
- shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
1133
- command_lines = [line.strip() for line in fix_command.split('\n')
1134
- if any(pattern in line for pattern in shell_patterns)]
467
+ print(f"⚠️ OpenRouter API error: {response.status_code}")
468
+ return None
1135
469
 
1136
- if command_lines:
1137
- # Use the first command line found
1138
- fix_command = command_lines[0]
1139
- print(f" Identified command by shell pattern: {fix_command}")
1140
- else:
1141
- # Fall back to the shortest non-empty line as it's likely the command
1142
- lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
1143
- if lines:
1144
- # Exclude very short lines that are likely not commands
1145
- valid_lines = [line for line in lines if len(line) > 5]
1146
- if valid_lines:
1147
- fix_command = min(valid_lines, key=len)
1148
- else:
1149
- fix_command = min(lines, key=len)
1150
- print(f"✅ Selected shortest line as command: {fix_command}")
1151
-
1152
- # Clean up the command - remove any trailing periods or quotes
1153
- fix_command = fix_command.rstrip('.;"\'')
1154
-
1155
- # Remove common prefixes that LLMs sometimes add
1156
- prefixes_to_remove = [
1157
- "Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
1158
- "You should run: ", "You can run: ", "You need to run: "
1159
- ]
1160
- for prefix in prefixes_to_remove:
1161
- if fix_command.startswith(prefix):
1162
- fix_command = fix_command[len(prefix):].strip()
1163
- print(f"✅ Removed prefix: {prefix}")
1164
- break
1165
-
1166
- # If the command is still multi-line or very long, it might not be a valid command
1167
- if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
1168
- print("⚠️ Extracted command appears invalid (multi-line or too long)")
1169
- print("🔍 Original response from LLM:")
1170
- print("-" * 60)
1171
- print(original_response)
1172
- print("-" * 60)
1173
- print("⚠️ Using best guess for command")
1174
-
1175
- print(f"🔧 Suggested fix: {fix_command}")
1176
- print(f"🔍 DEBUG: Returning fix command: {fix_command}")
1177
- return fix_command
1178
- except Exception as e:
1179
- print(f"❌ Error processing Anthropic response: {e}")
1180
- print(f"🔍 DEBUG: Exception type: {type(e).__name__}")
1181
- print(f"🔍 DEBUG: Exception details: {str(e)}")
1182
- return None
1183
-
1184
- def call_openrouter_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
1185
- """Call OpenRouter to debug a failed command and suggest a fix"""
1186
- print("\n🔍 DEBUG: Starting OpenRouter debugging...")
1187
- print(f"🔍 DEBUG: Command: {command}")
1188
- print(f"🔍 DEBUG: Error output length: {len(error_output) if error_output else 0}")
1189
- print(f"🔍 DEBUG: Current directory: {current_dir}")
1190
- print(f"🔍 DEBUG: Sandbox available: {sandbox is not None}")
470
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
471
+ continue # Retry
472
+ except Exception as e:
473
+ print(f"⚠️ OpenRouter request error: {e}")
474
+ return None
1191
475
 
1192
- # Define _to_str function locally to avoid NameError
1193
- def _to_str(maybe_bytes):
1194
- try:
1195
- return (maybe_bytes.decode('utf-8') if isinstance(maybe_bytes, (bytes, bytearray)) else maybe_bytes)
1196
- except UnicodeDecodeError:
1197
- # Handle non-UTF-8 bytes by replacing invalid characters
1198
- if isinstance(maybe_bytes, (bytes, bytearray)):
1199
- return maybe_bytes.decode('utf-8', errors='replace')
1200
- else:
1201
- return str(maybe_bytes)
1202
- except Exception:
1203
- # Last resort fallback
1204
- return str(maybe_bytes)
1205
-
1206
- # Skip debugging for certain commands that commonly return non-zero exit codes
1207
- # but aren't actually errors (like test commands)
476
+ return None
477
+
478
+
479
+ def call_llm_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
480
+ """Unified function to call LLM for debugging"""
481
+ # Skip debugging for test commands
1208
482
  if command.strip().startswith("test "):
1209
- print("🔍 Skipping debugging for test command - non-zero exit code is expected behavior")
1210
483
  return None
1211
484
 
1212
- # Validate error_output - if it's empty, we can't debug effectively
485
+ # Validate error output
1213
486
  if not error_output or not error_output.strip():
1214
- print("⚠️ Error output is empty. Cannot effectively debug the command.")
1215
- print("⚠️ Skipping OpenRouter debugging due to lack of error information.")
487
+ print("⚠️ Error output is empty. Cannot debug effectively.")
1216
488
  return None
1217
489
 
1218
- # Try to get API key from multiple sources
490
+ current_model = get_current_debug_model()
491
+ print(f"🔍 Using {current_model.upper()} for debugging...")
492
+
493
+ # Get API key
1219
494
  if not api_key:
1220
- print("🔍 DEBUG: No OpenRouter API key provided, searching for one...")
1221
-
1222
- # First try environment variable
1223
- api_key = os.environ.get("OPENROUTER_API_KEY")
1224
- print(f"🔍 DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
1225
- if api_key:
1226
- print(f"🔍 DEBUG: Environment API key value: {api_key}")
1227
-
1228
- # If not in environment, try to fetch from server using fetch_modal_tokens
1229
- if not api_key:
1230
- try:
1231
- print("🔍 DEBUG: Trying to fetch API key from server...")
1232
- from fetch_modal_tokens import get_tokens
1233
- # Assuming OpenRouter key is the 5th token in the tuple
1234
- tokens = get_tokens()
1235
- if len(tokens) >= 5:
1236
- api_key = tokens[4]
1237
- except Exception as e:
1238
- print(f"⚠️ Error fetching OpenRouter API key from server: {e}")
1239
-
1240
- # Then try credentials manager
1241
- if not api_key:
1242
- try:
1243
- from credentials_manager import CredentialsManager
1244
- credentials_manager = CredentialsManager()
1245
- api_key = credentials_manager.get_openrouter_api_key()
1246
- except Exception as e:
1247
- print(f"⚠️ Error getting OpenRouter API key from credentials manager: {e}")
1248
-
1249
- # Store the API key in a persistent file if found
1250
- if api_key:
1251
- try:
1252
- os.makedirs(os.path.expanduser("~/.gitarsenal"), exist_ok=True)
1253
- with open(os.path.expanduser("~/.gitarsenal/openrouter_key"), "w") as f:
1254
- f.write(api_key)
1255
- print("✅ Saved OpenRouter API key for future use")
1256
- except Exception as e:
1257
- print(f"⚠️ Could not save API key: {e}")
1258
-
1259
- # Try to load from saved file if not in environment
1260
- if not api_key:
1261
- try:
1262
- key_file = os.path.expanduser("~/.gitarsenal/openrouter_key")
1263
- print(f"🔍 DEBUG: Checking for saved API key at: {key_file}")
1264
- if os.path.exists(key_file):
1265
- with open(key_file, "r") as f:
1266
- api_key = f.read().strip()
1267
- if api_key:
1268
- print("✅ Loaded OpenRouter API key from saved file")
1269
- print(f"🔍 DEBUG: API key from file: {api_key}")
1270
- print(f"🔍 DEBUG: API key length: {len(api_key)}")
1271
- # Also set in environment for this session
1272
- os.environ["OPENROUTER_API_KEY"] = api_key
1273
- else:
1274
- print("🔍 DEBUG: Saved file exists but is empty")
1275
- else:
1276
- print("🔍 DEBUG: No saved API key file found")
1277
- except Exception as e:
1278
- print(f"⚠️ Could not load saved API key: {e}")
495
+ api_key = get_api_key(current_model)
1279
496
 
1280
497
  if not api_key:
1281
- print("❌ No OpenRouter API key available for debugging")
498
+ print(f"❌ No {current_model} API key available. Cannot perform LLM debugging.")
1282
499
  return None
1283
500
 
1284
- # Prepare the prompt for debugging
1285
- error_str = _to_str(error_output)
1286
- prompt = f"""You are a debugging assistant. Provide only the terminal command to fix the issue.
1287
-
1288
- Context:
1289
- - Current directory: {current_dir}
1290
- - Sandbox available: {sandbox is not None}
1291
- - Failed command: {command}
1292
- - Error output: {error_str}
1293
-
1294
- Analyze the issue first, understand why it's happening, then provide the command to fix it.
1295
-
1296
- Guidelines:
1297
- - For file not found errors, first search for the file using 'find . -name filename -type f' and navigate to the directory if found
1298
- - For missing packages, use appropriate package managers (pip, apt-get, npm)
1299
- - For Git SSH authentication failures, always convert SSH URLs to HTTPS URLs (git@github.com:user/repo.git -> https://github.com/user/repo.git)
1300
- - For authentication, suggest login commands with placeholders
1301
- - For permission errors, suggest commands with sudo if appropriate
1302
- - For network issues, suggest retry commands or alternative URLs
1303
-
1304
- Return only the command to fix the issue, nothing else."""
1305
-
1306
- # Set up headers for OpenRouter API
1307
- headers = {
1308
- "x-api-key": api_key,
1309
- "content-type": "application/json"
1310
- }
501
+ # Save API key for future use
502
+ save_api_key(current_model, api_key)
1311
503
 
1312
- # Models to try in order of preference
1313
- models_to_try = ["openrouter/openai-gpt-4o-mini"]
504
+ # Gather context
505
+ system_info, directory_context, file_context = gather_context(sandbox, current_dir)
1314
506
 
1315
- def try_api_call(model_name, retries=2, backoff_factor=1.5):
1316
- payload = {
1317
- "model": model_name,
1318
- "max_tokens": 300,
1319
- "messages": [
1320
- {"role": "user", "content": prompt}
1321
- ]
1322
- }
1323
-
1324
- print(f"🔍 DEBUG: Payload prepared, prompt length: {len(prompt)}")
1325
-
1326
- # Add specific handling for common errors
1327
- last_error = None
1328
- for attempt in range(retries + 1):
1329
- try:
1330
- if attempt > 0:
1331
- # Exponential backoff
1332
- wait_time = backoff_factor * (2 ** (attempt - 1))
1333
- print(f"⏱️ Retrying in {wait_time:.1f} seconds... (attempt {attempt+1}/{retries+1})")
1334
- time.sleep(wait_time)
1335
-
1336
- print(f"🤖 Calling OpenRouter with {model_name} model to debug the failed command...")
1337
- print(f"🔍 DEBUG: Making POST request to OpenRouter API...")
1338
- response = requests.post(
1339
- "https://openrouter.ai/api/v1/chat/completions", # OpenRouter API endpoint
1340
- headers=headers,
1341
- json=payload,
1342
- timeout=45 # Increased timeout for reliability
1343
- )
1344
-
1345
- print(f"🔍 DEBUG: Response received, status code: {response.status_code}")
1346
-
1347
- # Handle specific status codes
1348
- if response.status_code == 200:
1349
- print(f"🔍 DEBUG: Success! Response length: {len(response.text)}")
1350
- return response.json(), None
1351
- elif response.status_code == 401:
1352
- error_msg = "Authentication error: Invalid API key"
1353
- print(f"❌ {error_msg}")
1354
- print(f"🔍 DEBUG: Response text: {response.text}")
1355
- # Don't retry auth errors
1356
- return None, error_msg
1357
- elif response.status_code == 429:
1358
- error_msg = "Rate limit exceeded or quota reached"
1359
- print(f"⚠️ {error_msg}")
1360
- print(f"🔍 DEBUG: Response text: {response.text}")
1361
- # Always retry rate limit errors with increasing backoff
1362
- last_error = error_msg
1363
- continue
1364
- elif response.status_code == 500:
1365
- error_msg = "OpenRouter server error"
1366
- print(f"⚠️ {error_msg}")
1367
- print(f"🔍 DEBUG: Response text: {response.text}")
1368
- # Retry server errors
1369
- last_error = error_msg
1370
- continue
1371
- else:
1372
- error_msg = f"Status code: {response.status_code}, Response: {response.text}"
1373
- print(f"⚠️ OpenRouter API error: {error_msg}")
1374
- print(f"🔍 DEBUG: Full response text: {response.text}")
1375
- last_error = error_msg
1376
- # Only retry if we have attempts left
1377
- if attempt < retries:
1378
- continue
1379
- return None, error_msg
1380
- except requests.exceptions.Timeout:
1381
- error_msg = "Request timed out"
1382
- last_error = error_msg
1383
- # Always retry timeouts
1384
- continue
1385
- except requests.exceptions.ConnectionError:
1386
- error_msg = "Connection error"
1387
- print(f"⚠️ {error_msg}")
1388
- print(f"🔍 DEBUG: Connection failed to openrouter.ai")
1389
- last_error = error_msg
1390
- # Always retry connection errors
1391
- continue
1392
- except Exception as e:
1393
- error_msg = str(e)
1394
- print(f"⚠️ Unexpected error: {error_msg}")
1395
- print(f"🔍 DEBUG: Exception type: {type(e).__name__}")
1396
- print(f"🔍 DEBUG: Exception details: {str(e)}")
1397
- last_error = error_msg
1398
- # Only retry if we have attempts left
1399
- if attempt < retries:
1400
- continue
1401
- return None, error_msg
1402
-
1403
- # If we get here, all retries failed
1404
- return None, last_error
507
+ # Get credentials context
508
+ stored_credentials = get_stored_credentials()
509
+ auth_context = generate_auth_context(stored_credentials)
1405
510
 
1406
- # Try each model in sequence until one works
1407
- result = None
1408
- last_error = None
511
+ # Create prompt
512
+ prompt = create_debug_prompt(command, error_output, system_info, directory_context, file_context, auth_context)
1409
513
 
1410
- for model in models_to_try:
1411
- result, error = try_api_call(model)
1412
- if result:
1413
- break
1414
- else:
1415
- print(f"⚠️ Failed to get response from {model}: {error}")
1416
- last_error = error
514
+ print(f"\n{'='*60}")
515
+ print("DEBUG: ERROR_OUTPUT SENT TO LLM:")
516
+ print(f"{'='*60}")
517
+ print(f"{error_output}")
518
+ print(f"{'='*60}\n")
1417
519
 
1418
- if not result:
1419
- print(f" All model attempts failed. Last error: {last_error}")
1420
- return None
520
+ # Make API request
521
+ print(f"🤖 Calling {current_model} to debug the failed command...")
522
+ response_text = make_api_request(current_model, api_key, prompt)
1421
523
 
1422
- # Process the response
1423
- try:
1424
- print(f"🔍 DEBUG: Processing OpenRouter response...")
1425
- print(f"🔍 DEBUG: Choices count: {len(result.get('choices', []))}")
1426
-
1427
- fix_command = result["choices"][0]["message"]["content"].strip()
1428
- print(f"🔍 DEBUG: Raw response content: {fix_command}")
1429
-
1430
- # Save the original response for debugging
1431
- original_response = fix_command
1432
-
1433
- # Extract just the command if it's wrapped in backticks or explanation
1434
- if "```" in fix_command:
1435
- # Extract content between backticks
1436
- import re
1437
- code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
1438
- if code_blocks:
1439
- fix_command = code_blocks[0].strip()
1440
- print(f"✅ Extracted command from code block: {fix_command}")
1441
-
1442
- # If the response still has explanatory text, try to extract just the command
1443
- if len(fix_command.split('\n')) > 1:
1444
- # First try to find lines that look like commands (start with common command prefixes)
1445
- command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
1446
- 'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
1447
- 'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
1448
- 'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
1449
-
1450
- # Check for lines that start with common command prefixes
1451
- command_lines = [line.strip() for line in fix_command.split('\n')
1452
- if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
1453
-
1454
- if command_lines:
1455
- # Use the first command line found
1456
- fix_command = command_lines[0]
1457
- print(f"✅ Identified command by prefix: {fix_command}")
1458
- else:
1459
- # Try to find lines that look like commands (contain common shell patterns)
1460
- shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
1461
- command_lines = [line.strip() for line in fix_command.split('\n')
1462
- if any(pattern in line for pattern in shell_patterns)]
1463
-
1464
- if command_lines:
1465
- # Use the first command line found
1466
- fix_command = command_lines[0]
1467
- print(f"✅ Identified command by shell pattern: {fix_command}")
1468
- else:
1469
- # Fall back to the shortest non-empty line as it's likely the command
1470
- lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
1471
- if lines:
1472
- # Exclude very short lines that are likely not commands
1473
- valid_lines = [line for line in lines if len(line) > 5]
1474
- if valid_lines:
1475
- fix_command = min(valid_lines, key=len)
1476
- else:
1477
- fix_command = min(lines, key=len)
1478
- print(f"✅ Selected shortest line as command: {fix_command}")
1479
-
1480
- # Clean up the command - remove any trailing periods or quotes
1481
- fix_command = fix_command.rstrip('.;"\'')
1482
-
1483
- # Remove common prefixes that LLMs sometimes add
1484
- prefixes_to_remove = [
1485
- "Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
1486
- "You should run: ", "You can run: ", "You need to run: "
1487
- ]
1488
- for prefix in prefixes_to_remove:
1489
- if fix_command.startswith(prefix):
1490
- fix_command = fix_command[len(prefix):].strip()
1491
- print(f"✅ Removed prefix: {prefix}")
1492
- break
1493
-
1494
- # If the command is still multi-line or very long, it might not be a valid command
1495
- if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
1496
- print("⚠️ Extracted command appears invalid (multi-line or too long)")
1497
- print("🔍 Original response from LLM:")
1498
- print("-" * 60)
1499
- print(original_response)
1500
- print("-" * 60)
1501
- print("⚠️ Using best guess for command")
1502
-
1503
- print(f"🔧 Suggested fix: {fix_command}")
1504
- print(f"🔍 DEBUG: Returning fix command: {fix_command}")
1505
- return fix_command
1506
- except Exception as e:
1507
- print(f"❌ Error processing OpenRouter response: {e}")
1508
- print(f"🔍 DEBUG: Exception type: {type(e).__name__}")
1509
- print(f"🔍 DEBUG: Exception details: {str(e)}")
524
+ if not response_text:
1510
525
  return None
1511
-
1512
-
1513
- def call_llm_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
1514
- """Unified function to call LLM for batch debugging - routes to OpenAI or Anthropic based on configuration"""
1515
- current_model = get_current_debug_model()
1516
526
 
1517
- print(f"🔍 DEBUG: Using {current_model.upper()} for batch debugging...")
527
+ # Extract command from response
528
+ fix_command = extract_command_from_response(response_text)
1518
529
 
1519
- if current_model == "anthropic":
1520
- # Try to get Anthropic API key if not provided
1521
- if not api_key:
1522
- # First try environment variable
1523
- api_key = os.environ.get("ANTHROPIC_API_KEY")
1524
-
1525
- # If not in environment, try to fetch from server using fetch_modal_tokens
1526
- if not api_key:
1527
- try:
1528
- from fetch_modal_tokens import get_tokens
1529
- _, _, _, api_key = get_tokens()
1530
- except Exception as e:
1531
- print(f"⚠️ Error fetching Anthropic API key from server: {e}")
1532
-
1533
- # Then try credentials manager
1534
- if not api_key:
1535
- try:
1536
- from credentials_manager import CredentialsManager
1537
- credentials_manager = CredentialsManager()
1538
- api_key = credentials_manager.get_anthropic_api_key()
1539
- except Exception as e:
1540
- print(f"⚠️ Error getting Anthropic API key from credentials manager: {e}")
1541
-
1542
- return call_anthropic_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
1543
- elif current_model == "openrouter":
1544
- # Try to get OpenRouter API key if not provided
1545
- if not api_key:
1546
- # First try environment variable
1547
- api_key = os.environ.get("OPENROUTER_API_KEY")
1548
-
1549
- # If not in environment, try to fetch from server using fetch_modal_tokens
1550
- if not api_key:
1551
- try:
1552
- from fetch_modal_tokens import get_tokens
1553
- # Assuming OpenRouter key is the 5th token in the tuple
1554
- tokens = get_tokens()
1555
- if len(tokens) >= 5:
1556
- api_key = tokens[4]
1557
- except Exception as e:
1558
- print(f"⚠️ Error fetching OpenRouter API key from server: {e}")
1559
-
1560
- # Then try credentials manager
1561
- if not api_key:
1562
- try:
1563
- from credentials_manager import CredentialsManager
1564
- credentials_manager = CredentialsManager()
1565
- api_key = credentials_manager.get_openrouter_api_key()
1566
- except Exception as e:
1567
- print(f"⚠️ Error getting OpenRouter API key from credentials manager: {e}")
1568
-
1569
- return call_openrouter_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
1570
- else:
1571
- # Default to OpenAI
1572
- # Try to get OpenAI API key if not provided
1573
- if not api_key:
1574
- # First try environment variable
1575
- api_key = os.environ.get("OPENAI_API_KEY")
1576
-
1577
- # If not in environment, try to fetch from server using fetch_modal_tokens
1578
- if not api_key:
1579
- try:
1580
- from fetch_modal_tokens import get_tokens
1581
- _, _, api_key, _ = get_tokens()
1582
- except Exception as e:
1583
- print(f"⚠️ Error fetching OpenAI API key from server: {e}")
1584
-
1585
- # Then try credentials manager
1586
- if not api_key:
1587
- try:
1588
- from credentials_manager import CredentialsManager
1589
- credentials_manager = CredentialsManager()
1590
- api_key = credentials_manager.get_openai_api_key()
1591
- except Exception as e:
1592
- print(f"⚠️ Error getting OpenAI API key from credentials manager: {e}")
1593
-
1594
- return call_openai_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
530
+ print(f"🔧 Suggested fix: {fix_command}")
531
+ return fix_command
1595
532
 
1596
- def call_anthropic_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
1597
- """Call Anthropic Claude to debug multiple failed commands and suggest fixes for all of them at once"""
1598
- print("\n🔍 DEBUG: Starting batch Anthropic Claude debugging...")
1599
- print(f"🔍 DEBUG: Analyzing {len(failed_commands)} failed commands")
1600
-
533
+
534
+ def call_llm_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
535
+ """Call LLM for batch debugging of multiple failed commands"""
1601
536
  if not failed_commands:
1602
- print("⚠️ No failed commands to analyze")
1603
537
  return []
1604
538
 
539
+ current_model = get_current_debug_model()
540
+
541
+ # Get API key
1605
542
  if not api_key:
1606
- print("🔍 DEBUG: No Anthropic API key provided, searching for one...")
1607
-
1608
- # First try environment variable
1609
- api_key = os.environ.get("ANTHROPIC_API_KEY")
1610
- print(f"🔍 DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
1611
- if api_key:
1612
- print(f"🔍 DEBUG: Environment API key value: {api_key}")
1613
-
1614
- # If not in environment, try to fetch from server using fetch_modal_tokens
1615
- if not api_key:
1616
- try:
1617
- print("🔍 DEBUG: Trying to fetch API key from server...")
1618
- from fetch_modal_tokens import get_tokens
1619
- _, _, _, api_key = get_tokens()
1620
- if api_key:
1621
- # Set in environment for this session
1622
- os.environ["ANTHROPIC_API_KEY"] = api_key
1623
- else:
1624
- print("⚠️ Could not fetch Anthropic API key from server")
1625
- except Exception as e:
1626
- print(f"⚠️ Error fetching API key from server: {e}")
1627
-
1628
- # Then try credentials manager
1629
- if not api_key:
1630
- print("🔍 DEBUG: Trying credentials manager...")
1631
- try:
1632
- from credentials_manager import CredentialsManager
1633
- credentials_manager = CredentialsManager()
1634
- api_key = credentials_manager.get_anthropic_api_key()
1635
- if api_key:
1636
- print(f"🔍 DEBUG: API key from credentials manager: Found")
1637
- print(f"🔍 DEBUG: Credentials manager API key value: {api_key}")
1638
- # Set in environment for this session
1639
- os.environ["ANTHROPIC_API_KEY"] = api_key
1640
- else:
1641
- print("⚠️ Could not fetch Anthropic API key from credentials manager")
1642
- except Exception as e:
1643
- print(f"⚠️ Error fetching API key from credentials manager: {e}")
1644
-
1645
- if not api_key:
1646
- print("❌ No Anthropic API key available for batch debugging")
1647
- return []
543
+ api_key = get_api_key(current_model)
544
+
545
+ if not api_key:
546
+ print(f"❌ No {current_model} API key available for batch debugging")
547
+ return []
1648
548
 
1649
549
  # Prepare context for batch analysis
1650
- context_parts = []
1651
- context_parts.append(f"Current directory: {current_dir}")
1652
- context_parts.append(f"Sandbox available: {sandbox is not None}")
550
+ context_parts = [f"Current directory: {current_dir}", f"Sandbox available: {sandbox is not None}"]
1653
551
 
1654
- # Add failed commands with their errors
1655
552
  for i, failed_cmd in enumerate(failed_commands, 1):
1656
553
  cmd_type = failed_cmd.get('type', 'main')
1657
554
  original_cmd = failed_cmd.get('original_command', '')
@@ -1668,7 +565,7 @@ def call_anthropic_for_batch_debug(failed_commands, api_key=None, current_dir=No
1668
565
  if stdout:
1669
566
  context_parts.append(f"Standard Output: {stdout}")
1670
567
 
1671
- # Create the prompt for batch analysis
568
+ # Create batch prompt
1672
569
  prompt = f"""You are a debugging assistant analyzing multiple failed commands.
1673
570
 
1674
571
  Context:
@@ -1676,257 +573,78 @@ Context:
1676
573
 
1677
574
  Please analyze each failed command and provide a fix command for each one. For each failed command, respond with:
1678
575
 
1679
- FIX_COMMAND_{i}: <the fix command>
1680
- REASON_{i}: <brief explanation of why the original command failed and how the fix addresses it>
576
+ FIX_COMMAND_{{i}}: <the fix command>
577
+ REASON_{{i}}: <brief explanation of why the original command failed and how the fix addresses it>
1681
578
 
1682
579
  Guidelines:
1683
580
  - For file not found errors, first search for the file using 'find . -name filename -type f'
1684
581
  - For missing packages, use appropriate package managers (pip, apt-get, npm)
1685
582
  - For Git SSH authentication failures, convert SSH URLs to HTTPS URLs
1686
583
  - For permission errors, suggest commands with sudo if appropriate
1687
- - For network issues, suggest retry commands or alternative URLs
1688
584
  - Keep each fix command simple and focused on the specific error
1689
585
 
1690
586
  Provide fixes for all {len(failed_commands)} failed commands:"""
1691
587
 
1692
- # Set up headers for Anthropic API
1693
- headers = {
1694
- "x-api-key": api_key,
1695
- "anthropic-version": "2023-06-01",
1696
- "content-type": "application/json"
1697
- }
588
+ print(f"🤖 Calling {current_model} for batch debugging of {len(failed_commands)} commands...")
589
+ response_text = make_api_request(current_model, api_key, prompt)
1698
590
 
1699
- payload = {
1700
- "model": "claude-3-5-sonnet-20241022", # Use a more capable model for batch analysis
1701
- "max_tokens": 1000,
1702
- "messages": [
1703
- {"role": "user", "content": prompt}
1704
- ]
1705
- }
1706
-
1707
- try:
1708
- print(f"🤖 Calling Anthropic Claude for batch debugging of {len(failed_commands)} commands...")
1709
- response = requests.post(
1710
- "https://api.anthropic.com/v1/messages",
1711
- headers=headers,
1712
- json=payload,
1713
- timeout=60
1714
- )
1715
-
1716
- if response.status_code == 200:
1717
- result = response.json()
1718
- content = result['content'][0]['text']
1719
- print(f"✅ Batch analysis completed")
1720
-
1721
- # Parse the response to extract fix commands
1722
- fixes = []
1723
- for i in range(1, len(failed_commands) + 1):
1724
- fix_pattern = f"FIX_COMMAND_{i}: (.+)"
1725
- reason_pattern = f"REASON_{i}: (.+)"
1726
-
1727
- fix_match = re.search(fix_pattern, content, re.MULTILINE)
1728
- reason_match = re.search(reason_pattern, content, re.MULTILINE)
1729
-
1730
- if fix_match:
1731
- fix_command = fix_match.group(1).strip()
1732
- reason = reason_match.group(1).strip() if reason_match else "Anthropic Claude suggested fix"
1733
-
1734
- # Clean up the fix command
1735
- if fix_command.startswith('`') and fix_command.endswith('`'):
1736
- fix_command = fix_command[1:-1]
1737
-
1738
- fixes.append({
1739
- 'original_command': failed_commands[i-1]['command'],
1740
- 'fix_command': fix_command,
1741
- 'reason': reason,
1742
- 'command_index': i-1
1743
- })
1744
-
1745
- print(f"🔧 Generated {len(fixes)} fix commands from batch analysis")
1746
- return fixes
1747
- else:
1748
- print(f"❌ Anthropic API error: {response.status_code} - {response.text}")
1749
- return []
1750
-
1751
- except Exception as e:
1752
- print(f"❌ Error during batch debugging: {e}")
1753
- return []
1754
-
1755
- def call_openrouter_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
1756
- """Call OpenRouter to debug multiple failed commands and suggest fixes for all of them at once"""
1757
- print("\n🔍 DEBUG: Starting batch OpenRouter debugging...")
1758
- print(f"🔍 DEBUG: Analyzing {len(failed_commands)} failed commands")
1759
-
1760
- if not failed_commands:
1761
- print("⚠️ No failed commands to analyze")
591
+ if not response_text:
1762
592
  return []
1763
593
 
1764
- if not api_key:
1765
- print("🔍 DEBUG: No OpenRouter API key provided, searching for one...")
594
+ # Parse the response to extract fix commands
595
+ fixes = []
596
+ for i in range(1, len(failed_commands) + 1):
597
+ fix_pattern = f"FIX_COMMAND_{i}: (.+)"
598
+ reason_pattern = f"REASON_{i}: (.+)"
1766
599
 
1767
- # First try environment variable
1768
- api_key = os.environ.get("OPENROUTER_API_KEY")
1769
- print(f"🔍 DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
1770
- if api_key:
1771
- print(f"🔍 DEBUG: Environment API key value: {api_key}")
600
+ fix_match = re.search(fix_pattern, response_text, re.MULTILINE)
601
+ reason_match = re.search(reason_pattern, response_text, re.MULTILINE)
1772
602
 
1773
- # If not in environment, try to fetch from server using fetch_modal_tokens
1774
- if not api_key:
1775
- try:
1776
- print("🔍 DEBUG: Trying to fetch API key from server...")
1777
- from fetch_modal_tokens import get_tokens
1778
- # Assuming OpenRouter key is the 5th token in the tuple
1779
- tokens = get_tokens()
1780
- if len(tokens) >= 5:
1781
- api_key = tokens[4]
1782
- except Exception as e:
1783
- print(f"⚠️ Error fetching OpenRouter API key from server: {e}")
603
+ if fix_match:
604
+ fix_command = fix_match.group(1).strip()
605
+ reason = reason_match.group(1).strip() if reason_match else "LLM suggested fix"
1784
606
 
1785
- # Then try credentials manager
1786
- if not api_key:
1787
- try:
1788
- from credentials_manager import CredentialsManager
1789
- credentials_manager = CredentialsManager()
1790
- api_key = credentials_manager.get_openrouter_api_key()
1791
- except Exception as e:
1792
- print(f"⚠️ Error getting OpenRouter API key from credentials manager: {e}")
1793
-
1794
- # Store the API key in a persistent file if found
1795
- if api_key:
1796
- try:
1797
- os.makedirs(os.path.expanduser("~/.gitarsenal"), exist_ok=True)
1798
- with open(os.path.expanduser("~/.gitarsenal/openrouter_key"), "w") as f:
1799
- f.write(api_key)
1800
- print("✅ Saved OpenRouter API key for future use")
1801
- except Exception as e:
1802
- print(f"⚠️ Could not save API key: {e}")
1803
-
1804
- # Try to load from saved file if not in environment
1805
- if not api_key:
1806
- try:
1807
- key_file = os.path.expanduser("~/.gitarsenal/openrouter_key")
1808
- print(f"🔍 DEBUG: Checking for saved API key at: {key_file}")
1809
- if os.path.exists(key_file):
1810
- with open(key_file, "r") as f:
1811
- api_key = f.read().strip()
1812
- if api_key:
1813
- print("✅ Loaded OpenRouter API key from saved file")
1814
- print(f"🔍 DEBUG: API key from file: {api_key}")
1815
- print(f"🔍 DEBUG: API key length: {len(api_key)}")
1816
- # Also set in environment for this session
1817
- os.environ["OPENROUTER_API_KEY"] = api_key
1818
- else:
1819
- print("🔍 DEBUG: Saved file exists but is empty")
1820
- else:
1821
- print("🔍 DEBUG: No saved API key file found")
1822
- except Exception as e:
1823
- print(f"⚠️ Could not load saved API key: {e}")
1824
-
1825
- if not api_key:
1826
- print("❌ No OpenRouter API key available for batch debugging")
1827
- return []
1828
-
1829
- # Prepare context for batch analysis
1830
- context_parts = []
1831
- context_parts.append(f"Current directory: {current_dir}")
1832
- context_parts.append(f"Sandbox available: {sandbox is not None}")
1833
-
1834
- # Add failed commands with their errors
1835
- for i, failed_cmd in enumerate(failed_commands, 1):
1836
- cmd_type = failed_cmd.get('type', 'main')
1837
- original_cmd = failed_cmd.get('original_command', '')
1838
- cmd_text = failed_cmd['command']
1839
- stderr = failed_cmd.get('stderr', '')
1840
- stdout = failed_cmd.get('stdout', '')
1841
-
1842
- context_parts.append(f"\n--- Failed Command {i} ({cmd_type}) ---")
1843
- context_parts.append(f"Command: {cmd_text}")
1844
- if original_cmd and original_cmd != cmd_text:
1845
- context_parts.append(f"Original Command: {original_cmd}")
1846
- if stderr:
1847
- context_parts.append(f"Error Output: {stderr}")
1848
- if stdout:
1849
- context_parts.append(f"Standard Output: {stdout}")
607
+ # Clean up the fix command
608
+ if fix_command.startswith('`') and fix_command.endswith('`'):
609
+ fix_command = fix_command[1:-1]
610
+
611
+ fixes.append({
612
+ 'original_command': failed_commands[i-1]['command'],
613
+ 'fix_command': fix_command,
614
+ 'reason': reason,
615
+ 'command_index': i-1
616
+ })
1850
617
 
1851
- # Create the prompt for batch analysis
1852
- prompt = f"""You are a debugging assistant analyzing multiple failed commands.
618
+ print(f"🔧 Generated {len(fixes)} fix commands from batch analysis")
619
+ return fixes
1853
620
 
1854
- Context:
1855
- {chr(10).join(context_parts)}
1856
621
 
1857
- Please analyze each failed command and provide a fix command for each one. For each failed command, respond with:
622
+ # Legacy function aliases for backward compatibility
623
+ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
624
+ """Legacy OpenAI-specific function - now routes to unified function"""
625
+ return call_llm_for_debug(command, error_output, api_key, current_dir, sandbox)
1858
626
 
1859
- FIX_COMMAND_{i}: <the fix command>
1860
- REASON_{i}: <brief explanation of why the original command failed and how the fix addresses it>
1861
627
 
1862
- Guidelines:
1863
- - For file not found errors, first search for the file using 'find . -name filename -type f'
1864
- - For missing packages, use appropriate package managers (pip, apt-get, npm)
1865
- - For Git SSH authentication failures, convert SSH URLs to HTTPS URLs
1866
- - For permission errors, suggest commands with sudo if appropriate
1867
- - For network issues, suggest retry commands or alternative URLs
1868
- - Keep each fix command simple and focused on the specific error
628
+ def call_anthropic_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
629
+ """Legacy Anthropic-specific function - now routes to unified function"""
630
+ return call_llm_for_debug(command, error_output, api_key, current_dir, sandbox)
1869
631
 
1870
- Provide fixes for all {len(failed_commands)} failed commands:"""
1871
632
 
1872
- # Set up headers for OpenRouter API
1873
- headers = {
1874
- "x-api-key": api_key,
1875
- "content-type": "application/json"
1876
- }
1877
-
1878
- payload = {
1879
- "model": "openrouter/openai-gpt-4o-mini", # Use a more capable model for batch analysis
1880
- "max_tokens": 1000,
1881
- "messages": [
1882
- {"role": "user", "content": prompt}
1883
- ]
1884
- }
1885
-
1886
- try:
1887
- print(f"🤖 Calling OpenRouter for batch debugging of {len(failed_commands)} commands...")
1888
- response = requests.post(
1889
- "https://openrouter.ai/api/v1/chat/completions", # OpenRouter API endpoint
1890
- headers=headers,
1891
- json=payload,
1892
- timeout=60
1893
- )
1894
-
1895
- if response.status_code == 200:
1896
- result = response.json()
1897
- content = result['choices'][0]['message']['content']
1898
- print(f"✅ Batch analysis completed")
1899
-
1900
- # Parse the response to extract fix commands
1901
- fixes = []
1902
- for i in range(1, len(failed_commands) + 1):
1903
- fix_pattern = f"FIX_COMMAND_{i}: (.+)"
1904
- reason_pattern = f"REASON_{i}: (.+)"
1905
-
1906
- fix_match = re.search(fix_pattern, content, re.MULTILINE)
1907
- reason_match = re.search(reason_pattern, content, re.MULTILINE)
1908
-
1909
- if fix_match:
1910
- fix_command = fix_match.group(1).strip()
1911
- reason = reason_match.group(1).strip() if reason_match else "OpenRouter suggested fix"
1912
-
1913
- # Clean up the fix command
1914
- if fix_command.startswith('`') and fix_command.endswith('`'):
1915
- fix_command = fix_command[1:-1]
1916
-
1917
- fixes.append({
1918
- 'original_command': failed_commands[i-1]['command'],
1919
- 'fix_command': fix_command,
1920
- 'reason': reason,
1921
- 'command_index': i-1
1922
- })
1923
-
1924
- print(f"🔧 Generated {len(fixes)} fix commands from batch analysis")
1925
- return fixes
1926
- else:
1927
- print(f"❌ OpenRouter API error: {response.status_code} - {response.text}")
1928
- return []
1929
-
1930
- except Exception as e:
1931
- print(f"❌ Error during batch debugging: {e}")
1932
- return []
633
+ def call_openrouter_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
634
+ """Legacy OpenRouter-specific function - now routes to unified function"""
635
+ return call_llm_for_debug(command, error_output, api_key, current_dir, sandbox)
636
+
637
+
638
+ def call_openai_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
639
+ """Legacy OpenAI batch function - now routes to unified function"""
640
+ return call_llm_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
641
+
642
+
643
+ def call_anthropic_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
644
+ """Legacy Anthropic batch function - now routes to unified function"""
645
+ return call_llm_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
646
+
647
+
648
+ def call_openrouter_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
649
+ """Legacy OpenRouter batch function - now routes to unified function"""
650
+ return call_llm_for_batch_debug(failed_commands, api_key, current_dir, sandbox)