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.
Files changed (144) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +521 -786
  7. pdd/agentic_e2e_fix.py +319 -0
  8. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  9. pdd/agentic_fix.py +118 -3
  10. pdd/agentic_update.py +25 -8
  11. pdd/architecture_sync.py +565 -0
  12. pdd/auth_service.py +210 -0
  13. pdd/auto_deps_main.py +63 -53
  14. pdd/auto_include.py +185 -3
  15. pdd/auto_update.py +125 -47
  16. pdd/bug_main.py +195 -23
  17. pdd/cmd_test_main.py +345 -197
  18. pdd/code_generator.py +4 -2
  19. pdd/code_generator_main.py +118 -32
  20. pdd/commands/__init__.py +6 -0
  21. pdd/commands/analysis.py +87 -29
  22. pdd/commands/auth.py +309 -0
  23. pdd/commands/connect.py +290 -0
  24. pdd/commands/fix.py +136 -113
  25. pdd/commands/maintenance.py +3 -2
  26. pdd/commands/misc.py +8 -0
  27. pdd/commands/modify.py +190 -164
  28. pdd/commands/sessions.py +284 -0
  29. pdd/construct_paths.py +334 -32
  30. pdd/context_generator_main.py +167 -170
  31. pdd/continue_generation.py +6 -3
  32. pdd/core/__init__.py +33 -0
  33. pdd/core/cli.py +27 -3
  34. pdd/core/cloud.py +237 -0
  35. pdd/core/errors.py +4 -0
  36. pdd/core/remote_session.py +61 -0
  37. pdd/crash_main.py +219 -23
  38. pdd/data/llm_model.csv +4 -4
  39. pdd/docs/prompting_guide.md +864 -0
  40. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  41. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  42. pdd/fix_code_loop.py +208 -34
  43. pdd/fix_code_module_errors.py +6 -2
  44. pdd/fix_error_loop.py +291 -38
  45. pdd/fix_main.py +204 -4
  46. pdd/fix_verification_errors_loop.py +235 -26
  47. pdd/fix_verification_main.py +269 -83
  48. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  49. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  50. pdd/frontend/dist/index.html +376 -0
  51. pdd/frontend/dist/logo.svg +33 -0
  52. pdd/generate_output_paths.py +46 -5
  53. pdd/generate_test.py +212 -151
  54. pdd/get_comment.py +19 -44
  55. pdd/get_extension.py +8 -9
  56. pdd/get_jwt_token.py +309 -20
  57. pdd/get_language.py +8 -7
  58. pdd/get_run_command.py +7 -5
  59. pdd/insert_includes.py +2 -1
  60. pdd/llm_invoke.py +459 -95
  61. pdd/load_prompt_template.py +15 -34
  62. pdd/path_resolution.py +140 -0
  63. pdd/postprocess.py +4 -1
  64. pdd/preprocess.py +68 -12
  65. pdd/preprocess_main.py +33 -1
  66. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  67. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  68. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  69. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  70. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  71. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  72. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  73. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  74. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  75. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  76. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  77. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  78. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  79. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  80. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  81. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  82. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  83. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  84. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  85. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  86. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  87. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  88. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  89. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  90. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  91. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  92. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  93. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  94. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  95. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  96. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  97. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  98. pdd/prompts/agentic_update_LLM.prompt +192 -338
  99. pdd/prompts/auto_include_LLM.prompt +22 -0
  100. pdd/prompts/change_LLM.prompt +3093 -1
  101. pdd/prompts/detect_change_LLM.prompt +571 -14
  102. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  103. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  104. pdd/prompts/generate_test_LLM.prompt +20 -1
  105. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  106. pdd/prompts/insert_includes_LLM.prompt +262 -252
  107. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  108. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  109. pdd/remote_session.py +876 -0
  110. pdd/server/__init__.py +52 -0
  111. pdd/server/app.py +335 -0
  112. pdd/server/click_executor.py +587 -0
  113. pdd/server/executor.py +338 -0
  114. pdd/server/jobs.py +661 -0
  115. pdd/server/models.py +241 -0
  116. pdd/server/routes/__init__.py +31 -0
  117. pdd/server/routes/architecture.py +451 -0
  118. pdd/server/routes/auth.py +364 -0
  119. pdd/server/routes/commands.py +929 -0
  120. pdd/server/routes/config.py +42 -0
  121. pdd/server/routes/files.py +603 -0
  122. pdd/server/routes/prompts.py +1322 -0
  123. pdd/server/routes/websocket.py +473 -0
  124. pdd/server/security.py +243 -0
  125. pdd/server/terminal_spawner.py +209 -0
  126. pdd/server/token_counter.py +222 -0
  127. pdd/summarize_directory.py +236 -237
  128. pdd/sync_animation.py +8 -4
  129. pdd/sync_determine_operation.py +329 -47
  130. pdd/sync_main.py +272 -28
  131. pdd/sync_orchestration.py +136 -75
  132. pdd/template_expander.py +161 -0
  133. pdd/templates/architecture/architecture_json.prompt +41 -46
  134. pdd/trace.py +1 -1
  135. pdd/track_cost.py +0 -13
  136. pdd/unfinished_prompt.py +2 -1
  137. pdd/update_main.py +23 -5
  138. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
  139. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  140. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  141. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  142. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  143. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
  144. {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
- """Stores the Firebase refresh token in the system keyring."""
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
- try:
148
- keyring.set_password(self.keyring_service_name, self.keyring_user_name, refresh_token)
149
- except Exception as e:
150
- print(f"Warning: Failed to store refresh token in keyring: {e}")
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
- """Deletes the stored Firebase refresh token from the keyring."""
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
- # Handle both keyring.errors and generic exceptions for cross-platform compatibility
171
- if "NoKeyringError" in str(type(e)) or "no keyring" in str(e).lower():
172
- print("No keyring found. Token deletion skipped.")
173
- elif "PasswordDeleteError" in str(type(e)) or "delete" in str(e).lower():
174
- print("Failed to delete token from keyring.")
175
- else:
176
- print(f"Warning: Error deleting token from keyring: {e}")
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(firebase_api_key: str, github_client_id: str, app_name: str = "my-cli-app") -> str:
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: Load environment variable PDD_PATH
18
- pdd_path = os.environ.get('PDD_PATH')
19
- if not pdd_path:
20
- raise ValueError("PDD_PATH environment variable is not set")
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: Load environment variable PDD_PATH
22
- pdd_path = os.environ.get('PDD_PATH')
23
- if not pdd_path:
24
- raise ValueError("PDD_PATH environment variable is not set")
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,date\n"
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