pdd-cli 0.0.59__py3-none-any.whl → 0.0.61__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 pdd-cli might be problematic. Click here for more details.
- pdd/__init__.py +1 -1
- pdd/cli.py +16 -4
- pdd/pdd_completion.fish +24 -2
- pdd/pdd_completion.sh +18 -2
- pdd/pdd_completion.zsh +66 -0
- pdd/setup_tool.py +376 -277
- pdd/templates/generic/generate_prompt.prompt +142 -0
- {pdd_cli-0.0.59.dist-info → pdd_cli-0.0.61.dist-info}/METADATA +3 -3
- {pdd_cli-0.0.59.dist-info → pdd_cli-0.0.61.dist-info}/RECORD +13 -12
- {pdd_cli-0.0.59.dist-info → pdd_cli-0.0.61.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.59.dist-info → pdd_cli-0.0.61.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.59.dist-info → pdd_cli-0.0.61.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.59.dist-info → pdd_cli-0.0.61.dist-info}/top_level.txt +0 -0
pdd/setup_tool.py
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PDD Setup Script - Post-install configuration tool for PDD (Prompt Driven Development)
|
|
4
|
+
Helps new users bootstrap their PDD configuration with LLM API keys and basic settings.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
import os
|
|
4
8
|
import sys
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
import subprocess
|
|
10
|
+
import json
|
|
8
11
|
import requests
|
|
9
|
-
|
|
12
|
+
import csv
|
|
13
|
+
import importlib.resources
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, Optional, Tuple, List
|
|
10
16
|
|
|
11
17
|
# Global variables for non-ASCII characters and colors
|
|
12
18
|
HEAVY_HORIZONTAL = "━"
|
|
13
|
-
LIGHT_HORIZONTAL = "─"
|
|
19
|
+
LIGHT_HORIZONTAL = "─"
|
|
14
20
|
HEAVY_VERTICAL = "┃"
|
|
15
21
|
LIGHT_VERTICAL = "│"
|
|
16
22
|
TOP_LEFT_CORNER = "┏"
|
|
@@ -34,52 +40,44 @@ CYAN = "\033[96m"
|
|
|
34
40
|
YELLOW = "\033[93m"
|
|
35
41
|
BOLD = "\033[1m"
|
|
36
42
|
|
|
37
|
-
|
|
38
43
|
# Template content inline
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Google,gemini/gemini-2.5-pro,1.25,10.0,1360,,GOOGLE_API_KEY,0,True,none
|
|
56
|
-
OpenAI,gpt-5-mini,0.25,2.0,1325,,OPENAI_API_KEY,0,True,effort
|
|
57
|
-
OpenAI,gpt-5,1.25,10.0,1482,,OPENAI_API_KEY,0,True,effort
|
|
58
|
-
OpenAI,gpt-4.1,2.0,8.0,1253,,OPENAI_API_KEY,0,True,none"""
|
|
44
|
+
SUCCESS_PYTHON_TEMPLATE = """
|
|
45
|
+
Write a python script to print "You did it, <Username>!!!" to the console.
|
|
46
|
+
Do not write anything except that message.
|
|
47
|
+
Capitalize the username."""
|
|
48
|
+
|
|
49
|
+
def _read_packaged_llm_model_csv() -> Tuple[List[str], List[Dict[str, str]]]:
|
|
50
|
+
"""Load the packaged CSV (pdd/data/llm_model.csv) and return header + rows.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
(header_fields, rows) where header_fields is the list of column names
|
|
54
|
+
and rows is a list of dictionaries for each CSV row.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
csv_text = importlib.resources.files('pdd').joinpath('data/llm_model.csv').read_text()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise FileNotFoundError(f"Failed to load default LLM model CSV from package: {e}")
|
|
59
60
|
|
|
61
|
+
reader = csv.DictReader(csv_text.splitlines())
|
|
62
|
+
header = reader.fieldnames or []
|
|
63
|
+
rows = [row for row in reader]
|
|
64
|
+
return header, rows
|
|
60
65
|
|
|
61
66
|
def print_colored(text: str, color: str = WHITE, bold: bool = False) -> None:
|
|
62
|
-
"""Print colored text to console
|
|
63
|
-
|
|
67
|
+
"""Print colored text to console"""
|
|
64
68
|
style = BOLD + color if bold else color
|
|
65
69
|
print(f"{style}{text}{RESET}")
|
|
66
70
|
|
|
67
|
-
|
|
68
71
|
def create_divider(char: str = LIGHT_HORIZONTAL, width: int = 80) -> str:
|
|
69
|
-
"""Create a horizontal divider line
|
|
70
|
-
|
|
72
|
+
"""Create a horizontal divider line"""
|
|
71
73
|
return char * width
|
|
72
74
|
|
|
73
|
-
|
|
74
75
|
def create_fat_divider(width: int = 80) -> str:
|
|
75
|
-
"""Create a fat horizontal divider line
|
|
76
|
-
|
|
76
|
+
"""Create a fat horizontal divider line"""
|
|
77
77
|
return HEAVY_HORIZONTAL * width
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"""Print the PDD logo in ASCII art."""
|
|
82
|
-
|
|
79
|
+
def print_pdd_logo():
|
|
80
|
+
"""Print the PDD logo in ASCII art"""
|
|
83
81
|
logo = "\n".join(
|
|
84
82
|
[
|
|
85
83
|
" +xxxxxxxxxxxxxxx+",
|
|
@@ -100,77 +98,135 @@ def print_pdd_logo() -> None:
|
|
|
100
98
|
]
|
|
101
99
|
)
|
|
102
100
|
print(f"{CYAN}{logo}{RESET}")
|
|
103
|
-
|
|
101
|
+
print()
|
|
102
|
+
print_colored("Let's get set up quickly with a solid basic configuration!", WHITE, bold=True)
|
|
103
|
+
print()
|
|
104
|
+
print_colored("Supported: OpenAI, Google Gemini, and Anthropic Claude", WHITE)
|
|
104
105
|
print_colored("from their respective API endpoints (no third-parties, such as Azure)", WHITE)
|
|
105
106
|
print()
|
|
106
107
|
|
|
108
|
+
def get_csv_variable_names() -> Dict[str, str]:
|
|
109
|
+
"""Inspect packaged CSV to determine API key variable names per provider.
|
|
110
|
+
|
|
111
|
+
Focus on direct providers only: OpenAI GPT models (model startswith 'gpt-'),
|
|
112
|
+
Google Gemini (model startswith 'gemini/'), and Anthropic (model startswith 'anthropic/').
|
|
113
|
+
"""
|
|
114
|
+
header, rows = _read_packaged_llm_model_csv()
|
|
115
|
+
variable_names: Dict[str, str] = {}
|
|
116
|
+
|
|
117
|
+
for row in rows:
|
|
118
|
+
model = (row.get('model') or '').strip()
|
|
119
|
+
api_key = (row.get('api_key') or '').strip()
|
|
120
|
+
provider = (row.get('provider') or '').strip().upper()
|
|
121
|
+
|
|
122
|
+
if not api_key:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
if model.startswith('gpt-') and provider == 'OPENAI':
|
|
126
|
+
variable_names['OPENAI'] = api_key
|
|
127
|
+
elif model.startswith('gemini/') and provider == 'GOOGLE':
|
|
128
|
+
# Prefer direct Gemini key, not Vertex
|
|
129
|
+
variable_names['GOOGLE'] = api_key
|
|
130
|
+
elif model.startswith('anthropic/') and provider == 'ANTHROPIC':
|
|
131
|
+
variable_names['ANTHROPIC'] = api_key
|
|
132
|
+
|
|
133
|
+
# Fallbacks if not detected (keep prior behavior)
|
|
134
|
+
variable_names.setdefault('OPENAI', 'OPENAI_API_KEY')
|
|
135
|
+
# Prefer GEMINI_API_KEY name for Google if present
|
|
136
|
+
variable_names.setdefault('GOOGLE', 'GEMINI_API_KEY')
|
|
137
|
+
variable_names.setdefault('ANTHROPIC', 'ANTHROPIC_API_KEY')
|
|
138
|
+
return variable_names
|
|
107
139
|
|
|
108
140
|
def discover_api_keys() -> Dict[str, Optional[str]]:
|
|
109
|
-
"""Discover API keys from environment variables
|
|
110
|
-
|
|
141
|
+
"""Discover API keys from environment variables"""
|
|
142
|
+
# Get the variable names actually used in CSV template
|
|
143
|
+
csv_vars = get_csv_variable_names()
|
|
144
|
+
|
|
111
145
|
keys = {
|
|
112
|
-
|
|
113
|
-
|
|
146
|
+
'OPENAI_API_KEY': os.getenv('OPENAI_API_KEY'),
|
|
147
|
+
'ANTHROPIC_API_KEY': os.getenv('ANTHROPIC_API_KEY'),
|
|
114
148
|
}
|
|
149
|
+
|
|
150
|
+
# For Google, check both possible environment variables but use CSV template's variable name
|
|
151
|
+
google_var_name = csv_vars.get('GOOGLE', 'GEMINI_API_KEY') # Default to GEMINI_API_KEY
|
|
152
|
+
google_api_key = os.getenv('GEMINI_API_KEY') or os.getenv('GOOGLE_API_KEY')
|
|
153
|
+
keys[google_var_name] = google_api_key
|
|
154
|
+
|
|
115
155
|
return keys
|
|
116
156
|
|
|
117
|
-
|
|
118
157
|
def test_openai_key(api_key: str) -> bool:
|
|
119
|
-
"""Test OpenAI API key validity
|
|
120
|
-
|
|
158
|
+
"""Test OpenAI API key validity"""
|
|
121
159
|
if not api_key or not api_key.strip():
|
|
122
160
|
return False
|
|
123
|
-
|
|
161
|
+
|
|
124
162
|
try:
|
|
125
163
|
headers = {
|
|
126
|
-
|
|
127
|
-
|
|
164
|
+
'Authorization': f'Bearer {api_key.strip()}',
|
|
165
|
+
'Content-Type': 'application/json'
|
|
128
166
|
}
|
|
129
167
|
response = requests.get(
|
|
130
|
-
|
|
168
|
+
'https://api.openai.com/v1/models',
|
|
131
169
|
headers=headers,
|
|
132
|
-
timeout=10
|
|
170
|
+
timeout=10
|
|
133
171
|
)
|
|
134
172
|
return response.status_code == 200
|
|
135
173
|
except Exception:
|
|
136
174
|
return False
|
|
137
175
|
|
|
138
|
-
|
|
139
176
|
def test_google_key(api_key: str) -> bool:
|
|
140
|
-
"""Test Google Gemini API key validity
|
|
141
|
-
|
|
177
|
+
"""Test Google Gemini API key validity"""
|
|
142
178
|
if not api_key or not api_key.strip():
|
|
143
179
|
return False
|
|
144
|
-
|
|
180
|
+
|
|
145
181
|
try:
|
|
146
182
|
response = requests.get(
|
|
147
|
-
f
|
|
148
|
-
timeout=10
|
|
183
|
+
f'https://generativelanguage.googleapis.com/v1beta/models?key={api_key.strip()}',
|
|
184
|
+
timeout=10
|
|
149
185
|
)
|
|
150
186
|
return response.status_code == 200
|
|
151
187
|
except Exception:
|
|
152
188
|
return False
|
|
153
189
|
|
|
190
|
+
def test_anthropic_key(api_key: str) -> bool:
|
|
191
|
+
"""Test Anthropic API key validity"""
|
|
192
|
+
if not api_key or not api_key.strip():
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
headers = {
|
|
197
|
+
'x-api-key': api_key.strip(),
|
|
198
|
+
'Content-Type': 'application/json'
|
|
199
|
+
}
|
|
200
|
+
response = requests.get(
|
|
201
|
+
'https://api.anthropic.com/v1/messages',
|
|
202
|
+
headers=headers,
|
|
203
|
+
timeout=10
|
|
204
|
+
)
|
|
205
|
+
# Anthropic returns 400 for invalid request structure but 401/403 for bad keys
|
|
206
|
+
return response.status_code != 401 and response.status_code != 403
|
|
207
|
+
except Exception:
|
|
208
|
+
return False
|
|
154
209
|
|
|
155
210
|
def test_api_keys(keys: Dict[str, Optional[str]]) -> Dict[str, bool]:
|
|
156
|
-
"""Test all discovered API keys
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
211
|
+
"""Test all discovered API keys"""
|
|
212
|
+
results = {}
|
|
213
|
+
|
|
160
214
|
print_colored(f"\n{LIGHT_HORIZONTAL * 40}", CYAN)
|
|
161
215
|
print_colored("Testing discovered API keys...", CYAN, bold=True)
|
|
162
216
|
print_colored(f"{LIGHT_HORIZONTAL * 40}", CYAN)
|
|
163
|
-
|
|
217
|
+
|
|
164
218
|
for key_name, key_value in keys.items():
|
|
165
219
|
if key_value:
|
|
166
220
|
print(f"Testing {key_name}...", end=" ", flush=True)
|
|
167
|
-
if key_name ==
|
|
221
|
+
if key_name == 'OPENAI_API_KEY':
|
|
168
222
|
valid = test_openai_key(key_value)
|
|
169
|
-
elif key_name in [
|
|
223
|
+
elif key_name in ['GEMINI_API_KEY', 'GOOGLE_API_KEY']:
|
|
170
224
|
valid = test_google_key(key_value)
|
|
225
|
+
elif key_name == 'ANTHROPIC_API_KEY':
|
|
226
|
+
valid = test_anthropic_key(key_value)
|
|
171
227
|
else:
|
|
172
228
|
valid = False
|
|
173
|
-
|
|
229
|
+
|
|
174
230
|
if valid:
|
|
175
231
|
print_colored(f"{CHECK_MARK} Valid", CYAN)
|
|
176
232
|
results[key_name] = True
|
|
@@ -180,40 +236,41 @@ def test_api_keys(keys: Dict[str, Optional[str]]) -> Dict[str, bool]:
|
|
|
180
236
|
else:
|
|
181
237
|
print_colored(f"{key_name}: Not found", YELLOW)
|
|
182
238
|
results[key_name] = False
|
|
183
|
-
|
|
239
|
+
|
|
184
240
|
return results
|
|
185
241
|
|
|
186
|
-
|
|
187
242
|
def get_user_keys(current_keys: Dict[str, Optional[str]]) -> Dict[str, Optional[str]]:
|
|
188
|
-
"""Interactive key entry/modification
|
|
189
|
-
|
|
243
|
+
"""Interactive key entry/modification"""
|
|
190
244
|
print_colored(f"\n{create_fat_divider()}", YELLOW)
|
|
191
245
|
print_colored("API Key Configuration", YELLOW, bold=True)
|
|
192
246
|
print_colored(f"{create_fat_divider()}", YELLOW)
|
|
193
|
-
|
|
247
|
+
|
|
194
248
|
print_colored("You need only one API key to get started", WHITE)
|
|
195
249
|
print()
|
|
196
250
|
print_colored("Get API keys here:", WHITE)
|
|
197
251
|
print_colored(f" OpenAI {ARROW_RIGHT} https://platform.openai.com/api-keys", CYAN)
|
|
198
252
|
print_colored(f" Google Gemini {ARROW_RIGHT} https://aistudio.google.com/app/apikey", CYAN)
|
|
253
|
+
print_colored(f" Anthropic {ARROW_RIGHT} https://console.anthropic.com/settings/keys", CYAN)
|
|
199
254
|
print()
|
|
200
255
|
print_colored("A free instant starter key is available from Google Gemini (above)", CYAN)
|
|
201
256
|
print()
|
|
202
|
-
|
|
257
|
+
|
|
203
258
|
new_keys = current_keys.copy()
|
|
204
|
-
|
|
205
|
-
|
|
259
|
+
|
|
260
|
+
# Get the actual key names from discovered keys
|
|
261
|
+
key_names = list(current_keys.keys())
|
|
262
|
+
for key_name in key_names:
|
|
206
263
|
current_value = current_keys.get(key_name, "")
|
|
207
264
|
status = "found" if current_value else "not found"
|
|
208
|
-
|
|
265
|
+
|
|
209
266
|
print_colored(f"{LIGHT_HORIZONTAL * 60}", CYAN)
|
|
210
267
|
print_colored(f"{key_name} (currently: {status})", WHITE, bold=True)
|
|
211
|
-
|
|
268
|
+
|
|
212
269
|
if current_value:
|
|
213
|
-
prompt = "Enter new key or press ENTER to keep existing: "
|
|
270
|
+
prompt = f"Enter new key or press ENTER to keep existing: "
|
|
214
271
|
else:
|
|
215
|
-
prompt = "Enter API key (or press ENTER to skip): "
|
|
216
|
-
|
|
272
|
+
prompt = f"Enter API key (or press ENTER to skip): "
|
|
273
|
+
|
|
217
274
|
try:
|
|
218
275
|
user_input = input(f"{WHITE}{prompt}{RESET}").strip()
|
|
219
276
|
if user_input:
|
|
@@ -223,325 +280,367 @@ def get_user_keys(current_keys: Dict[str, Optional[str]]) -> Dict[str, Optional[
|
|
|
223
280
|
except KeyboardInterrupt:
|
|
224
281
|
print_colored("\n\nSetup cancelled.", YELLOW)
|
|
225
282
|
sys.exit(0)
|
|
226
|
-
|
|
283
|
+
|
|
227
284
|
return new_keys
|
|
228
285
|
|
|
229
|
-
|
|
230
286
|
def detect_shell() -> str:
|
|
231
|
-
"""Detect user's default shell
|
|
232
|
-
|
|
287
|
+
"""Detect user's default shell"""
|
|
233
288
|
try:
|
|
234
|
-
shell_path = os.getenv(
|
|
289
|
+
shell_path = os.getenv('SHELL', '/bin/bash')
|
|
235
290
|
shell_name = os.path.basename(shell_path)
|
|
236
291
|
return shell_name
|
|
237
|
-
except
|
|
238
|
-
return
|
|
239
|
-
|
|
292
|
+
except:
|
|
293
|
+
return 'bash'
|
|
240
294
|
|
|
241
295
|
def get_shell_init_file(shell: str) -> str:
|
|
242
|
-
"""Get the appropriate shell initialization file
|
|
243
|
-
|
|
296
|
+
"""Get the appropriate shell initialization file"""
|
|
244
297
|
home = Path.home()
|
|
245
|
-
|
|
298
|
+
|
|
246
299
|
shell_files = {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
300
|
+
'bash': home / '.bashrc',
|
|
301
|
+
'zsh': home / '.zshrc',
|
|
302
|
+
'fish': home / '.config/fish/config.fish',
|
|
303
|
+
'csh': home / '.cshrc',
|
|
304
|
+
'tcsh': home / '.tcshrc',
|
|
305
|
+
'ksh': home / '.kshrc',
|
|
306
|
+
'sh': home / '.profile'
|
|
253
307
|
}
|
|
254
|
-
|
|
255
|
-
return str(shell_files.get(shell, home /
|
|
256
|
-
|
|
308
|
+
|
|
309
|
+
return str(shell_files.get(shell, home / '.bashrc'))
|
|
257
310
|
|
|
258
311
|
def create_api_env_script(keys: Dict[str, str], shell: str) -> str:
|
|
259
|
-
"""Create shell-appropriate environment script
|
|
260
|
-
|
|
312
|
+
"""Create shell-appropriate environment script"""
|
|
261
313
|
valid_keys = {k: v for k, v in keys.items() if v}
|
|
262
|
-
|
|
263
|
-
if shell ==
|
|
314
|
+
|
|
315
|
+
if shell == 'fish':
|
|
264
316
|
lines = []
|
|
265
317
|
for key, value in valid_keys.items():
|
|
266
|
-
lines.append(f
|
|
267
|
-
return
|
|
268
|
-
|
|
318
|
+
lines.append(f'set -gx {key} "{value}"')
|
|
319
|
+
return '\n'.join(lines) + '\n'
|
|
320
|
+
elif shell in ['csh', 'tcsh']:
|
|
269
321
|
lines = []
|
|
270
322
|
for key, value in valid_keys.items():
|
|
271
|
-
lines.append(f
|
|
272
|
-
return
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def save_configuration(valid_keys: Dict[str, str]) -> Tuple[List[str], bool]:
|
|
281
|
-
"""Save configuration to ~/.pdd/ directory."""
|
|
323
|
+
lines.append(f'setenv {key} "{value}"')
|
|
324
|
+
return '\n'.join(lines) + '\n'
|
|
325
|
+
else: # bash, zsh, ksh, sh and others
|
|
326
|
+
lines = []
|
|
327
|
+
for key, value in valid_keys.items():
|
|
328
|
+
lines.append(f'export {key}="{value}"')
|
|
329
|
+
return '\n'.join(lines) + '\n'
|
|
282
330
|
|
|
331
|
+
def save_configuration(valid_keys: Dict[str, str]) -> Tuple[List[str], bool, Optional[str]]:
|
|
332
|
+
"""Save configuration to ~/.pdd/ directory"""
|
|
283
333
|
home = Path.home()
|
|
284
|
-
pdd_dir = home /
|
|
334
|
+
pdd_dir = home / '.pdd'
|
|
285
335
|
created_pdd_dir = False
|
|
286
|
-
saved_files
|
|
287
|
-
|
|
336
|
+
saved_files = []
|
|
337
|
+
|
|
338
|
+
# Create .pdd directory if it doesn't exist
|
|
288
339
|
if not pdd_dir.exists():
|
|
289
340
|
pdd_dir.mkdir(mode=0o755)
|
|
290
341
|
created_pdd_dir = True
|
|
291
|
-
|
|
342
|
+
|
|
343
|
+
# Detect shell and create api-env script
|
|
292
344
|
shell = detect_shell()
|
|
293
345
|
api_env_content = create_api_env_script(valid_keys, shell)
|
|
294
|
-
|
|
295
|
-
|
|
346
|
+
|
|
347
|
+
# Write shell-specific api-env file
|
|
348
|
+
api_env_file = pdd_dir / f'api-env.{shell}'
|
|
296
349
|
api_env_file.write_text(api_env_content)
|
|
297
350
|
api_env_file.chmod(0o755)
|
|
298
351
|
saved_files.append(str(api_env_file))
|
|
352
|
+
|
|
353
|
+
# Create llm_model.csv with models from packaged CSV filtered by provider and available keys
|
|
354
|
+
header_fields, rows = _read_packaged_llm_model_csv()
|
|
355
|
+
|
|
356
|
+
# Keep only direct Google Gemini (model startswith 'gemini/'), OpenAI GPT (gpt-*) and Anthropic (anthropic/*)
|
|
357
|
+
def _is_supported_model(row: Dict[str, str]) -> bool:
|
|
358
|
+
model = (row.get('model') or '').strip()
|
|
359
|
+
if model.startswith('gpt-'):
|
|
360
|
+
return True
|
|
361
|
+
if model.startswith('gemini/'):
|
|
362
|
+
return True
|
|
363
|
+
if model.startswith('anthropic/'):
|
|
364
|
+
return True
|
|
365
|
+
return False
|
|
299
366
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
llm_model_file
|
|
367
|
+
# Filter rows by supported models and by api_key presence in valid_keys
|
|
368
|
+
filtered_rows: List[Dict[str, str]] = []
|
|
369
|
+
for row in rows:
|
|
370
|
+
if not _is_supported_model(row):
|
|
371
|
+
continue
|
|
372
|
+
api_key_name = (row.get('api_key') or '').strip()
|
|
373
|
+
# Include only if we have a validated key for this row
|
|
374
|
+
if api_key_name and api_key_name in valid_keys:
|
|
375
|
+
filtered_rows.append(row)
|
|
376
|
+
|
|
377
|
+
# Write out the filtered CSV to ~/.pdd/llm_model.csv preserving column order
|
|
378
|
+
llm_model_file = pdd_dir / 'llm_model.csv'
|
|
379
|
+
with llm_model_file.open('w', newline='') as f:
|
|
380
|
+
writer = csv.DictWriter(f, fieldnames=header_fields)
|
|
381
|
+
writer.writeheader()
|
|
382
|
+
for row in filtered_rows:
|
|
383
|
+
writer.writerow({k: row.get(k, '') for k in header_fields})
|
|
312
384
|
saved_files.append(str(llm_model_file))
|
|
313
|
-
|
|
385
|
+
|
|
386
|
+
# Update shell init file
|
|
314
387
|
init_file_path = get_shell_init_file(shell)
|
|
315
388
|
init_file = Path(init_file_path)
|
|
316
|
-
|
|
389
|
+
init_file_updated = None
|
|
390
|
+
|
|
317
391
|
source_line = f'[ -f "{api_env_file}" ] && source "{api_env_file}"'
|
|
318
|
-
if shell ==
|
|
392
|
+
if shell == 'fish':
|
|
319
393
|
source_line = f'test -f "{api_env_file}"; and source "{api_env_file}"'
|
|
320
|
-
|
|
394
|
+
elif shell in ['csh', 'tcsh']:
|
|
395
|
+
source_line = f'if ( -f "{api_env_file}" ) source "{api_env_file}"'
|
|
396
|
+
elif shell == 'sh':
|
|
397
|
+
source_line = f'[ -f "{api_env_file}" ] && . "{api_env_file}"'
|
|
398
|
+
|
|
399
|
+
# Ensure parent directory exists (important for fish shell)
|
|
400
|
+
init_file.parent.mkdir(parents=True, exist_ok=True)
|
|
401
|
+
|
|
402
|
+
# Check if source line already exists
|
|
321
403
|
if init_file.exists():
|
|
322
404
|
content = init_file.read_text()
|
|
323
405
|
if str(api_env_file) not in content:
|
|
324
|
-
with init_file.open(
|
|
325
|
-
|
|
406
|
+
with init_file.open('a') as f:
|
|
407
|
+
f.write(f'\n# PDD API environment\n{source_line}\n')
|
|
408
|
+
init_file_updated = str(init_file)
|
|
326
409
|
else:
|
|
327
|
-
init_file.write_text(f
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def create_sample_prompt()
|
|
333
|
-
"""Create the sample prompt file
|
|
334
|
-
|
|
335
|
-
prompt_file
|
|
336
|
-
prompt_file.write_text(HELLO_PYTHON_TEMPLATE)
|
|
410
|
+
init_file.write_text(f'# PDD API environment\n{source_line}\n')
|
|
411
|
+
init_file_updated = str(init_file)
|
|
412
|
+
|
|
413
|
+
return saved_files, created_pdd_dir, init_file_updated
|
|
414
|
+
|
|
415
|
+
def create_sample_prompt():
|
|
416
|
+
"""Create the sample prompt file"""
|
|
417
|
+
prompt_file = Path('success_python.prompt')
|
|
418
|
+
prompt_file.write_text(SUCCESS_PYTHON_TEMPLATE)
|
|
337
419
|
return str(prompt_file)
|
|
338
420
|
|
|
339
|
-
|
|
340
421
|
def show_menu(keys: Dict[str, Optional[str]], test_results: Dict[str, bool]) -> str:
|
|
341
|
-
"""Show main menu and get user choice
|
|
342
|
-
|
|
422
|
+
"""Show main menu and get user choice"""
|
|
343
423
|
print_colored(f"\n{create_divider()}", CYAN)
|
|
344
424
|
print_colored("Main Menu", CYAN, bold=True)
|
|
345
425
|
print_colored(f"{create_divider()}", CYAN)
|
|
346
|
-
|
|
426
|
+
|
|
427
|
+
# Show current status
|
|
347
428
|
print_colored("Current API Key Status:", WHITE, bold=True)
|
|
348
|
-
|
|
429
|
+
# Get the actual key names from discovered keys
|
|
430
|
+
key_names = list(keys.keys())
|
|
431
|
+
for key_name in key_names:
|
|
349
432
|
key_value = keys.get(key_name)
|
|
350
433
|
if key_value:
|
|
351
|
-
status = (
|
|
352
|
-
f"{CHECK_MARK} Valid"
|
|
353
|
-
if test_results.get(key_name)
|
|
354
|
-
else f"{CROSS_MARK} Invalid"
|
|
355
|
-
)
|
|
434
|
+
status = f"{CHECK_MARK} Valid" if test_results.get(key_name) else f"{CROSS_MARK} Invalid"
|
|
356
435
|
status_color = CYAN if test_results.get(key_name) else YELLOW
|
|
357
436
|
else:
|
|
358
437
|
status = "Not configured"
|
|
359
438
|
status_color = YELLOW
|
|
360
|
-
|
|
439
|
+
|
|
361
440
|
print(f" {key_name}: ", end="")
|
|
362
441
|
print_colored(status, status_color)
|
|
363
|
-
|
|
442
|
+
|
|
364
443
|
print()
|
|
365
444
|
print_colored("Options:", WHITE, bold=True)
|
|
366
|
-
print(" 1. Re-enter API keys")
|
|
367
|
-
print(" 2. Re-test current keys")
|
|
368
|
-
print(" 3. Save configuration and exit")
|
|
369
|
-
print(" 4. Exit without saving")
|
|
445
|
+
print(f" 1. Re-enter API keys")
|
|
446
|
+
print(f" 2. Re-test current keys")
|
|
447
|
+
print(f" 3. Save configuration and exit")
|
|
448
|
+
print(f" 4. Exit without saving")
|
|
370
449
|
print()
|
|
371
|
-
|
|
450
|
+
|
|
372
451
|
while True:
|
|
373
452
|
try:
|
|
374
453
|
choice = input(f"{WHITE}Choose an option (1-4): {RESET}").strip()
|
|
375
|
-
if choice in [
|
|
454
|
+
if choice in ['1', '2', '3', '4']:
|
|
376
455
|
return choice
|
|
377
|
-
|
|
456
|
+
else:
|
|
457
|
+
print_colored("Please enter 1, 2, 3, or 4", YELLOW)
|
|
378
458
|
except KeyboardInterrupt:
|
|
379
459
|
print_colored("\n\nSetup cancelled.", YELLOW)
|
|
380
460
|
sys.exit(0)
|
|
381
461
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
saved_files: List[str],
|
|
385
|
-
created_pdd_dir: bool,
|
|
386
|
-
sample_prompt_file: str,
|
|
387
|
-
shell: str,
|
|
388
|
-
) -> str:
|
|
389
|
-
"""Create comprehensive exit summary."""
|
|
390
|
-
|
|
462
|
+
def create_exit_summary(saved_files: List[str], created_pdd_dir: bool, sample_prompt_file: str, shell: str, valid_keys: Dict[str, str], init_file_updated: Optional[str] = None) -> str:
|
|
463
|
+
"""Create comprehensive exit summary"""
|
|
391
464
|
summary_lines = [
|
|
392
465
|
"\n\n\n\n\n",
|
|
393
466
|
create_fat_divider(),
|
|
394
467
|
"PDD Setup Complete!",
|
|
395
468
|
create_fat_divider(),
|
|
396
469
|
"",
|
|
397
|
-
"
|
|
398
|
-
""
|
|
470
|
+
"API Keys Configured:",
|
|
471
|
+
""
|
|
399
472
|
]
|
|
400
|
-
|
|
401
|
-
|
|
473
|
+
|
|
474
|
+
# Add configured API keys information
|
|
475
|
+
if valid_keys:
|
|
476
|
+
for key_name, key_value in valid_keys.items():
|
|
477
|
+
# Show just the first and last few characters for security
|
|
478
|
+
masked_key = f"{key_value[:8]}...{key_value[-4:]}" if len(key_value) > 12 else "***"
|
|
479
|
+
summary_lines.append(f" {key_name}: {masked_key}")
|
|
480
|
+
summary_lines.extend(["", "Files created and configured:", ""])
|
|
481
|
+
else:
|
|
482
|
+
summary_lines.extend([" None", "", "Files created and configured:", ""])
|
|
483
|
+
|
|
484
|
+
# File descriptions with alignment
|
|
485
|
+
file_descriptions = []
|
|
402
486
|
if created_pdd_dir:
|
|
403
487
|
file_descriptions.append(("~/.pdd/", "PDD configuration directory"))
|
|
404
|
-
|
|
488
|
+
|
|
405
489
|
for file_path in saved_files:
|
|
406
|
-
if
|
|
407
|
-
file_descriptions.append((file_path, "API environment variables"))
|
|
408
|
-
elif
|
|
490
|
+
if 'api-env.' in file_path:
|
|
491
|
+
file_descriptions.append((file_path, f"API environment variables ({shell} shell)"))
|
|
492
|
+
elif 'llm_model.csv' in file_path:
|
|
409
493
|
file_descriptions.append((file_path, "LLM model configuration"))
|
|
410
|
-
|
|
494
|
+
|
|
411
495
|
file_descriptions.append((sample_prompt_file, "Sample prompt for testing"))
|
|
496
|
+
|
|
497
|
+
# Add shell init file if it was updated
|
|
498
|
+
if init_file_updated:
|
|
499
|
+
file_descriptions.append((init_file_updated, f"Shell startup file (updated to source API environment)"))
|
|
500
|
+
|
|
412
501
|
file_descriptions.append(("PDD-SETUP-SUMMARY.txt", "This summary"))
|
|
413
|
-
|
|
502
|
+
|
|
503
|
+
# Find max file path length for alignment
|
|
414
504
|
max_path_len = max(len(path) for path, _ in file_descriptions)
|
|
415
|
-
|
|
505
|
+
|
|
416
506
|
for file_path, description in file_descriptions:
|
|
417
507
|
summary_lines.append(f"{file_path:<{max_path_len + 2}}{description}")
|
|
418
|
-
|
|
419
|
-
summary_lines.extend(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if shell ==
|
|
432
|
-
source_cmd = f"
|
|
508
|
+
|
|
509
|
+
summary_lines.extend([
|
|
510
|
+
"",
|
|
511
|
+
create_divider(),
|
|
512
|
+
"",
|
|
513
|
+
"QUICK START:",
|
|
514
|
+
"",
|
|
515
|
+
f"1. Reload your shell environment:"
|
|
516
|
+
])
|
|
517
|
+
|
|
518
|
+
# Shell-specific source command for manual reloading
|
|
519
|
+
api_env_path = f"{Path.home()}/.pdd/api-env.{shell}"
|
|
520
|
+
# Use dot command for sh shell, source for others
|
|
521
|
+
if shell == 'sh':
|
|
522
|
+
source_cmd = f". {api_env_path}"
|
|
433
523
|
else:
|
|
434
524
|
source_cmd = f"source {api_env_path}"
|
|
435
|
-
|
|
436
|
-
summary_lines.extend(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
"""Main setup workflow."""
|
|
474
|
-
|
|
525
|
+
|
|
526
|
+
summary_lines.extend([
|
|
527
|
+
f" {source_cmd}",
|
|
528
|
+
"",
|
|
529
|
+
f"2. Generate code from the sample prompt:",
|
|
530
|
+
f" pdd generate success_python.prompt",
|
|
531
|
+
"",
|
|
532
|
+
create_divider(),
|
|
533
|
+
"",
|
|
534
|
+
"LEARN MORE:",
|
|
535
|
+
"",
|
|
536
|
+
f"{BULLET} PDD documentation: pdd --help",
|
|
537
|
+
f"{BULLET} PDD website: https://promptdriven.ai/",
|
|
538
|
+
f"{BULLET} Discord community: https://discord.gg/Yp4RTh8bG7",
|
|
539
|
+
"",
|
|
540
|
+
"TIPS:",
|
|
541
|
+
"",
|
|
542
|
+
f"{BULLET} IMPORTANT: Reload your shell environment using the source command above",
|
|
543
|
+
"",
|
|
544
|
+
f"{BULLET} Start with simple prompts and gradually increase complexity",
|
|
545
|
+
f"{BULLET} Try out 'pdd test' with your prompt+code to create test(s) pdd can use to automatically verify and fix your output code",
|
|
546
|
+
f"{BULLET} Try out 'pdd example' with your prompt+code to create examples which help pdd do better",
|
|
547
|
+
"",
|
|
548
|
+
f"{BULLET} As you get comfortable, learn configuration settings, including the .pddrc file, PDD_GENERATE_OUTPUT_PATH, and PDD_TEST_OUTPUT_PATH",
|
|
549
|
+
f"{BULLET} For larger projects, use Makefiles and/or 'pdd sync'",
|
|
550
|
+
f"{BULLET} For ongoing substantial projects, learn about llm_model.csv and the --strength,",
|
|
551
|
+
f" --temperature, and --time options to optimize model cost, latency, and output quality",
|
|
552
|
+
"",
|
|
553
|
+
f"{BULLET} Use 'pdd --help' to explore all available commands",
|
|
554
|
+
"",
|
|
555
|
+
"Problems? Shout out on our Discord for help! https://discord.gg/Yp4RTh8bG7"
|
|
556
|
+
])
|
|
557
|
+
|
|
558
|
+
return '\n'.join(summary_lines)
|
|
559
|
+
|
|
560
|
+
def main():
|
|
561
|
+
"""Main setup workflow"""
|
|
562
|
+
# Initial greeting
|
|
475
563
|
print_pdd_logo()
|
|
476
|
-
|
|
564
|
+
|
|
565
|
+
# Discover environment
|
|
477
566
|
print_colored(f"{create_divider()}", CYAN)
|
|
478
567
|
print_colored("Discovering local configuration...", CYAN, bold=True)
|
|
479
568
|
print_colored(f"{create_divider()}", CYAN)
|
|
480
|
-
|
|
569
|
+
|
|
481
570
|
keys = discover_api_keys()
|
|
571
|
+
|
|
572
|
+
# Test discovered keys
|
|
482
573
|
test_results = test_api_keys(keys)
|
|
483
|
-
|
|
574
|
+
|
|
575
|
+
# Main interaction loop
|
|
484
576
|
while True:
|
|
485
577
|
choice = show_menu(keys, test_results)
|
|
486
|
-
|
|
487
|
-
if choice ==
|
|
578
|
+
|
|
579
|
+
if choice == '1':
|
|
580
|
+
# Re-enter keys
|
|
488
581
|
keys = get_user_keys(keys)
|
|
489
582
|
test_results = test_api_keys(keys)
|
|
490
|
-
|
|
583
|
+
|
|
584
|
+
elif choice == '2':
|
|
585
|
+
# Re-test keys
|
|
491
586
|
test_results = test_api_keys(keys)
|
|
492
|
-
|
|
587
|
+
|
|
588
|
+
elif choice == '3':
|
|
589
|
+
# Save and exit
|
|
493
590
|
valid_keys = {k: v for k, v in keys.items() if v and test_results.get(k)}
|
|
494
|
-
|
|
591
|
+
|
|
495
592
|
if not valid_keys:
|
|
496
593
|
print_colored("\nNo valid API keys to save!", YELLOW)
|
|
497
594
|
continue
|
|
498
|
-
|
|
595
|
+
|
|
499
596
|
print_colored(f"\n{create_divider()}", CYAN)
|
|
500
597
|
print_colored("Saving configuration...", CYAN, bold=True)
|
|
501
598
|
print_colored(f"{create_divider()}", CYAN)
|
|
502
|
-
|
|
599
|
+
|
|
503
600
|
try:
|
|
504
|
-
saved_files, created_pdd_dir = save_configuration(valid_keys)
|
|
601
|
+
saved_files, created_pdd_dir, init_file_updated = save_configuration(valid_keys)
|
|
505
602
|
sample_prompt_file = create_sample_prompt()
|
|
506
603
|
shell = detect_shell()
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
)
|
|
514
|
-
|
|
515
|
-
summary_file = Path("PDD-SETUP-SUMMARY.txt")
|
|
604
|
+
|
|
605
|
+
# Create and display summary
|
|
606
|
+
summary = create_exit_summary(saved_files, created_pdd_dir, sample_prompt_file, shell, valid_keys, init_file_updated)
|
|
607
|
+
|
|
608
|
+
# Write summary to file
|
|
609
|
+
summary_file = Path('PDD-SETUP-SUMMARY.txt')
|
|
516
610
|
summary_file.write_text(summary)
|
|
517
|
-
|
|
518
|
-
|
|
611
|
+
|
|
612
|
+
# Display summary with colors
|
|
613
|
+
lines = summary.split('\n')
|
|
614
|
+
for line in lines:
|
|
519
615
|
if line == create_fat_divider():
|
|
520
616
|
print_colored(line, YELLOW, bold=True)
|
|
521
617
|
elif line == "PDD Setup Complete!":
|
|
522
618
|
print_colored(line, YELLOW, bold=True)
|
|
523
619
|
elif line == create_divider():
|
|
524
620
|
print_colored(line, CYAN)
|
|
525
|
-
elif
|
|
526
|
-
line
|
|
527
|
-
|
|
528
|
-
|
|
621
|
+
elif line.startswith("API Keys Configured:") or line.startswith("Files created and configured:"):
|
|
622
|
+
print_colored(line, CYAN, bold=True)
|
|
623
|
+
elif line.startswith("QUICK START:"):
|
|
624
|
+
print_colored(line, YELLOW, bold=True)
|
|
625
|
+
elif line.startswith("LEARN MORE:") or line.startswith("TIPS:"):
|
|
529
626
|
print_colored(line, CYAN, bold=True)
|
|
530
627
|
elif "IMPORTANT:" in line or "Problems?" in line:
|
|
531
628
|
print_colored(line, YELLOW, bold=True)
|
|
532
629
|
else:
|
|
533
630
|
print(line)
|
|
534
|
-
|
|
631
|
+
|
|
535
632
|
break
|
|
536
|
-
|
|
537
|
-
|
|
633
|
+
|
|
634
|
+
except Exception as e:
|
|
635
|
+
print_colored(f"Error saving configuration: {e}", YELLOW)
|
|
538
636
|
continue
|
|
539
|
-
|
|
637
|
+
|
|
638
|
+
elif choice == '4':
|
|
639
|
+
# Exit without saving
|
|
540
640
|
print_colored("\nExiting without saving configuration.", YELLOW)
|
|
541
641
|
break
|
|
542
642
|
|
|
543
|
-
|
|
544
|
-
if __name__ == "__main__":
|
|
643
|
+
if __name__ == '__main__':
|
|
545
644
|
try:
|
|
546
645
|
main()
|
|
547
646
|
except KeyboardInterrupt:
|