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.
- package/.venv_status.json +1 -1
- package/package.json +1 -1
- package/python/__pycache__/auth_manager.cpython-313.pyc +0 -0
- package/python/__pycache__/command_manager.cpython-313.pyc +0 -0
- package/python/__pycache__/fetch_modal_tokens.cpython-313.pyc +0 -0
- package/python/__pycache__/llm_debugging.cpython-313.pyc +0 -0
- package/python/__pycache__/modal_container.cpython-313.pyc +0 -0
- package/python/__pycache__/shell.cpython-313.pyc +0 -0
- package/python/api_integration.py +0 -0
- package/python/command_manager.py +613 -0
- package/python/credentials_manager.py +0 -0
- package/python/fetch_modal_tokens.py +0 -0
- package/python/fix_modal_token.py +0 -0
- package/python/fix_modal_token_advanced.py +0 -0
- package/python/gitarsenal.py +0 -0
- package/python/gitarsenal_proxy_client.py +0 -0
- package/python/llm_debugging.py +1369 -0
- package/python/modal_container.py +626 -0
- package/python/setup.py +15 -0
- package/python/setup_modal_token.py +0 -39
- package/python/shell.py +627 -0
- package/python/test_modalSandboxScript.py +75 -2639
- package/scripts/postinstall.js +22 -23
- package/python/__pycache__/credentials_manager.cpython-313.pyc +0 -0
- package/python/__pycache__/test_modalSandboxScript.cpython-313.pyc +0 -0
- package/python/__pycache__/test_modalSandboxScript_stable.cpython-313.pyc +0 -0
- package/python/debug_delete.py +0 -167
- package/python/documentation.py +0 -76
- package/python/fix_setup_commands.py +0 -116
- package/python/modal_auth_patch.py +0 -178
- package/python/modal_proxy_service.py +0 -665
- package/python/modal_token_solution.py +0 -293
- package/python/test_dynamic_commands.py +0 -147
- 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")
|
package/python/shell.py
ADDED
|
@@ -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")
|