pdd-cli 0.0.90__py3-none-any.whl → 0.0.118__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.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +497 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +526 -0
- pdd/agentic_common.py +521 -786
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +25 -8
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +185 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +87 -29
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +136 -113
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +190 -164
- pdd/commands/sessions.py +284 -0
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +27 -3
- pdd/core/cloud.py +237 -0
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +204 -4
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +459 -95
- pdd/load_prompt_template.py +15 -34
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +4 -1
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +20 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1322 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +209 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +136 -75
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +23 -5
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/get_jwt_token.py
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
2
7
|
import time
|
|
8
|
+
import webbrowser
|
|
9
|
+
from pathlib import Path
|
|
3
10
|
from typing import Dict, Optional, Tuple
|
|
4
11
|
|
|
5
12
|
# Cross-platform keyring import with fallback for WSL compatibility
|
|
@@ -39,6 +46,195 @@ class RateLimitError(AuthError):
|
|
|
39
46
|
"""Raised when rate limits are exceeded."""
|
|
40
47
|
pass
|
|
41
48
|
|
|
49
|
+
|
|
50
|
+
# JWT file cache path (Issue #273 - reduces keyring access to avoid password prompts)
|
|
51
|
+
JWT_CACHE_FILE = Path.home() / ".pdd" / "jwt_cache"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _decode_jwt_payload(token: str) -> Dict:
|
|
55
|
+
"""
|
|
56
|
+
Decode JWT payload without verification to extract claims.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
token: The JWT token string.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dict containing the JWT payload claims, or empty dict on error.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
# JWT is header.payload.signature
|
|
66
|
+
parts = token.split(".")
|
|
67
|
+
if len(parts) != 3:
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
payload = parts[1]
|
|
71
|
+
# Add padding if needed for base64 decoding
|
|
72
|
+
padding = len(payload) % 4
|
|
73
|
+
if padding:
|
|
74
|
+
payload += "=" * (4 - padding)
|
|
75
|
+
|
|
76
|
+
decoded = base64.urlsafe_b64decode(payload)
|
|
77
|
+
return json.loads(decoded)
|
|
78
|
+
except Exception:
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
def _get_expected_jwt_audience() -> Optional[str]:
|
|
82
|
+
"""
|
|
83
|
+
Determine the expected JWT audience based on PDD_ENV.
|
|
84
|
+
|
|
85
|
+
This keeps the JWT cache environment-aware when PDD_ENV is set
|
|
86
|
+
(e.g., staging vs prod) without changing the public API.
|
|
87
|
+
"""
|
|
88
|
+
explicit_aud = os.environ.get("PDD_JWT_EXPECTED_AUD")
|
|
89
|
+
if explicit_aud:
|
|
90
|
+
return explicit_aud
|
|
91
|
+
|
|
92
|
+
env = (os.environ.get("PDD_ENV") or "").lower()
|
|
93
|
+
if not env or env == "local":
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
project_id = os.environ.get("PDD_PROJECT_ID") or os.environ.get("GOOGLE_CLOUD_PROJECT")
|
|
97
|
+
if project_id:
|
|
98
|
+
return project_id
|
|
99
|
+
|
|
100
|
+
if env in ("prod", "production"):
|
|
101
|
+
return "prompt-driven-development"
|
|
102
|
+
if env == "staging":
|
|
103
|
+
return os.environ.get("STAGING_PROJECT_ID") or "prompt-driven-development-stg"
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _get_jwt_audience(jwt: str) -> Optional[str]:
|
|
108
|
+
"""Extract the aud claim without verifying the signature."""
|
|
109
|
+
try:
|
|
110
|
+
parts = jwt.split(".")
|
|
111
|
+
if len(parts) < 2:
|
|
112
|
+
return None
|
|
113
|
+
payload_part = parts[1] + "=" * (-len(parts[1]) % 4)
|
|
114
|
+
payload_bytes = base64.urlsafe_b64decode(payload_part.encode("utf-8"))
|
|
115
|
+
payload = json.loads(payload_bytes.decode("utf-8"))
|
|
116
|
+
return payload.get("aud") or payload.get("firebase", {}).get("aud")
|
|
117
|
+
except Exception:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_cached_jwt(verbose: bool = False) -> Optional[str]:
|
|
122
|
+
"""
|
|
123
|
+
Get cached JWT if it exists and is not expired.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
verbose: If True, print helpful messages when cache is invalid
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Optional[str]: The cached JWT if valid, None otherwise.
|
|
130
|
+
"""
|
|
131
|
+
if not JWT_CACHE_FILE.exists():
|
|
132
|
+
return None
|
|
133
|
+
try:
|
|
134
|
+
with open(JWT_CACHE_FILE, 'r') as f:
|
|
135
|
+
cache = json.load(f)
|
|
136
|
+
# Check expiration with 5 minute buffer
|
|
137
|
+
expires_at = cache.get('expires_at', 0)
|
|
138
|
+
current_time = time.time()
|
|
139
|
+
if expires_at > current_time + 300:
|
|
140
|
+
# Check both 'id_token' (new) and 'jwt' (legacy) keys for backwards compatibility
|
|
141
|
+
jwt = cache.get('id_token') or cache.get('jwt')
|
|
142
|
+
expected_aud = _get_expected_jwt_audience()
|
|
143
|
+
if expected_aud:
|
|
144
|
+
actual_aud = _get_jwt_audience(jwt or "")
|
|
145
|
+
if actual_aud != expected_aud:
|
|
146
|
+
if verbose:
|
|
147
|
+
print(f"JWT cache invalidated: audience mismatch")
|
|
148
|
+
print(f" Expected: {expected_aud}")
|
|
149
|
+
print(f" Got: {actual_aud}")
|
|
150
|
+
print(f" This usually means you switched environments (staging vs prod)")
|
|
151
|
+
print(f" Clearing cache and re-authenticating...")
|
|
152
|
+
try:
|
|
153
|
+
JWT_CACHE_FILE.unlink()
|
|
154
|
+
except OSError:
|
|
155
|
+
pass
|
|
156
|
+
return None
|
|
157
|
+
return jwt
|
|
158
|
+
else:
|
|
159
|
+
if verbose:
|
|
160
|
+
time_remaining = expires_at - current_time
|
|
161
|
+
if time_remaining < 0:
|
|
162
|
+
print(f"JWT cache invalidated: token expired {int(-time_remaining / 60)} minutes ago")
|
|
163
|
+
else:
|
|
164
|
+
print(f"JWT cache invalidated: token expires soon (in {int(time_remaining / 60)} minutes)")
|
|
165
|
+
except (json.JSONDecodeError, IOError, KeyError) as e:
|
|
166
|
+
if verbose:
|
|
167
|
+
print(f"JWT cache invalidated: corrupted cache file ({e})")
|
|
168
|
+
# Cache corrupted, delete it
|
|
169
|
+
try:
|
|
170
|
+
JWT_CACHE_FILE.unlink()
|
|
171
|
+
except OSError:
|
|
172
|
+
pass
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _cache_jwt(jwt: str, expires_in: int = 3600) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Cache JWT with expiration time.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
jwt: The JWT token to cache.
|
|
182
|
+
expires_in: Fallback time in seconds if exp claim cannot be extracted (default: 3600 = 1 hour).
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
JWT_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
|
|
187
|
+
# Try to extract actual expiration from JWT's exp claim
|
|
188
|
+
payload = _decode_jwt_payload(jwt)
|
|
189
|
+
exp_claim = payload.get('exp')
|
|
190
|
+
if exp_claim:
|
|
191
|
+
expires_at = exp_claim
|
|
192
|
+
else:
|
|
193
|
+
# Fallback to calculated expiration if exp claim not found
|
|
194
|
+
expires_at = time.time() + expires_in
|
|
195
|
+
|
|
196
|
+
cache = {
|
|
197
|
+
'id_token': jwt, # Use 'id_token' key to match auth.py format
|
|
198
|
+
'expires_at': expires_at,
|
|
199
|
+
'cached_at': time.time()
|
|
200
|
+
}
|
|
201
|
+
with open(JWT_CACHE_FILE, 'w') as f:
|
|
202
|
+
json.dump(cache, f)
|
|
203
|
+
# Secure the file (user read/write only)
|
|
204
|
+
os.chmod(JWT_CACHE_FILE, 0o600)
|
|
205
|
+
except (IOError, OSError) as e:
|
|
206
|
+
# Cache write failed, continue without caching
|
|
207
|
+
print(f"Warning: Failed to cache JWT: {e}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _macos_force_delete_keychain_item(service_name: str, account_name: str) -> bool:
|
|
211
|
+
"""
|
|
212
|
+
Force delete a keychain item using macOS security command.
|
|
213
|
+
|
|
214
|
+
This is a fallback when keyring.delete_password() fails due to ACL issues
|
|
215
|
+
in subprocess contexts (e.g., pytest-xdist workers).
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
service_name: The keychain service name
|
|
219
|
+
account_name: The keychain account name
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
bool: True if deletion succeeded or item didn't exist, False otherwise
|
|
223
|
+
"""
|
|
224
|
+
if sys.platform != 'darwin':
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
result = subprocess.run(
|
|
229
|
+
['security', 'delete-generic-password', '-s', service_name, '-a', account_name],
|
|
230
|
+
capture_output=True, text=True, timeout=10
|
|
231
|
+
)
|
|
232
|
+
# 0 = success, 44 = item not found (also acceptable)
|
|
233
|
+
return result.returncode in (0, 44)
|
|
234
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
|
|
42
238
|
class DeviceFlow:
|
|
43
239
|
"""
|
|
44
240
|
Handles the GitHub Device Flow authentication process.
|
|
@@ -139,15 +335,62 @@ class FirebaseAuthenticator:
|
|
|
139
335
|
self.keyring_service_name = f"firebase-auth-{app_name}"
|
|
140
336
|
self.keyring_user_name = "refresh_token"
|
|
141
337
|
|
|
142
|
-
def _store_refresh_token(self, refresh_token: str):
|
|
143
|
-
"""
|
|
338
|
+
def _store_refresh_token(self, refresh_token: str) -> bool:
|
|
339
|
+
"""
|
|
340
|
+
Stores the Firebase refresh token in the system keyring.
|
|
341
|
+
|
|
342
|
+
Handles the macOS errSecDuplicateItem (-25299) error by attempting
|
|
343
|
+
to force-delete the existing item and retrying.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
refresh_token: The Firebase refresh token to store
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
bool: True if storage succeeded, False otherwise
|
|
350
|
+
"""
|
|
144
351
|
if not KEYRING_AVAILABLE or keyring is None:
|
|
145
352
|
print("Warning: No keyring available, refresh token not stored")
|
|
146
|
-
return
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
max_retries = 2
|
|
356
|
+
|
|
357
|
+
for attempt in range(max_retries):
|
|
358
|
+
try:
|
|
359
|
+
keyring.set_password(
|
|
360
|
+
self.keyring_service_name,
|
|
361
|
+
self.keyring_user_name,
|
|
362
|
+
refresh_token
|
|
363
|
+
)
|
|
364
|
+
return True
|
|
365
|
+
except Exception as e:
|
|
366
|
+
error_str = str(e)
|
|
367
|
+
|
|
368
|
+
# Check for errSecDuplicateItem (-25299) on macOS
|
|
369
|
+
is_duplicate_error = '-25299' in error_str
|
|
370
|
+
|
|
371
|
+
if is_duplicate_error and attempt < max_retries - 1:
|
|
372
|
+
# Try to delete the existing item before retrying
|
|
373
|
+
try:
|
|
374
|
+
keyring.delete_password(
|
|
375
|
+
self.keyring_service_name,
|
|
376
|
+
self.keyring_user_name
|
|
377
|
+
)
|
|
378
|
+
except Exception:
|
|
379
|
+
pass # Ignore delete errors, try force delete
|
|
380
|
+
|
|
381
|
+
# Try macOS-specific force delete
|
|
382
|
+
if sys.platform == 'darwin':
|
|
383
|
+
_macos_force_delete_keychain_item(
|
|
384
|
+
self.keyring_service_name,
|
|
385
|
+
self.keyring_user_name
|
|
386
|
+
)
|
|
387
|
+
continue
|
|
388
|
+
|
|
389
|
+
# Non-duplicate error or final retry failed
|
|
390
|
+
print(f"Warning: Failed to store refresh token in keyring: {e}")
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
return False
|
|
151
394
|
|
|
152
395
|
def _get_stored_refresh_token(self) -> Optional[str]:
|
|
153
396
|
"""Retrieves the Firebase refresh token from the system keyring."""
|
|
@@ -159,21 +402,37 @@ class FirebaseAuthenticator:
|
|
|
159
402
|
print(f"Warning: Failed to retrieve refresh token from keyring: {e}")
|
|
160
403
|
return None
|
|
161
404
|
|
|
162
|
-
def _delete_stored_refresh_token(self):
|
|
163
|
-
"""
|
|
405
|
+
def _delete_stored_refresh_token(self) -> bool:
|
|
406
|
+
"""
|
|
407
|
+
Deletes the stored Firebase refresh token from the keyring.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
bool: True if deletion succeeded or token didn't exist, False otherwise
|
|
411
|
+
"""
|
|
164
412
|
if not KEYRING_AVAILABLE or keyring is None:
|
|
165
413
|
print("No keyring available. Token deletion skipped.")
|
|
166
|
-
return
|
|
414
|
+
return True
|
|
415
|
+
|
|
167
416
|
try:
|
|
168
417
|
keyring.delete_password(self.keyring_service_name, self.keyring_user_name)
|
|
418
|
+
return True
|
|
169
419
|
except Exception as e:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
420
|
+
error_str = str(e)
|
|
421
|
+
|
|
422
|
+
# Check if it's a "not found" error (acceptable)
|
|
423
|
+
if 'PasswordDeleteError' in str(type(e)) or 'not found' in error_str.lower():
|
|
424
|
+
return True
|
|
425
|
+
|
|
426
|
+
# Try macOS force delete as fallback
|
|
427
|
+
if sys.platform == 'darwin':
|
|
428
|
+
if _macos_force_delete_keychain_item(
|
|
429
|
+
self.keyring_service_name,
|
|
430
|
+
self.keyring_user_name
|
|
431
|
+
):
|
|
432
|
+
return True
|
|
433
|
+
|
|
434
|
+
print(f"Warning: Error deleting token from keyring: {e}")
|
|
435
|
+
return False
|
|
177
436
|
|
|
178
437
|
async def _refresh_firebase_token(self, refresh_token: str) -> str:
|
|
179
438
|
"""
|
|
@@ -266,7 +525,12 @@ class FirebaseAuthenticator:
|
|
|
266
525
|
"""
|
|
267
526
|
return bool(id_token)
|
|
268
527
|
|
|
269
|
-
async def get_jwt_token(
|
|
528
|
+
async def get_jwt_token(
|
|
529
|
+
firebase_api_key: str,
|
|
530
|
+
github_client_id: str,
|
|
531
|
+
app_name: str = "my-cli-app",
|
|
532
|
+
no_browser: bool = False
|
|
533
|
+
) -> str:
|
|
270
534
|
"""
|
|
271
535
|
Get a Firebase ID token using GitHub's Device Flow authentication.
|
|
272
536
|
|
|
@@ -274,6 +538,7 @@ async def get_jwt_token(firebase_api_key: str, github_client_id: str, app_name:
|
|
|
274
538
|
firebase_api_key: Firebase Web API key
|
|
275
539
|
github_client_id: OAuth client ID for GitHub app
|
|
276
540
|
app_name: Unique name for your CLI application
|
|
541
|
+
no_browser: If True, skip automatic browser opening (for remote/SSH sessions)
|
|
277
542
|
|
|
278
543
|
Returns:
|
|
279
544
|
str: A valid Firebase ID token
|
|
@@ -283,15 +548,21 @@ async def get_jwt_token(firebase_api_key: str, github_client_id: str, app_name:
|
|
|
283
548
|
NetworkError: If there are connectivity issues
|
|
284
549
|
TokenError: If token exchange fails
|
|
285
550
|
"""
|
|
551
|
+
# Check JWT cache FIRST to avoid keyring access (Issue #273)
|
|
552
|
+
cached_jwt = _get_cached_jwt()
|
|
553
|
+
if cached_jwt:
|
|
554
|
+
return cached_jwt
|
|
555
|
+
|
|
286
556
|
firebase_auth = FirebaseAuthenticator(firebase_api_key, app_name)
|
|
287
557
|
|
|
288
|
-
# Check for existing refresh token
|
|
558
|
+
# Check for existing refresh token in keyring
|
|
289
559
|
refresh_token = firebase_auth._get_stored_refresh_token()
|
|
290
560
|
if refresh_token:
|
|
291
561
|
try:
|
|
292
562
|
# Attempt to refresh the token
|
|
293
563
|
id_token = await firebase_auth._refresh_firebase_token(refresh_token)
|
|
294
564
|
if firebase_auth.verify_firebase_token(id_token):
|
|
565
|
+
_cache_jwt(id_token) # Cache for next time
|
|
295
566
|
return id_token
|
|
296
567
|
else:
|
|
297
568
|
print("Refreshed token is invalid. Attempting re-authentication.")
|
|
@@ -311,7 +582,21 @@ async def get_jwt_token(firebase_api_key: str, github_client_id: str, app_name:
|
|
|
311
582
|
# Display instructions to the user
|
|
312
583
|
print(f"To authenticate, visit: {device_code_response['verification_uri']}")
|
|
313
584
|
print(f"Enter code: {device_code_response['user_code']}")
|
|
585
|
+
sys.stdout.flush() # Ensure visibility in piped contexts
|
|
586
|
+
|
|
587
|
+
# Open browser only if not explicitly disabled
|
|
588
|
+
if not no_browser:
|
|
589
|
+
try:
|
|
590
|
+
webbrowser.open(device_code_response['verification_uri'])
|
|
591
|
+
print("Opening browser for authentication...")
|
|
592
|
+
except Exception as e:
|
|
593
|
+
print(f"Note: Could not open browser: {e}")
|
|
594
|
+
print("Please open the URL manually in your browser.")
|
|
595
|
+
else:
|
|
596
|
+
print("Browser opening disabled. Please open the URL manually in your browser.")
|
|
597
|
+
|
|
314
598
|
print("Waiting for authentication...")
|
|
599
|
+
sys.stdout.flush()
|
|
315
600
|
|
|
316
601
|
# Poll for GitHub token
|
|
317
602
|
github_token = await device_flow.poll_for_token(
|
|
@@ -319,11 +604,15 @@ async def get_jwt_token(firebase_api_key: str, github_client_id: str, app_name:
|
|
|
319
604
|
device_code_response["interval"],
|
|
320
605
|
device_code_response["expires_in"],
|
|
321
606
|
)
|
|
607
|
+
print("Authentication successful!")
|
|
322
608
|
|
|
323
609
|
# Exchange GitHub token for Firebase token
|
|
324
610
|
id_token, refresh_token = await firebase_auth.exchange_github_token_for_firebase_token(github_token)
|
|
325
611
|
|
|
326
|
-
# Store refresh token
|
|
612
|
+
# Store refresh token in keyring
|
|
327
613
|
firebase_auth._store_refresh_token(refresh_token)
|
|
328
614
|
|
|
615
|
+
# Cache JWT for subsequent calls
|
|
616
|
+
_cache_jwt(id_token)
|
|
617
|
+
|
|
329
618
|
return id_token
|
pdd/get_language.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import csv
|
|
2
|
+
from pdd.path_resolution import get_default_resolver
|
|
3
3
|
|
|
4
4
|
def get_language(extension: str) -> str:
|
|
5
5
|
"""
|
|
@@ -14,10 +14,12 @@ def get_language(extension: str) -> str:
|
|
|
14
14
|
Raises:
|
|
15
15
|
ValueError: If PDD_PATH environment variable is not set.
|
|
16
16
|
"""
|
|
17
|
-
# Step 1:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
# Step 1: Resolve CSV path from PDD_PATH
|
|
18
|
+
resolver = get_default_resolver()
|
|
19
|
+
try:
|
|
20
|
+
csv_path = resolver.resolve_data_file("data/language_format.csv")
|
|
21
|
+
except ValueError as exc:
|
|
22
|
+
raise ValueError("PDD_PATH environment variable is not set") from exc
|
|
21
23
|
|
|
22
24
|
# Step 2: Ensure the extension starts with a dot and convert to lowercase
|
|
23
25
|
if not extension.startswith('.'):
|
|
@@ -25,7 +27,6 @@ def get_language(extension: str) -> str:
|
|
|
25
27
|
extension = extension.lower()
|
|
26
28
|
|
|
27
29
|
# Step 3 & 4: Look up the language name and handle exceptions
|
|
28
|
-
csv_path = os.path.join(pdd_path, 'data', 'language_format.csv')
|
|
29
30
|
try:
|
|
30
31
|
with open(csv_path, 'r') as csvfile:
|
|
31
32
|
reader = csv.DictReader(csvfile)
|
|
@@ -38,4 +39,4 @@ def get_language(extension: str) -> str:
|
|
|
38
39
|
except csv.Error as e:
|
|
39
40
|
print(f"Error reading CSV file: {e}")
|
|
40
41
|
|
|
41
|
-
return '' # Return empty string if extension not found or any error occurs
|
|
42
|
+
return '' # Return empty string if extension not found or any error occurs
|
pdd/get_run_command.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import csv
|
|
5
|
+
from pdd.path_resolution import get_default_resolver
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def get_run_command(extension: str) -> str:
|
|
@@ -18,10 +19,12 @@ def get_run_command(extension: str) -> str:
|
|
|
18
19
|
Raises:
|
|
19
20
|
ValueError: If the PDD_PATH environment variable is not set.
|
|
20
21
|
"""
|
|
21
|
-
# Step 1:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
# Step 1: Resolve CSV path from PDD_PATH
|
|
23
|
+
resolver = get_default_resolver()
|
|
24
|
+
try:
|
|
25
|
+
csv_path = resolver.resolve_data_file("data/language_format.csv")
|
|
26
|
+
except ValueError as exc:
|
|
27
|
+
raise ValueError("PDD_PATH environment variable is not set") from exc
|
|
25
28
|
|
|
26
29
|
# Step 2: Ensure the extension starts with a dot and convert to lowercase
|
|
27
30
|
if not extension.startswith('.'):
|
|
@@ -29,7 +32,6 @@ def get_run_command(extension: str) -> str:
|
|
|
29
32
|
extension = extension.lower()
|
|
30
33
|
|
|
31
34
|
# Step 3: Look up the run command
|
|
32
|
-
csv_path = os.path.join(pdd_path, 'data', 'language_format.csv')
|
|
33
35
|
try:
|
|
34
36
|
with open(csv_path, 'r') as csvfile:
|
|
35
37
|
reader = csv.DictReader(csvfile)
|
pdd/insert_includes.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
from typing import Callable, Optional, Tuple
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from rich import print
|
|
@@ -62,7 +63,7 @@ def insert_includes(
|
|
|
62
63
|
except FileNotFoundError:
|
|
63
64
|
if verbose:
|
|
64
65
|
print(f"[yellow]CSV file {csv_filename} not found. Creating empty CSV.[/yellow]")
|
|
65
|
-
csv_content = "full_path,file_summary,
|
|
66
|
+
csv_content = "full_path,file_summary,content_hash\n"
|
|
66
67
|
Path(csv_filename).write_text(csv_content)
|
|
67
68
|
|
|
68
69
|
# Step 3: Preprocess the prompt template
|