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.
Files changed (34) hide show
  1. package/.venv_status.json +1 -1
  2. package/Step +0 -0
  3. package/kill_claude/prompts/claude-code-tool-prompts.md +9 -0
  4. package/package.json +1 -1
  5. package/python/__pycache__/credentials_manager.cpython-312.pyc +0 -0
  6. package/python/test_container_fail.py +239 -0
  7. package/python/test_container_pass.py +290 -0
  8. package/python/test_modalSandboxScript.py +378 -363
  9. package/kill_claude/nanoGPT/.gitattributes +0 -3
  10. package/kill_claude/nanoGPT/LICENSE +0 -21
  11. package/kill_claude/nanoGPT/README.md +0 -227
  12. package/kill_claude/nanoGPT/assets/gpt2_124M_loss.png +0 -0
  13. package/kill_claude/nanoGPT/assets/nanogpt.jpg +0 -0
  14. package/kill_claude/nanoGPT/bench.py +0 -117
  15. package/kill_claude/nanoGPT/config/eval_gpt2.py +0 -8
  16. package/kill_claude/nanoGPT/config/eval_gpt2_large.py +0 -8
  17. package/kill_claude/nanoGPT/config/eval_gpt2_medium.py +0 -8
  18. package/kill_claude/nanoGPT/config/eval_gpt2_xl.py +0 -8
  19. package/kill_claude/nanoGPT/config/finetune_shakespeare.py +0 -25
  20. package/kill_claude/nanoGPT/config/train_gpt2.py +0 -25
  21. package/kill_claude/nanoGPT/config/train_shakespeare_char.py +0 -37
  22. package/kill_claude/nanoGPT/configurator.py +0 -47
  23. package/kill_claude/nanoGPT/data/openwebtext/prepare.py +0 -81
  24. package/kill_claude/nanoGPT/data/openwebtext/readme.md +0 -15
  25. package/kill_claude/nanoGPT/data/shakespeare/prepare.py +0 -33
  26. package/kill_claude/nanoGPT/data/shakespeare/readme.md +0 -9
  27. package/kill_claude/nanoGPT/data/shakespeare_char/prepare.py +0 -68
  28. package/kill_claude/nanoGPT/data/shakespeare_char/readme.md +0 -9
  29. package/kill_claude/nanoGPT/model.py +0 -330
  30. package/kill_claude/nanoGPT/sample.py +0 -89
  31. package/kill_claude/nanoGPT/scaling_laws.ipynb +0 -792
  32. package/kill_claude/nanoGPT/train.py +0 -336
  33. package/kill_claude/nanoGPT/transformer_sizing.ipynb +0 -402
  34. 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, use_cuda_base=False):
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
- # 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"
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", "build-essential", "tmux", "screen", "nano",
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
- 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")
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
- # Configure SSH server
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
- "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",
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
- try:
297
- print("🔍 Testing app creation...")
298
- app = modal.App(app_name, image=ssh_image) # Pass image here
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
- # Define the SSH container function (remove image from decorator)
305
- @app.function(
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 in a new thread to avoid blocking
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
- api_key = os.environ.get("OPENAI_API_KEY")
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
- ssh_container_function.remote(ssh_password, repo_url, repo_name, setup_commands, api_key, anthropic_api_key, stored_credentials)
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
- # Clean up Modal token after container is successfully created
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 authentication manager
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
- cleanup_modal_token()
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)