gitarsenal-cli 1.9.75 → 1.9.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.venv_status.json +1 -1
- package/Step +0 -0
- package/kill_claude/prompts/claude-code-tool-prompts.md +9 -0
- package/package.json +1 -1
- package/python/__pycache__/credentials_manager.cpython-312.pyc +0 -0
- package/python/test_container_fail.py +239 -0
- package/python/test_container_pass.py +290 -0
- package/python/test_modalSandboxScript.py +378 -363
- package/kill_claude/nanoGPT/.gitattributes +0 -3
- package/kill_claude/nanoGPT/LICENSE +0 -21
- package/kill_claude/nanoGPT/README.md +0 -227
- package/kill_claude/nanoGPT/assets/gpt2_124M_loss.png +0 -0
- package/kill_claude/nanoGPT/assets/nanogpt.jpg +0 -0
- package/kill_claude/nanoGPT/bench.py +0 -117
- package/kill_claude/nanoGPT/config/eval_gpt2.py +0 -8
- package/kill_claude/nanoGPT/config/eval_gpt2_large.py +0 -8
- package/kill_claude/nanoGPT/config/eval_gpt2_medium.py +0 -8
- package/kill_claude/nanoGPT/config/eval_gpt2_xl.py +0 -8
- package/kill_claude/nanoGPT/config/finetune_shakespeare.py +0 -25
- package/kill_claude/nanoGPT/config/train_gpt2.py +0 -25
- package/kill_claude/nanoGPT/config/train_shakespeare_char.py +0 -37
- package/kill_claude/nanoGPT/configurator.py +0 -47
- package/kill_claude/nanoGPT/data/openwebtext/prepare.py +0 -81
- package/kill_claude/nanoGPT/data/openwebtext/readme.md +0 -15
- package/kill_claude/nanoGPT/data/shakespeare/prepare.py +0 -33
- package/kill_claude/nanoGPT/data/shakespeare/readme.md +0 -9
- package/kill_claude/nanoGPT/data/shakespeare_char/prepare.py +0 -68
- package/kill_claude/nanoGPT/data/shakespeare_char/readme.md +0 -9
- package/kill_claude/nanoGPT/model.py +0 -330
- package/kill_claude/nanoGPT/sample.py +0 -89
- package/kill_claude/nanoGPT/scaling_laws.ipynb +0 -792
- package/kill_claude/nanoGPT/train.py +0 -336
- package/kill_claude/nanoGPT/transformer_sizing.ipynb +0 -402
- package/python/test_container.py +0 -145
|
@@ -11,48 +11,6 @@ import string
|
|
|
11
11
|
import argparse
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
import modal
|
|
14
|
-
from auth_manager import AuthManager
|
|
15
|
-
|
|
16
|
-
# Removed unused boxed output functions since they're no longer used with the Agent-based approach
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Removed _execute_with_box function as it's no longer used with the Agent-based approach
|
|
20
|
-
|
|
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')
|
|
25
|
-
|
|
26
|
-
# Parse only proxy args early to avoid conflicts
|
|
27
|
-
early_args, _ = early_parser.parse_known_args()
|
|
28
|
-
|
|
29
|
-
# Set proxy URL and API key in environment variables if provided
|
|
30
|
-
if early_args.proxy_url:
|
|
31
|
-
os.environ["MODAL_PROXY_URL"] = early_args.proxy_url
|
|
32
|
-
|
|
33
|
-
if early_args.proxy_api_key:
|
|
34
|
-
os.environ["MODAL_PROXY_API_KEY"] = early_args.proxy_api_key
|
|
35
|
-
|
|
36
|
-
# Import the fetch_modal_tokens module
|
|
37
|
-
from fetch_modal_tokens import get_tokens
|
|
38
|
-
token_id, token_secret, openai_api_key, anthropic_api_key, openrouter_api_key, groq_api_key = get_tokens()
|
|
39
|
-
|
|
40
|
-
# Check if we got valid tokens
|
|
41
|
-
if token_id is None or token_secret is None:
|
|
42
|
-
raise ValueError("Could not get valid tokens")
|
|
43
|
-
|
|
44
|
-
# Explicitly set the environment variables again to be sure
|
|
45
|
-
os.environ["MODAL_TOKEN_ID"] = token_id
|
|
46
|
-
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
|
47
|
-
if openai_api_key:
|
|
48
|
-
os.environ["OPENAI_API_KEY"] = openai_api_key
|
|
49
|
-
if anthropic_api_key:
|
|
50
|
-
os.environ["ANTHROPIC_API_KEY"] = anthropic_api_key
|
|
51
|
-
# Also set the old environment variable for backward compatibility
|
|
52
|
-
os.environ["MODAL_TOKEN"] = token_id
|
|
53
|
-
|
|
54
|
-
# Set token variables for later use
|
|
55
|
-
token = token_id # For backward compatibility
|
|
56
14
|
|
|
57
15
|
|
|
58
16
|
def generate_random_password(length=16):
|
|
@@ -80,9 +38,281 @@ def get_stored_credentials():
|
|
|
80
38
|
return {}
|
|
81
39
|
|
|
82
40
|
|
|
41
|
+
# Global SSH container function (must be at global scope for Modal)
|
|
42
|
+
def ssh_container_function(ssh_password=None, repo_url=None, repo_name=None, setup_commands=None, openai_api_key=None, anthropic_api_key=None, stored_credentials=None):
|
|
43
|
+
"""Start SSH container with password authentication and intelligent repository setup using Agent."""
|
|
44
|
+
import subprocess
|
|
45
|
+
import time
|
|
46
|
+
import os
|
|
47
|
+
import json
|
|
48
|
+
import sys
|
|
49
|
+
|
|
50
|
+
# Set root password
|
|
51
|
+
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
|
52
|
+
|
|
53
|
+
# Set OpenAI API key if provided
|
|
54
|
+
if openai_api_key:
|
|
55
|
+
os.environ['OPENAI_API_KEY'] = openai_api_key
|
|
56
|
+
else:
|
|
57
|
+
print("⚠️ No OpenAI API key provided to container")
|
|
58
|
+
|
|
59
|
+
# Set up stored credentials in container environment
|
|
60
|
+
if stored_credentials:
|
|
61
|
+
print(f"🔐 Setting up {len(stored_credentials)} stored credentials in container...")
|
|
62
|
+
for key, value in stored_credentials.items():
|
|
63
|
+
# Set each credential as an environment variable
|
|
64
|
+
env_var_name = key.upper().replace('-', '_').replace(' ', '_')
|
|
65
|
+
os.environ[env_var_name] = value
|
|
66
|
+
print(f"✅ Set {env_var_name} in container environment")
|
|
67
|
+
|
|
68
|
+
# Also save credentials to a file in the container for easy access
|
|
69
|
+
credentials_dir = "/root/.gitarsenal"
|
|
70
|
+
os.makedirs(credentials_dir, exist_ok=True)
|
|
71
|
+
credentials_file = os.path.join(credentials_dir, "credentials.json")
|
|
72
|
+
with open(credentials_file, 'w') as f:
|
|
73
|
+
json.dump(stored_credentials, f, indent=2)
|
|
74
|
+
print(f"✅ Saved credentials to {credentials_file}")
|
|
75
|
+
|
|
76
|
+
# Print available credentials for user reference
|
|
77
|
+
print("\n🔐 AVAILABLE CREDENTIALS IN CONTAINER:")
|
|
78
|
+
print("="*50)
|
|
79
|
+
for key, value in stored_credentials.items():
|
|
80
|
+
masked_value = value[:8] + "..." if len(value) > 8 else "***"
|
|
81
|
+
env_var_name = key.upper().replace('-', '_').replace(' ', '_')
|
|
82
|
+
print(f" {key} -> {env_var_name} = {masked_value}")
|
|
83
|
+
print("="*50)
|
|
84
|
+
print("💡 These credentials are available as environment variables and in /root/.gitarsenal/credentials.json")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Start SSH service
|
|
88
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
|
89
|
+
|
|
90
|
+
# Use Agent for intelligent repository setup
|
|
91
|
+
if repo_url:
|
|
92
|
+
print("🤖 Using Agent for intelligent repository setup...")
|
|
93
|
+
|
|
94
|
+
# Set up environment variables for the Agent
|
|
95
|
+
if openai_api_key:
|
|
96
|
+
os.environ['OPENAI_API_KEY'] = openai_api_key
|
|
97
|
+
if anthropic_api_key:
|
|
98
|
+
os.environ['ANTHROPIC_API_KEY'] = anthropic_api_key
|
|
99
|
+
|
|
100
|
+
# Set up Anthropic API key from stored credentials
|
|
101
|
+
if stored_credentials:
|
|
102
|
+
# Look for Anthropic API key in various possible names
|
|
103
|
+
for key_name in ['ANTHROPIC_API_KEY', 'anthropic_api_key', 'anthropic-api-key']:
|
|
104
|
+
if key_name in stored_credentials:
|
|
105
|
+
anthropic_api_key = stored_credentials[key_name]
|
|
106
|
+
os.environ['ANTHROPIC_API_KEY'] = anthropic_api_key
|
|
107
|
+
print(f"✅ Set Anthropic API key from stored credentials")
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
if not anthropic_api_key:
|
|
111
|
+
print("⚠️ No Anthropic API key found in stored credentials")
|
|
112
|
+
print("💡 Agent will require an Anthropic API key for operation")
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
print("🔧 Running Agent for repository setup...")
|
|
116
|
+
|
|
117
|
+
print("\n" + "="*80)
|
|
118
|
+
print("🤖 AGENT REPOSITORY SETUP")
|
|
119
|
+
print("="*80)
|
|
120
|
+
print(f"Repository: {repo_url}")
|
|
121
|
+
print(f"Working Directory: /root")
|
|
122
|
+
if stored_credentials:
|
|
123
|
+
print(f"Available Credentials: {len(stored_credentials)} items")
|
|
124
|
+
print("="*80 + "\n")
|
|
125
|
+
|
|
126
|
+
# Call Agent directly as subprocess with real-time output
|
|
127
|
+
claude_prompt = f"clone, setup and run {repo_url}"
|
|
128
|
+
print(f"🚀 Executing the task: \"{claude_prompt}\"")
|
|
129
|
+
print("\n" + "="*60)
|
|
130
|
+
print("🎉 AGENT OUTPUT (LIVE)")
|
|
131
|
+
print("="*60)
|
|
132
|
+
|
|
133
|
+
# Use Popen for real-time output streaming with optimizations
|
|
134
|
+
import sys
|
|
135
|
+
import select
|
|
136
|
+
import fcntl
|
|
137
|
+
import os as os_module
|
|
138
|
+
|
|
139
|
+
process = subprocess.Popen(
|
|
140
|
+
["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt], # -u for unbuffered output
|
|
141
|
+
cwd="/root",
|
|
142
|
+
stdout=subprocess.PIPE,
|
|
143
|
+
stderr=subprocess.PIPE, # Keep separate for better handling
|
|
144
|
+
text=True,
|
|
145
|
+
bufsize=0, # Unbuffered for fastest output
|
|
146
|
+
universal_newlines=True,
|
|
147
|
+
env=dict(os.environ, PYTHONUNBUFFERED='1') # Force unbuffered Python output
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Make stdout and stderr non-blocking for faster reading
|
|
151
|
+
def make_non_blocking(fd):
|
|
152
|
+
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
153
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os_module.O_NONBLOCK)
|
|
154
|
+
|
|
155
|
+
make_non_blocking(process.stdout)
|
|
156
|
+
make_non_blocking(process.stderr)
|
|
157
|
+
|
|
158
|
+
# Stream output in real-time with robust error handling
|
|
159
|
+
try:
|
|
160
|
+
stdout_buffer = ""
|
|
161
|
+
stderr_buffer = ""
|
|
162
|
+
|
|
163
|
+
while process.poll() is None:
|
|
164
|
+
try:
|
|
165
|
+
# Use select for efficient I/O multiplexing with error handling
|
|
166
|
+
ready, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1) # 100ms timeout
|
|
167
|
+
|
|
168
|
+
for stream in ready:
|
|
169
|
+
try:
|
|
170
|
+
if stream == process.stdout:
|
|
171
|
+
chunk = stream.read(1024) # Read in chunks for efficiency
|
|
172
|
+
if chunk is not None and chunk:
|
|
173
|
+
stdout_buffer += chunk
|
|
174
|
+
# Process complete lines immediately
|
|
175
|
+
while '\n' in stdout_buffer:
|
|
176
|
+
line, stdout_buffer = stdout_buffer.split('\n', 1)
|
|
177
|
+
print(line, flush=True) # Force immediate flush
|
|
178
|
+
elif stream == process.stderr:
|
|
179
|
+
chunk = stream.read(1024)
|
|
180
|
+
if chunk is not None and chunk:
|
|
181
|
+
stderr_buffer += chunk
|
|
182
|
+
# Process complete lines immediately
|
|
183
|
+
while '\n' in stderr_buffer:
|
|
184
|
+
line, stderr_buffer = stderr_buffer.split('\n', 1)
|
|
185
|
+
print(f"STDERR: {line}", flush=True)
|
|
186
|
+
except (BlockingIOError, OSError, ValueError):
|
|
187
|
+
# Handle various I/O errors gracefully
|
|
188
|
+
continue
|
|
189
|
+
except (select.error, OSError):
|
|
190
|
+
# If select fails, fall back to simple polling
|
|
191
|
+
time.sleep(0.1)
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
# Process any remaining output after process ends
|
|
195
|
+
try:
|
|
196
|
+
# Read any remaining data from streams
|
|
197
|
+
remaining_stdout = process.stdout.read()
|
|
198
|
+
remaining_stderr = process.stderr.read()
|
|
199
|
+
|
|
200
|
+
if remaining_stdout:
|
|
201
|
+
stdout_buffer += remaining_stdout
|
|
202
|
+
if remaining_stderr:
|
|
203
|
+
stderr_buffer += remaining_stderr
|
|
204
|
+
|
|
205
|
+
# Output remaining buffered content
|
|
206
|
+
if stdout_buffer.strip():
|
|
207
|
+
print(stdout_buffer.strip(), flush=True)
|
|
208
|
+
if stderr_buffer.strip():
|
|
209
|
+
print(f"STDERR: {stderr_buffer.strip()}", flush=True)
|
|
210
|
+
except (OSError, ValueError):
|
|
211
|
+
# Handle cases where streams are already closed
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
# Get final return code
|
|
215
|
+
return_code = process.returncode
|
|
216
|
+
|
|
217
|
+
print("\n" + "="*60)
|
|
218
|
+
if return_code == 0:
|
|
219
|
+
print("✅ Agent completed successfully!")
|
|
220
|
+
else:
|
|
221
|
+
print(f"⚠️ Agent exited with code: {return_code}")
|
|
222
|
+
print("="*60)
|
|
223
|
+
|
|
224
|
+
except subprocess.TimeoutExpired:
|
|
225
|
+
print("\n⚠️ Agent timed out after 10 minutes")
|
|
226
|
+
process.kill()
|
|
227
|
+
process.wait()
|
|
228
|
+
except Exception as stream_error:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
# Fallback to simple readline approach
|
|
232
|
+
try:
|
|
233
|
+
# Restart the process with simpler streaming
|
|
234
|
+
if process.poll() is None:
|
|
235
|
+
process.kill()
|
|
236
|
+
process.wait()
|
|
237
|
+
|
|
238
|
+
fallback_process = subprocess.Popen(
|
|
239
|
+
["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt],
|
|
240
|
+
cwd="/root",
|
|
241
|
+
stdout=subprocess.PIPE,
|
|
242
|
+
stderr=subprocess.STDOUT,
|
|
243
|
+
text=True,
|
|
244
|
+
bufsize=1,
|
|
245
|
+
universal_newlines=True
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Simple line-by-line reading
|
|
249
|
+
while True:
|
|
250
|
+
line = fallback_process.stdout.readline()
|
|
251
|
+
if line == '' and fallback_process.poll() is not None:
|
|
252
|
+
break
|
|
253
|
+
if line:
|
|
254
|
+
print(line.rstrip(), flush=True)
|
|
255
|
+
|
|
256
|
+
return_code = fallback_process.returncode
|
|
257
|
+
|
|
258
|
+
print("\n" + "="*60)
|
|
259
|
+
if return_code == 0:
|
|
260
|
+
print("✅ Agent completed successfully!")
|
|
261
|
+
else:
|
|
262
|
+
print(f"⚠️ Agent exited with code: {return_code}")
|
|
263
|
+
print("="*60)
|
|
264
|
+
|
|
265
|
+
except Exception as fallback_error:
|
|
266
|
+
print(f"\n❌ Fallback streaming also failed: {fallback_error}")
|
|
267
|
+
print("⚠️ Agent may have completed, but output streaming failed")
|
|
268
|
+
return_code = 1
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
print(f"❌ Error during repository setup: {e}")
|
|
272
|
+
print("⚠️ Proceeding without setup...")
|
|
273
|
+
import traceback
|
|
274
|
+
traceback.print_exc()
|
|
275
|
+
else:
|
|
276
|
+
print("⚠️ No repository URL provided, skipping setup")
|
|
277
|
+
|
|
278
|
+
print("🔌 Creating SSH tunnel on port 22...")
|
|
279
|
+
# Create SSH tunnel
|
|
280
|
+
with modal.forward(22, unencrypted=True) as tunnel:
|
|
281
|
+
host, port = tunnel.tcp_socket
|
|
282
|
+
|
|
283
|
+
print("\n" + "=" * 80)
|
|
284
|
+
print("🎉 SSH CONTAINER IS READY!")
|
|
285
|
+
print("=" * 80)
|
|
286
|
+
print(f"🌐 SSH Host: {host}")
|
|
287
|
+
print(f"🔌 SSH Port: {port}")
|
|
288
|
+
print(f"👤 Username: root")
|
|
289
|
+
print(f"🔐 Password: {ssh_password}")
|
|
290
|
+
print()
|
|
291
|
+
print("🔗 CONNECT USING THIS COMMAND:")
|
|
292
|
+
print(f"ssh -p {port} root@{host}")
|
|
293
|
+
print("=" * 80)
|
|
294
|
+
|
|
295
|
+
print("🔄 Starting keep-alive loop...")
|
|
296
|
+
# Keep the container running
|
|
297
|
+
iteration = 0
|
|
298
|
+
while True:
|
|
299
|
+
iteration += 1
|
|
300
|
+
if iteration % 10 == 1: # Print every 5 minutes (10 * 30 seconds = 5 minutes)
|
|
301
|
+
print(f"💓 Container alive (iteration {iteration})")
|
|
302
|
+
|
|
303
|
+
time.sleep(30)
|
|
304
|
+
# Check if SSH service is still running
|
|
305
|
+
try:
|
|
306
|
+
subprocess.run(["service", "ssh", "status"], check=True,
|
|
307
|
+
capture_output=True)
|
|
308
|
+
except subprocess.CalledProcessError:
|
|
309
|
+
print("⚠️ SSH service stopped, restarting...")
|
|
310
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
|
311
|
+
|
|
312
|
+
|
|
83
313
|
# Create Modal SSH container with GPU support and intelligent repository setup using Agent
|
|
84
314
|
def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
|
|
85
|
-
volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False, gpu_count=1
|
|
315
|
+
volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False, gpu_count=1):
|
|
86
316
|
"""Create a Modal SSH container with GPU support and intelligent repository setup.
|
|
87
317
|
|
|
88
318
|
When repo_url is provided, uses Agent for intelligent repository setup.
|
|
@@ -240,25 +470,20 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
240
470
|
|
|
241
471
|
# Choose base image to avoid CUDA segfault issues
|
|
242
472
|
print("⚠️ Using CUDA base image - this may cause segfaults on some systems")
|
|
243
|
-
|
|
244
|
-
base_image = modal.Image.debian_slim()
|
|
245
|
-
pip_cmd = "uv_pip_install"
|
|
473
|
+
base_image = modal.Image.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.11")
|
|
474
|
+
# base_image = modal.Image.debian_slim()
|
|
246
475
|
|
|
247
476
|
# Build the SSH image with the chosen base
|
|
248
477
|
ssh_image = (
|
|
249
478
|
base_image
|
|
250
479
|
.apt_install(
|
|
251
480
|
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
|
252
|
-
"python3", "python3-pip"
|
|
253
|
-
"gpg", "ca-certificates", "software-properties-common"
|
|
481
|
+
"python3", "python3-pip"
|
|
254
482
|
)
|
|
255
483
|
)
|
|
256
484
|
|
|
257
485
|
# Add Python packages using the appropriate method
|
|
258
|
-
|
|
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")
|
|
486
|
+
ssh_image = ssh_image.uv_pip_install("uv", "modal", "gitingest", "requests", "openai", "anthropic", "exa-py")
|
|
262
487
|
|
|
263
488
|
# Add the rest of the configuration
|
|
264
489
|
ssh_image = ssh_image.run_commands(
|
|
@@ -266,21 +491,11 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
266
491
|
"mkdir -p /var/run/sshd",
|
|
267
492
|
"mkdir -p /root/.ssh",
|
|
268
493
|
"chmod 700 /root/.ssh",
|
|
269
|
-
|
|
270
|
-
|
|
494
|
+
|
|
495
|
+
"ssh-keygen -A",
|
|
271
496
|
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
|
272
497
|
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
|
273
|
-
"
|
|
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",
|
|
498
|
+
"echo 'export PATH=/usr/local/cuda/bin:$PATH' >> /root/.bashrc"
|
|
284
499
|
|
|
285
500
|
# Create base directories (subdirectories will be created automatically when mounting)
|
|
286
501
|
"mkdir -p /python",
|
|
@@ -293,306 +508,27 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
293
508
|
volumes_config[volume_mount_path] = volume
|
|
294
509
|
|
|
295
510
|
# Create app with image passed directly (THIS IS THE KEY CHANGE)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
print("✅ Created app successfully")
|
|
300
|
-
except Exception as e:
|
|
301
|
-
print(f"❌ Error creating app: {e}")
|
|
302
|
-
return None
|
|
511
|
+
print("🔍 Testing app creation...")
|
|
512
|
+
app = modal.App(app_name, image=ssh_image) # Pass image here
|
|
513
|
+
print("✅ Created app successfully")
|
|
303
514
|
|
|
304
|
-
#
|
|
305
|
-
|
|
515
|
+
# Apply the decorator to the global SSH container function
|
|
516
|
+
decorated_ssh_function = app.function(
|
|
306
517
|
timeout=timeout_minutes * 60, # Convert to seconds
|
|
307
518
|
gpu=gpu_spec['modal_gpu'], # Use the user-selected GPU type and count
|
|
308
|
-
serialized=True,
|
|
309
519
|
volumes=volumes_config if volumes_config else None,
|
|
310
|
-
)
|
|
311
|
-
def ssh_container_function(ssh_password=None, repo_url=None, repo_name=None, setup_commands=None, openai_api_key=None, anthropic_api_key=None, stored_credentials=None):
|
|
312
|
-
"""Start SSH container with password authentication and intelligent repository setup using Agent."""
|
|
313
|
-
import subprocess
|
|
314
|
-
import time
|
|
315
|
-
import os
|
|
316
|
-
import json
|
|
317
|
-
import sys
|
|
318
|
-
|
|
319
|
-
# Add the mounted python directory to the Python path
|
|
320
|
-
# sys.path.insert(0, "/python")
|
|
321
|
-
|
|
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")
|
|
325
|
-
|
|
326
|
-
# Set root password
|
|
327
|
-
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
|
328
|
-
|
|
329
|
-
# Set OpenAI API key if provided
|
|
330
|
-
if openai_api_key:
|
|
331
|
-
os.environ['OPENAI_API_KEY'] = openai_api_key
|
|
332
|
-
else:
|
|
333
|
-
print("⚠️ No OpenAI API key provided to container")
|
|
334
|
-
|
|
335
|
-
# Set up stored credentials in container environment
|
|
336
|
-
if stored_credentials:
|
|
337
|
-
print(f"🔐 Setting up {len(stored_credentials)} stored credentials in container...")
|
|
338
|
-
for key, value in stored_credentials.items():
|
|
339
|
-
# Set each credential as an environment variable
|
|
340
|
-
env_var_name = key.upper().replace('-', '_').replace(' ', '_')
|
|
341
|
-
os.environ[env_var_name] = value
|
|
342
|
-
print(f"✅ Set {env_var_name} in container environment")
|
|
343
|
-
|
|
344
|
-
# Also save credentials to a file in the container for easy access
|
|
345
|
-
try:
|
|
346
|
-
credentials_dir = "/root/.gitarsenal"
|
|
347
|
-
os.makedirs(credentials_dir, exist_ok=True)
|
|
348
|
-
credentials_file = os.path.join(credentials_dir, "credentials.json")
|
|
349
|
-
with open(credentials_file, 'w') as f:
|
|
350
|
-
json.dump(stored_credentials, f, indent=2)
|
|
351
|
-
print(f"✅ Saved credentials to {credentials_file}")
|
|
352
|
-
|
|
353
|
-
# Print available credentials for user reference
|
|
354
|
-
print("\n🔐 AVAILABLE CREDENTIALS IN CONTAINER:")
|
|
355
|
-
print("="*50)
|
|
356
|
-
for key, value in stored_credentials.items():
|
|
357
|
-
masked_value = value[:8] + "..." if len(value) > 8 else "***"
|
|
358
|
-
env_var_name = key.upper().replace('-', '_').replace(' ', '_')
|
|
359
|
-
print(f" {key} -> {env_var_name} = {masked_value}")
|
|
360
|
-
print("="*50)
|
|
361
|
-
print("💡 These credentials are available as environment variables and in /root/.gitarsenal/credentials.json")
|
|
362
|
-
|
|
363
|
-
except Exception as e:
|
|
364
|
-
print(f"⚠️ Could not save credentials file: {e}")
|
|
365
|
-
else:
|
|
366
|
-
print("⚠️ No stored credentials provided to container")
|
|
367
|
-
|
|
368
|
-
# Start SSH service
|
|
369
|
-
subprocess.run(["service", "ssh", "start"], check=True)
|
|
370
|
-
|
|
371
|
-
# Use Agent for intelligent repository setup
|
|
372
|
-
if repo_url:
|
|
373
|
-
print("🤖 Using Agent for intelligent repository setup...")
|
|
374
|
-
|
|
375
|
-
# Set up environment variables for the Agent
|
|
376
|
-
if openai_api_key:
|
|
377
|
-
os.environ['OPENAI_API_KEY'] = openai_api_key
|
|
378
|
-
if anthropic_api_key:
|
|
379
|
-
os.environ['ANTHROPIC_API_KEY'] = anthropic_api_key
|
|
380
|
-
|
|
381
|
-
# Set up Anthropic API key from stored credentials
|
|
382
|
-
anthropic_api_key = None
|
|
383
|
-
if stored_credentials:
|
|
384
|
-
# Look for Anthropic API key in various possible names
|
|
385
|
-
for key_name in ['ANTHROPIC_API_KEY', 'anthropic_api_key', 'anthropic-api-key']:
|
|
386
|
-
if key_name in stored_credentials:
|
|
387
|
-
anthropic_api_key = stored_credentials[key_name]
|
|
388
|
-
os.environ['ANTHROPIC_API_KEY'] = anthropic_api_key
|
|
389
|
-
print(f"✅ Set Anthropic API key from stored credentials")
|
|
390
|
-
break
|
|
391
|
-
|
|
392
|
-
if not anthropic_api_key:
|
|
393
|
-
print("⚠️ No Anthropic API key found in stored credentials")
|
|
394
|
-
print("💡 Agent will require an Anthropic API key for operation")
|
|
395
|
-
|
|
396
|
-
try:
|
|
397
|
-
print("🔧 Running Agent for repository setup...")
|
|
398
|
-
|
|
399
|
-
print("\n" + "="*80)
|
|
400
|
-
print("🤖 AGENT REPOSITORY SETUP")
|
|
401
|
-
print("="*80)
|
|
402
|
-
print(f"Repository: {repo_url}")
|
|
403
|
-
print(f"Working Directory: /root")
|
|
404
|
-
if stored_credentials:
|
|
405
|
-
print(f"Available Credentials: {len(stored_credentials)} items")
|
|
406
|
-
print("="*80 + "\n")
|
|
407
|
-
|
|
408
|
-
# Call Agent directly as subprocess with real-time output
|
|
409
|
-
claude_prompt = f"clone, setup and run {repo_url}"
|
|
410
|
-
print(f"🚀 Executing the task: \"{claude_prompt}\"")
|
|
411
|
-
print("\n" + "="*60)
|
|
412
|
-
print("🎉 AGENT OUTPUT (LIVE)")
|
|
413
|
-
print("="*60)
|
|
414
|
-
|
|
415
|
-
# Use Popen for real-time output streaming with optimizations
|
|
416
|
-
import sys
|
|
417
|
-
import select
|
|
418
|
-
import fcntl
|
|
419
|
-
import os as os_module
|
|
420
|
-
|
|
421
|
-
process = subprocess.Popen(
|
|
422
|
-
["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt], # -u for unbuffered output
|
|
423
|
-
cwd="/root",
|
|
424
|
-
stdout=subprocess.PIPE,
|
|
425
|
-
stderr=subprocess.PIPE, # Keep separate for better handling
|
|
426
|
-
text=True,
|
|
427
|
-
bufsize=0, # Unbuffered for fastest output
|
|
428
|
-
universal_newlines=True,
|
|
429
|
-
env=dict(os.environ, PYTHONUNBUFFERED='1') # Force unbuffered Python output
|
|
430
|
-
)
|
|
431
|
-
|
|
432
|
-
# Make stdout and stderr non-blocking for faster reading
|
|
433
|
-
def make_non_blocking(fd):
|
|
434
|
-
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
435
|
-
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os_module.O_NONBLOCK)
|
|
436
|
-
|
|
437
|
-
make_non_blocking(process.stdout)
|
|
438
|
-
make_non_blocking(process.stderr)
|
|
439
|
-
|
|
440
|
-
# Stream output in real-time with robust error handling
|
|
441
|
-
try:
|
|
442
|
-
stdout_buffer = ""
|
|
443
|
-
stderr_buffer = ""
|
|
444
|
-
|
|
445
|
-
while process.poll() is None:
|
|
446
|
-
try:
|
|
447
|
-
# Use select for efficient I/O multiplexing with error handling
|
|
448
|
-
ready, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1) # 100ms timeout
|
|
449
|
-
|
|
450
|
-
for stream in ready:
|
|
451
|
-
try:
|
|
452
|
-
if stream == process.stdout:
|
|
453
|
-
chunk = stream.read(1024) # Read in chunks for efficiency
|
|
454
|
-
if chunk is not None and chunk:
|
|
455
|
-
stdout_buffer += chunk
|
|
456
|
-
# Process complete lines immediately
|
|
457
|
-
while '\n' in stdout_buffer:
|
|
458
|
-
line, stdout_buffer = stdout_buffer.split('\n', 1)
|
|
459
|
-
print(line, flush=True) # Force immediate flush
|
|
460
|
-
elif stream == process.stderr:
|
|
461
|
-
chunk = stream.read(1024)
|
|
462
|
-
if chunk is not None and chunk:
|
|
463
|
-
stderr_buffer += chunk
|
|
464
|
-
# Process complete lines immediately
|
|
465
|
-
while '\n' in stderr_buffer:
|
|
466
|
-
line, stderr_buffer = stderr_buffer.split('\n', 1)
|
|
467
|
-
print(f"STDERR: {line}", flush=True)
|
|
468
|
-
except (BlockingIOError, OSError, ValueError):
|
|
469
|
-
# Handle various I/O errors gracefully
|
|
470
|
-
continue
|
|
471
|
-
except (select.error, OSError):
|
|
472
|
-
# If select fails, fall back to simple polling
|
|
473
|
-
time.sleep(0.1)
|
|
474
|
-
continue
|
|
475
|
-
|
|
476
|
-
# Process any remaining output after process ends
|
|
477
|
-
try:
|
|
478
|
-
# Read any remaining data from streams
|
|
479
|
-
remaining_stdout = process.stdout.read()
|
|
480
|
-
remaining_stderr = process.stderr.read()
|
|
481
|
-
|
|
482
|
-
if remaining_stdout:
|
|
483
|
-
stdout_buffer += remaining_stdout
|
|
484
|
-
if remaining_stderr:
|
|
485
|
-
stderr_buffer += remaining_stderr
|
|
486
|
-
|
|
487
|
-
# Output remaining buffered content
|
|
488
|
-
if stdout_buffer.strip():
|
|
489
|
-
print(stdout_buffer.strip(), flush=True)
|
|
490
|
-
if stderr_buffer.strip():
|
|
491
|
-
print(f"STDERR: {stderr_buffer.strip()}", flush=True)
|
|
492
|
-
except (OSError, ValueError):
|
|
493
|
-
# Handle cases where streams are already closed
|
|
494
|
-
pass
|
|
495
|
-
|
|
496
|
-
# Get final return code
|
|
497
|
-
return_code = process.returncode
|
|
498
|
-
|
|
499
|
-
print("\n" + "="*60)
|
|
500
|
-
if return_code == 0:
|
|
501
|
-
print("✅ Agent completed successfully!")
|
|
502
|
-
else:
|
|
503
|
-
print(f"⚠️ Agent exited with code: {return_code}")
|
|
504
|
-
print("="*60)
|
|
505
|
-
|
|
506
|
-
except subprocess.TimeoutExpired:
|
|
507
|
-
print("\n⚠️ Agent timed out after 10 minutes")
|
|
508
|
-
process.kill()
|
|
509
|
-
process.wait()
|
|
510
|
-
except Exception as stream_error:
|
|
511
|
-
pass
|
|
512
|
-
|
|
513
|
-
# Fallback to simple readline approach
|
|
514
|
-
try:
|
|
515
|
-
# Restart the process with simpler streaming
|
|
516
|
-
if process.poll() is None:
|
|
517
|
-
process.kill()
|
|
518
|
-
process.wait()
|
|
519
|
-
|
|
520
|
-
fallback_process = subprocess.Popen(
|
|
521
|
-
["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt],
|
|
522
|
-
cwd="/root",
|
|
523
|
-
stdout=subprocess.PIPE,
|
|
524
|
-
stderr=subprocess.STDOUT,
|
|
525
|
-
text=True,
|
|
526
|
-
bufsize=1,
|
|
527
|
-
universal_newlines=True
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
# Simple line-by-line reading
|
|
531
|
-
while True:
|
|
532
|
-
line = fallback_process.stdout.readline()
|
|
533
|
-
if line == '' and fallback_process.poll() is not None:
|
|
534
|
-
break
|
|
535
|
-
if line:
|
|
536
|
-
print(line.rstrip(), flush=True)
|
|
537
|
-
|
|
538
|
-
return_code = fallback_process.returncode
|
|
539
|
-
|
|
540
|
-
print("\n" + "="*60)
|
|
541
|
-
if return_code == 0:
|
|
542
|
-
print("✅ Agent completed successfully!")
|
|
543
|
-
else:
|
|
544
|
-
print(f"⚠️ Agent exited with code: {return_code}")
|
|
545
|
-
print("="*60)
|
|
546
|
-
|
|
547
|
-
except Exception as fallback_error:
|
|
548
|
-
print(f"\n❌ Fallback streaming also failed: {fallback_error}")
|
|
549
|
-
print("⚠️ Agent may have completed, but output streaming failed")
|
|
550
|
-
return_code = 1
|
|
551
|
-
|
|
552
|
-
except Exception as e:
|
|
553
|
-
print(f"❌ Error during repository setup: {e}")
|
|
554
|
-
print("⚠️ Proceeding without setup...")
|
|
555
|
-
import traceback
|
|
556
|
-
traceback.print_exc()
|
|
557
|
-
else:
|
|
558
|
-
print("⚠️ No repository URL provided, skipping setup")
|
|
559
|
-
|
|
560
|
-
# Create SSH tunnel
|
|
561
|
-
with modal.forward(22, unencrypted=True) as tunnel:
|
|
562
|
-
host, port = tunnel.tcp_socket
|
|
563
|
-
|
|
564
|
-
print("\n" + "=" * 80)
|
|
565
|
-
print("🎉 SSH CONTAINER IS READY!")
|
|
566
|
-
print("=" * 80)
|
|
567
|
-
print(f"🌐 SSH Host: {host}")
|
|
568
|
-
print(f"🔌 SSH Port: {port}")
|
|
569
|
-
print(f"👤 Username: root")
|
|
570
|
-
print(f"🔐 Password: {ssh_password}")
|
|
571
|
-
print()
|
|
572
|
-
print("🔗 CONNECT USING THIS COMMAND:")
|
|
573
|
-
print(f"ssh -p {port} root@{host}")
|
|
574
|
-
print("=" * 80)
|
|
575
|
-
|
|
576
|
-
# Keep the container running
|
|
577
|
-
while True:
|
|
578
|
-
time.sleep(30)
|
|
579
|
-
# Check if SSH service is still running
|
|
580
|
-
try:
|
|
581
|
-
subprocess.run(["service", "ssh", "status"], check=True,
|
|
582
|
-
capture_output=True)
|
|
583
|
-
except subprocess.CalledProcessError:
|
|
584
|
-
print("⚠️ SSH service stopped, restarting...")
|
|
585
|
-
subprocess.run(["service", "ssh", "start"], check=True)
|
|
520
|
+
)(ssh_container_function)
|
|
586
521
|
|
|
587
522
|
# Run the container
|
|
588
523
|
try:
|
|
589
524
|
print("⏳ Starting container... This may take 1-2 minutes...")
|
|
590
525
|
|
|
591
|
-
# Start the container
|
|
526
|
+
# Start the container and wait for it to complete (blocking)
|
|
592
527
|
with modal.enable_output():
|
|
593
528
|
with app.run():
|
|
594
529
|
# Get the API key from environment
|
|
595
|
-
|
|
530
|
+
openai_api_key = os.environ.get("OPENAI_API_KEY")
|
|
531
|
+
anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
596
532
|
|
|
597
533
|
# Get stored credentials from local file
|
|
598
534
|
stored_credentials = get_stored_credentials()
|
|
@@ -601,9 +537,55 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
601
537
|
else:
|
|
602
538
|
print("⚠️ No stored credentials found")
|
|
603
539
|
|
|
604
|
-
|
|
540
|
+
# Use spawn() to get a FunctionCall handle, then wait for it
|
|
541
|
+
print("🚀 Spawning SSH container...")
|
|
542
|
+
try:
|
|
543
|
+
function_call = decorated_ssh_function.spawn(ssh_password, repo_url, repo_name, setup_commands, openai_api_key, anthropic_api_key, stored_credentials)
|
|
544
|
+
print(f"✅ Container spawned with call ID: {function_call.object_id}")
|
|
545
|
+
print(f"🔍 Function call status: {function_call}")
|
|
546
|
+
except Exception as spawn_error:
|
|
547
|
+
print(f"❌ Error during spawn: {spawn_error}")
|
|
548
|
+
raise
|
|
549
|
+
|
|
550
|
+
try:
|
|
551
|
+
# Wait for the function to start and print connection info (with timeout)
|
|
552
|
+
print("⏳ Waiting for container to initialize...")
|
|
553
|
+
try:
|
|
554
|
+
print("\n⏳ Monitoring container (press Ctrl+C to stop monitoring)...")
|
|
555
|
+
result = function_call.get() # Wait indefinitely
|
|
556
|
+
print(f"🔚 Container function completed with result: {result}")
|
|
557
|
+
except KeyboardInterrupt:
|
|
558
|
+
print("\n🛑 Stopped monitoring. Container is still running remotely.")
|
|
559
|
+
print("💡 Use Modal's web UI or CLI to stop the container when done.")
|
|
560
|
+
print("🔒 Keeping tokens active since container is still running.")
|
|
561
|
+
return {
|
|
562
|
+
"app_name": app_name,
|
|
563
|
+
"ssh_password": ssh_password,
|
|
564
|
+
"volume_name": volume_name,
|
|
565
|
+
"status": "monitoring_stopped",
|
|
566
|
+
"function_call_id": function_call.object_id
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
except KeyboardInterrupt:
|
|
570
|
+
print("\n🛑 Interrupted by user. Container may still be running remotely.")
|
|
571
|
+
print("💡 Use Modal's web UI or CLI to check running containers.")
|
|
572
|
+
print("🔒 Keeping tokens active since container may still be running.")
|
|
573
|
+
return {
|
|
574
|
+
"app_name": app_name,
|
|
575
|
+
"ssh_password": ssh_password,
|
|
576
|
+
"volume_name": volume_name,
|
|
577
|
+
"status": "interrupted",
|
|
578
|
+
"function_call_id": function_call.object_id
|
|
579
|
+
}
|
|
580
|
+
except Exception as e:
|
|
581
|
+
print(f"⚠️ Container execution error: {e}")
|
|
582
|
+
print("💡 Container may still be accessible via SSH if it started successfully.")
|
|
583
|
+
print("🧹 Cleaning up tokens due to execution error.")
|
|
584
|
+
cleanup_modal_token()
|
|
585
|
+
raise
|
|
605
586
|
|
|
606
|
-
#
|
|
587
|
+
# Only clean up tokens if container actually completed normally
|
|
588
|
+
print("🧹 Container completed normally, cleaning up tokens.")
|
|
607
589
|
cleanup_modal_token()
|
|
608
590
|
|
|
609
591
|
return {
|
|
@@ -904,13 +886,10 @@ if __name__ == "__main__":
|
|
|
904
886
|
parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
|
|
905
887
|
parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
|
|
906
888
|
parser.add_argument('--yes', action='store_true', help='Automatically confirm prompts (non-interactive)')
|
|
907
|
-
|
|
908
|
-
parser.add_argument('--proxy-url', help='URL of the proxy server')
|
|
909
|
-
parser.add_argument('--proxy-api-key', help='API key for the proxy server')
|
|
889
|
+
|
|
910
890
|
parser.add_argument('--gpu', default='A10G', help='GPU type to use')
|
|
911
891
|
parser.add_argument('--gpu-count', type=int, default=1, help='Number of GPUs to use (default: 1)')
|
|
912
892
|
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)')
|
|
914
893
|
|
|
915
894
|
# Authentication-related arguments
|
|
916
895
|
parser.add_argument('--auth', action='store_true', help='Manage authentication (login, register, logout)')
|
|
@@ -930,7 +909,29 @@ if __name__ == "__main__":
|
|
|
930
909
|
|
|
931
910
|
args = parser.parse_args()
|
|
932
911
|
|
|
933
|
-
# Initialize
|
|
912
|
+
# Initialize tokens (import here to avoid container import issues)
|
|
913
|
+
from fetch_modal_tokens import get_tokens
|
|
914
|
+
token_id, token_secret, openai_api_key, anthropic_api_key, openrouter_api_key, groq_api_key = get_tokens()
|
|
915
|
+
|
|
916
|
+
# Check if we got valid tokens
|
|
917
|
+
if token_id is None or token_secret is None:
|
|
918
|
+
raise ValueError("Could not get valid tokens")
|
|
919
|
+
|
|
920
|
+
# Explicitly set the environment variables again to be sure
|
|
921
|
+
os.environ["MODAL_TOKEN_ID"] = token_id
|
|
922
|
+
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
|
923
|
+
if openai_api_key:
|
|
924
|
+
os.environ["OPENAI_API_KEY"] = openai_api_key
|
|
925
|
+
if anthropic_api_key:
|
|
926
|
+
os.environ["ANTHROPIC_API_KEY"] = anthropic_api_key
|
|
927
|
+
# Also set the old environment variable for backward compatibility
|
|
928
|
+
os.environ["MODAL_TOKEN"] = token_id
|
|
929
|
+
|
|
930
|
+
# Set token variables for later use
|
|
931
|
+
token = token_id # For backward compatibility
|
|
932
|
+
|
|
933
|
+
# Initialize authentication manager (import here to avoid container import issues)
|
|
934
|
+
from auth_manager import AuthManager
|
|
934
935
|
auth_manager = AuthManager()
|
|
935
936
|
|
|
936
937
|
# Handle authentication-related commands
|
|
@@ -1191,7 +1192,7 @@ if __name__ == "__main__":
|
|
|
1191
1192
|
repo_name = repo_name[:-4]
|
|
1192
1193
|
|
|
1193
1194
|
# Create the container
|
|
1194
|
-
create_modal_ssh_container(
|
|
1195
|
+
result = create_modal_ssh_container(
|
|
1195
1196
|
gpu_type=args.gpu,
|
|
1196
1197
|
repo_url=args.repo_url,
|
|
1197
1198
|
repo_name=repo_name,
|
|
@@ -1201,10 +1202,24 @@ if __name__ == "__main__":
|
|
|
1201
1202
|
ssh_password=ssh_password,
|
|
1202
1203
|
interactive=args.interactive,
|
|
1203
1204
|
gpu_count=getattr(args, 'gpu_count', 1),
|
|
1204
|
-
use_cuda_base=getattr(args, 'use_cuda_base', False)
|
|
1205
1205
|
)
|
|
1206
|
+
|
|
1207
|
+
if result:
|
|
1208
|
+
print(f"\n✅ Container operation completed: {result.get('status', 'success')}")
|
|
1209
|
+
if result.get('function_call_id'):
|
|
1210
|
+
print(f"🆔 Function Call ID: {result['function_call_id']}")
|
|
1211
|
+
print("💡 You can use this ID to check container status via Modal CLI")
|
|
1212
|
+
else:
|
|
1213
|
+
print("\n❌ Container creation failed")
|
|
1214
|
+
|
|
1206
1215
|
except KeyboardInterrupt:
|
|
1216
|
+
print("\n🛑 Operation cancelled by user")
|
|
1207
1217
|
cleanup_modal_token()
|
|
1208
1218
|
sys.exit(1)
|
|
1209
1219
|
except Exception as e:
|
|
1210
|
-
|
|
1220
|
+
print(f"\n❌ Unexpected error: {e}")
|
|
1221
|
+
print("📋 Error details:")
|
|
1222
|
+
import traceback
|
|
1223
|
+
traceback.print_exc()
|
|
1224
|
+
cleanup_modal_token()
|
|
1225
|
+
sys.exit(1)
|