gitarsenal-cli 1.9.74 โ†’ 1.9.76

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.
@@ -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,297 @@ 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
+ import modal
50
+
51
+ print("๐Ÿณ SSH Container Function Started!")
52
+ print(f"๐Ÿ“‹ Parameters received:")
53
+ print(f" - SSH Password: {'***' if ssh_password else 'None'}")
54
+ print(f" - Repo URL: {repo_url or 'None'}")
55
+ print(f" - Repo Name: {repo_name or 'None'}")
56
+ print(f" - Setup Commands: {len(setup_commands) if setup_commands else 0} commands")
57
+ print(f" - OpenAI API Key: {'Set' if openai_api_key else 'Not set'}")
58
+ print(f" - Anthropic API Key: {'Set' if anthropic_api_key else 'Not set'}")
59
+ print(f" - Stored Credentials: {len(stored_credentials) if stored_credentials else 0} items")
60
+
61
+ # Import only the modules we actually need (none currently for Agent-based approach)
62
+ # Note: CommandListManager and llm_debugging functions are not used in the Agent-based approach
63
+ print("โœ… Container setup complete - using Agent-based repository setup")
64
+
65
+ # Set root password
66
+ subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
67
+
68
+ # Set OpenAI API key if provided
69
+ if openai_api_key:
70
+ os.environ['OPENAI_API_KEY'] = openai_api_key
71
+ else:
72
+ print("โš ๏ธ No OpenAI API key provided to container")
73
+
74
+ # Set up stored credentials in container environment
75
+ if stored_credentials:
76
+ print(f"๐Ÿ” Setting up {len(stored_credentials)} stored credentials in container...")
77
+ for key, value in stored_credentials.items():
78
+ # Set each credential as an environment variable
79
+ env_var_name = key.upper().replace('-', '_').replace(' ', '_')
80
+ os.environ[env_var_name] = value
81
+ print(f"โœ… Set {env_var_name} in container environment")
82
+
83
+ # Also save credentials to a file in the container for easy access
84
+ credentials_dir = "/root/.gitarsenal"
85
+ os.makedirs(credentials_dir, exist_ok=True)
86
+ credentials_file = os.path.join(credentials_dir, "credentials.json")
87
+ with open(credentials_file, 'w') as f:
88
+ json.dump(stored_credentials, f, indent=2)
89
+ print(f"โœ… Saved credentials to {credentials_file}")
90
+
91
+ # Print available credentials for user reference
92
+ print("\n๐Ÿ” AVAILABLE CREDENTIALS IN CONTAINER:")
93
+ print("="*50)
94
+ for key, value in stored_credentials.items():
95
+ masked_value = value[:8] + "..." if len(value) > 8 else "***"
96
+ env_var_name = key.upper().replace('-', '_').replace(' ', '_')
97
+ print(f" {key} -> {env_var_name} = {masked_value}")
98
+ print("="*50)
99
+ print("๐Ÿ’ก These credentials are available as environment variables and in /root/.gitarsenal/credentials.json")
100
+
101
+
102
+ # Start SSH service
103
+ subprocess.run(["service", "ssh", "start"], check=True)
104
+
105
+ # Use Agent for intelligent repository setup
106
+ if repo_url:
107
+ print("๐Ÿค– Using Agent for intelligent repository setup...")
108
+
109
+ # Set up environment variables for the Agent
110
+ if openai_api_key:
111
+ os.environ['OPENAI_API_KEY'] = openai_api_key
112
+ if anthropic_api_key:
113
+ os.environ['ANTHROPIC_API_KEY'] = anthropic_api_key
114
+
115
+ # Set up Anthropic API key from stored credentials
116
+ anthropic_api_key = None
117
+ if stored_credentials:
118
+ # Look for Anthropic API key in various possible names
119
+ for key_name in ['ANTHROPIC_API_KEY', 'anthropic_api_key', 'anthropic-api-key']:
120
+ if key_name in stored_credentials:
121
+ anthropic_api_key = stored_credentials[key_name]
122
+ os.environ['ANTHROPIC_API_KEY'] = anthropic_api_key
123
+ print(f"โœ… Set Anthropic API key from stored credentials")
124
+ break
125
+
126
+ if not anthropic_api_key:
127
+ print("โš ๏ธ No Anthropic API key found in stored credentials")
128
+ print("๐Ÿ’ก Agent will require an Anthropic API key for operation")
129
+
130
+ try:
131
+ print("๐Ÿ”ง Running Agent for repository setup...")
132
+
133
+ print("\n" + "="*80)
134
+ print("๐Ÿค– AGENT REPOSITORY SETUP")
135
+ print("="*80)
136
+ print(f"Repository: {repo_url}")
137
+ print(f"Working Directory: /root")
138
+ if stored_credentials:
139
+ print(f"Available Credentials: {len(stored_credentials)} items")
140
+ print("="*80 + "\n")
141
+
142
+ # Call Agent directly as subprocess with real-time output
143
+ claude_prompt = f"clone, setup and run {repo_url}"
144
+ print(f"๐Ÿš€ Executing the task: \"{claude_prompt}\"")
145
+ print("\n" + "="*60)
146
+ print("๐ŸŽ‰ AGENT OUTPUT (LIVE)")
147
+ print("="*60)
148
+
149
+ # Use Popen for real-time output streaming with optimizations
150
+ import sys
151
+ import select
152
+ import fcntl
153
+ import os as os_module
154
+
155
+ process = subprocess.Popen(
156
+ ["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt], # -u for unbuffered output
157
+ cwd="/root",
158
+ stdout=subprocess.PIPE,
159
+ stderr=subprocess.PIPE, # Keep separate for better handling
160
+ text=True,
161
+ bufsize=0, # Unbuffered for fastest output
162
+ universal_newlines=True,
163
+ env=dict(os.environ, PYTHONUNBUFFERED='1') # Force unbuffered Python output
164
+ )
165
+
166
+ # Make stdout and stderr non-blocking for faster reading
167
+ def make_non_blocking(fd):
168
+ flags = fcntl.fcntl(fd, fcntl.F_GETFL)
169
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags | os_module.O_NONBLOCK)
170
+
171
+ make_non_blocking(process.stdout)
172
+ make_non_blocking(process.stderr)
173
+
174
+ # Stream output in real-time with robust error handling
175
+ try:
176
+ stdout_buffer = ""
177
+ stderr_buffer = ""
178
+
179
+ while process.poll() is None:
180
+ try:
181
+ # Use select for efficient I/O multiplexing with error handling
182
+ ready, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1) # 100ms timeout
183
+
184
+ for stream in ready:
185
+ try:
186
+ if stream == process.stdout:
187
+ chunk = stream.read(1024) # Read in chunks for efficiency
188
+ if chunk is not None and chunk:
189
+ stdout_buffer += chunk
190
+ # Process complete lines immediately
191
+ while '\n' in stdout_buffer:
192
+ line, stdout_buffer = stdout_buffer.split('\n', 1)
193
+ print(line, flush=True) # Force immediate flush
194
+ elif stream == process.stderr:
195
+ chunk = stream.read(1024)
196
+ if chunk is not None and chunk:
197
+ stderr_buffer += chunk
198
+ # Process complete lines immediately
199
+ while '\n' in stderr_buffer:
200
+ line, stderr_buffer = stderr_buffer.split('\n', 1)
201
+ print(f"STDERR: {line}", flush=True)
202
+ except (BlockingIOError, OSError, ValueError):
203
+ # Handle various I/O errors gracefully
204
+ continue
205
+ except (select.error, OSError):
206
+ # If select fails, fall back to simple polling
207
+ time.sleep(0.1)
208
+ continue
209
+
210
+ # Process any remaining output after process ends
211
+ try:
212
+ # Read any remaining data from streams
213
+ remaining_stdout = process.stdout.read()
214
+ remaining_stderr = process.stderr.read()
215
+
216
+ if remaining_stdout:
217
+ stdout_buffer += remaining_stdout
218
+ if remaining_stderr:
219
+ stderr_buffer += remaining_stderr
220
+
221
+ # Output remaining buffered content
222
+ if stdout_buffer.strip():
223
+ print(stdout_buffer.strip(), flush=True)
224
+ if stderr_buffer.strip():
225
+ print(f"STDERR: {stderr_buffer.strip()}", flush=True)
226
+ except (OSError, ValueError):
227
+ # Handle cases where streams are already closed
228
+ pass
229
+
230
+ # Get final return code
231
+ return_code = process.returncode
232
+
233
+ print("\n" + "="*60)
234
+ if return_code == 0:
235
+ print("โœ… Agent completed successfully!")
236
+ else:
237
+ print(f"โš ๏ธ Agent exited with code: {return_code}")
238
+ print("="*60)
239
+
240
+ except subprocess.TimeoutExpired:
241
+ print("\nโš ๏ธ Agent timed out after 10 minutes")
242
+ process.kill()
243
+ process.wait()
244
+ except Exception as stream_error:
245
+ pass
246
+
247
+ # Fallback to simple readline approach
248
+ try:
249
+ # Restart the process with simpler streaming
250
+ if process.poll() is None:
251
+ process.kill()
252
+ process.wait()
253
+
254
+ fallback_process = subprocess.Popen(
255
+ ["python", "-u", "/python/kill_claude/claude_code_agent.py", claude_prompt],
256
+ cwd="/root",
257
+ stdout=subprocess.PIPE,
258
+ stderr=subprocess.STDOUT,
259
+ text=True,
260
+ bufsize=1,
261
+ universal_newlines=True
262
+ )
263
+
264
+ # Simple line-by-line reading
265
+ while True:
266
+ line = fallback_process.stdout.readline()
267
+ if line == '' and fallback_process.poll() is not None:
268
+ break
269
+ if line:
270
+ print(line.rstrip(), flush=True)
271
+
272
+ return_code = fallback_process.returncode
273
+
274
+ print("\n" + "="*60)
275
+ if return_code == 0:
276
+ print("โœ… Agent completed successfully!")
277
+ else:
278
+ print(f"โš ๏ธ Agent exited with code: {return_code}")
279
+ print("="*60)
280
+
281
+ except Exception as fallback_error:
282
+ print(f"\nโŒ Fallback streaming also failed: {fallback_error}")
283
+ print("โš ๏ธ Agent may have completed, but output streaming failed")
284
+ return_code = 1
285
+
286
+ except Exception as e:
287
+ print(f"โŒ Error during repository setup: {e}")
288
+ print("โš ๏ธ Proceeding without setup...")
289
+ import traceback
290
+ traceback.print_exc()
291
+ else:
292
+ print("โš ๏ธ No repository URL provided, skipping setup")
293
+
294
+ print("๐Ÿ”Œ Creating SSH tunnel on port 22...")
295
+ # Create SSH tunnel
296
+ with modal.forward(22, unencrypted=True) as tunnel:
297
+ host, port = tunnel.tcp_socket
298
+
299
+ print("\n" + "=" * 80)
300
+ print("๐ŸŽ‰ SSH CONTAINER IS READY!")
301
+ print("=" * 80)
302
+ print(f"๐ŸŒ SSH Host: {host}")
303
+ print(f"๐Ÿ”Œ SSH Port: {port}")
304
+ print(f"๐Ÿ‘ค Username: root")
305
+ print(f"๐Ÿ” Password: {ssh_password}")
306
+ print()
307
+ print("๐Ÿ”— CONNECT USING THIS COMMAND:")
308
+ print(f"ssh -p {port} root@{host}")
309
+ print("=" * 80)
310
+
311
+ print("๐Ÿ”„ Starting keep-alive loop...")
312
+ # Keep the container running
313
+ iteration = 0
314
+ while True:
315
+ iteration += 1
316
+ if iteration % 10 == 1: # Print every 5 minutes (10 * 30 seconds = 5 minutes)
317
+ print(f"๐Ÿ’“ Container alive (iteration {iteration})")
318
+
319
+ time.sleep(30)
320
+ # Check if SSH service is still running
321
+ try:
322
+ subprocess.run(["service", "ssh", "status"], check=True,
323
+ capture_output=True)
324
+ except subprocess.CalledProcessError:
325
+ print("โš ๏ธ SSH service stopped, restarting...")
326
+ subprocess.run(["service", "ssh", "start"], check=True)
327
+
328
+
83
329
  # Create Modal SSH container with GPU support and intelligent repository setup using Agent
84
330
  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):
331
+ volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False, gpu_count=1):
86
332
  """Create a Modal SSH container with GPU support and intelligent repository setup.
87
333
 
88
334
  When repo_url is provided, uses Agent for intelligent repository setup.
@@ -240,25 +486,20 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
240
486
 
241
487
  # Choose base image to avoid CUDA segfault issues
242
488
  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"
489
+ base_image = modal.Image.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.11")
490
+ # base_image = modal.Image.debian_slim()
246
491
 
247
492
  # Build the SSH image with the chosen base
248
493
  ssh_image = (
249
494
  base_image
250
495
  .apt_install(
251
496
  "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
252
- "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
253
- "gpg", "ca-certificates", "software-properties-common"
497
+ "python3", "python3-pip"
254
498
  )
255
499
  )
256
500
 
257
501
  # 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")
502
+ ssh_image = ssh_image.uv_pip_install("uv", "modal", "gitingest", "requests", "openai", "anthropic", "exa-py")
262
503
 
263
504
  # Add the rest of the configuration
264
505
  ssh_image = ssh_image.run_commands(
@@ -266,21 +507,11 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
266
507
  "mkdir -p /var/run/sshd",
267
508
  "mkdir -p /root/.ssh",
268
509
  "chmod 700 /root/.ssh",
269
-
270
- # Configure SSH server
510
+
511
+ "ssh-keygen -A",
271
512
  "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
272
513
  "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",
514
+ "echo 'export PATH=/usr/local/cuda/bin:$PATH' >> /root/.bashrc"
284
515
 
285
516
  # Create base directories (subdirectories will be created automatically when mounting)
286
517
  "mkdir -p /python",
@@ -293,306 +524,27 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
293
524
  volumes_config[volume_mount_path] = volume
294
525
 
295
526
  # 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
527
+ print("๐Ÿ” Testing app creation...")
528
+ app = modal.App(app_name, image=ssh_image) # Pass image here
529
+ print("โœ… Created app successfully")
303
530
 
304
- # Define the SSH container function (remove image from decorator)
305
- @app.function(
531
+ # Apply the decorator to the global SSH container function
532
+ decorated_ssh_function = app.function(
306
533
  timeout=timeout_minutes * 60, # Convert to seconds
307
534
  gpu=gpu_spec['modal_gpu'], # Use the user-selected GPU type and count
308
- serialized=True,
309
535
  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)
536
+ )(ssh_container_function)
586
537
 
587
538
  # Run the container
588
539
  try:
589
540
  print("โณ Starting container... This may take 1-2 minutes...")
590
541
 
591
- # Start the container in a new thread to avoid blocking
542
+ # Start the container and wait for it to complete (blocking)
592
543
  with modal.enable_output():
593
544
  with app.run():
594
545
  # Get the API key from environment
595
- api_key = os.environ.get("OPENAI_API_KEY")
546
+ openai_api_key = os.environ.get("OPENAI_API_KEY")
547
+ anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY")
596
548
 
597
549
  # Get stored credentials from local file
598
550
  stored_credentials = get_stored_credentials()
@@ -601,9 +553,69 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
601
553
  else:
602
554
  print("โš ๏ธ No stored credentials found")
603
555
 
604
- ssh_container_function.remote(ssh_password, repo_url, repo_name, setup_commands, api_key, anthropic_api_key, stored_credentials)
556
+ # Use spawn() to get a FunctionCall handle, then wait for it
557
+ print("๐Ÿš€ Spawning SSH container...")
558
+ try:
559
+ function_call = decorated_ssh_function.spawn(ssh_password, repo_url, repo_name, setup_commands, openai_api_key, anthropic_api_key, stored_credentials)
560
+ print(f"โœ… Container spawned with call ID: {function_call.object_id}")
561
+ print(f"๐Ÿ” Function call status: {function_call}")
562
+ except Exception as spawn_error:
563
+ print(f"โŒ Error during spawn: {spawn_error}")
564
+ raise
565
+
566
+ try:
567
+ # Wait for the function to start and print connection info (with timeout)
568
+ print("โณ Waiting for container to initialize...")
569
+
570
+ # Use a timeout to see if the container is starting properly
571
+ print("๐Ÿ” Checking container status with 30-second timeout...")
572
+ try:
573
+ result = function_call.get(timeout=30)
574
+ print(f"๐Ÿ”š Container function completed with result: {result}")
575
+ except TimeoutError:
576
+ print("โฐ Container is still running after 30 seconds - this is expected!")
577
+ print("๐ŸŽฏ The container should be accessible via SSH now.")
578
+ print("๐Ÿ’ก The function will continue running until manually stopped.")
579
+ print("๐Ÿ”— Use Ctrl+C to stop monitoring, but the container will keep running.")
580
+ print("๐Ÿ”’ Keeping tokens active since container is still running.")
581
+
582
+ # Continue waiting for user interrupt
583
+ try:
584
+ print("\nโณ Monitoring container (press Ctrl+C to stop monitoring)...")
585
+ result = function_call.get() # Wait indefinitely
586
+ print(f"๐Ÿ”š Container function completed with result: {result}")
587
+ except KeyboardInterrupt:
588
+ print("\n๐Ÿ›‘ Stopped monitoring. Container is still running remotely.")
589
+ print("๐Ÿ’ก Use Modal's web UI or CLI to stop the container when done.")
590
+ print("๐Ÿ”’ Keeping tokens active since container is still running.")
591
+ return {
592
+ "app_name": app_name,
593
+ "ssh_password": ssh_password,
594
+ "volume_name": volume_name,
595
+ "status": "monitoring_stopped",
596
+ "function_call_id": function_call.object_id
597
+ }
598
+
599
+ except KeyboardInterrupt:
600
+ print("\n๐Ÿ›‘ Interrupted by user. Container may still be running remotely.")
601
+ print("๐Ÿ’ก Use Modal's web UI or CLI to check running containers.")
602
+ print("๐Ÿ”’ Keeping tokens active since container may still be running.")
603
+ return {
604
+ "app_name": app_name,
605
+ "ssh_password": ssh_password,
606
+ "volume_name": volume_name,
607
+ "status": "interrupted",
608
+ "function_call_id": function_call.object_id
609
+ }
610
+ except Exception as e:
611
+ print(f"โš ๏ธ Container execution error: {e}")
612
+ print("๐Ÿ’ก Container may still be accessible via SSH if it started successfully.")
613
+ print("๐Ÿงน Cleaning up tokens due to execution error.")
614
+ cleanup_modal_token()
615
+ raise
605
616
 
606
- # Clean up Modal token after container is successfully created
617
+ # Only clean up tokens if container actually completed normally
618
+ print("๐Ÿงน Container completed normally, cleaning up tokens.")
607
619
  cleanup_modal_token()
608
620
 
609
621
  return {
@@ -904,13 +916,10 @@ if __name__ == "__main__":
904
916
  parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
905
917
  parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
906
918
  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')
919
+
910
920
  parser.add_argument('--gpu', default='A10G', help='GPU type to use')
911
921
  parser.add_argument('--gpu-count', type=int, default=1, help='Number of GPUs to use (default: 1)')
912
922
  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
923
 
915
924
  # Authentication-related arguments
916
925
  parser.add_argument('--auth', action='store_true', help='Manage authentication (login, register, logout)')
@@ -930,7 +939,29 @@ if __name__ == "__main__":
930
939
 
931
940
  args = parser.parse_args()
932
941
 
933
- # Initialize authentication manager
942
+ # Initialize tokens (import here to avoid container import issues)
943
+ from fetch_modal_tokens import get_tokens
944
+ token_id, token_secret, openai_api_key, anthropic_api_key, openrouter_api_key, groq_api_key = get_tokens()
945
+
946
+ # Check if we got valid tokens
947
+ if token_id is None or token_secret is None:
948
+ raise ValueError("Could not get valid tokens")
949
+
950
+ # Explicitly set the environment variables again to be sure
951
+ os.environ["MODAL_TOKEN_ID"] = token_id
952
+ os.environ["MODAL_TOKEN_SECRET"] = token_secret
953
+ if openai_api_key:
954
+ os.environ["OPENAI_API_KEY"] = openai_api_key
955
+ if anthropic_api_key:
956
+ os.environ["ANTHROPIC_API_KEY"] = anthropic_api_key
957
+ # Also set the old environment variable for backward compatibility
958
+ os.environ["MODAL_TOKEN"] = token_id
959
+
960
+ # Set token variables for later use
961
+ token = token_id # For backward compatibility
962
+
963
+ # Initialize authentication manager (import here to avoid container import issues)
964
+ from auth_manager import AuthManager
934
965
  auth_manager = AuthManager()
935
966
 
936
967
  # Handle authentication-related commands
@@ -1191,7 +1222,7 @@ if __name__ == "__main__":
1191
1222
  repo_name = repo_name[:-4]
1192
1223
 
1193
1224
  # Create the container
1194
- create_modal_ssh_container(
1225
+ result = create_modal_ssh_container(
1195
1226
  gpu_type=args.gpu,
1196
1227
  repo_url=args.repo_url,
1197
1228
  repo_name=repo_name,
@@ -1201,10 +1232,24 @@ if __name__ == "__main__":
1201
1232
  ssh_password=ssh_password,
1202
1233
  interactive=args.interactive,
1203
1234
  gpu_count=getattr(args, 'gpu_count', 1),
1204
- use_cuda_base=getattr(args, 'use_cuda_base', False)
1205
1235
  )
1236
+
1237
+ if result:
1238
+ print(f"\nโœ… Container operation completed: {result.get('status', 'success')}")
1239
+ if result.get('function_call_id'):
1240
+ print(f"๐Ÿ†” Function Call ID: {result['function_call_id']}")
1241
+ print("๐Ÿ’ก You can use this ID to check container status via Modal CLI")
1242
+ else:
1243
+ print("\nโŒ Container creation failed")
1244
+
1206
1245
  except KeyboardInterrupt:
1246
+ print("\n๐Ÿ›‘ Operation cancelled by user")
1207
1247
  cleanup_modal_token()
1208
1248
  sys.exit(1)
1209
1249
  except Exception as e:
1210
- cleanup_modal_token()
1250
+ print(f"\nโŒ Unexpected error: {e}")
1251
+ print("๐Ÿ“‹ Error details:")
1252
+ import traceback
1253
+ traceback.print_exc()
1254
+ cleanup_modal_token()
1255
+ sys.exit(1)