gitarsenal-cli 1.6.1 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ import argparse
13
13
  from pathlib import Path
14
14
 
15
15
  # Parse command-line arguments
16
- parser = argparse.ArgumentParser(description='Launch a Modal sandbox')
16
+ parser = argparse.ArgumentParser()
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,24 +27,35 @@ 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 Modal tokens from proxy server...")
39
+ print("🔄 Fetching tokens from proxy server...")
40
40
  from fetch_modal_tokens import get_tokens
41
- token_id, token_secret = get_tokens()
42
- print(f"✅ Modal tokens fetched successfully")
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")
43
54
 
44
55
  # Explicitly set the environment variables again to be sure
45
56
  os.environ["MODAL_TOKEN_ID"] = token_id
46
57
  os.environ["MODAL_TOKEN_SECRET"] = token_secret
47
-
58
+ os.environ["OPENAI_API_KEY"] = openai_api_key
48
59
  # Also set the old environment variable for backward compatibility
49
60
  os.environ["MODAL_TOKEN"] = token_id
50
61
 
@@ -56,26 +67,26 @@ except Exception as e:
56
67
  # Apply the comprehensive Modal token solution as fallback
57
68
  try:
58
69
  # Import the comprehensive solution module
59
- print("🔄 Applying comprehensive Modal token solution...")
70
+ # print("🔄 Applying comprehensive Modal token solution...")
60
71
  import modal_token_solution
61
- print("✅ Comprehensive Modal token solution applied")
72
+ # print("✅ Comprehensive Modal token solution applied")
62
73
 
63
74
  # Set token variables for later use
64
75
  token = modal_token_solution.TOKEN_ID # For backward compatibility
65
76
  except Exception as e:
66
- print(f"⚠️ Error applying comprehensive Modal token solution: {e}")
77
+ # print(f"⚠️ Error applying comprehensive Modal token solution: {e}")
67
78
 
68
79
  # Fall back to the authentication patch
69
80
  try:
70
81
  # Import the patch module
71
- print("🔄 Falling back to Modal authentication patch...")
82
+ # print("🔄 Falling back to Modal authentication patch...")
72
83
  import modal_auth_patch
73
- print("✅ Modal authentication patch applied")
84
+ # print("✅ Modal authentication patch applied")
74
85
 
75
86
  # Set token variables for later use
76
87
  token = modal_auth_patch.TOKEN_ID # For backward compatibility
77
88
  except Exception as e:
78
- print(f"⚠️ Error applying Modal authentication patch: {e}")
89
+ # print(f"⚠️ Error applying Modal authentication patch: {e}")
79
90
 
80
91
  # Fall back to fix_modal_token.py
81
92
  try:
@@ -115,17 +126,17 @@ except Exception as e:
115
126
  token = "ak-sLhYqCjkvixiYcb9LAuCHp" # Default token ID
116
127
 
117
128
  # Print debug info
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'}")
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'}")
122
133
  if os.environ.get('MODAL_TOKEN_ID'):
123
- print(f"🔍 MODAL_TOKEN_ID length: {len(os.environ.get('MODAL_TOKEN_ID'))}")
134
+ print(f"🔍 Token ID length: {len(os.environ.get('MODAL_TOKEN_ID'))}")
124
135
  if os.environ.get('MODAL_TOKEN_SECRET'):
125
- print(f"🔍 MODAL_TOKEN_SECRET length: {len(os.environ.get('MODAL_TOKEN_SECRET'))}")
136
+ print(f"🔍 Token secret length: {len(os.environ.get('MODAL_TOKEN_SECRET'))}")
126
137
  if os.environ.get('MODAL_TOKEN'):
127
- print(f"🔍 MODAL_TOKEN length: {len(os.environ.get('MODAL_TOKEN'))}")
128
- print(f"✅ Modal token setup completed")
138
+ print(f"🔍 Token length: {len(os.environ.get('MODAL_TOKEN'))}")
139
+ # print(f"✅ Token setup completed")
129
140
 
130
141
  # Import modal after token setup
131
142
  import modal
@@ -315,17 +326,15 @@ def handle_huggingface_login(sandbox, current_dir):
315
326
 
316
327
  return exit_code == 0, stdout_buffer, stderr_buffer
317
328
 
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
326
329
 
327
330
  def call_openai_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
328
331
  """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
+
329
338
  # Define _to_str function locally to avoid NameError
330
339
  def _to_str(maybe_bytes):
331
340
  try:
@@ -351,22 +360,86 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
351
360
  print("⚠️ Error output is empty. Cannot effectively debug the command.")
352
361
  print("⚠️ Skipping OpenAI debugging due to lack of error information.")
353
362
  return None
354
-
363
+
364
+ # Try to get API key from multiple sources
355
365
  if not api_key:
356
- # Try to get API key from environment
366
+ print("🔍 DEBUG: No API key provided, searching for one...")
367
+
368
+ # First try environment variable
357
369
  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}")
358
373
 
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
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
+
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...")
370
443
  print("\n" + "="*60)
371
444
  print("🔑 OPENAI API KEY REQUIRED FOR DEBUGGING")
372
445
  print("="*60)
@@ -381,6 +454,9 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
381
454
  print("❌ No API key provided. Skipping debugging.")
382
455
  return None
383
456
  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
384
460
  except KeyboardInterrupt:
385
461
  print("\n❌ API key input cancelled by user.")
386
462
  return None
@@ -388,9 +464,19 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
388
464
  print(f"❌ Error getting API key: {e}")
389
465
  return None
390
466
 
391
- # Get current directory context
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
392
476
  directory_context = ""
393
477
  system_info = ""
478
+ command_history = ""
479
+ file_context = ""
394
480
 
395
481
  if sandbox:
396
482
  try:
@@ -404,6 +490,7 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
404
490
  uname -a
405
491
  echo -e "\nPython Information:"
406
492
  python --version
493
+ pip --version
407
494
  echo -e "\nPackage Manager:"
408
495
  which apt 2>/dev/null && echo "apt available" || echo "apt not available"
409
496
  which yum 2>/dev/null && echo "yum available" || echo "yum not available"
@@ -458,6 +545,72 @@ Directory contents:
458
545
  {parent_context}
459
546
  """
460
547
  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
+
461
614
  except Exception as e:
462
615
  print(f"⚠️ Error getting directory context: {e}")
463
616
  directory_context = f"\nCurrent directory: {current_dir}\n"
@@ -489,64 +642,262 @@ But it failed with this error:
489
642
  ```
490
643
  {system_info}
491
644
  {directory_context}
645
+ {file_context}
646
+
492
647
  Please analyze the error and provide ONLY a single terminal command that would fix the issue.
493
648
  Consider the current directory, system information, and directory contents carefully before suggesting a solution.
494
649
 
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
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)
500
670
 
501
671
  Do not provide any explanations, just the exact command to run.
502
672
  """
503
673
 
504
674
  # Prepare the API request payload
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
- }
675
+ print("🔍 DEBUG: Preparing API request...")
514
676
 
515
- try:
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
- )
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]}...")
523
700
 
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
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
545
804
  else:
546
- print(f" OpenAI API error: {response.status_code} - {response.text}")
547
- return None
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
811
+
812
+ # Process the response
813
+ 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")
893
+
894
+ print(f"🔧 Suggested fix: {fix_command}")
895
+ print(f"🔍 DEBUG: Returning fix command: {fix_command}")
896
+ return fix_command
548
897
  except Exception as e:
549
- print(f"❌ Error calling OpenAI API: {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)}")
550
901
  return None
551
902
 
552
903
  def prompt_for_hf_token():
@@ -586,1611 +937,106 @@ def prompt_for_hf_token():
586
937
  print(f"❌ Error getting token: {e}")
587
938
  return None
588
939
 
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")
940
+
941
+ def handle_interactive_input(prompt, is_password=False):
942
+ """Handle interactive input from the user with optional password masking"""
943
+ print("\n" + "="*60)
944
+ print(f"{prompt}")
945
+ print("="*60)
597
946
 
598
- # Check if Modal is authenticated
599
947
  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}")
948
+ if is_password:
949
+ user_input = getpass.getpass("Input (hidden): ").strip()
950
+ else:
951
+ user_input = input("Input: ").strip()
644
952
 
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)
953
+ if not user_input:
954
+ print(" No input provided.")
955
+ return None
956
+ print(" Input received successfully!")
957
+ return user_input
958
+ except KeyboardInterrupt:
959
+ print("\n❌ Input cancelled by user.")
666
960
  return None
667
961
  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")
962
+ print(f" Error getting input: {e}")
963
+ return None
756
964
 
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
- )
965
+ def generate_random_password(length=16):
966
+ """Generate a random password for SSH access"""
967
+ alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
968
+ password = ''.join(secrets.choice(alphabet) for i in range(length))
969
+ return password
765
970
 
766
- # Get the sandbox ID for reference
767
- sandbox_id = sandbox.object_id
768
- print(f"📋 Sandbox ID: {sandbox_id}")
971
+ # First, add the standalone ssh_container function at the module level, before the create_modal_ssh_container function
769
972
 
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
973
+ # Define a module-level ssh container function
974
+ ssh_app = modal.App("ssh-container-app")
773
975
 
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}")
976
+ @ssh_app.function(
977
+ image=modal.Image.debian_slim()
978
+ .apt_install(
979
+ "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
980
+ "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
981
+ "gpg", "ca-certificates", "software-properties-common"
982
+ )
983
+ .pip_install("uv", "modal", "requests", "openai") # Fast Python package installer and Modal
984
+ .run_commands(
985
+ # Create SSH directory
986
+ "mkdir -p /var/run/sshd",
987
+ "mkdir -p /root/.ssh",
988
+ "chmod 700 /root/.ssh",
786
989
 
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)
990
+ # Configure SSH server
991
+ "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
992
+ "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
993
+ "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
840
994
 
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}")
995
+ # SSH keep-alive settings
996
+ "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
997
+ "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
855
998
 
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
999
+ # Generate SSH host keys
1000
+ "ssh-keygen -A",
869
1001
 
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")
1002
+ # Install Modal CLI
1003
+ "pip install modal",
930
1004
 
931
- print("⚠️ Container connection may fail. You may need to connect manually.")
1005
+ # Set up a nice bash prompt
1006
+ "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
1007
+ ),
1008
+ timeout=3600, # Default 1 hour timeout
1009
+ gpu="a10g", # Default GPU - this will be overridden when called
1010
+ cpu=2,
1011
+ memory=8192,
1012
+ serialized=True,
1013
+ )
1014
+ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_commands=None, openai_api_key=None):
1015
+ import subprocess
1016
+ import time
1017
+ import os
1018
+
1019
+ # Set root password
1020
+ subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
1021
+
1022
+ # Start SSH service
1023
+ subprocess.run(["service", "ssh", "start"], check=True)
1024
+
1025
+ # Setup environment
1026
+ os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
1027
+
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)})")
932
1032
  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
-
2101
-
2102
- def handle_interactive_input(prompt, is_password=False):
2103
- """Handle interactive input from the user with optional password masking"""
2104
- print("\n" + "="*60)
2105
- print(f"{prompt}")
2106
- print("="*60)
2107
-
2108
- try:
2109
- if is_password:
2110
- user_input = getpass.getpass("Input (hidden): ").strip()
2111
- else:
2112
- user_input = input("Input: ").strip()
2113
-
2114
- if not user_input:
2115
- print("❌ No input provided.")
2116
- return None
2117
- print("✅ Input received successfully!")
2118
- return user_input
2119
- except KeyboardInterrupt:
2120
- print("\n❌ Input cancelled by user.")
2121
- return None
2122
- except Exception as e:
2123
- print(f"❌ Error getting input: {e}")
2124
- return None
2125
-
2126
- def generate_random_password(length=16):
2127
- """Generate a random password for SSH access"""
2128
- alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
2129
- password = ''.join(secrets.choice(alphabet) for i in range(length))
2130
- return password
2131
-
2132
- # First, add the standalone ssh_container function at the module level, before the create_modal_ssh_container function
2133
-
2134
- # Define a module-level ssh container function
2135
- ssh_app = modal.App("ssh-container-app")
2136
-
2137
- @ssh_app.function(
2138
- image=modal.Image.debian_slim()
2139
- .apt_install(
2140
- "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
2141
- "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2142
- "gpg", "ca-certificates", "software-properties-common"
2143
- )
2144
- .pip_install("uv", "modal") # Fast Python package installer and Modal
2145
- .run_commands(
2146
- # Create SSH directory
2147
- "mkdir -p /var/run/sshd",
2148
- "mkdir -p /root/.ssh",
2149
- "chmod 700 /root/.ssh",
2150
-
2151
- # Configure SSH server
2152
- "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
2153
- "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
2154
- "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
2155
-
2156
- # SSH keep-alive settings
2157
- "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
2158
- "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
2159
-
2160
- # Generate SSH host keys
2161
- "ssh-keygen -A",
2162
-
2163
- # Install Modal CLI
2164
- "pip install modal",
2165
-
2166
- # Set up a nice bash prompt
2167
- "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2168
- ),
2169
- timeout=3600, # Default 1 hour timeout
2170
- gpu="a10g", # Default GPU - this will be overridden when called
2171
- cpu=2,
2172
- memory=8192,
2173
- serialized=True,
2174
- )
2175
- def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_commands=None):
2176
- import subprocess
2177
- import time
2178
- import os
2179
-
2180
- # Set root password
2181
- subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
2182
-
2183
- # Start SSH service
2184
- subprocess.run(["service", "ssh", "start"], check=True)
2185
-
2186
- # Setup environment
2187
- os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
2188
-
2189
- # Clone repository if provided
2190
- if repo_url:
2191
- repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
2192
- print(f"📥 Cloning repository: {repo_url}")
2193
-
1033
+ print("⚠️ No OpenAI API key provided to container")
1034
+
1035
+ # Clone repository if provided
1036
+ if repo_url:
1037
+ repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
1038
+ print(f"📥 Cloning repository: {repo_url}")
1039
+
2194
1040
  try:
2195
1041
  subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
2196
1042
  print(f"✅ Repository cloned successfully: {repo_name_from_url}")
@@ -2207,17 +1053,118 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
2207
1053
  # Run setup commands if provided
2208
1054
  if setup_commands:
2209
1055
  print(f"⚙️ Running {len(setup_commands)} setup commands...")
2210
- for i, cmd in enumerate(setup_commands, 1):
2211
- print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
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}")
2212
1070
  try:
2213
- result = subprocess.run(cmd, shell=True, check=True,
2214
- capture_output=True, text=True)
2215
- if result.stdout:
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:
2216
1087
  print(f"✅ Output: {result.stdout}")
1088
+ return True, result.stdout, ""
2217
1089
  except subprocess.CalledProcessError as e:
1090
+ error_output = e.stderr if e.stderr else str(e)
2218
1091
  print(f"❌ Command failed: {e}")
2219
- if e.stderr:
2220
- print(f"❌ Error: {e.stderr}")
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
+ for i, cmd in enumerate(setup_commands, 1):
1138
+ 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)
2221
1168
 
2222
1169
  # Get container info
2223
1170
  print("🔍 Container started successfully!")
@@ -2235,23 +1182,56 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
2235
1182
  subprocess.run(["service", "ssh", "start"], check=True)
2236
1183
 
2237
1184
  # Now modify the create_modal_ssh_container function to use the standalone ssh_container_function
2238
-
2239
1185
  def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
2240
- volume_name=None, timeout_minutes=60, ssh_password=None):
1186
+ volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False):
2241
1187
  """Create a Modal SSH container with GPU support and tunneling"""
2242
1188
 
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
+
2243
1219
  # Check if Modal is authenticated
2244
1220
  try:
2245
1221
  # Print all environment variables for debugging
2246
1222
  print("🔍 DEBUG: Checking environment variables")
2247
1223
  modal_token_id = os.environ.get("MODAL_TOKEN_ID")
2248
1224
  modal_token = os.environ.get("MODAL_TOKEN")
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'}")
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'}")
2251
1229
  if modal_token_id:
2252
- print(f"🔍 MODAL_TOKEN_ID length: {len(modal_token_id)}")
1230
+ print(f"🔍 token length: {len(modal_token_id)}")
2253
1231
  if modal_token:
2254
- print(f"🔍 MODAL_TOKEN length: {len(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)}")
2255
1235
 
2256
1236
  # Try to access Modal token to check authentication
2257
1237
  try:
@@ -2262,13 +1242,13 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2262
1242
  # Try to get from MODAL_TOKEN
2263
1243
  modal_token = os.environ.get("MODAL_TOKEN")
2264
1244
  if modal_token:
2265
- print("✅ Found token in MODAL_TOKEN environment variable")
1245
+ print("✅ Found token in environment variable")
2266
1246
  os.environ["MODAL_TOKEN_ID"] = modal_token
2267
1247
  modal_token_id = modal_token
2268
- print(f"✅ Set MODAL_TOKEN_ID from MODAL_TOKEN (length: {len(modal_token)})")
1248
+ print(f"✅ Set token (length: {len(modal_token)})")
2269
1249
 
2270
1250
  if modal_token_id:
2271
- print(f"✅ Modal token found (length: {len(modal_token_id)})")
1251
+ print(f"✅ token found (length: {len(modal_token_id)})")
2272
1252
 
2273
1253
  # Use the comprehensive fix_modal_token script
2274
1254
  try:
@@ -2289,11 +1269,11 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2289
1269
  if result.stderr:
2290
1270
  print(f"Error: {result.stderr}")
2291
1271
 
2292
- print(f"✅ Modal token setup completed")
1272
+ print(f"✅ token setup completed")
2293
1273
  except Exception as e:
2294
1274
  print(f"⚠️ Error running fix_modal_token.py: {e}")
2295
1275
  else:
2296
- print("❌ No Modal token found in environment variables")
1276
+ print("❌ No token found in environment variables")
2297
1277
  # Try to get from file as a last resort
2298
1278
  try:
2299
1279
  home_dir = os.path.expanduser("~")
@@ -2312,7 +1292,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2312
1292
  else:
2313
1293
  print("❌ Token file does not contain token_id")
2314
1294
  else:
2315
- print("❌ Modal token file not found")
1295
+ print("❌ token file not found")
2316
1296
  except Exception as e:
2317
1297
  print(f"❌ Error loading token from file: {e}")
2318
1298
 
@@ -2326,9 +1306,9 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2326
1306
  modal_token_id = os.environ.get("MODAL_TOKEN_ID")
2327
1307
  modal_token = os.environ.get("MODAL_TOKEN")
2328
1308
  if modal_token_id:
2329
- print(f"🔄 Using MODAL_TOKEN_ID from environment (length: {len(modal_token_id)})")
1309
+ print(f"🔄 Using token from environment (length: {len(modal_token_id)})")
2330
1310
  elif modal_token:
2331
- print(f"🔄 Using MODAL_TOKEN from environment (length: {len(modal_token)})")
1311
+ print(f"🔄 Using token from environment (length: {len(modal_token)})")
2332
1312
  os.environ["MODAL_TOKEN_ID"] = modal_token
2333
1313
  modal_token_id = modal_token
2334
1314
  else:
@@ -2338,7 +1318,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2338
1318
  # Set it in both environment variables
2339
1319
  os.environ["MODAL_TOKEN_ID"] = modal_token_id
2340
1320
  os.environ["MODAL_TOKEN"] = modal_token_id
2341
- print("✅ Set both MODAL_TOKEN_ID and MODAL_TOKEN environment variables")
1321
+ print("✅ Set both token and id environment variables")
2342
1322
  except Exception as e:
2343
1323
  print(f"⚠️ Error checking Modal authentication: {e}")
2344
1324
  print("Continuing anyway, but Modal operations may fail")
@@ -2364,7 +1344,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2364
1344
  gpu_type = 'A10G'
2365
1345
 
2366
1346
  gpu_spec = gpu_configs[gpu_type]
2367
- print(f"🚀 Creating Modal SSH container with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
1347
+ print(f"🚀 Creating SSH container with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
2368
1348
 
2369
1349
  # Generate or use provided SSH password
2370
1350
  if not ssh_password:
@@ -2400,16 +1380,16 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2400
1380
  # Print debug info for authentication
2401
1381
  print("🔍 Modal authentication debug info:")
2402
1382
  modal_token = os.environ.get("MODAL_TOKEN_ID")
2403
- print(f" - MODAL_TOKEN_ID in env: {'Yes' if modal_token else 'No'}")
1383
+ print(f" - token in env: {'Yes' if modal_token else 'No'}")
2404
1384
  print(f" - Token length: {len(modal_token) if modal_token else 'N/A'}")
2405
1385
 
2406
1386
  # Verify we can create a Modal app
2407
1387
  try:
2408
- print("🔍 Testing Modal app creation...")
1388
+ print("🔍 Testing app creation...")
2409
1389
  app = modal.App(app_name)
2410
- print("✅ Created Modal app successfully")
1390
+ print("✅ Created app successfully")
2411
1391
  except Exception as e:
2412
- print(f"❌ Error creating Modal app: {e}")
1392
+ print(f"❌ Error creating app: {e}")
2413
1393
  return None
2414
1394
 
2415
1395
  # Create SSH-enabled image
@@ -2422,7 +1402,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2422
1402
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2423
1403
  "gpg", "ca-certificates", "software-properties-common"
2424
1404
  )
2425
- .pip_install("uv", "modal") # Fast Python package installer and Modal
1405
+ .pip_install("uv", "modal", "requests", "openai") # Fast Python package installer and Modal
2426
1406
  .run_commands(
2427
1407
  # Create SSH directory
2428
1408
  "mkdir -p /var/run/sshd",
@@ -2465,7 +1445,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2465
1445
  serialized=True,
2466
1446
  volumes=volumes_config if volumes_config else None,
2467
1447
  )
2468
- def ssh_container_function():
1448
+ def ssh_container_function(ssh_password=None, repo_url=None, repo_name=None, setup_commands=None, openai_api_key=None):
2469
1449
  """Start SSH container with password authentication and optional setup."""
2470
1450
  import subprocess
2471
1451
  import time
@@ -2474,6 +1454,13 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2474
1454
  # Set root password
2475
1455
  subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
2476
1456
 
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
+
2477
1464
  # Start SSH service
2478
1465
  subprocess.run(["service", "ssh", "start"], check=True)
2479
1466
 
@@ -2498,17 +1485,110 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2498
1485
  # Run setup commands if provided
2499
1486
  if setup_commands:
2500
1487
  print(f"⚙️ Running {len(setup_commands)} setup commands...")
2501
- for i, cmd in enumerate(setup_commands, 1):
2502
- print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
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}")
2503
1493
  try:
2504
- result = subprocess.run(cmd, shell=True, check=True,
2505
- capture_output=True, text=True)
2506
- if result.stdout:
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:
2507
1510
  print(f"✅ Output: {result.stdout}")
1511
+ return True, result.stdout, ""
2508
1512
  except subprocess.CalledProcessError as e:
1513
+ error_output = e.stderr if e.stderr else str(e)
2509
1514
  print(f"❌ Command failed: {e}")
2510
- if e.stderr:
2511
- print(f"❌ Error: {e.stderr}")
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)
2512
1592
 
2513
1593
  # Create SSH tunnel
2514
1594
  with modal.forward(22, unencrypted=True) as tunnel:
@@ -2544,7 +1624,9 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2544
1624
  # Start the container in a new thread to avoid blocking
2545
1625
  with modal.enable_output():
2546
1626
  with app.run():
2547
- ssh_container_function.remote()
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)
2548
1630
 
2549
1631
  # Clean up Modal token after container is successfully created
2550
1632
  cleanup_modal_token()
@@ -2565,8 +1647,13 @@ def fetch_setup_commands_from_api(repo_url):
2565
1647
  import os
2566
1648
  import shutil
2567
1649
  import json
1650
+ import time
1651
+ import requests
2568
1652
 
2569
- api_url = "https://git-arsenal.vercel.app/api/analyze-with-gitingest"
1653
+ # Define API endpoints to try in order - using only online endpoints
1654
+ api_endpoints = [
1655
+ "https://www.gitarsenal.dev/api/analyze-with-gitingest" # Working endpoint with www prefix
1656
+ ]
2570
1657
 
2571
1658
  print(f"🔍 Fetching setup commands from API for repository: {repo_url}")
2572
1659
 
@@ -2588,6 +1675,13 @@ def fetch_setup_commands_from_api(repo_url):
2588
1675
  temp_dir = tempfile.mkdtemp(prefix="repo_analysis_")
2589
1676
  output_file = os.path.join(temp_dir, "digest.json")
2590
1677
 
1678
+ # Create a directory to save GitIngest results
1679
+ save_dir = os.path.join(os.path.expanduser("~"), "gitarsenal_results")
1680
+ os.makedirs(save_dir, exist_ok=True)
1681
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
1682
+ repo_name = repo_url.split("/")[-1].replace(".git", "")
1683
+ save_file = os.path.join(save_dir, f"gitingest_{repo_name}_{timestamp}.txt")
1684
+
2591
1685
  try:
2592
1686
  if has_gitingest_cli:
2593
1687
  # Use gitingest CLI tool to analyze the repository directly from URL
@@ -2624,16 +1718,65 @@ def fetch_setup_commands_from_api(repo_url):
2624
1718
  try:
2625
1719
  with open(output_file, 'r', encoding='utf-8') as f:
2626
1720
  content = f.read()
1721
+
1722
+ # Save the GitIngest output to the results directory
1723
+ with open(save_file, 'w', encoding='utf-8') as save_f:
1724
+ save_f.write(content)
1725
+ print(f"📁 GitIngest output saved to: {save_file}")
1726
+
2627
1727
  try:
2628
1728
  gitingest_data = json.loads(content)
2629
1729
  print(f"✅ GitIngest data loaded as JSON from {output_file}")
2630
1730
  except json.JSONDecodeError:
2631
1731
  # If not JSON, convert the text output to a basic structure
2632
1732
  print(f"⚠️ GitIngest output is not in JSON format, converting text to structure")
1733
+
1734
+ # Process the text to extract useful information
1735
+ import re
1736
+
1737
+ # Try to identify language
1738
+ language_match = re.search(r"(?i)language[s]?:?\s*(\w+)", content)
1739
+ detected_language = language_match.group(1) if language_match else "Unknown"
1740
+
1741
+ # Try to identify technologies with stronger evidence requirements
1742
+ tech_patterns = {
1743
+ "python": r"(?i)(python|\.py\b|pip\b|requirements\.txt|setup\.py)",
1744
+ "javascript": r"(?i)(javascript|\.js\b|node|npm|yarn|package\.json)",
1745
+ "typescript": r"(?i)(typescript|\.ts\b|tsc\b|tsconfig\.json)",
1746
+ "go": r"(?i)(\bgo\b|golang|\.go\b|go\.mod|go\.sum)",
1747
+ "rust": r"(?i)(rust|\.rs\b|cargo|Cargo\.toml)",
1748
+ "java": r"(?i)(java\b|\.java\b|maven|gradle|pom\.xml)",
1749
+ "c++": r"(?i)(c\+\+|\.cpp\b|\.hpp\b|cmake\b|CMakeLists\.txt)",
1750
+ "pytorch": r"(?i)(pytorch|torch\b|nn\.Module)",
1751
+ "tensorflow": r"(?i)(tensorflow|tf\.|keras\b)",
1752
+ }
1753
+
1754
+ # Count occurrences to filter out false positives
1755
+ tech_counts = {}
1756
+ for tech, pattern in tech_patterns.items():
1757
+ matches = re.findall(pattern, content)
1758
+ if matches:
1759
+ tech_counts[tech] = len(matches)
1760
+
1761
+ # Filter technologies based on threshold
1762
+ thresholds = {
1763
+ "javascript": 3, # Higher threshold for JavaScript
1764
+ "go": 3, # Higher threshold for Go
1765
+ "default": 2 # Default threshold
1766
+ }
1767
+
1768
+ detected_technologies = []
1769
+ for tech, count in tech_counts.items():
1770
+ threshold = thresholds.get(tech, thresholds["default"])
1771
+ if count >= threshold:
1772
+ detected_technologies.append(tech)
1773
+ print(f"📊 Detected {tech} with confidence score {count}")
1774
+
1775
+ # Create a structured representation
2633
1776
  gitingest_data = {
2634
1777
  "system_info": {
2635
- "detected_language": "Unknown",
2636
- "detected_technologies": [],
1778
+ "detected_language": detected_language,
1779
+ "detected_technologies": detected_technologies,
2637
1780
  },
2638
1781
  "repository_analysis": {
2639
1782
  "summary": content[:5000], # First 5000 chars as summary
@@ -2641,6 +1784,12 @@ def fetch_setup_commands_from_api(repo_url):
2641
1784
  },
2642
1785
  "success": True
2643
1786
  }
1787
+
1788
+ # Save the processed data
1789
+ processed_file = os.path.join(save_dir, f"gitingest_processed_{repo_name}_{timestamp}.json")
1790
+ with open(processed_file, 'w', encoding='utf-8') as proc_f:
1791
+ json.dump(gitingest_data, proc_f, indent=2)
1792
+ print(f"📁 Processed GitIngest data saved to: {processed_file}")
2644
1793
  except FileNotFoundError:
2645
1794
  print(f"⚠️ Output file not found at {output_file}")
2646
1795
  gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
@@ -2660,17 +1809,46 @@ def fetch_setup_commands_from_api(repo_url):
2660
1809
 
2661
1810
  print(f"📤 API Request payload prepared (GitIngest data size: {len(json.dumps(gitingest_data))} bytes)")
2662
1811
 
2663
- # Make the API request
2664
- print(f"🌐 Making POST request to: {api_url}")
2665
- try:
2666
- response = requests.post(api_url, json=payload, timeout=60)
1812
+ # Try each endpoint in sequence until one succeeds
1813
+ response = None
1814
+ for api_url in api_endpoints:
1815
+ # Use the retry mechanism for more reliable requests
1816
+ response = make_api_request_with_retry(
1817
+ url=api_url,
1818
+ payload=payload,
1819
+ max_retries=2,
1820
+ timeout=180 # 3 minute timeout
1821
+ )
1822
+
1823
+ # If we got a response and it's successful, break out of the loop
1824
+ if response and response.status_code == 200:
1825
+ print(f"✅ Successful response from {api_url}")
1826
+ break
1827
+
1828
+ if response:
1829
+ print(f"⚠️ Endpoint {api_url} returned status code {response.status_code}, trying next endpoint...")
1830
+ else:
1831
+ print(f"⚠️ Failed to connect to {api_url}, trying next endpoint...")
1832
+
1833
+ # If we've tried all endpoints and still don't have a response, use fallback
1834
+ if response is None:
1835
+ print("❌ All API endpoints failed")
1836
+ return generate_fallback_commands(gitingest_data)
1837
+
1838
+ # Continue with the response we got from the successful endpoint
1839
+ if not response:
1840
+ print("❌ No valid response received from any endpoint")
1841
+ return generate_fallback_commands(gitingest_data)
2667
1842
 
1843
+ try:
2668
1844
  print(f"📥 API Response status code: {response.status_code}")
2669
1845
 
2670
1846
  if response.status_code == 200:
2671
1847
  try:
2672
1848
  data = response.json()
2673
1849
  print(f"📄 API Response data received")
1850
+ print(f"📄 Response size: {len(response.text)} bytes")
1851
+ print(f"📄 Response URL: {response.url}")
2674
1852
 
2675
1853
  # Extract setup commands from the response
2676
1854
  if "setupInstructions" in data and "commands" in data["setupInstructions"]:
@@ -2722,16 +1900,13 @@ def fetch_setup_commands_from_api(repo_url):
2722
1900
  return generate_fallback_commands(gitingest_data)
2723
1901
  else:
2724
1902
  print(f"❌ API request failed with status code: {response.status_code}")
2725
- print(f"Error response: {response.text[:500]}...")
1903
+ print(f" Response URL: {response.url}")
1904
+ print(f"❌ Response headers: {dict(response.headers)}")
1905
+ print(f"❌ Error response: {response.text[:500]}...")
2726
1906
  # Return fallback commands
2727
1907
  return generate_fallback_commands(gitingest_data)
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}")
1908
+ except Exception as e:
1909
+ print(f"❌ Error processing API response: {str(e)}")
2735
1910
  print("⚠️ Using fallback commands instead")
2736
1911
  # Return fallback commands
2737
1912
  return generate_fallback_commands(gitingest_data)
@@ -2954,232 +2129,73 @@ def generate_basic_repo_analysis(repo_dir):
2954
2129
  # Detect package managers
2955
2130
  package_managers = []
2956
2131
  package_files = {
2957
- 'requirements.txt': 'pip',
2958
- 'setup.py': 'pip',
2959
- 'pyproject.toml': 'pip',
2960
- 'package.json': 'npm',
2961
- 'yarn.lock': 'yarn',
2962
- 'pnpm-lock.yaml': 'pnpm',
2963
- 'Cargo.toml': 'cargo',
2964
- 'go.mod': 'go',
2965
- 'Gemfile': 'bundler',
2966
- 'pom.xml': 'maven',
2967
- 'build.gradle': 'gradle',
2968
- 'composer.json': 'composer'
2969
- }
2970
-
2971
- for file, manager in package_files.items():
2972
- if os.path.exists(os.path.join(repo_dir, file)):
2973
- package_managers.append(manager)
2974
-
2975
- primary_package_manager = package_managers[0] if package_managers else "Unknown"
2976
-
2977
- # Get README content
2978
- readme_content = ""
2979
- for readme_name in ['README.md', 'README', 'README.txt', 'readme.md']:
2980
- readme_path = os.path.join(repo_dir, readme_name)
2981
- if os.path.exists(readme_path):
2982
- with open(readme_path, 'r', encoding='utf-8', errors='ignore') as f:
2983
- readme_content = f.read()
2984
- break
2985
-
2986
- # Try to get repository info
2987
- repo_info = {}
2988
- try:
2989
- # Get remote origin URL
2990
- cmd = ["git", "config", "--get", "remote.origin.url"]
2991
- result = subprocess.run(cmd, cwd=repo_dir, capture_output=True, text=True)
2992
- if result.returncode == 0:
2993
- repo_info["url"] = result.stdout.strip()
2994
-
2995
- # Get commit count as a proxy for activity
2996
- cmd = ["git", "rev-list", "--count", "HEAD"]
2997
- result = subprocess.run(cmd, cwd=repo_dir, capture_output=True, text=True)
2998
- if result.returncode == 0:
2999
- repo_info["commit_count"] = int(result.stdout.strip())
3000
- except Exception:
3001
- pass
3002
-
3003
- # Build the analysis data
3004
- return {
3005
- "system_info": {
3006
- "platform": "linux", # Assuming Linux for container environment
3007
- "python_version": "3.10", # Common Python version
3008
- "detected_language": primary_language,
3009
- "detected_technologies": list(language_counts.keys()),
3010
- "file_count": file_count,
3011
- "repo_stars": repo_info.get("stars", 0),
3012
- "repo_forks": repo_info.get("forks", 0),
3013
- "primary_package_manager": primary_package_manager,
3014
- "complexity_level": "medium" # Default assumption
3015
- },
3016
- "repository_analysis": {
3017
- "summary": f"Repository analysis for {repo_dir}",
3018
- "readme_content": readme_content[:5000] if readme_content else "No README found",
3019
- "package_managers": package_managers,
3020
- "file_extensions": list(file_extensions.keys())
3021
- },
3022
- "success": True
3023
- }
3024
-
3025
- def get_setup_commands_from_local_api(repo_url, gitingest_data):
3026
- """Try to get setup commands from the local API."""
3027
- api_url = "http://localhost:3000/api/analyze-with-gitingest"
3028
-
3029
- # Prepare the request payload
3030
- payload = {
3031
- "repoUrl": repo_url,
3032
- "gitingestData": gitingest_data,
3033
- "userRequest": "Setup and run the repository"
3034
- }
3035
-
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)
3040
-
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}")
3063
-
3064
- return None
3065
-
3066
- # Define a function to create and return a properly configured ssh container function
3067
- def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=None, volume_mount_path="/persistent"):
3068
- # Create a new app for this specific container
3069
- app_name = f"ssh-container-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
3070
- ssh_app = modal.App.lookup(app_name, create_if_missing=True)
3071
-
3072
- # Create SSH-enabled image
3073
- ssh_image = (
3074
- modal.Image.debian_slim()
3075
- .apt_install(
3076
- "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
3077
- "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
3078
- "gpg", "ca-certificates", "software-properties-common"
3079
- )
3080
- .pip_install("uv", "modal") # Fast Python package installer and Modal
3081
- .run_commands(
3082
- # Create SSH directory
3083
- "mkdir -p /var/run/sshd",
3084
- "mkdir -p /root/.ssh",
3085
- "chmod 700 /root/.ssh",
3086
-
3087
- # Configure SSH server
3088
- "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
3089
- "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
3090
- "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
3091
-
3092
- # SSH keep-alive settings
3093
- "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
3094
- "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
3095
-
3096
- # Generate SSH host keys
3097
- "ssh-keygen -A",
3098
-
3099
- # Set up a nice bash prompt
3100
- "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
3101
- )
3102
- )
3103
-
3104
- # Setup volume mount if available
3105
- volumes = {}
3106
- if volume:
3107
- volumes[volume_mount_path] = volume
3108
-
3109
- # Define the function with the specific configuration
3110
- @ssh_app.function(
3111
- image=ssh_image,
3112
- timeout=timeout_minutes * 60, # Convert to seconds
3113
- gpu=gpu_type,
3114
- cpu=2,
3115
- memory=8192,
3116
- serialized=True,
3117
- volumes=volumes if volumes else None,
3118
- )
3119
- def ssh_container(ssh_password, repo_url=None, repo_name=None, setup_commands=None):
3120
- import subprocess
3121
- import time
3122
- import os
3123
-
3124
- # Set root password
3125
- subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
3126
-
3127
- # Start SSH service
3128
- subprocess.run(["service", "ssh", "start"], check=True)
3129
-
3130
- # Setup environment
3131
- os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
3132
-
3133
- # Clone repository if provided
3134
- if repo_url:
3135
- repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
3136
- print(f"📥 Cloning repository: {repo_url}")
3137
-
3138
- try:
3139
- subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
3140
- print(f"✅ Repository cloned successfully: {repo_name_from_url}")
3141
-
3142
- # Change to repository directory
3143
- repo_dir = f"/root/{repo_name_from_url}"
3144
- if os.path.exists(repo_dir):
3145
- os.chdir(repo_dir)
3146
- print(f"📂 Changed to repository directory: {repo_dir}")
3147
-
3148
- except subprocess.CalledProcessError as e:
3149
- print(f"❌ Failed to clone repository: {e}")
3150
-
3151
- # Run setup commands if provided
3152
- if setup_commands:
3153
- print(f"⚙️ Running {len(setup_commands)} setup commands...")
3154
- for i, cmd in enumerate(setup_commands, 1):
3155
- print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
3156
- try:
3157
- result = subprocess.run(cmd, shell=True, check=True,
3158
- capture_output=True, text=True)
3159
- if result.stdout:
3160
- print(f"✅ Output: {result.stdout}")
3161
- except subprocess.CalledProcessError as e:
3162
- print(f"❌ Command failed: {e}")
3163
- if e.stderr:
3164
- print(f"❌ Error: {e.stderr}")
3165
-
3166
- # Get container info
3167
- print("🔍 Container started successfully!")
3168
- print(f"🆔 Container ID: {os.environ.get('MODAL_TASK_ID', 'unknown')}")
2132
+ 'requirements.txt': 'pip',
2133
+ 'setup.py': 'pip',
2134
+ 'pyproject.toml': 'pip',
2135
+ 'package.json': 'npm',
2136
+ 'yarn.lock': 'yarn',
2137
+ 'pnpm-lock.yaml': 'pnpm',
2138
+ 'Cargo.toml': 'cargo',
2139
+ 'go.mod': 'go',
2140
+ 'Gemfile': 'bundler',
2141
+ 'pom.xml': 'maven',
2142
+ 'build.gradle': 'gradle',
2143
+ 'composer.json': 'composer'
2144
+ }
2145
+
2146
+ for file, manager in package_files.items():
2147
+ if os.path.exists(os.path.join(repo_dir, file)):
2148
+ package_managers.append(manager)
2149
+
2150
+ primary_package_manager = package_managers[0] if package_managers else "Unknown"
2151
+
2152
+ # Get README content
2153
+ readme_content = ""
2154
+ for readme_name in ['README.md', 'README', 'README.txt', 'readme.md']:
2155
+ readme_path = os.path.join(repo_dir, readme_name)
2156
+ if os.path.exists(readme_path):
2157
+ with open(readme_path, 'r', encoding='utf-8', errors='ignore') as f:
2158
+ readme_content = f.read()
2159
+ break
2160
+
2161
+ # Try to get repository info
2162
+ repo_info = {}
2163
+ try:
2164
+ # Get remote origin URL
2165
+ cmd = ["git", "config", "--get", "remote.origin.url"]
2166
+ result = subprocess.run(cmd, cwd=repo_dir, capture_output=True, text=True)
2167
+ if result.returncode == 0:
2168
+ repo_info["url"] = result.stdout.strip()
3169
2169
 
3170
- # Keep the container running
3171
- while True:
3172
- time.sleep(30)
3173
- # Check if SSH service is still running
3174
- try:
3175
- subprocess.run(["service", "ssh", "status"], check=True,
3176
- capture_output=True)
3177
- except subprocess.CalledProcessError:
3178
- print("⚠️ SSH service stopped, restarting...")
3179
- subprocess.run(["service", "ssh", "start"], check=True)
3180
-
3181
- # Return the configured function
3182
- return ssh_container, app_name
2170
+ # Get commit count as a proxy for activity
2171
+ cmd = ["git", "rev-list", "--count", "HEAD"]
2172
+ result = subprocess.run(cmd, cwd=repo_dir, capture_output=True, text=True)
2173
+ if result.returncode == 0:
2174
+ repo_info["commit_count"] = int(result.stdout.strip())
2175
+ except Exception:
2176
+ pass
2177
+
2178
+ # Build the analysis data
2179
+ return {
2180
+ "system_info": {
2181
+ "platform": "linux", # Assuming Linux for container environment
2182
+ "python_version": "3.10", # Common Python version
2183
+ "detected_language": primary_language,
2184
+ "detected_technologies": list(language_counts.keys()),
2185
+ "file_count": file_count,
2186
+ "repo_stars": repo_info.get("stars", 0),
2187
+ "repo_forks": repo_info.get("forks", 0),
2188
+ "primary_package_manager": primary_package_manager,
2189
+ "complexity_level": "medium" # Default assumption
2190
+ },
2191
+ "repository_analysis": {
2192
+ "summary": f"Repository analysis for {repo_dir}",
2193
+ "readme_content": readme_content[:5000] if readme_content else "No README found",
2194
+ "package_managers": package_managers,
2195
+ "file_extensions": list(file_extensions.keys())
2196
+ },
2197
+ "success": True
2198
+ }
3183
2199
 
3184
2200
  def fix_setup_commands(commands):
3185
2201
  """Fix setup commands by removing placeholders and comments."""
@@ -3233,140 +2249,431 @@ def find_entry_point(repo_dir):
3233
2249
 
3234
2250
  return None
3235
2251
 
3236
- def analyze_directory_navigation_with_llm(current_dir, target_dir, current_contents, target_contents, api_key=None):
3237
- """Use LLM to analyze if directory navigation makes sense"""
3238
- if not api_key:
3239
- # Try to get API key from environment
3240
- api_key = os.environ.get("OPENAI_API_KEY")
3241
-
3242
- if not api_key:
3243
- print("⚠️ No OpenAI API key available for directory analysis")
3244
- return None
3245
-
3246
- # Create analysis prompt
3247
- analysis_prompt = f"""
3248
- I'm trying to determine if a 'cd {target_dir}' command makes sense.
3249
-
3250
- CURRENT DIRECTORY: {current_dir}
3251
- Current directory contents:
3252
- {current_contents}
3253
-
3254
- TARGET DIRECTORY: {target_dir}
3255
- Target directory contents:
3256
- {target_contents}
3257
-
3258
- Please analyze if navigating to the target directory makes sense by considering:
3259
- 1. Are the contents significantly different?
3260
- 2. Does the target directory contain important files (like source code, config files, etc.)?
3261
- 3. Is this likely a nested project directory or just a duplicate?
3262
- 4. Would navigating provide access to different functionality or files?
3263
-
3264
- Respond with only 'NAVIGATE' if navigation makes sense, or 'SKIP' if it's redundant.
3265
- """
3266
-
3267
- # Prepare the API request
3268
- headers = {
3269
- "Content-Type": "application/json",
3270
- "Authorization": f"Bearer {api_key}"
3271
- }
3272
-
3273
- payload = {
3274
- "model": "gpt-4",
3275
- "messages": [
3276
- {"role": "system", "content": "You are a directory navigation assistant. Analyze if navigating to a target directory makes sense based on the contents of both directories. Respond with only 'NAVIGATE' or 'SKIP'."},
3277
- {"role": "user", "content": analysis_prompt}
3278
- ],
3279
- "temperature": 0.1,
3280
- "max_tokens": 50
3281
- }
3282
-
3283
- try:
3284
- print("🤖 Calling OpenAI for directory navigation analysis...")
3285
- response = requests.post(
3286
- "https://api.openai.com/v1/chat/completions",
3287
- headers=headers,
3288
- json=payload,
3289
- timeout=30
3290
- )
3291
-
3292
- if response.status_code == 200:
3293
- result = response.json()
3294
- llm_response = result["choices"][0]["message"]["content"].strip()
3295
- print(f"🤖 LLM Response: {llm_response}")
3296
- return llm_response
3297
- else:
3298
- print(f"❌ OpenAI API error: {response.status_code} - {response.text}")
3299
- return None
3300
- except Exception as e:
3301
- print(f"❌ Error calling OpenAI API: {e}")
3302
- return None
3303
-
3304
2252
  def cleanup_modal_token():
3305
- """Delete Modal token files and environment variables after SSH container is started"""
3306
- print("🧹 Cleaning up Modal token for security...")
2253
+ """Delete token files and environment variables after SSH container is started"""
2254
+ print("🧹 Cleaning up tokens for security...")
3307
2255
 
3308
2256
  try:
3309
2257
  # Remove token from environment variables
3310
2258
  if "MODAL_TOKEN_ID" in os.environ:
3311
2259
  del os.environ["MODAL_TOKEN_ID"]
3312
- print("✅ Removed MODAL_TOKEN_ID from environment")
2260
+ # print("✅ Removed token ID from environment")
3313
2261
 
3314
2262
  if "MODAL_TOKEN" in os.environ:
3315
2263
  del os.environ["MODAL_TOKEN"]
3316
- print("✅ Removed MODAL_TOKEN from environment")
2264
+ # print("✅ Removed token from environment")
3317
2265
 
3318
2266
  if "MODAL_TOKEN_SECRET" in os.environ:
3319
2267
  del os.environ["MODAL_TOKEN_SECRET"]
3320
- print("✅ Removed MODAL_TOKEN_SECRET from environment")
2268
+ # print("✅ Removed token secret from environment")
3321
2269
 
3322
2270
  # Delete ~/.modal.toml file
3323
2271
  home_dir = os.path.expanduser("~")
3324
2272
  modal_toml = os.path.join(home_dir, ".modal.toml")
3325
2273
  if os.path.exists(modal_toml):
3326
2274
  os.remove(modal_toml)
3327
- print(f"✅ Deleted Modal token file at {modal_toml}")
2275
+ # print(f"✅ Deleted token file at {modal_toml}")
3328
2276
 
3329
- print("✅ Modal token cleanup completed successfully")
2277
+ # print("✅ Token cleanup completed successfully")
3330
2278
  except Exception as e:
3331
- print(f"❌ Error during Modal token cleanup: {e}")
2279
+ print(f"❌ Error during token cleanup: {e}")
3332
2280
 
3333
2281
  def show_usage_examples():
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")
2282
+ """Display usage examples for the script."""
2283
+ print("Usage Examples\n")
2284
+
2285
+ print("Basic Container Creation")
2286
+ print("┌────────────────────────────────────────────────────────────────────────┐")
2287
+ print(" gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git ")
2288
+ print("└────────────────────────────────────────────────────────────────────────┘\n")
2289
+
2290
+ print("With Setup Commands")
2291
+ print("┌────────────────────────────────────────────────────────────────────────────────────────────────────┐")
2292
+ print(" gitarsenal --gpu A100 --repo-url https://github.com/username/repo.git \\ ")
2293
+ print("--setup-commands \"pip install -r requirements.txt\" \"python setup.py install\" ")
2294
+ print("└────────────────────────────────────────────────────────────────────────────────────────────────────┘\n")
2295
+
2296
+ print("With Persistent Storage")
2297
+ print("┌────────────────────────────────────────────────────────────────────────────────────┐")
2298
+ print(" gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\ ")
2299
+ print("--volume-name my-persistent-volume ")
2300
+ print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
2301
+
2302
+ print("With GitIngest API (default)")
2303
+ print("┌────────────────────────────────────────────────────────────────────────────────────┐")
2304
+ print(" gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git │")
2305
+ print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
2306
+
2307
+ print("Without GitIngest API")
2308
+ print("┌────────────────────────────────────────────────────────────────────────────────────┐")
2309
+ print("│ gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\ │")
2310
+ print("│ --no-gitingest │")
2311
+ print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
2312
+
2313
+ print("With Original API")
2314
+ print("┌────────────────────────────────────────────────────────────────────────────────────┐")
2315
+ print("│ gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\ │")
2316
+ print("│ --use-api │")
2317
+ print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
2318
+
2319
+ print("Available GPU Options:")
3360
2320
  print(" T4, L4, A10G, A100-40GB, A100-80GB, L40S, H100, H200, B200")
3361
- print("")
3362
2321
 
2322
+ def make_api_request_with_retry(url, payload, max_retries=2, timeout=180):
2323
+ """Make an API request with retry mechanism."""
2324
+ import requests
2325
+ import time
2326
+
2327
+ for attempt in range(max_retries + 1):
2328
+ try:
2329
+ if attempt > 0:
2330
+ print(f"🔄 Retry attempt {attempt}/{max_retries}...")
2331
+
2332
+ print(f"🌐 Making POST request to: {url}")
2333
+ print(f"⏳ Waiting up to {timeout//60} minutes for response...")
2334
+
2335
+ # Set allow_redirects=True to follow redirects automatically
2336
+ response = requests.post(
2337
+ url,
2338
+ json=payload,
2339
+ timeout=timeout,
2340
+ allow_redirects=True,
2341
+ headers={
2342
+ 'Content-Type': 'application/json',
2343
+ 'User-Agent': 'GitArsenal-CLI/1.0'
2344
+ }
2345
+ )
2346
+
2347
+ # Print redirect info if any
2348
+ if response.history:
2349
+ print(f"✅ Request was redirected {len(response.history)} times")
2350
+ for resp in response.history:
2351
+ print(f" - Redirect: {resp.status_code} from {resp.url}")
2352
+ print(f"✅ Final URL: {response.url}")
2353
+
2354
+ return response
2355
+ except requests.exceptions.RequestException as e:
2356
+ if attempt < max_retries:
2357
+ retry_delay = 2 ** attempt # Exponential backoff
2358
+ print(f"⚠️ Request failed: {str(e)}")
2359
+ print(f"⏳ Waiting {retry_delay} seconds before retrying...")
2360
+ time.sleep(retry_delay)
2361
+ else:
2362
+ print(f"❌ All retry attempts failed: {str(e)}")
2363
+ return None
2364
+
2365
+ return None
2366
+
2367
+ def get_setup_commands_from_gitingest(repo_url):
2368
+ """
2369
+ Get repository setup commands using the gitingest approach.
2370
+
2371
+ This function is inspired by gitingest_setup_client.py and provides a more
2372
+ robust way to get setup commands for a repository.
2373
+
2374
+ Args:
2375
+ repo_url: URL of the repository to set up
2376
+
2377
+ Returns:
2378
+ List of setup commands or None if failed
2379
+ """
2380
+ import requests
2381
+ import json
2382
+ import os
2383
+ import sys
2384
+ import tempfile
2385
+ import subprocess
2386
+
2387
+ print(f"🔍 Getting setup commands for repository: {repo_url}")
2388
+
2389
+ # Define API endpoints to try in order
2390
+ api_endpoints = [
2391
+ "https://www.gitarsenal.dev/api/gitingest-setup-commands",
2392
+ "https://gitarsenal.dev/api/gitingest-setup-commands",
2393
+ "https://www.gitarsenal.dev/api/analyze-with-gitingest",
2394
+ "http://localhost:3000/api/gitingest-setup-commands"
2395
+ ]
2396
+
2397
+ # Generate basic gitingest data
2398
+ def generate_basic_gitingest_data():
2399
+ # Extract repo name from URL
2400
+ repo_name = repo_url.split('/')[-1].replace('.git', '')
2401
+
2402
+ return {
2403
+ "system_info": {
2404
+ "platform": "Unknown",
2405
+ "python_version": "Unknown",
2406
+ "detected_language": "Unknown",
2407
+ "detected_technologies": [],
2408
+ "file_count": 0,
2409
+ "repo_stars": 0,
2410
+ "repo_forks": 0,
2411
+ "primary_package_manager": "Unknown",
2412
+ "complexity_level": "Unknown"
2413
+ },
2414
+ "repository_analysis": {
2415
+ "summary": f"Repository: {repo_name}",
2416
+ "tree": "",
2417
+ "content_preview": ""
2418
+ },
2419
+ "success": True
2420
+ }
2421
+
2422
+ # Try to generate gitingest data using CLI if available
2423
+ def generate_gitingest_data_from_cli():
2424
+ try:
2425
+ # Check if gitingest CLI is available
2426
+ subprocess.run(["gitingest", "--help"], check=True, capture_output=True, text=True)
2427
+
2428
+ # Create a temporary file for the output
2429
+ with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
2430
+ output_file = tmp.name
2431
+
2432
+ # Run gitingest command
2433
+ print(f"Running gitingest analysis on {repo_url}...")
2434
+ gitingest_cmd = ["gitingest", repo_url, "-o", output_file]
2435
+ result = subprocess.run(gitingest_cmd, capture_output=True, text=True)
2436
+
2437
+ if result.returncode != 0:
2438
+ print(f"GitIngest CLI failed: {result.stderr}")
2439
+ return None
2440
+
2441
+ # Read the output file
2442
+ try:
2443
+ with open(output_file, 'r', encoding='utf-8') as f:
2444
+ content = f.read()
2445
+ try:
2446
+ data = json.loads(content)
2447
+ return data
2448
+ except json.JSONDecodeError:
2449
+ # If not JSON, convert the text output to a basic structure
2450
+ return {
2451
+ "system_info": {
2452
+ "platform": "Unknown",
2453
+ "python_version": "Unknown",
2454
+ "detected_language": "Unknown",
2455
+ "detected_technologies": []
2456
+ },
2457
+ "repository_analysis": {
2458
+ "summary": content[:5000], # First 5000 chars as summary
2459
+ "tree": "",
2460
+ "content_preview": content[:10000] # First 10000 chars as preview
2461
+ },
2462
+ "success": True
2463
+ }
2464
+ except Exception as e:
2465
+ print(f"Error reading gitingest output: {e}")
2466
+ return None
2467
+ finally:
2468
+ # Clean up the temporary file
2469
+ if os.path.exists(output_file):
2470
+ os.unlink(output_file)
2471
+
2472
+ except (subprocess.SubprocessError, FileNotFoundError):
2473
+ print("GitIngest CLI not found")
2474
+ return None
2475
+
2476
+ # First try to get data from CLI
2477
+ gitingest_data = generate_gitingest_data_from_cli()
2478
+
2479
+ # If CLI failed, use basic data
2480
+ if not gitingest_data:
2481
+ print("Using basic gitingest data")
2482
+ gitingest_data = generate_basic_gitingest_data()
2483
+
2484
+ # Try each API endpoint
2485
+ for api_url in api_endpoints:
2486
+ try:
2487
+ print(f"Trying API endpoint: {api_url}")
2488
+
2489
+ payload = {
2490
+ "repoUrl": repo_url,
2491
+ "gitingestData": gitingest_data
2492
+ }
2493
+
2494
+ # Use the retry mechanism for more reliable requests
2495
+ response = make_api_request_with_retry(
2496
+ url=api_url,
2497
+ payload=payload,
2498
+ max_retries=2,
2499
+ timeout=180 # 3 minute timeout
2500
+ )
2501
+
2502
+ if not response:
2503
+ print(f"Failed to connect to {api_url}")
2504
+ continue
2505
+
2506
+ if response.status_code != 200:
2507
+ print(f"API request failed with status code: {response.status_code}")
2508
+ continue
2509
+
2510
+ try:
2511
+ result = response.json()
2512
+
2513
+ # Check if we have commands in the response
2514
+ commands = None
2515
+
2516
+ # Check for different response formats
2517
+ if "commands" in result:
2518
+ commands = result["commands"]
2519
+ elif "setupInstructions" in result and "commands" in result["setupInstructions"]:
2520
+ commands = result["setupInstructions"]["commands"]
2521
+
2522
+ if commands:
2523
+ print(f"✅ Successfully fetched {len(commands)} setup commands from API at {api_url}")
2524
+
2525
+ # Print the commands
2526
+ print("\n📋 Setup Commands:")
2527
+ for i, cmd in enumerate(commands, 1):
2528
+ print(f" {i}. {cmd}")
2529
+
2530
+ # Fix the commands
2531
+ fixed_commands = fix_setup_commands(commands)
2532
+
2533
+ # Print the fixed commands
2534
+ print("\n📋 Fixed commands:")
2535
+ for i, cmd in enumerate(fixed_commands, 1):
2536
+ print(f" {i}. {cmd}")
2537
+
2538
+ return fixed_commands
2539
+ else:
2540
+ print("No commands found in API response")
2541
+ except json.JSONDecodeError:
2542
+ print(f"Failed to parse API response as JSON")
2543
+ except Exception as e:
2544
+ print(f"Error with API endpoint {api_url}: {e}")
2545
+
2546
+ print("❌ All API endpoints failed")
2547
+ return generate_fallback_commands(gitingest_data)
2548
+
2549
+ def prompt_for_gpu():
2550
+ """
2551
+ Prompt the user to select a GPU type from available options using arrow keys.
2552
+ Returns the selected GPU type.
2553
+ """
2554
+ import sys
2555
+ import tty
2556
+ import termios
2557
+
2558
+ # Define available GPU types and their specifications
2559
+ gpu_specs = {
2560
+ 'T4': {'gpu': 'T4', 'memory': '16GB'},
2561
+ 'L4': {'gpu': 'L4', 'memory': '24GB'},
2562
+ 'A10G': {'gpu': 'A10G', 'memory': '24GB'},
2563
+ 'A100-40': {'gpu': 'A100-40GB', 'memory': '40GB'},
2564
+ 'A100-80': {'gpu': 'A100-80GB', 'memory': '80GB'},
2565
+ 'L40S': {'gpu': 'L40S', 'memory': '48GB'},
2566
+ 'H100': {'gpu': 'H100', 'memory': '80GB'},
2567
+ 'H200': {'gpu': 'H200', 'memory': '141GB'},
2568
+ 'B200': {'gpu': 'B200', 'memory': '141GB'}
2569
+ }
2570
+
2571
+ # Create a list of options
2572
+ options = list(gpu_specs.keys())
2573
+ selected_index = 2 # Default to A10G (index 2)
2574
+
2575
+ def get_key():
2576
+ """Get a single keypress from the user."""
2577
+ fd = sys.stdin.fileno()
2578
+ old_settings = termios.tcgetattr(fd)
2579
+ try:
2580
+ tty.setraw(sys.stdin.fileno())
2581
+ ch = sys.stdin.read(1)
2582
+ if ch == '\x1b': # Escape sequence
2583
+ ch2 = sys.stdin.read(1)
2584
+ if ch2 == '[':
2585
+ ch3 = sys.stdin.read(1)
2586
+ if ch3 == 'A':
2587
+ return 'UP'
2588
+ elif ch3 == 'B':
2589
+ return 'DOWN'
2590
+ elif ch == '\r' or ch == '\n':
2591
+ return 'ENTER'
2592
+ elif ch == '\x03': # Ctrl+C
2593
+ return 'CTRL_C'
2594
+ return ch
2595
+ finally:
2596
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
2597
+
2598
+ def display_menu():
2599
+ """Display the GPU selection menu with current selection highlighted."""
2600
+ print("\n📊 Available GPU Options:")
2601
+ print("┌──────────────┬─────────┐")
2602
+ print("│ GPU Type │ VRAM │")
2603
+ print("├──────────────┼─────────┤")
2604
+
2605
+ for i, gpu_type in enumerate(options):
2606
+ specs = gpu_specs[gpu_type]
2607
+ # Calculate proper spacing for alignment
2608
+ number_part = f"{i+1}."
2609
+ if i == selected_index:
2610
+ prefix = "> "
2611
+ suffix = " ←"
2612
+ else:
2613
+ prefix = " "
2614
+ suffix = ""
2615
+
2616
+ # Ensure consistent width for GPU type column
2617
+ gpu_display = f"{prefix}{number_part} {gpu_type}"
2618
+ gpu_padded = f"{gpu_display:<12}" # Fixed width for GPU column
2619
+
2620
+ print(f"│ {gpu_padded} │ {specs['memory']:<7} │{suffix}")
2621
+
2622
+ print("└──────────────┴─────────┘")
2623
+ print("Use ↑/↓ arrows to select, Enter to confirm, Ctrl+C to cancel")
2624
+
2625
+ # Clear screen and show initial menu
2626
+ print("\033[2J\033[H", end="") # Clear screen and move cursor to top
2627
+ display_menu()
2628
+
2629
+ while True:
2630
+ try:
2631
+ key = get_key()
2632
+
2633
+ if key == 'UP':
2634
+ selected_index = (selected_index - 1) % len(options)
2635
+ print("\033[2J\033[H", end="") # Clear screen
2636
+ display_menu()
2637
+ elif key == 'DOWN':
2638
+ selected_index = (selected_index + 1) % len(options)
2639
+ print("\033[2J\033[H", end="") # Clear screen
2640
+ display_menu()
2641
+ elif key == 'ENTER':
2642
+ selected_gpu = options[selected_index]
2643
+ print(f"\n✅ Selected GPU: {selected_gpu}")
2644
+ return selected_gpu
2645
+ elif key == 'CTRL_C':
2646
+ print("\n🛑 Selection cancelled.")
2647
+ sys.exit(1)
2648
+
2649
+ except KeyboardInterrupt:
2650
+ print("\n🛑 Selection cancelled.")
2651
+ sys.exit(1)
2652
+ except Exception as e:
2653
+ print(f"\n❌ Error: {e}")
2654
+ # Fall back to simple input method
2655
+ try:
2656
+ choice = input("\n🔍 Select GPU type (number or name, default is A10G): ").strip()
2657
+ if not choice:
2658
+ return "A10G"
2659
+ if choice.isdigit():
2660
+ index = int(choice) - 1
2661
+ if 0 <= index < len(options):
2662
+ return options[index]
2663
+ elif choice in options:
2664
+ return choice
2665
+ return "A10G"
2666
+ except:
2667
+ return "A10G"
2668
+
2669
+ # Replace the existing GPU argument parsing in the main section
3363
2670
  if __name__ == "__main__":
3364
2671
  # Parse command line arguments when script is run directly
3365
2672
  import argparse
3366
2673
  import sys
3367
2674
 
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)')
2675
+ parser = argparse.ArgumentParser()
2676
+ parser.add_argument('--gpu', type=str, help='GPU type (e.g., A10G, T4, A100-80GB)')
3370
2677
  parser.add_argument('--repo-url', type=str, help='Repository URL to clone')
3371
2678
  parser.add_argument('--repo-name', type=str, help='Repository name override')
3372
2679
  parser.add_argument('--setup-commands', type=str, nargs='+', help='Setup commands to run (deprecated)')
@@ -3377,169 +2684,257 @@ if __name__ == "__main__":
3377
2684
  parser.add_argument('--volume-name', type=str, help='Name of the Modal volume for persistent storage')
3378
2685
  parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
3379
2686
  parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
3380
- parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from API')
3381
- parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
2687
+ parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from original API')
2688
+ parser.add_argument('--use-gitingest', action='store_true', default=True, help='Use gitingest approach to fetch setup commands (default)')
2689
+ parser.add_argument('--no-gitingest', action='store_true', help='Disable gitingest approach for setup commands')
3382
2690
  parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
2691
+ parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
2692
+ parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
3383
2693
 
3384
2694
  args = parser.parse_args()
3385
2695
 
2696
+ # If --list-gpus is specified, just show GPU options and exit
2697
+ if args.list_gpus:
2698
+ prompt_for_gpu()
2699
+ sys.exit(0)
2700
+
3386
2701
  # If no arguments or only --show-examples is provided, show usage examples
3387
2702
  if len(sys.argv) == 1 or args.show_examples:
3388
2703
  show_usage_examples()
3389
2704
  sys.exit(0)
3390
-
3391
- # Get setup commands from file if specified
3392
- setup_commands = args.setup_commands or []
3393
2705
 
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.")
2706
+ # Check for dependencies
2707
+ print("⠏ Checking dependencies...")
2708
+ print("--- Dependency Check ---")
2709
+
2710
+ # Check Python version
2711
+ python_version = sys.version.split()[0]
2712
+ print(f" Python {python_version} found")
2713
+
2714
+ # Check Modal CLI
2715
+ try:
2716
+ subprocess.run(["modal", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2717
+ print("✓ Modal CLI found")
2718
+ except (subprocess.SubprocessError, FileNotFoundError):
2719
+ print("❌ Modal CLI not found. Please install with: pip install modal")
2720
+
2721
+ # Check Gitingest CLI
2722
+ try:
2723
+ subprocess.run(["gitingest", "--help"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2724
+ print("✓ Gitingest CLI found")
2725
+ except (subprocess.SubprocessError, FileNotFoundError):
2726
+ print("⚠️ Gitingest CLI not found (optional)")
2727
+
2728
+ # Check Git
2729
+ try:
2730
+ subprocess.run(["git", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2731
+ print("✓ Git found")
2732
+ except (subprocess.SubprocessError, FileNotFoundError):
2733
+ print("❌ Git not found. Please install Git.")
2734
+
2735
+ print("------------------------")
2736
+ print("\n✔ Dependencies checked")
2737
+
2738
+ # Always prompt for GPU selection regardless of interactive mode
2739
+ gpu_type = prompt_for_gpu()
2740
+ args.gpu = gpu_type
2741
+
2742
+ # Display configuration after GPU selection
2743
+ print("\n📋 Container Configuration:")
2744
+ print(f"Repository URL: {args.repo_url or 'Not specified'}")
2745
+ print(f"GPU Type: {gpu_type}")
2746
+ print(f"Volume: {args.volume_name or 'None'}")
2747
+ if args.use_api:
2748
+ print("Setup Commands: Auto-detect from repository")
2749
+ elif args.setup_commands:
2750
+ print(f"Setup Commands: {len(args.setup_commands)} custom commands")
2751
+ else:
2752
+ print("Setup Commands: Auto-detect from repository")
2753
+
2754
+ # Confirm settings
2755
+ try:
2756
+ proceed = input("Proceed with these settings? (Y/n): ").strip().lower()
2757
+ if proceed in ('n', 'no'):
2758
+ print("🛑 Operation cancelled by user.")
2759
+ sys.exit(0)
2760
+ except KeyboardInterrupt:
2761
+ print("\n🛑 Operation cancelled by user.")
2762
+ sys.exit(0)
2763
+
2764
+ # Interactive mode or missing required arguments
2765
+ if args.interactive or not args.repo_url or not args.volume_name:
2766
+ # Get repository URL if not provided
2767
+ repo_url = args.repo_url
2768
+ if not repo_url:
2769
+ try:
2770
+ repo_url = input("? Enter GitHub repository URL: ").strip()
2771
+ if not repo_url:
2772
+ print("❌ Repository URL is required.")
2773
+ sys.exit(1)
2774
+ except KeyboardInterrupt:
2775
+ print("\n🛑 Setup cancelled.")
3401
2776
  sys.exit(1)
3402
2777
 
3403
2778
  # Ask about persistent volume
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
2779
+ volume_name = args.volume_name
2780
+ if not volume_name:
2781
+ try:
2782
+ use_volume = input("? Use persistent volume for faster installs? (Y/n): ").strip().lower()
2783
+ if use_volume in ('', 'y', 'yes'):
2784
+ volume_name = input("? Enter volume name: ").strip()
2785
+ if not volume_name:
2786
+ volume_name = "gitarsenal-volume"
2787
+ print(f"Using default volume name: {volume_name}")
2788
+ except KeyboardInterrupt:
2789
+ print("\n🛑 Setup cancelled.")
2790
+ sys.exit(1)
3412
2791
 
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
2792
+ # Ask about setup commands
2793
+ use_gitingest = args.use_gitingest and not args.no_gitingest
2794
+ if not args.use_api and not args.setup_commands and not args.setup_commands_json:
2795
+ try:
2796
+ auto_detect = input("? Automatically detect setup commands for this repository? (Y/n): ").strip().lower()
2797
+ if auto_detect in ('n', 'no'):
2798
+ use_gitingest = False
2799
+ except KeyboardInterrupt:
2800
+ print("\n🛑 Setup cancelled.")
2801
+ sys.exit(1)
3419
2802
 
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}")
2803
+ # Update args with interactive values
2804
+ args.repo_url = repo_url
2805
+ args.volume_name = volume_name
2806
+ args.use_gitingest = use_gitingest
2807
+
2808
+ try:
2809
+ # Get setup commands from file if specified
2810
+ setup_commands = args.setup_commands or []
2811
+
2812
+ # Use gitingest by default unless --no-gitingest is set
2813
+ if args.repo_url and (args.use_gitingest and not args.no_gitingest):
2814
+ print("🔄 Using gitingest approach to fetch setup commands (default)")
2815
+ api_commands = get_setup_commands_from_gitingest(args.repo_url)
2816
+ if api_commands:
2817
+ setup_commands = api_commands
2818
+ print(f"📋 Using {len(setup_commands)} commands from gitingest API")
2819
+ else:
2820
+ print("⚠️ Failed to get commands from gitingest API")
2821
+ if not args.use_api:
2822
+ print("⚠️ Falling back to basic setup commands")
2823
+ setup_commands = generate_fallback_commands(None)
2824
+ else:
2825
+ setup_commands = []
2826
+ # If --use-api flag is set and repo_url is provided, fetch setup commands from API
2827
+ elif args.use_api and args.repo_url:
2828
+ print("🔄 Using original API to fetch setup commands")
2829
+ api_commands = fetch_setup_commands_from_api(args.repo_url)
2830
+ if api_commands:
2831
+ setup_commands = api_commands
2832
+ print(f"📋 Using {len(setup_commands)} commands from original API")
2833
+ else:
2834
+ print("⚠️ Failed to get commands from API, no fallback commands will be used")
2835
+ # Do not fall back to basic setup commands
2836
+ setup_commands = []
3425
2837
 
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:
2838
+ # Parse setup commands from JSON if provided
2839
+ if args.setup_commands_json:
3430
2840
  try:
3431
- gpu_index = int(gpu_choice) - 1
3432
- if 0 <= gpu_index < len(gpu_options):
3433
- args.gpu = gpu_options[gpu_index]
2841
+ json_commands = json.loads(args.setup_commands_json)
2842
+ if isinstance(json_commands, list):
2843
+ setup_commands = json_commands
2844
+ print(f"📋 Parsed {len(setup_commands)} commands from JSON:")
2845
+ for i, cmd in enumerate(setup_commands, 1):
2846
+ print(f" {i}. {cmd}")
3434
2847
  else:
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()
2848
+ print(f"⚠️ Invalid JSON format for setup commands: not a list")
2849
+ except json.JSONDecodeError as e:
2850
+ print(f"⚠️ Error parsing JSON setup commands: {e}")
2851
+ print(f"Received JSON string: {args.setup_commands_json}")
2852
+
2853
+ # Print received setup commands for debugging
2854
+ if setup_commands:
2855
+ print(f"📋 Using {len(setup_commands)} setup commands:")
2856
+ for i, cmd in enumerate(setup_commands, 1):
2857
+ print(f" {i}. {cmd}")
3493
2858
 
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
2859
+ # Load commands from file if specified
2860
+ if args.commands_file and os.path.exists(args.commands_file):
2861
+ try:
2862
+ with open(args.commands_file, 'r') as f:
2863
+ # Check if the file contains JSON or line-by-line commands
2864
+ content = f.read().strip()
2865
+
2866
+ if content.startswith('[') and content.endswith(']'):
2867
+ # JSON format
2868
+ try:
2869
+ json_commands = json.loads(content)
2870
+ if isinstance(json_commands, list):
2871
+ setup_commands.extend(json_commands)
2872
+ print(f"📋 Loaded {len(json_commands)} commands from JSON file {args.commands_file}")
2873
+ else:
2874
+ print(f"⚠️ Invalid JSON format in commands file: not a list")
2875
+ except json.JSONDecodeError as json_err:
2876
+ print(f"⚠️ Error parsing JSON commands file: {json_err}")
2877
+ # Fall back to line-by-line parsing
2878
+ file_commands = [line.strip() for line in content.split('\n') if line.strip()]
2879
+ setup_commands.extend(file_commands)
2880
+ print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line fallback)")
2881
+ else:
2882
+ # Line-by-line format
3506
2883
  file_commands = [line.strip() for line in content.split('\n') if line.strip()]
3507
2884
  setup_commands.extend(file_commands)
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
3527
- )
2885
+ print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line format)")
2886
+ except Exception as e:
2887
+ print(f"⚠️ Error loading commands from file: {e}")
3528
2888
 
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
-
2889
+ # Load commands from setup script if specified
2890
+ if args.setup_script and os.path.exists(args.setup_script):
2891
+ try:
2892
+ with open(args.setup_script, 'r') as f:
2893
+ script_content = f.read().strip()
2894
+ # Convert script to individual commands
2895
+ script_commands = [line.strip() for line in script_content.split('\n')
2896
+ if line.strip() and not line.strip().startswith('#')]
2897
+ setup_commands.extend(script_commands)
2898
+ print(f"📋 Loaded {len(script_commands)} commands from script {args.setup_script}")
2899
+ except Exception as e:
2900
+ print(f"⚠️ Error loading commands from script: {e}")
2901
+
2902
+ # Create the container with the specified options
2903
+ if args.ssh_password:
2904
+ print(f"🔑 Using provided SSH password")
2905
+ ssh_password = args.ssh_password
2906
+ else:
2907
+ ssh_password = generate_random_password()
2908
+ print(f"🔑 Generated random SSH password: {ssh_password}")
2909
+
2910
+ # Extract repository name from URL if not provided
2911
+ repo_name = args.repo_name
2912
+ if not repo_name and args.repo_url:
2913
+ # Try to extract repo name from URL
2914
+ url_parts = args.repo_url.rstrip('/').split('/')
2915
+ if url_parts:
2916
+ repo_name = url_parts[-1]
2917
+ if repo_name.endswith('.git'):
2918
+ repo_name = repo_name[:-4]
2919
+
2920
+ # Create the container
2921
+ create_modal_ssh_container(
2922
+ gpu_type=args.gpu,
2923
+ repo_url=args.repo_url,
2924
+ repo_name=repo_name,
2925
+ setup_commands=setup_commands,
2926
+ volume_name=args.volume_name,
2927
+ timeout_minutes=args.timeout,
2928
+ ssh_password=ssh_password,
2929
+ interactive=args.interactive
2930
+ )
3538
2931
  except KeyboardInterrupt:
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)
2932
+ # print("\n\n🛑 Execution interrupted")
2933
+ # print("🧹 Cleaning up resources...")
2934
+ cleanup_modal_token()
2935
+ sys.exit(1)
3543
2936
  except Exception as e:
3544
- print(f"❌ Error: {e}")
2937
+ # print(f"\n❌ Error: {e}")
2938
+ # print("🧹 Cleaning up resources...")
2939
+ cleanup_modal_token()
3545
2940
  sys.exit(1)