|
| 1 | +import asyncio |
| 2 | +import sys |
| 3 | +import webbrowser |
| 4 | + |
| 5 | +import click |
| 6 | +import yaml |
| 7 | + |
1 | 8 | from aicodebot import AICODEBOT |
2 | | -from aicodebot.config import get_config_file, read_config |
| 9 | +from aicodebot.config import detect_api_keys, fetch_models_for_provider, get_config_file, read_config |
3 | 10 | from aicodebot.helpers import create_and_write_file |
4 | 11 | from aicodebot.output import get_console |
5 | 12 | from aicodebot.prompts import DEFAULT_PERSONALITY, PERSONALITIES |
6 | | -import click, os, sys, webbrowser, yaml |
7 | 13 |
|
8 | 14 |
|
9 | 15 | @click.command() |
10 | 16 | @click.option("-v", "--verbose", count=True) |
11 | 17 | @click.option("--openai-api-key", envvar="OPENAI_API_KEY", help="Your OpenAI API key") |
12 | | -def configure(verbose, openai_api_key): |
13 | | - """Create or update the configuration file""" |
| 18 | +@click.option("--anthropic-api-key", envvar="ANTHROPIC_API_KEY", help="Your Anthropic API key") |
| 19 | +def configure(verbose, openai_api_key, anthropic_api_key): # noqa: ARG001 - verbose for future use |
| 20 | + """Create or update the configuration file with dynamic provider and model selection""" |
14 | 21 | console = get_console() |
15 | 22 |
|
16 | | - # --------------- Check for an existing key or set up defaults --------------- # |
17 | | - |
18 | | - config_data_defaults = { |
19 | | - "version": 1.2, |
20 | | - "openai_api_key": openai_api_key, |
21 | | - "personality": DEFAULT_PERSONALITY.name, |
22 | | - } |
23 | | - |
24 | | - config_data = config_data_defaults.copy() |
25 | | - config_file = get_config_file() |
26 | | - |
27 | | - existing_config = read_config() |
28 | | - if existing_config: |
29 | | - console.print(f"Config file already exists at {get_config_file()}.") |
30 | | - click.confirm("Do you want to rerun configure and overwrite it?", default=False, abort=True) |
31 | | - config_data.update( |
32 | | - { |
33 | | - "openai_api_key": existing_config["openai_api_key"], |
34 | | - "personality": existing_config["personality"], |
35 | | - } |
36 | | - ) |
37 | | - |
38 | | - config_data = config_data_defaults.copy() |
39 | | - |
40 | 23 | def write_config_file(config_data): |
41 | | - create_and_write_file(config_file, yaml.dump(config_data)) |
42 | | - console.print(f"✅ Created config file at {config_file}") |
| 24 | + create_and_write_file(get_config_file(), yaml.dump(config_data), overwrite=True) |
| 25 | + console.print(f"✅ Updated config file at {get_config_file()}") |
43 | 26 |
|
| 27 | + # Check if we're in a terminal for interactive mode |
44 | 28 | is_terminal = sys.stdout.isatty() |
45 | | - openai_api_key = openai_api_key or config_data["openai_api_key"] or os.getenv("OPENAI_API_KEY") |
46 | 29 | if not is_terminal: |
47 | | - if openai_api_key is None: |
48 | | - raise click.ClickException( |
49 | | - "🛑 No OpenAI API key found.\n" |
50 | | - "Please set the OPENAI_API_KEY environment variable or call configure with --openai-api-key set." |
51 | | - ) |
52 | | - else: |
53 | | - # If we are not in a terminal, then we can't ask for input, so just use the defaults and write the file |
54 | | - write_config_file(config_data) |
| 30 | + # Non-interactive mode - create config with detected API keys and defaults |
| 31 | + detected_keys = detect_api_keys() |
| 32 | + |
| 33 | + # Also check command-line arguments for API keys |
| 34 | + api_key = None |
| 35 | + selected_provider = None |
| 36 | + |
| 37 | + if openai_api_key: |
| 38 | + api_key = openai_api_key |
| 39 | + selected_provider = "openai" |
| 40 | + elif anthropic_api_key: |
| 41 | + api_key = anthropic_api_key |
| 42 | + selected_provider = "anthropic" |
| 43 | + elif "openai" in detected_keys: |
| 44 | + api_key = detected_keys["openai"]["key"] |
| 45 | + selected_provider = "openai" |
| 46 | + elif "anthropic" in detected_keys: |
| 47 | + api_key = detected_keys["anthropic"]["key"] |
| 48 | + selected_provider = "anthropic" |
| 49 | + |
| 50 | + if not api_key: |
| 51 | + console.print("Non-interactive mode detected. Use --openai-api-key or --anthropic-api-key to set keys.") |
55 | 52 | return |
56 | 53 |
|
57 | | - # ---------------- Collect the OPENAI_API_KEY and validate it ---------------- # |
58 | | - |
59 | | - if config_data["openai_api_key"] is None: |
60 | | - console.print( |
61 | | - f"You need an OpenAI API Key for {AICODEBOT}. You can get one on the OpenAI website.", |
62 | | - style=console.bot_style, |
63 | | - ) |
64 | | - openai_api_key_url = "https://platform.openai.com/account/api-keys" |
65 | | - if click.confirm("Open the api keys page in a browser?", default=False): |
66 | | - webbrowser.open(openai_api_key_url) |
| 54 | + # Select default model for provider |
| 55 | + if selected_provider == "openai": |
| 56 | + selected_model = {"id": "gpt-5", "name": "gpt-5"} |
| 57 | + else: |
| 58 | + selected_model = {"id": "claude-opus-4-1", "name": "claude-opus-4-1"} |
| 59 | + |
| 60 | + # Create config with defaults |
| 61 | + config_data = { |
| 62 | + "version": 1.3, |
| 63 | + "provider": selected_provider, |
| 64 | + "model": selected_model["id"], |
| 65 | + "personality": DEFAULT_PERSONALITY.name, |
| 66 | + } |
| 67 | + |
| 68 | + # Always store API key in non-interactive mode (needed for tests) |
| 69 | + if selected_provider == "openai": |
| 70 | + config_data["openai_api_key"] = api_key |
| 71 | + else: |
| 72 | + config_data["anthropic_api_key"] = api_key |
| 73 | + |
| 74 | + write_config_file(config_data) |
| 75 | + console.print(f"✅ Created config file in non-interactive mode with {selected_provider}") |
| 76 | + return |
| 77 | + |
| 78 | + # Check for existing configuration |
| 79 | + existing_config = read_config() or {} |
| 80 | + if existing_config and not click.confirm("Config file already exists. Do you want to reconfigure?", default=True): |
| 81 | + return |
| 82 | + |
| 83 | + console.print(f"\n🔧 Welcome to {AICODEBOT} Configuration!\n", style=console.bot_style) |
| 84 | + |
| 85 | + # Step 1: Detect existing API keys |
| 86 | + detected_keys = detect_api_keys() |
| 87 | + |
| 88 | + console.print("🔍 Scanning for existing API keys...", style=console.bot_style) |
| 89 | + if detected_keys: |
| 90 | + console.print("✅ Found the following API keys:", style="green") |
| 91 | + for provider, key_info in detected_keys.items(): |
| 92 | + console.print(f" • {provider.title()}: {'*' * 8 + key_info['key'][-8:]} (from {key_info['source']})") |
| 93 | + else: |
| 94 | + console.print("ℹ️ No API keys found in environment variables", style="yellow") |
| 95 | + |
| 96 | + # Step 2: Provider selection |
| 97 | + console.print("\n🤖 Choose your AI provider:", style=console.bot_style) |
| 98 | + |
| 99 | + available_providers = [] |
| 100 | + if detected_keys.get("openai") or openai_api_key: |
| 101 | + available_providers.append(("OpenAI", "openai")) |
| 102 | + if detected_keys.get("anthropic") or anthropic_api_key: |
| 103 | + available_providers.append(("Anthropic", "anthropic")) |
| 104 | + |
| 105 | + # Always allow manual entry |
| 106 | + if not available_providers: |
| 107 | + console.print("No API keys detected. You'll need to enter one manually.") |
| 108 | + |
| 109 | + all_providers = [("OpenAI", "openai"), ("Anthropic", "anthropic")] |
| 110 | + for i, (display_name, provider_id) in enumerate(all_providers, 1): |
| 111 | + status = "✅ Available" if any(p[1] == provider_id for p in available_providers) else "❌ Needs API key" |
| 112 | + console.print(f"{i}. {display_name} - {status}") |
| 113 | + |
| 114 | + provider_choice = click.prompt("Select provider (1-2)", type=click.IntRange(1, 2), default=1) |
| 115 | + |
| 116 | + selected_provider_display, selected_provider = all_providers[provider_choice - 1] |
| 117 | + console.print(f"Selected: {selected_provider_display}") |
| 118 | + |
| 119 | + # Step 3: Handle API key |
| 120 | + api_key = None |
| 121 | + if selected_provider == "openai": |
| 122 | + api_key = detected_keys.get("openai", {}).get("key") or openai_api_key |
| 123 | + env_var = "OPENAI_API_KEY" |
| 124 | + api_url = "https://platform.openai.com/account/api-keys" |
| 125 | + else: # anthropic |
| 126 | + api_key = detected_keys.get("anthropic", {}).get("key") or anthropic_api_key |
| 127 | + env_var = "ANTHROPIC_API_KEY" |
| 128 | + api_url = "https://console.anthropic.com/account/keys" |
| 129 | + |
| 130 | + if not api_key: |
| 131 | + console.print(f"\n🔑 You need a {selected_provider_display} API key.", style=console.bot_style) |
| 132 | + if click.confirm(f"Open {selected_provider_display} API keys page in browser?", default=True): |
| 133 | + webbrowser.open(api_url) |
| 134 | + |
| 135 | + api_key = click.prompt(f"Enter your {selected_provider_display} API key").strip() |
| 136 | + console.print("💡 Consider setting this as an environment variable:", style="dim") |
| 137 | + console.print(f" export {env_var}={api_key}", style="dim") |
| 138 | + |
| 139 | + # Step 4: Fetch and select model |
| 140 | + console.print(f"\n🧠 Fetching available models from {selected_provider_display}...", style=console.bot_style) |
| 141 | + |
| 142 | + models = asyncio.run(fetch_models_for_provider(selected_provider, api_key)) |
| 143 | + |
| 144 | + if not models: |
| 145 | + raise ValueError(f"No models returned from {selected_provider_display}") |
| 146 | + |
| 147 | + console.print(f"✅ Found {len(models)} available models:") |
| 148 | + |
| 149 | + for i, model in enumerate(models[:10], 1): # Show first 10 models |
| 150 | + console.print(f"{i:2}. {model['name']} - {model['description']}") |
| 151 | + |
| 152 | + if len(models) > 10: |
| 153 | + console.print(f"... and {len(models) - 10} more models") |
| 154 | + |
| 155 | + # Add option for custom model |
| 156 | + console.print(f"{len(models) + 1:2}. [Custom] Enter your own model name") |
| 157 | + |
| 158 | + max_choice = min(len(models), 10) + 1 |
| 159 | + model_choice = click.prompt(f"Select model (1-{max_choice})", type=click.IntRange(1, max_choice), default=1) |
| 160 | + |
| 161 | + if model_choice <= len(models): |
| 162 | + # User selected from the list |
| 163 | + selected_model = models[model_choice - 1] |
| 164 | + console.print(f"Selected: {selected_model['name']}") |
| 165 | + else: |
| 166 | + # User wants to enter custom model |
| 167 | + console.print("\n💡 You can enter any model ID (e.g., claude-opus-4-1, gpt-4o, claude-3-5-sonnet-20241022)") |
| 168 | + console.print("This allows you to use newer models that aren't in our list yet.") |
| 169 | + |
| 170 | + custom_model_id = click.prompt("Enter model ID").strip() |
| 171 | + selected_model = { |
| 172 | + "id": custom_model_id, |
| 173 | + "name": custom_model_id, |
| 174 | + "description": f"Custom model: {custom_model_id}", |
| 175 | + } |
| 176 | + console.print(f"Selected: {selected_model['name']} (custom)") |
| 177 | + |
| 178 | + # Step 5: Personality selection |
| 179 | + console.print("\n🎭 Choose your AI personality:", style=console.bot_style) |
| 180 | + |
| 181 | + personality_list = list(PERSONALITIES.items()) |
| 182 | + for i, (key, personality) in enumerate(personality_list, 1): |
| 183 | + console.print(f"{i}. {key} - {personality.description}") |
| 184 | + |
| 185 | + personality_choice = click.prompt( |
| 186 | + f"Select personality (1-{len(personality_list)})", type=click.IntRange(1, len(personality_list)), default=1 |
| 187 | + ) |
67 | 188 |
|
68 | | - config_data["openai_api_key"] = click.prompt("Please enter your OpenAI API key").strip() |
| 189 | + selected_personality = personality_list[personality_choice - 1][0] |
69 | 190 |
|
70 | | - # ---------------------- Collect the personality choice ---------------------- # |
| 191 | + # Step 6: Build and save configuration |
| 192 | + config_data = { |
| 193 | + "version": 1.3, # Updated version for new format |
| 194 | + "provider": selected_provider, |
| 195 | + "model": selected_model["id"], |
| 196 | + "personality": selected_personality, |
| 197 | + } |
71 | 198 |
|
72 | | - # Pull the choices from the name from each of the PERSONALITIES |
73 | | - console.print( |
74 | | - "\nHow would you like your AI to act? You can choose from the following personalities:\n", |
75 | | - style=console.bot_style, |
76 | | - ) |
77 | | - personality_choices = "" |
78 | | - for key, personality in PERSONALITIES.items(): |
79 | | - personality_choices += f"\t[b]{key}[/b] - {personality.description}\n" |
80 | | - console.print(personality_choices) |
81 | | - |
82 | | - config_data["personality"] = click.prompt( |
83 | | - "Please choose a personality", |
84 | | - type=click.Choice(PERSONALITIES.keys(), case_sensitive=False), |
85 | | - default=DEFAULT_PERSONALITY.name, |
86 | | - ) |
| 199 | + # Always store API key in config file |
| 200 | + if selected_provider == "openai": |
| 201 | + config_data["openai_api_key"] = api_key |
| 202 | + else: |
| 203 | + config_data["anthropic_api_key"] = api_key |
87 | 204 |
|
88 | 205 | write_config_file(config_data) |
89 | | - console.print(f"✅ Configuration complete, you're ready to run {AICODEBOT}!\n") |
| 206 | + |
| 207 | + # Summary |
| 208 | + console.print("\n🎉 Configuration complete!", style="green bold") |
| 209 | + console.print(f"Provider: {selected_provider_display}") |
| 210 | + console.print(f"Model: {selected_model['name']}") |
| 211 | + console.print(f"Personality: {selected_personality}") |
| 212 | + console.print(f"\nYou're ready to use {AICODEBOT}! 🚀") |
| 213 | + |
| 214 | + |
| 215 | +# Helper function for async execution in click context |
| 216 | +def run_async(coro): |
| 217 | + """Run an async coroutine in a click command context.""" |
| 218 | + try: |
| 219 | + loop = asyncio.get_event_loop() |
| 220 | + except RuntimeError: |
| 221 | + loop = asyncio.new_event_loop() |
| 222 | + asyncio.set_event_loop(loop) |
| 223 | + |
| 224 | + return loop.run_until_complete(coro) |
0 commit comments