pdd-cli 0.0.45__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 (195) hide show
  1. pdd/__init__.py +40 -8
  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 +598 -0
  7. pdd/agentic_crash.py +534 -0
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  10. pdd/agentic_fix.py +1294 -0
  11. pdd/agentic_langtest.py +162 -0
  12. pdd/agentic_update.py +387 -0
  13. pdd/agentic_verify.py +183 -0
  14. pdd/architecture_sync.py +565 -0
  15. pdd/auth_service.py +210 -0
  16. pdd/auto_deps_main.py +71 -51
  17. pdd/auto_include.py +245 -5
  18. pdd/auto_update.py +125 -47
  19. pdd/bug_main.py +196 -23
  20. pdd/bug_to_unit_test.py +2 -0
  21. pdd/change_main.py +11 -4
  22. pdd/cli.py +22 -1181
  23. pdd/cmd_test_main.py +350 -150
  24. pdd/code_generator.py +60 -18
  25. pdd/code_generator_main.py +790 -57
  26. pdd/commands/__init__.py +48 -0
  27. pdd/commands/analysis.py +306 -0
  28. pdd/commands/auth.py +309 -0
  29. pdd/commands/connect.py +290 -0
  30. pdd/commands/fix.py +163 -0
  31. pdd/commands/generate.py +257 -0
  32. pdd/commands/maintenance.py +175 -0
  33. pdd/commands/misc.py +87 -0
  34. pdd/commands/modify.py +256 -0
  35. pdd/commands/report.py +144 -0
  36. pdd/commands/sessions.py +284 -0
  37. pdd/commands/templates.py +215 -0
  38. pdd/commands/utility.py +110 -0
  39. pdd/config_resolution.py +58 -0
  40. pdd/conflicts_main.py +8 -3
  41. pdd/construct_paths.py +589 -111
  42. pdd/context_generator.py +10 -2
  43. pdd/context_generator_main.py +175 -76
  44. pdd/continue_generation.py +53 -10
  45. pdd/core/__init__.py +33 -0
  46. pdd/core/cli.py +527 -0
  47. pdd/core/cloud.py +237 -0
  48. pdd/core/dump.py +554 -0
  49. pdd/core/errors.py +67 -0
  50. pdd/core/remote_session.py +61 -0
  51. pdd/core/utils.py +90 -0
  52. pdd/crash_main.py +262 -33
  53. pdd/data/language_format.csv +71 -63
  54. pdd/data/llm_model.csv +20 -18
  55. pdd/detect_change_main.py +5 -4
  56. pdd/docs/prompting_guide.md +864 -0
  57. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  58. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  59. pdd/fix_code_loop.py +523 -95
  60. pdd/fix_code_module_errors.py +6 -2
  61. pdd/fix_error_loop.py +491 -92
  62. pdd/fix_errors_from_unit_tests.py +4 -3
  63. pdd/fix_main.py +278 -21
  64. pdd/fix_verification_errors.py +12 -100
  65. pdd/fix_verification_errors_loop.py +529 -286
  66. pdd/fix_verification_main.py +294 -89
  67. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  68. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  69. pdd/frontend/dist/index.html +376 -0
  70. pdd/frontend/dist/logo.svg +33 -0
  71. pdd/generate_output_paths.py +139 -15
  72. pdd/generate_test.py +218 -146
  73. pdd/get_comment.py +19 -44
  74. pdd/get_extension.py +8 -9
  75. pdd/get_jwt_token.py +318 -22
  76. pdd/get_language.py +8 -7
  77. pdd/get_run_command.py +75 -0
  78. pdd/get_test_command.py +68 -0
  79. pdd/git_update.py +70 -19
  80. pdd/incremental_code_generator.py +2 -2
  81. pdd/insert_includes.py +13 -4
  82. pdd/llm_invoke.py +1711 -181
  83. pdd/load_prompt_template.py +19 -12
  84. pdd/path_resolution.py +140 -0
  85. pdd/pdd_completion.fish +25 -2
  86. pdd/pdd_completion.sh +30 -4
  87. pdd/pdd_completion.zsh +79 -4
  88. pdd/postprocess.py +14 -4
  89. pdd/preprocess.py +293 -24
  90. pdd/preprocess_main.py +41 -6
  91. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  92. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  93. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  94. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  95. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  96. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  97. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  98. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  99. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  100. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  101. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  102. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  103. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  104. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  105. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  106. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  107. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  108. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  109. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  110. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  111. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  112. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  113. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  114. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  115. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  116. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  117. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  118. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  119. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  120. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  121. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  122. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  123. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  124. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  125. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  126. pdd/prompts/agentic_update_LLM.prompt +925 -0
  127. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  128. pdd/prompts/auto_include_LLM.prompt +122 -905
  129. pdd/prompts/change_LLM.prompt +3093 -1
  130. pdd/prompts/detect_change_LLM.prompt +686 -27
  131. pdd/prompts/example_generator_LLM.prompt +22 -1
  132. pdd/prompts/extract_code_LLM.prompt +5 -1
  133. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  134. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  135. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  136. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  137. pdd/prompts/fix_code_module_errors_LLM.prompt +12 -2
  138. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
  139. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  140. pdd/prompts/generate_test_LLM.prompt +41 -7
  141. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  142. pdd/prompts/increase_tests_LLM.prompt +1 -5
  143. pdd/prompts/insert_includes_LLM.prompt +316 -186
  144. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  145. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  146. pdd/prompts/trace_LLM.prompt +25 -22
  147. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  148. pdd/prompts/update_prompt_LLM.prompt +22 -1
  149. pdd/pytest_output.py +127 -12
  150. pdd/remote_session.py +876 -0
  151. pdd/render_mermaid.py +236 -0
  152. pdd/server/__init__.py +52 -0
  153. pdd/server/app.py +335 -0
  154. pdd/server/click_executor.py +587 -0
  155. pdd/server/executor.py +338 -0
  156. pdd/server/jobs.py +661 -0
  157. pdd/server/models.py +241 -0
  158. pdd/server/routes/__init__.py +31 -0
  159. pdd/server/routes/architecture.py +451 -0
  160. pdd/server/routes/auth.py +364 -0
  161. pdd/server/routes/commands.py +929 -0
  162. pdd/server/routes/config.py +42 -0
  163. pdd/server/routes/files.py +603 -0
  164. pdd/server/routes/prompts.py +1322 -0
  165. pdd/server/routes/websocket.py +473 -0
  166. pdd/server/security.py +243 -0
  167. pdd/server/terminal_spawner.py +209 -0
  168. pdd/server/token_counter.py +222 -0
  169. pdd/setup_tool.py +648 -0
  170. pdd/simple_math.py +2 -0
  171. pdd/split_main.py +3 -2
  172. pdd/summarize_directory.py +237 -195
  173. pdd/sync_animation.py +8 -4
  174. pdd/sync_determine_operation.py +839 -112
  175. pdd/sync_main.py +351 -57
  176. pdd/sync_orchestration.py +1400 -756
  177. pdd/sync_tui.py +848 -0
  178. pdd/template_expander.py +161 -0
  179. pdd/template_registry.py +264 -0
  180. pdd/templates/architecture/architecture_json.prompt +237 -0
  181. pdd/templates/generic/generate_prompt.prompt +174 -0
  182. pdd/trace.py +168 -12
  183. pdd/trace_main.py +4 -3
  184. pdd/track_cost.py +140 -63
  185. pdd/unfinished_prompt.py +51 -4
  186. pdd/update_main.py +567 -67
  187. pdd/update_model_costs.py +2 -2
  188. pdd/update_prompt.py +19 -4
  189. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
  190. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  191. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
  192. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  193. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  194. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  195. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,48 @@
1
+ """
2
+ Command registration module.
3
+ """
4
+ import click
5
+
6
+ from .generate import generate, test, example
7
+ from .fix import fix
8
+ from .modify import split, change, update
9
+ from .maintenance import sync, auto_deps, setup
10
+ from .analysis import detect_change, conflicts, bug, crash, trace
11
+ from .connect import connect
12
+ from .auth import auth_group
13
+ from .misc import preprocess
14
+ from .sessions import sessions
15
+ from .report import report_core
16
+ from .templates import templates_group
17
+ from .utility import install_completion_cmd, verify
18
+
19
+ def register_commands(cli: click.Group) -> None:
20
+ """Register all subcommands with the main CLI group."""
21
+ cli.add_command(generate)
22
+ cli.add_command(test)
23
+ cli.add_command(example)
24
+ cli.add_command(fix)
25
+ cli.add_command(split)
26
+ cli.add_command(change)
27
+ cli.add_command(update)
28
+ cli.add_command(sync)
29
+ cli.add_command(auto_deps)
30
+ cli.add_command(setup)
31
+ cli.add_command(detect_change)
32
+ cli.add_command(conflicts)
33
+ cli.add_command(bug)
34
+ cli.add_command(crash)
35
+ cli.add_command(trace)
36
+ cli.add_command(preprocess)
37
+ cli.add_command(report_core)
38
+ cli.add_command(install_completion_cmd, name="install_completion")
39
+ cli.add_command(verify)
40
+
41
+ # Register templates group directly to commands dict to handle nesting if needed,
42
+ # or just add_command works for groups too.
43
+ # The original code did: cli.commands["templates"] = templates_group
44
+ # Using add_command is cleaner if it works for the structure.
45
+ cli.add_command(templates_group)
46
+ cli.add_command(connect)
47
+ cli.add_command(auth_group)
48
+ cli.add_command(sessions)
@@ -0,0 +1,306 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Analysis commands (detect-change, conflicts, bug, crash, trace).
5
+ """
6
+ import os
7
+ import click
8
+ from typing import Optional, Tuple, List
9
+
10
+ from ..detect_change_main import detect_change_main
11
+ from ..conflicts_main import conflicts_main
12
+ from ..bug_main import bug_main
13
+ from ..agentic_bug import run_agentic_bug
14
+ from ..crash_main import crash_main
15
+ from ..trace_main import trace_main
16
+ from ..track_cost import track_cost
17
+ from ..core.errors import handle_error
18
+
19
+ @click.command("detect")
20
+ @click.argument("files", nargs=-1, type=click.Path(exists=True, dir_okay=False))
21
+ @click.option(
22
+ "--output",
23
+ type=click.Path(writable=True),
24
+ default=None,
25
+ help="Specify where to save the analysis results (CSV file).",
26
+ )
27
+ @click.pass_context
28
+ @track_cost
29
+ def detect_change(
30
+ ctx: click.Context,
31
+ files: Tuple[str, ...],
32
+ output: Optional[str],
33
+ ) -> Optional[Tuple[List, float, str]]:
34
+ """Detect if prompts need to be changed based on a description.
35
+
36
+ Usage: pdd detect [PROMPT_FILES...] CHANGE_FILE
37
+ """
38
+ try:
39
+ if len(files) < 2:
40
+ raise click.UsageError("Requires at least one PROMPT_FILE and one CHANGE_FILE.")
41
+
42
+ # According to usage conventions (and README), the last file is the change file
43
+ change_file = files[-1]
44
+ prompt_files = list(files[:-1])
45
+
46
+ result, total_cost, model_name = detect_change_main(
47
+ ctx=ctx,
48
+ prompt_files=prompt_files,
49
+ change_file=change_file,
50
+ output=output,
51
+ )
52
+ return result, total_cost, model_name
53
+ except (click.Abort, click.ClickException):
54
+ raise
55
+ except Exception as exception:
56
+ handle_error(exception, "detect", ctx.obj.get("quiet", False))
57
+ return None
58
+
59
+
60
+ @click.command("conflicts")
61
+ @click.argument("prompt1", type=click.Path(exists=True, dir_okay=False))
62
+ @click.argument("prompt2", type=click.Path(exists=True, dir_okay=False))
63
+ @click.option(
64
+ "--output",
65
+ type=click.Path(writable=True),
66
+ default=None,
67
+ help="Specify where to save the conflict analysis results (CSV file).",
68
+ )
69
+ @click.pass_context
70
+ @track_cost
71
+ def conflicts(
72
+ ctx: click.Context,
73
+ prompt1: str,
74
+ prompt2: str,
75
+ output: Optional[str],
76
+ ) -> Optional[Tuple[List, float, str]]:
77
+ """Check for conflicts between two prompt files."""
78
+ try:
79
+ result, total_cost, model_name = conflicts_main(
80
+ ctx=ctx,
81
+ prompt1=prompt1,
82
+ prompt2=prompt2,
83
+ output=output,
84
+ verbose=ctx.obj.get("verbose", False),
85
+ )
86
+ return result, total_cost, model_name
87
+ except (click.Abort, click.ClickException):
88
+ raise
89
+ except Exception as exception:
90
+ handle_error(exception, "conflicts", ctx.obj.get("quiet", False))
91
+ return None
92
+
93
+
94
+ @click.command("bug")
95
+ @click.option(
96
+ "--manual",
97
+ is_flag=True,
98
+ default=False,
99
+ help="Run in manual mode requiring 5 positional file arguments.",
100
+ )
101
+ @click.argument("args", nargs=-1)
102
+ @click.option(
103
+ "--output",
104
+ type=click.Path(writable=True),
105
+ default=None,
106
+ help="Specify where to save the generated unit test (file or directory).",
107
+ )
108
+ @click.option(
109
+ "--language",
110
+ type=str,
111
+ default="Python",
112
+ help="Programming language for the unit test (Manual mode only).",
113
+ )
114
+ @click.option(
115
+ "--timeout-adder",
116
+ type=float,
117
+ default=0.0,
118
+ help="Additional seconds to add to each step's timeout (agentic mode only).",
119
+ )
120
+ @click.option(
121
+ "--no-github-state",
122
+ is_flag=True,
123
+ default=False,
124
+ help="Disable GitHub state persistence (agentic mode only).",
125
+ )
126
+ @click.pass_context
127
+ @track_cost
128
+ def bug(
129
+ ctx: click.Context,
130
+ manual: bool,
131
+ args: Tuple[str, ...],
132
+ output: Optional[str],
133
+ language: str,
134
+ timeout_adder: float,
135
+ no_github_state: bool,
136
+ ) -> Optional[Tuple[str, float, str]]:
137
+ """Generate a unit test (manual) or investigate a bug (agentic).
138
+
139
+ Agentic Mode (default):
140
+ pdd bug ISSUE_URL
141
+
142
+ Manual Mode:
143
+ pdd bug --manual PROMPT_FILE CODE_FILE PROGRAM_FILE CURRENT_OUTPUT DESIRED_OUTPUT
144
+ """
145
+ try:
146
+ if manual:
147
+ if len(args) != 5:
148
+ raise click.UsageError(
149
+ "Manual mode requires 5 arguments: PROMPT_FILE CODE_FILE PROGRAM_FILE CURRENT_OUTPUT DESIRED_OUTPUT"
150
+ )
151
+
152
+ # Validate files exist (replicating click.Path(exists=True))
153
+ for f in args:
154
+ if not os.path.exists(f):
155
+ raise click.UsageError(f"File does not exist: {f}")
156
+ if os.path.isdir(f):
157
+ raise click.UsageError(f"Path is a directory, not a file: {f}")
158
+
159
+ prompt_file, code_file, program_file, current_output, desired_output = args
160
+
161
+ result, total_cost, model_name = bug_main(
162
+ ctx=ctx,
163
+ prompt_file=prompt_file,
164
+ code_file=code_file,
165
+ program_file=program_file,
166
+ current_output=current_output,
167
+ desired_output=desired_output,
168
+ output=output,
169
+ language=language,
170
+ )
171
+ return result, total_cost, model_name
172
+
173
+ else:
174
+ # Agentic mode
175
+ if len(args) != 1:
176
+ raise click.UsageError("Agentic mode requires exactly one argument: the GitHub Issue URL.")
177
+
178
+ issue_url = args[0]
179
+
180
+ success, message, cost, model, changed_files = run_agentic_bug(
181
+ issue_url=issue_url,
182
+ verbose=ctx.obj.get("verbose", False),
183
+ quiet=ctx.obj.get("quiet", False),
184
+ timeout_adder=timeout_adder,
185
+ use_github_state=not no_github_state,
186
+ )
187
+
188
+ result_str = f"Success: {success}\nMessage: {message}\nChanged Files: {changed_files}"
189
+ return result_str, cost, model
190
+
191
+ except (click.Abort, click.ClickException):
192
+ raise
193
+ except Exception as exception:
194
+ handle_error(exception, "bug", ctx.obj.get("quiet", False))
195
+ return None
196
+
197
+
198
+ @click.command("crash")
199
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
200
+ @click.argument("code_file", type=click.Path(exists=True, dir_okay=False))
201
+ @click.argument("program_file", type=click.Path(exists=True, dir_okay=False))
202
+ @click.argument("error_file", type=click.Path(exists=True, dir_okay=False))
203
+ @click.option(
204
+ "--output",
205
+ type=click.Path(writable=True),
206
+ default=None,
207
+ help="Specify where to save the fixed code file (file or directory).",
208
+ )
209
+ @click.option(
210
+ "--output-program",
211
+ type=click.Path(writable=True),
212
+ default=None,
213
+ help="Specify where to save the fixed program file (file or directory).",
214
+ )
215
+ @click.option(
216
+ "--loop",
217
+ is_flag=True,
218
+ default=False,
219
+ help="Enable iterative fixing process.",
220
+ )
221
+ @click.option(
222
+ "--max-attempts",
223
+ type=int,
224
+ default=None,
225
+ help="Maximum number of fix attempts (default: 3).",
226
+ )
227
+ @click.option(
228
+ "--budget",
229
+ type=float,
230
+ default=None,
231
+ help="Maximum cost allowed for the fixing process (default: 5.0).",
232
+ )
233
+ @click.pass_context
234
+ @track_cost
235
+ def crash(
236
+ ctx: click.Context,
237
+ prompt_file: str,
238
+ code_file: str,
239
+ program_file: str,
240
+ error_file: str,
241
+ output: Optional[str],
242
+ output_program: Optional[str],
243
+ loop: bool,
244
+ max_attempts: Optional[int],
245
+ budget: Optional[float],
246
+ ) -> Optional[Tuple[str, float, str]]:
247
+ """Analyze a crash and fix the code and program."""
248
+ try:
249
+ # crash_main returns: success, final_code, final_program, attempts, total_cost, model_name
250
+ success, final_code, final_program, attempts, total_cost, model_name = crash_main(
251
+ ctx=ctx,
252
+ prompt_file=prompt_file,
253
+ code_file=code_file,
254
+ program_file=program_file,
255
+ error_file=error_file,
256
+ output=output,
257
+ output_program=output_program,
258
+ loop=loop,
259
+ max_attempts=max_attempts,
260
+ budget=budget,
261
+ )
262
+ # Return a summary string as the result for track_cost/CLI output
263
+ result = f"Success: {success}, Attempts: {attempts}"
264
+ return result, total_cost, model_name
265
+ except (click.Abort, click.ClickException):
266
+ raise
267
+ except Exception as exception:
268
+ handle_error(exception, "crash", ctx.obj.get("quiet", False))
269
+ return None
270
+
271
+
272
+ @click.command("trace")
273
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
274
+ @click.argument("code_file", type=click.Path(exists=True, dir_okay=False))
275
+ @click.argument("code_line", type=int)
276
+ @click.option(
277
+ "--output",
278
+ type=click.Path(writable=True),
279
+ default=None,
280
+ help="Specify where to save the trace analysis results.",
281
+ )
282
+ @click.pass_context
283
+ @track_cost
284
+ def trace(
285
+ ctx: click.Context,
286
+ prompt_file: str,
287
+ code_file: str,
288
+ code_line: int,
289
+ output: Optional[str],
290
+ ) -> Optional[Tuple[str, float, str]]:
291
+ """Trace execution flow back to the prompt."""
292
+ try:
293
+ # trace_main returns: prompt_line, total_cost, model_name
294
+ result, total_cost, model_name = trace_main(
295
+ ctx=ctx,
296
+ prompt_file=prompt_file,
297
+ code_file=code_file,
298
+ code_line=code_line,
299
+ output=output,
300
+ )
301
+ return str(result), total_cost, model_name
302
+ except (click.Abort, click.ClickException):
303
+ raise
304
+ except Exception as exception:
305
+ handle_error(exception, "trace", ctx.obj.get("quiet", False))
306
+ return None
pdd/commands/auth.py ADDED
@@ -0,0 +1,309 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import base64
5
+ import json
6
+ import os
7
+ import sys
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Optional, Dict, Any
11
+
12
+ import click
13
+ from rich.console import Console
14
+
15
+ # Internal imports
16
+ try:
17
+ from ..auth_service import (
18
+ get_auth_status,
19
+ logout as service_logout,
20
+ JWT_CACHE_FILE,
21
+ )
22
+ from ..get_jwt_token import (
23
+ get_jwt_token,
24
+ AuthError,
25
+ NetworkError,
26
+ TokenError,
27
+ UserCancelledError,
28
+ RateLimitError,
29
+ )
30
+ except ImportError:
31
+ pass
32
+
33
+ console = Console()
34
+
35
+ # Constants
36
+ PDD_ENV = os.environ.get("PDD_ENV", "local")
37
+
38
+
39
+ def _load_firebase_api_key() -> str:
40
+ """Load the Firebase API key from environment or .env files."""
41
+ # 1. Check direct env var
42
+ env_key = os.environ.get("NEXT_PUBLIC_FIREBASE_API_KEY")
43
+ if env_key:
44
+ return env_key
45
+
46
+ # 2. Check .env files in current directory
47
+ candidates = [Path(".env"), Path(".env.local")]
48
+
49
+ for candidate in candidates:
50
+ if candidate.exists():
51
+ try:
52
+ content = candidate.read_text(encoding="utf-8")
53
+ for line in content.splitlines():
54
+ if line.strip().startswith("NEXT_PUBLIC_FIREBASE_API_KEY="):
55
+ return line.split("=", 1)[1].strip().strip('"').strip("'")
56
+ except Exception:
57
+ continue
58
+
59
+ return ""
60
+
61
+
62
+ def _get_client_id() -> Optional[str]:
63
+ """Get the GitHub Client ID for the current environment."""
64
+ return os.environ.get(f"GITHUB_CLIENT_ID_{PDD_ENV.upper()}") or os.environ.get("GITHUB_CLIENT_ID")
65
+
66
+
67
+ def _decode_jwt_payload(token: str) -> Dict[str, Any]:
68
+ """Decode JWT payload without verification to extract claims."""
69
+ try:
70
+ # JWT is header.payload.signature
71
+ parts = token.split(".")
72
+ if len(parts) != 3:
73
+ return {}
74
+
75
+ payload = parts[1]
76
+ # Add padding if needed
77
+ padding = len(payload) % 4
78
+ if padding:
79
+ payload += "=" * (4 - padding)
80
+
81
+ decoded = base64.urlsafe_b64decode(payload)
82
+ return json.loads(decoded)
83
+ except Exception:
84
+ return {}
85
+
86
+
87
+ @click.group("auth")
88
+ def auth_group():
89
+ """Manage PDD Cloud authentication."""
90
+ pass
91
+
92
+
93
+ @auth_group.command("login")
94
+ @click.option(
95
+ "--browser/--no-browser",
96
+ default=None,
97
+ help="Control browser opening (auto-detect if not specified)"
98
+ )
99
+ def login(browser: Optional[bool]):
100
+ """Authenticate with PDD Cloud via GitHub."""
101
+
102
+ api_key = _load_firebase_api_key()
103
+ if not api_key:
104
+ console.print("[red]Error: NEXT_PUBLIC_FIREBASE_API_KEY not found.[/red]")
105
+ console.print("Please set it in your environment or .env file.")
106
+ sys.exit(1)
107
+
108
+ client_id = _get_client_id()
109
+ app_name = "PDD CLI"
110
+
111
+ async def run_login():
112
+ try:
113
+ # Import remote session detection
114
+ from ..core.remote_session import should_skip_browser
115
+
116
+ # Determine if browser should be skipped
117
+ skip_browser, reason = should_skip_browser(explicit_flag=browser)
118
+
119
+ if skip_browser:
120
+ console.print(f"[yellow]Note: {reason}[/yellow]")
121
+ console.print("[yellow]Please open the authentication URL manually in a browser.[/yellow]")
122
+
123
+ # Pass no_browser parameter to get_jwt_token
124
+ token = await get_jwt_token(
125
+ firebase_api_key=api_key,
126
+ github_client_id=client_id,
127
+ app_name=app_name,
128
+ no_browser=skip_browser
129
+ )
130
+
131
+ if not token:
132
+ console.print("[red]Authentication failed: No token received.[/red]")
133
+ sys.exit(1)
134
+
135
+ # Decode token to get expiration
136
+ payload = _decode_jwt_payload(token)
137
+ expires_at = payload.get("exp")
138
+
139
+ # Ensure cache directory exists
140
+ if not JWT_CACHE_FILE.parent.exists():
141
+ JWT_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
142
+
143
+ # Save token and expiration to cache
144
+ # We store id_token for retrieval and expires_at for auth_service checks
145
+ cache_data = {
146
+ "id_token": token,
147
+ "expires_at": expires_at
148
+ }
149
+
150
+ JWT_CACHE_FILE.write_text(json.dumps(cache_data))
151
+
152
+ console.print("[green]Successfully authenticated to PDD Cloud.[/green]")
153
+
154
+ except AuthError as e:
155
+ console.print(f"[red]Authentication failed: {e}[/red]")
156
+ sys.exit(1)
157
+ except NetworkError as e:
158
+ console.print(f"[red]Network error: {e}[/red]")
159
+ sys.exit(1)
160
+ except TokenError as e:
161
+ console.print(f"[red]Token error: {e}[/red]")
162
+ sys.exit(1)
163
+ except UserCancelledError:
164
+ console.print("[yellow]Authentication cancelled by user.[/yellow]")
165
+ sys.exit(1)
166
+ except RateLimitError as e:
167
+ console.print(f"[red]Rate limit exceeded: {e}[/red]")
168
+ sys.exit(1)
169
+ except Exception as e:
170
+ console.print(f"[red]An unexpected error occurred: {e}[/red]")
171
+ sys.exit(1)
172
+
173
+ asyncio.run(run_login())
174
+
175
+
176
+ @auth_group.command("status")
177
+ def status():
178
+ """Check current authentication status."""
179
+ auth_status = get_auth_status()
180
+
181
+ if not auth_status.get("authenticated"):
182
+ console.print("Not authenticated.")
183
+ return
184
+
185
+ username = "Unknown"
186
+
187
+ # If we have a cached token, try to extract user info
188
+ if auth_status.get("cached") and JWT_CACHE_FILE.exists():
189
+ try:
190
+ data = json.loads(JWT_CACHE_FILE.read_text())
191
+ token = data.get("id_token")
192
+ if token:
193
+ payload = _decode_jwt_payload(token)
194
+ # Try to find a meaningful identifier
195
+ username = payload.get("email") or payload.get("sub")
196
+
197
+ # Check for GitHub specific claims if available in Firebase token
198
+ firebase_claims = payload.get("firebase", {})
199
+ identities = firebase_claims.get("identities", {})
200
+ if "github.com" in identities:
201
+ # identities['github.com'] is a list of IDs, not usernames usually
202
+ pass
203
+ except Exception:
204
+ pass
205
+
206
+ console.print(f"Authenticated as: [bold green]{username}[/bold green]")
207
+ sys.exit(0)
208
+
209
+
210
+ @auth_group.command("logout")
211
+ def logout_cmd():
212
+ """Log out of PDD Cloud."""
213
+ success, error = service_logout()
214
+ if success:
215
+ console.print("Logged out of PDD Cloud.")
216
+ else:
217
+ console.print(f"[red]Failed to logout: {error}[/red]")
218
+ # We don't exit with 1 here as partial logout might have occurred
219
+ # and the user is effectively logged out locally anyway.
220
+
221
+
222
+ @auth_group.command("token")
223
+ @click.option("--format", "output_format", type=click.Choice(["raw", "json"]), default="raw", help="Output format.")
224
+ def token_cmd(output_format: str):
225
+ """Print the current authentication token."""
226
+
227
+ token_str = None
228
+ expires_at = None
229
+
230
+ # Attempt to read valid token from cache
231
+ if JWT_CACHE_FILE.exists():
232
+ try:
233
+ data = json.loads(JWT_CACHE_FILE.read_text())
234
+ cached_token = data.get("id_token")
235
+ cached_exp = data.get("expires_at")
236
+
237
+ # Simple expiry check
238
+ if cached_token and cached_exp and cached_exp > time.time():
239
+ token_str = cached_token
240
+ expires_at = cached_exp
241
+ except Exception:
242
+ pass
243
+
244
+ if not token_str:
245
+ # Removed err=True because rich.console.Console.print does not support it
246
+ console.print("[red]No valid token available. Please login.[/red]")
247
+ sys.exit(1)
248
+
249
+ if output_format == "json":
250
+ output = {
251
+ "token": token_str,
252
+ "expires_at": expires_at
253
+ }
254
+ console.print_json(data=output)
255
+ else:
256
+ console.print(token_str)
257
+
258
+
259
+ @auth_group.command("clear-cache")
260
+ def clear_cache():
261
+ """Clear the JWT token cache.
262
+
263
+ This is useful when:
264
+ - Switching between environments (staging vs production)
265
+ - Experiencing authentication issues
266
+ - JWT token audience mismatch errors
267
+
268
+ After clearing the cache, you'll need to re-authenticate
269
+ with 'pdd auth login' or source the appropriate environment
270
+ setup script (e.g., setup_staging_env.sh).
271
+ """
272
+ if not JWT_CACHE_FILE.exists():
273
+ console.print("[yellow]No JWT cache found at ~/.pdd/jwt_cache[/yellow]")
274
+ console.print("Nothing to clear.")
275
+ return
276
+
277
+ try:
278
+ # Try to read cache before deleting to show what was cached
279
+ cache_data = json.loads(JWT_CACHE_FILE.read_text())
280
+ token = cache_data.get("id_token") or cache_data.get("jwt")
281
+ if token:
282
+ payload = _decode_jwt_payload(token)
283
+ aud = payload.get("aud") or payload.get("firebase", {}).get("aud")
284
+ exp = payload.get("exp")
285
+
286
+ console.print("[dim]Cached token info:[/dim]")
287
+ if aud:
288
+ console.print(f" Audience: {aud}")
289
+ if exp:
290
+ if exp > time.time():
291
+ time_remaining = int((exp - time.time()) / 60)
292
+ console.print(f" Expires in: {time_remaining} minutes")
293
+ else:
294
+ console.print(" Status: [red]Expired[/red]")
295
+ except Exception:
296
+ # If we can't read the cache, that's fine - just proceed with deletion
297
+ pass
298
+
299
+ # Delete the cache file
300
+ try:
301
+ JWT_CACHE_FILE.unlink()
302
+ console.print("[green]✓[/green] JWT cache cleared successfully")
303
+ console.print()
304
+ console.print("[dim]To re-authenticate:[/dim]")
305
+ console.print(" - For production: [bold]pdd auth login[/bold]")
306
+ console.print(" - For staging: [bold]source setup_staging_env.sh[/bold]")
307
+ except OSError as e:
308
+ console.print(f"[red]Failed to clear cache: {e}[/red]")
309
+ sys.exit(1)