supyagent 0.1.0__tar.gz → 0.2.0__tar.gz
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-0.1.0 → supyagent-0.2.0}/PKG-INFO +69 -30
- {supyagent-0.1.0 → supyagent-0.2.0}/README.md +68 -29
- {supyagent-0.1.0 → supyagent-0.2.0}/pyproject.toml +1 -1
- supyagent-0.2.0/scripts/release.sh +151 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/cli/main.py +180 -1
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/__init__.py +3 -0
- supyagent-0.2.0/supyagent/core/config.py +352 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/.gitignore +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/LICENSE +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/agents/assistant.yaml +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/agents/coder.yaml +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/agents/planner.yaml +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/agents/researcher.yaml +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/agents/summarizer.yaml +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/agents/writer.yaml +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/plans/initial_plan.md +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/sprints/README.md +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_1_foundation.md +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_2_sessions.md +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_3_repl.md +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_4_execution.md +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_5_multiagent.md +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_6_polish.md +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/__init__.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/__main__.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/cli/__init__.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/agent.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/context.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/credentials.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/delegation.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/executor.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/llm.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/registry.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/session_manager.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/tools.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/models/__init__.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/models/agent_config.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/models/session.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/utils/__init__.py +0 -0
- {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/utils/paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: supyagent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: LLM agents powered by supypowers - build AI agents with tool use, multi-agent orchestration, and secure credential management
|
|
5
5
|
Project-URL: Homepage, https://github.com/ergodic-ai/supyagent
|
|
6
6
|
Project-URL: Documentation, https://github.com/ergodic-ai/supyagent#readme
|
|
@@ -68,6 +68,9 @@ uv pip install supyagent
|
|
|
68
68
|
## Quick Start
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
+
# Set up your API key (stored securely)
|
|
72
|
+
supyagent config set ANTHROPIC_API_KEY
|
|
73
|
+
|
|
71
74
|
# Create your first agent
|
|
72
75
|
supyagent new myagent
|
|
73
76
|
|
|
@@ -133,39 +136,40 @@ supyagent plan "Build a Python library for data validation"
|
|
|
133
136
|
# The planner will delegate to specialist agents (coder, writer, researcher)
|
|
134
137
|
```
|
|
135
138
|
|
|
136
|
-
##
|
|
139
|
+
## Configuration
|
|
137
140
|
|
|
138
|
-
|
|
141
|
+
### Setting Up API Keys
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
name: researcher
|
|
142
|
-
description: An AI research assistant
|
|
143
|
-
type: interactive # or "execution"
|
|
143
|
+
Supyagent securely stores your LLM API keys so you don't need to export them every time:
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
```bash
|
|
146
|
+
# Interactive setup (recommended)
|
|
147
|
+
supyagent config set
|
|
148
|
+
# Shows a menu of common providers to choose from
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
# Set a specific key
|
|
151
|
+
supyagent config set ANTHROPIC_API_KEY
|
|
152
|
+
supyagent config set OPENAI_API_KEY
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
- "*" # Allow all tools
|
|
156
|
-
# or be specific:
|
|
157
|
-
# - "web:*" # All functions in web.py
|
|
158
|
-
# - "math:calc" # Specific function
|
|
159
|
-
deny:
|
|
160
|
-
- "dangerous:*" # Block specific tools
|
|
154
|
+
# Import from a .env file
|
|
155
|
+
supyagent config import .env
|
|
161
156
|
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
# Import only specific keys
|
|
158
|
+
supyagent config import .env --filter OPENAI
|
|
159
|
+
|
|
160
|
+
# List configured keys
|
|
161
|
+
supyagent config list
|
|
162
|
+
|
|
163
|
+
# Export keys to backup
|
|
164
|
+
supyagent config export backup.env
|
|
165
|
+
|
|
166
|
+
# Delete a key
|
|
167
|
+
supyagent config delete OPENAI_API_KEY
|
|
166
168
|
```
|
|
167
169
|
|
|
168
|
-
|
|
170
|
+
Keys are encrypted and stored in `~/.supyagent/config/`. They're automatically loaded when running any agent command.
|
|
171
|
+
|
|
172
|
+
### Supported Providers
|
|
169
173
|
|
|
170
174
|
Supyagent uses LiteLLM, supporting 100+ providers:
|
|
171
175
|
|
|
@@ -187,11 +191,36 @@ model:
|
|
|
187
191
|
provider: gemini/gemini-pro
|
|
188
192
|
```
|
|
189
193
|
|
|
190
|
-
|
|
194
|
+
## Agent Configuration
|
|
191
195
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
Agents are defined in YAML files in the `agents/` directory:
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
name: researcher
|
|
200
|
+
description: An AI research assistant
|
|
201
|
+
type: interactive # or "execution"
|
|
202
|
+
|
|
203
|
+
model:
|
|
204
|
+
provider: anthropic/claude-3-5-sonnet-20241022
|
|
205
|
+
temperature: 0.7
|
|
206
|
+
max_tokens: 4096
|
|
207
|
+
|
|
208
|
+
system_prompt: |
|
|
209
|
+
You are a helpful research assistant...
|
|
210
|
+
|
|
211
|
+
tools:
|
|
212
|
+
allow:
|
|
213
|
+
- "*" # Allow all tools
|
|
214
|
+
# or be specific:
|
|
215
|
+
# - "web:*" # All functions in web.py
|
|
216
|
+
# - "math:calc" # Specific function
|
|
217
|
+
deny:
|
|
218
|
+
- "dangerous:*" # Block specific tools
|
|
219
|
+
|
|
220
|
+
# For multi-agent support
|
|
221
|
+
delegates:
|
|
222
|
+
- coder
|
|
223
|
+
- writer
|
|
195
224
|
```
|
|
196
225
|
|
|
197
226
|
## CLI Reference
|
|
@@ -223,6 +252,16 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
223
252
|
| `supyagent agents` | List active agent instances |
|
|
224
253
|
| `supyagent cleanup` | Remove completed instances |
|
|
225
254
|
|
|
255
|
+
### Config Commands
|
|
256
|
+
|
|
257
|
+
| Command | Description |
|
|
258
|
+
|---------|-------------|
|
|
259
|
+
| `supyagent config set [KEY]` | Set an API key (interactive menu if no key specified) |
|
|
260
|
+
| `supyagent config list` | List all configured keys |
|
|
261
|
+
| `supyagent config import <file>` | Import keys from .env file |
|
|
262
|
+
| `supyagent config export <file>` | Export keys to .env file |
|
|
263
|
+
| `supyagent config delete <key>` | Delete a stored key |
|
|
264
|
+
|
|
226
265
|
### In-Chat Commands
|
|
227
266
|
|
|
228
267
|
While chatting, use these commands:
|
|
@@ -32,6 +32,9 @@ uv pip install supyagent
|
|
|
32
32
|
## Quick Start
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
+
# Set up your API key (stored securely)
|
|
36
|
+
supyagent config set ANTHROPIC_API_KEY
|
|
37
|
+
|
|
35
38
|
# Create your first agent
|
|
36
39
|
supyagent new myagent
|
|
37
40
|
|
|
@@ -97,39 +100,40 @@ supyagent plan "Build a Python library for data validation"
|
|
|
97
100
|
# The planner will delegate to specialist agents (coder, writer, researcher)
|
|
98
101
|
```
|
|
99
102
|
|
|
100
|
-
##
|
|
103
|
+
## Configuration
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
### Setting Up API Keys
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
name: researcher
|
|
106
|
-
description: An AI research assistant
|
|
107
|
-
type: interactive # or "execution"
|
|
107
|
+
Supyagent securely stores your LLM API keys so you don't need to export them every time:
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
```bash
|
|
110
|
+
# Interactive setup (recommended)
|
|
111
|
+
supyagent config set
|
|
112
|
+
# Shows a menu of common providers to choose from
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
# Set a specific key
|
|
115
|
+
supyagent config set ANTHROPIC_API_KEY
|
|
116
|
+
supyagent config set OPENAI_API_KEY
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
- "*" # Allow all tools
|
|
120
|
-
# or be specific:
|
|
121
|
-
# - "web:*" # All functions in web.py
|
|
122
|
-
# - "math:calc" # Specific function
|
|
123
|
-
deny:
|
|
124
|
-
- "dangerous:*" # Block specific tools
|
|
118
|
+
# Import from a .env file
|
|
119
|
+
supyagent config import .env
|
|
125
120
|
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
121
|
+
# Import only specific keys
|
|
122
|
+
supyagent config import .env --filter OPENAI
|
|
123
|
+
|
|
124
|
+
# List configured keys
|
|
125
|
+
supyagent config list
|
|
126
|
+
|
|
127
|
+
# Export keys to backup
|
|
128
|
+
supyagent config export backup.env
|
|
129
|
+
|
|
130
|
+
# Delete a key
|
|
131
|
+
supyagent config delete OPENAI_API_KEY
|
|
130
132
|
```
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
Keys are encrypted and stored in `~/.supyagent/config/`. They're automatically loaded when running any agent command.
|
|
135
|
+
|
|
136
|
+
### Supported Providers
|
|
133
137
|
|
|
134
138
|
Supyagent uses LiteLLM, supporting 100+ providers:
|
|
135
139
|
|
|
@@ -151,11 +155,36 @@ model:
|
|
|
151
155
|
provider: gemini/gemini-pro
|
|
152
156
|
```
|
|
153
157
|
|
|
154
|
-
|
|
158
|
+
## Agent Configuration
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
Agents are defined in YAML files in the `agents/` directory:
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
name: researcher
|
|
164
|
+
description: An AI research assistant
|
|
165
|
+
type: interactive # or "execution"
|
|
166
|
+
|
|
167
|
+
model:
|
|
168
|
+
provider: anthropic/claude-3-5-sonnet-20241022
|
|
169
|
+
temperature: 0.7
|
|
170
|
+
max_tokens: 4096
|
|
171
|
+
|
|
172
|
+
system_prompt: |
|
|
173
|
+
You are a helpful research assistant...
|
|
174
|
+
|
|
175
|
+
tools:
|
|
176
|
+
allow:
|
|
177
|
+
- "*" # Allow all tools
|
|
178
|
+
# or be specific:
|
|
179
|
+
# - "web:*" # All functions in web.py
|
|
180
|
+
# - "math:calc" # Specific function
|
|
181
|
+
deny:
|
|
182
|
+
- "dangerous:*" # Block specific tools
|
|
183
|
+
|
|
184
|
+
# For multi-agent support
|
|
185
|
+
delegates:
|
|
186
|
+
- coder
|
|
187
|
+
- writer
|
|
159
188
|
```
|
|
160
189
|
|
|
161
190
|
## CLI Reference
|
|
@@ -187,6 +216,16 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
187
216
|
| `supyagent agents` | List active agent instances |
|
|
188
217
|
| `supyagent cleanup` | Remove completed instances |
|
|
189
218
|
|
|
219
|
+
### Config Commands
|
|
220
|
+
|
|
221
|
+
| Command | Description |
|
|
222
|
+
|---------|-------------|
|
|
223
|
+
| `supyagent config set [KEY]` | Set an API key (interactive menu if no key specified) |
|
|
224
|
+
| `supyagent config list` | List all configured keys |
|
|
225
|
+
| `supyagent config import <file>` | Import keys from .env file |
|
|
226
|
+
| `supyagent config export <file>` | Export keys to .env file |
|
|
227
|
+
| `supyagent config delete <key>` | Delete a stored key |
|
|
228
|
+
|
|
190
229
|
### In-Chat Commands
|
|
191
230
|
|
|
192
231
|
While chatting, use these commands:
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Release script for supyagent
|
|
4
|
+
# Usage: ./scripts/release.sh [version]
|
|
5
|
+
#
|
|
6
|
+
# Examples:
|
|
7
|
+
# ./scripts/release.sh 0.2.0 # Release version 0.2.0
|
|
8
|
+
# ./scripts/release.sh patch # Bump patch version (0.1.0 -> 0.1.1)
|
|
9
|
+
# ./scripts/release.sh minor # Bump minor version (0.1.0 -> 0.2.0)
|
|
10
|
+
# ./scripts/release.sh major # Bump major version (0.1.0 -> 1.0.0)
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
set -e # Exit on error
|
|
14
|
+
|
|
15
|
+
# Colors for output
|
|
16
|
+
RED='\033[0;31m'
|
|
17
|
+
GREEN='\033[0;32m'
|
|
18
|
+
YELLOW='\033[1;33m'
|
|
19
|
+
BLUE='\033[0;34m'
|
|
20
|
+
NC='\033[0m' # No Color
|
|
21
|
+
|
|
22
|
+
# Change to project root
|
|
23
|
+
cd "$(dirname "$0")/.."
|
|
24
|
+
PROJECT_ROOT=$(pwd)
|
|
25
|
+
|
|
26
|
+
echo -e "${BLUE}📦 Supyagent Release Script${NC}"
|
|
27
|
+
echo "================================"
|
|
28
|
+
echo ""
|
|
29
|
+
|
|
30
|
+
# Get current version from pyproject.toml
|
|
31
|
+
CURRENT_VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
|
|
32
|
+
echo -e "Current version: ${YELLOW}${CURRENT_VERSION}${NC}"
|
|
33
|
+
|
|
34
|
+
# Determine new version
|
|
35
|
+
if [ -z "$1" ]; then
|
|
36
|
+
echo -e "${RED}Error: Version argument required${NC}"
|
|
37
|
+
echo ""
|
|
38
|
+
echo "Usage: $0 [version|patch|minor|major]"
|
|
39
|
+
echo ""
|
|
40
|
+
echo "Examples:"
|
|
41
|
+
echo " $0 0.2.0 # Set specific version"
|
|
42
|
+
echo " $0 patch # Bump patch (0.1.0 -> 0.1.1)"
|
|
43
|
+
echo " $0 minor # Bump minor (0.1.0 -> 0.2.0)"
|
|
44
|
+
echo " $0 major # Bump major (0.1.0 -> 1.0.0)"
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Parse version
|
|
49
|
+
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
|
|
50
|
+
|
|
51
|
+
case "$1" in
|
|
52
|
+
patch)
|
|
53
|
+
NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
|
|
54
|
+
;;
|
|
55
|
+
minor)
|
|
56
|
+
NEW_VERSION="$MAJOR.$((MINOR + 1)).0"
|
|
57
|
+
;;
|
|
58
|
+
major)
|
|
59
|
+
NEW_VERSION="$((MAJOR + 1)).0.0"
|
|
60
|
+
;;
|
|
61
|
+
*)
|
|
62
|
+
NEW_VERSION="$1"
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
echo -e "New version: ${GREEN}${NEW_VERSION}${NC}"
|
|
67
|
+
echo ""
|
|
68
|
+
|
|
69
|
+
# Confirm
|
|
70
|
+
read -p "Proceed with release? [y/N] " -n 1 -r
|
|
71
|
+
echo ""
|
|
72
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
73
|
+
echo "Cancelled."
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
echo ""
|
|
78
|
+
echo -e "${BLUE}Step 1: Update version in pyproject.toml${NC}"
|
|
79
|
+
sed -i.bak "s/^version = \".*\"/version = \"${NEW_VERSION}\"/" pyproject.toml
|
|
80
|
+
rm -f pyproject.toml.bak
|
|
81
|
+
|
|
82
|
+
# Also update version in CLI if it's hardcoded there
|
|
83
|
+
if grep -q "version=\"${CURRENT_VERSION}\"" supyagent/cli/main.py 2>/dev/null; then
|
|
84
|
+
sed -i.bak "s/version=\"${CURRENT_VERSION}\"/version=\"${NEW_VERSION}\"/" supyagent/cli/main.py
|
|
85
|
+
rm -f supyagent/cli/main.py.bak
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
echo -e "${GREEN}✓${NC} Version updated to ${NEW_VERSION}"
|
|
89
|
+
|
|
90
|
+
echo ""
|
|
91
|
+
echo -e "${BLUE}Step 2: Run tests${NC}"
|
|
92
|
+
source .venv/bin/activate 2>/dev/null || true
|
|
93
|
+
python -m pytest tests/ -q
|
|
94
|
+
echo -e "${GREEN}✓${NC} All tests passed"
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
echo -e "${BLUE}Step 3: Clean old builds${NC}"
|
|
98
|
+
rm -rf dist/ build/ *.egg-info
|
|
99
|
+
echo -e "${GREEN}✓${NC} Cleaned build directories"
|
|
100
|
+
|
|
101
|
+
echo ""
|
|
102
|
+
echo -e "${BLUE}Step 4: Build package${NC}"
|
|
103
|
+
python -m build
|
|
104
|
+
echo -e "${GREEN}✓${NC} Built successfully"
|
|
105
|
+
|
|
106
|
+
echo ""
|
|
107
|
+
echo -e "${BLUE}Step 5: Verify package${NC}"
|
|
108
|
+
twine check dist/*
|
|
109
|
+
echo -e "${GREEN}✓${NC} Package verified"
|
|
110
|
+
|
|
111
|
+
echo ""
|
|
112
|
+
echo -e "${BLUE}Step 6: Upload to PyPI${NC}"
|
|
113
|
+
# Load credentials from .env if present
|
|
114
|
+
if [ -f .env ]; then
|
|
115
|
+
export $(cat .env | grep -v '^#' | xargs)
|
|
116
|
+
fi
|
|
117
|
+
twine upload dist/*
|
|
118
|
+
echo -e "${GREEN}✓${NC} Uploaded to PyPI"
|
|
119
|
+
|
|
120
|
+
echo ""
|
|
121
|
+
echo -e "${BLUE}Step 7: Clear local caches${NC}"
|
|
122
|
+
|
|
123
|
+
# Clear pip cache
|
|
124
|
+
echo " Clearing pip cache..."
|
|
125
|
+
pip cache purge 2>/dev/null || true
|
|
126
|
+
|
|
127
|
+
# Clear uv cache for supyagent
|
|
128
|
+
echo " Clearing uv cache..."
|
|
129
|
+
uv cache clean supyagent 2>/dev/null || true
|
|
130
|
+
|
|
131
|
+
# Remove any local installs
|
|
132
|
+
echo " Removing local editable install..."
|
|
133
|
+
pip uninstall -y supyagent 2>/dev/null || true
|
|
134
|
+
uv pip uninstall supyagent 2>/dev/null || true
|
|
135
|
+
|
|
136
|
+
echo -e "${GREEN}✓${NC} Caches cleared"
|
|
137
|
+
|
|
138
|
+
echo ""
|
|
139
|
+
echo -e "${GREEN}========================================${NC}"
|
|
140
|
+
echo -e "${GREEN}🎉 Released supyagent ${NEW_VERSION}${NC}"
|
|
141
|
+
echo -e "${GREEN}========================================${NC}"
|
|
142
|
+
echo ""
|
|
143
|
+
echo "View on PyPI: https://pypi.org/project/supyagent/${NEW_VERSION}/"
|
|
144
|
+
echo ""
|
|
145
|
+
echo "To install the new version:"
|
|
146
|
+
echo " pip install supyagent==${NEW_VERSION}"
|
|
147
|
+
echo " # or"
|
|
148
|
+
echo " uv pip install supyagent==${NEW_VERSION}"
|
|
149
|
+
echo ""
|
|
150
|
+
echo "To reinstall locally for development:"
|
|
151
|
+
echo " uv pip install -e ."
|
|
@@ -17,6 +17,7 @@ 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
|
|
@@ -26,7 +27,7 @@ console = Console()
|
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
@click.group()
|
|
29
|
-
@click.version_option(version="0.
|
|
30
|
+
@click.version_option(version="0.2.0", prog_name="supyagent")
|
|
30
31
|
def cli():
|
|
31
32
|
"""
|
|
32
33
|
Supyagent - LLM agents powered by supypowers.
|
|
@@ -175,6 +176,9 @@ def chat(agent_name: str, new_session: bool, session_id: str | None):
|
|
|
175
176
|
By default, resumes the most recent session. Use --new to start fresh,
|
|
176
177
|
or --session <id> to resume a specific session.
|
|
177
178
|
"""
|
|
179
|
+
# Load global config (API keys) into environment
|
|
180
|
+
load_config()
|
|
181
|
+
|
|
178
182
|
# Load agent config
|
|
179
183
|
try:
|
|
180
184
|
config = load_agent_config(agent_name)
|
|
@@ -625,6 +629,9 @@ def run(
|
|
|
625
629
|
echo "text" | supyagent run summarizer
|
|
626
630
|
supyagent run api-caller '{"endpoint": "/users"}' --secrets API_KEY=xxx
|
|
627
631
|
"""
|
|
632
|
+
# Load global config (API keys) into environment
|
|
633
|
+
load_config()
|
|
634
|
+
|
|
628
635
|
# Load agent config
|
|
629
636
|
try:
|
|
630
637
|
config = load_agent_config(agent_name)
|
|
@@ -742,6 +749,9 @@ def batch(
|
|
|
742
749
|
supyagent batch summarizer inputs.jsonl --output results.jsonl
|
|
743
750
|
supyagent batch summarizer data.csv --format csv
|
|
744
751
|
"""
|
|
752
|
+
# Load global config (API keys) into environment
|
|
753
|
+
load_config()
|
|
754
|
+
|
|
745
755
|
# Load agent config
|
|
746
756
|
try:
|
|
747
757
|
config = load_agent_config(agent_name)
|
|
@@ -879,6 +889,9 @@ def plan(task: str, planner: str, new_session: bool):
|
|
|
879
889
|
supyagent plan "Create a Python library for data validation"
|
|
880
890
|
supyagent plan "Write a blog post about AI" --planner my-planner
|
|
881
891
|
"""
|
|
892
|
+
# Load global config (API keys) into environment
|
|
893
|
+
load_config()
|
|
894
|
+
|
|
882
895
|
# Load planner config
|
|
883
896
|
try:
|
|
884
897
|
config = load_agent_config(planner)
|
|
@@ -942,5 +955,171 @@ def cleanup():
|
|
|
942
955
|
console.print(f"[green]✓[/green] Cleaned up {count} instance(s)")
|
|
943
956
|
|
|
944
957
|
|
|
958
|
+
# =============================================================================
|
|
959
|
+
# Config Commands
|
|
960
|
+
# =============================================================================
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
@cli.group()
|
|
964
|
+
def config():
|
|
965
|
+
"""Manage API keys and global configuration."""
|
|
966
|
+
pass
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
@config.command("set")
|
|
970
|
+
@click.argument("key_name", required=False)
|
|
971
|
+
@click.option(
|
|
972
|
+
"--value",
|
|
973
|
+
"-v",
|
|
974
|
+
help="Set value directly (use with caution - visible in shell history)",
|
|
975
|
+
)
|
|
976
|
+
def config_set(key_name: str | None, value: str | None):
|
|
977
|
+
"""
|
|
978
|
+
Set an API key.
|
|
979
|
+
|
|
980
|
+
If KEY_NAME is not provided, shows an interactive menu of common keys.
|
|
981
|
+
|
|
982
|
+
\b
|
|
983
|
+
Examples:
|
|
984
|
+
supyagent config set # Interactive menu
|
|
985
|
+
supyagent config set OPENAI_API_KEY # Set specific key
|
|
986
|
+
supyagent config set MY_KEY -v "value" # Set with value (not recommended)
|
|
987
|
+
"""
|
|
988
|
+
config_mgr = ConfigManager()
|
|
989
|
+
|
|
990
|
+
if value:
|
|
991
|
+
if not key_name:
|
|
992
|
+
console.print("[red]Error:[/red] KEY_NAME required when using --value")
|
|
993
|
+
sys.exit(1)
|
|
994
|
+
config_mgr.set(key_name, value)
|
|
995
|
+
console.print(f"[green]✓[/green] Saved {key_name}")
|
|
996
|
+
else:
|
|
997
|
+
config_mgr.set_interactive(key_name)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
@config.command("list")
|
|
1001
|
+
def config_list():
|
|
1002
|
+
"""List all configured API keys."""
|
|
1003
|
+
config_mgr = ConfigManager()
|
|
1004
|
+
config_mgr.show_status()
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
@config.command("delete")
|
|
1008
|
+
@click.argument("key_name")
|
|
1009
|
+
def config_delete(key_name: str):
|
|
1010
|
+
"""Delete a stored API key."""
|
|
1011
|
+
config_mgr = ConfigManager()
|
|
1012
|
+
|
|
1013
|
+
if config_mgr.delete(key_name):
|
|
1014
|
+
console.print(f"[green]✓[/green] Deleted {key_name}")
|
|
1015
|
+
else:
|
|
1016
|
+
console.print(f"[yellow]Key not found:[/yellow] {key_name}")
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
@config.command("import")
|
|
1020
|
+
@click.argument("file_path", type=click.Path(exists=True))
|
|
1021
|
+
@click.option(
|
|
1022
|
+
"--filter",
|
|
1023
|
+
"-f",
|
|
1024
|
+
"key_filter",
|
|
1025
|
+
help="Only import keys matching this prefix (e.g., 'OPENAI')",
|
|
1026
|
+
)
|
|
1027
|
+
def config_import(file_path: str, key_filter: str | None):
|
|
1028
|
+
"""
|
|
1029
|
+
Import API keys from a .env file.
|
|
1030
|
+
|
|
1031
|
+
The file should contain KEY=VALUE pairs, one per line.
|
|
1032
|
+
Lines starting with # are ignored.
|
|
1033
|
+
|
|
1034
|
+
\b
|
|
1035
|
+
Examples:
|
|
1036
|
+
supyagent config import .env
|
|
1037
|
+
supyagent config import secrets.env --filter OPENAI
|
|
1038
|
+
"""
|
|
1039
|
+
config_mgr = ConfigManager()
|
|
1040
|
+
|
|
1041
|
+
try:
|
|
1042
|
+
# If filter is specified, we need custom handling
|
|
1043
|
+
if key_filter:
|
|
1044
|
+
from pathlib import Path
|
|
1045
|
+
import re
|
|
1046
|
+
|
|
1047
|
+
path = Path(file_path)
|
|
1048
|
+
pattern = re.compile(r"^(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.+)$")
|
|
1049
|
+
imported = 0
|
|
1050
|
+
|
|
1051
|
+
with open(path) as f:
|
|
1052
|
+
for line in f:
|
|
1053
|
+
line = line.strip()
|
|
1054
|
+
if not line or line.startswith("#"):
|
|
1055
|
+
continue
|
|
1056
|
+
|
|
1057
|
+
match = pattern.match(line)
|
|
1058
|
+
if match:
|
|
1059
|
+
name, value = match.groups()
|
|
1060
|
+
if name.startswith(key_filter.upper()):
|
|
1061
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
|
1062
|
+
value.startswith("'") and value.endswith("'")
|
|
1063
|
+
):
|
|
1064
|
+
value = value[1:-1]
|
|
1065
|
+
config_mgr.set(name, value)
|
|
1066
|
+
console.print(f" [green]✓[/green] {name}")
|
|
1067
|
+
imported += 1
|
|
1068
|
+
else:
|
|
1069
|
+
imported = config_mgr.set_from_file(file_path)
|
|
1070
|
+
|
|
1071
|
+
if imported == 0:
|
|
1072
|
+
console.print("[yellow]No keys found in file[/yellow]")
|
|
1073
|
+
else:
|
|
1074
|
+
console.print(f"\n[green]✓[/green] Imported {imported} key(s)")
|
|
1075
|
+
|
|
1076
|
+
except FileNotFoundError:
|
|
1077
|
+
console.print(f"[red]Error:[/red] File not found: {file_path}")
|
|
1078
|
+
sys.exit(1)
|
|
1079
|
+
except Exception as e:
|
|
1080
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
1081
|
+
sys.exit(1)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
@config.command("export")
|
|
1085
|
+
@click.argument("file_path", type=click.Path())
|
|
1086
|
+
@click.option("--force", "-f", is_flag=True, help="Overwrite existing file")
|
|
1087
|
+
def config_export(file_path: str, force: bool):
|
|
1088
|
+
"""
|
|
1089
|
+
Export stored API keys to a .env file.
|
|
1090
|
+
|
|
1091
|
+
\b
|
|
1092
|
+
Example:
|
|
1093
|
+
supyagent config export backup.env
|
|
1094
|
+
"""
|
|
1095
|
+
config_mgr = ConfigManager()
|
|
1096
|
+
path = Path(file_path)
|
|
1097
|
+
|
|
1098
|
+
if path.exists() and not force:
|
|
1099
|
+
console.print(f"[red]Error:[/red] File exists: {file_path}")
|
|
1100
|
+
console.print("Use --force to overwrite")
|
|
1101
|
+
sys.exit(1)
|
|
1102
|
+
|
|
1103
|
+
keys = config_mgr._load_keys()
|
|
1104
|
+
|
|
1105
|
+
if not keys:
|
|
1106
|
+
console.print("[yellow]No keys to export[/yellow]")
|
|
1107
|
+
return
|
|
1108
|
+
|
|
1109
|
+
with open(path, "w") as f:
|
|
1110
|
+
f.write("# Supyagent API Keys\n")
|
|
1111
|
+
f.write("# Generated export - keep this file secure!\n\n")
|
|
1112
|
+
for name, value in sorted(keys.items()):
|
|
1113
|
+
f.write(f"{name}={value}\n")
|
|
1114
|
+
|
|
1115
|
+
# Set restrictive permissions
|
|
1116
|
+
try:
|
|
1117
|
+
path.chmod(0o600)
|
|
1118
|
+
except OSError:
|
|
1119
|
+
pass
|
|
1120
|
+
|
|
1121
|
+
console.print(f"[green]✓[/green] Exported {len(keys)} key(s) to {file_path}")
|
|
1122
|
+
|
|
1123
|
+
|
|
945
1124
|
if __name__ == "__main__":
|
|
946
1125
|
cli()
|
|
@@ -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
|
]
|
|
@@ -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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|