pdd-cli 0.0.90__py3-none-any.whl → 0.0.121__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 (151) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +506 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +537 -0
  6. pdd/agentic_common.py +533 -770
  7. pdd/agentic_crash.py +2 -1
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +582 -0
  10. pdd/agentic_fix.py +118 -3
  11. pdd/agentic_update.py +27 -9
  12. pdd/agentic_verify.py +3 -2
  13. pdd/architecture_sync.py +565 -0
  14. pdd/auth_service.py +210 -0
  15. pdd/auto_deps_main.py +63 -53
  16. pdd/auto_include.py +236 -3
  17. pdd/auto_update.py +125 -47
  18. pdd/bug_main.py +195 -23
  19. pdd/cmd_test_main.py +345 -197
  20. pdd/code_generator.py +4 -2
  21. pdd/code_generator_main.py +118 -32
  22. pdd/commands/__init__.py +6 -0
  23. pdd/commands/analysis.py +113 -48
  24. pdd/commands/auth.py +309 -0
  25. pdd/commands/connect.py +358 -0
  26. pdd/commands/fix.py +155 -114
  27. pdd/commands/generate.py +5 -0
  28. pdd/commands/maintenance.py +3 -2
  29. pdd/commands/misc.py +8 -0
  30. pdd/commands/modify.py +225 -163
  31. pdd/commands/sessions.py +284 -0
  32. pdd/commands/utility.py +12 -7
  33. pdd/construct_paths.py +334 -32
  34. pdd/context_generator_main.py +167 -170
  35. pdd/continue_generation.py +6 -3
  36. pdd/core/__init__.py +33 -0
  37. pdd/core/cli.py +44 -7
  38. pdd/core/cloud.py +237 -0
  39. pdd/core/dump.py +68 -20
  40. pdd/core/errors.py +4 -0
  41. pdd/core/remote_session.py +61 -0
  42. pdd/crash_main.py +219 -23
  43. pdd/data/llm_model.csv +4 -4
  44. pdd/docs/prompting_guide.md +864 -0
  45. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  46. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  47. pdd/fix_code_loop.py +208 -34
  48. pdd/fix_code_module_errors.py +6 -2
  49. pdd/fix_error_loop.py +291 -38
  50. pdd/fix_main.py +208 -6
  51. pdd/fix_verification_errors_loop.py +235 -26
  52. pdd/fix_verification_main.py +269 -83
  53. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  54. pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
  55. pdd/frontend/dist/index.html +376 -0
  56. pdd/frontend/dist/logo.svg +33 -0
  57. pdd/generate_output_paths.py +46 -5
  58. pdd/generate_test.py +212 -151
  59. pdd/get_comment.py +19 -44
  60. pdd/get_extension.py +8 -9
  61. pdd/get_jwt_token.py +309 -20
  62. pdd/get_language.py +8 -7
  63. pdd/get_run_command.py +7 -5
  64. pdd/insert_includes.py +2 -1
  65. pdd/llm_invoke.py +531 -97
  66. pdd/load_prompt_template.py +15 -34
  67. pdd/operation_log.py +342 -0
  68. pdd/path_resolution.py +140 -0
  69. pdd/postprocess.py +122 -97
  70. pdd/preprocess.py +68 -12
  71. pdd/preprocess_main.py +33 -1
  72. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  73. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  74. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  75. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  76. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  77. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  78. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  79. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  80. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  81. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  82. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  83. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  84. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +140 -0
  85. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  86. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  87. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  88. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  89. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  90. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  91. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  92. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  93. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  94. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  95. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  96. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  97. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  98. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  99. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  100. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  101. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  102. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  103. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  104. pdd/prompts/agentic_update_LLM.prompt +192 -338
  105. pdd/prompts/auto_include_LLM.prompt +22 -0
  106. pdd/prompts/change_LLM.prompt +3093 -1
  107. pdd/prompts/detect_change_LLM.prompt +571 -14
  108. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  109. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  110. pdd/prompts/generate_test_LLM.prompt +19 -1
  111. pdd/prompts/generate_test_from_example_LLM.prompt +366 -0
  112. pdd/prompts/insert_includes_LLM.prompt +262 -252
  113. pdd/prompts/prompt_code_diff_LLM.prompt +123 -0
  114. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  115. pdd/remote_session.py +876 -0
  116. pdd/server/__init__.py +52 -0
  117. pdd/server/app.py +335 -0
  118. pdd/server/click_executor.py +587 -0
  119. pdd/server/executor.py +338 -0
  120. pdd/server/jobs.py +661 -0
  121. pdd/server/models.py +241 -0
  122. pdd/server/routes/__init__.py +31 -0
  123. pdd/server/routes/architecture.py +451 -0
  124. pdd/server/routes/auth.py +364 -0
  125. pdd/server/routes/commands.py +929 -0
  126. pdd/server/routes/config.py +42 -0
  127. pdd/server/routes/files.py +603 -0
  128. pdd/server/routes/prompts.py +1347 -0
  129. pdd/server/routes/websocket.py +473 -0
  130. pdd/server/security.py +243 -0
  131. pdd/server/terminal_spawner.py +217 -0
  132. pdd/server/token_counter.py +222 -0
  133. pdd/summarize_directory.py +236 -237
  134. pdd/sync_animation.py +8 -4
  135. pdd/sync_determine_operation.py +329 -47
  136. pdd/sync_main.py +272 -28
  137. pdd/sync_orchestration.py +289 -211
  138. pdd/sync_order.py +304 -0
  139. pdd/template_expander.py +161 -0
  140. pdd/templates/architecture/architecture_json.prompt +41 -46
  141. pdd/trace.py +1 -1
  142. pdd/track_cost.py +0 -13
  143. pdd/unfinished_prompt.py +2 -1
  144. pdd/update_main.py +68 -26
  145. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +15 -10
  146. pdd_cli-0.0.121.dist-info/RECORD +229 -0
  147. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  148. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
  149. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
  150. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
  151. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,364 @@
1
+ """Authentication routes for PDD Cloud.
2
+
3
+ Provides endpoints to check authentication status, force re-authentication,
4
+ and trigger GitHub Device Flow login directly from the web UI.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import time
10
+ import uuid
11
+ import webbrowser
12
+ from typing import Dict, Optional
13
+
14
+ from fastapi import APIRouter, BackgroundTasks
15
+ from pydantic import BaseModel
16
+
17
+ from pdd.auth_service import (
18
+ get_jwt_cache_info as _get_jwt_cache_info,
19
+ has_refresh_token as _has_refresh_token,
20
+ clear_jwt_cache as _clear_jwt_cache,
21
+ clear_refresh_token as _clear_refresh_token,
22
+ get_cached_jwt as _get_cached_jwt,
23
+ )
24
+
25
+ router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
26
+
27
+ # Environment variable names (same as cloud.py)
28
+ FIREBASE_API_KEY_ENV = "NEXT_PUBLIC_FIREBASE_API_KEY"
29
+ GITHUB_CLIENT_ID_ENV = "GITHUB_CLIENT_ID"
30
+
31
+ # Active login sessions (poll_id -> session state)
32
+ _active_sessions: Dict[str, dict] = {}
33
+
34
+
35
+ class AuthStatus(BaseModel):
36
+ """Response model for authentication status."""
37
+
38
+ authenticated: bool
39
+ cached: bool
40
+ expires_at: Optional[float] = None
41
+
42
+
43
+ class LogoutResult(BaseModel):
44
+ """Response model for logout operation."""
45
+
46
+ success: bool
47
+ message: str
48
+
49
+
50
+ class LoginRequest(BaseModel):
51
+ """Request model for starting login flow."""
52
+
53
+ no_browser: bool = False
54
+
55
+
56
+ class LoginResponse(BaseModel):
57
+ """Response model for starting login flow."""
58
+
59
+ success: bool
60
+ user_code: Optional[str] = None
61
+ verification_uri: Optional[str] = None
62
+ expires_in: Optional[int] = None
63
+ poll_id: Optional[str] = None
64
+ error: Optional[str] = None
65
+
66
+
67
+ class LoginPollResponse(BaseModel):
68
+ """Response model for polling login status."""
69
+
70
+ status: str # "pending", "completed", "expired", "error"
71
+ message: Optional[str] = None
72
+
73
+
74
+ @router.get("/status", response_model=AuthStatus)
75
+ async def get_auth_status() -> AuthStatus:
76
+ """
77
+ Check current authentication status.
78
+
79
+ Returns whether the user is authenticated (has valid cached JWT or refresh token).
80
+ """
81
+ # First check JWT cache
82
+ cache_valid, expires_at = _get_jwt_cache_info()
83
+ if cache_valid:
84
+ return AuthStatus(authenticated=True, cached=True, expires_at=expires_at)
85
+
86
+ # Check for refresh token in keyring
87
+ has_refresh = _has_refresh_token()
88
+ if has_refresh:
89
+ return AuthStatus(authenticated=True, cached=False, expires_at=None)
90
+
91
+ return AuthStatus(authenticated=False, cached=False, expires_at=None)
92
+
93
+
94
+ class JWTTokenResponse(BaseModel):
95
+ """Response model for JWT token."""
96
+
97
+ jwt: Optional[str] = None
98
+
99
+
100
+ @router.get("/jwt-token", response_model=JWTTokenResponse)
101
+ async def get_jwt_token() -> JWTTokenResponse:
102
+ """
103
+ Get the current JWT token from cache.
104
+
105
+ Returns the cached JWT token if valid, otherwise returns null.
106
+ Used by the frontend to authenticate with cloud services.
107
+ """
108
+ token = _get_cached_jwt()
109
+ return JWTTokenResponse(jwt=token)
110
+
111
+
112
+ @router.post("/logout", response_model=LogoutResult)
113
+ async def logout() -> LogoutResult:
114
+ """
115
+ Clear all authentication tokens to force fresh GitHub login.
116
+
117
+ Clears both the JWT cache file and the refresh token from keyring.
118
+ After calling this, the next pdd command will trigger the GitHub Device Flow.
119
+ """
120
+ errors = []
121
+
122
+ # Clear JWT cache
123
+ jwt_success, jwt_error = _clear_jwt_cache()
124
+ if not jwt_success and jwt_error:
125
+ errors.append(jwt_error)
126
+
127
+ # Clear refresh token from keyring
128
+ refresh_success, refresh_error = _clear_refresh_token()
129
+ if not refresh_success and refresh_error:
130
+ errors.append(refresh_error)
131
+
132
+ if errors:
133
+ return LogoutResult(success=False, message="; ".join(errors))
134
+
135
+ return LogoutResult(
136
+ success=True,
137
+ message="Tokens cleared successfully.",
138
+ )
139
+
140
+
141
+ async def _poll_for_auth(poll_id: str, device_code: str, interval: int, expires_in: int) -> None:
142
+ """
143
+ Background task that polls GitHub for authentication completion.
144
+ Updates the session state when auth completes or expires.
145
+ """
146
+ from pdd.get_jwt_token import (
147
+ DeviceFlow,
148
+ FirebaseAuthenticator,
149
+ AuthError,
150
+ NetworkError,
151
+ TokenError,
152
+ UserCancelledError,
153
+ _cache_jwt,
154
+ )
155
+
156
+ github_client_id = os.environ.get(GITHUB_CLIENT_ID_ENV)
157
+ firebase_api_key = os.environ.get(FIREBASE_API_KEY_ENV)
158
+
159
+ if not github_client_id or not firebase_api_key:
160
+ _active_sessions[poll_id]["status"] = "error"
161
+ _active_sessions[poll_id]["message"] = "Missing API credentials"
162
+ return
163
+
164
+ device_flow = DeviceFlow(github_client_id)
165
+ firebase_auth = FirebaseAuthenticator(firebase_api_key, "pdd")
166
+
167
+ try:
168
+ # Poll for GitHub token
169
+ github_token = await device_flow.poll_for_token(device_code, interval, expires_in)
170
+
171
+ # Exchange for Firebase token
172
+ id_token, refresh_token = await firebase_auth.exchange_github_token_for_firebase_token(
173
+ github_token
174
+ )
175
+
176
+ # Store tokens
177
+ firebase_auth._store_refresh_token(refresh_token)
178
+ _cache_jwt(id_token)
179
+
180
+ _active_sessions[poll_id]["status"] = "completed"
181
+ _active_sessions[poll_id]["message"] = "Authentication successful!"
182
+
183
+ except UserCancelledError:
184
+ _active_sessions[poll_id]["status"] = "error"
185
+ _active_sessions[poll_id]["message"] = "User denied access on GitHub"
186
+ except AuthError as e:
187
+ if "expired" in str(e).lower() or "timed out" in str(e).lower():
188
+ _active_sessions[poll_id]["status"] = "expired"
189
+ _active_sessions[poll_id]["message"] = "Authentication timed out. Please try again."
190
+ else:
191
+ _active_sessions[poll_id]["status"] = "error"
192
+ _active_sessions[poll_id]["message"] = str(e)
193
+ except (NetworkError, TokenError) as e:
194
+ _active_sessions[poll_id]["status"] = "error"
195
+ _active_sessions[poll_id]["message"] = str(e)
196
+ except Exception as e:
197
+ _active_sessions[poll_id]["status"] = "error"
198
+ _active_sessions[poll_id]["message"] = f"Unexpected error: {e}"
199
+
200
+
201
+ @router.post("/login", response_model=LoginResponse)
202
+ async def start_login(
203
+ background_tasks: BackgroundTasks,
204
+ request: LoginRequest = LoginRequest()
205
+ ) -> LoginResponse:
206
+ """
207
+ Start GitHub Device Flow authentication.
208
+
209
+ Clears existing tokens and initiates a new GitHub Device Flow.
210
+ Returns the user code and verification URL for the user to complete authentication.
211
+ Opens the browser automatically unless no_browser is True.
212
+ """
213
+ # Check for required environment variables
214
+ github_client_id = os.environ.get(GITHUB_CLIENT_ID_ENV)
215
+ firebase_api_key = os.environ.get(FIREBASE_API_KEY_ENV)
216
+
217
+ if not github_client_id:
218
+ return LoginResponse(
219
+ success=False,
220
+ error=f"Environment variable {GITHUB_CLIENT_ID_ENV} not set. Cloud authentication not available.",
221
+ )
222
+ if not firebase_api_key:
223
+ return LoginResponse(
224
+ success=False,
225
+ error=f"Environment variable {FIREBASE_API_KEY_ENV} not set. Cloud authentication not available.",
226
+ )
227
+
228
+ # Clear existing tokens first
229
+ _clear_jwt_cache()
230
+ _clear_refresh_token()
231
+
232
+ # Import DeviceFlow and exceptions
233
+ from pdd.get_jwt_token import DeviceFlow, AuthError, NetworkError
234
+
235
+ try:
236
+ device_flow = DeviceFlow(github_client_id)
237
+ device_code_response = await device_flow.request_device_code()
238
+
239
+ # Generate poll ID and store session
240
+ poll_id = str(uuid.uuid4())
241
+ _active_sessions[poll_id] = {
242
+ "status": "pending",
243
+ "message": "Waiting for user to authenticate on GitHub...",
244
+ "created_at": time.time(),
245
+ }
246
+
247
+ # Open browser for user (unless disabled)
248
+ verification_uri = device_code_response["verification_uri"]
249
+
250
+ if not request.no_browser:
251
+ try:
252
+ webbrowser.open(verification_uri)
253
+ except Exception as e:
254
+ # Log error but don't fail - user can still open manually
255
+ import logging
256
+ logging.warning(f"Failed to open browser: {e}")
257
+
258
+ # Start background polling task
259
+ background_tasks.add_task(
260
+ _poll_for_auth,
261
+ poll_id,
262
+ device_code_response["device_code"],
263
+ device_code_response["interval"],
264
+ device_code_response["expires_in"],
265
+ )
266
+
267
+ return LoginResponse(
268
+ success=True,
269
+ user_code=device_code_response["user_code"],
270
+ verification_uri=verification_uri,
271
+ expires_in=device_code_response["expires_in"],
272
+ poll_id=poll_id,
273
+ )
274
+
275
+ except (AuthError, NetworkError) as e:
276
+ return LoginResponse(success=False, error=str(e))
277
+ except Exception as e:
278
+ return LoginResponse(success=False, error=f"Failed to start authentication: {e}")
279
+
280
+
281
+ @router.get("/login/poll/{poll_id}", response_model=LoginPollResponse)
282
+ async def poll_login_status(poll_id: str) -> LoginPollResponse:
283
+ """
284
+ Poll for login completion status.
285
+
286
+ Returns the current status of the authentication flow.
287
+ """
288
+ if poll_id not in _active_sessions:
289
+ return LoginPollResponse(status="error", message="Invalid or expired session")
290
+
291
+ session = _active_sessions[poll_id]
292
+
293
+ # Clean up completed/expired sessions after returning status
294
+ if session["status"] in ("completed", "expired", "error"):
295
+ # Keep for a short time so client can get final status
296
+ if time.time() - session.get("created_at", 0) > 60:
297
+ del _active_sessions[poll_id]
298
+
299
+ return LoginPollResponse(status=session["status"], message=session.get("message"))
300
+
301
+
302
+ class CloudConnectionTestResponse(BaseModel):
303
+ """Response model for cloud connection test."""
304
+
305
+ connected: bool
306
+ session_count: Optional[int] = None
307
+ error: Optional[str] = None
308
+ cloud_url: str
309
+ environment: str
310
+
311
+
312
+ @router.get("/test-cloud-connection", response_model=CloudConnectionTestResponse)
313
+ async def test_cloud_connection() -> CloudConnectionTestResponse:
314
+ """
315
+ Test JWT token validity by calling cloud's /listSessions endpoint.
316
+
317
+ This helps diagnose connectivity issues and validates that:
318
+ 1. JWT token is present and valid
319
+ 2. Cloud URL is accessible
320
+ 3. Token has correct permissions
321
+
322
+ Returns:
323
+ CloudConnectionTestResponse with connection status and session count
324
+ """
325
+ from pdd.core.cloud import CloudConfig
326
+ from pdd.remote_session import RemoteSessionManager
327
+ import os
328
+
329
+ cloud_url = CloudConfig.get_base_url()
330
+ environment = os.environ.get("PDD_ENV", "production")
331
+
332
+ # Get JWT token
333
+ jwt_token = _get_cached_jwt()
334
+ if not jwt_token:
335
+ return CloudConnectionTestResponse(
336
+ connected=False,
337
+ error="No JWT token found. Please authenticate with 'pdd auth login'.",
338
+ cloud_url=cloud_url,
339
+ environment=environment
340
+ )
341
+
342
+ # Try to list sessions
343
+ try:
344
+ sessions = await RemoteSessionManager.list_sessions(jwt_token)
345
+ return CloudConnectionTestResponse(
346
+ connected=True,
347
+ session_count=len(sessions),
348
+ cloud_url=cloud_url,
349
+ environment=environment
350
+ )
351
+ except Exception as e:
352
+ error_msg = str(e)
353
+ # Parse common error types
354
+ if "401" in error_msg or "403" in error_msg or "Unauthorized" in error_msg:
355
+ error_msg = f"Authentication failed: {error_msg}. Token may be expired or invalid."
356
+ elif "timeout" in error_msg.lower() or "connection" in error_msg.lower():
357
+ error_msg = f"Network error: {error_msg}. Cloud may be unreachable."
358
+
359
+ return CloudConnectionTestResponse(
360
+ connected=False,
361
+ error=error_msg,
362
+ cloud_url=cloud_url,
363
+ environment=environment
364
+ )