gitarsenal-cli 1.6.10 → 1.6.12

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/bin/gitarsenal.js CHANGED
@@ -60,7 +60,7 @@ const keysCmd = program
60
60
  keysCmd
61
61
  .command('add')
62
62
  .description('Add an API key')
63
- .option('-s, --service <service>', 'Service name (openai, wandb, huggingface)')
63
+ .option('-s, --service <service>', 'Service name (any service supported)')
64
64
  .option('-k, --key <key>', 'API key (if not provided, will prompt)')
65
65
  .action(async (options) => {
66
66
  await handleKeysAdd(options);
@@ -277,14 +277,10 @@ async function handleKeysAdd(options) {
277
277
  spinner.stop();
278
278
  const serviceAnswer = await inquirer.prompt([
279
279
  {
280
- type: 'list',
280
+ type: 'input',
281
281
  name: 'service',
282
- message: 'Select service:',
283
- choices: [
284
- { name: 'OpenAI', value: 'openai' },
285
- { name: 'Weights & Biases', value: 'wandb' },
286
- { name: 'Hugging Face', value: 'huggingface' }
287
- ]
282
+ message: 'Enter service name:',
283
+ validate: (input) => input.trim() !== '' ? true : 'Service name is required'
288
284
  }
289
285
  ]);
290
286
  service = serviceAnswer.service;
@@ -305,11 +301,11 @@ async function handleKeysAdd(options) {
305
301
 
306
302
  // Call Python script to add the key
307
303
  const { spawn } = require('child_process');
308
- const scriptPath = require('../lib/sandbox').getPythonScriptPath();
304
+ const path = require('path');
305
+ const scriptPath = path.join(__dirname, '..', 'python', 'gitarsenal_keys.py');
309
306
 
310
307
  const pythonProcess = spawn('python', [
311
308
  scriptPath,
312
- 'keys',
313
309
  'add',
314
310
  '--service', service,
315
311
  '--key', key
@@ -344,11 +340,11 @@ async function handleKeysList() {
344
340
 
345
341
  // Call Python script to list keys
346
342
  const { spawn } = require('child_process');
347
- const scriptPath = require('../lib/sandbox').getPythonScriptPath();
343
+ const path = require('path');
344
+ const scriptPath = path.join(__dirname, '..', 'python', 'gitarsenal_keys.py');
348
345
 
349
346
  const pythonProcess = spawn('python', [
350
347
  scriptPath,
351
- 'keys',
352
348
  'list'
353
349
  ], { stdio: 'inherit' });
354
350
 
@@ -376,14 +372,10 @@ async function handleKeysView(options) {
376
372
  spinner.stop();
377
373
  const serviceAnswer = await inquirer.prompt([
378
374
  {
379
- type: 'list',
375
+ type: 'input',
380
376
  name: 'service',
381
- message: 'Select service:',
382
- choices: [
383
- { name: 'OpenAI', value: 'openai' },
384
- { name: 'Weights & Biases', value: 'wandb' },
385
- { name: 'Hugging Face', value: 'huggingface' }
386
- ]
377
+ message: 'Enter service name:',
378
+ validate: (input) => input.trim() !== '' ? true : 'Service name is required'
387
379
  }
388
380
  ]);
389
381
  service = serviceAnswer.service;
@@ -391,11 +383,11 @@ async function handleKeysView(options) {
391
383
 
392
384
  // Call Python script to view the key
393
385
  const { spawn } = require('child_process');
394
- const scriptPath = require('../lib/sandbox').getPythonScriptPath();
386
+ const path = require('path');
387
+ const scriptPath = path.join(__dirname, '..', 'python', 'gitarsenal_keys.py');
395
388
 
396
389
  const pythonProcess = spawn('python', [
397
390
  scriptPath,
398
- 'keys',
399
391
  'view',
400
392
  '--service', service
401
393
  ], { stdio: 'inherit' });
@@ -424,14 +416,10 @@ async function handleKeysDelete(options) {
424
416
  spinner.stop();
425
417
  const serviceAnswer = await inquirer.prompt([
426
418
  {
427
- type: 'list',
419
+ type: 'input',
428
420
  name: 'service',
429
- message: 'Select service:',
430
- choices: [
431
- { name: 'OpenAI', value: 'openai' },
432
- { name: 'Weights & Biases', value: 'wandb' },
433
- { name: 'Hugging Face', value: 'huggingface' }
434
- ]
421
+ message: 'Enter service name:',
422
+ validate: (input) => input.trim() !== '' ? true : 'Service name is required'
435
423
  }
436
424
  ]);
437
425
  service = serviceAnswer.service;
@@ -457,11 +445,11 @@ async function handleKeysDelete(options) {
457
445
 
458
446
  // Call Python script to delete the key
459
447
  const { spawn } = require('child_process');
460
- const scriptPath = require('../lib/sandbox').getPythonScriptPath();
448
+ const path = require('path');
449
+ const scriptPath = path.join(__dirname, '..', 'python', 'gitarsenal_keys.py');
461
450
 
462
451
  const pythonProcess = spawn('python', [
463
452
  scriptPath,
464
- 'keys',
465
453
  'delete',
466
454
  '--service', service
467
455
  ], { stdio: 'pipe' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.6.10",
3
+ "version": "1.6.12",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -115,17 +115,34 @@ class CredentialsManager:
115
115
  return None
116
116
 
117
117
  def get_openai_api_key(self):
118
- """Get OpenAI API key with validation"""
118
+ """Get OpenAI API key with validation - for user's repository execution"""
119
119
  def validate_openai_key(key):
120
120
  # Basic validation - OpenAI keys usually start with "sk-" and are 51 chars
121
121
  return key.startswith("sk-") and len(key) > 40
122
122
 
123
- # First check environment variable
123
+ # First check stored credentials (user's key)
124
+ credentials = self.load_credentials()
125
+ if "openai_api_key" in credentials:
126
+ stored_key = credentials["openai_api_key"]
127
+ if validate_openai_key(stored_key):
128
+ return stored_key
129
+
130
+ # Then check environment variable
124
131
  env_key = os.environ.get("OPENAI_API_KEY")
125
132
  if env_key and validate_openai_key(env_key):
126
133
  return env_key
127
134
 
128
- # Then try to fetch from server using fetch_modal_tokens if available
135
+ # For user's repository execution, prompt if no key found
136
+ prompt = "An OpenAI API key is needed to run this repository.\nYou can get your API key from: https://platform.openai.com/api-keys"
137
+ return self.get_credential("openai_api_key", prompt, is_password=True, validate_func=validate_openai_key)
138
+
139
+ def get_gitarsenal_openai_api_key(self):
140
+ """Get GitArsenal's OpenAI API key for debugging - never prompts user"""
141
+ def validate_openai_key(key):
142
+ # Basic validation - OpenAI keys usually start with "sk-" and are 51 chars
143
+ return key.startswith("sk-") and len(key) > 40
144
+
145
+ # First try to fetch from server using fetch_modal_tokens (GitArsenal's key)
129
146
  try:
130
147
  from fetch_modal_tokens import get_tokens
131
148
  _, _, api_key = get_tokens()
@@ -136,10 +153,23 @@ class CredentialsManager:
136
153
  except ImportError:
137
154
  pass
138
155
  except Exception as e:
139
- print(f"⚠️ Error fetching API key from server: {e}")
156
+ print(f"⚠️ Error fetching GitArsenal API key from server: {e}")
157
+
158
+ # Then check environment variable (for development/testing)
159
+ env_key = os.environ.get("GITARSENAL_OPENAI_API_KEY")
160
+ if env_key and validate_openai_key(env_key):
161
+ return env_key
140
162
 
141
- prompt = "To debug failed commands, an OpenAI API key is needed.\nYou can get your API key from: https://platform.openai.com/api-keys"
142
- return self.get_credential("openai_api_key", prompt, is_password=True, validate_func=validate_openai_key)
163
+ # Check for GitArsenal's key in credentials (for development)
164
+ credentials = self.load_credentials()
165
+ if "gitarsenal_openai_api_key" in credentials:
166
+ stored_key = credentials["gitarsenal_openai_api_key"]
167
+ if validate_openai_key(stored_key):
168
+ return stored_key
169
+
170
+ # If no GitArsenal key found, return None (don't prompt user)
171
+ print("⚠️ GitArsenal's OpenAI API key not available for debugging")
172
+ return None
143
173
 
144
174
  def get_modal_token(self):
145
175
  """Get Modal token with basic validation"""
@@ -163,6 +193,13 @@ class CredentialsManager:
163
193
  def validate_hf_token(token):
164
194
  # HF tokens are typically non-empty strings
165
195
  return bool(token) and len(token) > 8
196
+
197
+ # First check stored credentials
198
+ credentials = self.load_credentials()
199
+ if "huggingface_token" in credentials:
200
+ stored_token = credentials["huggingface_token"]
201
+ if validate_hf_token(stored_token):
202
+ return stored_token
166
203
 
167
204
  prompt = "A Hugging Face token is required.\nYou can get your token from: https://huggingface.co/settings/tokens"
168
205
  return self.get_credential("huggingface_token", prompt, is_password=True, validate_func=validate_hf_token)
@@ -172,6 +209,13 @@ class CredentialsManager:
172
209
  def validate_wandb_key(key):
173
210
  # W&B API keys are typically 40 characters
174
211
  return len(key) == 40
212
+
213
+ # First check stored credentials
214
+ credentials = self.load_credentials()
215
+ if "wandb_api_key" in credentials:
216
+ stored_key = credentials["wandb_api_key"]
217
+ if validate_wandb_key(stored_key):
218
+ return stored_key
175
219
 
176
220
  prompt = "A Weights & Biases API key is required.\nYou can get your API key from: https://wandb.ai/authorize"
177
221
  return self.get_credential("wandb_api_key", prompt, is_password=True, validate_func=validate_wandb_key)
@@ -189,6 +233,16 @@ class CredentialsManager:
189
233
  """Clear all saved credentials"""
190
234
  return self.save_credentials({})
191
235
 
236
+ def has_credential(self, key):
237
+ """Check if a credential exists without prompting"""
238
+ credentials = self.load_credentials()
239
+ return key in credentials and credentials[key]
240
+
241
+ def get_credential_silently(self, key):
242
+ """Get a credential without prompting if it exists"""
243
+ credentials = self.load_credentials()
244
+ return credentials.get(key)
245
+
192
246
  def cleanup_security_files(self):
193
247
  """Clean up all security-related files including legacy files"""
194
248
  try:
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ GitArsenal Keys Management Script
4
+ Handles API key storage and retrieval for GitArsenal CLI
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ import argparse
11
+ from pathlib import Path
12
+ from credentials_manager import CredentialsManager
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(description='GitArsenal Keys Management')
16
+ parser.add_argument('command', choices=['add', 'list', 'view', 'delete'], help='Command to execute')
17
+ parser.add_argument('--service', help='Service name (openai, wandb, huggingface)')
18
+ parser.add_argument('--key', help='API key (for add command)')
19
+
20
+ args = parser.parse_args()
21
+
22
+ # Initialize credentials manager
23
+ credentials_manager = CredentialsManager()
24
+
25
+ if args.command == 'add':
26
+ handle_add(credentials_manager, args)
27
+ elif args.command == 'list':
28
+ handle_list(credentials_manager)
29
+ elif args.command == 'view':
30
+ handle_view(credentials_manager, args)
31
+ elif args.command == 'delete':
32
+ handle_delete(credentials_manager, args)
33
+
34
+ def handle_add(credentials_manager, args):
35
+ """Handle adding a new API key"""
36
+ service = args.service
37
+ key = args.key
38
+
39
+ if not service:
40
+ print("❌ Service name is required. Use --service option.")
41
+ sys.exit(1)
42
+
43
+ if not key:
44
+ # Prompt for the API key interactively
45
+ print(f"\n🔑 {service.upper()} API KEY REQUIRED")
46
+ print("=" * 50)
47
+
48
+ # Get appropriate prompt based on service
49
+ prompts = {
50
+ 'openai': "Please enter your OpenAI API key:\nYou can get your API key from: https://platform.openai.com/api-keys",
51
+ 'wandb': "Please enter your Weights & Biases API key:\nYou can get your API key from: https://wandb.ai/authorize",
52
+ 'huggingface': "Please enter your Hugging Face token:\nYou can get your token from: https://huggingface.co/settings/tokens",
53
+ 'gitarsenal-openai': "Please enter GitArsenal's OpenAI API key for debugging:",
54
+ 'claude': "Please enter your Claude API key:\nYou can get your API key from: https://console.anthropic.com/"
55
+ }
56
+
57
+ prompt = prompts.get(service, f"Please enter your {service} API key:")
58
+ print(prompt)
59
+ print("-" * 50)
60
+
61
+ try:
62
+ import getpass
63
+ key = getpass.getpass("API Key (hidden): ").strip()
64
+ if not key:
65
+ print("❌ No API key provided.")
66
+ sys.exit(1)
67
+ print("✅ API key received successfully!")
68
+ except KeyboardInterrupt:
69
+ print("\n❌ API key input cancelled by user.")
70
+ sys.exit(1)
71
+ except Exception as e:
72
+ print(f"❌ Error getting API key: {e}")
73
+ sys.exit(1)
74
+
75
+ # Generate credential key from service name
76
+ # Convert service name to a standardized key format
77
+ credential_key = f"{service.replace('-', '_')}_api_key"
78
+
79
+ # Special mappings for backward compatibility
80
+ special_mappings = {
81
+ 'openai': 'openai_api_key',
82
+ 'wandb': 'wandb_api_key',
83
+ 'huggingface': 'huggingface_token',
84
+ 'gitarsenal-openai': 'gitarsenal_openai_api_key'
85
+ }
86
+
87
+ # Use special mapping if it exists, otherwise use generated key
88
+ credential_key = special_mappings.get(service, credential_key)
89
+
90
+ # Save the credential
91
+ credentials = credentials_manager.load_credentials()
92
+ credentials[credential_key] = key
93
+ success = credentials_manager.save_credentials(credentials)
94
+
95
+ if success:
96
+ print(f"✅ API key for {service} added successfully")
97
+ print(f"📁 Stored in: {credentials_manager.credentials_file}")
98
+ else:
99
+ print("❌ Failed to save API key")
100
+ sys.exit(1)
101
+
102
+ def handle_list(credentials_manager):
103
+ """Handle listing all stored API keys"""
104
+ credentials = credentials_manager.load_credentials()
105
+
106
+ if not credentials:
107
+ print("📭 No API keys stored")
108
+ return
109
+
110
+ print("🔑 Stored API Keys:")
111
+ print("=" * 50)
112
+
113
+ # Map credential keys to display names (for known services)
114
+ key_mapping = {
115
+ 'openai_api_key': 'OpenAI',
116
+ 'wandb_api_key': 'Weights & Biases',
117
+ 'huggingface_token': 'Hugging Face',
118
+ 'gitarsenal_openai_api_key': 'GitArsenal OpenAI',
119
+ 'claude_api_key': 'Claude'
120
+ }
121
+
122
+ for key, value in credentials.items():
123
+ # Generate display name from key if not in mapping
124
+ if key in key_mapping:
125
+ display_name = key_mapping[key]
126
+ else:
127
+ # Convert key back to service name for display
128
+ service_name = key.replace('_api_key', '').replace('_token', '').replace('_', '-')
129
+ display_name = service_name.title()
130
+
131
+ masked_value = value[:8] + "*" * (len(value) - 12) + value[-4:] if len(value) > 12 else "*" * len(value)
132
+ print(f" {display_name}: {masked_value}")
133
+
134
+ print(f"\n📁 Storage location: {credentials_manager.credentials_file}")
135
+
136
+ def handle_view(credentials_manager, args):
137
+ """Handle viewing a specific API key (masked)"""
138
+ service = args.service
139
+
140
+ if not service:
141
+ print("❌ Service name is required. Use --service option.")
142
+ sys.exit(1)
143
+
144
+ # Generate credential key from service name
145
+ credential_key = f"{service.replace('-', '_')}_api_key"
146
+
147
+ # Special mappings for backward compatibility
148
+ special_mappings = {
149
+ 'openai': 'openai_api_key',
150
+ 'wandb': 'wandb_api_key',
151
+ 'huggingface': 'huggingface_token',
152
+ 'gitarsenal-openai': 'gitarsenal_openai_api_key'
153
+ }
154
+
155
+ # Use special mapping if it exists, otherwise use generated key
156
+ credential_key = special_mappings.get(service, credential_key)
157
+ credentials = credentials_manager.load_credentials()
158
+
159
+ if credential_key not in credentials:
160
+ print(f"❌ No API key found for {service}")
161
+ sys.exit(1)
162
+
163
+ value = credentials[credential_key]
164
+ masked_value = value[:8] + "*" * (len(value) - 12) + value[-4:] if len(value) > 12 else "*" * len(value)
165
+
166
+ print(f"🔑 {service.upper()} API Key:")
167
+ print(f" {masked_value}")
168
+ print(f"📁 Stored in: {credentials_manager.credentials_file}")
169
+
170
+ def handle_delete(credentials_manager, args):
171
+ """Handle deleting an API key"""
172
+ service = args.service
173
+
174
+ if not service:
175
+ print("❌ Service name is required. Use --service option.")
176
+ sys.exit(1)
177
+
178
+ # Generate credential key from service name
179
+ credential_key = f"{service.replace('-', '_')}_api_key"
180
+
181
+ # Special mappings for backward compatibility
182
+ special_mappings = {
183
+ 'openai': 'openai_api_key',
184
+ 'wandb': 'wandb_api_key',
185
+ 'huggingface': 'huggingface_token',
186
+ 'gitarsenal-openai': 'gitarsenal_openai_api_key'
187
+ }
188
+
189
+ # Use special mapping if it exists, otherwise use generated key
190
+ credential_key = special_mappings.get(service, credential_key)
191
+ success = credentials_manager.clear_credential(credential_key)
192
+
193
+ if success:
194
+ print(f"✅ API key for {service} deleted successfully")
195
+ else:
196
+ print(f"❌ No API key found for {service}")
197
+
198
+ if __name__ == "__main__":
199
+ main()
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to verify dynamic service support
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ from gitarsenal_keys import main
9
+
10
+ def test_dynamic_services():
11
+ print("🧪 Testing GitArsenal Dynamic Service Support")
12
+ print("=" * 60)
13
+
14
+ # Test adding a new service (claude)
15
+ print("\n1. Testing Claude service addition...")
16
+ sys.argv = ['gitarsenal_keys.py', 'add', '--service', 'claude', '--key', 'sk-test-claude-key']
17
+
18
+ try:
19
+ main()
20
+ print("✅ Claude service test completed")
21
+ except SystemExit as e:
22
+ if e.code == 0:
23
+ print("✅ Claude service test completed successfully")
24
+ else:
25
+ print(f"❌ Claude service test failed with exit code {e.code}")
26
+
27
+ # Test adding another new service (custom-service)
28
+ print("\n2. Testing custom service addition...")
29
+ sys.argv = ['gitarsenal_keys.py', 'add', '--service', 'custom-service', '--key', 'custom-key-123']
30
+
31
+ try:
32
+ main()
33
+ print("✅ Custom service test completed")
34
+ except SystemExit as e:
35
+ if e.code == 0:
36
+ print("✅ Custom service test completed successfully")
37
+ else:
38
+ print(f"❌ Custom service test failed with exit code {e.code}")
39
+
40
+ # Test listing to see all services
41
+ print("\n3. Testing service listing...")
42
+ sys.argv = ['gitarsenal_keys.py', 'list']
43
+
44
+ try:
45
+ main()
46
+ print("✅ Service listing test completed")
47
+ except SystemExit as e:
48
+ if e.code == 0:
49
+ print("✅ Service listing test completed successfully")
50
+ else:
51
+ print(f"❌ Service listing test failed with exit code {e.code}")
52
+
53
+ # Test viewing a specific service
54
+ print("\n4. Testing service viewing...")
55
+ sys.argv = ['gitarsenal_keys.py', 'view', '--service', 'claude']
56
+
57
+ try:
58
+ main()
59
+ print("✅ Service viewing test completed")
60
+ except SystemExit as e:
61
+ if e.code == 0:
62
+ print("✅ Service viewing test completed successfully")
63
+ else:
64
+ print(f"❌ Service viewing test failed with exit code {e.code}")
65
+
66
+ if __name__ == "__main__":
67
+ test_dynamic_services()
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to verify interactive key prompting functionality
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ from gitarsenal_keys import main
9
+
10
+ def test_interactive_prompting():
11
+ print("🧪 Testing GitArsenal Interactive Key Prompting")
12
+ print("=" * 60)
13
+
14
+ # Test that the script can be called with just service
15
+ print("\n1. Testing interactive prompting...")
16
+ print(" This should prompt for the API key when only service is provided")
17
+
18
+ # Simulate command line arguments
19
+ sys.argv = ['gitarsenal_keys.py', 'add', '--service', 'huggingface']
20
+
21
+ try:
22
+ # This will prompt for the key interactively
23
+ main()
24
+ print("✅ Interactive prompting test completed")
25
+ except SystemExit as e:
26
+ if e.code == 0:
27
+ print("✅ Interactive prompting test completed successfully")
28
+ else:
29
+ print(f"❌ Interactive prompting test failed with exit code {e.code}")
30
+ except Exception as e:
31
+ print(f"❌ Interactive prompting test failed with error: {e}")
32
+
33
+ if __name__ == "__main__":
34
+ test_interactive_prompting()
@@ -1186,7 +1186,8 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1186
1186
 
1187
1187
  # Use a more stable CUDA base image and avoid problematic packages
1188
1188
  ssh_image = (
1189
- modal.Image.from_registry("nvidia/cuda:12.4.0-runtime-ubuntu22.04", add_python="3.11")
1189
+ # modal.Image.from_registry("nvidia/cuda:12.4.0-runtime-ubuntu22.04", add_python="3.11")
1190
+ modal.Image.debian_slim()
1190
1191
  .apt_install(
1191
1192
  "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
1192
1193
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
@@ -1186,7 +1186,8 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1186
1186
 
1187
1187
  # Use a more stable CUDA base image and avoid problematic packages
1188
1188
  ssh_image = (
1189
- modal.Image.from_registry("nvidia/cuda:12.4.0-runtime-ubuntu22.04", add_python="3.11")
1189
+ # modal.Image.from_registry("nvidia/cuda:12.4.0-runtime-ubuntu22.04", add_python="3.11")
1190
+ modal.Image.debian_slim()
1190
1191
  .apt_install(
1191
1192
  "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
1192
1193
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",