gitarsenal-cli 1.9.21 โ†’ 1.9.24

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.
Files changed (34) hide show
  1. package/.venv_status.json +1 -1
  2. package/package.json +1 -1
  3. package/python/__pycache__/auth_manager.cpython-313.pyc +0 -0
  4. package/python/__pycache__/command_manager.cpython-313.pyc +0 -0
  5. package/python/__pycache__/fetch_modal_tokens.cpython-313.pyc +0 -0
  6. package/python/__pycache__/llm_debugging.cpython-313.pyc +0 -0
  7. package/python/__pycache__/modal_container.cpython-313.pyc +0 -0
  8. package/python/__pycache__/shell.cpython-313.pyc +0 -0
  9. package/python/api_integration.py +0 -0
  10. package/python/command_manager.py +613 -0
  11. package/python/credentials_manager.py +0 -0
  12. package/python/fetch_modal_tokens.py +0 -0
  13. package/python/fix_modal_token.py +0 -0
  14. package/python/fix_modal_token_advanced.py +0 -0
  15. package/python/gitarsenal.py +0 -0
  16. package/python/gitarsenal_proxy_client.py +0 -0
  17. package/python/llm_debugging.py +1369 -0
  18. package/python/modal_container.py +626 -0
  19. package/python/setup.py +15 -0
  20. package/python/setup_modal_token.py +0 -39
  21. package/python/shell.py +627 -0
  22. package/python/test_modalSandboxScript.py +75 -2639
  23. package/scripts/postinstall.js +22 -23
  24. package/python/__pycache__/credentials_manager.cpython-313.pyc +0 -0
  25. package/python/__pycache__/test_modalSandboxScript.cpython-313.pyc +0 -0
  26. package/python/__pycache__/test_modalSandboxScript_stable.cpython-313.pyc +0 -0
  27. package/python/debug_delete.py +0 -167
  28. package/python/documentation.py +0 -76
  29. package/python/fix_setup_commands.py +0 -116
  30. package/python/modal_auth_patch.py +0 -178
  31. package/python/modal_proxy_service.py +0 -665
  32. package/python/modal_token_solution.py +0 -293
  33. package/python/test_dynamic_commands.py +0 -147
  34. package/test_modalSandboxScript.py +0 -5004
@@ -42,2653 +42,51 @@ if args.proxy_url:
42
42
  if args.proxy_api_key:
43
43
  os.environ["MODAL_PROXY_API_KEY"] = args.proxy_api_key
44
44
 
45
- class PersistentShell:
46
- """A persistent bash shell using subprocess.Popen for executing commands with state persistence."""
47
-
48
- def __init__(self, working_dir="/root", timeout=60):
49
- self.working_dir = working_dir
50
- self.timeout = timeout
51
- self.process = None
52
- self.stdout_lines = [] # Use list instead of queue
53
- self.stderr_lines = [] # Use list instead of queue
54
- self.stdout_lock = threading.Lock()
55
- self.stderr_lock = threading.Lock()
56
- self.stdout_thread = None
57
- self.stderr_thread = None
58
- self.command_counter = 0
59
- self.is_running = False
60
- self.virtual_env_path = None # Track activated virtual environment
61
- self.suggested_alternative = None # Store suggested alternative commands
62
- self.should_remove_command = False # Flag to indicate if a command should be removed
63
- self.removal_reason = None # Reason for removing a command
64
-
65
- def start(self):
66
- """Start the persistent bash shell."""
67
- if self.is_running:
68
- return
69
-
70
- print(f"๐Ÿš Starting persistent bash shell in {self.working_dir}")
71
-
72
- # Start bash with unbuffered output
73
- self.process = subprocess.Popen(
74
- ['bash', '-i'], # Interactive bash
75
- stdin=subprocess.PIPE,
76
- stdout=subprocess.PIPE,
77
- stderr=subprocess.PIPE,
78
- text=True,
79
- bufsize=0, # Unbuffered
80
- cwd=self.working_dir,
81
- preexec_fn=os.setsid # Create new process group
82
- )
83
-
84
- # Start threads to read stdout and stderr
85
- self.stdout_thread = threading.Thread(target=self._read_stdout, daemon=True)
86
- self.stderr_thread = threading.Thread(target=self._read_stderr, daemon=True)
87
-
88
- self.stdout_thread.start()
89
- self.stderr_thread.start()
90
-
91
- self.is_running = True
92
-
93
- # Initial setup commands
94
- self._send_command_raw("set +h") # Disable hash table for commands
95
- self._send_command_raw("export PS1='$ '") # Simpler prompt
96
- self._send_command_raw("cd " + self.working_dir) # Change to working directory
97
- time.sleep(0.5) # Let initial commands settle
98
-
99
-
100
- def _read_stdout(self):
101
- """Read stdout in a separate thread."""
102
- while self.process and self.process.poll() is None:
103
- try:
104
- line = self.process.stdout.readline()
105
- if line:
106
- with self.stdout_lock:
107
- self.stdout_lines.append(line.rstrip('\n'))
108
- else:
109
- time.sleep(0.01)
110
- except Exception as e:
111
- print(f"Error reading stdout: {e}")
112
- break
113
-
114
- def _read_stderr(self):
115
- """Read stderr in a separate thread."""
116
- while self.process and self.process.poll() is None:
117
- try:
118
- line = self.process.stderr.readline()
119
- if line:
120
- with self.stderr_lock:
121
- self.stderr_lines.append(line.rstrip('\n'))
122
- else:
123
- time.sleep(0.01)
124
- except Exception as e:
125
- print(f"Error reading stderr: {e}")
126
- break
127
-
128
- def _send_command_raw(self, command):
129
- """Send a raw command to the shell without waiting for completion."""
130
- if not self.is_running or not self.process:
131
- raise RuntimeError("Shell is not running")
132
-
133
- try:
134
- self.process.stdin.write(command + '\n')
135
- self.process.stdin.flush()
136
- except Exception as e:
137
- print(f"Error sending command: {e}")
138
- raise
139
-
140
- def _preprocess_command(self, command):
141
- """Preprocess commands to handle special cases like virtual environment activation."""
142
- # Handle virtual environment creation and activation
143
- if "uv venv" in command and "&&" in command and "source" in command:
144
- # Split the compound command into separate parts
145
- parts = [part.strip() for part in command.split("&&")]
146
- return parts
147
- elif command.strip().startswith("source ") and "/bin/activate" in command:
148
- # Handle standalone source command
149
- venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
150
- self.virtual_env_path = venv_path
151
- return [command]
152
- elif "source" in command and "activate" in command:
153
- # Handle any other source activation pattern
154
- return [command]
155
- elif "uv pip install" in command and self.is_in_venv():
156
- # If we're in a virtual environment, ensure we use the right pip
157
- return [command]
158
- else:
159
- return [command]
160
-
161
- def execute(self, command, timeout=None):
162
- """Execute a command and return (success, stdout, stderr)."""
163
- if not self.is_running:
164
- self.start()
165
-
166
- if timeout is None:
167
- timeout = self.timeout
168
-
169
- # Preprocess the command to handle special cases
170
- command_parts = self._preprocess_command(command)
171
-
172
- # If we have multiple parts, execute them sequentially
173
- if len(command_parts) > 1:
174
- print(f"๐Ÿ”ง Executing compound command in {len(command_parts)} parts")
175
- all_stdout = []
176
- all_stderr = []
177
-
178
- for i, part in enumerate(command_parts):
179
- print(f" Part {i+1}/{len(command_parts)}: {part}")
180
- success, stdout, stderr = self._execute_single(part, timeout)
181
-
182
- if stdout:
183
- all_stdout.append(stdout)
184
- if stderr:
185
- all_stderr.append(stderr)
186
-
187
- if not success:
188
- # If any part fails, return the failure
189
- return False, '\n'.join(all_stdout), '\n'.join(all_stderr)
190
-
191
- # Small delay between parts to let environment changes take effect
192
- time.sleep(0.1)
193
-
194
- return True, '\n'.join(all_stdout), '\n'.join(all_stderr)
195
- else:
196
- return self._execute_single(command_parts[0], timeout)
197
-
198
- def _execute_single(self, command, timeout):
199
- """Execute a single command and return (success, stdout, stderr)."""
200
- self.command_counter += 1
201
- marker = f"CMD_DONE_{self.command_counter}_{uuid.uuid4().hex[:8]}"
202
-
203
- print(f"๐Ÿ”ง Executing: {command}")
204
-
205
- # Clear any existing output
206
- self._clear_lines()
207
-
208
- # Wait for shell to be ready (prompt should be visible)
209
- if not self.wait_for_prompt(timeout=2):
210
- # print("โš ๏ธ Shell not ready, waiting...")
211
- time.sleep(0.5)
212
-
213
- # For source commands, we need special handling
214
- if command.strip().startswith("source "):
215
- # Send the source command in a way that preserves the environment
216
- try:
217
- # Extract the virtual environment path
218
- venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
219
-
220
- # Use a more robust approach that actually activates the environment
221
- activation_script = f"""
222
- if [ -f "{venv_path}/bin/activate" ]; then
223
- source "{venv_path}/bin/activate"
224
- echo "VIRTUAL_ENV=$VIRTUAL_ENV"
225
- echo "PATH=$PATH"
226
- echo 'SOURCE_SUCCESS'
227
- else
228
- echo 'SOURCE_FAILED - activation script not found'
229
- fi
230
- """
231
-
232
- self._send_command_raw(activation_script)
233
- time.sleep(0.3) # Give more time for environment changes
234
- self._send_command_raw(f'echo "EXIT_CODE:$?"')
235
- self._send_command_raw(f'echo "{marker}"')
236
- except Exception as e:
237
- return False, "", f"Failed to send source command: {e}"
238
- else:
239
- # Send the command followed by markers
240
- try:
241
- self._send_command_raw(command)
242
- # Wait a moment for the command to start
243
- time.sleep(0.1)
244
- self._send_command_raw(f'echo "EXIT_CODE:$?"')
245
- self._send_command_raw(f'echo "{marker}"')
246
- except Exception as e:
247
- return False, "", f"Failed to send command: {e}"
248
-
249
- # Collect output until we see the marker
250
- command_stdout = []
251
- command_stderr = []
252
- start_time = time.time()
253
- found_marker = False
254
- exit_code = None
255
- last_stdout_index = 0
256
- last_stderr_index = 0
257
- source_success = None
258
-
259
- while time.time() - start_time < timeout:
260
- # Check for new stdout lines
261
- with self.stdout_lock:
262
- current_stdout = self.stdout_lines[last_stdout_index:]
263
- last_stdout_index = len(self.stdout_lines)
264
-
265
- for line in current_stdout:
266
- if line == marker:
267
- found_marker = True
268
- break
269
- elif line.startswith("EXIT_CODE:"):
270
- try:
271
- exit_code = int(line.split(":", 1)[1])
272
- except (ValueError, IndexError):
273
- exit_code = 1
274
- elif line == "SOURCE_SUCCESS":
275
- source_success = True
276
- elif line.startswith("SOURCE_FAILED"):
277
- source_success = False
278
- command_stderr.append(line)
279
- elif line.startswith("VIRTUAL_ENV="):
280
- # Extract and store the virtual environment path
281
- venv_path = line.split("=", 1)[1]
282
- self.virtual_env_path = venv_path
283
- command_stdout.append(line)
284
- elif line.startswith("PATH="):
285
- # Store the updated PATH
286
- command_stdout.append(line)
287
- elif line.strip() and not line.startswith("$"): # Skip empty lines and prompt lines
288
- command_stdout.append(line)
289
-
290
- if found_marker:
291
- break
292
-
293
- # Check for new stderr lines
294
- with self.stderr_lock:
295
- current_stderr = self.stderr_lines[last_stderr_index:]
296
- last_stderr_index = len(self.stderr_lines)
297
-
298
- for line in current_stderr:
299
- if line.strip(): # Skip empty lines
300
- command_stderr.append(line)
301
-
302
- # Check if command is waiting for user input
303
- if not found_marker and time.time() - start_time > 5: # Wait at least 5 seconds before checking
304
- if self._is_waiting_for_input(command_stdout, command_stderr):
305
- print("โš ๏ธ Command appears to be waiting for user input")
306
- # Try to handle the input requirement
307
- input_handled = self._handle_input_requirement(command, command_stdout, command_stderr)
308
-
309
- if input_handled is True and self.should_remove_command:
310
- # If LLM suggested to remove the command
311
- self._send_command_raw("\x03") # Send Ctrl+C
312
- time.sleep(0.5)
313
- return False, '\n'.join(command_stdout), f"Command removed - {self.removal_reason}"
314
- elif not input_handled:
315
- # If we couldn't handle the input, abort the command
316
- self._send_command_raw("\x03") # Send Ctrl+C
317
- time.sleep(0.5)
318
- return False, '\n'.join(command_stdout), "Command aborted - requires user input"
319
-
320
- time.sleep(0.1)
321
-
322
- if not found_marker:
323
- print(f"โš ๏ธ Command timed out after {timeout} seconds")
324
- return False, '\n'.join(command_stdout), f"Command timed out after {timeout} seconds"
325
-
326
- stdout_text = '\n'.join(command_stdout)
327
- stderr_text = '\n'.join(command_stderr)
328
-
329
- # Determine success based on multiple factors
330
- if source_success is not None:
331
- success = source_success
332
- else:
333
- success = exit_code == 0 if exit_code is not None else len(command_stderr) == 0
334
-
335
- if success:
336
- if stdout_text:
337
- print(f"โœ… Output: {stdout_text}")
338
- # Track virtual environment activation
339
- if command.strip().startswith("source ") and "/bin/activate" in command:
340
- venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
341
- self.virtual_env_path = venv_path
342
- print(f"โœ… Virtual environment activated: {venv_path}")
343
- else:
344
- print(f"โŒ Command failed with exit code: {exit_code}")
345
- if stderr_text:
346
- print(f"โŒ Error: {stderr_text}")
347
-
348
- # Wait a moment for the shell to be ready for the next command
349
- time.sleep(0.2)
350
-
351
- return success, stdout_text, stderr_text
352
-
353
- def _is_waiting_for_input(self, stdout_lines, stderr_lines):
354
- """Detect if a command is waiting for user input."""
355
- # Common patterns that indicate waiting for user input
356
- input_patterns = [
357
- r'(?i)(y/n|yes/no)\??\s*$', # Yes/No prompts
358
- r'(?i)password:?\s*$', # Password prompts
359
- r'(?i)continue\??\s*$', # Continue prompts
360
- r'(?i)proceed\??\s*$', # Proceed prompts
361
- r'\[\s*[Yy]/[Nn]\s*\]\s*$', # [Y/n] style prompts
362
- r'(?i)username:?\s*$', # Username prompts
363
- r'(?i)token:?\s*$', # Token prompts
364
- r'(?i)api key:?\s*$', # API key prompts
365
- r'(?i)press enter to continue', # Press enter prompts
366
- r'(?i)select an option:?\s*$', # Selection prompts
367
- r'(?i)choose an option:?\s*$', # Choice prompts
368
- ]
369
-
370
- # Check the last few lines of stdout and stderr for input patterns
371
- last_lines = []
372
- if stdout_lines:
373
- last_lines.extend(stdout_lines[-3:]) # Check last 3 lines of stdout
374
- if stderr_lines:
375
- last_lines.extend(stderr_lines[-3:]) # Check last 3 lines of stderr
376
-
377
- for line in last_lines:
378
- for pattern in input_patterns:
379
- if re.search(pattern, line):
380
- print(f"๐Ÿ” Detected input prompt: {line}")
381
- return True
382
-
383
- # Check if there's no output for a while but the command is still running
384
- if len(stdout_lines) == 0 and len(stderr_lines) == 0:
385
- # This might be a command waiting for input without a prompt
386
- # We'll be cautious and only return True if we're sure
387
- return False
388
-
389
- return False
390
-
391
- def _handle_input_requirement(self, command, stdout_lines, stderr_lines):
392
- """Attempt to handle commands that require input."""
393
- # Extract the last few lines to analyze what kind of input is needed
394
- last_lines = []
395
- if stdout_lines:
396
- last_lines.extend(stdout_lines[-3:])
397
- if stderr_lines:
398
- last_lines.extend(stderr_lines[-3:])
399
-
400
- last_line = last_lines[-1] if last_lines else ""
401
-
402
- # Try to determine what kind of input is needed
403
- if re.search(r'(?i)(y/n|yes/no|\[y/n\])', last_line):
404
- # For yes/no prompts, usually 'yes' is safer
405
- print("๐Ÿ”ง Auto-responding with 'y' to yes/no prompt")
406
- self._send_command_raw("y")
407
- return True
408
-
409
- elif re.search(r'(?i)password', last_line):
410
- # For password prompts, check if we have stored credentials
411
- stored_creds = get_stored_credentials()
412
- if stored_creds and 'ssh_password' in stored_creds:
413
- print("๐Ÿ”ง Auto-responding with stored SSH password")
414
- self._send_command_raw(stored_creds['ssh_password'])
415
- return True
416
- else:
417
- print("โš ๏ธ Password prompt detected but no stored password available")
418
- return False
419
-
420
- elif re.search(r'(?i)token|api.key', last_line):
421
- # For token/API key prompts
422
- stored_creds = get_stored_credentials()
423
- if stored_creds:
424
- if 'openai_api_key' in stored_creds and re.search(r'(?i)openai|api.key', last_line):
425
- print("๐Ÿ”ง Auto-responding with stored OpenAI API key")
426
- self._send_command_raw(stored_creds['openai_api_key'])
427
- return True
428
- elif 'hf_token' in stored_creds and re.search(r'(?i)hugg|hf|token', last_line):
429
- print("๐Ÿ”ง Auto-responding with stored Hugging Face token")
430
- self._send_command_raw(stored_creds['hf_token'])
431
- return True
432
-
433
- print("โš ๏ธ Token/API key prompt detected but no matching stored credentials")
434
- return False
435
-
436
- elif re.search(r'(?i)press enter|continue|proceed', last_line):
437
- # For "press enter to continue" prompts
438
- print("๐Ÿ”ง Auto-responding with Enter to continue")
439
- self._send_command_raw("") # Empty string sends just Enter
440
- return True
441
-
442
- # If we can't determine the type of input needed
443
- print("โš ๏ธ Couldn't determine the type of input needed")
444
-
445
- # Try to use LLM to suggest an alternative command
446
- try:
447
- # Get current working directory for context
448
- cwd = self.get_cwd()
449
-
450
- # Reset command removal flags
451
- self.should_remove_command = False
452
- self.removal_reason = None
453
-
454
- # Call LLM to suggest an alternative
455
- alternative = self._suggest_alternative_command(command, stdout_lines, stderr_lines, cwd)
456
-
457
- # Check if LLM suggested to remove the command
458
- if self.should_remove_command:
459
- print(f"๐Ÿšซ Command will be removed: {self.removal_reason}")
460
- return True # Return True to indicate the command has been handled (by removing it)
461
-
462
- if alternative:
463
- print(f"๐Ÿ”ง LLM suggested alternative command: {alternative}")
464
- # We don't execute the alternative here, but return False so the calling code
465
- # can handle it (e.g., by adding it to the command list)
466
-
467
- # Store the suggested alternative for later use
468
- self.suggested_alternative = alternative
469
- return False
470
- except Exception as e:
471
- print(f"โš ๏ธ Error getting LLM suggestion: {e}")
472
-
473
- return False
474
-
475
- def _suggest_alternative_command(self, command, stdout_lines, stderr_lines, current_dir):
476
- """Use LLM to suggest an alternative command that doesn't require user input."""
477
- try:
478
- # Get API key
479
- api_key = os.environ.get("OPENAI_API_KEY")
480
- if not api_key:
481
- # Try to load from saved file
482
- key_file = os.path.expanduser("~/.gitarsenal/openai_key")
483
- if os.path.exists(key_file):
484
- with open(key_file, "r") as f:
485
- api_key = f.read().strip()
486
-
487
- if not api_key:
488
- print("โš ๏ธ No OpenAI API key available for suggesting alternative command")
489
- return None
490
-
491
- # Prepare the prompt
492
- stdout_text = '\n'.join(stdout_lines[-10:]) if stdout_lines else ""
493
- stderr_text = '\n'.join(stderr_lines[-10:]) if stderr_lines else ""
494
-
495
- prompt = f"""
496
- The command '{command}' appears to be waiting for user input.
497
-
498
- Current directory: {current_dir}
499
-
500
- Last stdout output:
501
- {stdout_text}
502
-
503
- Last stderr output:
504
- {stderr_text}
505
-
506
- Please analyze this command and determine if it's useful to continue with it.
507
- If it's useful, suggest an alternative command that achieves the same goal but doesn't require user input.
508
- For example, add flags like -y, --yes, --no-input, etc., or provide the required input in the command.
509
-
510
- If the command is not useful or cannot be executed non-interactively, respond with "REMOVE_COMMAND" and explain why.
511
-
512
- Format your response as:
513
- ALTERNATIVE: <alternative command>
514
- or
515
- REMOVE_COMMAND: <reason>
516
- """
517
-
518
- # Call OpenAI API
519
- import openai
520
- client = openai.OpenAI(api_key=api_key)
521
-
522
- response = client.chat.completions.create(
523
- model="gpt-4o-mini",
524
- messages=[
525
- {"role": "system", "content": "You are a helpful assistant that suggests alternative commands that don't require user input."},
526
- {"role": "user", "content": prompt}
527
- ],
528
- max_tokens=150,
529
- temperature=0.7
530
- )
531
-
532
- response_text = response.choices[0].message.content.strip()
533
-
534
- # Check if the response suggests removing the command
535
- if response_text.startswith("REMOVE_COMMAND:"):
536
- reason = response_text.replace("REMOVE_COMMAND:", "").strip()
537
- print(f"๐Ÿšซ LLM suggests removing command: {reason}")
538
- self.should_remove_command = True
539
- self.removal_reason = reason
540
- return None
541
-
542
- # Extract the alternative command
543
- if response_text.startswith("ALTERNATIVE:"):
544
- alternative_command = response_text.replace("ALTERNATIVE:", "").strip()
545
- else:
546
- # Try to extract the command from a free-form response
547
- lines = response_text.split('\n')
548
- for line in lines:
549
- line = line.strip()
550
- if line and not line.startswith(('Here', 'I', 'You', 'The', 'This', 'Use', 'Try')):
551
- alternative_command = line
552
- break
553
- else:
554
- alternative_command = lines[0].strip()
555
-
556
- return alternative_command
557
-
558
- except Exception as e:
559
- print(f"โš ๏ธ Error suggesting alternative command: {e}")
560
- return None
561
-
562
- def _clear_lines(self):
563
- """Clear both output line lists."""
564
- with self.stdout_lock:
565
- self.stdout_lines.clear()
566
- with self.stderr_lock:
567
- self.stderr_lines.clear()
568
-
569
- def get_cwd(self):
570
- """Get current working directory."""
571
- success, output, _ = self._execute_single("pwd", 10)
572
- if success:
573
- return output.strip()
574
- return self.working_dir
575
-
576
- def get_virtual_env(self):
577
- """Get the currently activated virtual environment path."""
578
- return self.virtual_env_path
579
-
580
- def is_in_venv(self):
581
- """Check if we're currently in a virtual environment."""
582
- return self.virtual_env_path is not None and self.virtual_env_path != ""
583
-
584
- def get_venv_name(self):
585
- """Get the name of the current virtual environment if active."""
586
- if self.is_in_venv():
587
- return os.path.basename(self.virtual_env_path)
588
- return None
589
-
590
- def exec(self, *args, **kwargs):
591
- """Compatibility method to make PersistentShell work with call_openai_for_debug."""
592
- # Convert exec call to execute method
593
- if len(args) >= 2 and args[0] == "bash" and args[1] == "-c":
594
- command = args[2]
595
- success, stdout, stderr = self.execute(command)
596
-
597
- # Create a mock result object that mimics the expected interface
598
- class MockResult:
599
- def __init__(self, stdout, stderr, returncode):
600
- self.stdout = [stdout] if stdout else []
601
- self.stderr = [stderr] if stderr else []
602
- self.returncode = 0 if returncode else 1
603
-
604
- def wait(self):
605
- pass
606
-
607
- return MockResult(stdout, stderr, success)
608
- else:
609
- raise NotImplementedError("exec method only supports bash -c commands")
610
-
611
- def wait_for_prompt(self, timeout=5):
612
- """Wait for the shell prompt to appear, indicating readiness for next command."""
613
- start_time = time.time()
614
- while time.time() - start_time < timeout:
615
- with self.stdout_lock:
616
- if self.stdout_lines and self.stdout_lines[-1].strip().endswith('$'):
617
- return True
618
- time.sleep(0.1)
619
- return False
620
-
621
- def cleanup(self):
622
- """Clean up the shell process."""
623
- print("๐Ÿงน Cleaning up persistent shell...")
624
- self.is_running = False
625
-
626
- if self.process:
627
- try:
628
- # Send exit command
629
- self._send_command_raw("exit")
630
-
631
- # Wait for process to terminate
632
- try:
633
- self.process.wait(timeout=5)
634
- except subprocess.TimeoutExpired:
635
- # Force kill if it doesn't exit gracefully
636
- os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
637
- try:
638
- self.process.wait(timeout=2)
639
- except subprocess.TimeoutExpired:
640
- os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
641
-
642
- except Exception as e:
643
- print(f"Error during cleanup: {e}")
644
- finally:
645
- self.process = None
646
-
647
- print("โœ… Shell cleanup completed")
648
-
649
-
650
- class CommandListManager:
651
- """Manages a dynamic list of setup commands with status tracking and LLM-suggested fixes."""
652
-
653
- def __init__(self, initial_commands=None):
654
- self.commands = []
655
- self.executed_commands = []
656
- self.failed_commands = []
657
- self.suggested_fixes = []
658
- self.current_index = 0
659
- self.total_commands = 0
660
-
661
- if initial_commands:
662
- self.add_commands(initial_commands)
663
-
664
- def add_commands(self, commands):
665
- """Add new commands to the list."""
666
- if isinstance(commands, str):
667
- commands = [commands]
668
-
669
- added_count = 0
670
- for cmd in commands:
671
- if cmd and cmd.strip():
672
- self.commands.append({
673
- 'command': cmd.strip(),
674
- 'status': 'pending',
675
- 'index': len(self.commands),
676
- 'stdout': '',
677
- 'stderr': '',
678
- 'execution_time': None,
679
- 'fix_attempts': 0,
680
- 'max_fix_attempts': 3
681
- })
682
- added_count += 1
683
-
684
- self.total_commands = len(self.commands)
685
- if added_count > 0:
686
- print(f"๐Ÿ“‹ Added {added_count} commands to list. Total: {self.total_commands}")
687
-
688
- def add_command_dynamically(self, command, priority='normal'):
689
- """Add a single command dynamically during execution."""
690
- if not command or not command.strip():
691
- return False
692
-
693
- new_command = {
694
- 'command': command.strip(),
695
- 'status': 'pending',
696
- 'index': len(self.commands),
697
- 'stdout': '',
698
- 'stderr': '',
699
- 'execution_time': None,
700
- 'fix_attempts': 0,
701
- 'max_fix_attempts': 3,
702
- 'priority': priority
703
- }
704
-
705
- if priority == 'high':
706
- # Insert at the beginning of pending commands
707
- self.commands.insert(self.current_index, new_command)
708
- # Update indices for all commands after insertion
709
- for i in range(self.current_index + 1, len(self.commands)):
710
- self.commands[i]['index'] = i
711
- else:
712
- # Add to the end
713
- self.commands.append(new_command)
714
-
715
- self.total_commands = len(self.commands)
716
- print(f"๐Ÿ“‹ Added dynamic command: {command.strip()}")
717
- return True
718
-
719
- def add_suggested_fix(self, original_command, fix_command, reason=""):
720
- """Add a LLM-suggested fix for a failed command."""
721
- fix_entry = {
722
- 'original_command': original_command,
723
- 'fix_command': fix_command,
724
- 'reason': reason,
725
- 'status': 'pending',
726
- 'index': len(self.suggested_fixes),
727
- 'stdout': '',
728
- 'stderr': '',
729
- 'execution_time': None
730
- }
731
- self.suggested_fixes.append(fix_entry)
732
- print(f"๐Ÿ”ง Added suggested fix: {fix_command}")
733
- return len(self.suggested_fixes) - 1
734
-
735
- def get_next_command(self):
736
- """Get the next pending command to execute."""
737
- # First, try to get a pending command from the main list
738
- for i in range(self.current_index, len(self.commands)):
739
- if self.commands[i]['status'] == 'pending':
740
- return self.commands[i], 'main'
741
-
742
- # If no pending commands in main list, check suggested fixes
743
- for fix in self.suggested_fixes:
744
- if fix['status'] == 'pending':
745
- return fix, 'fix'
746
-
747
- return None, None
748
-
749
- def mark_command_executed(self, command_index, command_type='main', success=True, stdout='', stderr='', execution_time=None):
750
- """Mark a command as executed with results."""
751
- if command_type == 'main':
752
- if 0 <= command_index < len(self.commands):
753
- self.commands[command_index].update({
754
- 'status': 'success' if success else 'failed',
755
- 'stdout': stdout,
756
- 'stderr': stderr,
757
- 'execution_time': execution_time
758
- })
759
-
760
- if success:
761
- self.executed_commands.append(self.commands[command_index])
762
- print(f"โœ… Command {command_index + 1}/{self.total_commands} completed successfully")
763
- else:
764
- self.failed_commands.append(self.commands[command_index])
765
- print(f"โŒ Command {command_index + 1}/{self.total_commands} failed")
766
-
767
- self.current_index = max(self.current_index, command_index + 1)
768
-
769
- elif command_type == 'fix':
770
- if 0 <= command_index < len(self.suggested_fixes):
771
- self.suggested_fixes[command_index].update({
772
- 'status': 'success' if success else 'failed',
773
- 'stdout': stdout,
774
- 'stderr': stderr,
775
- 'execution_time': execution_time
776
- })
777
-
778
- if success:
779
- print(f"โœ… Fix command {command_index + 1} completed successfully")
780
- else:
781
- print(f"โŒ Fix command {command_index + 1} failed")
782
-
783
- def get_status_summary(self):
784
- """Get a summary of command execution status."""
785
- total_main = len(self.commands)
786
- total_fixes = len(self.suggested_fixes)
787
- executed_main = len([c for c in self.commands if c['status'] == 'success'])
788
- failed_main = len([c for c in self.commands if c['status'] == 'failed'])
789
- pending_main = len([c for c in self.commands if c['status'] == 'pending'])
790
- executed_fixes = len([f for f in self.suggested_fixes if f['status'] == 'success'])
791
- failed_fixes = len([f for f in self.suggested_fixes if f['status'] == 'failed'])
792
-
793
- return {
794
- 'total_main_commands': total_main,
795
- 'executed_main_commands': executed_main,
796
- 'failed_main_commands': failed_main,
797
- 'pending_main_commands': pending_main,
798
- 'total_fix_commands': total_fixes,
799
- 'executed_fix_commands': executed_fixes,
800
- 'failed_fix_commands': failed_fixes,
801
- 'progress_percentage': (executed_main / total_main * 100) if total_main > 0 else 0
802
- }
803
-
804
- def print_status(self):
805
- """Print current status of all commands."""
806
- summary = self.get_status_summary()
807
-
808
- print("\n" + "="*60)
809
- print("๐Ÿ“‹ COMMAND EXECUTION STATUS")
810
- print("="*60)
811
-
812
- # Main commands status
813
- print(f"๐Ÿ“‹ Main Commands: {summary['executed_main_commands']}/{summary['total_main_commands']} completed")
814
- print(f" โœ… Successful: {summary['executed_main_commands']}")
815
- print(f" โŒ Failed: {summary['failed_main_commands']}")
816
- print(f" โณ Pending: {summary['pending_main_commands']}")
817
-
818
- # Fix commands status
819
- if summary['total_fix_commands'] > 0:
820
- print(f"๐Ÿ”ง Fix Commands: {summary['executed_fix_commands']}/{summary['total_fix_commands']} completed")
821
- print(f" โœ… Successful: {summary['executed_fix_commands']}")
822
- print(f" โŒ Failed: {summary['failed_fix_commands']}")
823
-
824
- # Progress bar
825
- progress = summary['progress_percentage']
826
- bar_length = 30
827
- filled_length = int(bar_length * progress / 100)
828
- bar = 'โ–ˆ' * filled_length + 'โ–‘' * (bar_length - filled_length)
829
- print(f"๐Ÿ“Š Progress: [{bar}] {progress:.1f}%")
830
-
831
- # Show current command if any
832
- next_cmd, cmd_type = self.get_next_command()
833
- if next_cmd:
834
- cmd_type_str = "main" if cmd_type == 'main' else "fix"
835
- cmd_text = next_cmd.get('command', next_cmd.get('fix_command', 'Unknown command'))
836
- print(f"๐Ÿ”„ Current: {cmd_type_str} command - {cmd_text[:50]}...")
837
-
838
- print("="*60)
839
-
840
- def get_failed_commands_for_llm(self):
841
- """Get failed commands for LLM analysis."""
842
- failed_commands = []
843
-
844
- # Get failed main commands
845
- for cmd in self.commands:
846
- if cmd['status'] == 'failed':
847
- failed_commands.append({
848
- 'command': cmd['command'],
849
- 'stderr': cmd['stderr'],
850
- 'stdout': cmd['stdout'],
851
- 'type': 'main'
852
- })
853
-
854
- # Get failed fix commands
855
- for fix in self.suggested_fixes:
856
- if fix['status'] == 'failed':
857
- failed_commands.append({
858
- 'command': fix['fix_command'],
859
- 'stderr': fix['stderr'],
860
- 'stdout': fix['stdout'],
861
- 'type': 'fix',
862
- 'original_command': fix['original_command']
863
- })
864
-
865
- return failed_commands
866
-
867
- def has_pending_commands(self):
868
- """Check if there are any pending commands."""
869
- return any(cmd['status'] == 'pending' for cmd in self.commands) or \
870
- any(fix['status'] == 'pending' for fix in self.suggested_fixes)
871
-
872
- def get_all_commands(self):
873
- """Get all commands (main + fixes) in execution order."""
874
- all_commands = []
875
-
876
- # Add main commands
877
- for cmd in self.commands:
878
- all_commands.append({
879
- **cmd,
880
- 'type': 'main'
881
- })
882
-
883
- # Add fix commands
884
- for fix in self.suggested_fixes:
885
- all_commands.append({
886
- **fix,
887
- 'type': 'fix'
888
- })
889
-
890
- return all_commands
891
-
892
- def analyze_failed_commands_with_llm(self, api_key=None, current_dir=None, sandbox=None):
893
- """Analyze all failed commands using LLM and add suggested fixes."""
894
- failed_commands = self.get_failed_commands_for_llm()
895
-
896
- if not failed_commands:
897
- print("โœ… No failed commands to analyze")
898
- return []
899
-
900
- print(f"๐Ÿ” Analyzing {len(failed_commands)} failed commands with LLM...")
901
-
902
- # Use unified batch debugging for efficiency
903
- fixes = call_llm_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
904
-
905
- # Add the fixes to the command list
906
- added_fixes = []
907
- for fix in fixes:
908
- fix_index = self.add_suggested_fix(
909
- fix['original_command'],
910
- fix['fix_command'],
911
- fix['reason']
912
- )
913
- added_fixes.append(fix_index)
914
-
915
- print(f"๐Ÿ”ง Added {len(added_fixes)} LLM-suggested fixes to command list")
916
- return added_fixes
917
-
918
- def should_skip_original_command(self, original_command, fix_command, fix_stdout, fix_stderr, api_key=None):
919
- """
920
- Use LLM to determine if the original command should be skipped after a successful fix.
921
-
922
- Args:
923
- original_command: The original command that failed
924
- fix_command: The fix command that succeeded
925
- fix_stdout: The stdout from the fix command
926
- fix_stderr: The stderr from the fix command
927
- api_key: OpenAI API key
928
-
929
- Returns:
930
- tuple: (should_skip, reason)
931
- """
932
- try:
933
- # Get API key if not provided
934
- if not api_key:
935
- api_key = os.environ.get("OPENAI_API_KEY")
936
- if not api_key:
937
- # Try to load from saved file
938
- key_file = os.path.expanduser("~/.gitarsenal/openai_key")
939
- if os.path.exists(key_file):
940
- with open(key_file, "r") as f:
941
- api_key = f.read().strip()
942
-
943
- if not api_key:
944
- print("โš ๏ธ No OpenAI API key available for command list analysis")
945
- return False, "No API key available"
946
-
947
- # Get all commands for context
948
- all_commands = self.get_all_commands()
949
- commands_context = "\n".join([f"{i+1}. {cmd['command']} - {cmd['status']}" for i, cmd in enumerate(all_commands)])
950
-
951
- # Prepare the prompt
952
- prompt = f"""
953
- I need to determine if an original command should be skipped after a successful fix command.
954
-
955
- Original command (failed): {original_command}
956
- Fix command (succeeded): {fix_command}
957
-
958
- Fix command stdout:
959
- {fix_stdout}
960
-
961
- Fix command stderr:
962
- {fix_stderr}
963
-
964
- Current command list:
965
- {commands_context}
966
-
967
- Based on this information, should I skip running the original command again?
968
- Consider:
969
- 1. If the fix command already accomplished what the original command was trying to do
970
- 2. If running the original command again would be redundant or cause errors
971
- 3. If the original command is still necessary after the fix
972
-
973
- Respond with ONLY:
974
- SKIP: <reason>
975
- or
976
- RUN: <reason>
977
- """
978
-
979
- # Call OpenAI API
980
- import openai
981
- client = openai.OpenAI(api_key=api_key)
982
-
983
- print("๐Ÿ” Analyzing if original command should be skipped...")
984
-
985
- response = client.chat.completions.create(
986
- model="gpt-3.5-turbo",
987
- messages=[
988
- {"role": "system", "content": "You are a helpful assistant that analyzes command execution."},
989
- {"role": "user", "content": prompt}
990
- ],
991
- max_tokens=100,
992
- temperature=0.3
993
- )
994
-
995
- response_text = response.choices[0].message.content.strip()
996
-
997
- # Parse the response
998
- if response_text.startswith("SKIP:"):
999
- reason = response_text.replace("SKIP:", "").strip()
1000
- print(f"๐Ÿ” LLM suggests skipping original command: {reason}")
1001
- return True, reason
1002
- elif response_text.startswith("RUN:"):
1003
- reason = response_text.replace("RUN:", "").strip()
1004
- print(f"๐Ÿ” LLM suggests running original command: {reason}")
1005
- return False, reason
1006
- else:
1007
- # Try to interpret a free-form response
1008
- if "skip" in response_text.lower() and "should" in response_text.lower():
1009
- print(f"๐Ÿ” Interpreting response as SKIP: {response_text}")
1010
- return True, response_text
1011
- else:
1012
- print(f"๐Ÿ” Interpreting response as RUN: {response_text}")
1013
- return False, response_text
1014
-
1015
- except Exception as e:
1016
- print(f"โš ๏ธ Error analyzing command skip decision: {e}")
1017
- return False, f"Error: {e}"
1018
-
1019
- def replace_command(self, command_index, new_command, reason=""):
1020
- """
1021
- Replace a command in the list with a new command.
1022
-
1023
- Args:
1024
- command_index: The index of the command to replace
1025
- new_command: The new command to use
1026
- reason: The reason for the replacement
1027
-
1028
- Returns:
1029
- bool: True if the command was replaced, False otherwise
1030
- """
1031
- if 0 <= command_index < len(self.commands):
1032
- old_command = self.commands[command_index]['command']
1033
- self.commands[command_index]['command'] = new_command
1034
- self.commands[command_index]['status'] = 'pending' # Reset status
1035
- self.commands[command_index]['stdout'] = ''
1036
- self.commands[command_index]['stderr'] = ''
1037
- self.commands[command_index]['execution_time'] = None
1038
- self.commands[command_index]['replacement_reason'] = reason
1039
-
1040
- print(f"๐Ÿ”„ Replaced command {command_index + 1}: '{old_command}' with '{new_command}'")
1041
- print(f"๐Ÿ” Reason: {reason}")
1042
- return True
1043
- else:
1044
- print(f"โŒ Invalid command index for replacement: {command_index}")
1045
- return False
1046
-
1047
- def update_command_list_with_llm(self, api_key=None):
1048
- """
1049
- Use LLM to analyze and update the entire command list.
1050
-
1051
- Args:
1052
- api_key: OpenAI API key
1053
-
1054
- Returns:
1055
- bool: True if the list was updated, False otherwise
1056
- """
1057
- try:
1058
- # Get API key if not provided
1059
- if not api_key:
1060
- api_key = os.environ.get("OPENAI_API_KEY")
1061
- if not api_key:
1062
- # Try to load from saved file
1063
- key_file = os.path.expanduser("~/.gitarsenal/openai_key")
1064
- if os.path.exists(key_file):
1065
- with open(key_file, "r") as f:
1066
- api_key = f.read().strip()
1067
-
1068
- if not api_key:
1069
- print("โš ๏ธ No OpenAI API key available for command list analysis")
1070
- return False
1071
-
1072
- # Get all commands for context
1073
- all_commands = self.get_all_commands()
1074
- commands_context = "\n".join([f"{i+1}. {cmd['command']} - {cmd['status']}"
1075
- for i, cmd in enumerate(all_commands)])
1076
-
1077
- # Get executed commands with their outputs for context
1078
- executed_context = ""
1079
- for cmd in self.executed_commands:
1080
- executed_context += f"Command: {cmd['command']}\n"
1081
- executed_context += f"Status: {cmd['status']}\n"
1082
- if cmd['stdout']:
1083
- executed_context += f"Stdout: {cmd['stdout'][:500]}...\n" if len(cmd['stdout']) > 500 else f"Stdout: {cmd['stdout']}\n"
1084
- if cmd['stderr']:
1085
- executed_context += f"Stderr: {cmd['stderr'][:500]}...\n" if len(cmd['stderr']) > 500 else f"Stderr: {cmd['stderr']}\n"
1086
- executed_context += "\n"
1087
-
1088
- # Prepare the prompt
1089
- prompt = f"""
1090
- I need you to analyze and optimize this command list. Some commands have been executed,
1091
- and some are still pending. Based on what has already been executed, I need you to:
1092
-
1093
- 1. Identify any pending commands that are now redundant or unnecessary
1094
- 2. Identify any pending commands that should be modified based on previous command results
1095
- 3. Suggest any new commands that should be added
1096
-
1097
- Current command list:
1098
- {commands_context}
1099
-
1100
- Details of executed commands:
1101
- {executed_context}
1102
-
1103
- For each pending command (starting from the next command to be executed), tell me if it should be:
1104
- 1. KEEP: Keep the command as is
1105
- 2. SKIP: Skip the command (mark as completed without running)
1106
- 3. MODIFY: Modify the command (provide the new command)
1107
- 4. ADD_AFTER: Add a new command after this one
1108
-
1109
- Format your response as a JSON array of actions:
1110
- [
1111
- {{
1112
- "command_index": <index>,
1113
- "action": "KEEP|SKIP|MODIFY|ADD_AFTER",
1114
- "new_command": "<new command if MODIFY or ADD_AFTER>",
1115
- "reason": "<reason for this action>"
1116
- }},
1117
- ...
1118
- ]
1119
-
1120
- Only include commands that need changes (SKIP, MODIFY, ADD_AFTER), not KEEP actions.
1121
- """
1122
-
1123
- # Call OpenAI API
1124
- import openai
1125
- import json
1126
- client = openai.OpenAI(api_key=api_key)
1127
-
1128
- print("๐Ÿ” Analyzing command list for optimizations...")
1129
-
1130
- response = client.chat.completions.create(
1131
- model="gpt-4o-mini", # Use a more capable model for this complex task
1132
- messages=[
1133
- {"role": "system", "content": "You are a helpful assistant that analyzes and optimizes command lists."},
1134
- {"role": "user", "content": prompt}
1135
- ],
1136
- max_tokens=1000,
1137
- temperature=0.2
1138
- )
1139
-
1140
- response_text = response.choices[0].message.content.strip()
1141
-
1142
- # Extract JSON from the response
1143
- try:
1144
- # Find JSON array in the response
1145
- json_match = re.search(r'\[\s*\{.*\}\s*\]', response_text, re.DOTALL)
1146
- if json_match:
1147
- json_str = json_match.group(0)
1148
- actions = json.loads(json_str)
1149
- else:
1150
- # Try to parse the entire response as JSON
1151
- actions = json.loads(response_text)
1152
-
1153
- if not isinstance(actions, list):
1154
- print("โŒ Invalid response format from LLM - not a list")
1155
- return False
1156
-
1157
- # Apply the suggested changes
1158
- changes_made = 0
1159
- commands_added = 0
1160
-
1161
- # Process in reverse order to avoid index shifting issues
1162
- for action in sorted(actions, key=lambda x: x.get('command_index', 0), reverse=True):
1163
- cmd_idx = action.get('command_index')
1164
- action_type = action.get('action')
1165
- new_cmd = action.get('new_command', '')
1166
- reason = action.get('reason', 'No reason provided')
1167
-
1168
- if cmd_idx is None or action_type is None:
1169
- continue
1170
-
1171
- # Convert to 0-based index if needed
1172
- if cmd_idx > 0: # Assume 1-based index from LLM
1173
- cmd_idx -= 1
1174
-
1175
- # Skip if the command index is invalid
1176
- if cmd_idx < 0 or cmd_idx >= len(self.commands):
1177
- print(f"โŒ Invalid command index: {cmd_idx}")
1178
- continue
1179
-
1180
- # Skip if the command has already been executed
1181
- if self.commands[cmd_idx]['status'] != 'pending':
1182
- print(f"โš ๏ธ Command {cmd_idx + 1} already executed, skipping action")
1183
- continue
1184
-
1185
- if action_type == "SKIP":
1186
- # Mark the command as successful without running it
1187
- self.mark_command_executed(
1188
- cmd_idx, 'main', True,
1189
- f"Command skipped: {reason}",
1190
- "", 0
1191
- )
1192
- print(f"๐Ÿ”„ Skipped command {cmd_idx + 1}: {reason}")
1193
- changes_made += 1
1194
-
1195
- elif action_type == "MODIFY":
1196
- if new_cmd:
1197
- if self.replace_command(cmd_idx, new_cmd, reason):
1198
- changes_made += 1
1199
- else:
1200
- print(f"โŒ No new command provided for MODIFY action on command {cmd_idx + 1}")
1201
-
1202
- elif action_type == "ADD_AFTER":
1203
- if new_cmd:
1204
- # Add new command after the current one
1205
- insert_idx = cmd_idx + 1
1206
- new_cmd_obj = {
1207
- 'command': new_cmd,
1208
- 'status': 'pending',
1209
- 'index': insert_idx,
1210
- 'stdout': '',
1211
- 'stderr': '',
1212
- 'execution_time': None,
1213
- 'fix_attempts': 0,
1214
- 'max_fix_attempts': 3,
1215
- 'added_reason': reason
1216
- }
1217
-
1218
- # Insert the new command
1219
- self.commands.insert(insert_idx, new_cmd_obj)
1220
-
1221
- # Update indices for all commands after insertion
1222
- for i in range(insert_idx + 1, len(self.commands)):
1223
- self.commands[i]['index'] = i
1224
-
1225
- print(f"โž• Added new command after {cmd_idx + 1}: '{new_cmd}'")
1226
- print(f"๐Ÿ” Reason: {reason}")
1227
- commands_added += 1
1228
- else:
1229
- print(f"โŒ No new command provided for ADD_AFTER action on command {cmd_idx + 1}")
1230
-
1231
- # Update total commands count
1232
- self.total_commands = len(self.commands)
1233
-
1234
- print(f"โœ… Command list updated: {changes_made} changes made, {commands_added} commands added")
1235
- return changes_made > 0 or commands_added > 0
1236
-
1237
- except json.JSONDecodeError as e:
1238
- print(f"โŒ Failed to parse LLM response as JSON: {e}")
1239
- print(f"Raw response: {response_text}")
1240
- return False
1241
- except Exception as e:
1242
- print(f"โŒ Error updating command list: {e}")
1243
- return False
1244
-
1245
- except Exception as e:
1246
- print(f"โš ๏ธ Error analyzing command list: {e}")
1247
- return False
1248
-
1249
-
1250
- # Import the fetch_modal_tokens module
1251
- # print("๐Ÿ”„ Fetching tokens from proxy server...")
1252
- from fetch_modal_tokens import get_tokens
1253
- token_id, token_secret, openai_api_key, _ = get_tokens()
1254
-
1255
- # Check if we got valid tokens
1256
- if token_id is None or token_secret is None:
1257
- raise ValueError("Could not get valid tokens")
1258
-
1259
- print(f"โœ… Tokens fetched successfully")
1260
-
1261
- # Explicitly set the environment variables again to be sure
1262
- os.environ["MODAL_TOKEN_ID"] = token_id
1263
- os.environ["MODAL_TOKEN_SECRET"] = token_secret
1264
- os.environ["OPENAI_API_KEY"] = openai_api_key
1265
- # Also set the old environment variable for backward compatibility
1266
- os.environ["MODAL_TOKEN"] = token_id
1267
-
1268
- # Set token variables for later use
1269
- token = token_id # For backward compatibility
1270
-
1271
-
1272
- def get_stored_credentials():
1273
- """Load stored credentials from ~/.gitarsenal/credentials.json"""
1274
- import json
1275
- from pathlib import Path
1276
-
1277
- try:
1278
- credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
1279
- if credentials_file.exists():
1280
- with open(credentials_file, 'r') as f:
1281
- credentials = json.load(f)
1282
- return credentials
1283
- else:
1284
- return {}
1285
- except Exception as e:
1286
- print(f"โš ๏ธ Error loading stored credentials: {e}")
1287
- return {}
1288
-
1289
- def generate_auth_context(stored_credentials):
1290
- """Generate simple authentication context for the OpenAI prompt"""
1291
- if not stored_credentials:
1292
- return "No stored credentials available."
1293
-
1294
- auth_context = "Available stored credentials (use actual values in commands):\n"
1295
-
1296
- for key, value in stored_credentials.items():
1297
- # Mask the actual value for security in logs, but provide the real value
1298
- masked_value = value[:8] + "..." if len(value) > 8 else "***"
1299
- auth_context += f"- {key}: {masked_value} (actual value: {value})\n"
1300
-
1301
- return auth_context
1302
-
1303
- def call_openai_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
1304
- """Call OpenAI to debug a failed command and suggest a fix"""
1305
- print("\n๐Ÿ” DEBUG: Starting LLM debugging...")
1306
- print(f"๐Ÿ” DEBUG: Command: {command}")
1307
- print(f"๐Ÿ” DEBUG: Error output length: {len(error_output) if error_output else 0}")
1308
- print(f"๐Ÿ” DEBUG: Current directory: {current_dir}")
1309
- print(f"๐Ÿ” DEBUG: Sandbox available: {sandbox is not None}")
1310
-
1311
- # Define _to_str function locally to avoid NameError
1312
- def _to_str(maybe_bytes):
1313
- try:
1314
- return (maybe_bytes.decode('utf-8') if isinstance(maybe_bytes, (bytes, bytearray)) else maybe_bytes)
1315
- except UnicodeDecodeError:
1316
- # Handle non-UTF-8 bytes by replacing invalid characters
1317
- if isinstance(maybe_bytes, (bytes, bytearray)):
1318
- return maybe_bytes.decode('utf-8', errors='replace')
1319
- else:
1320
- return str(maybe_bytes)
1321
- except Exception:
1322
- # Last resort fallback
1323
- return str(maybe_bytes)
1324
-
1325
- # Skip debugging for certain commands that commonly return non-zero exit codes
1326
- # but aren't actually errors (like test commands)
1327
- if command.strip().startswith("test "):
1328
- print("๐Ÿ” Skipping debugging for test command - non-zero exit code is expected behavior")
1329
- return None
1330
-
1331
- # Validate error_output - if it's empty, we can't debug effectively
1332
- if not error_output or not error_output.strip():
1333
- print("โš ๏ธ Error output is empty. Cannot effectively debug the command.")
1334
- print("โš ๏ธ Skipping OpenAI debugging due to lack of error information.")
1335
- return None
1336
-
1337
- # Try to get API key from multiple sources
1338
- if not api_key:
1339
- print("๐Ÿ” DEBUG: No API key provided, searching for one...")
1340
-
1341
- # First try environment variable
1342
- api_key = os.environ.get("OPENAI_API_KEY")
1343
- print(f"๐Ÿ” DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
1344
- if api_key:
1345
- print(f"๐Ÿ” DEBUG: Environment API key value: {api_key}")
1346
-
1347
- # If not in environment, try to fetch from server using fetch_modal_tokens
1348
- if not api_key:
1349
- try:
1350
- print("๐Ÿ” DEBUG: Trying to fetch API key from server...")
1351
- from fetch_modal_tokens import get_tokens
1352
- _, _, api_key, _ = get_tokens()
1353
- if api_key:
1354
- # Set in environment for this session
1355
- os.environ["OPENAI_API_KEY"] = api_key
1356
- else:
1357
- print("โš ๏ธ Could not fetch OpenAI API key from server")
1358
- except Exception as e:
1359
- print(f"โš ๏ธ Error fetching API key from server: {e}")
1360
-
1361
- # Store the API key in a persistent file if found
1362
- if api_key:
1363
- try:
1364
- os.makedirs(os.path.expanduser("~/.gitarsenal"), exist_ok=True)
1365
- with open(os.path.expanduser("~/.gitarsenal/openai_key"), "w") as f:
1366
- f.write(api_key)
1367
- print("โœ… Saved OpenAI API key for future use")
1368
- except Exception as e:
1369
- print(f"โš ๏ธ Could not save API key: {e}")
1370
-
1371
- # Try to load from saved file if not in environment
1372
- if not api_key:
1373
- try:
1374
- key_file = os.path.expanduser("~/.gitarsenal/openai_key")
1375
- print(f"๐Ÿ” DEBUG: Checking for saved API key at: {key_file}")
1376
- if os.path.exists(key_file):
1377
- with open(key_file, "r") as f:
1378
- api_key = f.read().strip()
1379
- if api_key:
1380
- print("โœ… Loaded OpenAI API key from saved file")
1381
- print(f"๐Ÿ” DEBUG: API key from file: {api_key}")
1382
- print(f"๐Ÿ” DEBUG: API key length: {len(api_key)}")
1383
- # Also set in environment for this session
1384
- os.environ["OPENAI_API_KEY"] = api_key
1385
- else:
1386
- print("๐Ÿ” DEBUG: Saved file exists but is empty")
1387
- else:
1388
- print("๐Ÿ” DEBUG: No saved API key file found")
1389
- except Exception as e:
1390
- print(f"โš ๏ธ Could not load saved API key: {e}")
1391
-
1392
- # Then try credentials manager
1393
- if not api_key:
1394
- print("๐Ÿ” DEBUG: Trying credentials manager...")
1395
- try:
1396
- from credentials_manager import CredentialsManager
1397
- credentials_manager = CredentialsManager()
1398
- api_key = credentials_manager.get_openai_api_key()
1399
- if api_key:
1400
- print(f"๐Ÿ” DEBUG: API key from credentials manager: Found")
1401
- print(f"๐Ÿ” DEBUG: Credentials manager API key value: {api_key}")
1402
- # Set in environment for this session
1403
- os.environ["OPENAI_API_KEY"] = api_key
1404
- else:
1405
- print(f"๐Ÿ” DEBUG: API key from credentials manager: Not found")
1406
- except ImportError as e:
1407
- print(f"๐Ÿ” DEBUG: Credentials manager not available: {e}")
1408
- # Fall back to direct input if credentials_manager is not available
1409
- pass
1410
-
1411
- # Finally, prompt the user if still no API key
1412
- if not api_key:
1413
- print("๐Ÿ” DEBUG: No API key found in any source, prompting user...")
1414
- print("\n" + "="*60)
1415
- print("๐Ÿ”‘ OPENAI API KEY REQUIRED FOR DEBUGGING")
1416
- print("="*60)
1417
- print("To debug failed commands, an OpenAI API key is needed.")
1418
- print("๐Ÿ“ Please paste your OpenAI API key below:")
1419
- print(" (Your input will be hidden for security)")
1420
- print("-" * 60)
1421
-
1422
- try:
1423
- api_key = getpass.getpass("OpenAI API Key: ").strip()
1424
- if not api_key:
1425
- print("โŒ No API key provided. Skipping debugging.")
1426
- return None
1427
- print("โœ… API key received successfully!")
1428
- print(f"๐Ÿ” DEBUG: User-provided API key: {api_key}")
1429
- # Save the API key to environment for future use in this session
1430
- os.environ["OPENAI_API_KEY"] = api_key
1431
- except KeyboardInterrupt:
1432
- print("\nโŒ API key input cancelled by user.")
1433
- return None
1434
- except Exception as e:
1435
- print(f"โŒ Error getting API key: {e}")
1436
- return None
1437
-
1438
- # If we still don't have an API key, we can't proceed
1439
- if not api_key:
1440
- print("โŒ No OpenAI API key available. Cannot perform LLM debugging.")
1441
- print("๐Ÿ’ก To enable LLM debugging, set the OPENAI_API_KEY environment variable")
1442
- return None
1443
-
1444
- # print(f"โœ… OpenAI API key available (length: {len(api_key)})")
1445
-
1446
- # Gather additional context to help with debugging
1447
- directory_context = ""
1448
- system_info = ""
1449
- command_history = ""
1450
- file_context = ""
1451
-
1452
- if sandbox:
1453
- try:
1454
- print("๐Ÿ” Getting system information for better debugging...")
1455
-
1456
- # Get OS information
1457
- os_info_cmd = """
1458
- echo "OS Information:"
1459
- cat /etc/os-release 2>/dev/null || echo "OS release info not available"
1460
- echo -e "\nKernel Information:"
1461
- uname -a
1462
- echo -e "\nPython Information:"
1463
- python --version
1464
- pip --version
1465
- echo -e "\nPackage Manager:"
1466
- which apt 2>/dev/null && echo "apt available" || echo "apt not available"
1467
- which yum 2>/dev/null && echo "yum available" || echo "yum not available"
1468
- which dnf 2>/dev/null && echo "dnf available" || echo "dnf not available"
1469
- which apk 2>/dev/null && echo "apk available" || echo "apk not available"
1470
- echo -e "\nEnvironment Variables:"
1471
- env | grep -E "^(PATH|PYTHON|VIRTUAL_ENV|HOME|USER|SHELL|LANG)" || echo "No relevant env vars found"
1472
- """
1473
-
1474
- os_result = sandbox.exec("bash", "-c", os_info_cmd)
1475
- os_output = ""
1476
- for line in os_result.stdout:
1477
- os_output += _to_str(line)
1478
- os_result.wait()
1479
-
1480
- system_info = f"""
1481
- System Information:
1482
- {os_output}
1483
- """
1484
- print("โœ… System information gathered successfully")
1485
- except Exception as e:
1486
- print(f"โš ๏ธ Error getting system information: {e}")
1487
- system_info = "System information not available\n"
1488
-
1489
- if current_dir and sandbox:
1490
- try:
1491
- # print("๐Ÿ” Getting directory context for better debugging...")
1492
-
1493
- # Get current directory contents
1494
- ls_result = sandbox.exec("bash", "-c", "ls -la")
1495
- ls_output = ""
1496
- for line in ls_result.stdout:
1497
- ls_output += _to_str(line)
1498
- ls_result.wait()
1499
-
1500
- # Get parent directory contents
1501
- parent_result = sandbox.exec("bash", "-c", "ls -la ../")
1502
- parent_ls = ""
1503
- for line in parent_result.stdout:
1504
- parent_ls += _to_str(line)
1505
- parent_result.wait()
1506
-
1507
- directory_context = f"""
1508
- Current directory contents:
1509
- {ls_output}
1510
-
1511
- Parent directory contents:
1512
- {parent_ls}
1513
- """
1514
- print("โœ… Directory context gathered successfully")
1515
-
1516
- # Check for relevant files that might provide additional context
1517
- # For example, if error mentions a specific file, try to get its content
1518
- relevant_files = []
1519
- error_files = re.findall(r'(?:No such file or directory|cannot open|not found): ([^\s:]+)', error_output)
1520
- if error_files:
1521
- for file_path in error_files:
1522
- # Clean up the file path
1523
- file_path = file_path.strip("'\"")
1524
- if not os.path.isabs(file_path):
1525
- file_path = os.path.join(current_dir, file_path)
1526
-
1527
- # Try to get the parent directory if the file doesn't exist
1528
- if '/' in file_path:
1529
- parent_file_dir = os.path.dirname(file_path)
1530
- relevant_files.append(parent_file_dir)
1531
-
1532
- # Look for package.json, requirements.txt, etc.
1533
- common_config_files = ["package.json", "requirements.txt", "pyproject.toml", "setup.py",
1534
- "Pipfile", "Dockerfile", "docker-compose.yml", "Makefile"]
1535
-
1536
- for config_file in common_config_files:
1537
- check_cmd = f"test -f {current_dir}/{config_file}"
1538
- check_result = sandbox.exec("bash", "-c", check_cmd)
1539
- check_result.wait()
1540
- if check_result.returncode == 0:
1541
- relevant_files.append(f"{current_dir}/{config_file}")
1542
-
1543
- # Get content of relevant files
1544
- if relevant_files:
1545
- file_context = "\nRelevant file contents:\n"
1546
- for file_path in relevant_files[:2]: # Limit to 2 files to avoid too much context
1547
- try:
1548
- file_check_cmd = f"test -f {file_path}"
1549
- file_check = sandbox.exec("bash", "-c", file_check_cmd)
1550
- file_check.wait()
1551
-
1552
- if file_check.returncode == 0:
1553
- # It's a file, get its content
1554
- cat_cmd = f"cat {file_path}"
1555
- cat_result = sandbox.exec("bash", "-c", cat_cmd)
1556
- file_content = ""
1557
- for line in cat_result.stdout:
1558
- file_content += _to_str(line)
1559
- cat_result.wait()
1560
-
1561
- # Truncate if too long
1562
- if len(file_content) > 1000:
1563
- file_content = file_content[:1000] + "\n... (truncated)"
1564
-
1565
- file_context += f"\n--- {file_path} ---\n{file_content}\n"
1566
- else:
1567
- # It's a directory, list its contents
1568
- ls_cmd = f"ls -la {file_path}"
1569
- ls_dir_result = sandbox.exec("bash", "-c", ls_cmd)
1570
- dir_content = ""
1571
- for line in ls_dir_result.stdout:
1572
- dir_content += _to_str(line)
1573
- ls_dir_result.wait()
1574
-
1575
- file_context += f"\n--- Directory: {file_path} ---\n{dir_content}\n"
1576
- except Exception as e:
1577
- print(f"โš ๏ธ Error getting content of {file_path}: {e}")
1578
-
1579
- # print(f"โœ… Additional file context gathered from {len(relevant_files)} relevant files")
1580
-
1581
- except Exception as e:
1582
- print(f"โš ๏ธ Error getting directory context: {e}")
1583
- directory_context = f"\nCurrent directory: {current_dir}\n"
1584
-
1585
- # Prepare the API request
1586
- headers = {
1587
- "Content-Type": "application/json",
1588
- "Authorization": f"Bearer {api_key}"
1589
- }
1590
-
1591
- stored_credentials = get_stored_credentials()
1592
- auth_context = generate_auth_context(stored_credentials)
1593
-
1594
- # Create a prompt for the LLM
1595
- print("\n" + "="*60)
1596
- print("DEBUG: ERROR_OUTPUT SENT TO LLM:")
1597
- print("="*60)
1598
- print(f"{error_output}")
1599
- print("="*60 + "\n")
1600
-
1601
- prompt = f"""
1602
- I'm trying to run the following command in a Linux environment:
1603
-
1604
- ```
1605
- {command}
1606
- ```
1607
-
1608
- But it failed with this error:
1609
-
1610
- ```
1611
- {error_output}
1612
- ```
1613
- {system_info}
1614
- {directory_context}
1615
- {file_context}
1616
-
1617
- AVAILABLE CREDENTIALS:
1618
- {auth_context}
1619
-
1620
- Please analyze the error and provide ONLY a single terminal command that would fix the issue.
1621
- Consider the current directory, system information, directory contents, and available credentials carefully before suggesting a solution.
1622
-
1623
- IMPORTANT GUIDELINES:
1624
- 1. For any commands that might ask for yes/no confirmation, use the appropriate non-interactive flag:
1625
- - For apt/apt-get: use -y or --yes
1626
- - For rm: use -f or --force
1627
-
1628
- 2. If the error indicates a file is not found:
1629
- - FIRST try to search for the file using: find . -name "filename" -type f 2>/dev/null
1630
- - If found, navigate to that directory using: cd /path/to/directory
1631
- - If not found, then consider creating the file or installing missing packages
1632
-
1633
- 3. For missing packages or dependencies:
1634
- - Use pip install for Python packages
1635
- - Use apt-get install -y for system packages
1636
- - Use npm install for Node.js packages
1637
-
1638
- 4. For authentication issues:
1639
- - Analyze the error to determine what type of authentication is needed
1640
- - ALWAYS use the actual credential values from the AVAILABLE CREDENTIALS section above (NOT placeholders)
1641
- - Look for the specific API key or token needed in the auth_context and use its exact value
1642
- - Common patterns:
1643
- * wandb errors: use wandb login with the actual WANDB_API_KEY value from auth_context
1644
- * huggingface errors: use huggingface-cli login with the actual HF_TOKEN or HUGGINGFACE_TOKEN value from auth_context
1645
- * github errors: configure git credentials with the actual GITHUB_TOKEN value from auth_context
1646
- * kaggle errors: create ~/.kaggle/kaggle.json with the actual KAGGLE_USERNAME and KAGGLE_KEY values from auth_context
1647
- * API errors: export the appropriate API key as environment variable using the actual value from auth_context
1648
-
1649
- 5. Environment variable exports:
1650
- - Use export commands for API keys that need to be in environment
1651
- - ALWAYS use the actual credential values from auth_context, never use placeholders like "YOUR_API_KEY"
1652
- - Example: export OPENAI_API_KEY="sk-..." (using the actual key from auth_context)
1653
-
1654
- 6. CRITICAL: When using any API key, token, or credential:
1655
- - Find the exact value in the AVAILABLE CREDENTIALS section
1656
- - Use that exact value in your command
1657
- - Do not use generic placeholders or dummy values
1658
- - The auth_context contains real, usable credentials
1659
-
1660
- 7. For Git SSH authentication failures:
1661
- - If the error contains "Host key verification failed" or "Could not read from remote repository"
1662
- - ALWAYS convert SSH URLs to HTTPS URLs for public repositories
1663
- - Replace git@github.com:username/repo.git with https://github.com/username/repo.git
1664
- - This works for public repositories without authentication
1665
- - Example: git clone https://github.com/xg-chu/ARTalk.git
1666
-
1667
- Do not provide any explanations, just the exact command to run.
1668
- """
1669
-
1670
- # Prepare the API request payload
1671
- # print("๐Ÿ” DEBUG: Preparing API request...")
1672
-
1673
- # Try to use GPT-4 first, but fall back to other models if needed
1674
- models_to_try = [
1675
- "gpt-4o-mini", # First choice: GPT-4o (most widely available)
1676
- ]
1677
-
1678
- # Check if we have a preferred model in environment
1679
- preferred_model = os.environ.get("OPENAI_MODEL")
1680
- if preferred_model:
1681
- # Insert the preferred model at the beginning of the list
1682
- models_to_try.insert(0, preferred_model)
1683
- # print(f"โœ… Using preferred model from environment: {preferred_model}")
1684
-
1685
- # Remove duplicates while preserving order
1686
- models_to_try = list(dict.fromkeys(models_to_try))
1687
- # print(f"๐Ÿ” DEBUG: Models to try: {models_to_try}")
1688
-
1689
- # Function to make the API call with a specific model
1690
- def try_api_call(model_name, retries=2, backoff_factor=1.5):
1691
- # print(f"๐Ÿ” DEBUG: Attempting API call with model: {model_name}")
1692
- # print(f"๐Ÿ” DEBUG: API key available: {'Yes' if api_key else 'No'}")
1693
- # if api_key:
1694
- # print(f"๐Ÿ” DEBUG: API key length: {len(api_key)}")
1695
- # print(f"๐Ÿ” DEBUG: API key starts with: {api_key[:10]}...")
1696
-
1697
- payload = {
1698
- "model": model_name,
1699
- "messages": [
1700
- {"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."},
1701
- {"role": "user", "content": prompt}
1702
- ],
1703
- "temperature": 0.2,
1704
- "max_tokens": 300
1705
- }
1706
-
1707
- print(f"๐Ÿ” DEBUG: Payload prepared, prompt length: {len(prompt)}")
1708
-
1709
- # Add specific handling for common errors
1710
- last_error = None
1711
- for attempt in range(retries + 1):
1712
- try:
1713
- if attempt > 0:
1714
- # Exponential backoff
1715
- wait_time = backoff_factor * (2 ** (attempt - 1))
1716
- print(f"โฑ๏ธ Retrying in {wait_time:.1f} seconds... (attempt {attempt+1}/{retries+1})")
1717
- time.sleep(wait_time)
1718
-
1719
- print(f"๐Ÿค– Calling OpenAI with {model_name} model to debug the failed command...")
1720
- print(f"๐Ÿ” DEBUG: Making POST request to OpenAI API...")
1721
- response = requests.post(
1722
- "https://api.openai.com/v1/chat/completions",
1723
- headers=headers,
1724
- json=payload,
1725
- timeout=45 # Increased timeout for reliability
1726
- )
1727
-
1728
- print(f"๐Ÿ” DEBUG: Response received, status code: {response.status_code}")
1729
-
1730
- # Handle specific status codes
1731
- if response.status_code == 200:
1732
- print(f"๐Ÿ” DEBUG: Success! Response length: {len(response.text)}")
1733
- return response.json(), None
1734
- elif response.status_code == 401:
1735
- error_msg = "Authentication error: Invalid API key"
1736
- print(f"โŒ {error_msg}")
1737
- print(f"๐Ÿ” DEBUG: Response text: {response.text}")
1738
- # Don't retry auth errors
1739
- return None, error_msg
1740
- elif response.status_code == 429:
1741
- error_msg = "Rate limit exceeded or quota reached"
1742
- print(f"โš ๏ธ {error_msg}")
1743
- print(f"๐Ÿ” DEBUG: Response text: {response.text}")
1744
- # Always retry rate limit errors with increasing backoff
1745
- last_error = error_msg
1746
- continue
1747
- elif response.status_code == 500:
1748
- error_msg = "OpenAI server error"
1749
- print(f"โš ๏ธ {error_msg}")
1750
- print(f"๐Ÿ” DEBUG: Response text: {response.text}")
1751
- # Retry server errors
1752
- last_error = error_msg
1753
- continue
1754
- else:
1755
- error_msg = f"Status code: {response.status_code}, Response: {response.text}"
1756
- print(f"โš ๏ธ OpenAI API error: {error_msg}")
1757
- print(f"๐Ÿ” DEBUG: Full response text: {response.text}")
1758
- last_error = error_msg
1759
- # Only retry if we have attempts left
1760
- if attempt < retries:
1761
- continue
1762
- return None, error_msg
1763
- except requests.exceptions.Timeout:
1764
- error_msg = "Request timed out"
1765
- # print(f"โš ๏ธ {error_msg}")
1766
- # print(f"๐Ÿ” DEBUG: Timeout after 45 seconds")
1767
- last_error = error_msg
1768
- # Always retry timeouts
1769
- continue
1770
- except requests.exceptions.ConnectionError:
1771
- error_msg = "Connection error"
1772
- print(f"โš ๏ธ {error_msg}")
1773
- print(f"๐Ÿ” DEBUG: Connection failed to api.openai.com")
1774
- last_error = error_msg
1775
- # Always retry connection errors
1776
- continue
1777
- except Exception as e:
1778
- error_msg = str(e)
1779
- print(f"โš ๏ธ Unexpected error: {error_msg}")
1780
- print(f"๐Ÿ” DEBUG: Exception type: {type(e).__name__}")
1781
- print(f"๐Ÿ” DEBUG: Exception details: {str(e)}")
1782
- last_error = error_msg
1783
- # Only retry if we have attempts left
1784
- if attempt < retries:
1785
- continue
1786
- return None, error_msg
1787
-
1788
- # If we get here, all retries failed
1789
- return None, last_error
1790
-
1791
- # Try each model in sequence until one works
1792
- result = None
1793
- last_error = None
1794
-
1795
- for model in models_to_try:
1796
- result, error = try_api_call(model)
1797
- if result:
1798
- # print(f"โœ… Successfully got response from {model}")
1799
- break
1800
- else:
1801
- print(f"โš ๏ธ Failed to get response from {model}: {error}")
1802
- last_error = error
1803
-
1804
- if not result:
1805
- print(f"โŒ All model attempts failed. Last error: {last_error}")
1806
- return None
1807
-
1808
- # Process the response
1809
- try:
1810
- print(f"๐Ÿ” DEBUG: Processing OpenAI response...")
1811
- # print(f"๐Ÿ” DEBUG: Response structure: {list(result.keys())}")
1812
- print(f"๐Ÿ” DEBUG: Choices count: {len(result.get('choices', []))}")
1813
-
1814
- fix_command = result["choices"][0]["message"]["content"].strip()
1815
- print(f"๐Ÿ” DEBUG: Raw response content: {fix_command}")
1816
-
1817
- # Save the original response for debugging
1818
- original_response = fix_command
1819
-
1820
- # Extract just the command if it's wrapped in backticks or explanation
1821
- if "```" in fix_command:
1822
- # Extract content between backticks
1823
- import re
1824
- code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
1825
- if code_blocks:
1826
- fix_command = code_blocks[0].strip()
1827
- print(f"โœ… Extracted command from code block: {fix_command}")
1828
-
1829
- # If the response still has explanatory text, try to extract just the command
1830
- if len(fix_command.split('\n')) > 1:
1831
- # First try to find lines that look like commands (start with common command prefixes)
1832
- command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
1833
- 'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
1834
- 'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
1835
- 'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
1836
-
1837
- # Check for lines that start with common command prefixes
1838
- command_lines = [line.strip() for line in fix_command.split('\n')
1839
- if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
1840
-
1841
- if command_lines:
1842
- # Use the first command line found
1843
- fix_command = command_lines[0]
1844
- print(f"โœ… Identified command by prefix: {fix_command}")
1845
- else:
1846
- # Try to find lines that look like commands (contain common shell patterns)
1847
- shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
1848
- command_lines = [line.strip() for line in fix_command.split('\n')
1849
- if any(pattern in line for pattern in shell_patterns)]
1850
-
1851
- if command_lines:
1852
- # Use the first command line found
1853
- fix_command = command_lines[0]
1854
- print(f"โœ… Identified command by shell pattern: {fix_command}")
1855
- else:
1856
- # Fall back to the shortest non-empty line as it's likely the command
1857
- lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
1858
- if lines:
1859
- # Exclude very short lines that are likely not commands
1860
- valid_lines = [line for line in lines if len(line) > 5]
1861
- if valid_lines:
1862
- fix_command = min(valid_lines, key=len)
1863
- else:
1864
- fix_command = min(lines, key=len)
1865
- print(f"โœ… Selected shortest line as command: {fix_command}")
1866
-
1867
- # Clean up the command - remove any trailing periods or quotes
1868
- fix_command = fix_command.rstrip('.;"\'')
1869
-
1870
- # Remove common prefixes that LLMs sometimes add
1871
- prefixes_to_remove = [
1872
- "Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
1873
- "You should run: ", "You can run: ", "You need to run: "
1874
- ]
1875
- for prefix in prefixes_to_remove:
1876
- if fix_command.startswith(prefix):
1877
- fix_command = fix_command[len(prefix):].strip()
1878
- print(f"โœ… Removed prefix: {prefix}")
1879
- break
1880
-
1881
- # If the command is still multi-line or very long, it might not be a valid command
1882
- if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
1883
- print("โš ๏ธ Extracted command appears invalid (multi-line or too long)")
1884
- print("๐Ÿ” Original response from LLM:")
1885
- print("-" * 60)
1886
- print(original_response)
1887
- print("-" * 60)
1888
- print("โš ๏ธ Using best guess for command")
1889
-
1890
- print(f"๐Ÿ”ง Suggested fix: {fix_command}")
1891
- print(f"๐Ÿ” DEBUG: Returning fix command: {fix_command}")
1892
- return fix_command
1893
- except Exception as e:
1894
- print(f"โŒ Error processing OpenAI response: {e}")
1895
- print(f"๐Ÿ” DEBUG: Exception type: {type(e).__name__}")
1896
- print(f"๐Ÿ” DEBUG: Exception details: {str(e)}")
1897
- return None
1898
-
1899
- def call_openai_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
1900
- """Call OpenAI to debug multiple failed commands and suggest fixes for all of them at once"""
1901
- print("\n๐Ÿ” DEBUG: Starting batch LLM debugging...")
1902
- print(f"๐Ÿ” DEBUG: Analyzing {len(failed_commands)} failed commands")
1903
-
1904
- if not failed_commands:
1905
- print("โš ๏ธ No failed commands to analyze")
1906
- return []
1907
-
1908
- if not api_key:
1909
- print("โŒ No OpenAI API key provided for batch debugging")
1910
- return []
1911
-
1912
- # Prepare context for batch analysis
1913
- context_parts = []
1914
- context_parts.append(f"Current directory: {current_dir}")
1915
- context_parts.append(f"Sandbox available: {sandbox is not None}")
1916
-
1917
- # Add failed commands with their errors
1918
- for i, failed_cmd in enumerate(failed_commands, 1):
1919
- cmd_type = failed_cmd.get('type', 'main')
1920
- original_cmd = failed_cmd.get('original_command', '')
1921
- cmd_text = failed_cmd['command']
1922
- stderr = failed_cmd.get('stderr', '')
1923
- stdout = failed_cmd.get('stdout', '')
1924
-
1925
- context_parts.append(f"\n--- Failed Command {i} ({cmd_type}) ---")
1926
- context_parts.append(f"Command: {cmd_text}")
1927
- if original_cmd and original_cmd != cmd_text:
1928
- context_parts.append(f"Original Command: {original_cmd}")
1929
- if stderr:
1930
- context_parts.append(f"Error Output: {stderr}")
1931
- if stdout:
1932
- context_parts.append(f"Standard Output: {stdout}")
1933
-
1934
- # Create the prompt for batch analysis
1935
- prompt = f"""You are a debugging assistant analyzing multiple failed commands.
1936
-
1937
- Context:
1938
- {chr(10).join(context_parts)}
1939
-
1940
- Please analyze each failed command and provide a fix command for each one. For each failed command, respond with:
1941
-
1942
- FIX_COMMAND_{i}: <the fix command>
1943
- REASON_{i}: <brief explanation of why the original command failed and how the fix addresses it>
1944
-
1945
- Guidelines:
1946
- - For file not found errors, first search for the file using 'find . -name filename -type f'
1947
- - For missing packages, use appropriate package managers (pip, apt-get, npm)
1948
- - For Git SSH authentication failures, convert SSH URLs to HTTPS URLs
1949
- - For permission errors, suggest commands with sudo if appropriate
1950
- - For network issues, suggest retry commands or alternative URLs
1951
- - Keep each fix command simple and focused on the specific error
1952
-
1953
- Provide fixes for all {len(failed_commands)} failed commands:"""
1954
-
1955
- # Make the API call
1956
- headers = {
1957
- "Authorization": f"Bearer {api_key}",
1958
- "Content-Type": "application/json"
1959
- }
1960
-
1961
- payload = {
1962
- "model": "gpt-4o-mini", # Use a more capable model for batch analysis
1963
- "messages": [
1964
- {"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."},
1965
- {"role": "user", "content": prompt}
1966
- ],
1967
- "temperature": 0.1,
1968
- "max_tokens": 1000
1969
- }
1970
-
1971
- try:
1972
- print(f"๐Ÿค– Calling OpenAI for batch debugging of {len(failed_commands)} commands...")
1973
- response = requests.post(
1974
- "https://api.openai.com/v1/chat/completions",
1975
- headers=headers,
1976
- json=payload,
1977
- timeout=60
1978
- )
1979
-
1980
- if response.status_code == 200:
1981
- result = response.json()
1982
- content = result['choices'][0]['message']['content']
1983
- print(f"โœ… Batch analysis completed")
1984
-
1985
- # Parse the response to extract fix commands
1986
- fixes = []
1987
- for i in range(1, len(failed_commands) + 1):
1988
- fix_pattern = f"FIX_COMMAND_{i}: (.+)"
1989
- reason_pattern = f"REASON_{i}: (.+)"
1990
-
1991
- fix_match = re.search(fix_pattern, content, re.MULTILINE)
1992
- reason_match = re.search(reason_pattern, content, re.MULTILINE)
1993
-
1994
- if fix_match:
1995
- fix_command = fix_match.group(1).strip()
1996
- reason = reason_match.group(1).strip() if reason_match else "LLM suggested fix"
1997
-
1998
- # Clean up the fix command
1999
- if fix_command.startswith('`') and fix_command.endswith('`'):
2000
- fix_command = fix_command[1:-1]
2001
-
2002
- fixes.append({
2003
- 'original_command': failed_commands[i-1]['command'],
2004
- 'fix_command': fix_command,
2005
- 'reason': reason,
2006
- 'command_index': i-1
2007
- })
2008
-
2009
- print(f"๐Ÿ”ง Generated {len(fixes)} fix commands from batch analysis")
2010
- return fixes
2011
- else:
2012
- print(f"โŒ OpenAI API error: {response.status_code} - {response.text}")
2013
- return []
2014
-
2015
- except Exception as e:
2016
- print(f"โŒ Error during batch debugging: {e}")
2017
- return []
2018
-
2019
- def call_anthropic_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
2020
- """Call Anthropic Claude to debug a failed command and suggest a fix"""
2021
- print("\n๐Ÿ” DEBUG: Starting Anthropic Claude debugging...")
2022
- print(f"๐Ÿ” DEBUG: Command: {command}")
2023
- print(f"๐Ÿ” DEBUG: Error output length: {len(error_output) if error_output else 0}")
2024
- print(f"๐Ÿ” DEBUG: Current directory: {current_dir}")
2025
- print(f"๐Ÿ” DEBUG: Sandbox available: {sandbox is not None}")
2026
-
2027
- # Define _to_str function locally to avoid NameError
2028
- def _to_str(maybe_bytes):
2029
- try:
2030
- return (maybe_bytes.decode('utf-8') if isinstance(maybe_bytes, (bytes, bytearray)) else maybe_bytes)
2031
- except UnicodeDecodeError:
2032
- # Handle non-UTF-8 bytes by replacing invalid characters
2033
- if isinstance(maybe_bytes, (bytes, bytearray)):
2034
- return maybe_bytes.decode('utf-8', errors='replace')
2035
- else:
2036
- return str(maybe_bytes)
2037
- except Exception:
2038
- # Last resort fallback
2039
- return str(maybe_bytes)
2040
-
2041
- # Skip debugging for certain commands that commonly return non-zero exit codes
2042
- # but aren't actually errors (like test commands)
2043
- if command.strip().startswith("test "):
2044
- print("๐Ÿ” Skipping debugging for test command - non-zero exit code is expected behavior")
2045
- return None
2046
-
2047
- # Validate error_output - if it's empty, we can't debug effectively
2048
- if not error_output or not error_output.strip():
2049
- print("โš ๏ธ Error output is empty. Cannot effectively debug the command.")
2050
- print("โš ๏ธ Skipping Anthropic debugging due to lack of error information.")
2051
- return None
2052
-
2053
- # Try to get API key from multiple sources
2054
- if not api_key:
2055
- print("๐Ÿ” DEBUG: No Anthropic API key provided, searching for one...")
2056
-
2057
- # First try environment variable
2058
- api_key = os.environ.get("ANTHROPIC_API_KEY")
2059
- print(f"๐Ÿ” DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
2060
- if api_key:
2061
- print(f"๐Ÿ” DEBUG: Environment API key value: {api_key}")
2062
-
2063
- # If not in environment, try to fetch from server using fetch_modal_tokens
2064
- if not api_key:
2065
- try:
2066
- print("๐Ÿ” DEBUG: Trying to fetch API key from server...")
2067
- from fetch_modal_tokens import get_tokens
2068
- _, _, _, api_key = get_tokens()
2069
- if api_key:
2070
- # Set in environment for this session
2071
- os.environ["ANTHROPIC_API_KEY"] = api_key
2072
- else:
2073
- print("โš ๏ธ Could not fetch Anthropic API key from server")
2074
- except Exception as e:
2075
- print(f"โš ๏ธ Error fetching API key from server: {e}")
2076
-
2077
- # Then try credentials manager
2078
- if not api_key:
2079
- print("๐Ÿ” DEBUG: Trying credentials manager...")
2080
- try:
2081
- from credentials_manager import CredentialsManager
2082
- credentials_manager = CredentialsManager()
2083
- api_key = credentials_manager.get_anthropic_api_key()
2084
- if api_key:
2085
- print(f"๐Ÿ” DEBUG: API key from credentials manager: Found")
2086
- print(f"๐Ÿ” DEBUG: Credentials manager API key value: {api_key}")
2087
- # Set in environment for this session
2088
- os.environ["ANTHROPIC_API_KEY"] = api_key
2089
- else:
2090
- print("โš ๏ธ Could not fetch Anthropic API key from credentials manager")
2091
- except Exception as e:
2092
- print(f"โš ๏ธ Error fetching API key from credentials manager: {e}")
2093
-
2094
- # Store the API key in a persistent file if found
2095
- if api_key:
2096
- try:
2097
- os.makedirs(os.path.expanduser("~/.gitarsenal"), exist_ok=True)
2098
- with open(os.path.expanduser("~/.gitarsenal/anthropic_key"), "w") as f:
2099
- f.write(api_key)
2100
- print("โœ… Saved Anthropic API key for future use")
2101
- except Exception as e:
2102
- print(f"โš ๏ธ Could not save API key: {e}")
2103
-
2104
- # Try to load from saved file if not in environment
2105
- if not api_key:
2106
- try:
2107
- key_file = os.path.expanduser("~/.gitarsenal/anthropic_key")
2108
- print(f"๐Ÿ” DEBUG: Checking for saved API key at: {key_file}")
2109
- if os.path.exists(key_file):
2110
- with open(key_file, "r") as f:
2111
- api_key = f.read().strip()
2112
- if api_key:
2113
- print("โœ… Loaded Anthropic API key from saved file")
2114
- print(f"๐Ÿ” DEBUG: API key from file: {api_key}")
2115
- print(f"๐Ÿ” DEBUG: API key length: {len(api_key)}")
2116
- # Also set in environment for this session
2117
- os.environ["ANTHROPIC_API_KEY"] = api_key
2118
- else:
2119
- print("๐Ÿ” DEBUG: Saved file exists but is empty")
2120
- else:
2121
- print("๐Ÿ” DEBUG: No saved API key file found")
2122
- except Exception as e:
2123
- print(f"โš ๏ธ Could not load saved API key: {e}")
2124
-
2125
- if not api_key:
2126
- print("โŒ No Anthropic API key available for debugging")
2127
- return None
2128
-
2129
- # Prepare the prompt for debugging
2130
- error_str = _to_str(error_output)
2131
- prompt = f"""You are a debugging assistant. Provide only the terminal command to fix the issue.
2132
-
2133
- Context:
2134
- - Current directory: {current_dir}
2135
- - Sandbox available: {sandbox is not None}
2136
- - Failed command: {command}
2137
- - Error output: {error_str}
2138
-
2139
- Analyze the issue first, understand why it's happening, then provide the command to fix it.
2140
-
2141
- Guidelines:
2142
- - For file not found errors, first search for the file using 'find . -name filename -type f' and navigate to the directory if found
2143
- - For missing packages, use appropriate package managers (pip, apt-get, npm)
2144
- - For Git SSH authentication failures, always convert SSH URLs to HTTPS URLs (git@github.com:user/repo.git -> https://github.com/user/repo.git)
2145
- - For authentication, suggest login commands with placeholders
2146
- - For permission errors, suggest commands with sudo if appropriate
2147
- - For network issues, suggest retry commands or alternative URLs
2148
-
2149
- Return only the command to fix the issue, nothing else."""
2150
-
2151
- # Set up headers for Anthropic API
2152
- headers = {
2153
- "x-api-key": api_key,
2154
- "anthropic-version": "2023-06-01",
2155
- "content-type": "application/json"
2156
- }
2157
-
2158
- # Models to try in order of preference
2159
- models_to_try = ["claude-4-sonnet"]
2160
-
2161
- def try_api_call(model_name, retries=2, backoff_factor=1.5):
2162
- payload = {
2163
- "model": model_name,
2164
- "max_tokens": 300,
2165
- "messages": [
2166
- {"role": "user", "content": prompt}
2167
- ]
2168
- }
2169
-
2170
- print(f"๐Ÿ” DEBUG: Payload prepared, prompt length: {len(prompt)}")
2171
-
2172
- # Add specific handling for common errors
2173
- last_error = None
2174
- for attempt in range(retries + 1):
2175
- try:
2176
- if attempt > 0:
2177
- # Exponential backoff
2178
- wait_time = backoff_factor * (2 ** (attempt - 1))
2179
- print(f"โฑ๏ธ Retrying in {wait_time:.1f} seconds... (attempt {attempt+1}/{retries+1})")
2180
- time.sleep(wait_time)
2181
-
2182
- print(f"๐Ÿค– Calling Anthropic Claude with {model_name} model to debug the failed command...")
2183
- print(f"๐Ÿ” DEBUG: Making POST request to Anthropic API...")
2184
- response = requests.post(
2185
- "https://api.anthropic.com/v1/messages",
2186
- headers=headers,
2187
- json=payload,
2188
- timeout=45 # Increased timeout for reliability
2189
- )
2190
-
2191
- print(f"๐Ÿ” DEBUG: Response received, status code: {response.status_code}")
2192
-
2193
- # Handle specific status codes
2194
- if response.status_code == 200:
2195
- print(f"๐Ÿ” DEBUG: Success! Response length: {len(response.text)}")
2196
- return response.json(), None
2197
- elif response.status_code == 401:
2198
- error_msg = "Authentication error: Invalid API key"
2199
- print(f"โŒ {error_msg}")
2200
- print(f"๐Ÿ” DEBUG: Response text: {response.text}")
2201
- # Don't retry auth errors
2202
- return None, error_msg
2203
- elif response.status_code == 429:
2204
- error_msg = "Rate limit exceeded or quota reached"
2205
- print(f"โš ๏ธ {error_msg}")
2206
- print(f"๐Ÿ” DEBUG: Response text: {response.text}")
2207
- # Always retry rate limit errors with increasing backoff
2208
- last_error = error_msg
2209
- continue
2210
- elif response.status_code == 500:
2211
- error_msg = "Anthropic server error"
2212
- print(f"โš ๏ธ {error_msg}")
2213
- print(f"๐Ÿ” DEBUG: Response text: {response.text}")
2214
- # Retry server errors
2215
- last_error = error_msg
2216
- continue
2217
- else:
2218
- error_msg = f"Status code: {response.status_code}, Response: {response.text}"
2219
- print(f"โš ๏ธ Anthropic API error: {error_msg}")
2220
- print(f"๐Ÿ” DEBUG: Full response text: {response.text}")
2221
- last_error = error_msg
2222
- # Only retry if we have attempts left
2223
- if attempt < retries:
2224
- continue
2225
- return None, error_msg
2226
- except requests.exceptions.Timeout:
2227
- error_msg = "Request timed out"
2228
- last_error = error_msg
2229
- # Always retry timeouts
2230
- continue
2231
- except requests.exceptions.ConnectionError:
2232
- error_msg = "Connection error"
2233
- print(f"โš ๏ธ {error_msg}")
2234
- print(f"๐Ÿ” DEBUG: Connection failed to api.anthropic.com")
2235
- last_error = error_msg
2236
- # Always retry connection errors
2237
- continue
2238
- except Exception as e:
2239
- error_msg = str(e)
2240
- print(f"โš ๏ธ Unexpected error: {error_msg}")
2241
- print(f"๐Ÿ” DEBUG: Exception type: {type(e).__name__}")
2242
- print(f"๐Ÿ” DEBUG: Exception details: {str(e)}")
2243
- last_error = error_msg
2244
- # Only retry if we have attempts left
2245
- if attempt < retries:
2246
- continue
2247
- return None, error_msg
2248
-
2249
- # If we get here, all retries failed
2250
- return None, last_error
2251
-
2252
- # Try each model in sequence until one works
2253
- result = None
2254
- last_error = None
2255
-
2256
- for model in models_to_try:
2257
- result, error = try_api_call(model)
2258
- if result:
2259
- break
2260
- else:
2261
- print(f"โš ๏ธ Failed to get response from {model}: {error}")
2262
- last_error = error
2263
-
2264
- if not result:
2265
- print(f"โŒ All model attempts failed. Last error: {last_error}")
2266
- return None
2267
-
2268
- # Process the response
2269
- try:
2270
- print(f"๐Ÿ” DEBUG: Processing Anthropic response...")
2271
- print(f"๐Ÿ” DEBUG: Choices count: {len(result.get('content', []))}")
2272
-
2273
- fix_command = result["content"][0]["text"].strip()
2274
- print(f"๐Ÿ” DEBUG: Raw response content: {fix_command}")
2275
-
2276
- # Save the original response for debugging
2277
- original_response = fix_command
2278
-
2279
- # Extract just the command if it's wrapped in backticks or explanation
2280
- if "```" in fix_command:
2281
- # Extract content between backticks
2282
- import re
2283
- code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
2284
- if code_blocks:
2285
- fix_command = code_blocks[0].strip()
2286
- print(f"โœ… Extracted command from code block: {fix_command}")
2287
-
2288
- # If the response still has explanatory text, try to extract just the command
2289
- if len(fix_command.split('\n')) > 1:
2290
- # First try to find lines that look like commands (start with common command prefixes)
2291
- command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
2292
- 'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
2293
- 'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
2294
- 'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
2295
-
2296
- # Check for lines that start with common command prefixes
2297
- command_lines = [line.strip() for line in fix_command.split('\n')
2298
- if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
2299
-
2300
- if command_lines:
2301
- # Use the first command line found
2302
- fix_command = command_lines[0]
2303
- print(f"โœ… Identified command by prefix: {fix_command}")
2304
- else:
2305
- # Try to find lines that look like commands (contain common shell patterns)
2306
- shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
2307
- command_lines = [line.strip() for line in fix_command.split('\n')
2308
- if any(pattern in line for pattern in shell_patterns)]
2309
-
2310
- if command_lines:
2311
- # Use the first command line found
2312
- fix_command = command_lines[0]
2313
- print(f"โœ… Identified command by shell pattern: {fix_command}")
2314
- else:
2315
- # Fall back to the shortest non-empty line as it's likely the command
2316
- lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
2317
- if lines:
2318
- # Exclude very short lines that are likely not commands
2319
- valid_lines = [line for line in lines if len(line) > 5]
2320
- if valid_lines:
2321
- fix_command = min(valid_lines, key=len)
2322
- else:
2323
- fix_command = min(lines, key=len)
2324
- print(f"โœ… Selected shortest line as command: {fix_command}")
2325
-
2326
- # Clean up the command - remove any trailing periods or quotes
2327
- fix_command = fix_command.rstrip('.;"\'')
2328
-
2329
- # Remove common prefixes that LLMs sometimes add
2330
- prefixes_to_remove = [
2331
- "Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
2332
- "You should run: ", "You can run: ", "You need to run: "
2333
- ]
2334
- for prefix in prefixes_to_remove:
2335
- if fix_command.startswith(prefix):
2336
- fix_command = fix_command[len(prefix):].strip()
2337
- print(f"โœ… Removed prefix: {prefix}")
2338
- break
2339
-
2340
- # If the command is still multi-line or very long, it might not be a valid command
2341
- if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
2342
- print("โš ๏ธ Extracted command appears invalid (multi-line or too long)")
2343
- print("๐Ÿ” Original response from LLM:")
2344
- print("-" * 60)
2345
- print(original_response)
2346
- print("-" * 60)
2347
- print("โš ๏ธ Using best guess for command")
2348
-
2349
- print(f"๐Ÿ”ง Suggested fix: {fix_command}")
2350
- print(f"๐Ÿ” DEBUG: Returning fix command: {fix_command}")
2351
- return fix_command
2352
- except Exception as e:
2353
- print(f"โŒ Error processing Anthropic response: {e}")
2354
- print(f"๐Ÿ” DEBUG: Exception type: {type(e).__name__}")
2355
- print(f"๐Ÿ” DEBUG: Exception details: {str(e)}")
2356
- return None
2357
-
2358
- def switch_to_anthropic_models():
2359
- """Switch the debugging system to use Anthropic Claude models instead of OpenAI"""
2360
- print("\n๐Ÿ”„ Switching to Anthropic Claude models for debugging...")
2361
-
2362
- # Set environment variable to indicate Anthropic preference
2363
- os.environ["GITARSENAL_DEBUG_MODEL"] = "anthropic"
2364
-
2365
- # Try to get Anthropic API key
2366
- try:
2367
- from credentials_manager import CredentialsManager
2368
- credentials_manager = CredentialsManager()
2369
- api_key = credentials_manager.get_anthropic_api_key()
2370
- if api_key:
2371
- os.environ["ANTHROPIC_API_KEY"] = api_key
2372
- print("โœ… Anthropic API key configured")
2373
- print("โœ… Debugging will now use Anthropic Claude models")
2374
- return True
2375
- else:
2376
- print("โš ๏ธ No Anthropic API key found")
2377
- print("๐Ÿ’ก You can set your Anthropic API key using:")
2378
- print(" export ANTHROPIC_API_KEY='your-key'")
2379
- print(" Or run the credentials manager to set it up")
2380
- return False
2381
- except Exception as e:
2382
- print(f"โŒ Error configuring Anthropic: {e}")
2383
- return False
2384
-
2385
- def switch_to_openai_models():
2386
- """Switch the debugging system to use OpenAI models (default)"""
2387
- print("\n๐Ÿ”„ Switching to OpenAI models for debugging...")
2388
-
2389
- # Set environment variable to indicate OpenAI preference
2390
- os.environ["GITARSENAL_DEBUG_MODEL"] = "openai"
2391
-
2392
- # Try to get OpenAI API key
2393
- try:
2394
- from credentials_manager import CredentialsManager
2395
- credentials_manager = CredentialsManager()
2396
- api_key = credentials_manager.get_openai_api_key()
2397
- if api_key:
2398
- os.environ["OPENAI_API_KEY"] = api_key
2399
- print("โœ… OpenAI API key configured")
2400
- print("โœ… Debugging will now use OpenAI models")
2401
- return True
2402
- else:
2403
- print("โš ๏ธ No OpenAI API key found")
2404
- print("๐Ÿ’ก You can set your OpenAI API key using:")
2405
- print(" export OPENAI_API_KEY='your-key'")
2406
- print(" Or run the credentials manager to set it up")
2407
- return False
2408
- except Exception as e:
2409
- print(f"โŒ Error configuring OpenAI: {e}")
2410
- return False
2411
-
2412
- def get_current_debug_model():
2413
- """Get the currently configured debugging model preference"""
2414
- return os.environ.get("GITARSENAL_DEBUG_MODEL", "anthropic")
2415
-
2416
- def call_llm_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
2417
- """Unified function to call LLM for debugging - routes to OpenAI or Anthropic based on configuration"""
2418
- current_model = get_current_debug_model()
2419
-
2420
- print(f"๐Ÿ” DEBUG: Using {current_model.upper()} for debugging...")
2421
-
2422
- if current_model == "anthropic":
2423
- # Try to get Anthropic API key if not provided
2424
- if not api_key:
2425
- # First try environment variable
2426
- api_key = os.environ.get("ANTHROPIC_API_KEY")
2427
-
2428
- # If not in environment, try to fetch from server using fetch_modal_tokens
2429
- if not api_key:
2430
- try:
2431
- from fetch_modal_tokens import get_tokens
2432
- _, _, _, api_key = get_tokens()
2433
- except Exception as e:
2434
- print(f"โš ๏ธ Error fetching Anthropic API key from server: {e}")
2435
-
2436
- # Then try credentials manager
2437
- if not api_key:
2438
- try:
2439
- from credentials_manager import CredentialsManager
2440
- credentials_manager = CredentialsManager()
2441
- api_key = credentials_manager.get_anthropic_api_key()
2442
- except Exception as e:
2443
- print(f"โš ๏ธ Error getting Anthropic API key from credentials manager: {e}")
2444
-
2445
- return call_anthropic_for_debug(command, error_output, api_key, current_dir, sandbox)
2446
- else:
2447
- # Default to OpenAI
2448
- # Try to get OpenAI API key if not provided
2449
- if not api_key:
2450
- # First try environment variable
2451
- api_key = os.environ.get("OPENAI_API_KEY")
2452
-
2453
- # If not in environment, try to fetch from server using fetch_modal_tokens
2454
- if not api_key:
2455
- try:
2456
- from fetch_modal_tokens import get_tokens
2457
- _, _, api_key, _ = get_tokens()
2458
- except Exception as e:
2459
- print(f"โš ๏ธ Error fetching OpenAI API key from server: {e}")
2460
-
2461
- # Then try credentials manager
2462
- if not api_key:
2463
- try:
2464
- from credentials_manager import CredentialsManager
2465
- credentials_manager = CredentialsManager()
2466
- api_key = credentials_manager.get_openai_api_key()
2467
- except Exception as e:
2468
- print(f"โš ๏ธ Error getting OpenAI API key from credentials manager: {e}")
2469
-
2470
- return call_openai_for_debug(command, error_output, api_key, current_dir, sandbox)
45
+ # Import the fetch_modal_tokens module
46
+ # print("๐Ÿ”„ Fetching tokens from proxy server...")
47
+ from fetch_modal_tokens import get_tokens
48
+ token_id, token_secret, openai_api_key, _ = get_tokens()
2471
49
 
2472
- def call_llm_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
2473
- """Unified function to call LLM for batch debugging - routes to OpenAI or Anthropic based on configuration"""
2474
- current_model = get_current_debug_model()
2475
-
2476
- print(f"๐Ÿ” DEBUG: Using {current_model.upper()} for batch debugging...")
2477
-
2478
- if current_model == "anthropic":
2479
- # Try to get Anthropic API key if not provided
2480
- if not api_key:
2481
- # First try environment variable
2482
- api_key = os.environ.get("ANTHROPIC_API_KEY")
2483
-
2484
- # If not in environment, try to fetch from server using fetch_modal_tokens
2485
- if not api_key:
2486
- try:
2487
- from fetch_modal_tokens import get_tokens
2488
- _, _, _, api_key = get_tokens()
2489
- except Exception as e:
2490
- print(f"โš ๏ธ Error fetching Anthropic API key from server: {e}")
2491
-
2492
- # Then try credentials manager
2493
- if not api_key:
2494
- try:
2495
- from credentials_manager import CredentialsManager
2496
- credentials_manager = CredentialsManager()
2497
- api_key = credentials_manager.get_anthropic_api_key()
2498
- except Exception as e:
2499
- print(f"โš ๏ธ Error getting Anthropic API key from credentials manager: {e}")
2500
-
2501
- return call_anthropic_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
2502
- else:
2503
- # Default to OpenAI
2504
- # Try to get OpenAI API key if not provided
2505
- if not api_key:
2506
- # First try environment variable
2507
- api_key = os.environ.get("OPENAI_API_KEY")
2508
-
2509
- # If not in environment, try to fetch from server using fetch_modal_tokens
2510
- if not api_key:
2511
- try:
2512
- from fetch_modal_tokens import get_tokens
2513
- _, _, api_key, _ = get_tokens()
2514
- except Exception as e:
2515
- print(f"โš ๏ธ Error fetching OpenAI API key from server: {e}")
2516
-
2517
- # Then try credentials manager
2518
- if not api_key:
2519
- try:
2520
- from credentials_manager import CredentialsManager
2521
- credentials_manager = CredentialsManager()
2522
- api_key = credentials_manager.get_openai_api_key()
2523
- except Exception as e:
2524
- print(f"โš ๏ธ Error getting OpenAI API key from credentials manager: {e}")
2525
-
2526
- return call_openai_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
50
+ # Check if we got valid tokens
51
+ if token_id is None or token_secret is None:
52
+ raise ValueError("Could not get valid tokens")
2527
53
 
2528
- def call_anthropic_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
2529
- """Call Anthropic Claude to debug multiple failed commands and suggest fixes for all of them at once"""
2530
- print("\n๐Ÿ” DEBUG: Starting batch Anthropic Claude debugging...")
2531
- print(f"๐Ÿ” DEBUG: Analyzing {len(failed_commands)} failed commands")
2532
-
2533
- if not failed_commands:
2534
- print("โš ๏ธ No failed commands to analyze")
2535
- return []
2536
-
2537
- if not api_key:
2538
- print("๐Ÿ” DEBUG: No Anthropic API key provided, searching for one...")
2539
-
2540
- # First try environment variable
2541
- api_key = os.environ.get("ANTHROPIC_API_KEY")
2542
- print(f"๐Ÿ” DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
2543
- if api_key:
2544
- print(f"๐Ÿ” DEBUG: Environment API key value: {api_key}")
2545
-
2546
- # If not in environment, try to fetch from server using fetch_modal_tokens
2547
- if not api_key:
2548
- try:
2549
- print("๐Ÿ” DEBUG: Trying to fetch API key from server...")
2550
- from fetch_modal_tokens import get_tokens
2551
- _, _, _, api_key = get_tokens()
2552
- if api_key:
2553
- # Set in environment for this session
2554
- os.environ["ANTHROPIC_API_KEY"] = api_key
2555
- else:
2556
- print("โš ๏ธ Could not fetch Anthropic API key from server")
2557
- except Exception as e:
2558
- print(f"โš ๏ธ Error fetching API key from server: {e}")
2559
-
2560
- # Then try credentials manager
2561
- if not api_key:
2562
- print("๐Ÿ” DEBUG: Trying credentials manager...")
2563
- try:
2564
- from credentials_manager import CredentialsManager
2565
- credentials_manager = CredentialsManager()
2566
- api_key = credentials_manager.get_anthropic_api_key()
2567
- if api_key:
2568
- print(f"๐Ÿ” DEBUG: API key from credentials manager: Found")
2569
- print(f"๐Ÿ” DEBUG: Credentials manager API key value: {api_key}")
2570
- # Set in environment for this session
2571
- os.environ["ANTHROPIC_API_KEY"] = api_key
2572
- else:
2573
- print("โš ๏ธ Could not fetch Anthropic API key from credentials manager")
2574
- except Exception as e:
2575
- print(f"โš ๏ธ Error fetching API key from credentials manager: {e}")
2576
-
2577
- if not api_key:
2578
- print("โŒ No Anthropic API key available for batch debugging")
2579
- return []
2580
-
2581
- # Prepare context for batch analysis
2582
- context_parts = []
2583
- context_parts.append(f"Current directory: {current_dir}")
2584
- context_parts.append(f"Sandbox available: {sandbox is not None}")
2585
-
2586
- # Add failed commands with their errors
2587
- for i, failed_cmd in enumerate(failed_commands, 1):
2588
- cmd_type = failed_cmd.get('type', 'main')
2589
- original_cmd = failed_cmd.get('original_command', '')
2590
- cmd_text = failed_cmd['command']
2591
- stderr = failed_cmd.get('stderr', '')
2592
- stdout = failed_cmd.get('stdout', '')
2593
-
2594
- context_parts.append(f"\n--- Failed Command {i} ({cmd_type}) ---")
2595
- context_parts.append(f"Command: {cmd_text}")
2596
- if original_cmd and original_cmd != cmd_text:
2597
- context_parts.append(f"Original Command: {original_cmd}")
2598
- if stderr:
2599
- context_parts.append(f"Error Output: {stderr}")
2600
- if stdout:
2601
- context_parts.append(f"Standard Output: {stdout}")
2602
-
2603
- # Create the prompt for batch analysis
2604
- prompt = f"""You are a debugging assistant analyzing multiple failed commands.
54
+ print(f"โœ… Tokens fetched successfully")
2605
55
 
2606
- Context:
2607
- {chr(10).join(context_parts)}
56
+ # Explicitly set the environment variables again to be sure
57
+ os.environ["MODAL_TOKEN_ID"] = token_id
58
+ os.environ["MODAL_TOKEN_SECRET"] = token_secret
59
+ os.environ["OPENAI_API_KEY"] = openai_api_key
60
+ # Also set the old environment variable for backward compatibility
61
+ os.environ["MODAL_TOKEN"] = token_id
2608
62
 
2609
- Please analyze each failed command and provide a fix command for each one. For each failed command, respond with:
63
+ # Set token variables for later use
64
+ token = token_id # For backward compatibility
2610
65
 
2611
- FIX_COMMAND_{i}: <the fix command>
2612
- REASON_{i}: <brief explanation of why the original command failed and how the fix addresses it>
2613
66
 
2614
- Guidelines:
2615
- - For file not found errors, first search for the file using 'find . -name filename -type f'
2616
- - For missing packages, use appropriate package managers (pip, apt-get, npm)
2617
- - For Git SSH authentication failures, convert SSH URLs to HTTPS URLs
2618
- - For permission errors, suggest commands with sudo if appropriate
2619
- - For network issues, suggest retry commands or alternative URLs
2620
- - Keep each fix command simple and focused on the specific error
67
+ def generate_random_password(length=16):
68
+ """Generate a random password for SSH access"""
69
+ alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
70
+ password = ''.join(secrets.choice(alphabet) for i in range(length))
71
+ return password
2621
72
 
2622
- Provide fixes for all {len(failed_commands)} failed commands:"""
2623
73
 
2624
- # Set up headers for Anthropic API
2625
- headers = {
2626
- "x-api-key": api_key,
2627
- "anthropic-version": "2023-06-01",
2628
- "content-type": "application/json"
2629
- }
2630
-
2631
- payload = {
2632
- "model": "claude-3-5-sonnet-20241022", # Use a more capable model for batch analysis
2633
- "max_tokens": 1000,
2634
- "messages": [
2635
- {"role": "user", "content": prompt}
2636
- ]
2637
- }
74
+ def get_stored_credentials():
75
+ """Load stored credentials from ~/.gitarsenal/credentials.json"""
76
+ import json
77
+ from pathlib import Path
2638
78
 
2639
79
  try:
2640
- print(f"๐Ÿค– Calling Anthropic Claude for batch debugging of {len(failed_commands)} commands...")
2641
- response = requests.post(
2642
- "https://api.anthropic.com/v1/messages",
2643
- headers=headers,
2644
- json=payload,
2645
- timeout=60
2646
- )
2647
-
2648
- if response.status_code == 200:
2649
- result = response.json()
2650
- content = result['content'][0]['text']
2651
- print(f"โœ… Batch analysis completed")
2652
-
2653
- # Parse the response to extract fix commands
2654
- fixes = []
2655
- for i in range(1, len(failed_commands) + 1):
2656
- fix_pattern = f"FIX_COMMAND_{i}: (.+)"
2657
- reason_pattern = f"REASON_{i}: (.+)"
2658
-
2659
- fix_match = re.search(fix_pattern, content, re.MULTILINE)
2660
- reason_match = re.search(reason_pattern, content, re.MULTILINE)
2661
-
2662
- if fix_match:
2663
- fix_command = fix_match.group(1).strip()
2664
- reason = reason_match.group(1).strip() if reason_match else "Anthropic Claude suggested fix"
2665
-
2666
- # Clean up the fix command
2667
- if fix_command.startswith('`') and fix_command.endswith('`'):
2668
- fix_command = fix_command[1:-1]
2669
-
2670
- fixes.append({
2671
- 'original_command': failed_commands[i-1]['command'],
2672
- 'fix_command': fix_command,
2673
- 'reason': reason,
2674
- 'command_index': i-1
2675
- })
2676
-
2677
- print(f"๐Ÿ”ง Generated {len(fixes)} fix commands from batch analysis")
2678
- return fixes
80
+ credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
81
+ if credentials_file.exists():
82
+ with open(credentials_file, 'r') as f:
83
+ credentials = json.load(f)
84
+ return credentials
2679
85
  else:
2680
- print(f"โŒ Anthropic API error: {response.status_code} - {response.text}")
2681
- return []
2682
-
86
+ return {}
2683
87
  except Exception as e:
2684
- print(f"โŒ Error during batch debugging: {e}")
2685
- return []
2686
-
2687
- def generate_random_password(length=16):
2688
- """Generate a random password for SSH access"""
2689
- alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
2690
- password = ''.join(secrets.choice(alphabet) for i in range(length))
2691
- return password
88
+ print(f"โš ๏ธ Error loading stored credentials: {e}")
89
+ return {}
2692
90
 
2693
91
 
2694
92
  # Now modify the create_modal_ssh_container function to use the PersistentShell
@@ -2900,6 +298,10 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2900
298
  try:
2901
299
  print("๐Ÿ“ฆ Building SSH-enabled image...")
2902
300
 
301
+ # Get the current directory path for mounting local Python sources
302
+ current_dir = os.path.dirname(os.path.abspath(__file__))
303
+ print(f"๐Ÿ” Current directory for mounting: {current_dir}")
304
+
2903
305
  # Use a more stable CUDA base image and avoid problematic packages
2904
306
  ssh_image = (
2905
307
  # modal.Image.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.11")
@@ -2909,7 +311,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2909
311
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2910
312
  "gpg", "ca-certificates", "software-properties-common"
2911
313
  )
2912
- .uv_pip_install("uv", "modal", "requests", "openai") # Remove problematic CUDA packages
314
+ .uv_pip_install("uv", "modal", "requests", "openai", "anthropic") # Remove problematic CUDA packages
2913
315
  .run_commands(
2914
316
  # Create SSH directory
2915
317
  "mkdir -p /var/run/sshd",
@@ -2931,6 +333,12 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2931
333
  # Set up a nice bash prompt
2932
334
  "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2933
335
  )
336
+ .add_local_file(os.path.join(current_dir, "shell.py"), "/python/shell.py") # Mount shell.py
337
+ .add_local_file(os.path.join(current_dir, "command_manager.py"), "/python/command_manager.py") # Mount command_manager.py
338
+ .add_local_file(os.path.join(current_dir, "fetch_modal_tokens.py"), "/python/fetch_modal_tokens.py") # Mount fetch_modal_token.py
339
+ .add_local_file(os.path.join(current_dir, "llm_debugging.py"), "/python/llm_debugging.py") # Mount llm_debugging.py
340
+ .add_local_file(os.path.join(current_dir, "credentials_manager.py"), "/python/credentials_manager.py") # Mount credentials_manager.py
341
+
2934
342
  )
2935
343
  print("โœ… SSH image built successfully")
2936
344
  except Exception as e:
@@ -2964,6 +372,31 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2964
372
  import time
2965
373
  import os
2966
374
  import json
375
+ import sys
376
+
377
+ # Add the mounted python directory to the Python path
378
+ sys.path.insert(0, "/python")
379
+
380
+ # Import the required classes from the mounted modules
381
+ try:
382
+ from command_manager import CommandListManager
383
+ from shell import PersistentShell
384
+ from llm_debugging import get_stored_credentials, generate_auth_context, call_llm_for_debug, call_llm_for_batch_debug, call_anthropic_for_debug, call_openai_for_debug, call_openai_for_batch_debug, call_anthropic_for_batch_debug, get_current_debug_model
385
+
386
+ print("โœ… Successfully imported CommandListManager, PersistentShell, and all llm_debugging functions from mounted modules")
387
+ except ImportError as e:
388
+ print(f"โŒ Failed to import modules from mounted directory: {e}")
389
+ print("๐Ÿ” Available files in /python:")
390
+ try:
391
+ import os
392
+ if os.path.exists("/python"):
393
+ for file in os.listdir("/python"):
394
+ print(f" - {file}")
395
+ else:
396
+ print(" /python directory does not exist")
397
+ except Exception as list_error:
398
+ print(f" Error listing files: {list_error}")
399
+ raise
2967
400
 
2968
401
  # Set root password
2969
402
  subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
@@ -4684,8 +2117,6 @@ if __name__ == "__main__":
4684
2117
  import sys
4685
2118
 
4686
2119
  parser = argparse.ArgumentParser()
4687
- parser.add_argument('--gpu', type=str, help='GPU type (e.g., A10G, T4, A100-80GB). If not provided, will prompt for GPU selection.')
4688
- parser.add_argument('--repo-url', type=str, help='Repository URL to clone')
4689
2120
  parser.add_argument('--repo-name', type=str, help='Repository name override')
4690
2121
  parser.add_argument('--setup-commands', type=str, nargs='+', help='Setup commands to run (deprecated)')
4691
2122
  parser.add_argument('--setup-commands-json', type=str, help='Setup commands as JSON array')
@@ -4701,6 +2132,11 @@ if __name__ == "__main__":
4701
2132
  parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
4702
2133
  parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
4703
2134
  parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
2135
+
2136
+ parser.add_argument('--proxy-url', help='URL of the proxy server')
2137
+ parser.add_argument('--proxy-api-key', help='API key for the proxy server')
2138
+ parser.add_argument('--gpu', default='A10G', help='GPU type to use')
2139
+ parser.add_argument('--repo-url', help='Repository URL')
4704
2140
 
4705
2141
  # Authentication-related arguments
4706
2142
  parser.add_argument('--auth', action='store_true', help='Manage authentication (login, register, logout)')
@@ -5001,4 +2437,4 @@ if __name__ == "__main__":
5001
2437
  # print(f"\nโŒ Error: {e}")
5002
2438
  # print("๐Ÿงน Cleaning up resources...")
5003
2439
  cleanup_modal_token()
5004
- sys.exit(1)
2440
+