gitarsenal-cli 1.9.26 → 1.9.27

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.
package/.venv_status.json CHANGED
@@ -1 +1 @@
1
- {"created":"2025-08-07T11:45:36.545Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
1
+ {"created":"2025-08-08T04:25:52.914Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.9.26",
3
+ "version": "1.9.27",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -255,7 +255,7 @@ class CommandListManager:
255
255
 
256
256
  return all_commands
257
257
 
258
- def analyze_failed_commands_with_llm(self, api_key=None, current_dir=None, sandbox=None):
258
+ def analyze_failed_commands_with_llm(self, api_key=None, current_dir=None, sandbox=None, use_web_search=False):
259
259
  """Analyze all failed commands using LLM and add suggested fixes."""
260
260
  failed_commands = self.get_failed_commands_for_llm()
261
261
 
@@ -266,7 +266,7 @@ class CommandListManager:
266
266
  print(f"šŸ” Analyzing {len(failed_commands)} failed commands with LLM...")
267
267
 
268
268
  # Use unified batch debugging for efficiency
269
- fixes = call_llm_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
269
+ fixes = call_llm_for_batch_debug(failed_commands, api_key, current_dir, sandbox, use_web_search)
270
270
 
271
271
  # Add the fixes to the command list
272
272
  added_fixes = []
@@ -296,18 +296,17 @@ class CommandListManager:
296
296
  tuple: (should_skip, reason)
297
297
  """
298
298
  try:
299
+ # Import required helpers once for this function scope
300
+ from llm_debugging import get_current_debug_model, get_api_key, make_api_request
301
+
299
302
  # Get API key if not provided
300
303
  if not api_key:
301
- api_key = os.environ.get("OPENAI_API_KEY")
302
- if not api_key:
303
- # Try to load from saved file
304
- key_file = os.path.expanduser("~/.gitarsenal/openai_key")
305
- if os.path.exists(key_file):
306
- with open(key_file, "r") as f:
307
- api_key = f.read().strip()
304
+ # Use the same API key retrieval logic as the debugging functions
305
+ current_model = get_current_debug_model()
306
+ api_key = get_api_key(current_model)
308
307
 
309
308
  if not api_key:
310
- print("āš ļø No OpenAI API key available for command list analysis")
309
+ print(f"āš ļø No {current_model} API key available for command list analysis")
311
310
  return False, "No API key available"
312
311
 
313
312
  # Get all commands for context
@@ -342,23 +341,15 @@ class CommandListManager:
342
341
  RUN: <reason>
343
342
  """
344
343
 
345
- # Call OpenAI API
346
- import openai
347
- client = openai.OpenAI(api_key=api_key)
344
+ current_model = get_current_debug_model()
348
345
 
349
- print("šŸ” Analyzing if original command should be skipped...")
346
+ print(f"šŸ” Analyzing if original command should be skipped using {current_model}...")
350
347
 
351
- response = client.chat.completions.create(
352
- model="gpt-3.5-turbo",
353
- messages=[
354
- {"role": "system", "content": "You are a helpful assistant that analyzes command execution."},
355
- {"role": "user", "content": prompt}
356
- ],
357
- max_tokens=100,
358
- temperature=0.3
359
- )
348
+ response_text = make_api_request(current_model, api_key, prompt)
360
349
 
361
- response_text = response.choices[0].message.content.strip()
350
+ if not response_text:
351
+ print(f"āš ļø Failed to get response from {current_model}")
352
+ return False, f"Failed to get response from {current_model}"
362
353
 
363
354
  # Parse the response
364
355
  if response_text.startswith("SKIP:"):
@@ -421,18 +412,16 @@ class CommandListManager:
421
412
  bool: True if the list was updated, False otherwise
422
413
  """
423
414
  try:
415
+ from llm_debugging import get_current_debug_model, get_api_key, make_api_request
424
416
  # Get API key if not provided
425
417
  if not api_key:
426
- api_key = os.environ.get("OPENAI_API_KEY")
427
- if not api_key:
428
- # Try to load from saved file
429
- key_file = os.path.expanduser("~/.gitarsenal/openai_key")
430
- if os.path.exists(key_file):
431
- with open(key_file, "r") as f:
432
- api_key = f.read().strip()
418
+ # Use the same API key retrieval logic as the debugging functions
419
+ from llm_debugging import get_current_debug_model, get_api_key
420
+ current_model = get_current_debug_model()
421
+ api_key = get_api_key(current_model)
433
422
 
434
423
  if not api_key:
435
- print("āš ļø No OpenAI API key available for command list analysis")
424
+ print(f"āš ļø No {current_model} API key available for command list analysis")
436
425
  return False
437
426
 
438
427
  # Get all commands for context
@@ -486,24 +475,18 @@ class CommandListManager:
486
475
  Only include commands that need changes (SKIP, MODIFY, ADD_AFTER), not KEEP actions.
487
476
  """
488
477
 
489
- # Call OpenAI API
490
- import openai
478
+ # Use the unified LLM API call
479
+ from llm_debugging import make_api_request
491
480
  import json
492
- client = openai.OpenAI(api_key=api_key)
493
-
494
- print("šŸ” Analyzing command list for optimizations...")
495
-
496
- response = client.chat.completions.create(
497
- model="gpt-4o-mini", # Use a more capable model for this complex task
498
- messages=[
499
- {"role": "system", "content": "You are a helpful assistant that analyzes and optimizes command lists."},
500
- {"role": "user", "content": prompt}
501
- ],
502
- max_tokens=1000,
503
- temperature=0.2
504
- )
481
+ current_model = get_current_debug_model()
505
482
 
506
- response_text = response.choices[0].message.content.strip()
483
+ print(f"šŸ” Analyzing command list for optimizations using {current_model}...")
484
+
485
+ response_text = make_api_request(current_model, api_key, prompt)
486
+
487
+ if not response_text:
488
+ print(f"āš ļø Failed to get response from {current_model}")
489
+ return False
507
490
 
508
491
  # Extract JSON from the response
509
492
  try:
@@ -145,7 +145,7 @@ class CredentialsManager:
145
145
  # First try to fetch from server using fetch_modal_tokens (GitArsenal's key)
146
146
  try:
147
147
  from fetch_modal_tokens import get_tokens
148
- _, _, api_key, _ = get_tokens()
148
+ _, _, api_key, _, _ = get_tokens()
149
149
  if api_key and validate_openai_key(api_key):
150
150
  # Set in environment for future use
151
151
  os.environ["OPENAI_API_KEY"] = api_key
@@ -240,6 +240,48 @@ class CredentialsManager:
240
240
 
241
241
  prompt = "An Anthropic API key is required.\nYou can get your API key from: https://console.anthropic.com/"
242
242
  return self.get_credential("anthropic_api_key", prompt, is_password=True, validate_func=validate_anthropic_key)
243
+
244
+ def get_groq_api_key(self):
245
+ """Get Groq API key with validation"""
246
+ def validate_groq_key(key):
247
+ # Groq keys are non-empty; basic length check
248
+ return bool(key) and len(key) > 20
249
+
250
+ # First check stored credentials
251
+ credentials = self.load_credentials()
252
+ if "groq_api_key" in credentials:
253
+ stored_key = credentials["groq_api_key"]
254
+ if validate_groq_key(stored_key):
255
+ return stored_key
256
+
257
+ # Then check environment variable
258
+ env_key = os.environ.get("GROQ_API_KEY")
259
+ if env_key and validate_groq_key(env_key):
260
+ return env_key
261
+
262
+ prompt = "A Groq API key is required for Groq models.\nYou can get your key from: https://console.groq.com/keys"
263
+ return self.get_credential("groq_api_key", prompt, is_password=True, validate_func=validate_groq_key)
264
+
265
+ def get_exa_api_key(self):
266
+ """Get Exa API key with validation"""
267
+ def validate_exa_key(key):
268
+ # Exa API keys are typically 32+ characters
269
+ return len(key) >= 32
270
+
271
+ # First check stored credentials
272
+ credentials = self.load_credentials()
273
+ if "exa_api_key" in credentials:
274
+ stored_key = credentials["exa_api_key"]
275
+ if validate_exa_key(stored_key):
276
+ return stored_key
277
+
278
+ # Then check environment variable
279
+ env_key = os.environ.get("EXA_API_KEY")
280
+ if env_key and validate_exa_key(env_key):
281
+ return env_key
282
+
283
+ prompt = "An Exa API key is required for web search functionality.\nYou can get your API key from: https://exa.ai/"
284
+ return self.get_credential("exa_api_key", prompt, is_password=True, validate_func=validate_exa_key)
243
285
 
244
286
  def clear_credential(self, key):
245
287
  """Remove a specific credential"""
@@ -285,7 +327,8 @@ class CredentialsManager:
285
327
  "WANDB_API_KEY",
286
328
  "MODAL_TOKEN_ID",
287
329
  "MODAL_TOKEN",
288
- "MODAL_TOKEN_SECRET"
330
+ "MODAL_TOKEN_SECRET",
331
+ "GROQ_API_KEY"
289
332
  ]
290
333
 
291
334
  for var in security_vars:
@@ -17,7 +17,7 @@ def fetch_default_tokens_from_gitarsenal():
17
17
  Fetch default Modal tokens and OpenAI API key from gitarsenal.dev API.
18
18
 
19
19
  Returns:
20
- tuple: (token_id, token_secret, openai_api_key) if successful, (None, None, None) otherwise
20
+ tuple: (token_id, token_secret, openai_api_key, anthropic_api_key, groq_api_key) if successful, (None, None, None, None, None) otherwise
21
21
  """
22
22
  endpoint = "https://gitarsenal.dev/api/credentials"
23
23
 
@@ -40,29 +40,30 @@ def fetch_default_tokens_from_gitarsenal():
40
40
  token_secret = data.get("modalTokenSecret")
41
41
  openai_api_key = data.get("openaiApiKey")
42
42
  anthropic_api_key = data.get("anthropicApiKey")
43
+ groq_api_key = data.get("groqApiKey")
43
44
 
44
45
  if token_id and token_secret:
45
46
  # print("āœ… Successfully fetched default tokens from gitarsenal.dev")
46
- return token_id, token_secret, openai_api_key, anthropic_api_key
47
+ return token_id, token_secret, openai_api_key, anthropic_api_key, groq_api_key
47
48
  else:
48
49
  print("āŒ Modal tokens not found in gitarsenal.dev response")
49
- return None, None, None, None
50
+ return None, None, None, None, None
50
51
  except json.JSONDecodeError:
51
52
  print("āŒ Invalid JSON response from gitarsenal.dev")
52
- return None, None, None, None
53
+ return None, None, None, None, None
53
54
  else:
54
55
  print(f"āŒ Failed to fetch from gitarsenal.dev: {response.status_code} - {response.text[:200]}")
55
- return None, None, None, None
56
+ return None, None, None, None, None
56
57
 
57
58
  except requests.exceptions.Timeout:
58
59
  print("āŒ Request timeout when fetching from gitarsenal.dev")
59
- return None, None, None, None
60
+ return None, None, None, None, None
60
61
  except requests.exceptions.ConnectionError:
61
62
  print("āŒ Connection failed to gitarsenal.dev")
62
- return None, None, None, None
63
+ return None, None, None, None, None
63
64
  except requests.exceptions.RequestException as e:
64
65
  print(f"āŒ Request failed to gitarsenal.dev: {e}")
65
- return None, None, None, None
66
+ return None, None, None, None, None
66
67
 
67
68
  def fetch_tokens_from_proxy(proxy_url=None, api_key=None):
68
69
  """
@@ -73,7 +74,7 @@ def fetch_tokens_from_proxy(proxy_url=None, api_key=None):
73
74
  api_key: API key for authentication
74
75
 
75
76
  Returns:
76
- tuple: (token_id, token_secret, openai_api_key) if successful, (None, None, None) otherwise
77
+ tuple: (token_id, token_secret, openai_api_key, anthropic_api_key, groq_api_key) if successful, (None, None, None, None, None) otherwise
77
78
  """
78
79
  # Use environment variables if not provided
79
80
  if not proxy_url:
@@ -90,12 +91,12 @@ def fetch_tokens_from_proxy(proxy_url=None, api_key=None):
90
91
  if not proxy_url:
91
92
  # print("āŒ No proxy URL provided or found in environment")
92
93
  print("šŸ’” Set MODAL_PROXY_URL environment variable or use --proxy-url argument")
93
- return None, None, None, None
94
+ return None, None, None, None, None
94
95
 
95
96
  if not api_key:
96
97
  print("āŒ No API key provided or found in environment")
97
98
  print("šŸ’” Set MODAL_PROXY_API_KEY environment variable or use --proxy-api-key argument")
98
- return None, None, None, None
99
+ return None, None, None, None, None
99
100
 
100
101
  # Ensure the URL ends with a slash
101
102
  if not proxy_url.endswith("/"):
@@ -119,48 +120,41 @@ def fetch_tokens_from_proxy(proxy_url=None, api_key=None):
119
120
  token_secret = data.get("token_secret")
120
121
  openai_api_key = data.get("openai_api_key")
121
122
  anthropic_api_key = data.get("anthropic_api_key")
122
-
123
+ groq_api_key = data.get("groq_api_key")
123
124
  if token_id and token_secret:
124
125
  print("āœ… Successfully fetched tokens from proxy server")
125
- return token_id, token_secret, openai_api_key, anthropic_api_key
126
+ return token_id, token_secret, openai_api_key, anthropic_api_key, groq_api_key
126
127
  else:
127
128
  print("āŒ Tokens not found in response")
128
- return None, None, None, None
129
+ return None, None, None, None, None
129
130
  else:
130
131
  print(f"āŒ Failed to fetch tokens: {response.status_code} - {response.text}")
131
- return None, None, None, None
132
+ return None, None, None, None, None
132
133
  except Exception as e:
133
134
  print(f"āŒ Error fetching tokens: {e}")
134
- return None, None, None, None
135
+ return None, None, None, None, None
135
136
 
136
137
  def get_tokens():
137
138
  """
138
- Get Modal tokens, OpenAI API key, and Anthropic API key, trying to fetch from the proxy server first.
139
+ Get Modal tokens, OpenAI API key, Anthropic API key, and Groq API key, trying to fetch from the proxy server first.
139
140
  Also sets the tokens in environment variables.
140
141
 
141
142
  Returns:
142
- tuple: (token_id, token_secret, openai_api_key, anthropic_api_key)
143
+ tuple: (token_id, token_secret, openai_api_key, anthropic_api_key, groq_api_key)
143
144
  """
144
145
  # Try to fetch from the proxy server
145
- token_id, token_secret, openai_api_key, anthropic_api_key = fetch_tokens_from_proxy()
146
+ token_id, token_secret, openai_api_key, anthropic_api_key, groq_api_key = fetch_tokens_from_proxy()
146
147
 
147
148
  # If we couldn't fetch from the server, try to get default tokens from gitarsenal.dev
148
149
  if not token_id or not token_secret:
149
150
  # print("āš ļø Proxy server failed, trying to fetch default tokens from gitarsenal.dev")
150
- token_id, token_secret, openai_api_key, anthropic_api_key = fetch_default_tokens_from_gitarsenal()
151
+ token_id, token_secret, openai_api_key, anthropic_api_key, groq_api_key = fetch_default_tokens_from_gitarsenal()
151
152
 
152
153
  # If we still don't have tokens, we can't proceed
153
154
  if not token_id or not token_secret:
154
155
  print("āŒ Failed to fetch tokens from both proxy server and gitarsenal.dev")
155
156
  print("šŸ’” Please check your network connection and API endpoints")
156
- return None, None, None, None
157
-
158
- # Debug print the full token values
159
- # print("\nšŸ” DEBUG: FULL TOKEN VALUES:")
160
- # print(f"šŸ” DEBUG: MODAL_TOKEN_ID: {token_id}")
161
- # print(f"šŸ” DEBUG: MODAL_TOKEN_SECRET: {token_secret}")
162
- # print(f"šŸ” DEBUG: OPENAI_API_KEY: {openai_api_key}")
163
- # print("šŸ” DEBUG: END OF TOKEN VALUES\n")
157
+ return None, None, None, None, None
164
158
 
165
159
  # Set the tokens in environment variables
166
160
  os.environ["MODAL_TOKEN_ID"] = token_id
@@ -175,7 +169,11 @@ def get_tokens():
175
169
  if anthropic_api_key:
176
170
  os.environ["ANTHROPIC_API_KEY"] = anthropic_api_key
177
171
 
178
- return token_id, token_secret, openai_api_key, anthropic_api_key
172
+ # Set Groq API key if available
173
+ if groq_api_key:
174
+ os.environ["GROQ_API_KEY"] = groq_api_key
175
+
176
+ return token_id, token_secret, openai_api_key, anthropic_api_key, groq_api_key
179
177
 
180
178
  if __name__ == "__main__":
181
179
  # Parse command-line arguments if run directly
@@ -196,11 +194,12 @@ if __name__ == "__main__":
196
194
  print(f"āœ… Set MODAL_PROXY_API_KEY from command line")
197
195
 
198
196
  # Get tokens
199
- token_id, token_secret, openai_api_key, anthropic_api_key = get_tokens()
197
+ token_id, token_secret, openai_api_key, anthropic_api_key, groq_api_key = get_tokens()
200
198
  print(f"Token ID: {token_id}")
201
199
  print(f"Token Secret: {token_secret}")
202
200
  print(f"OpenAI API Key: {openai_api_key[:5] + '...' if openai_api_key else None}")
203
201
  print(f"Anthropic API Key: {anthropic_api_key[:5] + '...' if anthropic_api_key else None}")
202
+ print(f"Groq API Key: {groq_api_key[:5] + '...' if groq_api_key else None}")
204
203
 
205
204
  # Check if tokens are set in environment variables
206
205
  print(f"\nšŸ” DEBUG: Checking environment variables")
@@ -208,6 +207,7 @@ if __name__ == "__main__":
208
207
  print(f"šŸ” MODAL_TOKEN_SECRET exists: {'Yes' if os.environ.get('MODAL_TOKEN_SECRET') else 'No'}")
209
208
  print(f"šŸ” OPENAI_API_KEY exists: {'Yes' if os.environ.get('OPENAI_API_KEY') else 'No'}")
210
209
  print(f"šŸ” ANTHROPIC_API_KEY exists: {'Yes' if os.environ.get('ANTHROPIC_API_KEY') else 'No'}")
210
+ print(f"šŸ” GROQ_API_KEY exists: {'Yes' if os.environ.get('GROQ_API_KEY') else 'No'}")
211
211
  if os.environ.get('MODAL_TOKEN_ID'):
212
212
  print(f"šŸ” MODAL_TOKEN_ID length: {len(os.environ.get('MODAL_TOKEN_ID'))}")
213
213
  if os.environ.get('MODAL_TOKEN_SECRET'):
@@ -216,6 +216,8 @@ if __name__ == "__main__":
216
216
  print(f"šŸ” OPENAI_API_KEY length: {len(os.environ.get('OPENAI_API_KEY'))}")
217
217
  if os.environ.get('ANTHROPIC_API_KEY'):
218
218
  print(f"šŸ” ANTHROPIC_API_KEY length: {len(os.environ.get('ANTHROPIC_API_KEY'))}")
219
+ if os.environ.get('GROQ_API_KEY'):
220
+ print(f"šŸ” GROQ_API_KEY length: {len(os.environ.get('GROQ_API_KEY'))}")
219
221
 
220
222
  # Write the tokens to a file for use by other scripts
221
223
  tokens_file = Path(__file__).parent / "modal_tokens.json"
@@ -224,7 +226,8 @@ if __name__ == "__main__":
224
226
  "token_id": token_id,
225
227
  "token_secret": token_secret,
226
228
  "openai_api_key": openai_api_key,
227
- "anthropic_api_key": anthropic_api_key
229
+ "anthropic_api_key": anthropic_api_key,
230
+ "groq_api_key": groq_api_key
228
231
  }, f)
229
232
  print(f"\nāœ… Tokens written to {tokens_file}")
230
233
 
@@ -279,6 +282,17 @@ if __name__ == "__main__":
279
282
  with open(env_file, 'w') as f:
280
283
  f.write(env_content)
281
284
  print(f"āœ… Updated Anthropic API key in {env_file}")
285
+
286
+ # Update or add GROQ_API_KEY
287
+ if groq_api_key:
288
+ if "GROQ_API_KEY" in env_content:
289
+ import re
290
+ env_content = re.sub(r'GROQ_API_KEY=.*\n', f'GROQ_API_KEY={groq_api_key}\n', env_content)
291
+ else:
292
+ env_content += f'\nGROQ_API_KEY={groq_api_key}\n'
293
+ with open(env_file, 'w') as f:
294
+ f.write(env_content)
295
+ print(f"āœ… Updated Groq API key in {env_file}")
282
296
 
283
297
  # Try to use the Modal CLI to set the token
284
298
  try:
@@ -42,7 +42,7 @@ except Exception as e:
42
42
  try:
43
43
  # First, try to import the fetch_modal_tokens module
44
44
  from fetch_modal_tokens import get_tokens
45
- TOKEN_ID, TOKEN_SECRET, _, _ = get_tokens()
45
+ TOKEN_ID, TOKEN_SECRET, _, _, _ = get_tokens()
46
46
 
47
47
  # Check if we got valid tokens
48
48
  if TOKEN_ID is None or TOKEN_SECRET is None:
@@ -55,19 +55,22 @@ def get_api_key(provider):
55
55
  env_var_map = {
56
56
  "openai": "OPENAI_API_KEY",
57
57
  "anthropic": "ANTHROPIC_API_KEY",
58
- "openrouter": "OPENROUTER_API_KEY"
58
+ "openrouter": "OPENROUTER_API_KEY",
59
+ "groq": "GROQ_API_KEY"
59
60
  }
60
61
 
61
62
  key_file_map = {
62
63
  "openai": "openai_key",
63
64
  "anthropic": "anthropic_key",
64
- "openrouter": "openrouter_key"
65
+ "openrouter": "openrouter_key",
66
+ "groq": "groq_key"
65
67
  }
66
68
 
67
69
  token_index_map = {
68
70
  "openai": 2,
69
71
  "anthropic": 3,
70
- "openrouter": 4
72
+ "openrouter": 4,
73
+ "groq": 5,
71
74
  }
72
75
 
73
76
  env_var = env_var_map.get(provider)
@@ -124,7 +127,8 @@ def save_api_key(provider, api_key):
124
127
  key_file_map = {
125
128
  "openai": "openai_key",
126
129
  "anthropic": "anthropic_key",
127
- "openrouter": "openrouter_key"
130
+ "openrouter": "openrouter_key",
131
+ "groq": "groq_key"
128
132
  }
129
133
 
130
134
  try:
@@ -329,6 +333,8 @@ def make_api_request(provider, api_key, prompt, retries=2):
329
333
  return make_anthropic_request(api_key, prompt, retries)
330
334
  elif provider == "openrouter":
331
335
  return make_openrouter_request(api_key, prompt, retries)
336
+ elif provider == "groq":
337
+ return make_groq_request(api_key, prompt, retries)
332
338
  else:
333
339
  return None
334
340
 
@@ -341,7 +347,7 @@ def make_openai_request(api_key, prompt, retries=2):
341
347
  }
342
348
 
343
349
  payload = {
344
- "model": os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
350
+ "model": os.environ.get("OPENAI_MODEL", "gpt-5-mini"),
345
351
  "messages": [
346
352
  {"role": "system", "content": "You are a debugging assistant. Provide only the terminal command to fix the issue."},
347
353
  {"role": "user", "content": prompt}
@@ -438,7 +444,7 @@ def make_openrouter_request(api_key, prompt, retries=2):
438
444
  }
439
445
 
440
446
  payload = {
441
- "model": "openai/gpt-4o-mini",
447
+ "model": "openai/gpt-5-mini",
442
448
  "max_tokens": 300,
443
449
  "messages": [{"role": "user", "content": prompt}]
444
450
  }
@@ -476,7 +482,59 @@ def make_openrouter_request(api_key, prompt, retries=2):
476
482
  return None
477
483
 
478
484
 
479
- def call_llm_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
485
+ def make_groq_request(api_key, prompt, retries=2):
486
+ """Make request to Groq API (OpenAI-compatible endpoint)"""
487
+ headers = {
488
+ "Content-Type": "application/json",
489
+ "Authorization": f"Bearer {api_key}"
490
+ }
491
+
492
+ payload = {
493
+ "model": os.environ.get("GROQ_MODEL", "openai/gpt-oss-20b"),
494
+ "messages": [
495
+ {"role": "system", "content": "You are a debugging assistant. Provide only the terminal command to fix the issue."},
496
+ {"role": "user", "content": prompt}
497
+ ],
498
+ "temperature": 0.2,
499
+ "max_tokens": 300
500
+ }
501
+
502
+ endpoint = os.environ.get("GROQ_BASE_URL", "https://api.groq.com/openai/v1/chat/completions")
503
+
504
+ for attempt in range(retries + 1):
505
+ try:
506
+ if attempt > 0:
507
+ time.sleep(1.5 * (2 ** (attempt - 1)))
508
+
509
+ response = requests.post(
510
+ endpoint,
511
+ headers=headers,
512
+ json=payload,
513
+ timeout=45
514
+ )
515
+
516
+ if response.status_code == 200:
517
+ result = response.json()
518
+ return result["choices"][0]["message"]["content"]
519
+ elif response.status_code == 401:
520
+ print("āŒ Invalid Groq API key")
521
+ return None
522
+ elif response.status_code in [429, 500]:
523
+ continue # Retry
524
+ else:
525
+ print(f"āš ļø Groq API error: {response.status_code}")
526
+ return None
527
+
528
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
529
+ continue # Retry
530
+ except Exception as e:
531
+ print(f"āš ļø Groq request error: {e}")
532
+ return None
533
+
534
+ return None
535
+
536
+
537
+ def call_llm_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None, use_web_search=False):
480
538
  """Unified function to call LLM for debugging"""
481
539
  # Skip debugging for test commands
482
540
  if command.strip().startswith("test "):
@@ -531,7 +589,7 @@ def call_llm_for_debug(command, error_output, api_key=None, current_dir=None, sa
531
589
  return fix_command
532
590
 
533
591
 
534
- def call_llm_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
592
+ def call_llm_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None, use_web_search=False):
535
593
  """Call LLM for batch debugging of multiple failed commands"""
536
594
  if not failed_commands:
537
595
  return []
@@ -584,7 +642,7 @@ Guidelines:
584
642
  - Keep each fix command simple and focused on the specific error
585
643
 
586
644
  Provide fixes for all {len(failed_commands)} failed commands:"""
587
-
645
+
588
646
  print(f"šŸ¤– Calling {current_model} for batch debugging of {len(failed_commands)} commands...")
589
647
  response_text = make_api_request(current_model, api_key, prompt)
590
648
 
@@ -647,4 +705,14 @@ def call_anthropic_for_batch_debug(failed_commands, api_key=None, current_dir=No
647
705
 
648
706
  def call_openrouter_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
649
707
  """Legacy OpenRouter batch function - now routes to unified function"""
708
+ return call_llm_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
709
+
710
+
711
+ def call_groq_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
712
+ """Legacy Groq-specific function - now routes to unified function"""
713
+ return call_llm_for_debug(command, error_output, api_key, current_dir, sandbox)
714
+
715
+
716
+ def call_groq_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
717
+ """Legacy Groq batch function - now routes to unified function"""
650
718
  return call_llm_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
@@ -408,7 +408,10 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
408
408
  fix_command = call_llm_for_debug(cmd_text, stderr, current_dir=current_dir, sandbox=shell)
409
409
 
410
410
  if fix_command:
411
- print(f"šŸ”§ OpenAI suggested fix command: {fix_command}")
411
+ # Get the current debug model to show the correct provider name
412
+ from llm_debugging import get_current_debug_model
413
+ current_model = get_current_debug_model()
414
+ print(f"šŸ”§ {current_model.capitalize()} suggested fix command: {fix_command}")
412
415
 
413
416
  # Add the fix to the command list manager
414
417
  fix_index = cmd_manager.add_suggested_fix(cmd_text, fix_command, "LLM suggested fix")
@@ -416,7 +419,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
416
419
  # Execute the fix command
417
420
  print(f"šŸ”„ Running suggested fix command: {fix_command}")
418
421
  fix_start_time = time.time()
419
- fix_success, fix_stdout, fix_stderr = shell.execute(fix_command, timeout=300)
422
+ fix_success, fix_stdout, fix_stderr = shell.execute(fix_command, timeout=120)
420
423
  fix_execution_time = time.time() - fix_start_time
421
424
 
422
425
  # Mark fix command as executed
@@ -472,9 +475,98 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
472
475
  print(f"āš ļø Original command still failed after fix, continuing...")
473
476
  else:
474
477
  print(f"āŒ Fix command failed: {fix_stderr}")
475
- print(f"āš ļø Continuing with remaining commands...")
478
+ print(f"🌐 First fix attempt failed, trying with web search...")
479
+
480
+ # Retry with web search enabled
481
+ try:
482
+ retry_fix_command = call_llm_for_debug(
483
+ cmd_text, stderr,
484
+ current_dir=current_dir,
485
+ sandbox=shell,
486
+ use_web_search=True
487
+ )
488
+
489
+ if retry_fix_command:
490
+ # Get the current debug model to show the correct provider name
491
+ from llm_debugging import get_current_debug_model
492
+ current_model = get_current_debug_model()
493
+ print(f"šŸ”§ {current_model.capitalize()} suggested web-enhanced fix: {retry_fix_command}")
494
+
495
+ # Add the web-enhanced fix to the command list manager
496
+ retry_fix_index = cmd_manager.add_suggested_fix(cmd_text, retry_fix_command, "Web-enhanced LLM fix")
497
+
498
+ # Execute the web-enhanced fix command
499
+ print(f"šŸ”„ Running web-enhanced fix command: {retry_fix_command}")
500
+ retry_fix_start_time = time.time()
501
+ retry_fix_success, retry_fix_stdout, retry_fix_stderr = shell.execute(retry_fix_command, timeout=120)
502
+ retry_fix_execution_time = time.time() - retry_fix_start_time
503
+
504
+ # Mark web-enhanced fix command as executed
505
+ cmd_manager.mark_command_executed(
506
+ retry_fix_index, 'fix', retry_fix_success, retry_fix_stdout, retry_fix_stderr, retry_fix_execution_time
507
+ )
508
+
509
+ if retry_fix_success:
510
+ print(f"āœ… Web-enhanced fix command succeeded!")
511
+
512
+ # Check if we should skip the original command
513
+ api_key = os.environ.get("OPENAI_API_KEY")
514
+ should_skip, skip_reason = cmd_manager.should_skip_original_command(
515
+ cmd_text, retry_fix_command, retry_fix_stdout, retry_fix_stderr, api_key
516
+ )
517
+
518
+ if should_skip:
519
+ print(f"šŸ”„ Skipping original command: {skip_reason}")
520
+
521
+ # Mark the original command as successful without running it
522
+ cmd_manager.mark_command_executed(
523
+ cmd_index, 'main', True,
524
+ f"Command skipped after successful web-enhanced fix: {skip_reason}",
525
+ "", time.time() - start_time
526
+ )
527
+
528
+ print(f"āœ… Original command marked as successful (skipped)")
529
+
530
+ # After a successful web-enhanced fix and skipping the original command,
531
+ # analyze and update the entire command list
532
+ print("\nšŸ” Analyzing and updating remaining commands based on web-enhanced fix results...")
533
+ cmd_manager.update_command_list_with_llm(api_key)
534
+ else:
535
+ # Retry the original command
536
+ print(f"šŸ”„ Retrying original command: {cmd_text}")
537
+ retry_start_time = time.time()
538
+ retry_success, retry_stdout, retry_stderr = shell.execute(cmd_text, timeout=300)
539
+ retry_execution_time = time.time() - retry_start_time
540
+
541
+ # Update the original command status
542
+ cmd_manager.mark_command_executed(
543
+ cmd_index, 'main', retry_success, retry_stdout, retry_stderr, retry_execution_time
544
+ )
545
+
546
+ if retry_success:
547
+ print(f"āœ… Original command succeeded after web-enhanced fix!")
548
+
549
+ # After a successful web-enhanced fix and successful retry,
550
+ # analyze and update the entire command list
551
+ print("\nšŸ” Analyzing and updating remaining commands based on web-enhanced fix results...")
552
+ cmd_manager.update_command_list_with_llm(api_key)
553
+ else:
554
+ print(f"āš ļø Original command still failed after web-enhanced fix, continuing...")
555
+ else:
556
+ print(f"āŒ Web-enhanced fix command also failed: {retry_fix_stderr}")
557
+ print(f"āš ļø Continuing with remaining commands...")
558
+ else:
559
+ print(f"āŒ No web-enhanced fix suggested")
560
+ print(f"āš ļø Continuing with remaining commands...")
561
+
562
+ except Exception as web_debug_e:
563
+ print(f"āŒ Web-enhanced debugging failed: {web_debug_e}")
564
+ print(f"āš ļø Continuing with remaining commands...")
476
565
  else:
477
- print("āŒ No fix suggested by OpenAI")
566
+ # Get the current debug model to show the correct provider name
567
+ from llm_debugging import get_current_debug_model
568
+ current_model = get_current_debug_model()
569
+ print(f"āŒ No fix suggested by {current_model.capitalize()}")
478
570
  print(f"āš ļø Continuing with remaining commands...")
479
571
 
480
572
  except Exception as debug_e:
@@ -516,10 +608,14 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
516
608
  if failed_commands:
517
609
  print(f"\nšŸ” Final batch analysis of {len(failed_commands)} failed commands...")
518
610
  current_dir = shell.get_cwd()
519
- api_key = os.environ.get("OPENAI_API_KEY")
520
611
 
521
- # Use batch analysis to get additional fixes
522
- additional_fixes = cmd_manager.analyze_failed_commands_with_llm(api_key, current_dir, shell)
612
+ # Get the correct API key for the current debug model
613
+ from llm_debugging import get_current_debug_model, get_api_key
614
+ current_model = get_current_debug_model()
615
+ api_key = get_api_key(current_model)
616
+
617
+ # Use batch analysis to get additional fixes with web search enabled
618
+ additional_fixes = cmd_manager.analyze_failed_commands_with_llm(api_key, current_dir, shell, use_web_search=True)
523
619
 
524
620
  if additional_fixes:
525
621
  print(f"šŸ”§ Executing {len(additional_fixes)} additional fix commands...")
@@ -6,4 +6,5 @@ flask
6
6
  flask-cors
7
7
  pexpect
8
8
  anthropic
9
- gitingest
9
+ gitingest
10
+ exa-py
package/python/setup.py CHANGED
@@ -9,7 +9,8 @@ setup(
9
9
  "modal",
10
10
  "requests",
11
11
  "openai",
12
- "anthropic"
12
+ "anthropic",
13
+ "exa-py"
13
14
  ],
14
15
  python_requires=">=3.8",
15
16
  )
@@ -45,7 +45,7 @@ if args.proxy_api_key:
45
45
  # Import the fetch_modal_tokens module
46
46
  # print("šŸ”„ Fetching tokens from proxy server...")
47
47
  from fetch_modal_tokens import get_tokens
48
- token_id, token_secret, openai_api_key, _ = get_tokens()
48
+ token_id, token_secret, openai_api_key, anthropic_api_key, groq_api_key = get_tokens()
49
49
 
50
50
  # Check if we got valid tokens
51
51
  if token_id is None or token_secret is None:
@@ -156,61 +156,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
156
156
  print("āœ… Found token in environment variable")
157
157
  os.environ["MODAL_TOKEN_ID"] = modal_token
158
158
  modal_token_id = modal_token
159
- print(f"āœ… Set token (length: {len(modal_token)})")
160
-
161
- if modal_token_id:
162
- print(f"āœ… token found (length: {len(modal_token_id)})")
163
-
164
- # Use the comprehensive fix_modal_token script
165
- try:
166
- # Execute the fix_modal_token.py script
167
- import subprocess
168
- print(f"šŸ”„ Running fix_modal_token.py to set up Modal token...")
169
- result = subprocess.run(
170
- ["python", os.path.join(os.path.dirname(__file__), "fix_modal_token.py")],
171
- capture_output=True,
172
- text=True
173
- )
174
-
175
- # Print the output
176
- print(result.stdout)
177
-
178
- if result.returncode != 0:
179
- print(f"āš ļø Warning: fix_modal_token.py exited with code {result.returncode}")
180
- if result.stderr:
181
- print(f"Error: {result.stderr}")
182
-
183
- print(f"āœ… token setup completed")
184
- except Exception as e:
185
- print(f"āš ļø Error running fix_modal_token.py: {e}")
186
- else:
187
- print("āŒ No token found in environment variables")
188
- # Try to get from file as a last resort
189
- try:
190
- home_dir = os.path.expanduser("~")
191
- modal_dir = os.path.join(home_dir, ".modal")
192
- token_file = os.path.join(modal_dir, "token.json")
193
- if os.path.exists(token_file):
194
- print(f"šŸ” Found Modal token file at {token_file}")
195
- with open(token_file, 'r') as f:
196
- import json
197
- token_data = json.load(f)
198
- if "token_id" in token_data:
199
- modal_token_id = token_data["token_id"]
200
- os.environ["MODAL_TOKEN_ID"] = modal_token_id
201
- os.environ["MODAL_TOKEN"] = modal_token_id
202
- print(f"āœ… Loaded token from file (length: {len(modal_token_id)})")
203
- else:
204
- print("āŒ Token file does not contain token_id")
205
- else:
206
- print("āŒ token file not found")
207
- except Exception as e:
208
- print(f"āŒ Error loading token from file: {e}")
209
-
210
- if not os.environ.get("MODAL_TOKEN_ID"):
211
- print("āŒ Could not find Modal token in any location")
212
- return None
213
-
159
+ print(f"āœ… Set token (length: {len(modal_token)})")
214
160
  except Exception as e:
215
161
  print(f"āš ļø Error checking Modal token: {e}")
216
162
  # Try to use the token from environment
@@ -311,7 +257,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
311
257
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
312
258
  "gpg", "ca-certificates", "software-properties-common"
313
259
  )
314
- .uv_pip_install("uv", "modal", "requests", "openai", "anthropic") # Remove problematic CUDA packages
260
+ .uv_pip_install("uv", "modal", "requests", "openai", "anthropic", "exa-py") # Remove problematic CUDA packages
315
261
  .run_commands(
316
262
  # Create SSH directory
317
263
  "mkdir -p /var/run/sshd",
@@ -527,7 +473,10 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
527
473
  fix_command = call_llm_for_debug(cmd_text, stderr, current_dir=current_dir, sandbox=shell)
528
474
 
529
475
  if fix_command:
530
- print(f"šŸ”§ OpenAI suggested fix command: {fix_command}")
476
+ # Get the current debug model to show the correct provider name
477
+ from llm_debugging import get_current_debug_model
478
+ current_model = get_current_debug_model()
479
+ print(f"šŸ”§ {current_model.capitalize()} suggested fix command: {fix_command}")
531
480
 
532
481
  # Add the fix to the command list manager
533
482
  fix_index = cmd_manager.add_suggested_fix(cmd_text, fix_command, "LLM suggested fix")
@@ -535,7 +484,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
535
484
  # Execute the fix command
536
485
  print(f"šŸ”„ Running suggested fix command: {fix_command}")
537
486
  fix_start_time = time.time()
538
- fix_success, fix_stdout, fix_stderr = shell.execute(fix_command, timeout=300)
487
+ fix_success, fix_stdout, fix_stderr = shell.execute(fix_command, timeout=120)
539
488
  fix_execution_time = time.time() - fix_start_time
540
489
 
541
490
  # Mark fix command as executed
@@ -591,9 +540,98 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
591
540
  print(f"āš ļø Original command still failed after fix, continuing...")
592
541
  else:
593
542
  print(f"āŒ Fix command failed: {fix_stderr}")
594
- print(f"āš ļø Continuing with remaining commands...")
543
+ print(f"🌐 First fix attempt failed, trying with web search...")
544
+
545
+ # Retry with web search enabled
546
+ try:
547
+ retry_fix_command = call_llm_for_debug(
548
+ cmd_text, stderr,
549
+ current_dir=current_dir,
550
+ sandbox=shell,
551
+ use_web_search=True
552
+ )
553
+
554
+ if retry_fix_command:
555
+ # Get the current debug model to show the correct provider name
556
+ from llm_debugging import get_current_debug_model
557
+ current_model = get_current_debug_model()
558
+ print(f"šŸ”§ {current_model.capitalize()} suggested web-enhanced fix: {retry_fix_command}")
559
+
560
+ # Add the web-enhanced fix to the command list manager
561
+ retry_fix_index = cmd_manager.add_suggested_fix(cmd_text, retry_fix_command, "Web-enhanced LLM fix")
562
+
563
+ # Execute the web-enhanced fix command
564
+ print(f"šŸ”„ Running web-enhanced fix command: {retry_fix_command}")
565
+ retry_fix_start_time = time.time()
566
+ retry_fix_success, retry_fix_stdout, retry_fix_stderr = shell.execute(retry_fix_command, timeout=120)
567
+ retry_fix_execution_time = time.time() - retry_fix_start_time
568
+
569
+ # Mark web-enhanced fix command as executed
570
+ cmd_manager.mark_command_executed(
571
+ retry_fix_index, 'fix', retry_fix_success, retry_fix_stdout, retry_fix_stderr, retry_fix_execution_time
572
+ )
573
+
574
+ if retry_fix_success:
575
+ print(f"āœ… Web-enhanced fix command succeeded!")
576
+
577
+ # Check if we should skip the original command
578
+ api_key = os.environ.get("OPENAI_API_KEY")
579
+ should_skip, skip_reason = cmd_manager.should_skip_original_command(
580
+ cmd_text, retry_fix_command, retry_fix_stdout, retry_fix_stderr, api_key
581
+ )
582
+
583
+ if should_skip:
584
+ print(f"šŸ”„ Skipping original command: {skip_reason}")
585
+
586
+ # Mark the original command as successful without running it
587
+ cmd_manager.mark_command_executed(
588
+ cmd_index, 'main', True,
589
+ f"Command skipped after successful web-enhanced fix: {skip_reason}",
590
+ "", time.time() - start_time
591
+ )
592
+
593
+ print(f"āœ… Original command marked as successful (skipped)")
594
+
595
+ # After a successful web-enhanced fix and skipping the original command,
596
+ # analyze and update the entire command list
597
+ print("\nšŸ” Analyzing and updating remaining commands based on web-enhanced fix results...")
598
+ cmd_manager.update_command_list_with_llm(api_key)
599
+ else:
600
+ # Retry the original command
601
+ print(f"šŸ”„ Retrying original command: {cmd_text}")
602
+ retry_start_time = time.time()
603
+ retry_success, retry_stdout, retry_stderr = shell.execute(cmd_text, timeout=300)
604
+ retry_execution_time = time.time() - retry_start_time
605
+
606
+ # Update the original command status
607
+ cmd_manager.mark_command_executed(
608
+ cmd_index, 'main', retry_success, retry_stdout, retry_stderr, retry_execution_time
609
+ )
610
+
611
+ if retry_success:
612
+ print(f"āœ… Original command succeeded after web-enhanced fix!")
613
+
614
+ # After a successful web-enhanced fix and successful retry,
615
+ # analyze and update the entire command list
616
+ print("\nšŸ” Analyzing and updating remaining commands based on web-enhanced fix results...")
617
+ cmd_manager.update_command_list_with_llm(api_key)
618
+ else:
619
+ print(f"āš ļø Original command still failed after web-enhanced fix, continuing...")
620
+ else:
621
+ print(f"āŒ Web-enhanced fix command also failed: {retry_fix_stderr}")
622
+ print(f"āš ļø Continuing with remaining commands...")
623
+ else:
624
+ print(f"āŒ No web-enhanced fix suggested")
625
+ print(f"āš ļø Continuing with remaining commands...")
626
+
627
+ except Exception as web_debug_e:
628
+ print(f"āŒ Web-enhanced debugging failed: {web_debug_e}")
629
+ print(f"āš ļø Continuing with remaining commands...")
595
630
  else:
596
- print("āŒ No fix suggested by OpenAI")
631
+ # Get the current debug model to show the correct provider name
632
+ from llm_debugging import get_current_debug_model
633
+ current_model = get_current_debug_model()
634
+ print(f"āŒ No fix suggested by {current_model.capitalize()}")
597
635
  print(f"āš ļø Continuing with remaining commands...")
598
636
 
599
637
  except Exception as debug_e:
@@ -635,10 +673,14 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
635
673
  if failed_commands:
636
674
  print(f"\nšŸ” Final batch analysis of {len(failed_commands)} failed commands...")
637
675
  current_dir = shell.get_cwd()
638
- api_key = os.environ.get("OPENAI_API_KEY")
639
676
 
640
- # Use batch analysis to get additional fixes
641
- additional_fixes = cmd_manager.analyze_failed_commands_with_llm(api_key, current_dir, shell)
677
+ # Get the correct API key for the current debug model
678
+ from llm_debugging import get_current_debug_model, get_api_key
679
+ current_model = get_current_debug_model()
680
+ api_key = get_api_key(current_model)
681
+
682
+ # Use batch analysis to get additional fixes with web search enabled
683
+ additional_fixes = cmd_manager.analyze_failed_commands_with_llm(api_key, current_dir, shell, use_web_search=True)
642
684
 
643
685
  if additional_fixes:
644
686
  print(f"šŸ”§ Executing {len(additional_fixes)} additional fix commands...")