supyagent 0.1.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
Potentially problematic release.
This version of supyagent might be problematic. Click here for more details.
- supyagent/cli/main.py +251 -1
- supyagent/core/__init__.py +3 -0
- supyagent/core/config.py +352 -0
- supyagent/default_tools/__init__.py +74 -0
- supyagent/default_tools/files.py +439 -0
- supyagent/default_tools/shell.py +217 -0
- {supyagent-0.1.0.dist-info → supyagent-0.2.1.dist-info}/METADATA +95 -32
- {supyagent-0.1.0.dist-info → supyagent-0.2.1.dist-info}/RECORD +11 -7
- {supyagent-0.1.0.dist-info → supyagent-0.2.1.dist-info}/WHEEL +0 -0
- {supyagent-0.1.0.dist-info → supyagent-0.2.1.dist-info}/entry_points.txt +0 -0
- {supyagent-0.1.0.dist-info → supyagent-0.2.1.dist-info}/licenses/LICENSE +0 -0
supyagent/cli/main.py
CHANGED
|
@@ -17,16 +17,18 @@ from rich.table import Table
|
|
|
17
17
|
from typing import Any
|
|
18
18
|
|
|
19
19
|
from supyagent.core.agent import Agent
|
|
20
|
+
from supyagent.core.config import ConfigManager, load_config
|
|
20
21
|
from supyagent.core.executor import ExecutionRunner
|
|
21
22
|
from supyagent.core.registry import AgentRegistry
|
|
22
23
|
from supyagent.core.session_manager import SessionManager
|
|
24
|
+
from supyagent.default_tools import install_default_tools, list_default_tools
|
|
23
25
|
from supyagent.models.agent_config import AgentNotFoundError, load_agent_config
|
|
24
26
|
|
|
25
27
|
console = Console()
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
@click.group()
|
|
29
|
-
@click.version_option(version="0.1
|
|
31
|
+
@click.version_option(version="0.2.1", prog_name="supyagent")
|
|
30
32
|
def cli():
|
|
31
33
|
"""
|
|
32
34
|
Supyagent - LLM agents powered by supypowers.
|
|
@@ -36,12 +38,82 @@ def cli():
|
|
|
36
38
|
Quick start:
|
|
37
39
|
|
|
38
40
|
\b
|
|
41
|
+
supyagent init # Set up default tools
|
|
42
|
+
supyagent config set # Configure API keys
|
|
39
43
|
supyagent new myagent # Create an agent
|
|
40
44
|
supyagent chat myagent # Start chatting
|
|
41
45
|
"""
|
|
42
46
|
pass
|
|
43
47
|
|
|
44
48
|
|
|
49
|
+
@cli.command()
|
|
50
|
+
@click.option(
|
|
51
|
+
"--tools-dir",
|
|
52
|
+
"-t",
|
|
53
|
+
default="supypowers",
|
|
54
|
+
help="Directory for tools (default: supypowers/)",
|
|
55
|
+
)
|
|
56
|
+
@click.option(
|
|
57
|
+
"--force",
|
|
58
|
+
"-f",
|
|
59
|
+
is_flag=True,
|
|
60
|
+
help="Overwrite existing files",
|
|
61
|
+
)
|
|
62
|
+
def init(tools_dir: str, force: bool):
|
|
63
|
+
"""
|
|
64
|
+
Initialize supyagent in the current directory.
|
|
65
|
+
|
|
66
|
+
This sets up:
|
|
67
|
+
- Default tools in supypowers/ (shell commands, file operations)
|
|
68
|
+
- agents/ directory for agent configurations
|
|
69
|
+
|
|
70
|
+
\b
|
|
71
|
+
Examples:
|
|
72
|
+
supyagent init
|
|
73
|
+
supyagent init --tools-dir my_tools
|
|
74
|
+
"""
|
|
75
|
+
console.print("[bold]Initializing supyagent...[/bold]")
|
|
76
|
+
console.print()
|
|
77
|
+
|
|
78
|
+
# Create agents directory
|
|
79
|
+
agents_dir = Path("agents")
|
|
80
|
+
if not agents_dir.exists():
|
|
81
|
+
agents_dir.mkdir(parents=True)
|
|
82
|
+
console.print(f" [green]✓[/green] Created {agents_dir}/")
|
|
83
|
+
else:
|
|
84
|
+
console.print(f" [dim]○[/dim] {agents_dir}/ already exists")
|
|
85
|
+
|
|
86
|
+
# Install default tools
|
|
87
|
+
tools_path = Path(tools_dir)
|
|
88
|
+
|
|
89
|
+
if force:
|
|
90
|
+
# Remove and reinstall
|
|
91
|
+
import shutil
|
|
92
|
+
|
|
93
|
+
if tools_path.exists():
|
|
94
|
+
shutil.rmtree(tools_path)
|
|
95
|
+
|
|
96
|
+
if tools_path.exists() and any(tools_path.glob("*.py")):
|
|
97
|
+
console.print(f" [dim]○[/dim] {tools_dir}/ already has tools")
|
|
98
|
+
else:
|
|
99
|
+
count = install_default_tools(tools_path)
|
|
100
|
+
console.print(
|
|
101
|
+
f" [green]✓[/green] Installed {count} default tools to {tools_dir}/"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Show available tools
|
|
105
|
+
console.print()
|
|
106
|
+
console.print("[bold]Available tools:[/bold]")
|
|
107
|
+
for tool in list_default_tools():
|
|
108
|
+
console.print(f" • [cyan]{tool['name']}[/cyan]: {tool['description']}")
|
|
109
|
+
|
|
110
|
+
console.print()
|
|
111
|
+
console.print("[bold]Next steps:[/bold]")
|
|
112
|
+
console.print(" 1. Configure your API key: [cyan]supyagent config set[/cyan]")
|
|
113
|
+
console.print(" 2. Create an agent: [cyan]supyagent new myagent[/cyan]")
|
|
114
|
+
console.print(" 3. Start chatting: [cyan]supyagent chat myagent[/cyan]")
|
|
115
|
+
|
|
116
|
+
|
|
45
117
|
@cli.command()
|
|
46
118
|
@click.argument("name")
|
|
47
119
|
@click.option(
|
|
@@ -175,6 +247,9 @@ def chat(agent_name: str, new_session: bool, session_id: str | None):
|
|
|
175
247
|
By default, resumes the most recent session. Use --new to start fresh,
|
|
176
248
|
or --session <id> to resume a specific session.
|
|
177
249
|
"""
|
|
250
|
+
# Load global config (API keys) into environment
|
|
251
|
+
load_config()
|
|
252
|
+
|
|
178
253
|
# Load agent config
|
|
179
254
|
try:
|
|
180
255
|
config = load_agent_config(agent_name)
|
|
@@ -625,6 +700,9 @@ def run(
|
|
|
625
700
|
echo "text" | supyagent run summarizer
|
|
626
701
|
supyagent run api-caller '{"endpoint": "/users"}' --secrets API_KEY=xxx
|
|
627
702
|
"""
|
|
703
|
+
# Load global config (API keys) into environment
|
|
704
|
+
load_config()
|
|
705
|
+
|
|
628
706
|
# Load agent config
|
|
629
707
|
try:
|
|
630
708
|
config = load_agent_config(agent_name)
|
|
@@ -742,6 +820,9 @@ def batch(
|
|
|
742
820
|
supyagent batch summarizer inputs.jsonl --output results.jsonl
|
|
743
821
|
supyagent batch summarizer data.csv --format csv
|
|
744
822
|
"""
|
|
823
|
+
# Load global config (API keys) into environment
|
|
824
|
+
load_config()
|
|
825
|
+
|
|
745
826
|
# Load agent config
|
|
746
827
|
try:
|
|
747
828
|
config = load_agent_config(agent_name)
|
|
@@ -879,6 +960,9 @@ def plan(task: str, planner: str, new_session: bool):
|
|
|
879
960
|
supyagent plan "Create a Python library for data validation"
|
|
880
961
|
supyagent plan "Write a blog post about AI" --planner my-planner
|
|
881
962
|
"""
|
|
963
|
+
# Load global config (API keys) into environment
|
|
964
|
+
load_config()
|
|
965
|
+
|
|
882
966
|
# Load planner config
|
|
883
967
|
try:
|
|
884
968
|
config = load_agent_config(planner)
|
|
@@ -942,5 +1026,171 @@ def cleanup():
|
|
|
942
1026
|
console.print(f"[green]✓[/green] Cleaned up {count} instance(s)")
|
|
943
1027
|
|
|
944
1028
|
|
|
1029
|
+
# =============================================================================
|
|
1030
|
+
# Config Commands
|
|
1031
|
+
# =============================================================================
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
@cli.group()
|
|
1035
|
+
def config():
|
|
1036
|
+
"""Manage API keys and global configuration."""
|
|
1037
|
+
pass
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
@config.command("set")
|
|
1041
|
+
@click.argument("key_name", required=False)
|
|
1042
|
+
@click.option(
|
|
1043
|
+
"--value",
|
|
1044
|
+
"-v",
|
|
1045
|
+
help="Set value directly (use with caution - visible in shell history)",
|
|
1046
|
+
)
|
|
1047
|
+
def config_set(key_name: str | None, value: str | None):
|
|
1048
|
+
"""
|
|
1049
|
+
Set an API key.
|
|
1050
|
+
|
|
1051
|
+
If KEY_NAME is not provided, shows an interactive menu of common keys.
|
|
1052
|
+
|
|
1053
|
+
\b
|
|
1054
|
+
Examples:
|
|
1055
|
+
supyagent config set # Interactive menu
|
|
1056
|
+
supyagent config set OPENAI_API_KEY # Set specific key
|
|
1057
|
+
supyagent config set MY_KEY -v "value" # Set with value (not recommended)
|
|
1058
|
+
"""
|
|
1059
|
+
config_mgr = ConfigManager()
|
|
1060
|
+
|
|
1061
|
+
if value:
|
|
1062
|
+
if not key_name:
|
|
1063
|
+
console.print("[red]Error:[/red] KEY_NAME required when using --value")
|
|
1064
|
+
sys.exit(1)
|
|
1065
|
+
config_mgr.set(key_name, value)
|
|
1066
|
+
console.print(f"[green]✓[/green] Saved {key_name}")
|
|
1067
|
+
else:
|
|
1068
|
+
config_mgr.set_interactive(key_name)
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
@config.command("list")
|
|
1072
|
+
def config_list():
|
|
1073
|
+
"""List all configured API keys."""
|
|
1074
|
+
config_mgr = ConfigManager()
|
|
1075
|
+
config_mgr.show_status()
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
@config.command("delete")
|
|
1079
|
+
@click.argument("key_name")
|
|
1080
|
+
def config_delete(key_name: str):
|
|
1081
|
+
"""Delete a stored API key."""
|
|
1082
|
+
config_mgr = ConfigManager()
|
|
1083
|
+
|
|
1084
|
+
if config_mgr.delete(key_name):
|
|
1085
|
+
console.print(f"[green]✓[/green] Deleted {key_name}")
|
|
1086
|
+
else:
|
|
1087
|
+
console.print(f"[yellow]Key not found:[/yellow] {key_name}")
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
@config.command("import")
|
|
1091
|
+
@click.argument("file_path", type=click.Path(exists=True))
|
|
1092
|
+
@click.option(
|
|
1093
|
+
"--filter",
|
|
1094
|
+
"-f",
|
|
1095
|
+
"key_filter",
|
|
1096
|
+
help="Only import keys matching this prefix (e.g., 'OPENAI')",
|
|
1097
|
+
)
|
|
1098
|
+
def config_import(file_path: str, key_filter: str | None):
|
|
1099
|
+
"""
|
|
1100
|
+
Import API keys from a .env file.
|
|
1101
|
+
|
|
1102
|
+
The file should contain KEY=VALUE pairs, one per line.
|
|
1103
|
+
Lines starting with # are ignored.
|
|
1104
|
+
|
|
1105
|
+
\b
|
|
1106
|
+
Examples:
|
|
1107
|
+
supyagent config import .env
|
|
1108
|
+
supyagent config import secrets.env --filter OPENAI
|
|
1109
|
+
"""
|
|
1110
|
+
config_mgr = ConfigManager()
|
|
1111
|
+
|
|
1112
|
+
try:
|
|
1113
|
+
# If filter is specified, we need custom handling
|
|
1114
|
+
if key_filter:
|
|
1115
|
+
from pathlib import Path
|
|
1116
|
+
import re
|
|
1117
|
+
|
|
1118
|
+
path = Path(file_path)
|
|
1119
|
+
pattern = re.compile(r"^(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.+)$")
|
|
1120
|
+
imported = 0
|
|
1121
|
+
|
|
1122
|
+
with open(path) as f:
|
|
1123
|
+
for line in f:
|
|
1124
|
+
line = line.strip()
|
|
1125
|
+
if not line or line.startswith("#"):
|
|
1126
|
+
continue
|
|
1127
|
+
|
|
1128
|
+
match = pattern.match(line)
|
|
1129
|
+
if match:
|
|
1130
|
+
name, value = match.groups()
|
|
1131
|
+
if name.startswith(key_filter.upper()):
|
|
1132
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
|
1133
|
+
value.startswith("'") and value.endswith("'")
|
|
1134
|
+
):
|
|
1135
|
+
value = value[1:-1]
|
|
1136
|
+
config_mgr.set(name, value)
|
|
1137
|
+
console.print(f" [green]✓[/green] {name}")
|
|
1138
|
+
imported += 1
|
|
1139
|
+
else:
|
|
1140
|
+
imported = config_mgr.set_from_file(file_path)
|
|
1141
|
+
|
|
1142
|
+
if imported == 0:
|
|
1143
|
+
console.print("[yellow]No keys found in file[/yellow]")
|
|
1144
|
+
else:
|
|
1145
|
+
console.print(f"\n[green]✓[/green] Imported {imported} key(s)")
|
|
1146
|
+
|
|
1147
|
+
except FileNotFoundError:
|
|
1148
|
+
console.print(f"[red]Error:[/red] File not found: {file_path}")
|
|
1149
|
+
sys.exit(1)
|
|
1150
|
+
except Exception as e:
|
|
1151
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
1152
|
+
sys.exit(1)
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
@config.command("export")
|
|
1156
|
+
@click.argument("file_path", type=click.Path())
|
|
1157
|
+
@click.option("--force", "-f", is_flag=True, help="Overwrite existing file")
|
|
1158
|
+
def config_export(file_path: str, force: bool):
|
|
1159
|
+
"""
|
|
1160
|
+
Export stored API keys to a .env file.
|
|
1161
|
+
|
|
1162
|
+
\b
|
|
1163
|
+
Example:
|
|
1164
|
+
supyagent config export backup.env
|
|
1165
|
+
"""
|
|
1166
|
+
config_mgr = ConfigManager()
|
|
1167
|
+
path = Path(file_path)
|
|
1168
|
+
|
|
1169
|
+
if path.exists() and not force:
|
|
1170
|
+
console.print(f"[red]Error:[/red] File exists: {file_path}")
|
|
1171
|
+
console.print("Use --force to overwrite")
|
|
1172
|
+
sys.exit(1)
|
|
1173
|
+
|
|
1174
|
+
keys = config_mgr._load_keys()
|
|
1175
|
+
|
|
1176
|
+
if not keys:
|
|
1177
|
+
console.print("[yellow]No keys to export[/yellow]")
|
|
1178
|
+
return
|
|
1179
|
+
|
|
1180
|
+
with open(path, "w") as f:
|
|
1181
|
+
f.write("# Supyagent API Keys\n")
|
|
1182
|
+
f.write("# Generated export - keep this file secure!\n\n")
|
|
1183
|
+
for name, value in sorted(keys.items()):
|
|
1184
|
+
f.write(f"{name}={value}\n")
|
|
1185
|
+
|
|
1186
|
+
# Set restrictive permissions
|
|
1187
|
+
try:
|
|
1188
|
+
path.chmod(0o600)
|
|
1189
|
+
except OSError:
|
|
1190
|
+
pass
|
|
1191
|
+
|
|
1192
|
+
console.print(f"[green]✓[/green] Exported {len(keys)} key(s) to {file_path}")
|
|
1193
|
+
|
|
1194
|
+
|
|
945
1195
|
if __name__ == "__main__":
|
|
946
1196
|
cli()
|
supyagent/core/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Core module for supyagent."""
|
|
2
2
|
|
|
3
3
|
from supyagent.core.agent import Agent
|
|
4
|
+
from supyagent.core.config import ConfigManager, load_config
|
|
4
5
|
from supyagent.core.context import DelegationContext
|
|
5
6
|
from supyagent.core.credentials import CredentialManager
|
|
6
7
|
from supyagent.core.delegation import DelegationManager
|
|
@@ -12,10 +13,12 @@ from supyagent.core.session_manager import SessionManager
|
|
|
12
13
|
__all__ = [
|
|
13
14
|
"Agent",
|
|
14
15
|
"AgentRegistry",
|
|
16
|
+
"ConfigManager",
|
|
15
17
|
"CredentialManager",
|
|
16
18
|
"DelegationContext",
|
|
17
19
|
"DelegationManager",
|
|
18
20
|
"ExecutionRunner",
|
|
19
21
|
"LLMClient",
|
|
20
22
|
"SessionManager",
|
|
23
|
+
"load_config",
|
|
21
24
|
]
|
supyagent/core/config.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration manager for global settings like LLM API keys.
|
|
3
|
+
|
|
4
|
+
Stores encrypted configuration in ~/.supyagent/config/ that is shared
|
|
5
|
+
across all agents and projects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import getpass
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.prompt import Confirm
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
# Common LLM provider API key names
|
|
23
|
+
KNOWN_LLM_KEYS = {
|
|
24
|
+
"OPENAI_API_KEY": "OpenAI (GPT-4, GPT-3.5)",
|
|
25
|
+
"ANTHROPIC_API_KEY": "Anthropic (Claude)",
|
|
26
|
+
"GOOGLE_API_KEY": "Google (Gemini)",
|
|
27
|
+
"AZURE_API_KEY": "Azure OpenAI",
|
|
28
|
+
"AZURE_API_BASE": "Azure OpenAI endpoint",
|
|
29
|
+
"COHERE_API_KEY": "Cohere",
|
|
30
|
+
"HUGGINGFACE_API_KEY": "Hugging Face",
|
|
31
|
+
"REPLICATE_API_KEY": "Replicate",
|
|
32
|
+
"TOGETHER_API_KEY": "Together AI",
|
|
33
|
+
"GROQ_API_KEY": "Groq",
|
|
34
|
+
"MISTRAL_API_KEY": "Mistral AI",
|
|
35
|
+
"PERPLEXITY_API_KEY": "Perplexity AI",
|
|
36
|
+
"DEEPSEEK_API_KEY": "DeepSeek",
|
|
37
|
+
"FIREWORKS_API_KEY": "Fireworks AI",
|
|
38
|
+
"OLLAMA_API_BASE": "Ollama (local) base URL",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ConfigManager:
|
|
43
|
+
"""
|
|
44
|
+
Manages global configuration including LLM API keys.
|
|
45
|
+
|
|
46
|
+
Configuration is stored encrypted in ~/.supyagent/config/
|
|
47
|
+
and automatically loaded into environment variables when
|
|
48
|
+
agents are run.
|
|
49
|
+
|
|
50
|
+
Directory structure:
|
|
51
|
+
~/.supyagent/config/.key # Encryption key
|
|
52
|
+
~/.supyagent/config/keys.enc # Encrypted API keys
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, base_dir: Path | None = None):
|
|
56
|
+
"""
|
|
57
|
+
Initialize the config manager.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
base_dir: Base directory for config storage.
|
|
61
|
+
Defaults to ~/.supyagent/config/
|
|
62
|
+
"""
|
|
63
|
+
if base_dir is None:
|
|
64
|
+
base_dir = Path.home() / ".supyagent" / "config"
|
|
65
|
+
|
|
66
|
+
self.base_dir = base_dir
|
|
67
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
self._fernet = self._get_fernet()
|
|
69
|
+
self._cache: dict[str, str] | None = None
|
|
70
|
+
|
|
71
|
+
def _get_fernet(self) -> Fernet:
|
|
72
|
+
"""Get or create the encryption key."""
|
|
73
|
+
key_file = self.base_dir / ".key"
|
|
74
|
+
|
|
75
|
+
if key_file.exists():
|
|
76
|
+
key = key_file.read_bytes()
|
|
77
|
+
else:
|
|
78
|
+
key = Fernet.generate_key()
|
|
79
|
+
key_file.write_bytes(key)
|
|
80
|
+
try:
|
|
81
|
+
key_file.chmod(0o600)
|
|
82
|
+
except OSError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
return Fernet(key)
|
|
86
|
+
|
|
87
|
+
def _keys_path(self) -> Path:
|
|
88
|
+
"""Get path to the encrypted keys file."""
|
|
89
|
+
return self.base_dir / "keys.enc"
|
|
90
|
+
|
|
91
|
+
def _load_keys(self) -> dict[str, str]:
|
|
92
|
+
"""Load and decrypt stored keys."""
|
|
93
|
+
if self._cache is not None:
|
|
94
|
+
return self._cache
|
|
95
|
+
|
|
96
|
+
path = self._keys_path()
|
|
97
|
+
if not path.exists():
|
|
98
|
+
self._cache = {}
|
|
99
|
+
return {}
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
encrypted = path.read_bytes()
|
|
103
|
+
decrypted = self._fernet.decrypt(encrypted)
|
|
104
|
+
keys = json.loads(decrypted)
|
|
105
|
+
self._cache = keys
|
|
106
|
+
return keys
|
|
107
|
+
except (InvalidToken, json.JSONDecodeError):
|
|
108
|
+
self._cache = {}
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
def _save_keys(self, keys: dict[str, str]) -> None:
|
|
112
|
+
"""Encrypt and save keys."""
|
|
113
|
+
encrypted = self._fernet.encrypt(json.dumps(keys).encode())
|
|
114
|
+
path = self._keys_path()
|
|
115
|
+
path.write_bytes(encrypted)
|
|
116
|
+
try:
|
|
117
|
+
path.chmod(0o600)
|
|
118
|
+
except OSError:
|
|
119
|
+
pass
|
|
120
|
+
self._cache = keys
|
|
121
|
+
|
|
122
|
+
def get(self, name: str) -> str | None:
|
|
123
|
+
"""
|
|
124
|
+
Get a config value.
|
|
125
|
+
|
|
126
|
+
Checks environment first, then stored config.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
name: Key name
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Value or None
|
|
133
|
+
"""
|
|
134
|
+
# Environment takes precedence
|
|
135
|
+
if name in os.environ:
|
|
136
|
+
return os.environ[name]
|
|
137
|
+
|
|
138
|
+
keys = self._load_keys()
|
|
139
|
+
return keys.get(name)
|
|
140
|
+
|
|
141
|
+
def set(self, name: str, value: str) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Set a config value.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
name: Key name
|
|
147
|
+
value: Key value
|
|
148
|
+
"""
|
|
149
|
+
keys = self._load_keys()
|
|
150
|
+
keys[name] = value
|
|
151
|
+
self._save_keys(keys)
|
|
152
|
+
|
|
153
|
+
def delete(self, name: str) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
Delete a stored key.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
name: Key name
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
True if deleted, False if not found
|
|
162
|
+
"""
|
|
163
|
+
keys = self._load_keys()
|
|
164
|
+
if name in keys:
|
|
165
|
+
del keys[name]
|
|
166
|
+
self._save_keys(keys)
|
|
167
|
+
return True
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
def list_keys(self) -> list[str]:
|
|
171
|
+
"""List all stored key names."""
|
|
172
|
+
return list(self._load_keys().keys())
|
|
173
|
+
|
|
174
|
+
def load_into_environment(self) -> int:
|
|
175
|
+
"""
|
|
176
|
+
Load all stored keys into environment variables.
|
|
177
|
+
|
|
178
|
+
Only sets variables that aren't already in the environment.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Number of keys loaded
|
|
182
|
+
"""
|
|
183
|
+
keys = self._load_keys()
|
|
184
|
+
loaded = 0
|
|
185
|
+
|
|
186
|
+
for name, value in keys.items():
|
|
187
|
+
if name not in os.environ:
|
|
188
|
+
os.environ[name] = value
|
|
189
|
+
loaded += 1
|
|
190
|
+
|
|
191
|
+
return loaded
|
|
192
|
+
|
|
193
|
+
def set_interactive(self, name: str | None = None) -> bool:
|
|
194
|
+
"""
|
|
195
|
+
Interactively prompt user to set a key.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
name: Key name, or None to show a menu of common keys
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
True if key was set
|
|
202
|
+
"""
|
|
203
|
+
if name is None:
|
|
204
|
+
# Show menu of common keys
|
|
205
|
+
console.print()
|
|
206
|
+
console.print("[bold]Common LLM API Keys:[/bold]")
|
|
207
|
+
console.print()
|
|
208
|
+
|
|
209
|
+
items = list(KNOWN_LLM_KEYS.items())
|
|
210
|
+
for i, (key, desc) in enumerate(items, 1):
|
|
211
|
+
status = "[green]✓[/green]" if self.get(key) else "[dim]○[/dim]"
|
|
212
|
+
console.print(f" {status} [{i}] {key}")
|
|
213
|
+
console.print(f" [dim]{desc}[/dim]")
|
|
214
|
+
|
|
215
|
+
console.print()
|
|
216
|
+
console.print(f" [0] Enter custom key name")
|
|
217
|
+
console.print()
|
|
218
|
+
|
|
219
|
+
choice = input("Select key to set (number or name): ").strip()
|
|
220
|
+
|
|
221
|
+
if choice == "0":
|
|
222
|
+
name = input("Enter key name: ").strip()
|
|
223
|
+
if not name:
|
|
224
|
+
return False
|
|
225
|
+
elif choice.isdigit():
|
|
226
|
+
idx = int(choice) - 1
|
|
227
|
+
if 0 <= idx < len(items):
|
|
228
|
+
name = items[idx][0]
|
|
229
|
+
else:
|
|
230
|
+
console.print("[red]Invalid selection[/red]")
|
|
231
|
+
return False
|
|
232
|
+
else:
|
|
233
|
+
# Treat as key name
|
|
234
|
+
name = choice.upper()
|
|
235
|
+
|
|
236
|
+
# Get the value
|
|
237
|
+
description = KNOWN_LLM_KEYS.get(name, "API key")
|
|
238
|
+
console.print()
|
|
239
|
+
console.print(
|
|
240
|
+
Panel(
|
|
241
|
+
f"[bold]{name}[/bold]\n{description}",
|
|
242
|
+
title="🔑 Set API Key",
|
|
243
|
+
border_style="blue",
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
value = getpass.getpass("Enter value (or press Enter to cancel): ")
|
|
248
|
+
|
|
249
|
+
if not value:
|
|
250
|
+
console.print("[dim]Cancelled[/dim]")
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
self.set(name, value)
|
|
254
|
+
console.print(f"[green]✓[/green] Saved {name}")
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
def set_from_file(self, file_path: str | Path) -> int:
|
|
258
|
+
"""
|
|
259
|
+
Load keys from a .env file.
|
|
260
|
+
|
|
261
|
+
File format (one per line):
|
|
262
|
+
KEY_NAME=value
|
|
263
|
+
# comments are ignored
|
|
264
|
+
export KEY_NAME=value # export prefix is stripped
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
file_path: Path to .env file
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Number of keys imported
|
|
271
|
+
"""
|
|
272
|
+
path = Path(file_path)
|
|
273
|
+
if not path.exists():
|
|
274
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
275
|
+
|
|
276
|
+
imported = 0
|
|
277
|
+
pattern = re.compile(r"^(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.+)$")
|
|
278
|
+
|
|
279
|
+
with open(path) as f:
|
|
280
|
+
for line in f:
|
|
281
|
+
line = line.strip()
|
|
282
|
+
|
|
283
|
+
# Skip empty lines and comments
|
|
284
|
+
if not line or line.startswith("#"):
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
match = pattern.match(line)
|
|
288
|
+
if match:
|
|
289
|
+
name, value = match.groups()
|
|
290
|
+
|
|
291
|
+
# Strip quotes if present
|
|
292
|
+
if (value.startswith('"') and value.endswith('"')) or \
|
|
293
|
+
(value.startswith("'") and value.endswith("'")):
|
|
294
|
+
value = value[1:-1]
|
|
295
|
+
|
|
296
|
+
self.set(name, value)
|
|
297
|
+
imported += 1
|
|
298
|
+
|
|
299
|
+
return imported
|
|
300
|
+
|
|
301
|
+
def show_status(self) -> None:
|
|
302
|
+
"""Display current configuration status."""
|
|
303
|
+
keys = self._load_keys()
|
|
304
|
+
|
|
305
|
+
if not keys:
|
|
306
|
+
console.print("[dim]No API keys configured[/dim]")
|
|
307
|
+
console.print()
|
|
308
|
+
console.print("Run [cyan]supyagent config set[/cyan] to add keys")
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
table = Table(title="Configured API Keys")
|
|
312
|
+
table.add_column("Key", style="cyan")
|
|
313
|
+
table.add_column("Provider")
|
|
314
|
+
table.add_column("Status")
|
|
315
|
+
|
|
316
|
+
for name in sorted(keys.keys()):
|
|
317
|
+
provider = KNOWN_LLM_KEYS.get(name, "Custom")
|
|
318
|
+
# Show if it's overridden by environment
|
|
319
|
+
if name in os.environ and os.environ[name] != keys[name]:
|
|
320
|
+
status = "[yellow]env override[/yellow]"
|
|
321
|
+
else:
|
|
322
|
+
status = "[green]stored[/green]"
|
|
323
|
+
|
|
324
|
+
table.add_row(name, provider, status)
|
|
325
|
+
|
|
326
|
+
console.print(table)
|
|
327
|
+
console.print()
|
|
328
|
+
console.print(f"[dim]Config location: {self.base_dir}[/dim]")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# Global config manager instance
|
|
332
|
+
_config_manager: ConfigManager | None = None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def get_config_manager() -> ConfigManager:
|
|
336
|
+
"""Get the global config manager instance."""
|
|
337
|
+
global _config_manager
|
|
338
|
+
if _config_manager is None:
|
|
339
|
+
_config_manager = ConfigManager()
|
|
340
|
+
return _config_manager
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def load_config() -> int:
|
|
344
|
+
"""
|
|
345
|
+
Load global config into environment.
|
|
346
|
+
|
|
347
|
+
Call this at the start of any agent execution.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Number of keys loaded
|
|
351
|
+
"""
|
|
352
|
+
return get_config_manager().load_into_environment()
|