gitarsenal-cli 1.9.21 โ†’ 1.9.23

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 +605 -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 +1061 -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 +610 -0
  22. package/python/test_modalSandboxScript.py +2 -2
  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
@@ -56,45 +56,6 @@ def setup_modal_token():
56
56
 
57
57
  return True
58
58
 
59
- def get_token_from_gitarsenal():
60
- """Get Modal token from GitArsenal credentials file (fallback method)"""
61
- try:
62
- from credentials_manager import CredentialsManager
63
- credentials_manager = CredentialsManager()
64
- token = credentials_manager.get_modal_token()
65
- if token:
66
- logger.info("Found Modal token in GitArsenal credentials")
67
- return token
68
- except ImportError:
69
- logger.warning("Could not import CredentialsManager")
70
- except Exception as e:
71
- logger.warning(f"Error getting token from GitArsenal credentials: {e}")
72
-
73
- return None
74
-
75
- def get_token_from_modal_file():
76
- """Get Modal token from Modal token file (fallback method)"""
77
- try:
78
- import json
79
- token_file = Path.home() / ".modal" / "token.json"
80
-
81
- if not token_file.exists():
82
- logger.warning(f"Modal token file not found at {token_file}")
83
- return None
84
-
85
- with open(token_file, 'r') as f:
86
- token_data = json.load(f)
87
-
88
- # The token file contains both token_id and token
89
- token_id = token_data.get("token_id")
90
- if token_id:
91
- logger.info("Found token_id in Modal token file")
92
- return token_id
93
- except Exception as e:
94
- logger.warning(f"Error reading Modal token file: {e}")
95
-
96
- return None
97
-
98
59
  if __name__ == "__main__":
99
60
  if setup_modal_token():
100
61
  print("โœ… Modal token set up successfully")
@@ -0,0 +1,610 @@
1
+ import threading
2
+ import subprocess
3
+ import os
4
+ import time
5
+ import uuid
6
+ import re
7
+
8
+ class PersistentShell:
9
+ """A persistent bash shell using subprocess.Popen for executing commands with state persistence."""
10
+
11
+ def __init__(self, working_dir="/root", timeout=60):
12
+ self.working_dir = working_dir
13
+ self.timeout = timeout
14
+ self.process = None
15
+ self.stdout_lines = [] # Use list instead of queue
16
+ self.stderr_lines = [] # Use list instead of queue
17
+ self.stdout_lock = threading.Lock()
18
+ self.stderr_lock = threading.Lock()
19
+ self.stdout_thread = None
20
+ self.stderr_thread = None
21
+ self.command_counter = 0
22
+ self.is_running = False
23
+ self.virtual_env_path = None # Track activated virtual environment
24
+ self.suggested_alternative = None # Store suggested alternative commands
25
+ self.should_remove_command = False # Flag to indicate if a command should be removed
26
+ self.removal_reason = None # Reason for removing a command
27
+
28
+ def start(self):
29
+ """Start the persistent bash shell."""
30
+ if self.is_running:
31
+ return
32
+
33
+ print(f"๐Ÿš Starting persistent bash shell in {self.working_dir}")
34
+
35
+ # Start bash with unbuffered output
36
+ self.process = subprocess.Popen(
37
+ ['bash', '-i'], # Interactive bash
38
+ stdin=subprocess.PIPE,
39
+ stdout=subprocess.PIPE,
40
+ stderr=subprocess.PIPE,
41
+ text=True,
42
+ bufsize=0, # Unbuffered
43
+ cwd=self.working_dir,
44
+ preexec_fn=os.setsid # Create new process group
45
+ )
46
+
47
+ # Start threads to read stdout and stderr
48
+ self.stdout_thread = threading.Thread(target=self._read_stdout, daemon=True)
49
+ self.stderr_thread = threading.Thread(target=self._read_stderr, daemon=True)
50
+
51
+ self.stdout_thread.start()
52
+ self.stderr_thread.start()
53
+
54
+ self.is_running = True
55
+
56
+ # Initial setup commands
57
+ self._send_command_raw("set +h") # Disable hash table for commands
58
+ self._send_command_raw("export PS1='$ '") # Simpler prompt
59
+ self._send_command_raw("cd " + self.working_dir) # Change to working directory
60
+ time.sleep(0.5) # Let initial commands settle
61
+
62
+
63
+ def _read_stdout(self):
64
+ """Read stdout in a separate thread."""
65
+ while self.process and self.process.poll() is None:
66
+ try:
67
+ line = self.process.stdout.readline()
68
+ if line:
69
+ with self.stdout_lock:
70
+ self.stdout_lines.append(line.rstrip('\n'))
71
+ else:
72
+ time.sleep(0.01)
73
+ except Exception as e:
74
+ print(f"Error reading stdout: {e}")
75
+ break
76
+
77
+ def _read_stderr(self):
78
+ """Read stderr in a separate thread."""
79
+ while self.process and self.process.poll() is None:
80
+ try:
81
+ line = self.process.stderr.readline()
82
+ if line:
83
+ with self.stderr_lock:
84
+ self.stderr_lines.append(line.rstrip('\n'))
85
+ else:
86
+ time.sleep(0.01)
87
+ except Exception as e:
88
+ print(f"Error reading stderr: {e}")
89
+ break
90
+
91
+ def _send_command_raw(self, command):
92
+ """Send a raw command to the shell without waiting for completion."""
93
+ if not self.is_running or not self.process:
94
+ raise RuntimeError("Shell is not running")
95
+
96
+ try:
97
+ self.process.stdin.write(command + '\n')
98
+ self.process.stdin.flush()
99
+ except Exception as e:
100
+ print(f"Error sending command: {e}")
101
+ raise
102
+
103
+ def _preprocess_command(self, command):
104
+ """Preprocess commands to handle special cases like virtual environment activation."""
105
+ # Handle virtual environment creation and activation
106
+ if "uv venv" in command and "&&" in command and "source" in command:
107
+ # Split the compound command into separate parts
108
+ parts = [part.strip() for part in command.split("&&")]
109
+ return parts
110
+ elif command.strip().startswith("source ") and "/bin/activate" in command:
111
+ # Handle standalone source command
112
+ venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
113
+ self.virtual_env_path = venv_path
114
+ return [command]
115
+ elif "source" in command and "activate" in command:
116
+ # Handle any other source activation pattern
117
+ return [command]
118
+ elif "uv pip install" in command and self.is_in_venv():
119
+ # If we're in a virtual environment, ensure we use the right pip
120
+ return [command]
121
+ else:
122
+ return [command]
123
+
124
+ def execute(self, command, timeout=None):
125
+ """Execute a command and return (success, stdout, stderr)."""
126
+ if not self.is_running:
127
+ self.start()
128
+
129
+ if timeout is None:
130
+ timeout = self.timeout
131
+
132
+ # Preprocess the command to handle special cases
133
+ command_parts = self._preprocess_command(command)
134
+
135
+ # If we have multiple parts, execute them sequentially
136
+ if len(command_parts) > 1:
137
+ print(f"๐Ÿ”ง Executing compound command in {len(command_parts)} parts")
138
+ all_stdout = []
139
+ all_stderr = []
140
+
141
+ for i, part in enumerate(command_parts):
142
+ print(f" Part {i+1}/{len(command_parts)}: {part}")
143
+ success, stdout, stderr = self._execute_single(part, timeout)
144
+
145
+ if stdout:
146
+ all_stdout.append(stdout)
147
+ if stderr:
148
+ all_stderr.append(stderr)
149
+
150
+ if not success:
151
+ # If any part fails, return the failure
152
+ return False, '\n'.join(all_stdout), '\n'.join(all_stderr)
153
+
154
+ # Small delay between parts to let environment changes take effect
155
+ time.sleep(0.1)
156
+
157
+ return True, '\n'.join(all_stdout), '\n'.join(all_stderr)
158
+ else:
159
+ return self._execute_single(command_parts[0], timeout)
160
+
161
+ def _execute_single(self, command, timeout):
162
+ """Execute a single command and return (success, stdout, stderr)."""
163
+ self.command_counter += 1
164
+ marker = f"CMD_DONE_{self.command_counter}_{uuid.uuid4().hex[:8]}"
165
+
166
+ print(f"๐Ÿ”ง Executing: {command}")
167
+
168
+ # Clear any existing output
169
+ self._clear_lines()
170
+
171
+ # Wait for shell to be ready (prompt should be visible)
172
+ if not self.wait_for_prompt(timeout=2):
173
+ # print("โš ๏ธ Shell not ready, waiting...")
174
+ time.sleep(0.5)
175
+
176
+ # For source commands, we need special handling
177
+ if command.strip().startswith("source "):
178
+ # Send the source command in a way that preserves the environment
179
+ try:
180
+ # Extract the virtual environment path
181
+ venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
182
+
183
+ # Use a more robust approach that actually activates the environment
184
+ activation_script = f"""
185
+ if [ -f "{venv_path}/bin/activate" ]; then
186
+ source "{venv_path}/bin/activate"
187
+ echo "VIRTUAL_ENV=$VIRTUAL_ENV"
188
+ echo "PATH=$PATH"
189
+ echo 'SOURCE_SUCCESS'
190
+ else
191
+ echo 'SOURCE_FAILED - activation script not found'
192
+ fi
193
+ """
194
+
195
+ self._send_command_raw(activation_script)
196
+ time.sleep(0.3) # Give more time for environment changes
197
+ self._send_command_raw(f'echo "EXIT_CODE:$?"')
198
+ self._send_command_raw(f'echo "{marker}"')
199
+ except Exception as e:
200
+ return False, "", f"Failed to send source command: {e}"
201
+ else:
202
+ # Send the command followed by markers
203
+ try:
204
+ self._send_command_raw(command)
205
+ # Wait a moment for the command to start
206
+ time.sleep(0.1)
207
+ self._send_command_raw(f'echo "EXIT_CODE:$?"')
208
+ self._send_command_raw(f'echo "{marker}"')
209
+ except Exception as e:
210
+ return False, "", f"Failed to send command: {e}"
211
+
212
+ # Collect output until we see the marker
213
+ command_stdout = []
214
+ command_stderr = []
215
+ start_time = time.time()
216
+ found_marker = False
217
+ exit_code = None
218
+ last_stdout_index = 0
219
+ last_stderr_index = 0
220
+ source_success = None
221
+
222
+ while time.time() - start_time < timeout:
223
+ # Check for new stdout lines
224
+ with self.stdout_lock:
225
+ current_stdout = self.stdout_lines[last_stdout_index:]
226
+ last_stdout_index = len(self.stdout_lines)
227
+
228
+ for line in current_stdout:
229
+ if line == marker:
230
+ found_marker = True
231
+ break
232
+ elif line.startswith("EXIT_CODE:"):
233
+ try:
234
+ exit_code = int(line.split(":", 1)[1])
235
+ except (ValueError, IndexError):
236
+ exit_code = 1
237
+ elif line == "SOURCE_SUCCESS":
238
+ source_success = True
239
+ elif line.startswith("SOURCE_FAILED"):
240
+ source_success = False
241
+ command_stderr.append(line)
242
+ elif line.startswith("VIRTUAL_ENV="):
243
+ # Extract and store the virtual environment path
244
+ venv_path = line.split("=", 1)[1]
245
+ self.virtual_env_path = venv_path
246
+ command_stdout.append(line)
247
+ elif line.startswith("PATH="):
248
+ # Store the updated PATH
249
+ command_stdout.append(line)
250
+ elif line.strip() and not line.startswith("$"): # Skip empty lines and prompt lines
251
+ command_stdout.append(line)
252
+
253
+ if found_marker:
254
+ break
255
+
256
+ # Check for new stderr lines
257
+ with self.stderr_lock:
258
+ current_stderr = self.stderr_lines[last_stderr_index:]
259
+ last_stderr_index = len(self.stderr_lines)
260
+
261
+ for line in current_stderr:
262
+ if line.strip(): # Skip empty lines
263
+ command_stderr.append(line)
264
+
265
+ # Check if command is waiting for user input
266
+ if not found_marker and time.time() - start_time > 5: # Wait at least 5 seconds before checking
267
+ if self._is_waiting_for_input(command_stdout, command_stderr):
268
+ print("โš ๏ธ Command appears to be waiting for user input")
269
+ # Try to handle the input requirement
270
+ input_handled = self._handle_input_requirement(command, command_stdout, command_stderr)
271
+
272
+ if input_handled is True and self.should_remove_command:
273
+ # If LLM suggested to remove the command
274
+ self._send_command_raw("\x03") # Send Ctrl+C
275
+ time.sleep(0.5)
276
+ return False, '\n'.join(command_stdout), f"Command removed - {self.removal_reason}"
277
+ elif not input_handled:
278
+ # If we couldn't handle the input, abort the command
279
+ self._send_command_raw("\x03") # Send Ctrl+C
280
+ time.sleep(0.5)
281
+ return False, '\n'.join(command_stdout), "Command aborted - requires user input"
282
+
283
+ time.sleep(0.1)
284
+
285
+ if not found_marker:
286
+ print(f"โš ๏ธ Command timed out after {timeout} seconds")
287
+ return False, '\n'.join(command_stdout), f"Command timed out after {timeout} seconds"
288
+
289
+ stdout_text = '\n'.join(command_stdout)
290
+ stderr_text = '\n'.join(command_stderr)
291
+
292
+ # Determine success based on multiple factors
293
+ if source_success is not None:
294
+ success = source_success
295
+ else:
296
+ success = exit_code == 0 if exit_code is not None else len(command_stderr) == 0
297
+
298
+ if success:
299
+ if stdout_text:
300
+ print(f"โœ… Output: {stdout_text}")
301
+ # Track virtual environment activation
302
+ if command.strip().startswith("source ") and "/bin/activate" in command:
303
+ venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
304
+ self.virtual_env_path = venv_path
305
+ print(f"โœ… Virtual environment activated: {venv_path}")
306
+ else:
307
+ print(f"โŒ Command failed with exit code: {exit_code}")
308
+ if stderr_text:
309
+ print(f"โŒ Error: {stderr_text}")
310
+
311
+ # Wait a moment for the shell to be ready for the next command
312
+ time.sleep(0.2)
313
+
314
+ return success, stdout_text, stderr_text
315
+
316
+ def _is_waiting_for_input(self, stdout_lines, stderr_lines):
317
+ """Detect if a command is waiting for user input."""
318
+ # Common patterns that indicate waiting for user input
319
+ input_patterns = [
320
+ r'(?i)(y/n|yes/no)\??\s*$', # Yes/No prompts
321
+ r'(?i)password:?\s*$', # Password prompts
322
+ r'(?i)continue\??\s*$', # Continue prompts
323
+ r'(?i)proceed\??\s*$', # Proceed prompts
324
+ r'\[\s*[Yy]/[Nn]\s*\]\s*$', # [Y/n] style prompts
325
+ r'(?i)username:?\s*$', # Username prompts
326
+ r'(?i)token:?\s*$', # Token prompts
327
+ r'(?i)api key:?\s*$', # API key prompts
328
+ r'(?i)press enter to continue', # Press enter prompts
329
+ r'(?i)select an option:?\s*$', # Selection prompts
330
+ r'(?i)choose an option:?\s*$', # Choice prompts
331
+ ]
332
+
333
+ # Check the last few lines of stdout and stderr for input patterns
334
+ last_lines = []
335
+ if stdout_lines:
336
+ last_lines.extend(stdout_lines[-3:]) # Check last 3 lines of stdout
337
+ if stderr_lines:
338
+ last_lines.extend(stderr_lines[-3:]) # Check last 3 lines of stderr
339
+
340
+ for line in last_lines:
341
+ for pattern in input_patterns:
342
+ if re.search(pattern, line):
343
+ print(f"๐Ÿ” Detected input prompt: {line}")
344
+ return True
345
+
346
+ # Check if there's no output for a while but the command is still running
347
+ if len(stdout_lines) == 0 and len(stderr_lines) == 0:
348
+ # This might be a command waiting for input without a prompt
349
+ # We'll be cautious and only return True if we're sure
350
+ return False
351
+
352
+ return False
353
+
354
+ def _handle_input_requirement(self, command, stdout_lines, stderr_lines):
355
+ """Attempt to handle commands that require input."""
356
+ # Extract the last few lines to analyze what kind of input is needed
357
+ last_lines = []
358
+ if stdout_lines:
359
+ last_lines.extend(stdout_lines[-3:])
360
+ if stderr_lines:
361
+ last_lines.extend(stderr_lines[-3:])
362
+
363
+ last_line = last_lines[-1] if last_lines else ""
364
+
365
+ # Try to determine what kind of input is needed
366
+ if re.search(r'(?i)(y/n|yes/no|\[y/n\])', last_line):
367
+ # For yes/no prompts, usually 'yes' is safer
368
+ print("๐Ÿ”ง Auto-responding with 'y' to yes/no prompt")
369
+ self._send_command_raw("y")
370
+ return True
371
+
372
+ elif re.search(r'(?i)password', last_line):
373
+ # For password prompts, check if we have stored credentials
374
+ stored_creds = get_stored_credentials()
375
+ if stored_creds and 'ssh_password' in stored_creds:
376
+ print("๐Ÿ”ง Auto-responding with stored SSH password")
377
+ self._send_command_raw(stored_creds['ssh_password'])
378
+ return True
379
+ else:
380
+ print("โš ๏ธ Password prompt detected but no stored password available")
381
+ return False
382
+
383
+ elif re.search(r'(?i)token|api.key', last_line):
384
+ # For token/API key prompts
385
+ stored_creds = get_stored_credentials()
386
+ if stored_creds:
387
+ if 'openai_api_key' in stored_creds and re.search(r'(?i)openai|api.key', last_line):
388
+ print("๐Ÿ”ง Auto-responding with stored OpenAI API key")
389
+ self._send_command_raw(stored_creds['openai_api_key'])
390
+ return True
391
+ elif 'hf_token' in stored_creds and re.search(r'(?i)hugg|hf|token', last_line):
392
+ print("๐Ÿ”ง Auto-responding with stored Hugging Face token")
393
+ self._send_command_raw(stored_creds['hf_token'])
394
+ return True
395
+
396
+ print("โš ๏ธ Token/API key prompt detected but no matching stored credentials")
397
+ return False
398
+
399
+ elif re.search(r'(?i)press enter|continue|proceed', last_line):
400
+ # For "press enter to continue" prompts
401
+ print("๐Ÿ”ง Auto-responding with Enter to continue")
402
+ self._send_command_raw("") # Empty string sends just Enter
403
+ return True
404
+
405
+ # If we can't determine the type of input needed
406
+ print("โš ๏ธ Couldn't determine the type of input needed")
407
+
408
+ # Try to use LLM to suggest an alternative command
409
+ try:
410
+ # Get current working directory for context
411
+ cwd = self.get_cwd()
412
+
413
+ # Reset command removal flags
414
+ self.should_remove_command = False
415
+ self.removal_reason = None
416
+
417
+ # Call LLM to suggest an alternative
418
+ alternative = self._suggest_alternative_command(command, stdout_lines, stderr_lines, cwd)
419
+
420
+ # Check if LLM suggested to remove the command
421
+ if self.should_remove_command:
422
+ print(f"๐Ÿšซ Command will be removed: {self.removal_reason}")
423
+ return True # Return True to indicate the command has been handled (by removing it)
424
+
425
+ if alternative:
426
+ print(f"๐Ÿ”ง LLM suggested alternative command: {alternative}")
427
+ # We don't execute the alternative here, but return False so the calling code
428
+ # can handle it (e.g., by adding it to the command list)
429
+
430
+ # Store the suggested alternative for later use
431
+ self.suggested_alternative = alternative
432
+ return False
433
+ except Exception as e:
434
+ print(f"โš ๏ธ Error getting LLM suggestion: {e}")
435
+
436
+ return False
437
+
438
+ def _suggest_alternative_command(self, command, stdout_lines, stderr_lines, current_dir):
439
+ """Use LLM to suggest an alternative command that doesn't require user input."""
440
+ try:
441
+ # Get API key
442
+ api_key = os.environ.get("OPENAI_API_KEY")
443
+ if not api_key:
444
+ # Try to load from saved file
445
+ key_file = os.path.expanduser("~/.gitarsenal/openai_key")
446
+ if os.path.exists(key_file):
447
+ with open(key_file, "r") as f:
448
+ api_key = f.read().strip()
449
+
450
+ if not api_key:
451
+ print("โš ๏ธ No OpenAI API key available for suggesting alternative command")
452
+ return None
453
+
454
+ # Prepare the prompt
455
+ stdout_text = '\n'.join(stdout_lines[-10:]) if stdout_lines else ""
456
+ stderr_text = '\n'.join(stderr_lines[-10:]) if stderr_lines else ""
457
+
458
+ prompt = f"""
459
+ The command '{command}' appears to be waiting for user input.
460
+
461
+ Current directory: {current_dir}
462
+
463
+ Last stdout output:
464
+ {stdout_text}
465
+
466
+ Last stderr output:
467
+ {stderr_text}
468
+
469
+ Please analyze this command and determine if it's useful to continue with it.
470
+ If it's useful, suggest an alternative command that achieves the same goal but doesn't require user input.
471
+ For example, add flags like -y, --yes, --no-input, etc., or provide the required input in the command.
472
+
473
+ If the command is not useful or cannot be executed non-interactively, respond with "REMOVE_COMMAND" and explain why.
474
+
475
+ Format your response as:
476
+ ALTERNATIVE: <alternative command>
477
+ or
478
+ REMOVE_COMMAND: <reason>
479
+ """
480
+
481
+ # Call OpenAI API
482
+ import openai
483
+ client = openai.OpenAI(api_key=api_key)
484
+
485
+ response = client.chat.completions.create(
486
+ model="gpt-4o-mini",
487
+ messages=[
488
+ {"role": "system", "content": "You are a helpful assistant that suggests alternative commands that don't require user input."},
489
+ {"role": "user", "content": prompt}
490
+ ],
491
+ max_tokens=150,
492
+ temperature=0.7
493
+ )
494
+
495
+ response_text = response.choices[0].message.content.strip()
496
+
497
+ # Check if the response suggests removing the command
498
+ if response_text.startswith("REMOVE_COMMAND:"):
499
+ reason = response_text.replace("REMOVE_COMMAND:", "").strip()
500
+ print(f"๐Ÿšซ LLM suggests removing command: {reason}")
501
+ self.should_remove_command = True
502
+ self.removal_reason = reason
503
+ return None
504
+
505
+ # Extract the alternative command
506
+ if response_text.startswith("ALTERNATIVE:"):
507
+ alternative_command = response_text.replace("ALTERNATIVE:", "").strip()
508
+ else:
509
+ # Try to extract the command from a free-form response
510
+ lines = response_text.split('\n')
511
+ for line in lines:
512
+ line = line.strip()
513
+ if line and not line.startswith(('Here', 'I', 'You', 'The', 'This', 'Use', 'Try')):
514
+ alternative_command = line
515
+ break
516
+ else:
517
+ alternative_command = lines[0].strip()
518
+
519
+ return alternative_command
520
+
521
+ except Exception as e:
522
+ print(f"โš ๏ธ Error suggesting alternative command: {e}")
523
+ return None
524
+
525
+ def _clear_lines(self):
526
+ """Clear both output line lists."""
527
+ with self.stdout_lock:
528
+ self.stdout_lines.clear()
529
+ with self.stderr_lock:
530
+ self.stderr_lines.clear()
531
+
532
+ def get_cwd(self):
533
+ """Get current working directory."""
534
+ success, output, _ = self._execute_single("pwd", 10)
535
+ if success:
536
+ return output.strip()
537
+ return self.working_dir
538
+
539
+ def get_virtual_env(self):
540
+ """Get the currently activated virtual environment path."""
541
+ return self.virtual_env_path
542
+
543
+ def is_in_venv(self):
544
+ """Check if we're currently in a virtual environment."""
545
+ return self.virtual_env_path is not None and self.virtual_env_path != ""
546
+
547
+ def get_venv_name(self):
548
+ """Get the name of the current virtual environment if active."""
549
+ if self.is_in_venv():
550
+ return os.path.basename(self.virtual_env_path)
551
+ return None
552
+
553
+ def exec(self, *args, **kwargs):
554
+ """Compatibility method to make PersistentShell work with call_openai_for_debug."""
555
+ # Convert exec call to execute method
556
+ if len(args) >= 2 and args[0] == "bash" and args[1] == "-c":
557
+ command = args[2]
558
+ success, stdout, stderr = self.execute(command)
559
+
560
+ # Create a mock result object that mimics the expected interface
561
+ class MockResult:
562
+ def __init__(self, stdout, stderr, returncode):
563
+ self.stdout = [stdout] if stdout else []
564
+ self.stderr = [stderr] if stderr else []
565
+ self.returncode = 0 if returncode else 1
566
+
567
+ def wait(self):
568
+ pass
569
+
570
+ return MockResult(stdout, stderr, success)
571
+ else:
572
+ raise NotImplementedError("exec method only supports bash -c commands")
573
+
574
+ def wait_for_prompt(self, timeout=5):
575
+ """Wait for the shell prompt to appear, indicating readiness for next command."""
576
+ start_time = time.time()
577
+ while time.time() - start_time < timeout:
578
+ with self.stdout_lock:
579
+ if self.stdout_lines and self.stdout_lines[-1].strip().endswith('$'):
580
+ return True
581
+ time.sleep(0.1)
582
+ return False
583
+
584
+ def cleanup(self):
585
+ """Clean up the shell process."""
586
+ print("๐Ÿงน Cleaning up persistent shell...")
587
+ self.is_running = False
588
+
589
+ if self.process:
590
+ try:
591
+ # Send exit command
592
+ self._send_command_raw("exit")
593
+
594
+ # Wait for process to terminate
595
+ try:
596
+ self.process.wait(timeout=5)
597
+ except subprocess.TimeoutExpired:
598
+ # Force kill if it doesn't exit gracefully
599
+ os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
600
+ try:
601
+ self.process.wait(timeout=2)
602
+ except subprocess.TimeoutExpired:
603
+ os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
604
+
605
+ except Exception as e:
606
+ print(f"Error during cleanup: {e}")
607
+ finally:
608
+ self.process = None
609
+
610
+ print("โœ… Shell cleanup completed")
@@ -2411,7 +2411,7 @@ def switch_to_openai_models():
2411
2411
 
2412
2412
  def get_current_debug_model():
2413
2413
  """Get the currently configured debugging model preference"""
2414
- return os.environ.get("GITARSENAL_DEBUG_MODEL", "anthropic")
2414
+ return os.environ.get("GITARSENAL_DEBUG_MODEL", "openai")
2415
2415
 
2416
2416
  def call_llm_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
2417
2417
  """Unified function to call LLM for debugging - routes to OpenAI or Anthropic based on configuration"""
@@ -5001,4 +5001,4 @@ if __name__ == "__main__":
5001
5001
  # print(f"\nโŒ Error: {e}")
5002
5002
  # print("๐Ÿงน Cleaning up resources...")
5003
5003
  cleanup_modal_token()
5004
- sys.exit(1)
5004
+