gitarsenal-cli 1.0.9 → 1.1.1

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/README.md CHANGED
@@ -1,105 +1,74 @@
1
1
  # GitArsenal CLI
2
2
 
3
- A command-line tool for easily creating Modal sandboxes with GitHub repositories.
3
+ A tool for creating and managing GPU-accelerated development environments using Modal.
4
4
 
5
5
  ## Features
6
6
 
7
- - Interactive prompts to configure your sandbox
8
- - Support for custom GPU types (A10G, A100, H100, T4, V100)
9
- - Persistent volume support for faster installations
10
- - Custom setup commands
11
- - Automatic repository cloning
7
+ - Create Modal containers with GPU support
8
+ - Clone repositories and run setup commands
9
+ - Persistent storage with Modal volumes
10
+ - SSH access to containers
11
+ - API key management for various services
12
12
 
13
- ## Installation
13
+ ## API Key Management
14
14
 
15
- ### Prerequisites
15
+ The CLI now supports secure storage of API keys for various services. Keys are stored in `~/.gitarsenal/keys/` with proper permissions (only readable by the current user).
16
16
 
17
- - Node.js 14 or higher
18
- - Python 3.8 or higher
19
- - Modal CLI (`pip install modal`)
20
- - Git
17
+ ### Supported Services
21
18
 
22
- ### Global Installation
19
+ - `openai` - OpenAI API keys for debugging and assistance
20
+ - `wandb` - Weights & Biases API keys for experiment tracking
21
+ - `huggingface` - Hugging Face tokens for model access
23
22
 
24
- ```bash
25
- npm install -g gitarsenal-cli
26
- ```
23
+ ### Managing API Keys
27
24
 
28
- ### Local Installation
25
+ #### Adding a new API key
29
26
 
30
27
  ```bash
31
- npm install gitarsenal-cli
32
- ```
28
+ # Add a key interactively (will prompt for the key)
29
+ python test_modalSandboxScript.py keys add --service openai
33
30
 
34
- ## Usage
35
-
36
- ### Command Line Interface
31
+ # Add a key directly (not recommended for security)
32
+ python test_modalSandboxScript.py keys add --service wandb --key YOUR_API_KEY
33
+ ```
37
34
 
38
- Simply run the command:
35
+ #### Listing saved API keys
39
36
 
40
37
  ```bash
41
- gitarsenal
38
+ python test_modalSandboxScript.py keys list
42
39
  ```
43
40
 
44
- The CLI will guide you through the process with interactive prompts:
45
-
46
- 1. Enter the GitHub repository URL
47
- 2. Select a GPU type
48
- 3. Choose whether to use a persistent volume
49
- 4. Optionally provide custom setup commands
50
- 5. Confirm your settings
41
+ #### Viewing a specific API key (masked)
51
42
 
52
- ### Command Line Options
53
-
54
- ```
55
- Usage: gitarsenal [options]
56
-
57
- Options:
58
- -V, --version output the version number
59
- -r, --repo <url> GitHub repository URL
60
- -g, --gpu <type> GPU type (A10G, A100, H100, T4, V100) (default: "A10G")
61
- -v, --volume <name> Name of persistent volume
62
- -y, --yes Skip confirmation prompts
63
- -h, --help display help for command
43
+ ```bash
44
+ python test_modalSandboxScript.py keys view --service huggingface
64
45
  ```
65
46
 
66
- ### Example
47
+ #### Deleting an API key
67
48
 
68
49
  ```bash
69
- gitarsenal --repo https://github.com/username/repo-name --gpu A10G --volume my-volume
50
+ python test_modalSandboxScript.py keys delete --service openai
70
51
  ```
71
52
 
72
- ## Programmatic Usage
53
+ ## Creating a Modal Container
73
54
 
74
- You can also use GitArsenal programmatically in your Node.js applications:
75
-
76
- ```javascript
77
- const { runModalSandbox } = require('gitarsenal-cli');
55
+ ```bash
56
+ # Basic container creation
57
+ python test_modalSandboxScript.py container --gpu A10G --repo-url https://github.com/username/repo.git
78
58
 
79
- async function main() {
80
- await runModalSandbox({
81
- repoUrl: 'https://github.com/username/repo-name',
82
- gpuType: 'A10G',
83
- volumeName: 'my-volume',
84
- setupCommands: [
85
- 'pip install -r requirements.txt',
86
- 'python setup.py install'
87
- ]
88
- });
89
- }
59
+ # With setup commands
60
+ python test_modalSandboxScript.py container --gpu A100 --repo-url https://github.com/username/repo.git --setup-commands "pip install -r requirements.txt" "python setup.py install"
90
61
 
91
- main().catch(console.error);
62
+ # With volume for persistent storage
63
+ python test_modalSandboxScript.py container --gpu A10G --repo-url https://github.com/username/repo.git --volume-name my-persistent-volume
92
64
  ```
93
65
 
94
- ## How It Works
95
-
96
- GitArsenal CLI is a Node.js wrapper around a Python script that creates Modal sandboxes. The CLI:
66
+ ## Automatic API Key Usage
97
67
 
98
- 1. Checks for required dependencies (Python, Modal, Git)
99
- 2. Prompts for configuration options
100
- 3. Executes the Python script with the provided options
101
- 4. Handles errors and provides feedback
68
+ When using commands that require API keys (like `wandb login` or `huggingface-cli login`), the system will:
102
69
 
103
- ## License
70
+ 1. Check if a saved API key exists for the service
71
+ 2. If found, use the saved key automatically
72
+ 3. If not found, prompt for the key and offer to save it for future use
104
73
 
105
- MIT
74
+ This makes it easy to work with multiple projects that require the same API keys without having to re-enter them each time.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,97 @@
1
+ # GitArsenal CLI
2
+
3
+ GitArsenal CLI is a powerful tool for setting up and running GPU-accelerated environments in the cloud using Modal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Clone the repository
9
+ git clone https://github.com/yourusername/gitarsenal-cli.git
10
+ cd gitarsenal-cli/python
11
+
12
+ # Install dependencies
13
+ pip install -r requirements.txt
14
+ ```
15
+
16
+ ## Credentials Setup
17
+
18
+ GitArsenal CLI requires several API keys and tokens to function properly:
19
+
20
+ - **OpenAI API Key**: Used for debugging failed commands
21
+ - **Modal Token**: Used to create cloud environments
22
+ - **Hugging Face Token**: Used for accessing Hugging Face models
23
+ - **Weights & Biases API Key**: Used for experiment tracking
24
+
25
+ You can set up your credentials using the credentials manager:
26
+
27
+ ```bash
28
+ python manage_credentials.py setup
29
+ ```
30
+
31
+ This will guide you through setting up all required credentials, which will be stored securely in your home directory.
32
+
33
+ ### Managing Individual Credentials
34
+
35
+ You can also manage individual credentials:
36
+
37
+ ```bash
38
+ # Set a specific credential
39
+ python manage_credentials.py set openai_api_key
40
+
41
+ # View a credential (masked for security)
42
+ python manage_credentials.py get modal_token
43
+
44
+ # Clear a specific credential
45
+ python manage_credentials.py clear huggingface_token
46
+
47
+ # Clear all credentials
48
+ python manage_credentials.py clear
49
+
50
+ # List all saved credentials (without showing values)
51
+ python manage_credentials.py list
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ### Creating a Modal Sandbox
57
+
58
+ ```bash
59
+ python test_modalSandboxScript.py --gpu A10G --repo-url "https://github.com/username/repo.git"
60
+ ```
61
+
62
+ ### Options
63
+
64
+ - `--gpu`: GPU type (A10G, A100, H100, T4, V100)
65
+ - `--repo-url`: Repository URL to clone
66
+ - `--repo-name`: Repository name override
67
+ - `--setup-commands`: Setup commands to run
68
+ - `--volume-name`: Name of the Modal volume for persistent storage
69
+
70
+ ## Security
71
+
72
+ Your credentials are stored securely in `~/.gitarsenal/credentials.json` with restrictive file permissions. The file is only readable by your user account.
73
+
74
+ ## Troubleshooting
75
+
76
+ If you encounter authentication issues:
77
+
78
+ 1. Check that your credentials are set up correctly:
79
+ ```bash
80
+ python manage_credentials.py list
81
+ ```
82
+
83
+ 2. If needed, clear and reset your credentials:
84
+ ```bash
85
+ python manage_credentials.py clear
86
+ python manage_credentials.py setup
87
+ ```
88
+
89
+ 3. Ensure Modal CLI is installed and authenticated:
90
+ ```bash
91
+ pip install modal
92
+ modal token new
93
+ ```
94
+
95
+ ## License
96
+
97
+ [MIT License](LICENSE)
@@ -0,0 +1,164 @@
1
+ import os
2
+ import json
3
+ import getpass
4
+ from pathlib import Path
5
+
6
+ class CredentialsManager:
7
+ """
8
+ Manages API keys and tokens for GitArsenal CLI.
9
+ Provides secure storage and retrieval of credentials.
10
+ """
11
+
12
+ def __init__(self, config_dir=None):
13
+ """Initialize the credentials manager with optional custom config directory"""
14
+ if config_dir:
15
+ self.config_dir = Path(config_dir)
16
+ else:
17
+ self.config_dir = Path.home() / ".gitarsenal"
18
+
19
+ self.credentials_file = self.config_dir / "credentials.json"
20
+ self.ensure_config_dir()
21
+
22
+ def ensure_config_dir(self):
23
+ """Ensure the configuration directory exists"""
24
+ if not self.config_dir.exists():
25
+ self.config_dir.mkdir(parents=True)
26
+ # Set restrictive permissions on Unix-like systems
27
+ if os.name == 'posix':
28
+ self.config_dir.chmod(0o700) # Only owner can read/write/execute
29
+
30
+ def load_credentials(self):
31
+ """Load credentials from the credentials file"""
32
+ if not self.credentials_file.exists():
33
+ return {}
34
+
35
+ try:
36
+ with open(self.credentials_file, 'r') as f:
37
+ return json.load(f)
38
+ except (json.JSONDecodeError, IOError):
39
+ print("āš ļø Error reading credentials file. Using empty credentials.")
40
+ return {}
41
+
42
+ def save_credentials(self, credentials):
43
+ """Save credentials to the credentials file"""
44
+ try:
45
+ with open(self.credentials_file, 'w') as f:
46
+ json.dump(credentials, f)
47
+
48
+ # Set restrictive permissions on Unix-like systems
49
+ if os.name == 'posix':
50
+ self.credentials_file.chmod(0o600) # Only owner can read/write
51
+
52
+ return True
53
+ except IOError as e:
54
+ print(f"āŒ Error saving credentials: {e}")
55
+ return False
56
+
57
+ def get_credential(self, key, prompt=None, is_password=False, validate_func=None):
58
+ """
59
+ Get a credential by key. If not found, prompt the user.
60
+
61
+ Args:
62
+ key: The credential key
63
+ prompt: Custom prompt message
64
+ is_password: Whether to mask input
65
+ validate_func: Optional function to validate the credential
66
+
67
+ Returns:
68
+ The credential value
69
+ """
70
+ credentials = self.load_credentials()
71
+
72
+ # Check if credential exists and is valid
73
+ if key in credentials:
74
+ value = credentials[key]
75
+ if not validate_func or validate_func(value):
76
+ return value
77
+
78
+ # Credential not found or invalid, prompt user
79
+ if not prompt:
80
+ prompt = f"Please enter your {key}:"
81
+
82
+ print("\n" + "="*60)
83
+ print(f"šŸ”‘ {key.upper()} REQUIRED")
84
+ print("="*60)
85
+ print(prompt)
86
+ print("-" * 60)
87
+
88
+ try:
89
+ if is_password:
90
+ value = getpass.getpass("Input (hidden): ").strip()
91
+ else:
92
+ value = input("Input: ").strip()
93
+
94
+ if not value:
95
+ print("āŒ No input provided.")
96
+ return None
97
+
98
+ # Validate if function provided
99
+ if validate_func and not validate_func(value):
100
+ print("āŒ Invalid input.")
101
+ return None
102
+
103
+ # Save the credential
104
+ credentials[key] = value
105
+ self.save_credentials(credentials)
106
+
107
+ print("āœ… Input received and saved successfully!")
108
+ return value
109
+
110
+ except KeyboardInterrupt:
111
+ print("\nāŒ Input cancelled by user.")
112
+ return None
113
+ except Exception as e:
114
+ print(f"āŒ Error getting input: {e}")
115
+ return None
116
+
117
+ def get_openai_api_key(self):
118
+ """Get OpenAI API key with validation"""
119
+ def validate_openai_key(key):
120
+ # Basic validation - OpenAI keys usually start with "sk-" and are 51 chars
121
+ return key.startswith("sk-") and len(key) > 40
122
+
123
+ prompt = "To debug failed commands, an OpenAI API key is needed.\nYou can get your API key from: https://platform.openai.com/api-keys"
124
+ return self.get_credential("openai_api_key", prompt, is_password=True, validate_func=validate_openai_key)
125
+
126
+ def get_modal_token(self):
127
+ """Get Modal token with basic validation"""
128
+ def validate_modal_token(token):
129
+ # Modal tokens are typically non-empty strings
130
+ return bool(token) and len(token) > 10
131
+
132
+ prompt = "A Modal token is required to create cloud environments.\nYou can get your token by running 'modal token new' in your terminal."
133
+ return self.get_credential("modal_token", prompt, is_password=True, validate_func=validate_modal_token)
134
+
135
+ def get_huggingface_token(self):
136
+ """Get Hugging Face token with basic validation"""
137
+ def validate_hf_token(token):
138
+ # HF tokens are typically non-empty strings
139
+ return bool(token) and len(token) > 8
140
+
141
+ prompt = "A Hugging Face token is required.\nYou can get your token from: https://huggingface.co/settings/tokens"
142
+ return self.get_credential("huggingface_token", prompt, is_password=True, validate_func=validate_hf_token)
143
+
144
+ def get_wandb_api_key(self):
145
+ """Get Weights & Biases API key with validation"""
146
+ def validate_wandb_key(key):
147
+ # W&B API keys are typically 40 characters
148
+ return len(key) == 40
149
+
150
+ prompt = "A Weights & Biases API key is required.\nYou can get your API key from: https://wandb.ai/authorize"
151
+ return self.get_credential("wandb_api_key", prompt, is_password=True, validate_func=validate_wandb_key)
152
+
153
+ def clear_credential(self, key):
154
+ """Remove a specific credential"""
155
+ credentials = self.load_credentials()
156
+ if key in credentials:
157
+ del credentials[key]
158
+ self.save_credentials(credentials)
159
+ return True
160
+ return False
161
+
162
+ def clear_all_credentials(self):
163
+ """Clear all saved credentials"""
164
+ return self.save_credentials({})
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ GitArsenal CLI - Main entry point
4
+
5
+ This script provides a user-friendly interface to the GitArsenal CLI tools.
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+ import os
11
+ import subprocess
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser(description="GitArsenal CLI - GPU-accelerated cloud environments")
15
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
16
+
17
+ # Credentials command
18
+ cred_parser = subparsers.add_parser("credentials", help="Manage credentials")
19
+ cred_subparsers = cred_parser.add_subparsers(dest="cred_command", help="Credentials command")
20
+
21
+ # Credentials setup command
22
+ cred_setup_parser = cred_subparsers.add_parser("setup", help="Set up all credentials")
23
+
24
+ # Credentials set command
25
+ cred_set_parser = cred_subparsers.add_parser("set", help="Set a specific credential")
26
+ cred_set_parser.add_argument("key", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key"],
27
+ help="The credential to set")
28
+
29
+ # Credentials get command
30
+ cred_get_parser = cred_subparsers.add_parser("get", help="Get a specific credential")
31
+ cred_get_parser.add_argument("key", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key"],
32
+ help="The credential to get")
33
+
34
+ # Credentials clear command
35
+ cred_clear_parser = cred_subparsers.add_parser("clear", help="Clear credentials")
36
+ cred_clear_parser.add_argument("key", nargs="?", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key", "all"],
37
+ default="all", help="The credential to clear (default: all)")
38
+
39
+ # Credentials list command
40
+ cred_list_parser = cred_subparsers.add_parser("list", help="List all saved credentials")
41
+
42
+ # Sandbox command
43
+ sandbox_parser = subparsers.add_parser("sandbox", help="Create a Modal sandbox")
44
+ sandbox_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
45
+ help="GPU type (default: A10G)")
46
+ sandbox_parser.add_argument("--repo-url", type=str, help="Repository URL to clone")
47
+ sandbox_parser.add_argument("--repo-name", type=str, help="Repository name override")
48
+ sandbox_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
49
+ sandbox_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
50
+
51
+ # SSH container command
52
+ ssh_parser = subparsers.add_parser("ssh", help="Create a Modal SSH container")
53
+ ssh_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
54
+ help="GPU type (default: A10G)")
55
+ ssh_parser.add_argument("--repo-url", type=str, help="Repository URL to clone")
56
+ ssh_parser.add_argument("--repo-name", type=str, help="Repository name override")
57
+ ssh_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
58
+ ssh_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
59
+ ssh_parser.add_argument("--timeout", type=int, default=60, help="Container timeout in minutes (default: 60)")
60
+
61
+ args = parser.parse_args()
62
+
63
+ # If no command is provided, show help
64
+ if not args.command:
65
+ parser.print_help()
66
+ return 1
67
+
68
+ # Handle credentials commands
69
+ if args.command == "credentials":
70
+ cred_args = []
71
+
72
+ if not args.cred_command:
73
+ cred_parser.print_help()
74
+ return 1
75
+
76
+ cred_args.append(args.cred_command)
77
+
78
+ if args.cred_command == "set" or args.cred_command == "get":
79
+ cred_args.append(args.key)
80
+ elif args.cred_command == "clear" and args.key != "all":
81
+ cred_args.append(args.key)
82
+
83
+ return run_script("manage_credentials.py", cred_args)
84
+
85
+ # Handle sandbox command
86
+ elif args.command == "sandbox":
87
+ sandbox_args = []
88
+
89
+ if args.gpu:
90
+ sandbox_args.extend(["--gpu", args.gpu])
91
+ if args.repo_url:
92
+ sandbox_args.extend(["--repo-url", args.repo_url])
93
+ if args.repo_name:
94
+ sandbox_args.extend(["--repo-name", args.repo_name])
95
+ if args.setup_commands:
96
+ sandbox_args.extend(["--setup-commands"] + args.setup_commands)
97
+ if args.volume_name:
98
+ sandbox_args.extend(["--volume-name", args.volume_name])
99
+
100
+ return run_script("test_modalSandboxScript.py", sandbox_args)
101
+
102
+ # Handle SSH container command
103
+ elif args.command == "ssh":
104
+ ssh_args = []
105
+
106
+ if args.gpu:
107
+ ssh_args.extend(["--gpu", args.gpu])
108
+ if args.repo_url:
109
+ ssh_args.extend(["--repo-url", args.repo_url])
110
+ if args.repo_name:
111
+ ssh_args.extend(["--repo-name", args.repo_name])
112
+ if args.setup_commands:
113
+ ssh_args.extend(["--setup-commands"] + args.setup_commands)
114
+ if args.volume_name:
115
+ ssh_args.extend(["--volume-name", args.volume_name])
116
+ if args.timeout:
117
+ ssh_args.extend(["--timeout", str(args.timeout)])
118
+
119
+ # Use test_modalSandboxScript.py with SSH mode
120
+ ssh_args.extend(["--ssh"])
121
+ return run_script("test_modalSandboxScript.py", ssh_args)
122
+
123
+ return 0
124
+
125
+ def run_script(script_name, args):
126
+ """Run a Python script with the given arguments"""
127
+ # Get the directory of the current script
128
+ script_dir = os.path.dirname(os.path.abspath(__file__))
129
+ script_path = os.path.join(script_dir, script_name)
130
+
131
+ # Build the command
132
+ cmd = [sys.executable, script_path] + args
133
+
134
+ try:
135
+ # Run the command
136
+ result = subprocess.run(cmd)
137
+ return result.returncode
138
+ except KeyboardInterrupt:
139
+ print("\nāš ļø Command interrupted by user")
140
+ return 130
141
+ except Exception as e:
142
+ print(f"āŒ Error running {script_name}: {e}")
143
+ return 1
144
+
145
+ if __name__ == "__main__":
146
+ sys.exit(main())
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ GitArsenal Credentials Manager CLI
4
+
5
+ This script allows users to manage their API keys and tokens for GitArsenal CLI.
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+ from credentials_manager import CredentialsManager
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="Manage credentials for GitArsenal CLI")
14
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
15
+
16
+ # Setup command
17
+ setup_parser = subparsers.add_parser("setup", help="Set up all credentials")
18
+
19
+ # Set command
20
+ set_parser = subparsers.add_parser("set", help="Set a specific credential")
21
+ set_parser.add_argument("key", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key"],
22
+ help="The credential to set")
23
+
24
+ # Get command
25
+ get_parser = subparsers.add_parser("get", help="Get a specific credential")
26
+ get_parser.add_argument("key", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key"],
27
+ help="The credential to get")
28
+
29
+ # Clear command
30
+ clear_parser = subparsers.add_parser("clear", help="Clear credentials")
31
+ clear_parser.add_argument("key", nargs="?", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key", "all"],
32
+ default="all", help="The credential to clear (default: all)")
33
+
34
+ # List command
35
+ list_parser = subparsers.add_parser("list", help="List all saved credentials (shows only existence, not values)")
36
+
37
+ args = parser.parse_args()
38
+
39
+ # Create credentials manager
40
+ credentials_manager = CredentialsManager()
41
+
42
+ if args.command == "setup":
43
+ print("šŸ“‹ Setting up all credentials for GitArsenal CLI")
44
+ print("You'll be prompted for each credential. Press Ctrl+C to skip any credential.")
45
+
46
+ try:
47
+ # OpenAI API key
48
+ print("\nšŸ”‘ Setting up OpenAI API key")
49
+ credentials_manager.get_openai_api_key()
50
+
51
+ # Modal token
52
+ print("\nšŸ”‘ Setting up Modal token")
53
+ credentials_manager.get_modal_token()
54
+
55
+ # Hugging Face token
56
+ print("\nšŸ”‘ Setting up Hugging Face token")
57
+ credentials_manager.get_huggingface_token()
58
+
59
+ # Weights & Biases API key
60
+ print("\nšŸ”‘ Setting up Weights & Biases API key")
61
+ credentials_manager.get_wandb_api_key()
62
+
63
+ print("\nāœ… Credentials setup complete!")
64
+
65
+ except KeyboardInterrupt:
66
+ print("\n\nāš ļø Setup interrupted. Some credentials may not have been set.")
67
+ return 1
68
+
69
+ elif args.command == "set":
70
+ print(f"šŸ”‘ Setting {args.key}")
71
+ if args.key == "openai_api_key":
72
+ credentials_manager.get_openai_api_key()
73
+ elif args.key == "modal_token":
74
+ credentials_manager.get_modal_token()
75
+ elif args.key == "huggingface_token":
76
+ credentials_manager.get_huggingface_token()
77
+ elif args.key == "wandb_api_key":
78
+ credentials_manager.get_wandb_api_key()
79
+
80
+ elif args.command == "get":
81
+ credentials = credentials_manager.load_credentials()
82
+ if args.key in credentials:
83
+ # Show only first and last few characters for security
84
+ value = credentials[args.key]
85
+ masked_value = value[:4] + "*" * (len(value) - 8) + value[-4:] if len(value) > 8 else "****"
86
+ print(f"{args.key}: {masked_value}")
87
+ else:
88
+ print(f"āŒ {args.key} not found in saved credentials")
89
+ return 1
90
+
91
+ elif args.command == "clear":
92
+ if args.key == "all":
93
+ credentials_manager.clear_all_credentials()
94
+ print("āœ… All credentials cleared")
95
+ else:
96
+ if credentials_manager.clear_credential(args.key):
97
+ print(f"āœ… {args.key} cleared")
98
+ else:
99
+ print(f"āŒ {args.key} not found in saved credentials")
100
+ return 1
101
+
102
+ elif args.command == "list":
103
+ credentials = credentials_manager.load_credentials()
104
+ if not credentials:
105
+ print("No credentials saved")
106
+ return 0
107
+
108
+ print("šŸ“‹ Saved credentials:")
109
+ for key in credentials:
110
+ print(f"- {key}: {'*' * 8}")
111
+
112
+ else:
113
+ parser.print_help()
114
+ return 1
115
+
116
+ return 0
117
+
118
+ if __name__ == "__main__":
119
+ sys.exit(main())
@@ -56,13 +56,25 @@ def handle_wandb_login(sandbox, current_dir):
56
56
  print("Setting up Weights & Biases credentials")
57
57
  print("You can get your API key from: https://wandb.ai/authorize")
58
58
 
59
- # Get API key from user
60
- api_key = handle_interactive_input(
61
- "šŸ”‘ WEIGHTS & BIASES API KEY REQUIRED\n" +
62
- "Please paste your W&B API key below:\n" +
63
- "(Your API key should be 40 characters long)",
64
- is_password=True
65
- )
59
+ # Try to use credentials manager first
60
+ api_key = None
61
+ try:
62
+ from credentials_manager import CredentialsManager
63
+ credentials_manager = CredentialsManager()
64
+ api_key = credentials_manager.get_wandb_api_key()
65
+ except ImportError:
66
+ # Fall back to direct input if credentials_manager is not available
67
+ pass
68
+
69
+ # If credentials manager didn't provide a key, use direct input
70
+ if not api_key:
71
+ # Get API key from user
72
+ api_key = handle_interactive_input(
73
+ "šŸ”‘ WEIGHTS & BIASES API KEY REQUIRED\n" +
74
+ "Please paste your W&B API key below:\n" +
75
+ "(Your API key should be 40 characters long)",
76
+ is_password=True
77
+ )
66
78
 
67
79
  if not api_key:
68
80
  print("āŒ No API key provided. Cannot continue with W&B login.")
@@ -226,26 +238,36 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
226
238
  api_key = os.environ.get("OPENAI_API_KEY")
227
239
 
228
240
  if not api_key:
229
- print("\n" + "="*60)
230
- print("šŸ”‘ OPENAI API KEY REQUIRED FOR DEBUGGING")
231
- print("="*60)
232
- print("To debug failed commands, an OpenAI API key is needed.")
233
- print("šŸ“ Please paste your OpenAI API key below:")
234
- print(" (Your input will be hidden for security)")
235
- print("-" * 60)
236
-
241
+ # Use the CredentialsManager to get the API key
237
242
  try:
238
- api_key = getpass.getpass("OpenAI API Key: ").strip()
243
+ from credentials_manager import CredentialsManager
244
+ credentials_manager = CredentialsManager()
245
+ api_key = credentials_manager.get_openai_api_key()
239
246
  if not api_key:
240
247
  print("āŒ No API key provided. Skipping debugging.")
241
248
  return None
242
- print("āœ… API key received successfully!")
243
- except KeyboardInterrupt:
244
- print("\nāŒ API key input cancelled by user.")
245
- return None
246
- except Exception as e:
247
- print(f"āŒ Error getting API key: {e}")
248
- return None
249
+ except ImportError:
250
+ # Fall back to direct input if credentials_manager module is not available
251
+ print("\n" + "="*60)
252
+ print("šŸ”‘ OPENAI API KEY REQUIRED FOR DEBUGGING")
253
+ print("="*60)
254
+ print("To debug failed commands, an OpenAI API key is needed.")
255
+ print("šŸ“ Please paste your OpenAI API key below:")
256
+ print(" (Your input will be hidden for security)")
257
+ print("-" * 60)
258
+
259
+ try:
260
+ api_key = getpass.getpass("OpenAI API Key: ").strip()
261
+ if not api_key:
262
+ print("āŒ No API key provided. Skipping debugging.")
263
+ return None
264
+ print("āœ… API key received successfully!")
265
+ except KeyboardInterrupt:
266
+ print("\nāŒ API key input cancelled by user.")
267
+ return None
268
+ except Exception as e:
269
+ print(f"āŒ Error getting API key: {e}")
270
+ return None
249
271
 
250
272
  # Get current directory context
251
273
  directory_context = ""
@@ -410,6 +432,18 @@ Do not provide any explanations, just the exact command to run.
410
432
 
411
433
  def prompt_for_hf_token():
412
434
  """Prompt user for Hugging Face token when needed"""
435
+ # Try to use credentials manager first
436
+ try:
437
+ from credentials_manager import CredentialsManager
438
+ credentials_manager = CredentialsManager()
439
+ token = credentials_manager.get_huggingface_token()
440
+ if token:
441
+ return token
442
+ except ImportError:
443
+ # Fall back to direct input if credentials_manager is not available
444
+ pass
445
+
446
+ # Traditional direct input method as fallback
413
447
  print("\n" + "="*60)
414
448
  print("šŸ”‘ HUGGING FACE TOKEN REQUIRED")
415
449
  print("="*60)
@@ -434,6 +468,50 @@ def prompt_for_hf_token():
434
468
  return None
435
469
 
436
470
  def create_modal_sandbox(gpu_type, repo_url=None, repo_name=None, setup_commands=None, volume_name=None):
471
+ # Import the credentials manager if available
472
+ try:
473
+ from credentials_manager import CredentialsManager
474
+ credentials_manager = CredentialsManager()
475
+ except ImportError:
476
+ credentials_manager = None
477
+ print("āš ļø Credentials manager not found, will use environment variables or prompt for credentials")
478
+
479
+ # Check if Modal is authenticated
480
+ try:
481
+ # Try to list apps to check if authentication is working
482
+ import subprocess
483
+ result = subprocess.run(["modal", "app", "list"], capture_output=True, text=True)
484
+ if result.returncode != 0 and "not authenticated" in result.stderr:
485
+ print("šŸ”‘ Modal authentication required")
486
+
487
+ # Try to use credentials manager first
488
+ if credentials_manager:
489
+ modal_token = credentials_manager.get_modal_token()
490
+ if modal_token:
491
+ # Set the token in the environment
492
+ os.environ["MODAL_TOKEN_ID"] = modal_token
493
+ print("āœ… Modal token set from credentials manager")
494
+
495
+ # Try to authenticate with the token
496
+ try:
497
+ token_result = subprocess.run(["modal", "token", "set", modal_token],
498
+ capture_output=True, text=True)
499
+ if token_result.returncode == 0:
500
+ print("āœ… Successfully authenticated with Modal")
501
+ else:
502
+ print(f"āš ļø Failed to authenticate with Modal: {token_result.stderr}")
503
+ print("Please run 'modal token new' manually and try again")
504
+ return None
505
+ except Exception as e:
506
+ print(f"āš ļø Error setting Modal token: {e}")
507
+ return None
508
+ else:
509
+ print("āš ļø Modal is not authenticated. Please run 'modal token new' first")
510
+ return None
511
+ except Exception as e:
512
+ print(f"āš ļø Error checking Modal authentication: {e}")
513
+ print("Continuing anyway, but Modal operations may fail")
514
+
437
515
  # Execution history for tracking all commands and their results in this session
438
516
  execution_history = []
439
517
 
@@ -1853,7 +1931,7 @@ ssh_app = modal.App("ssh-container-app")
1853
1931
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
1854
1932
  "gpg", "ca-certificates", "software-properties-common"
1855
1933
  )
1856
- .pip_install("uv")
1934
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
1857
1935
  .run_commands(
1858
1936
  # Create SSH directory
1859
1937
  "mkdir -p /var/run/sshd",
@@ -1872,6 +1950,9 @@ ssh_app = modal.App("ssh-container-app")
1872
1950
  # Generate SSH host keys
1873
1951
  "ssh-keygen -A",
1874
1952
 
1953
+ # Install Modal CLI
1954
+ "pip install modal",
1955
+
1875
1956
  # Set up a nice bash prompt
1876
1957
  "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
1877
1958
  ),
@@ -1998,7 +2079,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1998
2079
  print(f"āš ļø Could not create default volume: {e}")
1999
2080
  print("āš ļø Continuing without persistent volume")
2000
2081
  volume = None
2001
-
2082
+
2002
2083
  # Create SSH-enabled image
2003
2084
  ssh_image = (
2004
2085
  modal.Image.debian_slim()
@@ -2007,7 +2088,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2007
2088
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2008
2089
  "gpg", "ca-certificates", "software-properties-common"
2009
2090
  )
2010
- .pip_install("uv") # Fast Python package installer
2091
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
2011
2092
  .run_commands(
2012
2093
  # Create SSH directory
2013
2094
  "mkdir -p /var/run/sshd",
@@ -2026,11 +2107,14 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2026
2107
  # Generate SSH host keys
2027
2108
  "ssh-keygen -A",
2028
2109
 
2110
+ # Install Modal CLI
2111
+ "pip install modal",
2112
+
2029
2113
  # Set up a nice bash prompt
2030
2114
  "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2031
2115
  )
2032
2116
  )
2033
-
2117
+
2034
2118
  # Create the Modal app
2035
2119
  app = modal.App(app_name)
2036
2120
 
@@ -2564,7 +2648,7 @@ def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=No
2564
2648
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2565
2649
  "gpg", "ca-certificates", "software-properties-common"
2566
2650
  )
2567
- .pip_install("uv")
2651
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
2568
2652
  .run_commands(
2569
2653
  # Create SSH directory
2570
2654
  "mkdir -p /var/run/sshd",