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