gitarsenal-cli 1.5.9 → 1.5.11

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.
@@ -13,7 +13,7 @@ import argparse
13
13
  from pathlib import Path
14
14
 
15
15
  # Parse command-line arguments
16
- parser = argparse.ArgumentParser()
16
+ parser = argparse.ArgumentParser(description='Launch a Modal sandbox')
17
17
  parser.add_argument('--proxy-url', help='URL of the proxy server')
18
18
  parser.add_argument('--proxy-api-key', help='API key for the proxy server')
19
19
  parser.add_argument('--gpu', default='A10G', help='GPU type to use')
@@ -27,35 +27,24 @@ args, unknown = parser.parse_known_args()
27
27
  # Set proxy URL and API key in environment variables if provided
28
28
  if args.proxy_url:
29
29
  os.environ["MODAL_PROXY_URL"] = args.proxy_url
30
- # print(f"✅ Set MODAL_PROXY_URL from command line")
30
+ print(f"✅ Set MODAL_PROXY_URL from command line")
31
31
 
32
32
  if args.proxy_api_key:
33
33
  os.environ["MODAL_PROXY_API_KEY"] = args.proxy_api_key
34
- # print(f"✅ Set MODAL_PROXY_API_KEY from command line")
34
+ print(f"✅ Set MODAL_PROXY_API_KEY from command line")
35
35
 
36
36
  # First, try to fetch tokens from the proxy server
37
37
  try:
38
38
  # Import the fetch_modal_tokens module
39
- print("🔄 Fetching tokens from proxy server...")
39
+ print("🔄 Fetching Modal tokens from proxy server...")
40
40
  from fetch_modal_tokens import get_tokens
41
- token_id, token_secret, openai_api_key = get_tokens()
42
-
43
- # Debug print the tokens
44
- # print("🔍 DEBUG: Modal Tokens Fetched:")
45
- # print(f"🔍 DEBUG: Token ID: {token_id}")
46
- # print(f"🔍 DEBUG: Token Secret: {token_secret}")
47
- # print(f"🔍 DEBUG: OpenAI API Key: {openai_api_key}")
48
-
49
- # Check if we got valid tokens
50
- if token_id is None or token_secret is None:
51
- raise ValueError("Could not get valid tokens")
52
-
53
- print(f"✅ Tokens fetched successfully")
41
+ token_id, token_secret = get_tokens()
42
+ print(f"✅ Modal tokens fetched successfully")
54
43
 
55
44
  # Explicitly set the environment variables again to be sure
56
45
  os.environ["MODAL_TOKEN_ID"] = token_id
57
46
  os.environ["MODAL_TOKEN_SECRET"] = token_secret
58
- os.environ["OPENAI_API_KEY"] = openai_api_key
47
+
59
48
  # Also set the old environment variable for backward compatibility
60
49
  os.environ["MODAL_TOKEN"] = token_id
61
50
 
@@ -67,26 +56,26 @@ except Exception as e:
67
56
  # Apply the comprehensive Modal token solution as fallback
68
57
  try:
69
58
  # Import the comprehensive solution module
70
- # print("🔄 Applying comprehensive Modal token solution...")
59
+ print("🔄 Applying comprehensive Modal token solution...")
71
60
  import modal_token_solution
72
- # print("✅ Comprehensive Modal token solution applied")
61
+ print("✅ Comprehensive Modal token solution applied")
73
62
 
74
63
  # Set token variables for later use
75
64
  token = modal_token_solution.TOKEN_ID # For backward compatibility
76
65
  except Exception as e:
77
- # print(f"⚠️ Error applying comprehensive Modal token solution: {e}")
66
+ print(f"⚠️ Error applying comprehensive Modal token solution: {e}")
78
67
 
79
68
  # Fall back to the authentication patch
80
69
  try:
81
70
  # Import the patch module
82
- # print("🔄 Falling back to Modal authentication patch...")
71
+ print("🔄 Falling back to Modal authentication patch...")
83
72
  import modal_auth_patch
84
- # print("✅ Modal authentication patch applied")
73
+ print("✅ Modal authentication patch applied")
85
74
 
86
75
  # Set token variables for later use
87
76
  token = modal_auth_patch.TOKEN_ID # For backward compatibility
88
77
  except Exception as e:
89
- # print(f"⚠️ Error applying Modal authentication patch: {e}")
78
+ print(f"⚠️ Error applying Modal authentication patch: {e}")
90
79
 
91
80
  # Fall back to fix_modal_token.py
92
81
  try:
@@ -126,17 +115,17 @@ except Exception as e:
126
115
  token = "ak-sLhYqCjkvixiYcb9LAuCHp" # Default token ID
127
116
 
128
117
  # Print debug info
129
- # print(f"🔍 DEBUG: Checking environment variables")
130
- # print(f"🔍 Token ID exists: {'Yes' if os.environ.get('MODAL_TOKEN_ID') else 'No'}")
131
- # print(f"🔍 Token secret exists: {'Yes' if os.environ.get('MODAL_TOKEN_SECRET') else 'No'}")
132
- # print(f"🔍 Token exists: {'Yes' if os.environ.get('MODAL_TOKEN') else 'No'}")
118
+ print(f"🔍 DEBUG: Checking environment variables")
119
+ print(f"🔍 MODAL_TOKEN_ID exists: {'Yes' if os.environ.get('MODAL_TOKEN_ID') else 'No'}")
120
+ print(f"🔍 MODAL_TOKEN_SECRET exists: {'Yes' if os.environ.get('MODAL_TOKEN_SECRET') else 'No'}")
121
+ print(f"🔍 MODAL_TOKEN exists: {'Yes' if os.environ.get('MODAL_TOKEN') else 'No'}")
133
122
  if os.environ.get('MODAL_TOKEN_ID'):
134
- print(f"🔍 Token ID length: {len(os.environ.get('MODAL_TOKEN_ID'))}")
123
+ print(f"🔍 MODAL_TOKEN_ID length: {len(os.environ.get('MODAL_TOKEN_ID'))}")
135
124
  if os.environ.get('MODAL_TOKEN_SECRET'):
136
- print(f"🔍 Token secret length: {len(os.environ.get('MODAL_TOKEN_SECRET'))}")
125
+ print(f"🔍 MODAL_TOKEN_SECRET length: {len(os.environ.get('MODAL_TOKEN_SECRET'))}")
137
126
  if os.environ.get('MODAL_TOKEN'):
138
- print(f"🔍 Token length: {len(os.environ.get('MODAL_TOKEN'))}")
139
- # print(f"✅ Token setup completed")
127
+ print(f"🔍 MODAL_TOKEN length: {len(os.environ.get('MODAL_TOKEN'))}")
128
+ print(f"✅ Modal token setup completed")
140
129
 
141
130
  # Import modal after token setup
142
131
  import modal
@@ -326,15 +315,17 @@ def handle_huggingface_login(sandbox, current_dir):
326
315
 
327
316
  return exit_code == 0, stdout_buffer, stderr_buffer
328
317
 
318
+ def handle_interactive_command(cmd, sandbox, current_dir):
319
+ """Handle interactive commands by prompting the user for input"""
320
+ print(f"⚠️ Interactive command detected: {cmd}")
321
+ print("⚠️ Some prompts may not be visible. If the command appears stuck, it may be waiting for input.")
322
+
323
+ # This is a placeholder for more sophisticated interactive command handling
324
+ # In a real implementation, you would need to handle specific interactive commands differently
325
+ return None
329
326
 
330
327
  def call_openai_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
331
328
  """Call OpenAI to debug a failed command and suggest a fix"""
332
- print("\n🔍 DEBUG: Starting LLM debugging...")
333
- print(f"🔍 DEBUG: Command: {command}")
334
- print(f"🔍 DEBUG: Error output length: {len(error_output) if error_output else 0}")
335
- print(f"🔍 DEBUG: Current directory: {current_dir}")
336
- print(f"🔍 DEBUG: Sandbox available: {sandbox is not None}")
337
-
338
329
  # Define _to_str function locally to avoid NameError
339
330
  def _to_str(maybe_bytes):
340
331
  try:
@@ -360,86 +351,22 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
360
351
  print("⚠️ Error output is empty. Cannot effectively debug the command.")
361
352
  print("⚠️ Skipping OpenAI debugging due to lack of error information.")
362
353
  return None
363
-
364
- # Try to get API key from multiple sources
365
- if not api_key:
366
- print("🔍 DEBUG: No API key provided, searching for one...")
367
354
 
368
- # First try environment variable
355
+ if not api_key:
356
+ # Try to get API key from environment
369
357
  api_key = os.environ.get("OPENAI_API_KEY")
370
- print(f"🔍 DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
371
- if api_key:
372
- print(f"🔍 DEBUG: Environment API key value: {api_key}")
373
-
374
- # If not in environment, try to fetch from server using fetch_modal_tokens
375
- if not api_key:
376
- try:
377
- print("🔍 DEBUG: Trying to fetch API key from server...")
378
- from fetch_modal_tokens import get_tokens
379
- _, _, api_key = get_tokens()
380
- if api_key:
381
- # print("✅ Successfully fetched OpenAI API key from server")
382
- # print(f"🔍 DEBUG: Fetched OpenAI API key value: {api_key}")
383
- # Set in environment for this session
384
- os.environ["OPENAI_API_KEY"] = api_key
385
- else:
386
- print("⚠️ Could not fetch OpenAI API key from server")
387
- except Exception as e:
388
- print(f"⚠️ Error fetching API key from server: {e}")
389
-
390
- # Store the API key in a persistent file if found
391
- if api_key:
392
- try:
393
- os.makedirs(os.path.expanduser("~/.gitarsenal"), exist_ok=True)
394
- with open(os.path.expanduser("~/.gitarsenal/openai_key"), "w") as f:
395
- f.write(api_key)
396
- print("✅ Saved OpenAI API key for future use")
397
- except Exception as e:
398
- print(f"⚠️ Could not save API key: {e}")
399
-
400
- # Try to load from saved file if not in environment
401
- if not api_key:
402
- try:
403
- key_file = os.path.expanduser("~/.gitarsenal/openai_key")
404
- print(f"🔍 DEBUG: Checking for saved API key at: {key_file}")
405
- if os.path.exists(key_file):
406
- with open(key_file, "r") as f:
407
- api_key = f.read().strip()
408
- if api_key:
409
- print("✅ Loaded OpenAI API key from saved file")
410
- print(f"🔍 DEBUG: API key from file: {api_key}")
411
- print(f"🔍 DEBUG: API key length: {len(api_key)}")
412
- # Also set in environment for this session
413
- os.environ["OPENAI_API_KEY"] = api_key
414
- else:
415
- print("🔍 DEBUG: Saved file exists but is empty")
416
- else:
417
- print("🔍 DEBUG: No saved API key file found")
418
- except Exception as e:
419
- print(f"⚠️ Could not load saved API key: {e}")
420
358
 
421
- # Then try credentials manager
422
- if not api_key:
423
- print("🔍 DEBUG: Trying credentials manager...")
424
- try:
425
- from credentials_manager import CredentialsManager
426
- credentials_manager = CredentialsManager()
427
- api_key = credentials_manager.get_openai_api_key()
428
- if api_key:
429
- print(f"🔍 DEBUG: API key from credentials manager: Found")
430
- print(f"🔍 DEBUG: Credentials manager API key value: {api_key}")
431
- # Set in environment for this session
432
- os.environ["OPENAI_API_KEY"] = api_key
433
- else:
434
- print(f"🔍 DEBUG: API key from credentials manager: Not found")
435
- except ImportError as e:
436
- print(f"🔍 DEBUG: Credentials manager not available: {e}")
437
- # Fall back to direct input if credentials_manager is not available
438
- pass
439
-
440
- # Finally, prompt the user if still no API key
441
- if not api_key:
442
- print("🔍 DEBUG: No API key found in any source, prompting user...")
359
+ if not api_key:
360
+ # Use the CredentialsManager to get the API key
361
+ try:
362
+ from credentials_manager import CredentialsManager
363
+ credentials_manager = CredentialsManager()
364
+ api_key = credentials_manager.get_openai_api_key()
365
+ if not api_key:
366
+ print("❌ No API key provided. Skipping debugging.")
367
+ return None
368
+ except ImportError:
369
+ # Fall back to direct input if credentials_manager module is not available
443
370
  print("\n" + "="*60)
444
371
  print("🔑 OPENAI API KEY REQUIRED FOR DEBUGGING")
445
372
  print("="*60)
@@ -454,9 +381,6 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
454
381
  print("❌ No API key provided. Skipping debugging.")
455
382
  return None
456
383
  print("✅ API key received successfully!")
457
- print(f"🔍 DEBUG: User-provided API key: {api_key}")
458
- # Save the API key to environment for future use in this session
459
- os.environ["OPENAI_API_KEY"] = api_key
460
384
  except KeyboardInterrupt:
461
385
  print("\n❌ API key input cancelled by user.")
462
386
  return None
@@ -464,19 +388,9 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
464
388
  print(f"❌ Error getting API key: {e}")
465
389
  return None
466
390
 
467
- # If we still don't have an API key, we can't proceed
468
- if not api_key:
469
- print("❌ No OpenAI API key available. Cannot perform LLM debugging.")
470
- print("💡 To enable LLM debugging, set the OPENAI_API_KEY environment variable")
471
- return None
472
-
473
- print(f"✅ OpenAI API key available (length: {len(api_key)})")
474
-
475
- # Gather additional context to help with debugging
391
+ # Get current directory context
476
392
  directory_context = ""
477
393
  system_info = ""
478
- command_history = ""
479
- file_context = ""
480
394
 
481
395
  if sandbox:
482
396
  try:
@@ -490,7 +404,6 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
490
404
  uname -a
491
405
  echo -e "\nPython Information:"
492
406
  python --version
493
- pip --version
494
407
  echo -e "\nPackage Manager:"
495
408
  which apt 2>/dev/null && echo "apt available" || echo "apt not available"
496
409
  which yum 2>/dev/null && echo "yum available" || echo "yum not available"
@@ -545,72 +458,6 @@ Directory contents:
545
458
  {parent_context}
546
459
  """
547
460
  print("✅ Directory context gathered successfully")
548
-
549
- # Check for relevant files that might provide additional context
550
- # For example, if error mentions a specific file, try to get its content
551
- relevant_files = []
552
- error_files = re.findall(r'(?:No such file or directory|cannot open|not found): ([^\s:]+)', error_output)
553
- if error_files:
554
- for file_path in error_files:
555
- # Clean up the file path
556
- file_path = file_path.strip("'\"")
557
- if not os.path.isabs(file_path):
558
- file_path = os.path.join(current_dir, file_path)
559
-
560
- # Try to get the parent directory if the file doesn't exist
561
- if '/' in file_path:
562
- parent_file_dir = os.path.dirname(file_path)
563
- relevant_files.append(parent_file_dir)
564
-
565
- # Look for package.json, requirements.txt, etc.
566
- common_config_files = ["package.json", "requirements.txt", "pyproject.toml", "setup.py",
567
- "Pipfile", "Dockerfile", "docker-compose.yml", "Makefile"]
568
-
569
- for config_file in common_config_files:
570
- check_cmd = f"test -f {current_dir}/{config_file}"
571
- check_result = sandbox.exec("bash", "-c", check_cmd)
572
- check_result.wait()
573
- if check_result.returncode == 0:
574
- relevant_files.append(f"{current_dir}/{config_file}")
575
-
576
- # Get content of relevant files
577
- if relevant_files:
578
- file_context = "\nRelevant file contents:\n"
579
- for file_path in relevant_files[:2]: # Limit to 2 files to avoid too much context
580
- try:
581
- file_check_cmd = f"test -f {file_path}"
582
- file_check = sandbox.exec("bash", "-c", file_check_cmd)
583
- file_check.wait()
584
-
585
- if file_check.returncode == 0:
586
- # It's a file, get its content
587
- cat_cmd = f"cat {file_path}"
588
- cat_result = sandbox.exec("bash", "-c", cat_cmd)
589
- file_content = ""
590
- for line in cat_result.stdout:
591
- file_content += _to_str(line)
592
- cat_result.wait()
593
-
594
- # Truncate if too long
595
- if len(file_content) > 1000:
596
- file_content = file_content[:1000] + "\n... (truncated)"
597
-
598
- file_context += f"\n--- {file_path} ---\n{file_content}\n"
599
- else:
600
- # It's a directory, list its contents
601
- ls_cmd = f"ls -la {file_path}"
602
- ls_dir_result = sandbox.exec("bash", "-c", ls_cmd)
603
- dir_content = ""
604
- for line in ls_dir_result.stdout:
605
- dir_content += _to_str(line)
606
- ls_dir_result.wait()
607
-
608
- file_context += f"\n--- Directory: {file_path} ---\n{dir_content}\n"
609
- except Exception as e:
610
- print(f"⚠️ Error getting content of {file_path}: {e}")
611
-
612
- print(f"✅ Additional file context gathered from {len(relevant_files)} relevant files")
613
-
614
461
  except Exception as e:
615
462
  print(f"⚠️ Error getting directory context: {e}")
616
463
  directory_context = f"\nCurrent directory: {current_dir}\n"
@@ -642,262 +489,64 @@ But it failed with this error:
642
489
  ```
643
490
  {system_info}
644
491
  {directory_context}
645
- {file_context}
646
-
647
492
  Please analyze the error and provide ONLY a single terminal command that would fix the issue.
648
493
  Consider the current directory, system information, and directory contents carefully before suggesting a solution.
649
494
 
650
- IMPORTANT GUIDELINES:
651
- 1. For any commands that might ask for yes/no confirmation, use the appropriate non-interactive flag:
652
- - For apt/apt-get: use -y or --yes
653
- - For pip: use --no-input
654
- - For rm: use -f or --force
655
- - For other commands: check their documentation for the appropriate non-interactive flag
656
-
657
- 2. If the error indicates a file is not found (e.g., "No such file or directory", "cannot open", "not found"):
658
- - FIRST try to search for the file using: find . -name "filename" -type f 2>/dev/null
659
- - If found, navigate to that directory using: cd /path/to/directory
660
- - If not found, then consider creating the file or installing missing packages
661
-
662
- 3. For missing packages or dependencies:
663
- - Use pip install for Python packages
664
- - Use apt-get install for system packages
665
- - Use npm install for Node.js packages
666
-
667
- 4. For authentication issues:
668
- - For wandb login: suggest 'wandb login YOUR_API_KEY' (system will prompt for actual key)
669
- - For huggingface: suggest 'huggingface-cli login' (system will prompt for token)
495
+ IMPORTANT: For any commands that might ask for yes/no confirmation, use the appropriate non-interactive flag:
496
+ - For apt/apt-get: use -y or --yes
497
+ - For pip: use --no-input
498
+ - For rm: use -f or --force
499
+ - For other commands: check their documentation for the appropriate non-interactive flag
670
500
 
671
501
  Do not provide any explanations, just the exact command to run.
672
502
  """
673
503
 
674
504
  # Prepare the API request payload
675
- print("🔍 DEBUG: Preparing API request...")
676
-
677
- # Try to use GPT-4 first, but fall back to other models if needed
678
- models_to_try = [
679
- "gpt-4o-mini", # First choice: GPT-4o (most widely available)
680
- ]
681
-
682
- # Check if we have a preferred model in environment
683
- preferred_model = os.environ.get("OPENAI_MODEL")
684
- if preferred_model:
685
- # Insert the preferred model at the beginning of the list
686
- models_to_try.insert(0, preferred_model)
687
- print(f"✅ Using preferred model from environment: {preferred_model}")
688
-
689
- # Remove duplicates while preserving order
690
- models_to_try = list(dict.fromkeys(models_to_try))
691
- print(f"🔍 DEBUG: Models to try: {models_to_try}")
692
-
693
- # Function to make the API call with a specific model
694
- def try_api_call(model_name, retries=2, backoff_factor=1.5):
695
- print(f"🔍 DEBUG: Attempting API call with model: {model_name}")
696
- print(f"🔍 DEBUG: API key available: {'Yes' if api_key else 'No'}")
697
- # if api_key:
698
- # print(f"🔍 DEBUG: API key length: {len(api_key)}")
699
- # print(f"🔍 DEBUG: API key starts with: {api_key[:10]}...")
700
-
701
- payload = {
702
- "model": model_name,
703
- "messages": [
704
- {"role": "system", "content": "You are a debugging assistant. Provide only the terminal command to fix the issue. Analyze the issue first, understand why it's happening, then provide the command to fix it. For file not found errors, first search for the file using 'find . -name filename -type f' and navigate to the directory if found. For missing packages, use appropriate package managers (pip, apt-get, npm). For authentication, suggest login commands with placeholders."},
705
- {"role": "user", "content": prompt}
706
- ],
707
- "temperature": 0.2,
708
- "max_tokens": 300
709
- }
710
-
711
- print(f"🔍 DEBUG: Payload prepared, prompt length: {len(prompt)}")
712
-
713
- # Add specific handling for common errors
714
- last_error = None
715
- for attempt in range(retries + 1):
716
- try:
717
- if attempt > 0:
718
- # Exponential backoff
719
- wait_time = backoff_factor * (2 ** (attempt - 1))
720
- print(f"⏱️ Retrying in {wait_time:.1f} seconds... (attempt {attempt+1}/{retries+1})")
721
- time.sleep(wait_time)
722
-
723
- print(f"🤖 Calling OpenAI with {model_name} model to debug the failed command...")
724
- print(f"🔍 DEBUG: Making POST request to OpenAI API...")
725
- response = requests.post(
726
- "https://api.openai.com/v1/chat/completions",
727
- headers=headers,
728
- json=payload,
729
- timeout=45 # Increased timeout for reliability
730
- )
731
-
732
- print(f"🔍 DEBUG: Response received, status code: {response.status_code}")
733
-
734
- # Handle specific status codes
735
- if response.status_code == 200:
736
- print(f"🔍 DEBUG: Success! Response length: {len(response.text)}")
737
- return response.json(), None
738
- elif response.status_code == 401:
739
- error_msg = "Authentication error: Invalid API key"
740
- print(f"❌ {error_msg}")
741
- print(f"🔍 DEBUG: Response text: {response.text}")
742
- # Don't retry auth errors
743
- return None, error_msg
744
- elif response.status_code == 429:
745
- error_msg = "Rate limit exceeded or quota reached"
746
- print(f"⚠️ {error_msg}")
747
- print(f"🔍 DEBUG: Response text: {response.text}")
748
- # Always retry rate limit errors with increasing backoff
749
- last_error = error_msg
750
- continue
751
- elif response.status_code == 500:
752
- error_msg = "OpenAI server error"
753
- print(f"⚠️ {error_msg}")
754
- print(f"🔍 DEBUG: Response text: {response.text}")
755
- # Retry server errors
756
- last_error = error_msg
757
- continue
758
- else:
759
- error_msg = f"Status code: {response.status_code}, Response: {response.text}"
760
- print(f"⚠️ OpenAI API error: {error_msg}")
761
- print(f"🔍 DEBUG: Full response text: {response.text}")
762
- last_error = error_msg
763
- # Only retry if we have attempts left
764
- if attempt < retries:
765
- continue
766
- return None, error_msg
767
- except requests.exceptions.Timeout:
768
- error_msg = "Request timed out"
769
- print(f"⚠️ {error_msg}")
770
- print(f"🔍 DEBUG: Timeout after 45 seconds")
771
- last_error = error_msg
772
- # Always retry timeouts
773
- continue
774
- except requests.exceptions.ConnectionError:
775
- error_msg = "Connection error"
776
- print(f"⚠️ {error_msg}")
777
- print(f"🔍 DEBUG: Connection failed to api.openai.com")
778
- last_error = error_msg
779
- # Always retry connection errors
780
- continue
781
- except Exception as e:
782
- error_msg = str(e)
783
- print(f"⚠️ Unexpected error: {error_msg}")
784
- print(f"🔍 DEBUG: Exception type: {type(e).__name__}")
785
- print(f"🔍 DEBUG: Exception details: {str(e)}")
786
- last_error = error_msg
787
- # Only retry if we have attempts left
788
- if attempt < retries:
789
- continue
790
- return None, error_msg
791
-
792
- # If we get here, all retries failed
793
- return None, last_error
794
-
795
- # Try each model in sequence until one works
796
- result = None
797
- last_error = None
798
-
799
- for model in models_to_try:
800
- result, error = try_api_call(model)
801
- if result:
802
- # print(f"✅ Successfully got response from {model}")
803
- break
804
- else:
805
- print(f"⚠️ Failed to get response from {model}: {error}")
806
- last_error = error
807
-
808
- if not result:
809
- print(f"❌ All model attempts failed. Last error: {last_error}")
810
- return None
505
+ payload = {
506
+ "model": "gpt-4.1",
507
+ "messages": [
508
+ {"role": "system", "content": "You are a debugging assistant. Provide only the terminal command to fix the issue, analyze the issue first understand why its happening and then provide the command to fix the issue. If you see missing pytest errors, suggest 'pip install pytest'. For wandb login issues, suggest 'wandb login YOUR_API_KEY' and the system will handle prompting for the actual key."},
509
+ {"role": "user", "content": prompt}
510
+ ],
511
+ "temperature": 0.2,
512
+ "max_tokens": 300
513
+ }
811
514
 
812
- # Process the response
813
515
  try:
814
- print(f"🔍 DEBUG: Processing OpenAI response...")
815
- # print(f"🔍 DEBUG: Response structure: {list(result.keys())}")
816
- print(f"🔍 DEBUG: Choices count: {len(result.get('choices', []))}")
817
-
818
- fix_command = result["choices"][0]["message"]["content"].strip()
819
- print(f"🔍 DEBUG: Raw response content: {fix_command}")
820
-
821
- # Save the original response for debugging
822
- original_response = fix_command
823
-
824
- # Extract just the command if it's wrapped in backticks or explanation
825
- if "```" in fix_command:
826
- # Extract content between backticks
827
- import re
828
- code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
829
- if code_blocks:
830
- fix_command = code_blocks[0].strip()
831
- print(f"✅ Extracted command from code block: {fix_command}")
832
-
833
- # If the response still has explanatory text, try to extract just the command
834
- if len(fix_command.split('\n')) > 1:
835
- # First try to find lines that look like commands (start with common command prefixes)
836
- command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
837
- 'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
838
- 'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
839
- 'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
840
-
841
- # Check for lines that start with common command prefixes
842
- command_lines = [line.strip() for line in fix_command.split('\n')
843
- if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
844
-
845
- if command_lines:
846
- # Use the first command line found
847
- fix_command = command_lines[0]
848
- print(f"✅ Identified command by prefix: {fix_command}")
849
- else:
850
- # Try to find lines that look like commands (contain common shell patterns)
851
- shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
852
- command_lines = [line.strip() for line in fix_command.split('\n')
853
- if any(pattern in line for pattern in shell_patterns)]
854
-
855
- if command_lines:
856
- # Use the first command line found
857
- fix_command = command_lines[0]
858
- print(f"✅ Identified command by shell pattern: {fix_command}")
859
- else:
860
- # Fall back to the shortest non-empty line as it's likely the command
861
- lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
862
- if lines:
863
- # Exclude very short lines that are likely not commands
864
- valid_lines = [line for line in lines if len(line) > 5]
865
- if valid_lines:
866
- fix_command = min(valid_lines, key=len)
867
- else:
868
- fix_command = min(lines, key=len)
869
- print(f"✅ Selected shortest line as command: {fix_command}")
870
-
871
- # Clean up the command - remove any trailing periods or quotes
872
- fix_command = fix_command.rstrip('.;"\'')
873
-
874
- # Remove common prefixes that LLMs sometimes add
875
- prefixes_to_remove = [
876
- "Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
877
- "You should run: ", "You can run: ", "You need to run: "
878
- ]
879
- for prefix in prefixes_to_remove:
880
- if fix_command.startswith(prefix):
881
- fix_command = fix_command[len(prefix):].strip()
882
- print(f"✅ Removed prefix: {prefix}")
883
- break
884
-
885
- # If the command is still multi-line or very long, it might not be a valid command
886
- if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
887
- print("⚠️ Extracted command appears invalid (multi-line or too long)")
888
- print("🔍 Original response from LLM:")
889
- print("-" * 60)
890
- print(original_response)
891
- print("-" * 60)
892
- print("⚠️ Using best guess for command")
516
+ print("🤖 Calling OpenAI to debug the failed command...")
517
+ response = requests.post(
518
+ "https://api.openai.com/v1/chat/completions",
519
+ headers=headers,
520
+ json=payload,
521
+ timeout=30
522
+ )
893
523
 
894
- print(f"🔧 Suggested fix: {fix_command}")
895
- print(f"🔍 DEBUG: Returning fix command: {fix_command}")
896
- return fix_command
524
+ if response.status_code == 200:
525
+ result = response.json()
526
+ fix_command = result["choices"][0]["message"]["content"].strip()
527
+
528
+ # Extract just the command if it's wrapped in backticks or explanation
529
+ if "```" in fix_command:
530
+ # Extract content between backticks
531
+ import re
532
+ code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
533
+ if code_blocks:
534
+ fix_command = code_blocks[0].strip()
535
+
536
+ # If the response still has explanatory text, try to extract just the command
537
+ if len(fix_command.split('\n')) > 1:
538
+ # Take the shortest non-empty line as it's likely the command
539
+ lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
540
+ if lines:
541
+ fix_command = min(lines, key=len)
542
+
543
+ print(f"🔧 Suggested fix: {fix_command}")
544
+ return fix_command
545
+ else:
546
+ print(f"❌ OpenAI API error: {response.status_code} - {response.text}")
547
+ return None
897
548
  except Exception as e:
898
- print(f"❌ Error processing OpenAI response: {e}")
899
- print(f"🔍 DEBUG: Exception type: {type(e).__name__}")
900
- print(f"🔍 DEBUG: Exception details: {str(e)}")
549
+ print(f"❌ Error calling OpenAI API: {e}")
901
550
  return None
902
551
 
903
552
  def prompt_for_hf_token():
@@ -937,6 +586,1518 @@ def prompt_for_hf_token():
937
586
  print(f"❌ Error getting token: {e}")
938
587
  return None
939
588
 
589
+ def create_modal_sandbox(gpu_type, repo_url=None, repo_name=None, setup_commands=None, volume_name=None):
590
+ # Import the credentials manager if available
591
+ try:
592
+ from credentials_manager import CredentialsManager
593
+ credentials_manager = CredentialsManager()
594
+ except ImportError:
595
+ credentials_manager = None
596
+ print("⚠️ Credentials manager not found, will use environment variables or prompt for credentials")
597
+
598
+ # Check if Modal is authenticated
599
+ try:
600
+ # Try to import modal first to check if it's installed
601
+ import modal
602
+
603
+ # Try to access Modal token to check authentication
604
+ try:
605
+ # This will raise an exception if not authenticated
606
+ modal.config.get_current_workspace_name()
607
+ print("✅ Modal authentication verified")
608
+ except modal.exception.AuthError:
609
+ print("\n" + "="*80)
610
+ print("🔑 MODAL AUTHENTICATION REQUIRED")
611
+ print("="*80)
612
+ print("GitArsenal requires Modal authentication to create cloud environments.")
613
+
614
+ # Try to get token from credentials manager
615
+ modal_token = None
616
+ if credentials_manager:
617
+ try:
618
+ modal_token = credentials_manager.get_modal_token()
619
+ if modal_token:
620
+ # Set the token in the environment
621
+ os.environ["MODAL_TOKEN_ID"] = modal_token
622
+ print("✅ Modal token set from credentials manager")
623
+
624
+ # Try to authenticate with the token
625
+ try:
626
+ import subprocess
627
+ token_result = subprocess.run(
628
+ ["modal", "token", "set", "--from-env"],
629
+ capture_output=True, text=True
630
+ )
631
+ if token_result.returncode == 0:
632
+ print("✅ Successfully authenticated with Modal")
633
+ else:
634
+ print(f"⚠️ Failed to authenticate with Modal: {token_result.stderr}")
635
+ print("\nPlease authenticate manually:")
636
+ print("1. Run 'modal token new' to get a new token")
637
+ print("2. Then restart this command")
638
+ return None
639
+ except Exception as e:
640
+ print(f"⚠️ Error setting Modal token: {e}")
641
+ return None
642
+ except Exception as e:
643
+ print(f"⚠️ Error getting Modal token: {e}")
644
+
645
+ if not modal_token:
646
+ print("\nTo authenticate with Modal, you need to:")
647
+ print("1. Create a Modal account at https://modal.com if you don't have one")
648
+ print("2. Run the following command to get a token:")
649
+ print(" modal token new")
650
+ print("3. Then set up your credentials in GitArsenal:")
651
+ print(" ./gitarsenal.py credentials set modal_token")
652
+ print("\nAfter completing these steps, try your command again.")
653
+ print("="*80)
654
+ return None
655
+ except ImportError:
656
+ print("\n" + "="*80)
657
+ print("❌ MODAL PACKAGE NOT INSTALLED")
658
+ print("="*80)
659
+ print("GitArsenal requires the Modal package to be installed.")
660
+ print("\nTo install Modal, run:")
661
+ print(" pip install modal")
662
+ print("\nAfter installation, authenticate with Modal:")
663
+ print("1. Run 'modal token new'")
664
+ print("2. Then run './gitarsenal.py credentials set modal_token'")
665
+ print("="*80)
666
+ return None
667
+ except Exception as e:
668
+ print(f"⚠️ Error checking Modal authentication: {e}")
669
+ print("Continuing anyway, but Modal operations may fail")
670
+
671
+ # Execution history for tracking all commands and their results in this session
672
+ execution_history = []
673
+
674
+ # Track session start time
675
+ session_start = datetime.datetime.now().isoformat()
676
+
677
+ # Track previous errors to detect repeated failures
678
+ previous_errors = {}
679
+
680
+ # Track Python version management
681
+ conda_installed = False
682
+ python_version_switched = False
683
+ current_python_version = None
684
+
685
+ # Generate a unique app name with timestamp to avoid conflicts
686
+ timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
687
+ app_name = f"sandbox-{timestamp}"
688
+
689
+ gpu_configs = {
690
+ 'T4': {'gpu': 'T4', 'memory': 16},
691
+ 'L4': {'gpu': 'L4', 'memory': 24},
692
+ 'A10G': {'gpu': 'A10G', 'memory': 24},
693
+ 'A100-40GB': {'gpu': 'A100-SXM4-40GB', 'memory': 40},
694
+ 'A100-80GB': {'gpu': 'A100-80GB', 'memory': 80},
695
+ 'L40S': {'gpu': 'L40S', 'memory': 48},
696
+ 'H100': {'gpu': 'H100', 'memory': 80},
697
+ 'H200': {'gpu': 'H200', 'memory': 141},
698
+ 'B200': {'gpu': 'B200', 'memory': 96}
699
+ }
700
+
701
+ if gpu_type not in gpu_configs:
702
+ print(f"⚠️ Unknown GPU type: {gpu_type}. Using A10G as default.")
703
+ gpu_type = 'A10G'
704
+
705
+ gpu_spec = gpu_configs[gpu_type]
706
+ print(f"🚀 Creating Modal sandbox with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
707
+
708
+ # Initialize uv_path variable
709
+ uv_path = ""
710
+
711
+ # Setup volume if specified
712
+ volume = None
713
+ volume_mount_path = "/persistent"
714
+
715
+ if volume_name:
716
+ print(f"📦 Setting up volume: {volume_name}")
717
+ try:
718
+ # Try to get existing volume or create new one
719
+ volume = modal.Volume.from_name(volume_name, create_if_missing=True)
720
+ print(f"✅ Volume '{volume_name}' ready for use")
721
+ except Exception as e:
722
+ print(f"⚠️ Could not setup volume '{volume_name}': {e}")
723
+ print("⚠️ Continuing without persistent volume")
724
+ volume = None
725
+ else:
726
+ # Create a default volume for this session
727
+ default_volume_name = f"sandbox-vol-{timestamp}"
728
+ print(f"📦 Creating default volume: {default_volume_name}")
729
+ try:
730
+ volume = modal.Volume.from_name(default_volume_name, create_if_missing=True)
731
+ volume_name = default_volume_name
732
+ print(f"✅ Default volume '{default_volume_name}' created")
733
+ except Exception as e:
734
+ print(f"⚠️ Could not create default volume: {e}")
735
+ print("⚠️ Continuing without persistent volume")
736
+ volume = None
737
+
738
+ # Enable output for image building
739
+ with modal.enable_output():
740
+ # Create a Modal app and sandbox
741
+ print(f"🚀 Creating Modal sandbox with GPU: {gpu_type.lower()} (App: {app_name})...")
742
+ # Always use lookup with create_if_missing=True to properly initialize the app
743
+ app = modal.App.lookup(app_name, create_if_missing=True)
744
+ print(f"Created app: {app_name}")
745
+
746
+ # Create the sandbox with increased timeout for long-running operations
747
+ print("⏱️ Setting 30-minute timeout for long-running installations...")
748
+
749
+ # Setup volume mount if available
750
+ volumes = {}
751
+ if volume:
752
+ volumes[volume_mount_path] = volume
753
+ print(f"📦 Mounting volume '{volume_name}' at {volume_mount_path}")
754
+
755
+ cuda_image = modal.Image.from_registry("nvidia/cuda:12.8.1-devel-ubuntu24.04", add_python="3.12")
756
+
757
+ sandbox = modal.Sandbox.create(
758
+ "sleep", "infinity",
759
+ app=app,
760
+ gpu=gpu_type.lower(),
761
+ image=cuda_image,
762
+ timeout=3600, # 40 minutes instead of 15 minutes
763
+ volumes=volumes if volumes else None
764
+ )
765
+
766
+ # Get the sandbox ID for reference
767
+ sandbox_id = sandbox.object_id
768
+ print(f"📋 Sandbox ID: {sandbox_id}")
769
+
770
+ # Wait a moment for the container to be registered
771
+ print("⏳ Waiting for container to be registered...")
772
+ time.sleep(5) # Increased wait time
773
+
774
+ # Function to extract container ID from text output
775
+ def extract_container_id_from_text(output):
776
+ print("Extracting container ID from text output...")
777
+
778
+ # First, try to find lines with the app name
779
+ lines = output.split('\n')
780
+ app_lines = [line for line in lines if app_name in line]
781
+
782
+ if app_lines:
783
+ # Get the first line with the app name
784
+ app_line = app_lines[0]
785
+ print(f"Found line with app name: {app_line}")
786
+
787
+ # Try to extract the container ID
788
+ if '│' in app_line:
789
+ parts = app_line.split('│')
790
+ if len(parts) >= 2:
791
+ container_id_part = parts[1].strip()
792
+ if container_id_part.startswith('ta-'):
793
+ return container_id_part
794
+
795
+ # If that didn't work, try regex pattern matching
796
+ container_matches = re.findall(r'ta-[A-Z0-9]+', output)
797
+ if container_matches:
798
+ return container_matches[0]
799
+
800
+ return None
801
+
802
+ # Get the container ID using multiple approaches
803
+ print("📋 Getting container ID...")
804
+ container_id = None
805
+
806
+ # Approach 1: Use modal container list --json
807
+ try:
808
+ print("Trying JSON approach...")
809
+ result = subprocess.run(["modal", "container", "list", "--json"], capture_output=True, text=True)
810
+ output = result.stdout
811
+ print(f"JSON output: {output}")
812
+
813
+ import json
814
+ try:
815
+ containers = json.loads(output)
816
+ print(f"Parsed JSON: {containers}")
817
+ if containers and isinstance(containers, list) and len(containers) > 0:
818
+ # The container ID is in the "Container ID" field, not "id"
819
+ container_id = containers[0].get("Container ID")
820
+ if container_id:
821
+ print(f"📋 Found container ID from JSON: {container_id}")
822
+ else:
823
+ # Try lowercase keys as a fallback
824
+ container_id = containers[0].get("container_id") or containers[0].get("container id")
825
+ if container_id:
826
+ print(f"📋 Found container ID from JSON with lowercase keys: {container_id}")
827
+ except json.JSONDecodeError as json_err:
828
+ print(f"JSON parse error: {json_err}")
829
+ except Exception as e:
830
+ print(f"Error with JSON approach: {e}")
831
+
832
+ # Approach 2: Use modal container list with text parsing
833
+ if not container_id:
834
+ try:
835
+ print("Trying text output approach...")
836
+ result = subprocess.run(["modal", "container", "list"], capture_output=True, text=True)
837
+ output = result.stdout
838
+ print("Modal container list output:")
839
+ print(output)
840
+
841
+ container_id = extract_container_id_from_text(output)
842
+ if container_id:
843
+ print(f"📋 Found container ID from text: {container_id}")
844
+ except Exception as e:
845
+ print(f"Error with text approach: {e}")
846
+
847
+ # Approach 3: Use shell command to get first container
848
+ if not container_id:
849
+ try:
850
+ print("Trying shell command approach...")
851
+ cmd = "modal container list | grep -v Container | grep -v '─' | head -1 | awk '{print $1}'"
852
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
853
+ output = result.stdout.strip()
854
+ print(f"Shell command output: {output}")
855
+
856
+ if output and output.startswith('ta-'):
857
+ container_id = output
858
+ print(f"📋 Found container ID from shell command: {container_id}")
859
+ except Exception as e:
860
+ print(f"Error with shell command approach: {e}")
861
+
862
+ # Approach 4: Get all containers and find the one with our app
863
+ if not container_id:
864
+ try:
865
+ print("Trying app matching approach...")
866
+ cmd = "modal container list"
867
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
868
+ output = result.stdout
869
+
870
+ # Look for our app name in the output
871
+ if app_name in output:
872
+ print(f"Found {app_name} in container list")
873
+ # Try to get the container ID from the same line
874
+ lines = output.split('\n')
875
+ for line in lines:
876
+ if app_name in line:
877
+ print(f"Found line: {line}")
878
+ # Try to extract the first column
879
+ if '│' in line:
880
+ container_id_part = line.split('│')[1].strip()
881
+ if container_id_part.startswith('ta-'):
882
+ container_id = container_id_part
883
+ print(f"📋 Found container ID from app matching: {container_id}")
884
+ break
885
+ except Exception as e:
886
+ print(f"Error with app matching approach: {e}")
887
+
888
+ # Final fallback: Use sandbox ID to create a container ID
889
+ if not container_id:
890
+ print("⚠️ All approaches failed to find container ID")
891
+ # Use sandbox ID as container prefix
892
+ short_id = sandbox_id.split('-')[1][:8] if '-' in sandbox_id else sandbox_id[:8]
893
+ container_id = f"ta-{short_id.upper()}"
894
+ print(f"📋 Using derived container ID: {container_id}")
895
+
896
+ # Ensure we have a non-None container ID
897
+ if not container_id:
898
+ print("⚠️ Critical error: Failed to determine container ID")
899
+ print("⚠️ Using a placeholder container ID")
900
+ container_id = "ta-UNKNOWN"
901
+
902
+ # Try to verify the container ID exists
903
+ print("🔍 Verifying container ID...")
904
+ verify_cmd = f"modal container logs {container_id} --tail 1 2>/dev/null || echo 'Container not found'"
905
+ verify_result = subprocess.run(verify_cmd, shell=True, capture_output=True, text=True)
906
+ if "Container not found" in verify_result.stdout:
907
+ print(f"⚠️ Container ID verification failed: {container_id}")
908
+
909
+ # Last resort: Try to find any valid container
910
+ print("🔍 Looking for any valid container as last resort...")
911
+ list_cmd = "modal container list | grep -v Container | grep -v '─' | grep -v '┏' | grep -v '┃' | head -1"
912
+ list_result = subprocess.run(list_cmd, shell=True, capture_output=True, text=True)
913
+ if list_result.stdout.strip():
914
+ print(f"Found container line: {list_result.stdout.strip()}")
915
+ # Try to extract the ID from the first column
916
+ container_line = list_result.stdout.strip()
917
+ if '│' in container_line:
918
+ possible_id = container_line.split('│')[1].strip()
919
+ if possible_id.startswith('ta-'):
920
+ container_id = possible_id
921
+ print(f"📋 Using container ID from list as last resort: {container_id}")
922
+
923
+ # Verify this container
924
+ verify_cmd = f"modal container logs {container_id} --tail 1 2>/dev/null || echo 'Container not found'"
925
+ verify_result = subprocess.run(verify_cmd, shell=True, capture_output=True, text=True)
926
+ if "Container not found" not in verify_result.stdout:
927
+ print(f"✅ Last resort container ID verified: {container_id}")
928
+ else:
929
+ print("⚠️ Last resort container ID also failed verification")
930
+
931
+ print("⚠️ Container connection may fail. You may need to connect manually.")
932
+ else:
933
+ print(f"✅ Container ID verified: {container_id}")
934
+
935
+ # Function to convert bytes to string
936
+ def _to_str(maybe_bytes):
937
+ try:
938
+ return (maybe_bytes.decode('utf-8') if isinstance(maybe_bytes, (bytes, bytearray)) else maybe_bytes)
939
+ except UnicodeDecodeError:
940
+ # Handle non-UTF-8 bytes by replacing invalid characters
941
+ if isinstance(maybe_bytes, (bytes, bytearray)):
942
+ return maybe_bytes.decode('utf-8', errors='replace')
943
+ else:
944
+ return str(maybe_bytes)
945
+ except Exception:
946
+ # Last resort fallback
947
+ return str(maybe_bytes)
948
+
949
+ # Skip the persistent shell approach for now due to async stream complexity
950
+ print("🔍 Modal's async streams require complex async handling")
951
+ print("🔄 Switching to individual command execution approach for reliability...")
952
+
953
+ # Initialize state tracking variables
954
+ current_dir = "/"
955
+ execution_history = []
956
+
957
+ # Function to run commands using individual sandbox.exec calls
958
+ def run_command(cmd, show_output=True, retry_count=0, max_retries=3, debug_with_llm=True, timeout=600):
959
+ """
960
+ Execute a command in the sandbox with error handling and automatic retries.
961
+
962
+ When a command fails and is fixed by the LLM debugging system, the retry count
963
+ is reset to 0, so successful fixes don't count against the maximum retry limit.
964
+ This ensures that a command that's been fixed gets a fresh set of retry attempts.
965
+ """
966
+ # Use the outer scope variables
967
+ nonlocal current_dir, execution_history, sandbox, previous_errors
968
+ nonlocal conda_installed, python_version_switched, current_python_version
969
+
970
+ # Record command start time
971
+ command_start_time = datetime.datetime.now().isoformat()
972
+ start_time = time.time()
973
+
974
+ # Prevent infinite retry loops
975
+ if retry_count >= max_retries:
976
+ print(f"⚠️ Maximum retry count ({max_retries}) reached. Stopping retries.")
977
+ return False, "", f"Maximum retry count ({max_retries}) reached"
978
+
979
+ # Special handling for cd commands to prevent common navigation errors
980
+ if cmd.strip().startswith("cd "):
981
+ # Extract the target directory from the cd command
982
+ cd_parts = cmd.split(None, 1)
983
+ if len(cd_parts) >= 2:
984
+ target_dir = cd_parts[1].strip().strip('"\'')
985
+
986
+ # Check if this is a repo name that matches the end of current_dir
987
+ # This prevents errors like "cd repo-name" when already in "/root/repo-name"
988
+ # BUT we need to be careful about nested directories like /root/litex/litex
989
+ if (target_dir != "/" and target_dir != "." and target_dir != ".." and
990
+ not target_dir.startswith("/") and not target_dir.startswith("./") and
991
+ not target_dir.startswith("../") and current_dir.endswith("/" + target_dir)):
992
+
993
+ # Advanced check: analyze directory contents to determine if navigation makes sense
994
+ print(f"🔍 Analyzing directory contents to determine navigation necessity...")
995
+
996
+ # Get current directory contents
997
+ current_contents_cmd = "ls -la"
998
+ current_result = sandbox.exec("bash", "-c", current_contents_cmd)
999
+ current_result.wait()
1000
+ current_contents = _to_str(current_result.stdout) if current_result.stdout else ""
1001
+
1002
+ # Check if target directory exists
1003
+ test_cmd = f"test -d \"{target_dir}\""
1004
+ test_result = sandbox.exec("bash", "-c", test_cmd)
1005
+ test_result.wait()
1006
+
1007
+ if test_result.returncode == 0:
1008
+ # Target directory exists, get its contents
1009
+ target_contents_cmd = f"ls -la \"{target_dir}\""
1010
+ target_result = sandbox.exec("bash", "-c", target_contents_cmd)
1011
+ target_result.wait()
1012
+ target_contents = _to_str(target_result.stdout) if target_result.stdout else ""
1013
+
1014
+ try:
1015
+ # Call LLM for analysis with the dedicated function
1016
+ llm_response = analyze_directory_navigation_with_llm(current_dir, target_dir, current_contents, target_contents, api_key)
1017
+
1018
+ # Extract decision from LLM response
1019
+ if llm_response and "NAVIGATE" in llm_response.upper():
1020
+ print(f"🤖 LLM Analysis: Navigation makes sense - contents are different")
1021
+ print(f"📂 Current: {current_dir}")
1022
+ print(f"🎯 Target: {target_dir}")
1023
+ print(f"🔄 Proceeding with navigation...")
1024
+ else:
1025
+ print(f"🤖 LLM Analysis: Navigation is redundant - contents are similar")
1026
+ print(f"⚠️ Detected redundant directory navigation: {cmd}")
1027
+ print(f"📂 Already in the correct directory: {current_dir}")
1028
+ print(f"✅ Skipping unnecessary navigation command")
1029
+ return True, f"Already in directory {current_dir}", ""
1030
+
1031
+ except Exception as e:
1032
+ print(f"⚠️ LLM analysis failed: {e}")
1033
+ print(f"🔄 Falling back to simple directory existence check...")
1034
+ # Fallback to simple check
1035
+ print(f"🔍 Detected nested directory '{target_dir}' exists in current location")
1036
+ print(f"📂 Current: {current_dir}")
1037
+ print(f"🎯 Target: {target_dir}")
1038
+ print(f"🔄 Proceeding with navigation to nested directory...")
1039
+ else:
1040
+ # No nested directory exists, so this is truly redundant
1041
+ print(f"⚠️ Detected redundant directory navigation: {cmd}")
1042
+ print(f"📂 Already in the correct directory: {current_dir}")
1043
+ print(f"✅ Skipping unnecessary navigation command")
1044
+ return True, f"Already in directory {current_dir}", ""
1045
+
1046
+ # Remove any parenthetical text that could cause syntax errors in bash
1047
+ if '(' in cmd:
1048
+ original_cmd = cmd
1049
+ cmd = re.sub(r'\([^)]*\)', '', cmd).strip()
1050
+ print(f"🔄 Removing parenthetical text:")
1051
+ print(f" Original: {original_cmd}")
1052
+ print(f" Cleaned: {cmd}")
1053
+
1054
+ # Convert pip install commands to use uv for faster installation
1055
+ original_cmd = cmd
1056
+ if 'uv_path' in globals() and uv_path and ('pip install' in cmd or 'pip3 install' in cmd) and not cmd.startswith(uv_path):
1057
+ # Replace pip/pip3 install with uv pip install, but only if not already using uv
1058
+ cmd = cmd.replace('pip install', f'{uv_path} pip install')
1059
+ cmd = cmd.replace('pip3 install', f'{uv_path} pip install')
1060
+ print(f"🚀 Converting to uv for faster installation:")
1061
+ print(f" Original: {original_cmd}")
1062
+ print(f" Converted: {cmd}")
1063
+
1064
+ print(f"\n▶ {cmd}\n")
1065
+
1066
+ # Check if this is a potentially long-running command
1067
+ long_running_patterns = [
1068
+ 'pip install', 'apt install', 'yum install',
1069
+ 'wget', 'curl', 'git clone', 'npm install', 'yarn install',
1070
+ 'cmake', 'make', 'gcc', 'g++', 'python setup.py'
1071
+ ]
1072
+
1073
+ is_long_running = any(pattern in cmd.lower() for pattern in long_running_patterns)
1074
+ if is_long_running:
1075
+ print(f"⏱️ Detected potentially long-running command. This may take several minutes...")
1076
+ print(f"📦 Large packages (like PyTorch) can take 5-10 minutes to download and install.")
1077
+ print(f"🔄 The container has a 30-minute timeout to accommodate this.")
1078
+
1079
+ # Use the original command without modification for interactivity
1080
+ cmd_to_execute = cmd
1081
+
1082
+ # Special handling for huggingface-cli login command
1083
+ if "huggingface-cli login" in cmd_to_execute:
1084
+ print("🔍 Detected huggingface-cli login command")
1085
+ print("🔄 Using non-interactive login approach with token instead")
1086
+
1087
+ # Check if the command already has a token
1088
+ if "--token" in cmd_to_execute:
1089
+ print("✅ Command already includes token parameter")
1090
+ else:
1091
+ # Prompt for HF token
1092
+ hf_token = prompt_for_hf_token()
1093
+ if hf_token:
1094
+ # Replace with non-interactive command
1095
+ cmd_to_execute = f"huggingface-cli login --token {hf_token} --add-to-git-credential"
1096
+ print(f"🔄 Using non-interactive command: {cmd_to_execute}")
1097
+ else:
1098
+ print("❌ No token provided. Cannot continue with Hugging Face login.")
1099
+ return False, "", "No Hugging Face token provided"
1100
+
1101
+ # Special handling for wandb login command
1102
+ elif "wandb login" in cmd_to_execute and "YOUR_API_KEY" not in cmd_to_execute:
1103
+ print("🔍 Detected Weights & Biases login command")
1104
+ print("🔄 Using API key approach for non-interactive login")
1105
+
1106
+ # Check if the command already includes an API key
1107
+ has_api_key = False
1108
+ cmd_parts = cmd_to_execute.split()
1109
+ for part in cmd_parts:
1110
+ if part != "wandb" and part != "login" and not part.startswith("-"):
1111
+ has_api_key = True
1112
+ break
1113
+
1114
+ if not has_api_key:
1115
+ # Prompt for W&B API key
1116
+ print("\n" + "="*60)
1117
+ print("🔑 WEIGHTS & BIASES API KEY REQUIRED")
1118
+ print("="*60)
1119
+ print("You can get your API key from: https://wandb.ai/authorize")
1120
+ print("📝 Please paste your W&B API key below:")
1121
+ print(" (Your input will be hidden for security)")
1122
+ print("-" * 60)
1123
+
1124
+ try:
1125
+ api_key = getpass.getpass("W&B API Key: ").strip()
1126
+ if not api_key:
1127
+ print("❌ No API key provided. Cannot continue with W&B login.")
1128
+ return False, "", "No W&B API key provided"
1129
+
1130
+ # Validate API key length (typically 40 characters)
1131
+ if len(api_key) != 40:
1132
+ print(f"⚠️ Warning: API key should be 40 characters long, yours was {len(api_key)}")
1133
+ confirm = input("Continue anyway? (yes/no): ").strip().lower()
1134
+ if confirm not in ["yes", "y"]:
1135
+ print("❌ W&B login cancelled.")
1136
+ return False, "", "W&B login cancelled"
1137
+
1138
+ print("✅ API key received successfully!")
1139
+
1140
+ # Replace with non-interactive command
1141
+ cmd_to_execute = f"wandb login {api_key}"
1142
+ print(f"🔄 Using non-interactive command: wandb login [API_KEY_HIDDEN]")
1143
+ except KeyboardInterrupt:
1144
+ print("\n❌ API key input cancelled by user.")
1145
+ return False, "", "W&B API key input cancelled"
1146
+ except Exception as e:
1147
+ print(f"❌ Error getting API key: {e}")
1148
+ return False, "", f"Error getting W&B API key: {e}"
1149
+
1150
+ # Validate the command before execution
1151
+ if not cmd_to_execute or cmd_to_execute.strip() == "":
1152
+ print("⚠️ Empty command detected, skipping execution")
1153
+ return False, "", "Empty command"
1154
+
1155
+ # Sanitize command to prevent issues with special characters
1156
+ # Remove any null bytes or other problematic characters
1157
+ cmd_to_execute = cmd_to_execute.replace('\x00', '').strip()
1158
+
1159
+ if len(cmd_to_execute) > 10000: # Prevent extremely long commands
1160
+ print("⚠️ Command too long, truncating")
1161
+ cmd_to_execute = cmd_to_execute[:10000]
1162
+
1163
+ # Prepare the command with environment variables and error handling
1164
+ full_command = f"""
1165
+ # Change to current directory
1166
+ cd "{current_dir}"
1167
+
1168
+ # Execute the command
1169
+ {cmd_to_execute}
1170
+ """
1171
+
1172
+ # Execute the command using sandbox.exec
1173
+ try:
1174
+ print(f"🔄 Executing command in directory: {current_dir}")
1175
+
1176
+ # Use sandbox.exec for individual command execution
1177
+ result = sandbox.exec("bash", "-c", full_command.strip())
1178
+
1179
+ # Collect output in real-time - Modal streams are already set up for line-by-line streaming
1180
+ stdout_lines = []
1181
+ stderr_lines = []
1182
+
1183
+ # Process output streams in real-time - Modal handles this natively
1184
+ # We don't need to use threading here as Modal's streams are designed to be consumed directly
1185
+ if show_output:
1186
+ print("\n--- Command Output ---")
1187
+
1188
+ # Track if we've shown timeout warnings
1189
+ timeout_warnings = set()
1190
+ last_output_time = time.time()
1191
+
1192
+ # Read stdout in real-time
1193
+ for line in result.stdout:
1194
+ # Check for timeout
1195
+ current_time = time.time()
1196
+ elapsed = current_time - start_time
1197
+ time_since_output = current_time - last_output_time
1198
+
1199
+ # Show timeout warning every 30 seconds if no output for 30+ seconds
1200
+ if time_since_output > 30 and int(time_since_output) // 30 not in timeout_warnings:
1201
+ warning_time = int(time_since_output) // 30 * 30
1202
+ timeout_warnings.add(int(time_since_output) // 30)
1203
+ print(f"Still running after {int(elapsed)} seconds...")
1204
+
1205
+ # If total time exceeds timeout, break
1206
+ if elapsed > timeout:
1207
+ print(f"⚠️ Command timed out after {timeout} seconds")
1208
+ # Force terminate the command
1209
+ try:
1210
+ result.terminate()
1211
+ except:
1212
+ pass
1213
+ return False, "Command timed out", f"Command execution exceeded timeout of {timeout} seconds"
1214
+
1215
+ # Process the line
1216
+ line_str = _to_str(line)
1217
+ stdout_lines.append(line_str)
1218
+ if show_output:
1219
+ # Print immediately with flush to ensure real-time display
1220
+ print(line_str, end="", flush=True)
1221
+
1222
+ # Update last output time
1223
+ last_output_time = time.time()
1224
+
1225
+ # Read stderr in real-time
1226
+ for line in result.stderr:
1227
+ # Check for timeout
1228
+ current_time = time.time()
1229
+ elapsed = current_time - start_time
1230
+ time_since_output = current_time - last_output_time
1231
+
1232
+ # Show timeout warning every 30 seconds if no output for 30+ seconds
1233
+ if time_since_output > 30 and int(time_since_output) // 30 not in timeout_warnings:
1234
+ warning_time = int(time_since_output) // 30 * 30
1235
+ timeout_warnings.add(int(time_since_output) // 30)
1236
+ print(f"Still running after {int(elapsed)} seconds...")
1237
+
1238
+ # If total time exceeds timeout, break
1239
+ if elapsed > timeout:
1240
+ print(f"⚠️ Command timed out after {timeout} seconds")
1241
+ # Force terminate the command
1242
+ try:
1243
+ result.terminate()
1244
+ except:
1245
+ pass
1246
+ return False, "Command timed out", f"Command execution exceeded timeout of {timeout} seconds"
1247
+
1248
+ # Process the line
1249
+ line_str = _to_str(line)
1250
+ stderr_lines.append(line_str)
1251
+ if show_output:
1252
+ # Print immediately with flush to ensure real-time display
1253
+ print(line_str, end="", file=sys.stderr, flush=True)
1254
+
1255
+ # Update last output time
1256
+ last_output_time = time.time()
1257
+
1258
+ if show_output:
1259
+ print("--- End Output ---\n")
1260
+
1261
+ stdout_buffer = ''.join(stdout_lines)
1262
+ stderr_buffer = ''.join(stderr_lines)
1263
+
1264
+ # Wait for the process to complete before accessing returncode
1265
+ result.wait()
1266
+ exit_code = result.returncode
1267
+
1268
+ except Exception as e:
1269
+ print(f"❌ Error executing command: {e}")
1270
+ return False, "", str(e)
1271
+
1272
+ # Record command completion time
1273
+ command_end_time = datetime.datetime.now().isoformat()
1274
+
1275
+ # Calculate duration in seconds
1276
+ start_dt = datetime.datetime.fromisoformat(command_start_time)
1277
+ end_dt = datetime.datetime.fromisoformat(command_end_time)
1278
+ duration = (end_dt - start_dt).total_seconds()
1279
+
1280
+ # Record this command execution in history
1281
+ execution_record = {
1282
+ "command": cmd_to_execute,
1283
+ "original_command": cmd if cmd != cmd_to_execute else None,
1284
+ "start_time": command_start_time,
1285
+ "end_time": command_end_time,
1286
+ "duration_seconds": duration,
1287
+ "exit_code": exit_code,
1288
+ "stdout": stdout_buffer,
1289
+ "stderr": stderr_buffer,
1290
+ "directory": current_dir
1291
+ }
1292
+ execution_history.append(execution_record)
1293
+
1294
+ # Update current directory if this was a cd command and it succeeded
1295
+ if cmd_to_execute.strip().startswith("cd ") and exit_code == 0:
1296
+ # Extract the target directory from the cd command
1297
+ cd_parts = cmd_to_execute.split(None, 1)
1298
+ if len(cd_parts) >= 2:
1299
+ target_dir = cd_parts[1].strip('"\'')
1300
+
1301
+ # Store the previous directory for logging
1302
+ previous_dir = current_dir
1303
+
1304
+ # Handle different types of paths
1305
+ if target_dir.startswith('/'):
1306
+ # Absolute path
1307
+ current_dir = target_dir
1308
+ elif target_dir == '..':
1309
+ # Parent directory
1310
+ current_dir = '/'.join(current_dir.rstrip('/').split('/')[:-1]) or '/'
1311
+ elif target_dir == '.':
1312
+ # Current directory - no change
1313
+ pass
1314
+ else:
1315
+ # Relative path - handle special case where target is already at the end of current_dir
1316
+ if current_dir.endswith('/' + target_dir):
1317
+ print(f"📂 Already in directory {current_dir}, no change needed")
1318
+ else:
1319
+ current_dir = f"{current_dir.rstrip('/')}/{target_dir}"
1320
+
1321
+ print(f"📂 Updated current directory: {previous_dir} -> {current_dir}")
1322
+ execution_record["new_current_dir"] = current_dir
1323
+
1324
+ # Verify the directory actually exists
1325
+ verify_cmd = f"test -d \"{current_dir}\""
1326
+ verify_result = sandbox.exec("bash", "-c", verify_cmd)
1327
+ verify_result.wait()
1328
+
1329
+ if verify_result.returncode != 0:
1330
+ print(f"⚠️ Warning: Directory {current_dir} does not exist")
1331
+ print(f"⚠️ Reverting to previous directory: {previous_dir}")
1332
+ current_dir = previous_dir
1333
+ execution_record["new_current_dir"] = current_dir
1334
+
1335
+ # Check for errors and handle Hugging Face token issues
1336
+ if exit_code != 0:
1337
+ # Check for specific Hugging Face token errors
1338
+ hf_token_error_patterns = [
1339
+ "Token is required",
1340
+ "LocalTokenNotFoundError",
1341
+ "Invalid user token",
1342
+ "401 Client Error: Unauthorized",
1343
+ "Invalid credentials in Authorization header",
1344
+ "HF_TOKEN environment variable is invalid"
1345
+ ]
1346
+
1347
+ is_hf_token_error = any(pattern in stderr_buffer for pattern in hf_token_error_patterns)
1348
+
1349
+ if is_hf_token_error:
1350
+ print(f"🔑 Detected Hugging Face token authentication error!")
1351
+ print(f"🔍 Error details: {stderr_buffer}")
1352
+
1353
+ # Prompt for the real token
1354
+ real_token = prompt_for_hf_token()
1355
+
1356
+ if real_token:
1357
+ print(f"🔄 Setting HF_TOKEN and retrying command...")
1358
+
1359
+ # Retry with the token set
1360
+ token_command = f"export HF_TOKEN='{real_token}'; {cmd_to_execute}"
1361
+ return run_command(token_command, show_output, retry_count + 1, max_retries)
1362
+ else:
1363
+ print("❌ No token provided. Cannot continue with Hugging Face operations.")
1364
+ return False, stdout_buffer, "No Hugging Face token provided"
1365
+
1366
+ # Check for "No such file or directory" errors with cd commands
1367
+ if "cd " in cmd_to_execute and "No such file or directory" in stderr_buffer:
1368
+ print("⚠️ Directory navigation error detected")
1369
+
1370
+ # Extract the target directory from the cd command
1371
+ cd_parts = cmd_to_execute.split(None, 1)
1372
+ if len(cd_parts) >= 2:
1373
+ target_dir = cd_parts[1].strip('"\'')
1374
+
1375
+ # Check if this might be a repository name that's already in the path
1376
+ if not target_dir.startswith('/') and '/' + target_dir in current_dir:
1377
+ print(f"🔍 The directory '{target_dir}' appears to be part of the current path: {current_dir}")
1378
+ print(f"⚠️ This is likely a redundant navigation attempt")
1379
+
1380
+ # If we're already in a directory that ends with the target, consider it a success
1381
+ if current_dir.endswith('/' + target_dir):
1382
+ print(f"✅ Already in the correct directory: {current_dir}")
1383
+ return True, f"Already in directory {current_dir}", ""
1384
+
1385
+ print(f"⚠️ Command failed with exit code {exit_code}")
1386
+ if stderr_buffer.strip():
1387
+ print(f"Error output: {stderr_buffer}")
1388
+
1389
+ # If command failed and we're debugging with LLM
1390
+ if debug_with_llm:
1391
+ print("🔍 Attempting to debug the failed command with OpenAI...")
1392
+
1393
+ # Check if the command is a hanging huggingface-cli login
1394
+ if "huggingface-cli login" in cmd_to_execute and not stderr_buffer.strip():
1395
+ print("🔍 Detected hanging huggingface-cli login command")
1396
+ print("🔄 Using non-interactive login approach with HF_TOKEN instead")
1397
+
1398
+ # Prompt for HF token
1399
+ hf_token = prompt_for_hf_token()
1400
+ if hf_token:
1401
+ # Set the token as environment variable and create .huggingface folder
1402
+ print("✅ Token received, setting up non-interactive authentication")
1403
+ setup_commands = [
1404
+ "mkdir -p ~/.huggingface",
1405
+ f"echo '{hf_token}' > ~/.huggingface/token",
1406
+ f"export HF_TOKEN='{hf_token}'",
1407
+ "echo 'HF_TOKEN and token file have been set up'"
1408
+ ]
1409
+
1410
+ for setup_cmd in setup_commands:
1411
+ setup_success, setup_stdout, _ = run_command(setup_cmd, show_output=True, debug_with_llm=False)
1412
+ if not setup_success:
1413
+ print(f"⚠️ Setup command failed: {setup_cmd}")
1414
+
1415
+ print("✅ Hugging Face authentication set up non-interactively")
1416
+ return True, "Hugging Face authentication set up successfully", ""
1417
+ else:
1418
+ print("❌ No token provided. Cannot set up Hugging Face authentication.")
1419
+ return False, "", "No Hugging Face token provided"
1420
+
1421
+ # Check if the error is related to missing pytest
1422
+ if "ModuleNotFoundError: No module named 'pytest'" in stderr_buffer or "ImportError: No module named pytest" in stderr_buffer:
1423
+ print("🔍 Detected missing pytest module, installing it automatically...")
1424
+ pytest_install_success, _, _ = run_command("pip install pytest", show_output=True, debug_with_llm=False)
1425
+ if pytest_install_success:
1426
+ print("✅ Successfully installed pytest, retrying original command...")
1427
+ return run_command(cmd, show_output, retry_count + 1, max_retries)
1428
+
1429
+ # Check for Python version-specific errors
1430
+ python_version_errors = [
1431
+ # Python 3.13 distutils issue
1432
+ ("ModuleNotFoundError: No module named 'distutils'", "3.13"),
1433
+ # Add more version-specific error patterns here
1434
+ ("ImportError: cannot import name 'soft_unicode' from 'markupsafe'", None),
1435
+ ("AttributeError: module 'setuptools.dist' has no attribute 'check_specifier'", None)
1436
+ ]
1437
+
1438
+ # Check if any of the error patterns match
1439
+ for error_pattern, problematic_version in python_version_errors:
1440
+ if error_pattern in stderr_buffer:
1441
+ print(f"🔍 Detected Python version-specific error: {error_pattern}")
1442
+
1443
+ # Get current Python version if not already known
1444
+ if not current_python_version:
1445
+ version_cmd = "python --version"
1446
+ version_success, version_stdout, _ = run_command(version_cmd, show_output=False, debug_with_llm=False)
1447
+ if version_success:
1448
+ current_python_version = version_stdout.strip()
1449
+ print(f"🐍 Current Python version: {current_python_version}")
1450
+
1451
+ # Check if we've already tried switching Python versions
1452
+ if python_version_switched:
1453
+ print("⚠️ Already attempted to switch Python versions once, not trying again")
1454
+ break
1455
+
1456
+ print("🔄 Attempting to fix by switching Python version...")
1457
+
1458
+ # Install conda if not already installed
1459
+ if not conda_installed:
1460
+ print("📦 Installing Miniconda to manage Python versions...")
1461
+ conda_install_cmds = [
1462
+ "apt-get update -y",
1463
+ "apt-get install -y wget bzip2",
1464
+ "wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh",
1465
+ "bash /tmp/miniconda.sh -b -p /opt/conda",
1466
+ "rm /tmp/miniconda.sh",
1467
+ "echo 'export PATH=/opt/conda/bin:$PATH' >> ~/.bashrc",
1468
+ "export PATH=/opt/conda/bin:$PATH",
1469
+ "conda init bash",
1470
+ "source ~/.bashrc",
1471
+ "conda activate base"
1472
+ ]
1473
+
1474
+ for conda_cmd in conda_install_cmds:
1475
+ print(f"🔄 Running: {conda_cmd}")
1476
+ conda_success, _, _ = run_command(conda_cmd, show_output=True, debug_with_llm=False)
1477
+ if not conda_success:
1478
+ print("⚠️ Failed to install conda, continuing with system Python")
1479
+ break
1480
+
1481
+ # Check if conda was successfully installed
1482
+ conda_check_cmd = "conda --version"
1483
+ conda_check_success, conda_check_stdout, _ = run_command(conda_check_cmd, show_output=True, debug_with_llm=False)
1484
+ conda_installed = conda_check_success
1485
+
1486
+ if conda_installed:
1487
+ print(f"✅ Successfully installed conda: {conda_check_stdout.strip()}")
1488
+ else:
1489
+ print("⚠️ Failed to verify conda installation")
1490
+ break
1491
+
1492
+ # Determine target Python version
1493
+ target_version = "3.10" # Default to a stable version
1494
+ if problematic_version == "3.13":
1495
+ # If we're on 3.13 and having issues, go to 3.10
1496
+ target_version = "3.10"
1497
+ elif "3.13" in str(current_python_version):
1498
+ # If we're on 3.13 for any other error, try 3.10
1499
+ target_version = "3.10"
1500
+ elif "3.10" in str(current_python_version):
1501
+ # If we're on 3.10 and having issues, try 3.9
1502
+ target_version = "3.9"
1503
+
1504
+ print(f"🐍 Switching from {current_python_version} to Python {target_version}...")
1505
+
1506
+ # Create and activate a conda environment with the target Python version
1507
+ conda_cmds = [
1508
+ f"conda create -y -n py{target_version} python={target_version}",
1509
+ f"echo 'conda activate py{target_version}' >> ~/.bashrc",
1510
+ f"conda init bash",
1511
+ f"source ~/.bashrc",
1512
+ f"conda activate py{target_version}"
1513
+ ]
1514
+
1515
+ for conda_cmd in conda_cmds:
1516
+ print(f"🔄 Running: {conda_cmd}")
1517
+ conda_success, _, _ = run_command(conda_cmd, show_output=True, debug_with_llm=False)
1518
+ if not conda_success:
1519
+ print(f"⚠️ Failed to run conda command: {conda_cmd}")
1520
+
1521
+ # Verify Python version changed
1522
+ verify_cmd = "python --version"
1523
+ verify_success, verify_stdout, _ = run_command(verify_cmd, show_output=True, debug_with_llm=False)
1524
+
1525
+ if verify_success and target_version in verify_stdout:
1526
+ print(f"✅ Successfully switched to Python {verify_stdout.strip()}")
1527
+ python_version_switched = True
1528
+ current_python_version = verify_stdout.strip()
1529
+
1530
+ # Reinstall pip and setuptools in the new environment
1531
+ print("📦 Installing pip and setuptools in new environment...")
1532
+ run_command("pip install --upgrade pip setuptools wheel", show_output=True, debug_with_llm=False)
1533
+
1534
+ # Retry the original command with the new Python version
1535
+ print(f"🔄 Retrying original command with Python {target_version}...")
1536
+ # Reset the retry counter since we've made a significant change
1537
+ return run_command(cmd, show_output, 0, max_retries)
1538
+ else:
1539
+ print("⚠️ Failed to switch Python version, continuing with current version")
1540
+
1541
+ break
1542
+
1543
+ # Check if stderr is empty, try to use stdout as fallback
1544
+ debug_output = stderr_buffer
1545
+ if not debug_output or not debug_output.strip():
1546
+ print("⚠️ stderr is empty, checking if stdout contains error information...")
1547
+ if stdout_buffer and stdout_buffer.strip():
1548
+ print("✅ Using stdout for debugging as stderr is empty")
1549
+ debug_output = stdout_buffer
1550
+ else:
1551
+ print("⚠️ Both stderr and stdout are empty. Limited debugging information available.")
1552
+ debug_output = f"Command failed with exit code {exit_code}, but no error output was captured."
1553
+
1554
+ # Print debug output for verification
1555
+ print(f"🔍 Debug output to be sent to OpenAI ({len(debug_output)} chars):")
1556
+ print("="*60)
1557
+ print(debug_output if debug_output else "[EMPTY]")
1558
+ print("="*60)
1559
+
1560
+ fix_command = call_openai_for_debug(cmd_to_execute, debug_output, current_dir=current_dir, sandbox=sandbox)
1561
+
1562
+ if fix_command:
1563
+ print(f"🔧 OpenAI suggested fix command: {fix_command}")
1564
+
1565
+ # Check if the suggested command is "wandb login YOUR_API_KEY" or similar
1566
+ if "wandb login" in fix_command and ("YOUR_API_KEY" in fix_command or "[your_api_key]" in fix_command):
1567
+ print("🔍 Detected placeholder API key in suggested command")
1568
+ print("🔄 Prompting for actual W&B API key instead")
1569
+
1570
+ # Prompt for W&B API key
1571
+ print("\n" + "="*60)
1572
+ print("🔑 WEIGHTS & BIASES API KEY REQUIRED")
1573
+ print("="*60)
1574
+ print("You can get your API key from: https://wandb.ai/authorize")
1575
+ print("📝 Please paste your W&B API key below:")
1576
+ print(" (Your input will be hidden for security)")
1577
+ print("-" * 60)
1578
+
1579
+ try:
1580
+ api_key = getpass.getpass("W&B API Key: ").strip()
1581
+ if api_key:
1582
+ # Replace placeholder with actual API key
1583
+ fix_command = f"wandb login {api_key}"
1584
+ print(f"🔄 Using actual API key: wandb login [API_KEY_HIDDEN]")
1585
+ else:
1586
+ print("❌ No API key provided. Cannot continue with W&B login.")
1587
+ return False, stdout_buffer, stderr_buffer
1588
+ except Exception as e:
1589
+ print(f"❌ Error getting API key: {e}")
1590
+ return False, stdout_buffer, stderr_buffer
1591
+
1592
+ # Special handling for cd commands to prevent directory navigation loops
1593
+ if fix_command.strip().startswith("cd "):
1594
+ # Extract the target directory from the cd command
1595
+ cd_parts = fix_command.split(None, 1)
1596
+ if len(cd_parts) >= 2:
1597
+ target_dir = cd_parts[1].strip('"\'')
1598
+
1599
+ # Check if this is trying to navigate to a directory we're already in
1600
+ if target_dir.endswith(current_dir.split('/')[-1]) or current_dir.endswith('/' + target_dir):
1601
+ print(f"⚠️ Detected potential directory navigation loop")
1602
+ print(f"🔍 Current directory: {current_dir}")
1603
+ print(f"🔍 Suggested navigation: {target_dir}")
1604
+
1605
+ # Check if we're already in the target directory or a directory that contains it
1606
+ if current_dir.endswith('/' + target_dir) or ('/' + target_dir + '/' in current_dir):
1607
+ print(f"✅ Already in or past the target directory")
1608
+ print(f"🔄 Skipping redundant navigation and retrying the original command")
1609
+ return run_command(cmd, show_output, retry_count + 1, max_retries)
1610
+
1611
+ # Automatically run the fix command without asking for permission
1612
+ print(f"🔄 Running suggested fix command: {fix_command}")
1613
+ # Run the fix command with debugging disabled to prevent infinite loop
1614
+ fix_success, fix_stdout, fix_stderr = run_command(fix_command, show_output=True, debug_with_llm=False)
1615
+
1616
+ if fix_success:
1617
+ print("✅ Fix command succeeded!")
1618
+ # Retry the original command with reset retry count
1619
+ print(f"🔄 Retrying original command: {cmd}")
1620
+
1621
+ # Create a key for tracking this error
1622
+ error_key = f"{cmd}:{stderr_buffer[:100]}"
1623
+
1624
+ # Check if we've seen this error before
1625
+ if error_key in previous_errors:
1626
+ # We've seen this error before, don't reset the retry count
1627
+ previous_errors[error_key] += 1
1628
+ print(f"⚠️ Same error encountered {previous_errors[error_key]} times. Not resetting retry count.")
1629
+ return run_command(cmd, show_output, retry_count + 1, max_retries)
1630
+ else:
1631
+ # First time seeing this error, track it and reset retry count
1632
+ previous_errors[error_key] = 1
1633
+ print(f"🔄 Resetting retry count to 0 after successful fix")
1634
+ return run_command(cmd, show_output, 0, max_retries) # Reset retry count to 0
1635
+ else:
1636
+ print("❌ Fix command failed.")
1637
+ return False, stdout_buffer, stderr_buffer
1638
+
1639
+ return exit_code == 0, stdout_buffer, stderr_buffer
1640
+
1641
+ # Initialize the environment with basic commands
1642
+ print("🔄 Initializing environment...")
1643
+ init_commands = [
1644
+ "export PS1='$ '", # Set a simple prompt
1645
+ "export TERM=xterm-256color", # Set terminal type
1646
+ "source ~/.bashrc 2>/dev/null || true" # Source bashrc if available
1647
+ ]
1648
+
1649
+ # Add volume-specific initialization if volume is available
1650
+ if volume:
1651
+ volume_commands = [
1652
+ f"mkdir -p {volume_mount_path}/venvs", # Create virtual environments directory
1653
+ f"mkdir -p {volume_mount_path}/cache", # Create cache directory
1654
+ f"export PIP_CACHE_DIR={volume_mount_path}/cache/pip", # Pip cache
1655
+ f"export UV_CACHE_DIR={volume_mount_path}/cache/uv", # UV cache
1656
+ ]
1657
+ init_commands.extend(volume_commands)
1658
+ print(f"📦 Setting up persistent storage directories in {volume_mount_path}")
1659
+
1660
+ # Run initialization commands
1661
+ for i, init_cmd in enumerate(init_commands, 1):
1662
+ print(f"📋 Running init command {i}/{len(init_commands)}: {init_cmd}")
1663
+ success, stdout, stderr = run_command(init_cmd, show_output=False)
1664
+ if not success:
1665
+ print(f"⚠️ Init command failed: {stderr}")
1666
+
1667
+ print("✅ Environment initialization completed")
1668
+
1669
+ print("📦 Installing basic tools...")
1670
+ run_command("apt-get update && apt-get install -y git curl wget")
1671
+
1672
+ print("📦 Installing uv with pip...")
1673
+ run_command("pip install uv")
1674
+
1675
+ # Set uv path to system installation
1676
+ uv_path = "uv"
1677
+
1678
+ # Test if uv is available and working
1679
+ test_uv_cmd = f"{uv_path} --version || echo 'uv not found'"
1680
+ test_success, test_stdout, test_stderr = run_command(test_uv_cmd)
1681
+ if not test_success or 'uv not found' in test_stdout:
1682
+ print("⚠️ uv installation not found in system path, trying alternative installation...")
1683
+ # Try alternative installation method
1684
+ print("📦 Installing uv using the official installer...")
1685
+ run_command("curl -LsSf https://astral.sh/uv/install.sh | sh")
1686
+ run_command("source $HOME/.local/bin/env")
1687
+ run_command('export PATH="$HOME/.local/bin:$PATH"')
1688
+
1689
+ # Update path to the local installation
1690
+ uv_path = "$HOME/.local/bin/uv"
1691
+
1692
+ # Test again
1693
+ test_uv_cmd = f"{uv_path} --version || echo 'uv not found'"
1694
+ test_success, test_stdout, test_stderr = run_command(test_uv_cmd)
1695
+ if not test_success or 'uv not found' in test_stdout:
1696
+ print("⚠️ uv installation still failed, using standard pip")
1697
+ uv_path = ""
1698
+ else:
1699
+ print(f"✅ uv installed successfully via alternative method: {test_stdout.strip()}")
1700
+ else:
1701
+ print(f"✅ uv installed successfully via pip: {test_stdout.strip()}")
1702
+
1703
+ # Initialize repo_clone_dir for use throughout the function
1704
+ repo_clone_dir = "/root" # Always use home directory for repositories
1705
+
1706
+ # Clone repository if URL is provided
1707
+ if repo_url:
1708
+ try:
1709
+ # Extract repo name from URL
1710
+ repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
1711
+
1712
+ print(f"📥 Cloning repository in Modal container: {repo_url}")
1713
+
1714
+ # Determine the best location for the repository
1715
+ repo_clone_dir = "/root" # Always use home directory for repositories
1716
+ print(f"📦 Using home directory for repository: {repo_clone_dir}")
1717
+
1718
+ # Ensure we're in the home directory and update current directory tracking
1719
+ cd_success, cd_stdout, cd_stderr = run_command(f"cd {repo_clone_dir}", show_output=False)
1720
+ if cd_success:
1721
+ current_dir = repo_clone_dir
1722
+ print(f"📂 Successfully changed to: {repo_clone_dir}")
1723
+ else:
1724
+ print(f"⚠️ Failed to change to {repo_clone_dir}: {cd_stderr}")
1725
+ current_dir = "/"
1726
+
1727
+ # First, list current directory contents for debugging
1728
+ print("📂 Current directory contents before cloning:")
1729
+ run_command("pwd && ls -la", show_output=True)
1730
+
1731
+ # Check if repository already exists in current location
1732
+ print(f"🔍 Checking if {repo_name_from_url} directory exists...")
1733
+
1734
+ # First ensure we're in the right directory and check with absolute path
1735
+ check_cmd = f"cd {repo_clone_dir} && test -d {repo_name_from_url}"
1736
+ success, stdout, stderr = run_command(check_cmd, show_output=False, retry_count=0, max_retries=0)
1737
+
1738
+ # The directory exists if the test command succeeds (exit code 0)
1739
+ repo_exists = success
1740
+ print(f"📂 Repository check result: exists={repo_exists} (exit code: {0 if success else 1})")
1741
+ print(f"📂 Checking in directory: {repo_clone_dir}/{repo_name_from_url}")
1742
+
1743
+ if repo_exists:
1744
+ print(f"📂 Repository directory already exists: {repo_name_from_url}")
1745
+ # Check if it's actually a git repository - disable retries to avoid bad debugging
1746
+ git_check_cmd = f"cd {repo_clone_dir}/{repo_name_from_url} && git status"
1747
+ git_check_success, git_stdout, git_stderr = run_command(git_check_cmd, show_output=False, retry_count=0, max_retries=0)
1748
+ if git_check_success:
1749
+ print(f"✅ Valid git repository found, using existing: {repo_name_from_url}")
1750
+ else:
1751
+ print(f"⚠️ Directory exists but is not a valid git repository, removing and re-cloning...")
1752
+ remove_cmd = f"cd {repo_clone_dir} && rm -rf {repo_name_from_url}"
1753
+ run_command(remove_cmd, show_output=False)
1754
+ repo_exists = False
1755
+
1756
+ if not repo_exists:
1757
+ print(f"📥 Repository does not exist, proceeding with clone...")
1758
+ print(f"📥 Cloning repository: {repo_url}")
1759
+ print(f"📥 Repository name will be: {repo_name_from_url}")
1760
+ print(f"📥 Clone location: {repo_clone_dir}")
1761
+
1762
+ # Ensure we're in the right directory before cloning
1763
+ run_command(f"cd {repo_clone_dir}", show_output=False)
1764
+
1765
+ # Execute the git clone command with verbose output - use absolute path, disable retries
1766
+ clone_cmd = f"cd {repo_clone_dir} && git clone {repo_url}"
1767
+ clone_success, clone_stdout, clone_stderr = run_command(clone_cmd, show_output=True, retry_count=0, max_retries=0)
1768
+
1769
+ print(f"📥 Clone command completed. Success: {clone_success}")
1770
+ if clone_stdout.strip():
1771
+ print(f"📥 Clone stdout: {clone_stdout.strip()}")
1772
+ if clone_stderr.strip():
1773
+ print(f"📥 Clone stderr: {clone_stderr.strip()}")
1774
+
1775
+ if not clone_success:
1776
+ print(f"❌ Failed to clone repository: {clone_stderr}")
1777
+ print("🔄 Trying alternative clone methods...")
1778
+
1779
+ # Try with different git options - use absolute path, disable retries
1780
+ print("🔄 Attempting shallow clone...")
1781
+ shallow_clone_cmd = f"cd {repo_clone_dir} && git clone --depth 1 {repo_url}"
1782
+ clone_success, clone_stdout, clone_stderr = run_command(shallow_clone_cmd, show_output=True, retry_count=0, max_retries=0)
1783
+
1784
+ print(f"📥 Shallow clone command completed. Success: {clone_success}")
1785
+ if clone_stdout.strip():
1786
+ print(f"📥 Shallow clone stdout: {clone_stdout.strip()}")
1787
+ if clone_stderr.strip():
1788
+ print(f"📥 Shallow clone stderr: {clone_stderr.strip()}")
1789
+
1790
+ if not clone_success:
1791
+ print(f"❌ Alternative clone also failed: {clone_stderr}")
1792
+ print("⚠️ Continuing without repository...")
1793
+ repo_name_from_url = None
1794
+ else:
1795
+ print(f"✅ Repository cloned successfully with shallow clone")
1796
+ else:
1797
+ print(f"✅ Repository cloned successfully")
1798
+ else:
1799
+ print(f"📂 Repository already exists, skipping clone")
1800
+
1801
+ # Verify repository directory exists and change to it
1802
+ if repo_name_from_url:
1803
+ print("📂 Verifying repository directory...")
1804
+
1805
+ # List available directories for debugging
1806
+ print("📂 Available directories after cloning:")
1807
+ run_command("ls -la", show_output=True)
1808
+
1809
+ # Check if the repository directory exists using simple test
1810
+ check_success, _, _ = run_command(f"test -d {repo_name_from_url}", show_output=False)
1811
+
1812
+ if check_success:
1813
+ print(f"📂 Repository directory confirmed: {repo_name_from_url}")
1814
+ # Change to the repository directory
1815
+ cd_success, cd_stdout, cd_stderr = run_command(f"cd {repo_name_from_url}")
1816
+ if cd_success:
1817
+ print(f"📂 Successfully changed to repository directory: {repo_name_from_url}")
1818
+ repo_dir_name = f"{repo_clone_dir}/{repo_name_from_url}" if repo_clone_dir != "/" else repo_name_from_url
1819
+ else:
1820
+ print(f"⚠️ Failed to change to repository directory: {cd_stderr}")
1821
+ repo_dir_name = repo_clone_dir
1822
+ else:
1823
+ print(f"⚠️ Repository directory not found after cloning: {repo_name_from_url}")
1824
+ print("🔍 Looking for alternative directories...")
1825
+
1826
+ # Look for any git repositories
1827
+ search_success, search_stdout, search_stderr = run_command("find . -maxdepth 1 -type d -name '.git' -exec dirname {} \\;", show_output=False)
1828
+
1829
+ if search_success and search_stdout.strip():
1830
+ found_dirs = [d.replace('./', '') for d in search_stdout.strip().split('\n') if d.strip() and d != '.']
1831
+ if found_dirs:
1832
+ repo_dir_name = f"{repo_clone_dir}/{found_dirs[0]}" if repo_clone_dir != "/" else found_dirs[0]
1833
+ print(f"📂 Found git repository: {repo_dir_name}")
1834
+ run_command(f"cd {found_dirs[0]}")
1835
+ else:
1836
+ repo_dir_name = repo_clone_dir
1837
+ print("📂 Using current directory")
1838
+ else:
1839
+ repo_dir_name = repo_clone_dir
1840
+ print("📂 Using current directory")
1841
+ else:
1842
+ repo_dir_name = repo_clone_dir
1843
+ print("📂 No valid repository, using current directory")
1844
+
1845
+ # Show final directory status
1846
+ print("📂 Final directory status:")
1847
+ run_command("pwd && ls -la", show_output=True)
1848
+
1849
+ except Exception as e:
1850
+ print(f"❌ Error during repository cloning: {e}")
1851
+ print(f"❌ Exception type: {type(e).__name__}")
1852
+ print("⚠️ Continuing without repository...")
1853
+ repo_dir_name = repo_clone_dir
1854
+ run_command("pwd && ls -la", show_output=True)
1855
+ else:
1856
+ repo_dir_name = repo_clone_dir
1857
+ print("📂 No repository URL provided, using current directory")
1858
+ run_command("pwd && ls -la", show_output=True)
1859
+
1860
+ # Run setup commands if provided - now we're already in the repository directory
1861
+ if setup_commands:
1862
+ print("⚙️ Running user setup commands in Modal container...")
1863
+
1864
+ # Check if git clone is already in the setup commands
1865
+ has_git_clone = any('git clone' in cmd for cmd in setup_commands)
1866
+
1867
+ # Only add git clone if:
1868
+ # 1. No git clone in setup commands AND
1869
+ # 2. We have a repo URL AND
1870
+ # 3. Repository was NOT already cloned successfully
1871
+ if not has_git_clone and repo_url and not repo_exists:
1872
+ print("📥 Git clone not found in setup commands and repository not yet cloned, adding it...")
1873
+ clone_cmd = f"git clone {repo_url}"
1874
+ setup_commands = [clone_cmd] + setup_commands
1875
+ print(f"📥 Added git clone command: {clone_cmd}")
1876
+ elif has_git_clone and repo_exists:
1877
+ print("⚠️ Repository already cloned successfully, removing duplicate git clone from setup commands...")
1878
+ # Remove git clone commands since repository is already cloned
1879
+ setup_commands = [cmd for cmd in setup_commands if 'git clone' not in cmd]
1880
+ print(f"📥 Removed duplicate git clone commands")
1881
+ elif repo_exists:
1882
+ print("📂 Repository already cloned successfully, skipping git clone in setup commands")
1883
+
1884
+ # Print all commands that will be executed
1885
+ print("📋 Setup commands to execute in container:")
1886
+ for i, cmd in enumerate(setup_commands, 1):
1887
+ print(f" {i}. {cmd}")
1888
+
1889
+ print(f"\n🚀 Executing commands in container directory: {repo_dir_name}")
1890
+
1891
+ # Ensure we start in the /root directory and reset current_dir
1892
+ current_dir = "/root"
1893
+ print(f"📂 Resetting working directory to: {current_dir}")
1894
+
1895
+ # Verify we can access /root directory
1896
+ verify_success, verify_output, _ = run_command("pwd", show_output=True)
1897
+ if verify_success:
1898
+ print(f"✅ Current directory verified: {verify_output.strip()}")
1899
+
1900
+ # Execute each command individually in the repository directory within the container
1901
+ for i, cmd in enumerate(setup_commands, 1):
1902
+ print(f"\n📋 Executing command {i}/{len(setup_commands)} in container: {cmd}")
1903
+
1904
+ # If this is a cd command, just run it directly
1905
+ if cmd.strip().startswith('cd '):
1906
+ # Execute the command directly (we're already in the right directory)
1907
+ success, stdout, stderr = run_command(cmd)
1908
+ continue
1909
+
1910
+ # For git clone commands, handle as before
1911
+ if 'git clone' in cmd:
1912
+ # Execute the command directly
1913
+ success, stdout, stderr = run_command(cmd)
1914
+
1915
+ if success:
1916
+ print(f"✅ Command executed successfully in container: {cmd}")
1917
+ if stdout.strip():
1918
+ print(f"📄 Output: {stdout.strip()}")
1919
+
1920
+ # Handle repository directory change as before
1921
+ print("📂 Git clone detected, attempting to change to repository directory...")
1922
+ # Extract repository name from the clone command
1923
+ parts = cmd.split()
1924
+ if len(parts) >= 3:
1925
+ clone_url = parts[2] # git clone <url>
1926
+ target_dir = clone_url.split('/')[-1].replace('.git', '')
1927
+
1928
+ # Check if we're already in the target directory
1929
+ if current_dir.endswith(f"/{target_dir}") or current_dir == f"/{target_dir}":
1930
+ print(f"📂 Already in target directory: {current_dir}")
1931
+ else:
1932
+ # The repository should now be at current_dir/target_dir
1933
+ repo_full_path = f"{current_dir.rstrip('/')}/{target_dir}"
1934
+
1935
+ # Check if directory exists using absolute path
1936
+ dir_check_success, _, _ = run_command(f"test -d '{repo_full_path}'", show_output=False)
1937
+ if dir_check_success:
1938
+ current_dir = repo_full_path
1939
+ print(f"📂 Successfully changed current directory to: {current_dir}")
1940
+ # Verify the change worked
1941
+ verify_success, verify_output, _ = run_command("pwd", show_output=True)
1942
+ if verify_success:
1943
+ print(f"✅ Directory change verified: {verify_output.strip()}")
1944
+ # List contents to confirm we're in the right place
1945
+ run_command("ls -la", show_output=True)
1946
+
1947
+ # Initialize git submodules if they exist
1948
+ print("📦 Checking for git submodules...")
1949
+ submodule_check_success, _, _ = run_command("test -f .gitmodules", show_output=False)
1950
+ if submodule_check_success:
1951
+ print("📦 Git submodules found, initializing...")
1952
+ run_command("git submodule update --init --recursive", show_output=True)
1953
+ print("✅ Git submodules initialized")
1954
+ else:
1955
+ print("📦 No git submodules found")
1956
+ else:
1957
+ print("⚠️ Directory change verification failed")
1958
+ else:
1959
+ print(f"⚠️ Repository directory {repo_full_path} not found after clone")
1960
+ print("🔍 Checking what was actually created:")
1961
+ run_command("find . -maxdepth 2 -name '*.git' -type d", show_output=True)
1962
+ run_command("ls -la", show_output=True)
1963
+ else:
1964
+ # For Python commands, make sure we're in the correct directory first
1965
+ if cmd.startswith('python '):
1966
+ # Fix the directory path issue - ensure we're in the correct repository directory
1967
+ # Check if we're in a nested directory that matches the repo name
1968
+ repo_dir_parts = current_dir.split('/')
1969
+ if len(repo_dir_parts) >= 2 and repo_dir_parts[-1] == repo_dir_parts[-2]:
1970
+ # We're in a nested directory like /root/nanoGPT/nanoGPT
1971
+ # Move up one level to /root/nanoGPT
1972
+ print(f"⚠️ Detected nested directory structure: {current_dir}")
1973
+ parent_dir = '/'.join(repo_dir_parts[:-1])
1974
+ print(f"🔄 Moving to parent directory: {parent_dir}")
1975
+ cd_success, _, _ = run_command(f"cd {parent_dir}", show_output=False)
1976
+ if cd_success:
1977
+ current_dir = parent_dir
1978
+ print(f"📂 Updated current directory to: {current_dir}")
1979
+
1980
+ # Execute the command directly (we're already in the right directory)
1981
+ success, stdout, stderr = run_command(cmd)
1982
+
1983
+ if success:
1984
+ print(f"✅ Command executed successfully in container: {cmd}")
1985
+ if stdout.strip():
1986
+ print(f"📄 Output: {stdout.strip()}")
1987
+ else:
1988
+ print(f"❌ Command failed in container: {cmd}")
1989
+ print(f"❌ Error: {stderr}")
1990
+ # Continue with next command even if this one failed
1991
+
1992
+ # Show final status of the repository directory in container
1993
+ print(f"\n📂 Final directory contents in container ({repo_dir_name}):")
1994
+ run_command("pwd && ls -la")
1995
+
1996
+ else:
1997
+ print("⚠️ No setup commands provided.")
1998
+
1999
+ # If no setup commands but we have a repo URL, at least try to clone it
2000
+ if repo_url and not repo_exists:
2001
+ print("📥 No setup commands provided, but cloning repository anyway...")
2002
+ clone_success, _, _ = run_command(f"git clone {repo_url}", show_output=True)
2003
+ if clone_success:
2004
+ print(f"✅ Repository cloned successfully")
2005
+ # Try to change to the repository directory
2006
+ if repo_name_from_url:
2007
+ run_command(f"cd {repo_name_from_url}")
2008
+ print("📂 Final directory status after clone:")
2009
+ run_command("pwd && ls -la", show_output=True)
2010
+
2011
+ # Write container ID to file for future reference
2012
+ with open(os.path.expanduser("~/.modal_last_container_id"), "w") as f:
2013
+ f.write(container_id)
2014
+
2015
+ # Print connection instructions
2016
+ print(f"✅ Modal sandbox created successfully!")
2017
+ print(f"📋 Sandbox ID: {sandbox_id}")
2018
+ print(f"📋 Container ID: {container_id}")
2019
+ if volume:
2020
+ print(f"📦 Volume: {volume_name} (mounted at {volume_mount_path})")
2021
+ print(f"💾 Persistent storage available for pip and uv caches")
2022
+ print(f"📂 Repositories will be cloned in home directory (/root) for faster access")
2023
+ print("🔗 To connect to this container, run:")
2024
+ print(f"modal container exec --pty {container_id} bash")
2025
+ print("⏳ Sandbox will remain running until you terminate it with:")
2026
+ print(f"modal sandbox terminate {sandbox_id}")
2027
+
2028
+ # Try to open a new terminal window and connect to the container
2029
+ if container_id:
2030
+ print("🖥️ Attempting to open new terminal window...")
2031
+ # Use osascript to open a new terminal with the modal shell command
2032
+ terminal_script = f'''
2033
+ tell application "Terminal"
2034
+ do script "modal shell {container_id}"
2035
+ activate
2036
+ end tell
2037
+ '''
2038
+
2039
+ try:
2040
+ result = subprocess.run(['osascript', '-e', terminal_script],
2041
+ capture_output=True, text=True, timeout=30)
2042
+ if result.returncode == 0:
2043
+ print("✅ New terminal window opened successfully")
2044
+ else:
2045
+ print(f"⚠️ Failed to open terminal window: {result.stderr}")
2046
+
2047
+ # Try alternative approach with iTerm2 if Terminal failed
2048
+ print("🔄 Trying with iTerm2 instead...")
2049
+ iterm_script = f'''
2050
+ tell application "iTerm"
2051
+ create window with default profile
2052
+ tell current session of current window
2053
+ write text "modal shell {container_id}"
2054
+ end tell
2055
+ end tell
2056
+ '''
2057
+
2058
+ try:
2059
+ iterm_result = subprocess.run(['osascript', '-e', iterm_script],
2060
+ capture_output=True, text=True, timeout=30)
2061
+ if iterm_result.returncode == 0:
2062
+ print("✅ New iTerm2 window opened successfully")
2063
+ else:
2064
+ print(f"⚠️ Failed to open iTerm2 window: {iterm_result.stderr}")
2065
+ print("📝 You can manually connect using:")
2066
+ print(f" modal shell {container_id}")
2067
+ except Exception as e:
2068
+ print(f"⚠️ Error opening iTerm2: {e}")
2069
+ print("📝 You can manually connect using:")
2070
+ print(f" modal shell {container_id}")
2071
+ except subprocess.TimeoutExpired:
2072
+ print("⚠️ Terminal opening timed out")
2073
+ except Exception as e:
2074
+ print(f"⚠️ Error opening terminal: {e}")
2075
+ print("📝 You can manually connect using:")
2076
+ print(f" modal shell {container_id}")
2077
+
2078
+ # Also provide manual connection instructions
2079
+ print("\n" + "="*60)
2080
+ print("🚀 SANDBOX READY!")
2081
+ print("="*60)
2082
+ print(f"📋 Sandbox ID: {sandbox_id}")
2083
+ print(f"🆔 Container ID: {container_id}")
2084
+ if volume:
2085
+ print(f"💾 Volume: {volume_name} mounted at {volume_mount_path}")
2086
+ print("📁 Persistent storage available for caches and repositories")
2087
+ print("\n🔗 To connect to your container, run:")
2088
+ print(f" modal shell {container_id}")
2089
+ print("="*60)
2090
+ else:
2091
+ print("❌ No container ID available for connection")
2092
+
2093
+ return {
2094
+ "run_command": run_command,
2095
+ "current_dir": current_dir,
2096
+ "execution_history": execution_history,
2097
+ "container_id": container_id,
2098
+ "sandbox_id": sandbox_id
2099
+ }
2100
+
940
2101
 
941
2102
  def handle_interactive_input(prompt, is_password=False):
942
2103
  """Handle interactive input from the user with optional password masking"""
@@ -980,7 +2141,7 @@ ssh_app = modal.App("ssh-container-app")
980
2141
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
981
2142
  "gpg", "ca-certificates", "software-properties-common"
982
2143
  )
983
- .pip_install("uv", "modal", "requests", "openai") # Fast Python package installer and Modal
2144
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
984
2145
  .run_commands(
985
2146
  # Create SSH directory
986
2147
  "mkdir -p /var/run/sshd",
@@ -1011,7 +2172,7 @@ ssh_app = modal.App("ssh-container-app")
1011
2172
  memory=8192,
1012
2173
  serialized=True,
1013
2174
  )
1014
- def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_commands=None, openai_api_key=None):
2175
+ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_commands=None):
1015
2176
  import subprocess
1016
2177
  import time
1017
2178
  import os
@@ -1025,13 +2186,6 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
1025
2186
  # Setup environment
1026
2187
  os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
1027
2188
 
1028
- # Set OpenAI API key if provided
1029
- if openai_api_key:
1030
- os.environ['OPENAI_API_KEY'] = openai_api_key
1031
- print(f"✅ Set OpenAI API key in container environment (length: {len(openai_api_key)})")
1032
- else:
1033
- print("⚠️ No OpenAI API key provided to container")
1034
-
1035
2189
  # Clone repository if provided
1036
2190
  if repo_url:
1037
2191
  repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
@@ -1053,118 +2207,17 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
1053
2207
  # Run setup commands if provided
1054
2208
  if setup_commands:
1055
2209
  print(f"⚙️ Running {len(setup_commands)} setup commands...")
1056
-
1057
- # First, let's check the current directory structure
1058
- print("🔍 Checking current directory structure before running setup commands...")
1059
- try:
1060
- result = subprocess.run("pwd && ls -la", shell=True, check=True,
1061
- capture_output=True, text=True)
1062
- print(f"📂 Current directory: {result.stdout}")
1063
- except subprocess.CalledProcessError as e:
1064
- print(f"⚠️ Could not check directory structure: {e}")
1065
-
1066
- # Define a simple run_command function for SSH container
1067
- def run_command_with_llm_debug(cmd, show_output=True, retry_count=0, max_retries=3):
1068
- """Execute a command with LLM debugging enabled"""
1069
- print(f"🔧 Executing: {cmd}")
1070
- try:
1071
- # Handle special case for source command which doesn't work with subprocess.run
1072
- if cmd.strip().startswith("source ") or " source " in cmd:
1073
- print("⚠️ Detected 'source' command which doesn't work with subprocess.run")
1074
- print("🔄 Converting to bash -c with dot (.) instead of source")
1075
- # Replace source with . (dot) which is the same as source but works in sh
1076
- modified_cmd = cmd.replace("source ", ". ")
1077
- # Wrap in bash -c to ensure it runs in bash
1078
- bash_cmd = f"bash -c '{modified_cmd}'"
1079
- print(f"🔄 Modified command: {bash_cmd}")
1080
- result = subprocess.run(bash_cmd, shell=True, check=True,
1081
- capture_output=True, text=True)
1082
- else:
1083
- result = subprocess.run(cmd, shell=True, check=True,
1084
- capture_output=True, text=True)
1085
-
1086
- if result.stdout and show_output:
1087
- print(f"✅ Output: {result.stdout}")
1088
- return True, result.stdout, ""
1089
- except subprocess.CalledProcessError as e:
1090
- error_output = e.stderr if e.stderr else str(e)
1091
- print(f"❌ Command failed: {e}")
1092
- print(f"❌ Error: {error_output}")
1093
-
1094
- # Call OpenAI for debugging
1095
- print("🔍 Attempting to debug the failed command with OpenAI...")
1096
- try:
1097
- # Get the current directory for context
1098
- current_dir = os.getcwd()
1099
-
1100
- # Call OpenAI for debugging
1101
- print(f"🔍 DEBUG: About to call call_openai_for_debug...")
1102
- print(f"🔍 DEBUG: Command: {cmd}")
1103
- print(f"🔍 DEBUG: Error output length: {len(error_output)}")
1104
- print(f"🔍 DEBUG: Current directory: {current_dir}")
1105
-
1106
- # Get the API key from environment or use the one that was fetched earlier
1107
- api_key = os.environ.get("OPENAI_API_KEY")
1108
- fix_command = call_openai_for_debug(cmd, error_output, api_key=api_key, current_dir=current_dir)
1109
-
1110
- print(f"🔍 DEBUG: call_openai_for_debug returned: {fix_command}")
1111
-
1112
- if fix_command:
1113
- print(f"🔧 OpenAI suggested fix command: {fix_command}")
1114
-
1115
- # Run the fix command
1116
- print(f"🔄 Running suggested fix command: {fix_command}")
1117
- try:
1118
- fix_result = subprocess.run(fix_command, shell=True, check=True,
1119
- capture_output=True, text=True)
1120
- if fix_result.stdout:
1121
- print(f"✅ Fix command output: {fix_result.stdout}")
1122
-
1123
- # Retry the original command
1124
- print(f"🔄 Retrying original command: {cmd}")
1125
- return run_command_with_llm_debug(cmd, show_output, retry_count + 1, max_retries)
1126
- except subprocess.CalledProcessError as fix_e:
1127
- print(f"❌ Fix command also failed: {fix_e}")
1128
- return False, "", error_output
1129
- else:
1130
- print("❌ No fix suggested by OpenAI")
1131
- return False, "", error_output
1132
-
1133
- except Exception as debug_e:
1134
- print(f"❌ LLM debugging failed: {debug_e}")
1135
- return False, "", error_output
1136
-
1137
2210
  for i, cmd in enumerate(setup_commands, 1):
1138
2211
  print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
1139
-
1140
- # Check if this is a cd command and if the directory exists
1141
- if cmd.strip().startswith("cd "):
1142
- cd_parts = cmd.split(None, 1)
1143
- if len(cd_parts) >= 2:
1144
- target_dir = cd_parts[1].strip('"\'')
1145
- print(f"🔍 Checking if directory exists: {target_dir}")
1146
- try:
1147
- check_result = subprocess.run(f"test -d '{target_dir}'", shell=True,
1148
- capture_output=True, text=True)
1149
- if check_result.returncode != 0:
1150
- print(f"⚠️ Directory does not exist: {target_dir}")
1151
- print(f"🔍 Current directory contents:")
1152
- subprocess.run("pwd && ls -la", shell=True, check=False)
1153
-
1154
- # Try to find similar directories
1155
- print(f"🔍 Looking for similar directories...")
1156
- subprocess.run("find . -type d -name '*llama*' -o -name '*nano*' 2>/dev/null | head -10", shell=True, check=False)
1157
- except Exception as e:
1158
- print(f"⚠️ Could not check directory: {e}")
1159
-
1160
- success, stdout, stderr = run_command_with_llm_debug(cmd, show_output=True)
1161
- if not success:
1162
- print(f"⚠️ Command {i} failed, but continuing with remaining commands...")
1163
-
1164
- # If this was a cd command that failed, try to understand the directory structure
1165
- if cmd.strip().startswith("cd ") and "No such file or directory" in stderr:
1166
- print(f"🔍 Analyzing directory structure after failed cd command...")
1167
- subprocess.run("pwd && ls -la && echo '--- Parent directory ---' && ls -la ..", shell=True, check=False)
2212
+ try:
2213
+ result = subprocess.run(cmd, shell=True, check=True,
2214
+ capture_output=True, text=True)
2215
+ if result.stdout:
2216
+ print(f"✅ Output: {result.stdout}")
2217
+ except subprocess.CalledProcessError as e:
2218
+ print(f" Command failed: {e}")
2219
+ if e.stderr:
2220
+ print(f" Error: {e.stderr}")
1168
2221
 
1169
2222
  # Get container info
1170
2223
  print("🔍 Container started successfully!")
@@ -1182,56 +2235,23 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
1182
2235
  subprocess.run(["service", "ssh", "start"], check=True)
1183
2236
 
1184
2237
  # Now modify the create_modal_ssh_container function to use the standalone ssh_container_function
2238
+
1185
2239
  def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
1186
- volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False):
2240
+ volume_name=None, timeout_minutes=60, ssh_password=None):
1187
2241
  """Create a Modal SSH container with GPU support and tunneling"""
1188
2242
 
1189
- # Use interactive mode if specified
1190
- if interactive:
1191
- # If GPU type is not specified, prompt for it
1192
- if not gpu_type:
1193
- gpu_type = prompt_for_gpu()
1194
-
1195
- # If repo URL is not specified, prompt for it
1196
- if not repo_url:
1197
- try:
1198
- repo_url = input("? Enter GitHub repository URL: ").strip()
1199
- if not repo_url:
1200
- print("❌ Repository URL is required.")
1201
- return None
1202
- except KeyboardInterrupt:
1203
- print("\n🛑 Setup cancelled.")
1204
- return None
1205
-
1206
- # If volume name is not specified, ask about persistent volume
1207
- if not volume_name:
1208
- try:
1209
- use_volume = input("? Use persistent volume for faster installs? (Y/n): ").strip().lower()
1210
- if use_volume in ('', 'y', 'yes'):
1211
- volume_name = input("? Enter volume name: ").strip()
1212
- if not volume_name:
1213
- volume_name = "gitarsenal-volume"
1214
- print(f"Using default volume name: {volume_name}")
1215
- except KeyboardInterrupt:
1216
- print("\n🛑 Setup cancelled.")
1217
- return None
1218
-
1219
2243
  # Check if Modal is authenticated
1220
2244
  try:
1221
2245
  # Print all environment variables for debugging
1222
2246
  print("🔍 DEBUG: Checking environment variables")
1223
2247
  modal_token_id = os.environ.get("MODAL_TOKEN_ID")
1224
2248
  modal_token = os.environ.get("MODAL_TOKEN")
1225
- openai_api_key = os.environ.get("OPENAI_API_KEY")
1226
- print(f"🔍 token exists: {'Yes' if modal_token_id else 'No'}")
1227
- print(f"🔍 token exists: {'Yes' if modal_token else 'No'}")
1228
- print(f"🔍 openai_api_key exists: {'Yes' if openai_api_key else 'No'}")
2249
+ print(f"🔍 MODAL_TOKEN_ID exists: {'Yes' if modal_token_id else 'No'}")
2250
+ print(f"🔍 MODAL_TOKEN exists: {'Yes' if modal_token else 'No'}")
1229
2251
  if modal_token_id:
1230
- print(f"🔍 token length: {len(modal_token_id)}")
2252
+ print(f"🔍 MODAL_TOKEN_ID length: {len(modal_token_id)}")
1231
2253
  if modal_token:
1232
- print(f"🔍 token length: {len(modal_token)}")
1233
- if openai_api_key:
1234
- print(f"🔍 openai_api_key length: {len(openai_api_key)}")
2254
+ print(f"🔍 MODAL_TOKEN length: {len(modal_token)}")
1235
2255
 
1236
2256
  # Try to access Modal token to check authentication
1237
2257
  try:
@@ -1242,13 +2262,13 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1242
2262
  # Try to get from MODAL_TOKEN
1243
2263
  modal_token = os.environ.get("MODAL_TOKEN")
1244
2264
  if modal_token:
1245
- print("✅ Found token in environment variable")
2265
+ print("✅ Found token in MODAL_TOKEN environment variable")
1246
2266
  os.environ["MODAL_TOKEN_ID"] = modal_token
1247
2267
  modal_token_id = modal_token
1248
- print(f"✅ Set token (length: {len(modal_token)})")
2268
+ print(f"✅ Set MODAL_TOKEN_ID from MODAL_TOKEN (length: {len(modal_token)})")
1249
2269
 
1250
2270
  if modal_token_id:
1251
- print(f"✅ token found (length: {len(modal_token_id)})")
2271
+ print(f"✅ Modal token found (length: {len(modal_token_id)})")
1252
2272
 
1253
2273
  # Use the comprehensive fix_modal_token script
1254
2274
  try:
@@ -1269,11 +2289,11 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1269
2289
  if result.stderr:
1270
2290
  print(f"Error: {result.stderr}")
1271
2291
 
1272
- print(f"✅ token setup completed")
2292
+ print(f"✅ Modal token setup completed")
1273
2293
  except Exception as e:
1274
2294
  print(f"⚠️ Error running fix_modal_token.py: {e}")
1275
2295
  else:
1276
- print("❌ No token found in environment variables")
2296
+ print("❌ No Modal token found in environment variables")
1277
2297
  # Try to get from file as a last resort
1278
2298
  try:
1279
2299
  home_dir = os.path.expanduser("~")
@@ -1292,7 +2312,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1292
2312
  else:
1293
2313
  print("❌ Token file does not contain token_id")
1294
2314
  else:
1295
- print("❌ token file not found")
2315
+ print("❌ Modal token file not found")
1296
2316
  except Exception as e:
1297
2317
  print(f"❌ Error loading token from file: {e}")
1298
2318
 
@@ -1306,9 +2326,9 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1306
2326
  modal_token_id = os.environ.get("MODAL_TOKEN_ID")
1307
2327
  modal_token = os.environ.get("MODAL_TOKEN")
1308
2328
  if modal_token_id:
1309
- print(f"🔄 Using token from environment (length: {len(modal_token_id)})")
2329
+ print(f"🔄 Using MODAL_TOKEN_ID from environment (length: {len(modal_token_id)})")
1310
2330
  elif modal_token:
1311
- print(f"🔄 Using token from environment (length: {len(modal_token)})")
2331
+ print(f"🔄 Using MODAL_TOKEN from environment (length: {len(modal_token)})")
1312
2332
  os.environ["MODAL_TOKEN_ID"] = modal_token
1313
2333
  modal_token_id = modal_token
1314
2334
  else:
@@ -1318,7 +2338,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1318
2338
  # Set it in both environment variables
1319
2339
  os.environ["MODAL_TOKEN_ID"] = modal_token_id
1320
2340
  os.environ["MODAL_TOKEN"] = modal_token_id
1321
- print("✅ Set both token and id environment variables")
2341
+ print("✅ Set both MODAL_TOKEN_ID and MODAL_TOKEN environment variables")
1322
2342
  except Exception as e:
1323
2343
  print(f"⚠️ Error checking Modal authentication: {e}")
1324
2344
  print("Continuing anyway, but Modal operations may fail")
@@ -1344,7 +2364,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1344
2364
  gpu_type = 'A10G'
1345
2365
 
1346
2366
  gpu_spec = gpu_configs[gpu_type]
1347
- print(f"🚀 Creating SSH container with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
2367
+ print(f"🚀 Creating Modal SSH container with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
1348
2368
 
1349
2369
  # Generate or use provided SSH password
1350
2370
  if not ssh_password:
@@ -1380,16 +2400,16 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1380
2400
  # Print debug info for authentication
1381
2401
  print("🔍 Modal authentication debug info:")
1382
2402
  modal_token = os.environ.get("MODAL_TOKEN_ID")
1383
- print(f" - token in env: {'Yes' if modal_token else 'No'}")
2403
+ print(f" - MODAL_TOKEN_ID in env: {'Yes' if modal_token else 'No'}")
1384
2404
  print(f" - Token length: {len(modal_token) if modal_token else 'N/A'}")
1385
2405
 
1386
2406
  # Verify we can create a Modal app
1387
2407
  try:
1388
- print("🔍 Testing app creation...")
2408
+ print("🔍 Testing Modal app creation...")
1389
2409
  app = modal.App(app_name)
1390
- print("✅ Created app successfully")
2410
+ print("✅ Created Modal app successfully")
1391
2411
  except Exception as e:
1392
- print(f"❌ Error creating app: {e}")
2412
+ print(f"❌ Error creating Modal app: {e}")
1393
2413
  return None
1394
2414
 
1395
2415
  # Create SSH-enabled image
@@ -1402,7 +2422,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1402
2422
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
1403
2423
  "gpg", "ca-certificates", "software-properties-common"
1404
2424
  )
1405
- .pip_install("uv", "modal", "requests", "openai") # Fast Python package installer and Modal
2425
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
1406
2426
  .run_commands(
1407
2427
  # Create SSH directory
1408
2428
  "mkdir -p /var/run/sshd",
@@ -1445,7 +2465,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1445
2465
  serialized=True,
1446
2466
  volumes=volumes_config if volumes_config else None,
1447
2467
  )
1448
- def ssh_container_function(ssh_password=None, repo_url=None, repo_name=None, setup_commands=None, openai_api_key=None):
2468
+ def ssh_container_function():
1449
2469
  """Start SSH container with password authentication and optional setup."""
1450
2470
  import subprocess
1451
2471
  import time
@@ -1454,13 +2474,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1454
2474
  # Set root password
1455
2475
  subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
1456
2476
 
1457
- # Set OpenAI API key if provided
1458
- if openai_api_key:
1459
- os.environ['OPENAI_API_KEY'] = openai_api_key
1460
- print(f"✅ Set OpenAI API key in container environment (length: {len(openai_api_key)})")
1461
- else:
1462
- print("⚠️ No OpenAI API key provided to container")
1463
-
1464
2477
  # Start SSH service
1465
2478
  subprocess.run(["service", "ssh", "start"], check=True)
1466
2479
 
@@ -1485,110 +2498,17 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1485
2498
  # Run setup commands if provided
1486
2499
  if setup_commands:
1487
2500
  print(f"⚙️ Running {len(setup_commands)} setup commands...")
1488
-
1489
- # Define a helper function for running commands with LLM debugging
1490
- def run_command_with_basic_error_handling(cmd, show_output=True, retry_count=0, max_retries=2):
1491
- """Execute a command with LLM debugging enabled"""
1492
- print(f"🔧 Executing: {cmd}")
2501
+ for i, cmd in enumerate(setup_commands, 1):
2502
+ print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
1493
2503
  try:
1494
- # Handle special case for source command which doesn't work with subprocess.run
1495
- if cmd.strip().startswith("source ") or " source " in cmd:
1496
- print("⚠️ Detected 'source' command which doesn't work with subprocess.run")
1497
- print("🔄 Converting to bash -c with dot (.) instead of source")
1498
- # Replace source with . (dot) which is the same as source but works in sh
1499
- modified_cmd = cmd.replace("source ", ". ")
1500
- # Wrap in bash -c to ensure it runs in bash
1501
- bash_cmd = f"bash -c '{modified_cmd}'"
1502
- print(f"🔄 Modified command: {bash_cmd}")
1503
- result = subprocess.run(bash_cmd, shell=True, check=True,
1504
- capture_output=True, text=True)
1505
- else:
1506
- result = subprocess.run(cmd, shell=True, check=True,
1507
- capture_output=True, text=True)
1508
-
1509
- if result.stdout and show_output:
2504
+ result = subprocess.run(cmd, shell=True, check=True,
2505
+ capture_output=True, text=True)
2506
+ if result.stdout:
1510
2507
  print(f"✅ Output: {result.stdout}")
1511
- return True, result.stdout, ""
1512
2508
  except subprocess.CalledProcessError as e:
1513
- error_output = e.stderr if e.stderr else str(e)
1514
2509
  print(f"❌ Command failed: {e}")
1515
- print(f"❌ Error: {error_output}")
1516
-
1517
- # Call OpenAI for debugging
1518
- print("🔍 Attempting to debug the failed command with OpenAI...")
1519
- try:
1520
- # Get the current directory for context
1521
- current_dir = os.getcwd()
1522
-
1523
- # Call OpenAI for debugging
1524
- print(f"🔍 DEBUG: About to call call_openai_for_debug...")
1525
- print(f"🔍 DEBUG: Command: {cmd}")
1526
- print(f"🔍 DEBUG: Error output length: {len(error_output)}")
1527
- print(f"🔍 DEBUG: Current directory: {current_dir}")
1528
-
1529
- # Get the API key from environment or use the one that was fetched earlier
1530
- api_key = os.environ.get("OPENAI_API_KEY")
1531
- fix_command = call_openai_for_debug(cmd, error_output, api_key=api_key, current_dir=current_dir)
1532
-
1533
- print(f"🔍 DEBUG: call_openai_for_debug returned: {fix_command}")
1534
-
1535
- if fix_command:
1536
- print(f"🔧 OpenAI suggested fix command: {fix_command}")
1537
-
1538
- # Run the fix command
1539
- print(f"🔄 Running suggested fix command: {fix_command}")
1540
- try:
1541
- fix_result = subprocess.run(fix_command, shell=True, check=True,
1542
- capture_output=True, text=True)
1543
- if fix_result.stdout:
1544
- print(f"✅ Fix command output: {fix_result.stdout}")
1545
-
1546
- # Retry the original command
1547
- print(f"🔄 Retrying original command: {cmd}")
1548
- return run_command_with_basic_error_handling(cmd, show_output, retry_count + 1, max_retries)
1549
- except subprocess.CalledProcessError as fix_e:
1550
- print(f"❌ Fix command also failed: {fix_e}")
1551
- return False, "", error_output
1552
- else:
1553
- print("❌ No fix suggested by OpenAI")
1554
- return False, "", error_output
1555
-
1556
- except Exception as debug_e:
1557
- print(f"❌ LLM debugging failed: {debug_e}")
1558
- return False, "", error_output
1559
-
1560
- # Run each setup command
1561
- for i, cmd in enumerate(setup_commands, 1):
1562
- print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
1563
-
1564
- # Check if this is a cd command and if the directory exists
1565
- if cmd.strip().startswith("cd "):
1566
- cd_parts = cmd.split(None, 1)
1567
- if len(cd_parts) >= 2:
1568
- target_dir = cd_parts[1].strip('"\'')
1569
- print(f"🔍 Checking if directory exists: {target_dir}")
1570
- try:
1571
- check_result = subprocess.run(f"test -d '{target_dir}'", shell=True,
1572
- capture_output=True, text=True)
1573
- if check_result.returncode != 0:
1574
- print(f"⚠️ Directory does not exist: {target_dir}")
1575
- print(f"🔍 Current directory contents:")
1576
- subprocess.run("pwd && ls -la", shell=True, check=False)
1577
-
1578
- # Try to find similar directories
1579
- print(f"🔍 Looking for similar directories...")
1580
- subprocess.run("find . -type d -name '*llama*' -o -name '*nano*' 2>/dev/null | head -10", shell=True, check=False)
1581
- except Exception as e:
1582
- print(f"⚠️ Could not check directory: {e}")
1583
-
1584
- success, stdout, stderr = run_command_with_basic_error_handling(cmd, show_output=True)
1585
- if not success:
1586
- print(f"⚠️ Command {i} failed, but continuing with remaining commands...")
1587
-
1588
- # If this was a cd command that failed, try to understand the directory structure
1589
- if cmd.strip().startswith("cd ") and "No such file or directory" in stderr:
1590
- print(f"🔍 Analyzing directory structure after failed cd command...")
1591
- subprocess.run("pwd && ls -la && echo '--- Parent directory ---' && ls -la ..", shell=True, check=False)
2510
+ if e.stderr:
2511
+ print(f"❌ Error: {e.stderr}")
1592
2512
 
1593
2513
  # Create SSH tunnel
1594
2514
  with modal.forward(22, unencrypted=True) as tunnel:
@@ -1624,9 +2544,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1624
2544
  # Start the container in a new thread to avoid blocking
1625
2545
  with modal.enable_output():
1626
2546
  with app.run():
1627
- # Get the API key from environment
1628
- api_key = os.environ.get("OPENAI_API_KEY")
1629
- ssh_container_function.remote(ssh_password, repo_url, repo_name, setup_commands, api_key)
2547
+ ssh_container_function.remote()
1630
2548
 
1631
2549
  # Clean up Modal token after container is successfully created
1632
2550
  cleanup_modal_token()
@@ -1640,15 +2558,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1640
2558
  print(f"❌ Error running container: {e}")
1641
2559
  return None
1642
2560
 
1643
- def is_local_server_running(url, timeout=2):
1644
- """Check if a local server is running by attempting a connection."""
1645
- import requests
1646
- try:
1647
- response = requests.get(url, timeout=timeout)
1648
- return True
1649
- except requests.exceptions.RequestException:
1650
- return False
1651
-
1652
2561
  def fetch_setup_commands_from_api(repo_url):
1653
2562
  """Fetch setup commands from the GitIngest API using real repository analysis."""
1654
2563
  import tempfile
@@ -1656,13 +2565,8 @@ def fetch_setup_commands_from_api(repo_url):
1656
2565
  import os
1657
2566
  import shutil
1658
2567
  import json
1659
- import time
1660
- import requests
1661
2568
 
1662
- # Define API endpoints to try in order - using only online endpoints
1663
- api_endpoints = [
1664
- "https://www.gitarsenal.dev/api/analyze-with-gitingest" # Working endpoint with www prefix
1665
- ]
2569
+ api_url = "https://git-arsenal.vercel.app/api/analyze-with-gitingest"
1666
2570
 
1667
2571
  print(f"🔍 Fetching setup commands from API for repository: {repo_url}")
1668
2572
 
@@ -1684,13 +2588,6 @@ def fetch_setup_commands_from_api(repo_url):
1684
2588
  temp_dir = tempfile.mkdtemp(prefix="repo_analysis_")
1685
2589
  output_file = os.path.join(temp_dir, "digest.json")
1686
2590
 
1687
- # Create a directory to save GitIngest results
1688
- save_dir = os.path.join(os.path.expanduser("~"), "gitarsenal_results")
1689
- os.makedirs(save_dir, exist_ok=True)
1690
- timestamp = time.strftime("%Y%m%d_%H%M%S")
1691
- repo_name = repo_url.split("/")[-1].replace(".git", "")
1692
- save_file = os.path.join(save_dir, f"gitingest_{repo_name}_{timestamp}.txt")
1693
-
1694
2591
  try:
1695
2592
  if has_gitingest_cli:
1696
2593
  # Use gitingest CLI tool to analyze the repository directly from URL
@@ -1727,65 +2624,16 @@ def fetch_setup_commands_from_api(repo_url):
1727
2624
  try:
1728
2625
  with open(output_file, 'r', encoding='utf-8') as f:
1729
2626
  content = f.read()
1730
-
1731
- # Save the GitIngest output to the results directory
1732
- with open(save_file, 'w', encoding='utf-8') as save_f:
1733
- save_f.write(content)
1734
- print(f"📁 GitIngest output saved to: {save_file}")
1735
-
1736
2627
  try:
1737
2628
  gitingest_data = json.loads(content)
1738
2629
  print(f"✅ GitIngest data loaded as JSON from {output_file}")
1739
2630
  except json.JSONDecodeError:
1740
2631
  # If not JSON, convert the text output to a basic structure
1741
2632
  print(f"⚠️ GitIngest output is not in JSON format, converting text to structure")
1742
-
1743
- # Process the text to extract useful information
1744
- import re
1745
-
1746
- # Try to identify language
1747
- language_match = re.search(r"(?i)language[s]?:?\s*(\w+)", content)
1748
- detected_language = language_match.group(1) if language_match else "Unknown"
1749
-
1750
- # Try to identify technologies with stronger evidence requirements
1751
- tech_patterns = {
1752
- "python": r"(?i)(python|\.py\b|pip\b|requirements\.txt|setup\.py)",
1753
- "javascript": r"(?i)(javascript|\.js\b|node|npm|yarn|package\.json)",
1754
- "typescript": r"(?i)(typescript|\.ts\b|tsc\b|tsconfig\.json)",
1755
- "go": r"(?i)(\bgo\b|golang|\.go\b|go\.mod|go\.sum)",
1756
- "rust": r"(?i)(rust|\.rs\b|cargo|Cargo\.toml)",
1757
- "java": r"(?i)(java\b|\.java\b|maven|gradle|pom\.xml)",
1758
- "c++": r"(?i)(c\+\+|\.cpp\b|\.hpp\b|cmake\b|CMakeLists\.txt)",
1759
- "pytorch": r"(?i)(pytorch|torch\b|nn\.Module)",
1760
- "tensorflow": r"(?i)(tensorflow|tf\.|keras\b)",
1761
- }
1762
-
1763
- # Count occurrences to filter out false positives
1764
- tech_counts = {}
1765
- for tech, pattern in tech_patterns.items():
1766
- matches = re.findall(pattern, content)
1767
- if matches:
1768
- tech_counts[tech] = len(matches)
1769
-
1770
- # Filter technologies based on threshold
1771
- thresholds = {
1772
- "javascript": 3, # Higher threshold for JavaScript
1773
- "go": 3, # Higher threshold for Go
1774
- "default": 2 # Default threshold
1775
- }
1776
-
1777
- detected_technologies = []
1778
- for tech, count in tech_counts.items():
1779
- threshold = thresholds.get(tech, thresholds["default"])
1780
- if count >= threshold:
1781
- detected_technologies.append(tech)
1782
- print(f"📊 Detected {tech} with confidence score {count}")
1783
-
1784
- # Create a structured representation
1785
2633
  gitingest_data = {
1786
2634
  "system_info": {
1787
- "detected_language": detected_language,
1788
- "detected_technologies": detected_technologies,
2635
+ "detected_language": "Unknown",
2636
+ "detected_technologies": [],
1789
2637
  },
1790
2638
  "repository_analysis": {
1791
2639
  "summary": content[:5000], # First 5000 chars as summary
@@ -1793,12 +2641,6 @@ def fetch_setup_commands_from_api(repo_url):
1793
2641
  },
1794
2642
  "success": True
1795
2643
  }
1796
-
1797
- # Save the processed data
1798
- processed_file = os.path.join(save_dir, f"gitingest_processed_{repo_name}_{timestamp}.json")
1799
- with open(processed_file, 'w', encoding='utf-8') as proc_f:
1800
- json.dump(gitingest_data, proc_f, indent=2)
1801
- print(f"📁 Processed GitIngest data saved to: {processed_file}")
1802
2644
  except FileNotFoundError:
1803
2645
  print(f"⚠️ Output file not found at {output_file}")
1804
2646
  gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
@@ -1818,46 +2660,17 @@ def fetch_setup_commands_from_api(repo_url):
1818
2660
 
1819
2661
  print(f"📤 API Request payload prepared (GitIngest data size: {len(json.dumps(gitingest_data))} bytes)")
1820
2662
 
1821
- # Try each endpoint in sequence until one succeeds
1822
- response = None
1823
- for api_url in api_endpoints:
1824
- # Use the retry mechanism for more reliable requests
1825
- response = make_api_request_with_retry(
1826
- url=api_url,
1827
- payload=payload,
1828
- max_retries=2,
1829
- timeout=180 # 3 minute timeout
1830
- )
1831
-
1832
- # If we got a response and it's successful, break out of the loop
1833
- if response and response.status_code == 200:
1834
- print(f"✅ Successful response from {api_url}")
1835
- break
1836
-
1837
- if response:
1838
- print(f"⚠️ Endpoint {api_url} returned status code {response.status_code}, trying next endpoint...")
1839
- else:
1840
- print(f"⚠️ Failed to connect to {api_url}, trying next endpoint...")
1841
-
1842
- # If we've tried all endpoints and still don't have a response, use fallback
1843
- if response is None:
1844
- print("❌ All API endpoints failed")
1845
- return generate_fallback_commands(gitingest_data)
1846
-
1847
- # Continue with the response we got from the successful endpoint
1848
- if not response:
1849
- print("❌ No valid response received from any endpoint")
1850
- return generate_fallback_commands(gitingest_data)
1851
-
2663
+ # Make the API request
2664
+ print(f"🌐 Making POST request to: {api_url}")
1852
2665
  try:
2666
+ response = requests.post(api_url, json=payload, timeout=60)
2667
+
1853
2668
  print(f"📥 API Response status code: {response.status_code}")
1854
2669
 
1855
2670
  if response.status_code == 200:
1856
2671
  try:
1857
2672
  data = response.json()
1858
2673
  print(f"📄 API Response data received")
1859
- print(f"📄 Response size: {len(response.text)} bytes")
1860
- print(f"📄 Response URL: {response.url}")
1861
2674
 
1862
2675
  # Extract setup commands from the response
1863
2676
  if "setupInstructions" in data and "commands" in data["setupInstructions"]:
@@ -1909,13 +2722,16 @@ def fetch_setup_commands_from_api(repo_url):
1909
2722
  return generate_fallback_commands(gitingest_data)
1910
2723
  else:
1911
2724
  print(f"❌ API request failed with status code: {response.status_code}")
1912
- print(f" Response URL: {response.url}")
1913
- print(f"❌ Response headers: {dict(response.headers)}")
1914
- print(f"❌ Error response: {response.text[:500]}...")
2725
+ print(f"Error response: {response.text[:500]}...")
1915
2726
  # Return fallback commands
1916
2727
  return generate_fallback_commands(gitingest_data)
1917
- except Exception as e:
1918
- print(f"❌ Error processing API response: {str(e)}")
2728
+ except requests.exceptions.Timeout:
2729
+ print("❌ API request timed out after 60 seconds")
2730
+ print("⚠️ Using fallback commands instead")
2731
+ # Return fallback commands
2732
+ return generate_fallback_commands(gitingest_data)
2733
+ except requests.exceptions.ConnectionError:
2734
+ print(f"❌ Connection error: Could not connect to {api_url}")
1919
2735
  print("⚠️ Using fallback commands instead")
1920
2736
  # Return fallback commands
1921
2737
  return generate_fallback_commands(gitingest_data)
@@ -2207,11 +3023,8 @@ def generate_basic_repo_analysis(repo_dir):
2207
3023
  }
2208
3024
 
2209
3025
  def get_setup_commands_from_local_api(repo_url, gitingest_data):
2210
- """Try to get setup commands from the API."""
2211
- # Use only online endpoints
2212
- api_endpoints = [
2213
- "https://www.gitarsenal.dev/api/analyze-with-gitingest" # Working endpoint with www prefix
2214
- ]
3026
+ """Try to get setup commands from the local API."""
3027
+ api_url = "http://localhost:3000/api/analyze-with-gitingest"
2215
3028
 
2216
3029
  # Prepare the request payload
2217
3030
  payload = {
@@ -2220,49 +3033,34 @@ def get_setup_commands_from_local_api(repo_url, gitingest_data):
2220
3033
  "userRequest": "Setup and run the repository"
2221
3034
  }
2222
3035
 
2223
- # Try each API endpoint
2224
- for api_url in api_endpoints:
2225
- # Use the retry mechanism for more reliable requests
2226
- response = make_api_request_with_retry(
2227
- url=api_url,
2228
- payload=payload,
2229
- max_retries=2,
2230
- timeout=180 # 3 minute timeout
2231
- )
3036
+ try:
3037
+ # Make the API request
3038
+ print(f"🌐 Making POST request to local API: {api_url}")
3039
+ response = requests.post(api_url, json=payload, timeout=60)
2232
3040
 
2233
- if response and response.status_code == 200:
2234
- try:
2235
- data = response.json()
2236
- print(f"📄 Response size: {len(response.text)} bytes")
2237
- print(f"📄 Response URL: {response.url}")
2238
- if "setupInstructions" in data and "commands" in data["setupInstructions"]:
2239
- commands = data["setupInstructions"]["commands"]
2240
- print(f" Successfully fetched {len(commands)} setup commands from API at {api_url}")
2241
-
2242
- # Print the original commands
2243
- print("📋 Original commands from API:")
2244
- for i, cmd in enumerate(commands, 1):
2245
- print(f" {i}. {cmd}")
2246
-
2247
- # Fix the commands
2248
- fixed_commands = fix_setup_commands(commands)
2249
-
2250
- # Print the fixed commands
2251
- print("\n📋 Fixed commands:")
2252
- for i, cmd in enumerate(fixed_commands, 1):
2253
- print(f" {i}. {cmd}")
2254
-
2255
- return fixed_commands
2256
- else:
2257
- print("⚠️ API response did not contain setupInstructions.commands field")
2258
- except json.JSONDecodeError:
2259
- print(f"❌ Failed to parse API response as JSON")
2260
- elif response:
2261
- print(f"❌ API request failed with status code: {response.status_code}")
2262
- else:
2263
- print(f"❌ Failed to connect to {api_url}")
3041
+ if response.status_code == 200:
3042
+ data = response.json()
3043
+ if "setupInstructions" in data and "commands" in data["setupInstructions"]:
3044
+ commands = data["setupInstructions"]["commands"]
3045
+ print(f" Successfully fetched {len(commands)} setup commands from local API")
3046
+
3047
+ # Print the original commands
3048
+ print("📋 Original commands from local API:")
3049
+ for i, cmd in enumerate(commands, 1):
3050
+ print(f" {i}. {cmd}")
3051
+
3052
+ # Fix the commands
3053
+ fixed_commands = fix_setup_commands(commands)
3054
+
3055
+ # Print the fixed commands
3056
+ print("\n📋 Fixed commands:")
3057
+ for i, cmd in enumerate(fixed_commands, 1):
3058
+ print(f" {i}. {cmd}")
3059
+
3060
+ return fixed_commands
3061
+ except Exception as e:
3062
+ print(f"❌ Error connecting to local API: {e}")
2264
3063
 
2265
- print("❌ All API endpoints failed")
2266
3064
  return None
2267
3065
 
2268
3066
  # Define a function to create and return a properly configured ssh container function
@@ -2273,13 +3071,13 @@ def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=No
2273
3071
 
2274
3072
  # Create SSH-enabled image
2275
3073
  ssh_image = (
2276
- modal.Image.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.10")
3074
+ modal.Image.debian_slim()
2277
3075
  .apt_install(
2278
3076
  "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
2279
3077
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2280
3078
  "gpg", "ca-certificates", "software-properties-common"
2281
3079
  )
2282
- .pip_install("uv", "modal", "requests", "openai") # Fast Python package installer and Modal
3080
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
2283
3081
  .run_commands(
2284
3082
  # Create SSH directory
2285
3083
  "mkdir -p /var/run/sshd",
@@ -2318,7 +3116,7 @@ def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=No
2318
3116
  serialized=True,
2319
3117
  volumes=volumes if volumes else None,
2320
3118
  )
2321
- def ssh_container(ssh_password, repo_url=None, repo_name=None, setup_commands=None, openai_api_key=None):
3119
+ def ssh_container(ssh_password, repo_url=None, repo_name=None, setup_commands=None):
2322
3120
  import subprocess
2323
3121
  import time
2324
3122
  import os
@@ -2326,13 +3124,6 @@ def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=No
2326
3124
  # Set root password
2327
3125
  subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
2328
3126
 
2329
- # Set OpenAI API key if provided
2330
- if openai_api_key:
2331
- os.environ['OPENAI_API_KEY'] = openai_api_key
2332
- print(f"✅ Set OpenAI API key in container environment (length: {len(openai_api_key)})")
2333
- else:
2334
- print("⚠️ No OpenAI API key provided to container")
2335
-
2336
3127
  # Start SSH service
2337
3128
  subprocess.run(["service", "ssh", "start"], check=True)
2338
3129
 
@@ -2511,430 +3302,71 @@ Respond with only 'NAVIGATE' if navigation makes sense, or 'SKIP' if it's redund
2511
3302
  return None
2512
3303
 
2513
3304
  def cleanup_modal_token():
2514
- """Delete token files and environment variables after SSH container is started"""
2515
- print("🧹 Cleaning up tokens for security...")
3305
+ """Delete Modal token files and environment variables after SSH container is started"""
3306
+ print("🧹 Cleaning up Modal token for security...")
2516
3307
 
2517
3308
  try:
2518
3309
  # Remove token from environment variables
2519
3310
  if "MODAL_TOKEN_ID" in os.environ:
2520
3311
  del os.environ["MODAL_TOKEN_ID"]
2521
- # print("✅ Removed token ID from environment")
3312
+ print("✅ Removed MODAL_TOKEN_ID from environment")
2522
3313
 
2523
3314
  if "MODAL_TOKEN" in os.environ:
2524
3315
  del os.environ["MODAL_TOKEN"]
2525
- # print("✅ Removed token from environment")
3316
+ print("✅ Removed MODAL_TOKEN from environment")
2526
3317
 
2527
3318
  if "MODAL_TOKEN_SECRET" in os.environ:
2528
3319
  del os.environ["MODAL_TOKEN_SECRET"]
2529
- # print("✅ Removed token secret from environment")
3320
+ print("✅ Removed MODAL_TOKEN_SECRET from environment")
2530
3321
 
2531
3322
  # Delete ~/.modal.toml file
2532
3323
  home_dir = os.path.expanduser("~")
2533
3324
  modal_toml = os.path.join(home_dir, ".modal.toml")
2534
3325
  if os.path.exists(modal_toml):
2535
3326
  os.remove(modal_toml)
2536
- # print(f"✅ Deleted token file at {modal_toml}")
3327
+ print(f"✅ Deleted Modal token file at {modal_toml}")
2537
3328
 
2538
- # print("✅ Token cleanup completed successfully")
3329
+ print("✅ Modal token cleanup completed successfully")
2539
3330
  except Exception as e:
2540
- print(f"❌ Error during token cleanup: {e}")
3331
+ print(f"❌ Error during Modal token cleanup: {e}")
2541
3332
 
2542
3333
  def show_usage_examples():
2543
- """Display usage examples for the script."""
2544
- print("Usage Examples\n")
2545
-
2546
- print("Basic Container Creation")
2547
- print("┌────────────────────────────────────────────────────────────────────────┐")
2548
- print(" gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git ")
2549
- print("└────────────────────────────────────────────────────────────────────────┘\n")
2550
-
2551
- print("With Setup Commands")
2552
- print("┌────────────────────────────────────────────────────────────────────────────────────────────────────┐")
2553
- print(" gitarsenal --gpu A100 --repo-url https://github.com/username/repo.git \\ ")
2554
- print("--setup-commands \"pip install -r requirements.txt\" \"python setup.py install\" ")
2555
- print("└────────────────────────────────────────────────────────────────────────────────────────────────────┘\n")
2556
-
2557
- print("With Persistent Storage")
2558
- print("┌────────────────────────────────────────────────────────────────────────────────────┐")
2559
- print(" gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\ ")
2560
- print("--volume-name my-persistent-volume ")
2561
- print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
2562
-
2563
- print("With GitIngest API (default)")
2564
- print("┌────────────────────────────────────────────────────────────────────────────────────┐")
2565
- print(" gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git │")
2566
- print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
2567
-
2568
- print("Without GitIngest API")
2569
- print("┌────────────────────────────────────────────────────────────────────────────────────┐")
2570
- print("│ gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\ │")
2571
- print("│ --no-gitingest │")
2572
- print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
2573
-
2574
- print("With Original API")
2575
- print("┌────────────────────────────────────────────────────────────────────────────────────┐")
2576
- print("│ gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\ │")
2577
- print("│ --use-api │")
2578
- print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
2579
-
2580
- print("Available GPU Options:")
3334
+ """Display usage examples for the command-line interface."""
3335
+ print("\033[92mUsage Examples\033[0m")
3336
+ print("")
3337
+ print("\033[92mBasic Container Creation\033[0m")
3338
+ print("\033[90m┌────────────────────────────────────────────────────────────────────────┐\033[0m")
3339
+ print("\033[90m│\033[0m \033[92mgitarsenal --gpu A10G --repo-url https://github.com/username/repo.git\033[0m \033[90m│\033[0m")
3340
+ print("\033[90m└────────────────────────────────────────────────────────────────────────┘\033[0m")
3341
+ print("")
3342
+ print("\033[92mWith Setup Commands\033[0m")
3343
+ print("\033[90m┌────────────────────────────────────────────────────────────────────────────────────────────────────┐\033[0m")
3344
+ print("\033[90m│\033[0m \033[92mgitarsenal --gpu A100 --repo-url https://github.com/username/repo.git \\\033[0m \033[90m│\033[0m")
3345
+ print("\033[90m│\033[0m \033[92m --setup-commands \"pip install -r requirements.txt\" \"python setup.py install\"\033[0m \033[90m│\033[0m")
3346
+ print("\033[90m└────────────────────────────────────────────────────────────────────────────────────────────────────┘\033[0m")
3347
+ print("")
3348
+ print("\033[92mWith Persistent Storage\033[0m")
3349
+ print("\033[90m┌────────────────────────────────────────────────────────────────────────────────────┐\033[0m")
3350
+ print("\033[90m│\033[0m \033[92mgitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\\033[0m \033[90m│\033[0m")
3351
+ print("\033[90m│\033[0m \033[92m --volume-name my-persistent-volume\033[0m \033[90m│\033[0m")
3352
+ print("\033[90m└────────────────────────────────────────────────────────────────────────────────────┘\033[0m")
3353
+ print("")
3354
+ print("\033[92mInteractive Mode\033[0m")
3355
+ print("\033[90m┌────────────────────────────┐\033[0m")
3356
+ print("\033[90m│\033[0m \033[92mgitarsenal --interactive\033[0m \033[90m│\033[0m")
3357
+ print("\033[90m└────────────────────────────┘\033[0m")
3358
+ print("")
3359
+ print("\033[92mAvailable GPU Options:\033[0m")
2581
3360
  print(" T4, L4, A10G, A100-40GB, A100-80GB, L40S, H100, H200, B200")
3361
+ print("")
2582
3362
 
2583
- def make_api_request_with_retry(url, payload, max_retries=2, timeout=180):
2584
- """Make an API request with retry mechanism."""
2585
- import requests
2586
- import time
2587
-
2588
- for attempt in range(max_retries + 1):
2589
- try:
2590
- if attempt > 0:
2591
- print(f"🔄 Retry attempt {attempt}/{max_retries}...")
2592
-
2593
- print(f"🌐 Making POST request to: {url}")
2594
- print(f"⏳ Waiting up to {timeout//60} minutes for response...")
2595
-
2596
- # Set allow_redirects=True to follow redirects automatically
2597
- response = requests.post(
2598
- url,
2599
- json=payload,
2600
- timeout=timeout,
2601
- allow_redirects=True,
2602
- headers={
2603
- 'Content-Type': 'application/json',
2604
- 'User-Agent': 'GitArsenal-CLI/1.0'
2605
- }
2606
- )
2607
-
2608
- # Print redirect info if any
2609
- if response.history:
2610
- print(f"✅ Request was redirected {len(response.history)} times")
2611
- for resp in response.history:
2612
- print(f" - Redirect: {resp.status_code} from {resp.url}")
2613
- print(f"✅ Final URL: {response.url}")
2614
-
2615
- return response
2616
- except requests.exceptions.RequestException as e:
2617
- if attempt < max_retries:
2618
- retry_delay = 2 ** attempt # Exponential backoff
2619
- print(f"⚠️ Request failed: {str(e)}")
2620
- print(f"⏳ Waiting {retry_delay} seconds before retrying...")
2621
- time.sleep(retry_delay)
2622
- else:
2623
- print(f"❌ All retry attempts failed: {str(e)}")
2624
- return None
2625
-
2626
- return None
2627
-
2628
- def get_setup_commands_from_gitingest(repo_url):
2629
- """
2630
- Get repository setup commands using the gitingest approach.
2631
-
2632
- This function is inspired by gitingest_setup_client.py and provides a more
2633
- robust way to get setup commands for a repository.
2634
-
2635
- Args:
2636
- repo_url: URL of the repository to set up
2637
-
2638
- Returns:
2639
- List of setup commands or None if failed
2640
- """
2641
- import requests
2642
- import json
2643
- import os
2644
- import sys
2645
- import tempfile
2646
- import subprocess
2647
-
2648
- print(f"🔍 Getting setup commands for repository: {repo_url}")
2649
-
2650
- # Define API endpoints to try in order
2651
- api_endpoints = [
2652
- "https://www.gitarsenal.dev/api/gitingest-setup-commands",
2653
- "https://gitarsenal.dev/api/gitingest-setup-commands",
2654
- "https://www.gitarsenal.dev/api/analyze-with-gitingest",
2655
- "http://localhost:3000/api/gitingest-setup-commands"
2656
- ]
2657
-
2658
- # Generate basic gitingest data
2659
- def generate_basic_gitingest_data():
2660
- # Extract repo name from URL
2661
- repo_name = repo_url.split('/')[-1].replace('.git', '')
2662
-
2663
- return {
2664
- "system_info": {
2665
- "platform": "Unknown",
2666
- "python_version": "Unknown",
2667
- "detected_language": "Unknown",
2668
- "detected_technologies": [],
2669
- "file_count": 0,
2670
- "repo_stars": 0,
2671
- "repo_forks": 0,
2672
- "primary_package_manager": "Unknown",
2673
- "complexity_level": "Unknown"
2674
- },
2675
- "repository_analysis": {
2676
- "summary": f"Repository: {repo_name}",
2677
- "tree": "",
2678
- "content_preview": ""
2679
- },
2680
- "success": True
2681
- }
2682
-
2683
- # Try to generate gitingest data using CLI if available
2684
- def generate_gitingest_data_from_cli():
2685
- try:
2686
- # Check if gitingest CLI is available
2687
- subprocess.run(["gitingest", "--help"], check=True, capture_output=True, text=True)
2688
-
2689
- # Create a temporary file for the output
2690
- with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
2691
- output_file = tmp.name
2692
-
2693
- # Run gitingest command
2694
- print(f"Running gitingest analysis on {repo_url}...")
2695
- gitingest_cmd = ["gitingest", repo_url, "-o", output_file]
2696
- result = subprocess.run(gitingest_cmd, capture_output=True, text=True)
2697
-
2698
- if result.returncode != 0:
2699
- print(f"GitIngest CLI failed: {result.stderr}")
2700
- return None
2701
-
2702
- # Read the output file
2703
- try:
2704
- with open(output_file, 'r', encoding='utf-8') as f:
2705
- content = f.read()
2706
- try:
2707
- data = json.loads(content)
2708
- return data
2709
- except json.JSONDecodeError:
2710
- # If not JSON, convert the text output to a basic structure
2711
- return {
2712
- "system_info": {
2713
- "platform": "Unknown",
2714
- "python_version": "Unknown",
2715
- "detected_language": "Unknown",
2716
- "detected_technologies": []
2717
- },
2718
- "repository_analysis": {
2719
- "summary": content[:5000], # First 5000 chars as summary
2720
- "tree": "",
2721
- "content_preview": content[:10000] # First 10000 chars as preview
2722
- },
2723
- "success": True
2724
- }
2725
- except Exception as e:
2726
- print(f"Error reading gitingest output: {e}")
2727
- return None
2728
- finally:
2729
- # Clean up the temporary file
2730
- if os.path.exists(output_file):
2731
- os.unlink(output_file)
2732
-
2733
- except (subprocess.SubprocessError, FileNotFoundError):
2734
- print("GitIngest CLI not found")
2735
- return None
2736
-
2737
- # First try to get data from CLI
2738
- gitingest_data = generate_gitingest_data_from_cli()
2739
-
2740
- # If CLI failed, use basic data
2741
- if not gitingest_data:
2742
- print("Using basic gitingest data")
2743
- gitingest_data = generate_basic_gitingest_data()
2744
-
2745
- # Try each API endpoint
2746
- for api_url in api_endpoints:
2747
- try:
2748
- print(f"Trying API endpoint: {api_url}")
2749
-
2750
- payload = {
2751
- "repoUrl": repo_url,
2752
- "gitingestData": gitingest_data
2753
- }
2754
-
2755
- # Use the retry mechanism for more reliable requests
2756
- response = make_api_request_with_retry(
2757
- url=api_url,
2758
- payload=payload,
2759
- max_retries=2,
2760
- timeout=180 # 3 minute timeout
2761
- )
2762
-
2763
- if not response:
2764
- print(f"Failed to connect to {api_url}")
2765
- continue
2766
-
2767
- if response.status_code != 200:
2768
- print(f"API request failed with status code: {response.status_code}")
2769
- continue
2770
-
2771
- try:
2772
- result = response.json()
2773
-
2774
- # Check if we have commands in the response
2775
- commands = None
2776
-
2777
- # Check for different response formats
2778
- if "commands" in result:
2779
- commands = result["commands"]
2780
- elif "setupInstructions" in result and "commands" in result["setupInstructions"]:
2781
- commands = result["setupInstructions"]["commands"]
2782
-
2783
- if commands:
2784
- print(f"✅ Successfully fetched {len(commands)} setup commands from API at {api_url}")
2785
-
2786
- # Print the commands
2787
- print("\n📋 Setup Commands:")
2788
- for i, cmd in enumerate(commands, 1):
2789
- print(f" {i}. {cmd}")
2790
-
2791
- # Fix the commands
2792
- fixed_commands = fix_setup_commands(commands)
2793
-
2794
- # Print the fixed commands
2795
- print("\n📋 Fixed commands:")
2796
- for i, cmd in enumerate(fixed_commands, 1):
2797
- print(f" {i}. {cmd}")
2798
-
2799
- return fixed_commands
2800
- else:
2801
- print("No commands found in API response")
2802
- except json.JSONDecodeError:
2803
- print(f"Failed to parse API response as JSON")
2804
- except Exception as e:
2805
- print(f"Error with API endpoint {api_url}: {e}")
2806
-
2807
- print("❌ All API endpoints failed")
2808
- return generate_fallback_commands(gitingest_data)
2809
-
2810
- def prompt_for_gpu():
2811
- """
2812
- Prompt the user to select a GPU type from available options using arrow keys.
2813
- Returns the selected GPU type.
2814
- """
2815
- import sys
2816
- import tty
2817
- import termios
2818
-
2819
- # Define available GPU types and their specifications
2820
- gpu_specs = {
2821
- 'T4': {'gpu': 'T4', 'memory': '16GB'},
2822
- 'L4': {'gpu': 'L4', 'memory': '24GB'},
2823
- 'A10G': {'gpu': 'A10G', 'memory': '24GB'},
2824
- 'A100-40': {'gpu': 'A100-40GB', 'memory': '40GB'},
2825
- 'A100-80': {'gpu': 'A100-80GB', 'memory': '80GB'},
2826
- 'L40S': {'gpu': 'L40S', 'memory': '48GB'},
2827
- 'H100': {'gpu': 'H100', 'memory': '80GB'},
2828
- 'H200': {'gpu': 'H200', 'memory': '141GB'},
2829
- 'B200': {'gpu': 'B200', 'memory': '141GB'}
2830
- }
2831
-
2832
- # Create a list of options
2833
- options = list(gpu_specs.keys())
2834
- selected_index = 2 # Default to A10G (index 2)
2835
-
2836
- def get_key():
2837
- """Get a single keypress from the user."""
2838
- fd = sys.stdin.fileno()
2839
- old_settings = termios.tcgetattr(fd)
2840
- try:
2841
- tty.setraw(sys.stdin.fileno())
2842
- ch = sys.stdin.read(1)
2843
- if ch == '\x1b': # Escape sequence
2844
- ch2 = sys.stdin.read(1)
2845
- if ch2 == '[':
2846
- ch3 = sys.stdin.read(1)
2847
- if ch3 == 'A':
2848
- return 'UP'
2849
- elif ch3 == 'B':
2850
- return 'DOWN'
2851
- elif ch == '\r' or ch == '\n':
2852
- return 'ENTER'
2853
- elif ch == '\x03': # Ctrl+C
2854
- return 'CTRL_C'
2855
- return ch
2856
- finally:
2857
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
2858
-
2859
- def display_menu():
2860
- """Display the GPU selection menu with current selection highlighted."""
2861
- print("\n📊 Available GPU Options:")
2862
- print("┌──────────────┬─────────┐")
2863
- print("│ GPU Type │ VRAM │")
2864
- print("├──────────────┼─────────┤")
2865
-
2866
- for i, gpu_type in enumerate(options):
2867
- specs = gpu_specs[gpu_type]
2868
- # Calculate proper spacing for alignment
2869
- number_part = f"{i+1}."
2870
- if i == selected_index:
2871
- prefix = "> "
2872
- suffix = " ←"
2873
- else:
2874
- prefix = " "
2875
- suffix = ""
2876
-
2877
- # Ensure consistent width for GPU type column
2878
- gpu_display = f"{prefix}{number_part} {gpu_type}"
2879
- gpu_padded = f"{gpu_display:<12}" # Fixed width for GPU column
2880
-
2881
- print(f"│ {gpu_padded} │ {specs['memory']:<7} │{suffix}")
2882
-
2883
- print("└──────────────┴─────────┘")
2884
- print("Use ↑/↓ arrows to select, Enter to confirm, Ctrl+C to cancel")
2885
-
2886
- # Clear screen and show initial menu
2887
- print("\033[2J\033[H", end="") # Clear screen and move cursor to top
2888
- display_menu()
2889
-
2890
- while True:
2891
- try:
2892
- key = get_key()
2893
-
2894
- if key == 'UP':
2895
- selected_index = (selected_index - 1) % len(options)
2896
- print("\033[2J\033[H", end="") # Clear screen
2897
- display_menu()
2898
- elif key == 'DOWN':
2899
- selected_index = (selected_index + 1) % len(options)
2900
- print("\033[2J\033[H", end="") # Clear screen
2901
- display_menu()
2902
- elif key == 'ENTER':
2903
- selected_gpu = options[selected_index]
2904
- print(f"\n✅ Selected GPU: {selected_gpu}")
2905
- return selected_gpu
2906
- elif key == 'CTRL_C':
2907
- print("\n🛑 Selection cancelled.")
2908
- sys.exit(1)
2909
-
2910
- except KeyboardInterrupt:
2911
- print("\n🛑 Selection cancelled.")
2912
- sys.exit(1)
2913
- except Exception as e:
2914
- print(f"\n❌ Error: {e}")
2915
- # Fall back to simple input method
2916
- try:
2917
- choice = input("\n🔍 Select GPU type (number or name, default is A10G): ").strip()
2918
- if not choice:
2919
- return "A10G"
2920
- if choice.isdigit():
2921
- index = int(choice) - 1
2922
- if 0 <= index < len(options):
2923
- return options[index]
2924
- elif choice in options:
2925
- return choice
2926
- return "A10G"
2927
- except:
2928
- return "A10G"
2929
-
2930
- # Replace the existing GPU argument parsing in the main section
2931
3363
  if __name__ == "__main__":
2932
3364
  # Parse command line arguments when script is run directly
2933
3365
  import argparse
2934
3366
  import sys
2935
3367
 
2936
- parser = argparse.ArgumentParser()
2937
- parser.add_argument('--gpu', type=str, help='GPU type (e.g., A10G, T4, A100-80GB)')
3368
+ parser = argparse.ArgumentParser(description='Create a Modal SSH container with GPU')
3369
+ parser.add_argument('--gpu', type=str, default='A10G', help='GPU type (default: A10G)')
2938
3370
  parser.add_argument('--repo-url', type=str, help='Repository URL to clone')
2939
3371
  parser.add_argument('--repo-name', type=str, help='Repository name override')
2940
3372
  parser.add_argument('--setup-commands', type=str, nargs='+', help='Setup commands to run (deprecated)')
@@ -2945,257 +3377,169 @@ if __name__ == "__main__":
2945
3377
  parser.add_argument('--volume-name', type=str, help='Name of the Modal volume for persistent storage')
2946
3378
  parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
2947
3379
  parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
2948
- parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from original API')
2949
- parser.add_argument('--use-gitingest', action='store_true', default=True, help='Use gitingest approach to fetch setup commands (default)')
2950
- parser.add_argument('--no-gitingest', action='store_true', help='Disable gitingest approach for setup commands')
2951
- parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
2952
- parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
3380
+ parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from API')
2953
3381
  parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
3382
+ parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
2954
3383
 
2955
3384
  args = parser.parse_args()
2956
3385
 
2957
- # If --list-gpus is specified, just show GPU options and exit
2958
- if args.list_gpus:
2959
- prompt_for_gpu()
2960
- sys.exit(0)
2961
-
2962
3386
  # If no arguments or only --show-examples is provided, show usage examples
2963
3387
  if len(sys.argv) == 1 or args.show_examples:
2964
3388
  show_usage_examples()
2965
3389
  sys.exit(0)
3390
+
3391
+ # Get setup commands from file if specified
3392
+ setup_commands = args.setup_commands or []
2966
3393
 
2967
- # Check for dependencies
2968
- print("⠏ Checking dependencies...")
2969
- print("--- Dependency Check ---")
2970
-
2971
- # Check Python version
2972
- python_version = sys.version.split()[0]
2973
- print(f" Python {python_version} found")
2974
-
2975
- # Check Modal CLI
2976
- try:
2977
- subprocess.run(["modal", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2978
- print("✓ Modal CLI found")
2979
- except (subprocess.SubprocessError, FileNotFoundError):
2980
- print("❌ Modal CLI not found. Please install with: pip install modal")
2981
-
2982
- # Check Gitingest CLI
2983
- try:
2984
- subprocess.run(["gitingest", "--help"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2985
- print("✓ Gitingest CLI found")
2986
- except (subprocess.SubprocessError, FileNotFoundError):
2987
- print("⚠️ Gitingest CLI not found (optional)")
2988
-
2989
- # Check Git
2990
- try:
2991
- subprocess.run(["git", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2992
- print("✓ Git found")
2993
- except (subprocess.SubprocessError, FileNotFoundError):
2994
- print("❌ Git not found. Please install Git.")
2995
-
2996
- print("------------------------")
2997
- print("\n✔ Dependencies checked")
2998
-
2999
- # Always prompt for GPU selection regardless of interactive mode
3000
- gpu_type = prompt_for_gpu()
3001
- args.gpu = gpu_type
3002
-
3003
- # Display configuration after GPU selection
3004
- print("\n📋 Container Configuration:")
3005
- print(f"Repository URL: {args.repo_url or 'Not specified'}")
3006
- print(f"GPU Type: {gpu_type}")
3007
- print(f"Volume: {args.volume_name or 'None'}")
3008
- if args.use_api:
3009
- print("Setup Commands: Auto-detect from repository")
3010
- elif args.setup_commands:
3011
- print(f"Setup Commands: {len(args.setup_commands)} custom commands")
3012
- else:
3013
- print("Setup Commands: Auto-detect from repository")
3014
-
3015
- # Confirm settings
3016
- try:
3017
- proceed = input("Proceed with these settings? (Y/n): ").strip().lower()
3018
- if proceed in ('n', 'no'):
3019
- print("🛑 Operation cancelled by user.")
3020
- sys.exit(0)
3021
- except KeyboardInterrupt:
3022
- print("\n🛑 Operation cancelled by user.")
3023
- sys.exit(0)
3024
-
3025
- # Interactive mode or missing required arguments
3026
- if args.interactive or not args.repo_url or not args.volume_name:
3027
- # Get repository URL if not provided
3028
- repo_url = args.repo_url
3029
- if not repo_url:
3030
- try:
3031
- repo_url = input("? Enter GitHub repository URL: ").strip()
3032
- if not repo_url:
3033
- print("❌ Repository URL is required.")
3034
- sys.exit(1)
3035
- except KeyboardInterrupt:
3036
- print("\n🛑 Setup cancelled.")
3394
+ # If interactive mode is enabled, prompt for options
3395
+ if args.interactive:
3396
+ # If repo URL wasn't provided via command line, ask for it
3397
+ if not args.repo_url:
3398
+ args.repo_url = input("✔ Dependencies checked\n? Enter GitHub repository URL: ").strip()
3399
+ if not args.repo_url:
3400
+ print(" No repository URL provided. Exiting.")
3037
3401
  sys.exit(1)
3038
3402
 
3039
3403
  # Ask about persistent volume
3040
- volume_name = args.volume_name
3041
- if not volume_name:
3042
- try:
3043
- use_volume = input("? Use persistent volume for faster installs? (Y/n): ").strip().lower()
3044
- if use_volume in ('', 'y', 'yes'):
3045
- volume_name = input("? Enter volume name: ").strip()
3046
- if not volume_name:
3047
- volume_name = "gitarsenal-volume"
3048
- print(f"Using default volume name: {volume_name}")
3049
- except KeyboardInterrupt:
3050
- print("\n🛑 Setup cancelled.")
3051
- sys.exit(1)
3404
+ use_volume = input("? Use persistent volume for faster installs? (Yes/No) [Yes]: ").strip().lower()
3405
+ if not use_volume or use_volume.startswith('y'):
3406
+ if not args.volume_name:
3407
+ args.volume_name = input("? Enter volume name [gitarsenal-volume]: ").strip()
3408
+ if not args.volume_name:
3409
+ args.volume_name = "gitarsenal-volume"
3410
+ else:
3411
+ args.volume_name = None
3052
3412
 
3053
- # Ask about setup commands
3054
- use_gitingest = args.use_gitingest and not args.no_gitingest
3055
- if not args.use_api and not args.setup_commands and not args.setup_commands_json:
3056
- try:
3057
- auto_detect = input("? Automatically detect setup commands for this repository? (Y/n): ").strip().lower()
3058
- if auto_detect in ('n', 'no'):
3059
- use_gitingest = False
3060
- except KeyboardInterrupt:
3061
- print("\n🛑 Setup cancelled.")
3062
- sys.exit(1)
3413
+ # Ask about auto-detecting setup commands
3414
+ use_api = input("? Automatically detect setup commands for this repository? (Yes/No) [Yes]: ").strip().lower()
3415
+ if not use_api or use_api.startswith('y'):
3416
+ args.use_api = True
3417
+ else:
3418
+ args.use_api = False
3063
3419
 
3064
- # Update args with interactive values
3065
- args.repo_url = repo_url
3066
- args.volume_name = volume_name
3067
- args.use_gitingest = use_gitingest
3068
-
3069
- try:
3070
- # Get setup commands from file if specified
3071
- setup_commands = args.setup_commands or []
3072
-
3073
- # Use gitingest by default unless --no-gitingest is set
3074
- if args.repo_url and (args.use_gitingest and not args.no_gitingest):
3075
- print("🔄 Using gitingest approach to fetch setup commands (default)")
3076
- api_commands = get_setup_commands_from_gitingest(args.repo_url)
3077
- if api_commands:
3078
- setup_commands = api_commands
3079
- print(f"📋 Using {len(setup_commands)} commands from gitingest API")
3080
- else:
3081
- print("⚠️ Failed to get commands from gitingest API")
3082
- if not args.use_api:
3083
- print("⚠️ Falling back to basic setup commands")
3084
- setup_commands = generate_fallback_commands(None)
3085
- else:
3086
- setup_commands = []
3087
- # If --use-api flag is set and repo_url is provided, fetch setup commands from API
3088
- elif args.use_api and args.repo_url:
3089
- print("🔄 Using original API to fetch setup commands")
3090
- api_commands = fetch_setup_commands_from_api(args.repo_url)
3091
- if api_commands:
3092
- setup_commands = api_commands
3093
- print(f"📋 Using {len(setup_commands)} commands from original API")
3094
- else:
3095
- print("⚠️ Failed to get commands from API, no fallback commands will be used")
3096
- # Do not fall back to basic setup commands
3097
- setup_commands = []
3420
+ # GPU selection
3421
+ gpu_options = ['T4', 'L4', 'A10G', 'A100-40GB', 'A100-80GB', 'L40S', 'H100', 'H200', 'B200']
3422
+ print("\n? Select GPU type:")
3423
+ for i, gpu in enumerate(gpu_options, 1):
3424
+ print(f" {i}. {gpu}")
3098
3425
 
3099
- # Parse setup commands from JSON if provided
3100
- if args.setup_commands_json:
3426
+ gpu_choice = input(f"Enter choice (1-{len(gpu_options)}) [default: 3 for A10G]: ").strip()
3427
+ if not gpu_choice:
3428
+ args.gpu = 'A10G' # Default
3429
+ else:
3101
3430
  try:
3102
- json_commands = json.loads(args.setup_commands_json)
3103
- if isinstance(json_commands, list):
3104
- setup_commands = json_commands
3105
- print(f"📋 Parsed {len(setup_commands)} commands from JSON:")
3106
- for i, cmd in enumerate(setup_commands, 1):
3107
- print(f" {i}. {cmd}")
3431
+ gpu_index = int(gpu_choice) - 1
3432
+ if 0 <= gpu_index < len(gpu_options):
3433
+ args.gpu = gpu_options[gpu_index]
3108
3434
  else:
3109
- print(f"⚠️ Invalid JSON format for setup commands: not a list")
3110
- except json.JSONDecodeError as e:
3111
- print(f"⚠️ Error parsing JSON setup commands: {e}")
3112
- print(f"Received JSON string: {args.setup_commands_json}")
3113
-
3114
- # Print received setup commands for debugging
3115
- if setup_commands:
3116
- print(f"📋 Using {len(setup_commands)} setup commands:")
3117
- for i, cmd in enumerate(setup_commands, 1):
3118
- print(f" {i}. {cmd}")
3435
+ print("⚠️ Invalid choice. Using default: A10G")
3436
+ args.gpu = 'A10G'
3437
+ except ValueError:
3438
+ print("⚠️ Invalid input. Using default: A10G")
3439
+ args.gpu = 'A10G'
3440
+
3441
+ # Show configuration summary
3442
+ print("\n📋 Container Configuration:")
3443
+ print(f"Repository URL: {args.repo_url}")
3444
+ print(f"GPU Type: {args.gpu}")
3445
+ print(f"Volume: {args.volume_name if args.volume_name else 'None'}")
3446
+ print(f"Setup Commands: {'Auto-detect from repository' if args.use_api else 'None'}")
3447
+
3448
+ # Confirm settings
3449
+ confirm = input("? Proceed with these settings? (Yes/No) [Yes]: ").strip().lower()
3450
+ if confirm and not confirm.startswith('y'):
3451
+ print("❌ Setup cancelled by user.")
3452
+ sys.exit(0)
3453
+
3454
+ # If --use-api flag is set and repo_url is provided, fetch setup commands from API
3455
+ if args.use_api and args.repo_url:
3456
+ print("🔄 Using API to fetch setup commands")
3457
+ api_commands = fetch_setup_commands_from_api(args.repo_url)
3458
+ if api_commands:
3459
+ setup_commands = api_commands
3460
+ print(f"📋 Using {len(setup_commands)} commands from API")
3461
+ else:
3462
+ print("⚠️ Failed to get commands from API, no fallback commands will be used")
3463
+ # Do not fall back to basic setup commands
3464
+ setup_commands = []
3465
+
3466
+ # Parse setup commands from JSON if provided
3467
+ if args.setup_commands_json:
3468
+ try:
3469
+ json_commands = json.loads(args.setup_commands_json)
3470
+ if isinstance(json_commands, list):
3471
+ setup_commands = json_commands
3472
+ print(f"📋 Parsed {len(setup_commands)} commands from JSON:")
3473
+ for i, cmd in enumerate(setup_commands, 1):
3474
+ print(f" {i}. {cmd}")
3475
+ else:
3476
+ print(f"⚠️ Invalid JSON format for setup commands: not a list")
3477
+ except json.JSONDecodeError as e:
3478
+ print(f"⚠️ Error parsing JSON setup commands: {e}")
3479
+ print(f"Received JSON string: {args.setup_commands_json}")
3480
+
3481
+ # Print received setup commands for debugging
3482
+ if setup_commands:
3483
+ print(f"📋 Using {len(setup_commands)} setup commands:")
3484
+ for i, cmd in enumerate(setup_commands, 1):
3485
+ print(f" {i}. {cmd}")
3486
+
3487
+ # Load commands from file if specified
3488
+ if args.commands_file and os.path.exists(args.commands_file):
3489
+ try:
3490
+ with open(args.commands_file, 'r') as f:
3491
+ # Check if the file contains JSON or line-by-line commands
3492
+ content = f.read().strip()
3119
3493
 
3120
- # Load commands from file if specified
3121
- if args.commands_file and os.path.exists(args.commands_file):
3122
- try:
3123
- with open(args.commands_file, 'r') as f:
3124
- # Check if the file contains JSON or line-by-line commands
3125
- content = f.read().strip()
3126
-
3127
- if content.startswith('[') and content.endswith(']'):
3128
- # JSON format
3129
- try:
3130
- json_commands = json.loads(content)
3131
- if isinstance(json_commands, list):
3132
- setup_commands.extend(json_commands)
3133
- print(f"📋 Loaded {len(json_commands)} commands from JSON file {args.commands_file}")
3134
- else:
3135
- print(f"⚠️ Invalid JSON format in commands file: not a list")
3136
- except json.JSONDecodeError as json_err:
3137
- print(f"⚠️ Error parsing JSON commands file: {json_err}")
3138
- # Fall back to line-by-line parsing
3139
- file_commands = [line.strip() for line in content.split('\n') if line.strip()]
3140
- setup_commands.extend(file_commands)
3141
- print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line fallback)")
3142
- else:
3143
- # Line-by-line format
3494
+ if content.startswith('[') and content.endswith(']'):
3495
+ # JSON format
3496
+ try:
3497
+ json_commands = json.loads(content)
3498
+ if isinstance(json_commands, list):
3499
+ setup_commands.extend(json_commands)
3500
+ print(f"📋 Loaded {len(json_commands)} commands from JSON file {args.commands_file}")
3501
+ else:
3502
+ print(f"⚠️ Invalid JSON format in commands file: not a list")
3503
+ except json.JSONDecodeError as json_err:
3504
+ print(f"⚠️ Error parsing JSON commands file: {json_err}")
3505
+ # Fall back to line-by-line parsing
3144
3506
  file_commands = [line.strip() for line in content.split('\n') if line.strip()]
3145
3507
  setup_commands.extend(file_commands)
3146
- print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line format)")
3147
- except Exception as e:
3148
- print(f"⚠️ Error loading commands from file: {e}")
3149
-
3150
- # Load commands from setup script if specified
3151
- if args.setup_script and os.path.exists(args.setup_script):
3152
- try:
3153
- with open(args.setup_script, 'r') as f:
3154
- script_content = f.read().strip()
3155
- # Convert script to individual commands
3156
- script_commands = [line.strip() for line in script_content.split('\n')
3157
- if line.strip() and not line.strip().startswith('#')]
3158
- setup_commands.extend(script_commands)
3159
- print(f"📋 Loaded {len(script_commands)} commands from script {args.setup_script}")
3160
- except Exception as e:
3161
- print(f"⚠️ Error loading commands from script: {e}")
3162
-
3163
- # Create the container with the specified options
3164
- if args.ssh_password:
3165
- print(f"🔑 Using provided SSH password")
3166
- ssh_password = args.ssh_password
3167
- else:
3168
- ssh_password = generate_random_password()
3169
- print(f"🔑 Generated random SSH password: {ssh_password}")
3170
-
3171
- # Extract repository name from URL if not provided
3172
- repo_name = args.repo_name
3173
- if not repo_name and args.repo_url:
3174
- # Try to extract repo name from URL
3175
- url_parts = args.repo_url.rstrip('/').split('/')
3176
- if url_parts:
3177
- repo_name = url_parts[-1]
3178
- if repo_name.endswith('.git'):
3179
- repo_name = repo_name[:-4]
3180
-
3181
- # Create the container
3182
- create_modal_ssh_container(
3183
- gpu_type=args.gpu,
3184
- repo_url=args.repo_url,
3185
- repo_name=repo_name,
3186
- setup_commands=setup_commands,
3187
- volume_name=args.volume_name,
3188
- timeout_minutes=args.timeout,
3189
- ssh_password=ssh_password,
3190
- interactive=args.interactive
3508
+ print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line fallback)")
3509
+ else:
3510
+ # Line-by-line format
3511
+ file_commands = [line.strip() for line in content.split('\n') if line.strip()]
3512
+ setup_commands.extend(file_commands)
3513
+ print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line format)")
3514
+
3515
+ except Exception as e:
3516
+ print(f"⚠️ Error reading commands file: {e}")
3517
+
3518
+ try:
3519
+ result = create_modal_ssh_container(
3520
+ args.gpu,
3521
+ args.repo_url,
3522
+ args.repo_name,
3523
+ setup_commands,
3524
+ getattr(args, 'volume_name', None),
3525
+ args.timeout,
3526
+ args.ssh_password
3191
3527
  )
3528
+
3529
+ print("\n⏳ Keeping the SSH container alive. Press Ctrl+C to exit (container will continue running)...")
3530
+ try:
3531
+ while True:
3532
+ time.sleep(90)
3533
+ print(".", end="", flush=True)
3534
+ except KeyboardInterrupt:
3535
+ print("\n👋 Script exited. The SSH container will continue running.")
3536
+ sys.exit(0)
3537
+
3192
3538
  except KeyboardInterrupt:
3193
- # print("\n\n🛑 Execution interrupted")
3194
- # print("🧹 Cleaning up resources...")
3195
- cleanup_modal_token()
3196
- sys.exit(1)
3539
+ # Handle Ctrl+C during container creation
3540
+ print("\n👋 Script interrupted during container creation.")
3541
+ print("📝 You may need to check if a container was created with: modal container list")
3542
+ sys.exit(0)
3197
3543
  except Exception as e:
3198
- # print(f"\n❌ Error: {e}")
3199
- # print("🧹 Cleaning up resources...")
3200
- cleanup_modal_token()
3544
+ print(f"❌ Error: {e}")
3201
3545
  sys.exit(1)