gitarsenal-cli 1.7.10 → 1.8.3

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.
@@ -38,7 +38,7 @@ if args.proxy_api_key:
38
38
  class PersistentShell:
39
39
  """A persistent bash shell using subprocess.Popen for executing commands with state persistence."""
40
40
 
41
- def __init__(self, working_dir="/root", timeout=240):
41
+ def __init__(self, working_dir="/root", timeout=60):
42
42
  self.working_dir = working_dir
43
43
  self.timeout = timeout
44
44
  self.process = None
@@ -51,6 +51,9 @@ class PersistentShell:
51
51
  self.command_counter = 0
52
52
  self.is_running = False
53
53
  self.virtual_env_path = None # Track activated virtual environment
54
+ self.suggested_alternative = None # Store suggested alternative commands
55
+ self.should_remove_command = False # Flag to indicate if a command should be removed
56
+ self.removal_reason = None # Reason for removing a command
54
57
 
55
58
  def start(self):
56
59
  """Start the persistent bash shell."""
@@ -197,7 +200,7 @@ class PersistentShell:
197
200
 
198
201
  # Wait for shell to be ready (prompt should be visible)
199
202
  if not self.wait_for_prompt(timeout=2):
200
- print("⚠️ Shell not ready, waiting...")
203
+ # print("⚠️ Shell not ready, waiting...")
201
204
  time.sleep(0.5)
202
205
 
203
206
  # For source commands, we need special handling
@@ -288,7 +291,25 @@ class PersistentShell:
288
291
  for line in current_stderr:
289
292
  if line.strip(): # Skip empty lines
290
293
  command_stderr.append(line)
291
-
294
+
295
+ # Check if command is waiting for user input
296
+ if not found_marker and time.time() - start_time > 5: # Wait at least 5 seconds before checking
297
+ if self._is_waiting_for_input(command_stdout, command_stderr):
298
+ print("⚠️ Command appears to be waiting for user input")
299
+ # Try to handle the input requirement
300
+ input_handled = self._handle_input_requirement(command, command_stdout, command_stderr)
301
+
302
+ if input_handled is True and self.should_remove_command:
303
+ # If LLM suggested to remove the command
304
+ self._send_command_raw("\x03") # Send Ctrl+C
305
+ time.sleep(0.5)
306
+ return False, '\n'.join(command_stdout), f"Command removed - {self.removal_reason}"
307
+ elif not input_handled:
308
+ # If we couldn't handle the input, abort the command
309
+ self._send_command_raw("\x03") # Send Ctrl+C
310
+ time.sleep(0.5)
311
+ return False, '\n'.join(command_stdout), "Command aborted - requires user input"
312
+
292
313
  time.sleep(0.1)
293
314
 
294
315
  if not found_marker:
@@ -306,13 +327,12 @@ class PersistentShell:
306
327
 
307
328
  if success:
308
329
  if stdout_text:
309
- print("")
310
330
  print(f"✅ Output: {stdout_text}")
311
331
  # Track virtual environment activation
312
332
  if command.strip().startswith("source ") and "/bin/activate" in command:
313
333
  venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
314
334
  self.virtual_env_path = venv_path
315
- # print(f"✅ Virtual environment activated: {venv_path}")
335
+ print(f"✅ Virtual environment activated: {venv_path}")
316
336
  else:
317
337
  print(f"❌ Command failed with exit code: {exit_code}")
318
338
  if stderr_text:
@@ -323,6 +343,215 @@ class PersistentShell:
323
343
 
324
344
  return success, stdout_text, stderr_text
325
345
 
346
+ def _is_waiting_for_input(self, stdout_lines, stderr_lines):
347
+ """Detect if a command is waiting for user input."""
348
+ # Common patterns that indicate waiting for user input
349
+ input_patterns = [
350
+ r'(?i)(y/n|yes/no)\??\s*$', # Yes/No prompts
351
+ r'(?i)password:?\s*$', # Password prompts
352
+ r'(?i)continue\??\s*$', # Continue prompts
353
+ r'(?i)proceed\??\s*$', # Proceed prompts
354
+ r'\[\s*[Yy]/[Nn]\s*\]\s*$', # [Y/n] style prompts
355
+ r'(?i)username:?\s*$', # Username prompts
356
+ r'(?i)token:?\s*$', # Token prompts
357
+ r'(?i)api key:?\s*$', # API key prompts
358
+ r'(?i)press enter to continue', # Press enter prompts
359
+ r'(?i)select an option:?\s*$', # Selection prompts
360
+ r'(?i)choose an option:?\s*$', # Choice prompts
361
+ ]
362
+
363
+ # Check the last few lines of stdout and stderr for input patterns
364
+ last_lines = []
365
+ if stdout_lines:
366
+ last_lines.extend(stdout_lines[-3:]) # Check last 3 lines of stdout
367
+ if stderr_lines:
368
+ last_lines.extend(stderr_lines[-3:]) # Check last 3 lines of stderr
369
+
370
+ for line in last_lines:
371
+ for pattern in input_patterns:
372
+ if re.search(pattern, line):
373
+ print(f"🔍 Detected input prompt: {line}")
374
+ return True
375
+
376
+ # Check if there's no output for a while but the command is still running
377
+ if len(stdout_lines) == 0 and len(stderr_lines) == 0:
378
+ # This might be a command waiting for input without a prompt
379
+ # We'll be cautious and only return True if we're sure
380
+ return False
381
+
382
+ return False
383
+
384
+ def _handle_input_requirement(self, command, stdout_lines, stderr_lines):
385
+ """Attempt to handle commands that require input."""
386
+ # Extract the last few lines to analyze what kind of input is needed
387
+ last_lines = []
388
+ if stdout_lines:
389
+ last_lines.extend(stdout_lines[-3:])
390
+ if stderr_lines:
391
+ last_lines.extend(stderr_lines[-3:])
392
+
393
+ last_line = last_lines[-1] if last_lines else ""
394
+
395
+ # Try to determine what kind of input is needed
396
+ if re.search(r'(?i)(y/n|yes/no|\[y/n\])', last_line):
397
+ # For yes/no prompts, usually 'yes' is safer
398
+ print("🔧 Auto-responding with 'y' to yes/no prompt")
399
+ self._send_command_raw("y")
400
+ return True
401
+
402
+ elif re.search(r'(?i)password', last_line):
403
+ # For password prompts, check if we have stored credentials
404
+ stored_creds = get_stored_credentials()
405
+ if stored_creds and 'ssh_password' in stored_creds:
406
+ print("🔧 Auto-responding with stored SSH password")
407
+ self._send_command_raw(stored_creds['ssh_password'])
408
+ return True
409
+ else:
410
+ print("⚠️ Password prompt detected but no stored password available")
411
+ return False
412
+
413
+ elif re.search(r'(?i)token|api.key', last_line):
414
+ # For token/API key prompts
415
+ stored_creds = get_stored_credentials()
416
+ if stored_creds:
417
+ if 'openai_api_key' in stored_creds and re.search(r'(?i)openai|api.key', last_line):
418
+ print("🔧 Auto-responding with stored OpenAI API key")
419
+ self._send_command_raw(stored_creds['openai_api_key'])
420
+ return True
421
+ elif 'hf_token' in stored_creds and re.search(r'(?i)hugg|hf|token', last_line):
422
+ print("🔧 Auto-responding with stored Hugging Face token")
423
+ self._send_command_raw(stored_creds['hf_token'])
424
+ return True
425
+
426
+ print("⚠️ Token/API key prompt detected but no matching stored credentials")
427
+ return False
428
+
429
+ elif re.search(r'(?i)press enter|continue|proceed', last_line):
430
+ # For "press enter to continue" prompts
431
+ print("🔧 Auto-responding with Enter to continue")
432
+ self._send_command_raw("") # Empty string sends just Enter
433
+ return True
434
+
435
+ # If we can't determine the type of input needed
436
+ print("⚠️ Couldn't determine the type of input needed")
437
+
438
+ # Try to use LLM to suggest an alternative command
439
+ try:
440
+ # Get current working directory for context
441
+ cwd = self.get_cwd()
442
+
443
+ # Reset command removal flags
444
+ self.should_remove_command = False
445
+ self.removal_reason = None
446
+
447
+ # Call LLM to suggest an alternative
448
+ alternative = self._suggest_alternative_command(command, stdout_lines, stderr_lines, cwd)
449
+
450
+ # Check if LLM suggested to remove the command
451
+ if self.should_remove_command:
452
+ print(f"🚫 Command will be removed: {self.removal_reason}")
453
+ return True # Return True to indicate the command has been handled (by removing it)
454
+
455
+ if alternative:
456
+ print(f"🔧 LLM suggested alternative command: {alternative}")
457
+ # We don't execute the alternative here, but return False so the calling code
458
+ # can handle it (e.g., by adding it to the command list)
459
+
460
+ # Store the suggested alternative for later use
461
+ self.suggested_alternative = alternative
462
+ return False
463
+ except Exception as e:
464
+ print(f"⚠️ Error getting LLM suggestion: {e}")
465
+
466
+ return False
467
+
468
+ def _suggest_alternative_command(self, command, stdout_lines, stderr_lines, current_dir):
469
+ """Use LLM to suggest an alternative command that doesn't require user input."""
470
+ try:
471
+ # Get API key
472
+ api_key = os.environ.get("OPENAI_API_KEY")
473
+ if not api_key:
474
+ # Try to load from saved file
475
+ key_file = os.path.expanduser("~/.gitarsenal/openai_key")
476
+ if os.path.exists(key_file):
477
+ with open(key_file, "r") as f:
478
+ api_key = f.read().strip()
479
+
480
+ if not api_key:
481
+ print("⚠️ No OpenAI API key available for suggesting alternative command")
482
+ return None
483
+
484
+ # Prepare the prompt
485
+ stdout_text = '\n'.join(stdout_lines[-10:]) if stdout_lines else ""
486
+ stderr_text = '\n'.join(stderr_lines[-10:]) if stderr_lines else ""
487
+
488
+ prompt = f"""
489
+ The command '{command}' appears to be waiting for user input.
490
+
491
+ Current directory: {current_dir}
492
+
493
+ Last stdout output:
494
+ {stdout_text}
495
+
496
+ Last stderr output:
497
+ {stderr_text}
498
+
499
+ Please analyze this command and determine if it's useful to continue with it.
500
+ If it's useful, suggest an alternative command that achieves the same goal but doesn't require user input.
501
+ For example, add flags like -y, --yes, --no-input, etc., or provide the required input in the command.
502
+
503
+ If the command is not useful or cannot be executed non-interactively, respond with "REMOVE_COMMAND" and explain why.
504
+
505
+ Format your response as:
506
+ ALTERNATIVE: <alternative command>
507
+ or
508
+ REMOVE_COMMAND: <reason>
509
+ """
510
+
511
+ # Call OpenAI API
512
+ import openai
513
+ client = openai.OpenAI(api_key=api_key)
514
+
515
+ response = client.chat.completions.create(
516
+ model="gpt-4o-mini",
517
+ messages=[
518
+ {"role": "system", "content": "You are a helpful assistant that suggests alternative commands that don't require user input."},
519
+ {"role": "user", "content": prompt}
520
+ ],
521
+ max_tokens=150,
522
+ temperature=0.7
523
+ )
524
+
525
+ response_text = response.choices[0].message.content.strip()
526
+
527
+ # Check if the response suggests removing the command
528
+ if response_text.startswith("REMOVE_COMMAND:"):
529
+ reason = response_text.replace("REMOVE_COMMAND:", "").strip()
530
+ print(f"🚫 LLM suggests removing command: {reason}")
531
+ self.should_remove_command = True
532
+ self.removal_reason = reason
533
+ return None
534
+
535
+ # Extract the alternative command
536
+ if response_text.startswith("ALTERNATIVE:"):
537
+ alternative_command = response_text.replace("ALTERNATIVE:", "").strip()
538
+ else:
539
+ # Try to extract the command from a free-form response
540
+ lines = response_text.split('\n')
541
+ for line in lines:
542
+ line = line.strip()
543
+ if line and not line.startswith(('Here', 'I', 'You', 'The', 'This', 'Use', 'Try')):
544
+ alternative_command = line
545
+ break
546
+ else:
547
+ alternative_command = lines[0].strip()
548
+
549
+ return alternative_command
550
+
551
+ except Exception as e:
552
+ print(f"⚠️ Error suggesting alternative command: {e}")
553
+ return None
554
+
326
555
  def _clear_lines(self):
327
556
  """Clear both output line lists."""
328
557
  with self.stdout_lock:
@@ -678,6 +907,337 @@ class CommandListManager:
678
907
 
679
908
  print(f"🔧 Added {len(added_fixes)} LLM-suggested fixes to command list")
680
909
  return added_fixes
910
+
911
+ def should_skip_original_command(self, original_command, fix_command, fix_stdout, fix_stderr, api_key=None):
912
+ """
913
+ Use LLM to determine if the original command should be skipped after a successful fix.
914
+
915
+ Args:
916
+ original_command: The original command that failed
917
+ fix_command: The fix command that succeeded
918
+ fix_stdout: The stdout from the fix command
919
+ fix_stderr: The stderr from the fix command
920
+ api_key: OpenAI API key
921
+
922
+ Returns:
923
+ tuple: (should_skip, reason)
924
+ """
925
+ try:
926
+ # Get API key if not provided
927
+ if not api_key:
928
+ api_key = os.environ.get("OPENAI_API_KEY")
929
+ if not api_key:
930
+ # Try to load from saved file
931
+ key_file = os.path.expanduser("~/.gitarsenal/openai_key")
932
+ if os.path.exists(key_file):
933
+ with open(key_file, "r") as f:
934
+ api_key = f.read().strip()
935
+
936
+ if not api_key:
937
+ print("⚠️ No OpenAI API key available for command list analysis")
938
+ return False, "No API key available"
939
+
940
+ # Get all commands for context
941
+ all_commands = self.get_all_commands()
942
+ commands_context = "\n".join([f"{i+1}. {cmd['command']} - {cmd['status']}" for i, cmd in enumerate(all_commands)])
943
+
944
+ # Prepare the prompt
945
+ prompt = f"""
946
+ I need to determine if an original command should be skipped after a successful fix command.
947
+
948
+ Original command (failed): {original_command}
949
+ Fix command (succeeded): {fix_command}
950
+
951
+ Fix command stdout:
952
+ {fix_stdout}
953
+
954
+ Fix command stderr:
955
+ {fix_stderr}
956
+
957
+ Current command list:
958
+ {commands_context}
959
+
960
+ Based on this information, should I skip running the original command again?
961
+ Consider:
962
+ 1. If the fix command already accomplished what the original command was trying to do
963
+ 2. If running the original command again would be redundant or cause errors
964
+ 3. If the original command is still necessary after the fix
965
+
966
+ Respond with ONLY:
967
+ SKIP: <reason>
968
+ or
969
+ RUN: <reason>
970
+ """
971
+
972
+ # Call OpenAI API
973
+ import openai
974
+ client = openai.OpenAI(api_key=api_key)
975
+
976
+ print("🔍 Analyzing if original command should be skipped...")
977
+
978
+ response = client.chat.completions.create(
979
+ model="gpt-3.5-turbo",
980
+ messages=[
981
+ {"role": "system", "content": "You are a helpful assistant that analyzes command execution."},
982
+ {"role": "user", "content": prompt}
983
+ ],
984
+ max_tokens=100,
985
+ temperature=0.3
986
+ )
987
+
988
+ response_text = response.choices[0].message.content.strip()
989
+
990
+ # Parse the response
991
+ if response_text.startswith("SKIP:"):
992
+ reason = response_text.replace("SKIP:", "").strip()
993
+ print(f"🔍 LLM suggests skipping original command: {reason}")
994
+ return True, reason
995
+ elif response_text.startswith("RUN:"):
996
+ reason = response_text.replace("RUN:", "").strip()
997
+ print(f"🔍 LLM suggests running original command: {reason}")
998
+ return False, reason
999
+ else:
1000
+ # Try to interpret a free-form response
1001
+ if "skip" in response_text.lower() and "should" in response_text.lower():
1002
+ print(f"🔍 Interpreting response as SKIP: {response_text}")
1003
+ return True, response_text
1004
+ else:
1005
+ print(f"🔍 Interpreting response as RUN: {response_text}")
1006
+ return False, response_text
1007
+
1008
+ except Exception as e:
1009
+ print(f"⚠️ Error analyzing command skip decision: {e}")
1010
+ return False, f"Error: {e}"
1011
+
1012
+ def replace_command(self, command_index, new_command, reason=""):
1013
+ """
1014
+ Replace a command in the list with a new command.
1015
+
1016
+ Args:
1017
+ command_index: The index of the command to replace
1018
+ new_command: The new command to use
1019
+ reason: The reason for the replacement
1020
+
1021
+ Returns:
1022
+ bool: True if the command was replaced, False otherwise
1023
+ """
1024
+ if 0 <= command_index < len(self.commands):
1025
+ old_command = self.commands[command_index]['command']
1026
+ self.commands[command_index]['command'] = new_command
1027
+ self.commands[command_index]['status'] = 'pending' # Reset status
1028
+ self.commands[command_index]['stdout'] = ''
1029
+ self.commands[command_index]['stderr'] = ''
1030
+ self.commands[command_index]['execution_time'] = None
1031
+ self.commands[command_index]['replacement_reason'] = reason
1032
+
1033
+ print(f"🔄 Replaced command {command_index + 1}: '{old_command}' with '{new_command}'")
1034
+ print(f"🔍 Reason: {reason}")
1035
+ return True
1036
+ else:
1037
+ print(f"❌ Invalid command index for replacement: {command_index}")
1038
+ return False
1039
+
1040
+ def update_command_list_with_llm(self, api_key=None):
1041
+ """
1042
+ Use LLM to analyze and update the entire command list.
1043
+
1044
+ Args:
1045
+ api_key: OpenAI API key
1046
+
1047
+ Returns:
1048
+ bool: True if the list was updated, False otherwise
1049
+ """
1050
+ try:
1051
+ # Get API key if not provided
1052
+ if not api_key:
1053
+ api_key = os.environ.get("OPENAI_API_KEY")
1054
+ if not api_key:
1055
+ # Try to load from saved file
1056
+ key_file = os.path.expanduser("~/.gitarsenal/openai_key")
1057
+ if os.path.exists(key_file):
1058
+ with open(key_file, "r") as f:
1059
+ api_key = f.read().strip()
1060
+
1061
+ if not api_key:
1062
+ print("⚠️ No OpenAI API key available for command list analysis")
1063
+ return False
1064
+
1065
+ # Get all commands for context
1066
+ all_commands = self.get_all_commands()
1067
+ commands_context = "\n".join([f"{i+1}. {cmd['command']} - {cmd['status']}"
1068
+ for i, cmd in enumerate(all_commands)])
1069
+
1070
+ # Get executed commands with their outputs for context
1071
+ executed_context = ""
1072
+ for cmd in self.executed_commands:
1073
+ executed_context += f"Command: {cmd['command']}\n"
1074
+ executed_context += f"Status: {cmd['status']}\n"
1075
+ if cmd['stdout']:
1076
+ executed_context += f"Stdout: {cmd['stdout'][:500]}...\n" if len(cmd['stdout']) > 500 else f"Stdout: {cmd['stdout']}\n"
1077
+ if cmd['stderr']:
1078
+ executed_context += f"Stderr: {cmd['stderr'][:500]}...\n" if len(cmd['stderr']) > 500 else f"Stderr: {cmd['stderr']}\n"
1079
+ executed_context += "\n"
1080
+
1081
+ # Prepare the prompt
1082
+ prompt = f"""
1083
+ I need you to analyze and optimize this command list. Some commands have been executed,
1084
+ and some are still pending. Based on what has already been executed, I need you to:
1085
+
1086
+ 1. Identify any pending commands that are now redundant or unnecessary
1087
+ 2. Identify any pending commands that should be modified based on previous command results
1088
+ 3. Suggest any new commands that should be added
1089
+
1090
+ Current command list:
1091
+ {commands_context}
1092
+
1093
+ Details of executed commands:
1094
+ {executed_context}
1095
+
1096
+ For each pending command (starting from the next command to be executed), tell me if it should be:
1097
+ 1. KEEP: Keep the command as is
1098
+ 2. SKIP: Skip the command (mark as completed without running)
1099
+ 3. MODIFY: Modify the command (provide the new command)
1100
+ 4. ADD_AFTER: Add a new command after this one
1101
+
1102
+ Format your response as a JSON array of actions:
1103
+ [
1104
+ {{
1105
+ "command_index": <index>,
1106
+ "action": "KEEP|SKIP|MODIFY|ADD_AFTER",
1107
+ "new_command": "<new command if MODIFY or ADD_AFTER>",
1108
+ "reason": "<reason for this action>"
1109
+ }},
1110
+ ...
1111
+ ]
1112
+
1113
+ Only include commands that need changes (SKIP, MODIFY, ADD_AFTER), not KEEP actions.
1114
+ """
1115
+
1116
+ # Call OpenAI API
1117
+ import openai
1118
+ import json
1119
+ client = openai.OpenAI(api_key=api_key)
1120
+
1121
+ print("🔍 Analyzing command list for optimizations...")
1122
+
1123
+ response = client.chat.completions.create(
1124
+ model="gpt-4o-mini", # Use a more capable model for this complex task
1125
+ messages=[
1126
+ {"role": "system", "content": "You are a helpful assistant that analyzes and optimizes command lists."},
1127
+ {"role": "user", "content": prompt}
1128
+ ],
1129
+ max_tokens=1000,
1130
+ temperature=0.2
1131
+ )
1132
+
1133
+ response_text = response.choices[0].message.content.strip()
1134
+
1135
+ # Extract JSON from the response
1136
+ try:
1137
+ # Find JSON array in the response
1138
+ json_match = re.search(r'\[\s*\{.*\}\s*\]', response_text, re.DOTALL)
1139
+ if json_match:
1140
+ json_str = json_match.group(0)
1141
+ actions = json.loads(json_str)
1142
+ else:
1143
+ # Try to parse the entire response as JSON
1144
+ actions = json.loads(response_text)
1145
+
1146
+ if not isinstance(actions, list):
1147
+ print("❌ Invalid response format from LLM - not a list")
1148
+ return False
1149
+
1150
+ # Apply the suggested changes
1151
+ changes_made = 0
1152
+ commands_added = 0
1153
+
1154
+ # Process in reverse order to avoid index shifting issues
1155
+ for action in sorted(actions, key=lambda x: x.get('command_index', 0), reverse=True):
1156
+ cmd_idx = action.get('command_index')
1157
+ action_type = action.get('action')
1158
+ new_cmd = action.get('new_command', '')
1159
+ reason = action.get('reason', 'No reason provided')
1160
+
1161
+ if cmd_idx is None or action_type is None:
1162
+ continue
1163
+
1164
+ # Convert to 0-based index if needed
1165
+ if cmd_idx > 0: # Assume 1-based index from LLM
1166
+ cmd_idx -= 1
1167
+
1168
+ # Skip if the command index is invalid
1169
+ if cmd_idx < 0 or cmd_idx >= len(self.commands):
1170
+ print(f"❌ Invalid command index: {cmd_idx}")
1171
+ continue
1172
+
1173
+ # Skip if the command has already been executed
1174
+ if self.commands[cmd_idx]['status'] != 'pending':
1175
+ print(f"⚠️ Command {cmd_idx + 1} already executed, skipping action")
1176
+ continue
1177
+
1178
+ if action_type == "SKIP":
1179
+ # Mark the command as successful without running it
1180
+ self.mark_command_executed(
1181
+ cmd_idx, 'main', True,
1182
+ f"Command skipped: {reason}",
1183
+ "", 0
1184
+ )
1185
+ print(f"🔄 Skipped command {cmd_idx + 1}: {reason}")
1186
+ changes_made += 1
1187
+
1188
+ elif action_type == "MODIFY":
1189
+ if new_cmd:
1190
+ if self.replace_command(cmd_idx, new_cmd, reason):
1191
+ changes_made += 1
1192
+ else:
1193
+ print(f"❌ No new command provided for MODIFY action on command {cmd_idx + 1}")
1194
+
1195
+ elif action_type == "ADD_AFTER":
1196
+ if new_cmd:
1197
+ # Add new command after the current one
1198
+ insert_idx = cmd_idx + 1
1199
+ new_cmd_obj = {
1200
+ 'command': new_cmd,
1201
+ 'status': 'pending',
1202
+ 'index': insert_idx,
1203
+ 'stdout': '',
1204
+ 'stderr': '',
1205
+ 'execution_time': None,
1206
+ 'fix_attempts': 0,
1207
+ 'max_fix_attempts': 3,
1208
+ 'added_reason': reason
1209
+ }
1210
+
1211
+ # Insert the new command
1212
+ self.commands.insert(insert_idx, new_cmd_obj)
1213
+
1214
+ # Update indices for all commands after insertion
1215
+ for i in range(insert_idx + 1, len(self.commands)):
1216
+ self.commands[i]['index'] = i
1217
+
1218
+ print(f"➕ Added new command after {cmd_idx + 1}: '{new_cmd}'")
1219
+ print(f"🔍 Reason: {reason}")
1220
+ commands_added += 1
1221
+ else:
1222
+ print(f"❌ No new command provided for ADD_AFTER action on command {cmd_idx + 1}")
1223
+
1224
+ # Update total commands count
1225
+ self.total_commands = len(self.commands)
1226
+
1227
+ print(f"✅ Command list updated: {changes_made} changes made, {commands_added} commands added")
1228
+ return changes_made > 0 or commands_added > 0
1229
+
1230
+ except json.JSONDecodeError as e:
1231
+ print(f"❌ Failed to parse LLM response as JSON: {e}")
1232
+ print(f"Raw response: {response_text}")
1233
+ return False
1234
+ except Exception as e:
1235
+ print(f"❌ Error updating command list: {e}")
1236
+ return False
1237
+
1238
+ except Exception as e:
1239
+ print(f"⚠️ Error analyzing command list: {e}")
1240
+ return False
681
1241
 
682
1242
 
683
1243
  # Import the fetch_modal_tokens module
@@ -1138,7 +1698,7 @@ Do not provide any explanations, just the exact command to run.
1138
1698
  "max_tokens": 300
1139
1699
  }
1140
1700
 
1141
- # print(f"🔍 DEBUG: Payload prepared, prompt length: {len(prompt)}")
1701
+ print(f"🔍 DEBUG: Payload prepared, prompt length: {len(prompt)}")
1142
1702
 
1143
1703
  # Add specific handling for common errors
1144
1704
  last_error = None
@@ -1150,8 +1710,8 @@ Do not provide any explanations, just the exact command to run.
1150
1710
  print(f"⏱️ Retrying in {wait_time:.1f} seconds... (attempt {attempt+1}/{retries+1})")
1151
1711
  time.sleep(wait_time)
1152
1712
 
1153
- # print(f"🤖 Calling OpenAI with {model_name} model to debug the failed command...")
1154
- # print(f"🔍 DEBUG: Making POST request to OpenAI API...")
1713
+ print(f"🤖 Calling OpenAI with {model_name} model to debug the failed command...")
1714
+ print(f"🔍 DEBUG: Making POST request to OpenAI API...")
1155
1715
  response = requests.post(
1156
1716
  "https://api.openai.com/v1/chat/completions",
1157
1717
  headers=headers,
@@ -1159,8 +1719,41 @@ Do not provide any explanations, just the exact command to run.
1159
1719
  timeout=45 # Increased timeout for reliability
1160
1720
  )
1161
1721
 
1162
- # print(f"🔍 DEBUG: Response received, status code: {response.status_code}")
1722
+ print(f"🔍 DEBUG: Response received, status code: {response.status_code}")
1163
1723
 
1724
+ # Handle specific status codes
1725
+ if response.status_code == 200:
1726
+ print(f"🔍 DEBUG: Success! Response length: {len(response.text)}")
1727
+ return response.json(), None
1728
+ elif response.status_code == 401:
1729
+ error_msg = "Authentication error: Invalid API key"
1730
+ print(f"❌ {error_msg}")
1731
+ print(f"🔍 DEBUG: Response text: {response.text}")
1732
+ # Don't retry auth errors
1733
+ return None, error_msg
1734
+ elif response.status_code == 429:
1735
+ error_msg = "Rate limit exceeded or quota reached"
1736
+ print(f"⚠️ {error_msg}")
1737
+ print(f"🔍 DEBUG: Response text: {response.text}")
1738
+ # Always retry rate limit errors with increasing backoff
1739
+ last_error = error_msg
1740
+ continue
1741
+ elif response.status_code == 500:
1742
+ error_msg = "OpenAI server error"
1743
+ print(f"⚠️ {error_msg}")
1744
+ print(f"🔍 DEBUG: Response text: {response.text}")
1745
+ # Retry server errors
1746
+ last_error = error_msg
1747
+ continue
1748
+ else:
1749
+ error_msg = f"Status code: {response.status_code}, Response: {response.text}"
1750
+ print(f"⚠️ OpenAI API error: {error_msg}")
1751
+ print(f"🔍 DEBUG: Full response text: {response.text}")
1752
+ last_error = error_msg
1753
+ # Only retry if we have attempts left
1754
+ if attempt < retries:
1755
+ continue
1756
+ return None, error_msg
1164
1757
  except requests.exceptions.Timeout:
1165
1758
  error_msg = "Request timed out"
1166
1759
  # print(f"⚠️ {error_msg}")
@@ -1780,20 +2373,26 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1780
2373
  # Start SSH service
1781
2374
  subprocess.run(["service", "ssh", "start"], check=True)
1782
2375
 
1783
- # Run setup commands if provided using PersistentShell and CommandListManager
2376
+ # Preprocess setup commands using LLM to inject credentials
1784
2377
  if setup_commands:
1785
- print(f"⚙️ Running {len(setup_commands)} setup commands with dynamic command list...")
2378
+ print(f"🔧 Preprocessing {len(setup_commands)} setup commands with LLM to inject credentials...")
2379
+ api_key = os.environ.get("OPENAI_API_KEY")
2380
+ processed_commands = preprocess_commands_with_llm(setup_commands, stored_credentials, api_key)
2381
+ print(f"⚙️ Running {len(processed_commands)} preprocessed setup commands with dynamic command list...")
1786
2382
 
1787
- # Create command list manager
1788
- cmd_manager = CommandListManager(setup_commands)
2383
+ # Create command list manager with processed commands
2384
+ cmd_manager = CommandListManager(processed_commands)
1789
2385
 
1790
2386
  # Create persistent shell instance starting in /root
1791
- shell = PersistentShell(working_dir="/root", timeout=240)
2387
+ shell = PersistentShell(working_dir="/root", timeout=300)
1792
2388
 
1793
2389
  try:
1794
2390
  # Start the persistent shell
1795
2391
  shell.start()
1796
2392
 
2393
+ # Track how many commands have been executed since last analysis
2394
+ commands_since_analysis = 0
2395
+
1797
2396
  # Execute commands using the command list manager
1798
2397
  while cmd_manager.has_pending_commands():
1799
2398
  # Get next command to execute
@@ -1805,6 +2404,13 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1805
2404
  # Print status before executing
1806
2405
  cmd_manager.print_status()
1807
2406
 
2407
+ # Periodically analyze and update the command list
2408
+ if commands_since_analysis >= 3 and cmd_type == 'main':
2409
+ print("\n🔍 Periodic command list analysis...")
2410
+ api_key = os.environ.get("OPENAI_API_KEY")
2411
+ cmd_manager.update_command_list_with_llm(api_key)
2412
+ commands_since_analysis = 0
2413
+
1808
2414
  # Execute the command
1809
2415
  if cmd_type == 'main':
1810
2416
  cmd_text = next_cmd['command']
@@ -1812,14 +2418,33 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1812
2418
  print(f"📋 Executing main command {cmd_index + 1}/{cmd_manager.total_commands}: {cmd_text}")
1813
2419
 
1814
2420
  start_time = time.time()
1815
- success, stdout, stderr = shell.execute(cmd_text, timeout=240)
2421
+ success, stdout, stderr = shell.execute(cmd_text, timeout=300)
1816
2422
  execution_time = time.time() - start_time
1817
2423
 
2424
+ # Check if the command was aborted due to waiting for input and an alternative was suggested
2425
+ if not success and "Command aborted - requires user input" in stderr and shell.suggested_alternative:
2426
+ alternative_cmd = shell.suggested_alternative
2427
+ print(f"🔄 Command aborted due to input requirement. Adding suggested alternative: {alternative_cmd}")
2428
+
2429
+ # Add the alternative command with high priority
2430
+ cmd_manager.add_command_dynamically(alternative_cmd, priority='high')
2431
+
2432
+ # Clear the suggested alternative
2433
+ shell.suggested_alternative = None
2434
+ # Check if the command should be removed as suggested by LLM
2435
+ elif not success and stderr.startswith("Command removed -"):
2436
+ reason = stderr.replace("Command removed -", "").strip()
2437
+ print(f"🚫 Removed command as suggested by LLM: {reason}")
2438
+ # We don't need to do anything else, just mark it as executed and move on
2439
+
1818
2440
  # Mark command as executed
1819
2441
  cmd_manager.mark_command_executed(
1820
2442
  cmd_index, 'main', success, stdout, stderr, execution_time
1821
2443
  )
1822
2444
 
2445
+ # Increment counter for periodic analysis
2446
+ commands_since_analysis += 1
2447
+
1823
2448
  if not success:
1824
2449
  print(f"⚠️ Command failed, attempting LLM debugging...")
1825
2450
 
@@ -1840,7 +2465,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1840
2465
  # Execute the fix command
1841
2466
  print(f"🔄 Running suggested fix command: {fix_command}")
1842
2467
  fix_start_time = time.time()
1843
- fix_success, fix_stdout, fix_stderr = shell.execute(fix_command, timeout=240)
2468
+ fix_success, fix_stdout, fix_stderr = shell.execute(fix_command, timeout=300)
1844
2469
  fix_execution_time = time.time() - fix_start_time
1845
2470
 
1846
2471
  # Mark fix command as executed
@@ -1851,21 +2476,49 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1851
2476
  if fix_success:
1852
2477
  print(f"✅ Fix command succeeded")
1853
2478
 
1854
- # Retry the original command
1855
- print(f"🔄 Retrying original command: {cmd_text}")
1856
- retry_start_time = time.time()
1857
- retry_success, retry_stdout, retry_stderr = shell.execute(cmd_text, timeout=240)
1858
- retry_execution_time = time.time() - retry_start_time
1859
-
1860
- # Update the original command status
1861
- cmd_manager.mark_command_executed(
1862
- cmd_index, 'main', retry_success, retry_stdout, retry_stderr, retry_execution_time
2479
+ # Check if we should skip the original command
2480
+ api_key = os.environ.get("OPENAI_API_KEY")
2481
+ should_skip, skip_reason = cmd_manager.should_skip_original_command(
2482
+ cmd_text, fix_command, fix_stdout, fix_stderr, api_key
1863
2483
  )
1864
2484
 
1865
- if retry_success:
1866
- print(f" Original command succeeded after fix!")
2485
+ if should_skip:
2486
+ print(f"🔄 Skipping original command: {skip_reason}")
2487
+
2488
+ # Mark the original command as successful without running it
2489
+ cmd_manager.mark_command_executed(
2490
+ cmd_index, 'main', True,
2491
+ f"Command skipped after successful fix: {skip_reason}",
2492
+ "", time.time() - start_time
2493
+ )
2494
+
2495
+ print(f"✅ Original command marked as successful (skipped)")
2496
+
2497
+ # After a successful fix and skipping the original command,
2498
+ # analyze and update the entire command list
2499
+ print("\n🔍 Analyzing and updating remaining commands based on fix results...")
2500
+ cmd_manager.update_command_list_with_llm(api_key)
1867
2501
  else:
1868
- print(f"⚠️ Original command still failed after fix, continuing...")
2502
+ # Retry the original command
2503
+ print(f"🔄 Retrying original command: {cmd_text}")
2504
+ retry_start_time = time.time()
2505
+ retry_success, retry_stdout, retry_stderr = shell.execute(cmd_text, timeout=300)
2506
+ retry_execution_time = time.time() - retry_start_time
2507
+
2508
+ # Update the original command status
2509
+ cmd_manager.mark_command_executed(
2510
+ cmd_index, 'main', retry_success, retry_stdout, retry_stderr, retry_execution_time
2511
+ )
2512
+
2513
+ if retry_success:
2514
+ print(f"✅ Original command succeeded after fix!")
2515
+
2516
+ # After a successful fix and successful retry,
2517
+ # analyze and update the entire command list
2518
+ print("\n🔍 Analyzing and updating remaining commands based on fix results...")
2519
+ cmd_manager.update_command_list_with_llm(api_key)
2520
+ else:
2521
+ print(f"⚠️ Original command still failed after fix, continuing...")
1869
2522
  else:
1870
2523
  print(f"❌ Fix command failed: {fix_stderr}")
1871
2524
  print(f"⚠️ Continuing with remaining commands...")
@@ -1883,9 +2536,25 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1883
2536
  print(f"🔧 Executing fix command {cmd_index + 1}: {cmd_text}")
1884
2537
 
1885
2538
  start_time = time.time()
1886
- success, stdout, stderr = shell.execute(cmd_text, timeout=240)
2539
+ success, stdout, stderr = shell.execute(cmd_text, timeout=300)
1887
2540
  execution_time = time.time() - start_time
1888
2541
 
2542
+ # Check if the fix command was aborted due to waiting for input and an alternative was suggested
2543
+ if not success and "Command aborted - requires user input" in stderr and shell.suggested_alternative:
2544
+ alternative_cmd = shell.suggested_alternative
2545
+ print(f"🔄 Fix command aborted due to input requirement. Adding suggested alternative: {alternative_cmd}")
2546
+
2547
+ # Add the alternative command with high priority
2548
+ cmd_manager.add_command_dynamically(alternative_cmd, priority='high')
2549
+
2550
+ # Clear the suggested alternative
2551
+ shell.suggested_alternative = None
2552
+ # Check if the fix command should be removed as suggested by LLM
2553
+ elif not success and stderr.startswith("Command removed -"):
2554
+ reason = stderr.replace("Command removed -", "").strip()
2555
+ print(f"🚫 Removed fix command as suggested by LLM: {reason}")
2556
+ # We don't need to do anything else, just mark it as executed and move on
2557
+
1889
2558
  # Mark fix command as executed
1890
2559
  cmd_manager.mark_command_executed(
1891
2560
  cmd_index, 'fix', success, stdout, stderr, execution_time
@@ -1911,9 +2580,25 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1911
2580
  print(f"🔧 Executing additional fix: {cmd_text}")
1912
2581
 
1913
2582
  start_time = time.time()
1914
- success, stdout, stderr = shell.execute(cmd_text, timeout=240)
2583
+ success, stdout, stderr = shell.execute(cmd_text, timeout=300)
1915
2584
  execution_time = time.time() - start_time
1916
2585
 
2586
+ # Check if the fix command was aborted due to waiting for input and an alternative was suggested
2587
+ if not success and "Command aborted - requires user input" in stderr and shell.suggested_alternative:
2588
+ alternative_cmd = shell.suggested_alternative
2589
+ print(f"🔄 Additional fix command aborted due to input requirement. Adding suggested alternative: {alternative_cmd}")
2590
+
2591
+ # Add the alternative command with high priority
2592
+ cmd_manager.add_command_dynamically(alternative_cmd, priority='high')
2593
+
2594
+ # Clear the suggested alternative
2595
+ shell.suggested_alternative = None
2596
+ # Check if the additional fix command should be removed as suggested by LLM
2597
+ elif not success and stderr.startswith("Command removed -"):
2598
+ reason = stderr.replace("Command removed -", "").strip()
2599
+ print(f"🚫 Removed additional fix command as suggested by LLM: {reason}")
2600
+ # We don't need to do anything else, just mark it as executed and move on
2601
+
1917
2602
  # Mark fix command as executed
1918
2603
  cmd_manager.mark_command_executed(
1919
2604
  fix_index, 'fix', success, stdout, stderr, execution_time
@@ -3459,4 +4144,90 @@ if __name__ == "__main__":
3459
4144
  # print(f"\n❌ Error: {e}")
3460
4145
  # print("🧹 Cleaning up resources...")
3461
4146
  cleanup_modal_token()
3462
- sys.exit(1)
4147
+ sys.exit(1)
4148
+
4149
+ def preprocess_commands_with_llm(setup_commands, stored_credentials, api_key=None):
4150
+ """
4151
+ Use LLM to preprocess setup commands and inject available credentials.
4152
+
4153
+ Args:
4154
+ setup_commands: List of setup commands
4155
+ stored_credentials: Dictionary of stored credentials
4156
+ api_key: OpenAI API key for LLM calls
4157
+
4158
+ Returns:
4159
+ List of processed commands with credentials injected
4160
+ """
4161
+ if not setup_commands or not stored_credentials:
4162
+ return setup_commands
4163
+
4164
+ try:
4165
+ # Create context for the LLM
4166
+ credentials_info = "\n".join([f"- {key}: {value[:8]}..." for key, value in stored_credentials.items()])
4167
+
4168
+ prompt = f"""
4169
+ You are a command preprocessing assistant. Your task is to modify setup commands to use available credentials and make them non-interactive.
4170
+
4171
+ AVAILABLE CREDENTIALS:
4172
+ {credentials_info}
4173
+
4174
+ ORIGINAL COMMANDS:
4175
+ {chr(10).join([f"{i+1}. {cmd}" for i, cmd in enumerate(setup_commands)])}
4176
+
4177
+ INSTRUCTIONS:
4178
+ 1. Replace any authentication commands with token-based versions using available credentials
4179
+ 2. Make all commands non-interactive (add --yes, --no-input, -y flags where needed)
4180
+ 3. Use environment variables or direct token injection where appropriate
4181
+ 4. Skip commands that cannot be made non-interactive due to missing credentials
4182
+ 5. Add any necessary environment variable exports
4183
+
4184
+ Return the modified commands as a JSON array of strings. If a command should be skipped, prefix it with "# SKIPPED: ".
4185
+
4186
+ Example transformations:
4187
+ - "huggingface-cli login" → "huggingface-cli login --token $HUGGINGFACE_TOKEN"
4188
+ - "npm install" → "npm install --yes"
4189
+ - "pip install package" → "pip install package --no-input"
4190
+
4191
+ Return only the JSON array, no other text.
4192
+ """
4193
+
4194
+ if not api_key:
4195
+ print("⚠️ No OpenAI API key available for command preprocessing")
4196
+ return setup_commands
4197
+
4198
+ # Call OpenAI API
4199
+ import openai
4200
+ client = openai.OpenAI(api_key=api_key)
4201
+
4202
+ response = client.chat.completions.create(
4203
+ model="gpt-3.5-turbo",
4204
+ messages=[
4205
+ {"role": "system", "content": "You are a command preprocessing assistant that modifies setup commands to use available credentials and make them non-interactive."},
4206
+ {"role": "user", "content": prompt}
4207
+ ],
4208
+ temperature=0.1,
4209
+ max_tokens=2000
4210
+ )
4211
+
4212
+ result = response.choices[0].message.content.strip()
4213
+
4214
+ # Parse the JSON response
4215
+ import json
4216
+ try:
4217
+ processed_commands = json.loads(result)
4218
+ if isinstance(processed_commands, list):
4219
+ print(f"🔧 LLM preprocessed {len(processed_commands)} commands")
4220
+ for i, cmd in enumerate(processed_commands):
4221
+ if cmd != setup_commands[i]:
4222
+ print(f" {i+1}. {setup_commands[i]} → {cmd}")
4223
+ return processed_commands
4224
+ else:
4225
+ print("⚠️ LLM returned invalid format, using original commands")
4226
+ return setup_commands
4227
+ except json.JSONDecodeError:
4228
+ print("⚠️ Failed to parse LLM response, using original commands")
4229
+ return setup_commands
4230
+
4231
+ except Exception as e:
4232
+ print(f"⚠️ LLM preprocessing failed: {e}")
4233
+ return setup_commands