gitarsenal-cli 1.9.71 → 1.9.73

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.
@@ -6,150 +6,34 @@ import json
6
6
  import re
7
7
  import datetime
8
8
  import getpass
9
- import requests
10
9
  import secrets
11
10
  import string
12
11
  import argparse
13
- import threading
14
- import uuid
15
- import signal
16
12
  from pathlib import Path
17
13
  import modal
18
- import io
19
- import contextlib
20
- import unicodedata
21
- import shutil
22
14
  from auth_manager import AuthManager
23
15
 
24
- # Global flag to indicate when an outer boxed capture is active
25
- _BOX_CAPTURE_ACTIVE = False
16
+ # Removed unused boxed output functions since they're no longer used with the Agent-based approach
26
17
 
27
18
 
28
- def _print_boxed_block(content: str):
29
- # Use NBSP for padding to prevent renderers from trimming trailing spaces
30
- NBSP = "\u00A0"
31
- lines = content.splitlines() if content else ["(no output)"]
19
+ # Removed _execute_with_box function as it's no longer used with the Agent-based approach
32
20
 
33
- def _strip_ansi(s: str) -> str:
34
- return re.sub(r"\x1B\[[0-9;]*[A-Za-z]", "", s)
21
+ # Early argument parsing for proxy settings only
22
+ early_parser = argparse.ArgumentParser(add_help=False)
23
+ early_parser.add_argument('--proxy-url', help='URL of the proxy server')
24
+ early_parser.add_argument('--proxy-api-key', help='API key for the proxy server')
35
25
 
36
- def _display_width(s: str) -> int:
37
- s = s.replace("\t", " ")
38
- width = 0
39
- for ch in s:
40
- if unicodedata.combining(ch):
41
- continue
42
- ea = unicodedata.east_asian_width(ch)
43
- width += 2 if ea in ("W", "F") else 1
44
- return width
45
-
46
- # Compute desired inner width: full terminal width when available
47
- visible_widths = [_display_width(_strip_ansi(line)) for line in lines]
48
- term_width = shutil.get_terminal_size(fallback=(120, 24)).columns
49
- # Borders add 4 chars: left '│ ', right ' │'
50
- desired_inner = max(20, term_width - 4)
51
-
52
- # If content has lines longer than max_inner, hard-truncate with ellipsis preserving ANSI
53
- def _truncate_line(line: str, target: int) -> str:
54
- plain = _strip_ansi(line)
55
- if _display_width(plain) <= target:
56
- return line
57
- out = []
58
- acc = 0
59
- i = 0
60
- while i < len(line) and acc < max(0, target - 1):
61
- ch = line[i]
62
- out.append(ch)
63
- # Skip ANSI sequences intact
64
- if ch == "\x1b":
65
- # consume until letter
66
- j = i + 1
67
- while j < len(line) and not (line[j].isalpha() and line[j-1] == 'm'):
68
- j += 1
69
- # include the terminator if present
70
- if j < len(line):
71
- out.append(line[i+1:j+1])
72
- i = j + 1
73
- else:
74
- i += 1
75
- continue
76
- # width accounting
77
- if not unicodedata.combining(ch):
78
- ea = unicodedata.east_asian_width(ch)
79
- acc += 2 if ea in ("W", "F") else 1
80
- i += 1
81
- return ''.join(out) + '…'
82
-
83
- adjusted_lines = [_truncate_line(line, desired_inner) for line in lines]
84
- inner_width = desired_inner
85
-
86
- top = "┌" + "─" * (inner_width + 2) + "┐"
87
- bottom = "└" + "─" * (inner_width + 2) + "┘"
88
- print(top)
89
- for line in adjusted_lines:
90
- vis = _strip_ansi(line)
91
- pad_len = inner_width - _display_width(vis)
92
- pad = NBSP * max(pad_len, 0)
93
- print("│ " + line + pad + " │")
94
- print(bottom)
95
-
96
-
97
- @contextlib.contextmanager
98
- def _boxed_capture():
99
- global _BOX_CAPTURE_ACTIVE
100
- buf = io.StringIO()
101
- prev = _BOX_CAPTURE_ACTIVE
102
- _BOX_CAPTURE_ACTIVE = True
103
- try:
104
- with contextlib.redirect_stdout(buf):
105
- yield
106
- finally:
107
- _BOX_CAPTURE_ACTIVE = prev
108
- content = buf.getvalue().rstrip("\n")
109
- _print_boxed_block(content)
110
-
111
-
112
- def _execute_with_box(shell, cmd_text: str, timeout: int, title_line: str = None, box: bool = True):
113
- start_time = time.time()
114
- if box and not _BOX_CAPTURE_ACTIVE:
115
- buf = io.StringIO()
116
- with contextlib.redirect_stdout(buf):
117
- if title_line:
118
- print(title_line)
119
- success, stdout, stderr = shell.execute(cmd_text, timeout=timeout)
120
- execution_time = time.time() - start_time
121
- content = buf.getvalue().rstrip("\n")
122
- _print_boxed_block(content)
123
- return success, stdout, stderr, execution_time
124
- else:
125
- if title_line:
126
- print(title_line)
127
- success, stdout, stderr = shell.execute(cmd_text, timeout=timeout)
128
- execution_time = time.time() - start_time
129
- return success, stdout, stderr, execution_time
130
-
131
- # Parse command-line arguments
132
- parser = argparse.ArgumentParser()
133
- parser.add_argument('--proxy-url', help='URL of the proxy server')
134
- parser.add_argument('--proxy-api-key', help='API key for the proxy server')
135
- parser.add_argument('--gpu', default='A10G', help='GPU type to use')
136
- parser.add_argument('--repo-url', help='Repository URL')
137
- parser.add_argument('--volume-name', help='Volume name')
138
- parser.add_argument('--use-api', action='store_true', help='Use API to fetch setup commands')
139
- parser.add_argument('--yes', action='store_true', help='Automatically confirm prompts (non-interactive)')
140
-
141
- # Parse only known args to avoid conflicts with other arguments
142
- args, unknown = parser.parse_known_args()
26
+ # Parse only proxy args early to avoid conflicts
27
+ early_args, _ = early_parser.parse_known_args()
143
28
 
144
29
  # Set proxy URL and API key in environment variables if provided
145
- if args.proxy_url:
146
- os.environ["MODAL_PROXY_URL"] = args.proxy_url
30
+ if early_args.proxy_url:
31
+ os.environ["MODAL_PROXY_URL"] = early_args.proxy_url
147
32
 
148
- if args.proxy_api_key:
149
- os.environ["MODAL_PROXY_API_KEY"] = args.proxy_api_key
33
+ if early_args.proxy_api_key:
34
+ os.environ["MODAL_PROXY_API_KEY"] = early_args.proxy_api_key
150
35
 
151
36
  # Import the fetch_modal_tokens module
152
- # print("🔄 Fetching tokens from proxy server...")
153
37
  from fetch_modal_tokens import get_tokens
154
38
  token_id, token_secret, openai_api_key, anthropic_api_key, openrouter_api_key, groq_api_key = get_tokens()
155
39
 
@@ -157,8 +41,6 @@ token_id, token_secret, openai_api_key, anthropic_api_key, openrouter_api_key, g
157
41
  if token_id is None or token_secret is None:
158
42
  raise ValueError("Could not get valid tokens")
159
43
 
160
- # print(f"✅ Tokens fetched successfully")
161
-
162
44
  # Explicitly set the environment variables again to be sure
163
45
  os.environ["MODAL_TOKEN_ID"] = token_id
164
46
  os.environ["MODAL_TOKEN_SECRET"] = token_secret
@@ -198,9 +80,9 @@ def get_stored_credentials():
198
80
  return {}
199
81
 
200
82
 
201
- # Now modify the create_modal_ssh_container function to use the PersistentShell
83
+ # Create Modal SSH container with GPU support and intelligent repository setup using Agent
202
84
  def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
203
- volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False, gpu_count=1):
85
+ volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False, gpu_count=1, use_cuda_base=False):
204
86
  """Create a Modal SSH container with GPU support and intelligent repository setup.
205
87
 
206
88
  When repo_url is provided, uses Agent for intelligent repository setup.
@@ -355,47 +237,54 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
355
237
  current_dir = os.path.dirname(os.path.abspath(__file__))
356
238
  # Get the gitarsenal-cli root directory for kill_claude files
357
239
  gitarsenal_root = os.path.dirname(current_dir)
358
- # print(f"🔍 Current directory for mounting: {current_dir}")
359
240
 
360
- # Use a more stable CUDA base image and avoid problematic packages
241
+ # Choose base image to avoid CUDA segfault issues
242
+ print("⚠️ Using CUDA base image - this may cause segfaults on some systems")
243
+ # base_image = modal.Image.from_registry("nvidia/cuda:12.4.0-runtime-ubuntu22.04", add_python="3.11")
244
+ base_image = modal.Image.debian_slim()
245
+ pip_cmd = "uv_pip_install"
246
+
247
+ # Build the SSH image with the chosen base
361
248
  ssh_image = (
362
- # modal.Image.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.11")
363
- modal.Image.debian_slim()
249
+ base_image
364
250
  .apt_install(
365
251
  "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
366
252
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
367
253
  "gpg", "ca-certificates", "software-properties-common"
368
254
  )
369
- .uv_pip_install("uv", "modal", "gitingest", "requests", "openai", "anthropic", "exa-py") # Remove problematic CUDA packages
370
- .run_commands(
371
- # Create SSH directory
372
- "mkdir -p /var/run/sshd",
373
- "mkdir -p /root/.ssh",
374
- "chmod 700 /root/.ssh",
375
-
376
- # Configure SSH server
377
- "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
378
- "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
379
- "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
380
-
381
- # SSH keep-alive settings
382
- "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
383
- "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
384
-
385
- # Generate SSH host keys
386
- "ssh-keygen -A",
387
-
388
- # Set up a nice bash prompt
389
- "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
390
-
391
- # Create base directories (subdirectories will be created automatically when mounting)
392
- "mkdir -p /python",
393
- )
394
- # Mount entire directories instead of individual files
395
- .add_local_dir(current_dir, "/python", ignore=lambda p: not p.name.endswith('.py')) # Mount all Python files from current directory
396
- .add_local_dir(os.path.join(gitarsenal_root, "kill_claude"), "/python/kill_claude") # Mount entire kill_claude directory with all subdirectories
397
-
398
255
  )
256
+
257
+ # Add Python packages using the appropriate method
258
+ if pip_cmd == "uv_pip_install":
259
+ ssh_image = ssh_image.uv_pip_install("uv", "modal", "gitingest", "requests", "openai", "anthropic", "exa-py")
260
+ else:
261
+ ssh_image = ssh_image.pip_install("modal", "gitingest", "requests", "openai", "anthropic", "exa-py")
262
+
263
+ # Add the rest of the configuration
264
+ ssh_image = ssh_image.run_commands(
265
+ # Create SSH directory
266
+ "mkdir -p /var/run/sshd",
267
+ "mkdir -p /root/.ssh",
268
+ "chmod 700 /root/.ssh",
269
+
270
+ # Configure SSH server
271
+ "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
272
+ "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
273
+ "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
274
+
275
+ # SSH keep-alive settings
276
+ "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
277
+ "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
278
+
279
+ # Generate SSH host keys
280
+ "ssh-keygen -A",
281
+
282
+ # Set up a nice bash prompt
283
+ "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
284
+
285
+ # Create base directories (subdirectories will be created automatically when mounting)
286
+ "mkdir -p /python",
287
+ ).add_local_dir(current_dir, "/python", ignore=lambda p: not p.name.endswith('.py')).add_local_dir(os.path.join(gitarsenal_root, "kill_claude"), "/python/kill_claude")
399
288
  print("✅ SSH image built successfully")
400
289
 
401
290
  # Configure volumes if available
@@ -428,28 +317,11 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
428
317
  import sys
429
318
 
430
319
  # Add the mounted python directory to the Python path
431
- sys.path.insert(0, "/python")
320
+ # sys.path.insert(0, "/python")
432
321
 
433
- # Import the required classes from the mounted modules
434
- try:
435
- from command_manager import CommandListManager
436
- from shell import PersistentShell
437
- from llm_debugging import get_stored_credentials, generate_auth_context, call_llm_for_debug, call_llm_for_batch_debug, call_anthropic_for_debug, call_openai_for_debug, call_openai_for_batch_debug, call_anthropic_for_batch_debug, call_openrouter_for_debug, call_openrouter_for_batch_debug, get_current_debug_model
438
-
439
- print("✅ Successfully imported CommandListManager, PersistentShell, and all llm_debugging functions from mounted modules")
440
- except ImportError as e:
441
- print(f"❌ Failed to import modules from mounted directory: {e}")
442
- print("🔍 Available files in /python:")
443
- try:
444
- import os
445
- if os.path.exists("/python"):
446
- for file in os.listdir("/python"):
447
- print(f" - {file}")
448
- else:
449
- print(" /python directory does not exist")
450
- except Exception as list_error:
451
- print(f" Error listing files: {list_error}")
452
- raise
322
+ # Import only the modules we actually need (none currently for Agent-based approach)
323
+ # Note: CommandListManager and llm_debugging functions are not used in the Agent-based approach
324
+ print("✅ Container setup complete - using Agent-based repository setup")
453
325
 
454
326
  # Set root password
455
327
  subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
@@ -457,7 +329,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
457
329
  # Set OpenAI API key if provided
458
330
  if openai_api_key:
459
331
  os.environ['OPENAI_API_KEY'] = openai_api_key
460
- # print(f"✅ Set OpenAI API key in container environment (length: {len(openai_api_key)})")
461
332
  else:
462
333
  print("⚠️ No OpenAI API key provided to container")
463
334
 
@@ -637,8 +508,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
637
508
  process.kill()
638
509
  process.wait()
639
510
  except Exception as stream_error:
640
- # print(f"\n⚠️ Error with advanced streaming: {stream_error}")
641
- # print("🔄 Falling back to simple streaming...")
642
511
  pass
643
512
 
644
513
  # Fallback to simple readline approach
@@ -724,11 +593,9 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
724
593
  with app.run():
725
594
  # Get the API key from environment
726
595
  api_key = os.environ.get("OPENAI_API_KEY")
727
- # print(f"🔐 API key: {api_key}")
728
596
 
729
597
  # Get stored credentials from local file
730
598
  stored_credentials = get_stored_credentials()
731
- # print(f"🔐 Stored credentials: {stored_credentials}")
732
599
  if stored_credentials:
733
600
  print(f"🔐 Found {len(stored_credentials)} stored credentials to send to container")
734
601
  else:
@@ -748,393 +615,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
748
615
  print(f"❌ Error running container: {e}")
749
616
  return None
750
617
 
751
- def fetch_setup_commands_from_api(repo_url):
752
- """Fetch setup commands from the GitIngest API using real repository analysis."""
753
- import tempfile
754
- import subprocess
755
- import os
756
- import shutil
757
- import json
758
- import time
759
- import requests
760
-
761
- # Define API endpoints to try in order - using only online endpoints
762
- api_endpoints = [
763
- "https://www.gitarsenal.dev/api/analyze-with-gitingest" # Working endpoint with www prefix
764
- ]
765
-
766
- print(f"🔍 Fetching setup commands from API for repository: {repo_url}")
767
-
768
- # Check if gitingest command line tool is available - try multiple possible command names
769
- has_gitingest_cli = False
770
- gitingest_cmd_name = None
771
-
772
- # Try the standard command name first
773
- try:
774
- print(f"🔍 Checking for GitIngest CLI tool...")
775
- result = subprocess.run(["gitingest", "--help"], check=True, capture_output=True, text=True)
776
- has_gitingest_cli = True
777
- gitingest_cmd_name = "gitingest"
778
- print(f"✅ GitIngest CLI tool found")
779
- except (subprocess.SubprocessError, FileNotFoundError) as e:
780
- print(f" - GitIngest command not found: {str(e)}")
781
-
782
- # Create a temporary directory for output
783
- temp_dir = tempfile.mkdtemp(prefix="repo_analysis_")
784
- output_file = os.path.join(temp_dir, "digest.json")
785
-
786
- # Create a directory to save GitIngest results
787
- save_dir = os.path.join(os.path.expanduser("~"), "gitarsenal_results")
788
- os.makedirs(save_dir, exist_ok=True)
789
- timestamp = time.strftime("%Y%m%d_%H%M%S")
790
- repo_name = repo_url.split("/")[-1].replace(".git", "")
791
- save_file = os.path.join(save_dir, f"gitingest_{repo_name}_{timestamp}.txt")
792
-
793
- try:
794
- if has_gitingest_cli:
795
- # Use gitingest CLI tool to analyze the repository directly from URL
796
- print(f"🔎 Running GitIngest analysis on {repo_url}...")
797
-
798
- # Based on the help output, the correct format is:
799
- # gitingest [OPTIONS] [SOURCE]
800
- # With options:
801
- # -o, --output TEXT Output file path
802
- # --format TEXT Output format (json)
803
-
804
- # Run gitingest command with proper parameters
805
- gitingest_run_cmd = [
806
- gitingest_cmd_name,
807
- repo_url,
808
- "-o", output_file, # Use -o for output file
809
- ]
810
-
811
- print(f"🔄 Executing: {' '.join(gitingest_run_cmd)}")
812
-
813
- result = subprocess.run(gitingest_run_cmd, capture_output=True, text=True)
814
-
815
- if result.returncode != 0:
816
- print(f"⚠️ GitIngest CLI failed with exit code {result.returncode}")
817
- print(f"⚠️ Error output: {result.stderr}")
818
- print("Falling back to basic analysis")
819
- gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
820
- else:
821
- print(f"✅ GitIngest analysis completed successfully")
822
-
823
- # Read the output file - note that the default format might not be JSON
824
- try:
825
- # First try to parse as JSON
826
- try:
827
- with open(output_file, 'r', encoding='utf-8') as f:
828
- content = f.read()
829
-
830
- # Save the GitIngest output to the results directory
831
- with open(save_file, 'w', encoding='utf-8') as save_f:
832
- save_f.write(content)
833
- print(f"📁 GitIngest output saved to: {save_file}")
834
-
835
- try:
836
- gitingest_data = json.loads(content)
837
- print(f"✅ GitIngest data loaded as JSON from {output_file}")
838
- except json.JSONDecodeError:
839
- # If not JSON, convert the text output to a basic structure
840
- print(f"⚠️ GitIngest output is not in JSON format, converting text to structure")
841
-
842
- # Process the text to extract useful information
843
- import re
844
-
845
- # Try to identify language
846
- language_match = re.search(r"(?i)language[s]?:?\s*(\w+)", content)
847
- detected_language = language_match.group(1) if language_match else "Unknown"
848
-
849
- # Try to identify technologies with stronger evidence requirements
850
- tech_patterns = {
851
- "python": r"(?i)(python|\.py\b|pip\b|requirements\.txt|setup\.py)",
852
- "javascript": r"(?i)(javascript|\.js\b|node|npm|yarn|package\.json)",
853
- "typescript": r"(?i)(typescript|\.ts\b|tsc\b|tsconfig\.json)",
854
- "go": r"(?i)(\bgo\b|golang|\.go\b|go\.mod|go\.sum)",
855
- "rust": r"(?i)(rust|\.rs\b|cargo|Cargo\.toml)",
856
- "java": r"(?i)(java\b|\.java\b|maven|gradle|pom\.xml)",
857
- "c++": r"(?i)(c\+\+|\.cpp\b|\.hpp\b|cmake\b|CMakeLists\.txt)",
858
- "pytorch": r"(?i)(pytorch|torch\b|nn\.Module)",
859
- "tensorflow": r"(?i)(tensorflow|tf\.|keras\b)",
860
- }
861
-
862
- # Count occurrences to filter out false positives
863
- tech_counts = {}
864
- for tech, pattern in tech_patterns.items():
865
- matches = re.findall(pattern, content)
866
- if matches:
867
- tech_counts[tech] = len(matches)
868
-
869
- # Filter technologies based on threshold
870
- thresholds = {
871
- "javascript": 3, # Higher threshold for JavaScript
872
- "go": 3, # Higher threshold for Go
873
- "default": 2 # Default threshold
874
- }
875
-
876
- detected_technologies = []
877
- for tech, count in tech_counts.items():
878
- threshold = thresholds.get(tech, thresholds["default"])
879
- if count >= threshold:
880
- detected_technologies.append(tech)
881
- print(f"📊 Detected {tech} with confidence score {count}")
882
-
883
- # Create a structured representation
884
- gitingest_data = {
885
- "system_info": {
886
- "detected_language": detected_language,
887
- "detected_technologies": detected_technologies,
888
- },
889
- "repository_analysis": {
890
- "summary": content[:5000], # First 5000 chars as summary
891
- "content_preview": content[:10000] # First 10000 chars as preview
892
- },
893
- "success": True
894
- }
895
-
896
- # Save the processed data
897
- processed_file = os.path.join(save_dir, f"gitingest_processed_{repo_name}_{timestamp}.json")
898
- with open(processed_file, 'w', encoding='utf-8') as proc_f:
899
- json.dump(gitingest_data, proc_f, indent=2)
900
- print(f"📁 Processed GitIngest data saved to: {processed_file}")
901
- except FileNotFoundError:
902
- print(f"⚠️ Output file not found at {output_file}")
903
- gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
904
- except Exception as e:
905
- print(f"⚠️ Error reading GitIngest output: {e}")
906
- gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
907
- else:
908
- # Fall back to basic analysis if gitingest CLI is not available
909
- gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
910
-
911
- # Prepare the request payload with GitIngest data
912
- payload = {
913
- "repoUrl": repo_url,
914
- "gitingestData": gitingest_data,
915
- "userRequest": "Setup and run the repository"
916
- }
917
-
918
- print(f"📤 API Request payload prepared (GitIngest data size: {len(json.dumps(gitingest_data))} bytes)")
919
-
920
- # Try each endpoint in sequence until one succeeds
921
- response = None
922
- for api_url in api_endpoints:
923
- # Use the retry mechanism for more reliable requests
924
- response = make_api_request_with_retry(
925
- url=api_url,
926
- payload=payload,
927
- max_retries=2,
928
- timeout=180 # 3 minute timeout
929
- )
930
-
931
- # If we got a response and it's successful, break out of the loop
932
- if response and response.status_code == 200:
933
- print(f"✅ Successful response from {api_url}")
934
- break
935
-
936
- if response:
937
- print(f"⚠️ Endpoint {api_url} returned status code {response.status_code}, trying next endpoint...")
938
- else:
939
- print(f"⚠️ Failed to connect to {api_url}, trying next endpoint...")
940
-
941
- # If we've tried all endpoints and still don't have a response, use fallback
942
- if response is None:
943
- print("❌ All API endpoints failed")
944
- return generate_fallback_commands(gitingest_data)
945
-
946
- # Continue with the response we got from the successful endpoint
947
- if not response:
948
- print("❌ No valid response received from any endpoint")
949
- return generate_fallback_commands(gitingest_data)
950
-
951
- try:
952
- print(f"📥 API Response status code: {response.status_code}")
953
-
954
- if response.status_code == 200:
955
- try:
956
- data = response.json()
957
- print(f"📄 API Response data received")
958
- print(f"📄 Response size: {len(response.text)} bytes")
959
- print(f"📄 Response URL: {response.url}")
960
-
961
- # Extract setup commands from the response
962
- if "setupInstructions" in data and "commands" in data["setupInstructions"]:
963
- commands = data["setupInstructions"]["commands"]
964
- print(f"✅ Successfully fetched {len(commands)} setup commands from API")
965
-
966
- # Print the original commands for reference
967
- print("📋 Original commands from API:")
968
- for i, cmd in enumerate(commands, 1):
969
- print(f" {i}. {cmd}")
970
-
971
- # Fix the commands by removing placeholders and comments
972
- fixed_commands = fix_setup_commands(commands)
973
-
974
- # If we have a temp_dir with the cloned repo, try to find the entry point
975
- # and replace any placeholder entry points
976
- for i, cmd in enumerate(fixed_commands):
977
- if "python main.py" in cmd or "python3 main.py" in cmd:
978
- try:
979
- entry_point = find_entry_point(temp_dir)
980
- if entry_point and entry_point != "main.py":
981
- fixed_commands[i] = cmd.replace("main.py", entry_point)
982
- print(f"🔄 Replaced main.py with detected entry point: {entry_point}")
983
- except Exception as e:
984
- print(f"⚠️ Error finding entry point: {e}")
985
-
986
- # Print the fixed commands
987
- print("\n📋 Fixed commands:")
988
- for i, cmd in enumerate(fixed_commands, 1):
989
- print(f" {i}. {cmd}")
990
-
991
- return fixed_commands
992
- else:
993
- print("⚠️ API response did not contain setupInstructions.commands field")
994
- print("📋 Available fields in response:")
995
- for key in data.keys():
996
- print(f" - {key}")
997
- # Return fallback commands
998
- return generate_fallback_commands(gitingest_data)
999
- except json.JSONDecodeError as e:
1000
- print(f"❌ Failed to parse API response as JSON: {e}")
1001
- print(f"Raw response: {response.text[:500]}...")
1002
- # Return fallback commands
1003
- return generate_fallback_commands(gitingest_data)
1004
- elif response.status_code == 504:
1005
- print(f"❌ API request timed out (504 Gateway Timeout)")
1006
- print("⚠️ The server took too long to respond. Using fallback commands instead.")
1007
- # Return fallback commands
1008
- return generate_fallback_commands(gitingest_data)
1009
- else:
1010
- print(f"❌ API request failed with status code: {response.status_code}")
1011
- print(f"❌ Response URL: {response.url}")
1012
- print(f"❌ Response headers: {dict(response.headers)}")
1013
- print(f"❌ Error response: {response.text[:500]}...")
1014
- # Return fallback commands
1015
- return generate_fallback_commands(gitingest_data)
1016
- except Exception as e:
1017
- print(f"❌ Error processing API response: {str(e)}")
1018
- print("⚠️ Using fallback commands instead")
1019
- # Return fallback commands
1020
- return generate_fallback_commands(gitingest_data)
1021
- except Exception as e:
1022
- print(f"❌ Error fetching setup commands from API: {e}")
1023
- import traceback
1024
- traceback.print_exc()
1025
- # Return fallback commands
1026
- return generate_fallback_commands(None)
1027
- finally:
1028
- # Clean up the temporary directory
1029
- print(f"🧹 Cleaning up temporary directory...")
1030
- shutil.rmtree(temp_dir, ignore_errors=True)
1031
-
1032
- def generate_fallback_commands(gitingest_data):
1033
- return True
1034
-
1035
- def generate_basic_repo_analysis_from_url(repo_url):
1036
- """Generate basic repository analysis data from a repository URL."""
1037
- import tempfile
1038
- import subprocess
1039
- import os
1040
- import shutil
1041
-
1042
- # Create a temporary directory for cloning
1043
- temp_dir = tempfile.mkdtemp(prefix="repo_basic_analysis_")
1044
-
1045
- try:
1046
- print(f"📥 Cloning repository to {temp_dir} for basic analysis...")
1047
- clone_result = subprocess.run(
1048
- ["git", "clone", "--depth", "1", repo_url, temp_dir],
1049
- capture_output=True,
1050
- text=True
1051
- )
1052
-
1053
- if clone_result.returncode != 0:
1054
- print(f"❌ Failed to clone repository: {clone_result.stderr}")
1055
- return {
1056
- "system_info": {
1057
- "platform": "linux",
1058
- "python_version": "3.10",
1059
- "detected_language": "Unknown",
1060
- "detected_technologies": [],
1061
- "file_count": 0,
1062
- "repo_stars": 0,
1063
- "repo_forks": 0,
1064
- "primary_package_manager": "Unknown",
1065
- "complexity_level": "low"
1066
- },
1067
- "repository_analysis": {
1068
- "summary": f"Repository analysis for {repo_url}",
1069
- "tree": "Failed to clone repository",
1070
- "content_preview": "No content available"
1071
- },
1072
- "success": False
1073
- }
1074
-
1075
- print(f"✅ Repository cloned successfully for basic analysis")
1076
-
1077
- # Use the existing generate_basic_repo_analysis function
1078
- return generate_basic_repo_analysis(temp_dir)
1079
- finally:
1080
- # Clean up the temporary directory
1081
- print(f"🧹 Cleaning up temporary directory for basic analysis...")
1082
- shutil.rmtree(temp_dir, ignore_errors=True)
1083
-
1084
- def generate_basic_repo_analysis(repo_dir):
1085
- return True
1086
-
1087
- def fix_setup_commands(commands):
1088
- """Fix setup commands by removing placeholders and comments."""
1089
- fixed_commands = []
1090
-
1091
- for cmd in commands:
1092
- # Remove placeholders like "(or the appropriate entry point...)"
1093
- cmd = re.sub(r'\([^)]*\)', '', cmd).strip()
1094
-
1095
- # Skip empty commands or pure comments
1096
- if not cmd or cmd.startswith('#'):
1097
- continue
1098
-
1099
- # Remove trailing comments
1100
- cmd = re.sub(r'#.*$', '', cmd).strip()
1101
-
1102
- if cmd:
1103
- fixed_commands.append(cmd)
1104
-
1105
- return fixed_commands
1106
-
1107
- def find_entry_point(repo_dir):
1108
- """Find the entry point script for a repository."""
1109
- # Common entry point files to check
1110
- common_entry_points = [
1111
- "main.py", "app.py", "run.py", "train.py", "start.py",
1112
- "server.py", "cli.py", "demo.py", "example.py"
1113
- ]
1114
-
1115
- # Check if any of the common entry points exist
1116
- for entry_point in common_entry_points:
1117
- if os.path.exists(os.path.join(repo_dir, entry_point)):
1118
- return entry_point
1119
-
1120
- # Look for Python files in the root directory
1121
- python_files = [f for f in os.listdir(repo_dir) if f.endswith('.py')]
1122
- if python_files:
1123
- # Prioritize files with main function or if_name_main pattern
1124
- for py_file in python_files:
1125
- file_path = os.path.join(repo_dir, py_file)
1126
- try:
1127
- with open(file_path, 'r') as f:
1128
- content = f.read()
1129
- if "def main" in content or "if __name__ == '__main__'" in content or 'if __name__ == "__main__"' in content:
1130
- return py_file
1131
- except:
1132
- pass
1133
-
1134
- # If no main function found, return the first Python file
1135
- return python_files[0]
1136
-
1137
- return None
1138
618
 
1139
619
  def cleanup_security_tokens():
1140
620
  """Delete all security tokens and API keys after SSH container is started"""
@@ -1146,27 +626,22 @@ def cleanup_security_tokens():
1146
626
  for var in modal_env_vars:
1147
627
  if var in os.environ:
1148
628
  del os.environ[var]
1149
- # print(f"✅ Removed {var} from environment")
1150
629
 
1151
630
  # Remove OpenAI API key from environment
1152
631
  if "OPENAI_API_KEY" in os.environ:
1153
632
  del os.environ["OPENAI_API_KEY"]
1154
- # print("✅ Removed OpenAI API key from environment")
1155
633
 
1156
634
  # Delete ~/.modal.toml file
1157
635
  home_dir = os.path.expanduser("~")
1158
636
  modal_toml = os.path.join(home_dir, ".modal.toml")
1159
637
  if os.path.exists(modal_toml):
1160
638
  os.remove(modal_toml)
1161
- # print(f"✅ Deleted Modal token file at {modal_toml}")
1162
639
 
1163
640
  # Delete ~/.gitarsenal/openai_key file
1164
641
  openai_key_file = os.path.join(home_dir, ".gitarsenal", "openai_key")
1165
642
  if os.path.exists(openai_key_file):
1166
643
  os.remove(openai_key_file)
1167
- # print(f"✅ Deleted OpenAI API key file at {openai_key_file}")
1168
644
 
1169
- # print("✅ Security cleanup completed successfully")
1170
645
  except Exception as e:
1171
646
  print(f"❌ Error during security cleanup: {e}")
1172
647
 
@@ -1175,6 +650,7 @@ def cleanup_modal_token():
1175
650
  """Legacy function - now calls the comprehensive cleanup"""
1176
651
  cleanup_security_tokens()
1177
652
 
653
+
1178
654
  def show_usage_examples():
1179
655
  """Display usage examples for the script."""
1180
656
  print("Usage Examples\n")
@@ -1254,497 +730,6 @@ def show_usage_examples():
1254
730
  print(" # Manual setup (advanced users):")
1255
731
  print(" gitarsenal --gpu A10G --setup-commands \"pip install torch\" \"python train.py\"")
1256
732
 
1257
- def make_api_request_with_retry(url, payload, max_retries=2, timeout=180):
1258
- """Make an API request with retry mechanism."""
1259
- import requests
1260
- import time
1261
-
1262
- for attempt in range(max_retries + 1):
1263
- try:
1264
- if attempt > 0:
1265
- print(f"🔄 Retry attempt {attempt}/{max_retries}...")
1266
-
1267
- # print(f"🌐 Making POST request to: {url}")
1268
- # print(f"⏳ Waiting up to {timeout//60} minutes for response...")
1269
-
1270
- # Set allow_redirects=True to follow redirects automatically
1271
- response = requests.post(
1272
- url,
1273
- json=payload,
1274
- timeout=timeout,
1275
- allow_redirects=True,
1276
- headers={
1277
- 'Content-Type': 'application/json',
1278
- 'User-Agent': 'GitArsenal-CLI/1.0'
1279
- }
1280
- )
1281
-
1282
- # Print redirect info if any
1283
- if response.history:
1284
- print(f"✅ Request was redirected {len(response.history)} times")
1285
- for resp in response.history:
1286
- print(f" - Redirect: {resp.status_code} from {resp.url}")
1287
- print(f"✅ Final URL: {response.url}")
1288
-
1289
- return response
1290
- except requests.exceptions.RequestException as e:
1291
- if attempt < max_retries:
1292
- retry_delay = 2 ** attempt # Exponential backoff
1293
- print(f"⚠️ Request failed: {str(e)}")
1294
- print(f"⏳ Waiting {retry_delay} seconds before retrying...")
1295
- time.sleep(retry_delay)
1296
- else:
1297
- print(f"❌ All retry attempts failed: {str(e)}")
1298
- return None
1299
-
1300
- return None
1301
-
1302
- def get_setup_commands_from_gitingest(repo_url):
1303
- """
1304
- Get repository setup commands using the gitingest approach.
1305
-
1306
- This function is inspired by gitingest_setup_client.py and provides a more
1307
- robust way to get setup commands for a repository.
1308
-
1309
- Args:
1310
- repo_url: URL of the repository to set up
1311
-
1312
- Returns:
1313
- List of setup commands or None if failed
1314
- """
1315
- import requests
1316
- import json
1317
- import os
1318
- import sys
1319
- import tempfile
1320
- import subprocess
1321
-
1322
- print(f"🔍 Getting setup commands for repository: {repo_url}")
1323
-
1324
- # Define API endpoints to try in order
1325
- api_endpoints = [
1326
- "https://www.gitarsenal.dev/api/gitingest-setup-commands",
1327
- "https://gitarsenal.dev/api/gitingest-setup-commands",
1328
- ]
1329
-
1330
- # Generate basic gitingest data
1331
- def generate_basic_gitingest_data():
1332
- # Extract repo name from URL
1333
- repo_name = repo_url.split('/')[-1].replace('.git', '')
1334
-
1335
- return {
1336
- "system_info": {
1337
- "platform": "Unknown",
1338
- "python_version": "Unknown",
1339
- "detected_language": "Unknown",
1340
- "detected_technologies": [],
1341
- "file_count": 0,
1342
- "repo_stars": 0,
1343
- "repo_forks": 0,
1344
- "primary_package_manager": "Unknown",
1345
- "complexity_level": "Unknown"
1346
- },
1347
- "repository_analysis": {
1348
- "summary": f"Repository: {repo_name}",
1349
- "tree": "",
1350
- "content_preview": ""
1351
- },
1352
- "success": True
1353
- }
1354
-
1355
- # Try to generate gitingest data using CLI if available
1356
- def generate_gitingest_data_from_cli():
1357
- try:
1358
- # Check if gitingest CLI is available
1359
- subprocess.run(["gitingest", "--help"], check=True, capture_output=True, text=True)
1360
-
1361
- # Create a temporary file for the output
1362
- with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
1363
- output_file = tmp.name
1364
-
1365
- # Run gitingest command
1366
- print(f"Running gitingest analysis on {repo_url}...")
1367
- gitingest_cmd = ["gitingest", repo_url, "-o", output_file]
1368
- result = subprocess.run(gitingest_cmd, capture_output=True, text=True)
1369
-
1370
- if result.returncode != 0:
1371
- print(f"GitIngest CLI failed: {result.stderr}")
1372
- return None
1373
-
1374
- # Read the output file
1375
- try:
1376
- with open(output_file, 'r', encoding='utf-8') as f:
1377
- content = f.read()
1378
- try:
1379
- data = json.loads(content)
1380
- return data
1381
- except json.JSONDecodeError:
1382
- # If not JSON, convert the text output to a basic structure
1383
- return {
1384
- "system_info": {
1385
- "platform": "Unknown",
1386
- "python_version": "Unknown",
1387
- "detected_language": "Unknown",
1388
- "detected_technologies": []
1389
- },
1390
- "repository_analysis": {
1391
- "summary": content[:5000], # First 5000 chars as summary
1392
- "tree": "",
1393
- "content_preview": content[:10000] # First 10000 chars as preview
1394
- },
1395
- "success": True
1396
- }
1397
- except Exception as e:
1398
- print(f"Error reading gitingest output: {e}")
1399
- return None
1400
- finally:
1401
- # Clean up the temporary file
1402
- if os.path.exists(output_file):
1403
- os.unlink(output_file)
1404
-
1405
- except (subprocess.SubprocessError, FileNotFoundError):
1406
- print("GitIngest CLI not found")
1407
- return None
1408
-
1409
- # First try to get data from CLI
1410
- gitingest_data = generate_gitingest_data_from_cli()
1411
-
1412
- # If CLI failed, use basic data
1413
- if not gitingest_data:
1414
- print("Using basic gitingest data")
1415
- gitingest_data = generate_basic_gitingest_data()
1416
-
1417
- # Try each API endpoint
1418
- for api_url in api_endpoints:
1419
- try:
1420
- # print(f"Trying API endpoint: {api_url}")
1421
-
1422
- # Load stored credentials
1423
- stored_credentials = get_stored_credentials()
1424
-
1425
- payload = {
1426
- "repoUrl": repo_url,
1427
- "gitingestData": gitingest_data,
1428
- "storedCredentials": stored_credentials, # Add back stored credentials
1429
- "preview": False
1430
- }
1431
-
1432
- # Use the retry mechanism for more reliable requests
1433
- response = make_api_request_with_retry(
1434
- url=api_url,
1435
- payload=payload,
1436
- max_retries=2,
1437
- timeout=180, # 3 minute timeout
1438
- )
1439
-
1440
- if not response:
1441
- print(f"Failed to connect to {api_url}")
1442
- continue
1443
-
1444
- if response.status_code != 200:
1445
- print(f"API request failed with status code: {response.status_code}")
1446
- continue
1447
-
1448
- try:
1449
- result = response.json()
1450
-
1451
- # Check if we have commands in the response
1452
- commands = None
1453
-
1454
- # Check for different response formats
1455
- if "commands" in result:
1456
- commands = result["commands"]
1457
- elif "setupInstructions" in result and "commands" in result["setupInstructions"]:
1458
- commands = result["setupInstructions"]["commands"]
1459
-
1460
- if commands:
1461
- print(f"✅ Successfully fetched {len(commands)} setup commands from API at {api_url}")
1462
-
1463
- # Enhanced response handling for API key detection
1464
- if "requiredApiKeys" in result:
1465
- api_keys = result.get("requiredApiKeys", [])
1466
- if api_keys:
1467
- print(f"\n🔑 Required API Keys ({len(api_keys)}):")
1468
- # Load stored GitArsenal credentials
1469
- stored_credentials = {}
1470
- try:
1471
- credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
1472
- if credentials_file.exists():
1473
- with open(credentials_file, 'r') as f:
1474
- stored_credentials = json.load(f)
1475
- print(f"📋 Found {len(stored_credentials)} stored GitArsenal credentials")
1476
- else:
1477
- print("📋 No stored GitArsenal credentials found")
1478
- except Exception as e:
1479
- print(f"⚠️ Error loading stored credentials: {e}")
1480
-
1481
- # Identify missing required API keys
1482
- missing_required_keys = []
1483
- available_keys = []
1484
-
1485
- for i, api_key in enumerate(api_keys, 1):
1486
- key_name = api_key.get('name', 'Unknown')
1487
- is_required = api_key.get("required", False)
1488
- has_stored_key = key_name in stored_credentials
1489
-
1490
- if is_required:
1491
- if has_stored_key:
1492
- status = "✅ Required (Available)"
1493
- available_keys.append(key_name)
1494
- else:
1495
- status = "🔴 Required (Missing)"
1496
- missing_required_keys.append(api_key)
1497
- else:
1498
- status = "🟡 Optional"
1499
-
1500
- print(f" {i}. {key_name} - {status}")
1501
- print(f" Service: {api_key.get('service', 'Unknown')}")
1502
- print(f" Description: {api_key.get('description', 'No description')}")
1503
- if api_key.get('example'):
1504
- print(f" Example: {api_key.get('example')}")
1505
- if api_key.get('documentation_url'):
1506
- print(f" Docs: {api_key.get('documentation_url')}")
1507
- print()
1508
-
1509
- # Prompt for missing required API keys
1510
- if missing_required_keys:
1511
- print("🔧 Setting up missing required API keys...")
1512
- print("Press Enter to continue or Ctrl+C to skip...")
1513
-
1514
- for api_key in missing_required_keys:
1515
- key_name = api_key.get('name', 'Unknown')
1516
- service = api_key.get('service', 'Unknown')
1517
- description = api_key.get('description', 'No description')
1518
- example = api_key.get('example', '')
1519
- docs_url = api_key.get('documentation_url', '')
1520
-
1521
- print(f"\n📝 Setting up {key_name} for {service}:")
1522
- print(f" Description: {description}")
1523
- if example:
1524
- print(f" Example: {example}")
1525
- if docs_url:
1526
- print(f" Documentation: {docs_url}")
1527
-
1528
- # Prompt user for the API key
1529
- try:
1530
- import getpass
1531
- print(f"\nPlease enter your {key_name} for {service}:")
1532
- new_key = getpass.getpass(f"{key_name} ({service}) API Key (hidden): ").strip()
1533
-
1534
- if new_key:
1535
- # Save to credentials file
1536
- credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
1537
- credentials_file.parent.mkdir(parents=True, exist_ok=True)
1538
-
1539
- # Load existing credentials
1540
- if credentials_file.exists():
1541
- with open(credentials_file, 'r') as f:
1542
- all_credentials = json.load(f)
1543
- else:
1544
- all_credentials = {}
1545
-
1546
- # Add new key
1547
- all_credentials[key_name] = new_key
1548
-
1549
- # Save back to file
1550
- with open(credentials_file, 'w') as f:
1551
- json.dump(all_credentials, f, indent=2)
1552
-
1553
- print(f"✅ {key_name} saved successfully!")
1554
- available_keys.append(key_name)
1555
- else:
1556
- print(f"⚠️ Skipping {key_name} (no input provided)")
1557
- except KeyboardInterrupt:
1558
- print(f"\n⚠️ Skipping {key_name} (cancelled by user)")
1559
- except Exception as e:
1560
- print(f"❌ Error saving {key_name}: {e}")
1561
-
1562
- # Show summary
1563
- if available_keys:
1564
- print(f"✅ Available API keys: {', '.join(available_keys)}")
1565
- if missing_required_keys:
1566
- print(f"⚠️ Missing required keys: {', '.join([k.get('name') for k in missing_required_keys])}")
1567
- else:
1568
- print("ℹ️ All required API keys are already available.")
1569
-
1570
- # Display setup complexity if available
1571
- if "setupComplexity" in result:
1572
- complexity = result.get("setupComplexity", "medium")
1573
- estimated_time = result.get("estimatedSetupTime", "Unknown")
1574
- print(f"📊 Setup Complexity: {complexity.upper()}")
1575
- print(f"⏱️ Estimated Time: {estimated_time}")
1576
-
1577
- # Print the commands
1578
- print("\n📋 Setup Commands:")
1579
- for i, cmd in enumerate(commands, 1):
1580
- print(f" {i}. {cmd}")
1581
-
1582
- # Fix the commands
1583
- fixed_commands = fix_setup_commands(commands)
1584
-
1585
- # Print the fixed commands
1586
- print("\n📋 Fixed commands:")
1587
- for i, cmd in enumerate(fixed_commands, 1):
1588
- print(f" {i}. {cmd}")
1589
-
1590
- return fixed_commands
1591
- else:
1592
- print("No commands found in API response")
1593
- except json.JSONDecodeError:
1594
- print(f"Failed to parse API response as JSON")
1595
- except Exception as e:
1596
- print(f"Error with API endpoint {api_url}: {e}")
1597
-
1598
- print("❌ All API endpoints failed")
1599
- return generate_fallback_commands(gitingest_data)
1600
-
1601
-
1602
-
1603
-
1604
-
1605
- def preprocess_commands_with_llm(setup_commands, stored_credentials, api_key=None):
1606
- """
1607
- Use LLM to preprocess setup commands and inject available credentials.
1608
-
1609
- Args:
1610
- setup_commands: List of setup commands
1611
- stored_credentials: Dictionary of stored credentials
1612
- api_key: OpenAI API key for LLM calls
1613
-
1614
- Returns:
1615
- List of processed commands with credentials injected
1616
- """
1617
- if not setup_commands or not stored_credentials:
1618
- return setup_commands
1619
-
1620
- try:
1621
- # Create context for the LLM
1622
- credentials_info = "\n".join([f"- {key}: {value[:8]}..." for key, value in stored_credentials.items()])
1623
-
1624
- prompt = f"""
1625
- You are a command preprocessing assistant. Your task is to modify setup commands to use available credentials and make them non-interactive.
1626
-
1627
- AVAILABLE CREDENTIALS:
1628
- {credentials_info}
1629
-
1630
- ORIGINAL COMMANDS:
1631
- {chr(10).join([f"{i+1}. {cmd}" for i, cmd in enumerate(setup_commands)])}
1632
-
1633
- INSTRUCTIONS:
1634
- 1. Replace any authentication commands with token-based versions using available credentials
1635
- 2. Make all commands non-interactive (add --yes, --no-input, -y flags where needed)
1636
- 3. Use environment variables or direct token injection where appropriate
1637
- 4. Skip commands that cannot be made non-interactive due to missing credentials
1638
- 5. Add any necessary environment variable exports
1639
-
1640
- Return the modified commands as a JSON array of strings. If a command should be skipped, prefix it with "# SKIPPED: ".
1641
-
1642
- Example transformations:
1643
- - "huggingface-cli login" → "huggingface-cli login --token $HUGGINGFACE_TOKEN"
1644
- - "npm install" → "npm install --yes"
1645
-
1646
- Return only the JSON array, no other text.
1647
- """
1648
-
1649
- if not api_key:
1650
- print("⚠️ No OpenAI API key available for command preprocessing")
1651
- return setup_commands
1652
-
1653
- # Call OpenAI API
1654
- import openai
1655
- client = openai.OpenAI(api_key=api_key)
1656
-
1657
- response = client.chat.completions.create(
1658
- model="gpt-4.1",
1659
- messages=[
1660
- {"role": "system", "content": "You are a command preprocessing assistant that modifies setup commands to use available credentials and make them non-interactive."},
1661
- {"role": "user", "content": prompt}
1662
- ],
1663
- temperature=0.1,
1664
- max_tokens=2000
1665
- )
1666
-
1667
- result = response.choices[0].message.content.strip()
1668
-
1669
- # Debug: Print the raw response
1670
- print(f"🔍 LLM Response: {result[:200]}...")
1671
-
1672
- # Parse the JSON response
1673
- import json
1674
- try:
1675
- processed_commands = json.loads(result)
1676
- if isinstance(processed_commands, list):
1677
- print(f"🔧 LLM preprocessed {len(processed_commands)} commands")
1678
- for i, cmd in enumerate(processed_commands):
1679
- if cmd != setup_commands[i]:
1680
- print(f" {i+1}. {setup_commands[i]} → {cmd}")
1681
- return processed_commands
1682
- else:
1683
- print("⚠️ LLM returned invalid format, using fallback preprocessing")
1684
- return fallback_preprocess_commands(setup_commands, stored_credentials)
1685
- except json.JSONDecodeError as e:
1686
- print(f"⚠️ Failed to parse LLM response: {e}")
1687
- print("🔄 Using fallback preprocessing...")
1688
- return fallback_preprocess_commands(setup_commands, stored_credentials)
1689
-
1690
- except Exception as e:
1691
- print(f"⚠️ LLM preprocessing failed: {e}")
1692
- print("🔄 Using fallback preprocessing...")
1693
- return fallback_preprocess_commands(setup_commands, stored_credentials)
1694
-
1695
- def fallback_preprocess_commands(setup_commands, stored_credentials):
1696
- """
1697
- Fallback preprocessing function that manually handles common credential injection patterns.
1698
-
1699
- Args:
1700
- setup_commands: List of setup commands
1701
- stored_credentials: Dictionary of stored credentials
1702
-
1703
- Returns:
1704
- List of processed commands with credentials injected
1705
- """
1706
- if not setup_commands or not stored_credentials:
1707
- return setup_commands
1708
-
1709
- processed_commands = []
1710
-
1711
- for i, command in enumerate(setup_commands):
1712
- processed_command = command
1713
-
1714
- # Handle Hugging Face login
1715
- if 'huggingface-cli login' in command and '--token' not in command:
1716
- if 'HUGGINGFACE_TOKEN' in stored_credentials:
1717
- processed_command = f"huggingface-cli login --token $HUGGINGFACE_TOKEN"
1718
- print(f"🔧 Fallback: Injected HF token into command {i+1}")
1719
- else:
1720
- processed_command = f"# SKIPPED: {command} (no HF token available)"
1721
- print(f"🔧 Fallback: Skipped command {i+1} (no HF token)")
1722
-
1723
- # Handle OpenAI API key
1724
- elif 'openai' in command.lower() and 'api_key' not in command.lower():
1725
- if 'OPENAI_API_KEY' in stored_credentials:
1726
- processed_command = f"export OPENAI_API_KEY=$OPENAI_API_KEY && {command}"
1727
- print(f"🔧 Fallback: Added OpenAI API key export to command {i+1}")
1728
-
1729
- # Handle npm install
1730
- elif 'npm install' in command and '--yes' not in command and '--no-interactive' not in command:
1731
- processed_command = command.replace('npm install', 'npm install --yes')
1732
- print(f"🔧 Fallback: Made npm install non-interactive in command {i+1}")
1733
-
1734
- # Handle git clone
1735
- elif command.strip().startswith('git clone') and '--depth 1' not in command:
1736
- processed_command = command.replace('git clone', 'git clone --depth 1')
1737
- print(f"🔧 Fallback: Made git clone non-interactive in command {i+1}")
1738
-
1739
- # Handle apt-get install
1740
- elif 'apt-get install' in command and '-y' not in command:
1741
- processed_command = command.replace('apt-get install', 'apt-get install -y')
1742
- print(f"🔧 Fallback: Made apt-get install non-interactive in command {i+1}")
1743
-
1744
- processed_commands.append(processed_command)
1745
-
1746
- print(f"🔧 Fallback preprocessing completed: {len(processed_commands)} commands")
1747
- return processed_commands
1748
733
 
1749
734
  def _check_authentication(auth_manager):
1750
735
  """Check if user is authenticated, prompt for login if not"""
@@ -1756,6 +741,7 @@ def _check_authentication(auth_manager):
1756
741
  print("\n🔐 Authentication required")
1757
742
  return auth_manager.interactive_auth_flow()
1758
743
 
744
+
1759
745
  def _handle_auth_commands(auth_manager, args):
1760
746
  """Handle authentication-related commands"""
1761
747
  if args.login:
@@ -1914,9 +900,6 @@ if __name__ == "__main__":
1914
900
  parser.add_argument('--volume-name', type=str, help='Name of the Modal volume for persistent storage')
1915
901
  parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
1916
902
  parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
1917
- parser.add_argument('--use-api', action='store_true', help='[DEPRECATED] Fetch setup commands from original API (use --repo-url for Agent instead)')
1918
- parser.add_argument('--use-gitingest', action='store_true', default=True, help='[DEPRECATED] Use gitingest approach (Agent is now used when --repo-url is provided)')
1919
- parser.add_argument('--no-gitingest', action='store_true', help='[DEPRECATED] Disable gitingest approach (no longer needed with Agent)')
1920
903
  parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
1921
904
  parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
1922
905
  parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
@@ -1927,6 +910,7 @@ if __name__ == "__main__":
1927
910
  parser.add_argument('--gpu', default='A10G', help='GPU type to use')
1928
911
  parser.add_argument('--gpu-count', type=int, default=1, help='Number of GPUs to use (default: 1)')
1929
912
  parser.add_argument('--repo-url', help='Repository URL')
913
+ parser.add_argument('--use-cuda-base', action='store_true', help='Use CUDA base image (may cause segfaults, use only if needed for CUDA libraries)')
1930
914
 
1931
915
  # Authentication-related arguments
1932
916
  parser.add_argument('--auth', action='store_true', help='Manage authentication (login, register, logout)')
@@ -2050,8 +1034,6 @@ if __name__ == "__main__":
2050
1034
  print(f"Volume: {args.volume_name or 'None'}")
2051
1035
  if args.repo_url:
2052
1036
  print("Repository Setup: Agent (intelligent)")
2053
- elif args.use_api:
2054
- print("Setup Commands: Auto-detect from repository")
2055
1037
  elif args.setup_commands:
2056
1038
  print(f"Setup Commands: {len(args.setup_commands)} custom commands")
2057
1039
  else:
@@ -2068,7 +1050,6 @@ if __name__ == "__main__":
2068
1050
  print("\n🛑 Operation cancelled by user.")
2069
1051
  sys.exit(0)
2070
1052
  else:
2071
- # print("🔍 Debug: yes parameter = true")
2072
1053
  print("")
2073
1054
 
2074
1055
  # Interactive mode or missing required arguments
@@ -2117,25 +1098,13 @@ if __name__ == "__main__":
2117
1098
  print("\n🛑 Setup cancelled.")
2118
1099
  sys.exit(1)
2119
1100
 
2120
- # Ask about setup commands
2121
- use_gitingest = args.use_gitingest and not args.no_gitingest
2122
- if not args.use_api and not args.setup_commands and not args.setup_commands_json:
2123
- try:
2124
- auto_detect = input("? Automatically detect setup commands for this repository? (Y/n): ").strip().lower()
2125
- if auto_detect in ('n', 'no'):
2126
- use_gitingest = False
2127
- except KeyboardInterrupt:
2128
- print("\n🛑 Setup cancelled.")
2129
- sys.exit(1)
2130
-
2131
1101
  # Update args with interactive values
2132
1102
  args.repo_url = repo_url
2133
1103
  args.volume_name = volume_name
2134
1104
  args.gpu_count = gpu_count
2135
- args.use_gitingest = use_gitingest
2136
1105
 
2137
1106
  try:
2138
- # Setup commands are no longer used when repo_url is provided ( Agent handles setup)
1107
+ # Setup commands are no longer used when repo_url is provided (Agent handles setup)
2139
1108
  setup_commands = args.setup_commands or []
2140
1109
 
2141
1110
  # Repository setup approach
@@ -2145,7 +1114,6 @@ if __name__ == "__main__":
2145
1114
  else:
2146
1115
  print("⚠️ No repository URL provided - setup commands may be needed manually")
2147
1116
 
2148
-
2149
1117
  # Parse setup commands from JSON if provided
2150
1118
  if args.setup_commands_json:
2151
1119
  try:
@@ -2161,7 +1129,6 @@ if __name__ == "__main__":
2161
1129
  print(f"⚠️ Error parsing JSON setup commands: {e}")
2162
1130
  print(f"Received JSON string: {args.setup_commands_json}")
2163
1131
 
2164
-
2165
1132
  # Load commands from file if specified
2166
1133
  if args.commands_file and os.path.exists(args.commands_file):
2167
1134
  try:
@@ -2197,7 +1164,7 @@ if __name__ == "__main__":
2197
1164
  try:
2198
1165
  with open(args.setup_script, 'r') as f:
2199
1166
  script_content = f.read().strip()
2200
- # Convert script to individual commandsr
1167
+ # Convert script to individual commands
2201
1168
  script_commands = [line.strip() for line in script_content.split('\n')
2202
1169
  if line.strip() and not line.strip().startswith('#')]
2203
1170
  setup_commands.extend(script_commands)
@@ -2233,7 +1200,8 @@ if __name__ == "__main__":
2233
1200
  timeout_minutes=args.timeout,
2234
1201
  ssh_password=ssh_password,
2235
1202
  interactive=args.interactive,
2236
- gpu_count=getattr(args, 'gpu_count', 1)
1203
+ gpu_count=getattr(args, 'gpu_count', 1),
1204
+ use_cuda_base=getattr(args, 'use_cuda_base', False)
2237
1205
  )
2238
1206
  except KeyboardInterrupt:
2239
1207
  cleanup_modal_token()