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
pdd/commands/modify.py ADDED
@@ -0,0 +1,256 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Optional, Tuple, Any
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+ # Relative imports from parent package
11
+ from ..split_main import split_main
12
+ from ..change_main import change_main
13
+ from ..agentic_change import run_agentic_change
14
+ from ..update_main import update_main
15
+ from ..track_cost import track_cost
16
+ from ..core.errors import handle_error
17
+
18
+ console = Console()
19
+
20
+ @click.command()
21
+ @click.argument("input_prompt", type=click.Path(exists=True))
22
+ @click.argument("input_code", type=click.Path(exists=True))
23
+ @click.argument("example_code", type=click.Path(exists=True))
24
+ @click.option("--output-sub", help="Optional path for saving the sub-prompt.")
25
+ @click.option("--output-modified", help="Optional path for saving the modified prompt.")
26
+ @click.pass_context
27
+ @track_cost
28
+ def split(
29
+ ctx: click.Context,
30
+ input_prompt: str,
31
+ input_code: str,
32
+ example_code: str,
33
+ output_sub: Optional[str],
34
+ output_modified: Optional[str],
35
+ ) -> Optional[Tuple[Any, float, str]]:
36
+ """
37
+ Split large complex prompt files into smaller, more manageable prompt files.
38
+ """
39
+ ctx.ensure_object(dict)
40
+ try:
41
+ # Call split_main with required arguments
42
+ result_data, total_cost, model_name = split_main(
43
+ ctx,
44
+ input_prompt_file=input_prompt,
45
+ input_code_file=input_code,
46
+ example_code_file=example_code,
47
+ output_sub=output_sub,
48
+ output_modified=output_modified,
49
+ )
50
+ return result_data, total_cost, model_name
51
+
52
+ except click.Abort:
53
+ raise
54
+ except Exception as e:
55
+ handle_error(e, "split", ctx.obj.get("quiet", False))
56
+ return None
57
+
58
+
59
+ @click.command()
60
+ @click.argument("args", nargs=-1)
61
+ @click.option("--manual", is_flag=True, default=False, help="Use legacy manual mode.")
62
+ @click.option("--budget", type=float, default=5.0, help="Budget for the operation.")
63
+ @click.option("--output", help="Output path.")
64
+ @click.option("--csv", is_flag=True, help="Use CSV input for batch processing.")
65
+ @click.option("--timeout-adder", type=float, default=0.0, help="Additional seconds to add to each step's timeout (agentic mode only).")
66
+ @click.option("--no-github-state", is_flag=True, default=False, help="Disable GitHub state persistence (agentic mode only).")
67
+ @click.pass_context
68
+ @track_cost
69
+ def change(
70
+ ctx: click.Context,
71
+ args: Tuple[str, ...],
72
+ manual: bool,
73
+ budget: float,
74
+ output: Optional[str],
75
+ csv: bool,
76
+ timeout_adder: float,
77
+ no_github_state: bool,
78
+ ) -> Optional[Tuple[Any, float, str]]:
79
+ """
80
+ Modify an input prompt file based on a change prompt or issue.
81
+
82
+ Agentic Mode (default):
83
+ pdd change ISSUE_URL
84
+
85
+ Manual Mode (--manual):
86
+ pdd change --manual CHANGE_PROMPT_FILE INPUT_CODE_FILE [INPUT_PROMPT_FILE]
87
+ """
88
+ ctx.ensure_object(dict)
89
+
90
+ try:
91
+ # Set budget in context for manual mode usage
92
+ ctx.obj["budget"] = budget
93
+
94
+ quiet = ctx.obj.get("quiet", False)
95
+ verbose = ctx.obj.get("verbose", False)
96
+
97
+ if manual:
98
+ # Manual Mode Validation and Execution
99
+ if csv:
100
+ # CSV Mode: Expecting CSV_FILE and CODE_DIRECTORY (no input_prompt)
101
+ if len(args) == 3:
102
+ raise click.UsageError("Cannot use --csv and specify an INPUT_PROMPT_FILE simultaneously.")
103
+ if len(args) != 2:
104
+ raise click.UsageError("CSV mode requires 2 arguments: CSV_FILE CODE_DIRECTORY")
105
+
106
+ change_file, input_code = args
107
+ input_prompt = None
108
+
109
+ # CSV mode requires input_code to be a directory
110
+ if not Path(input_code).is_dir():
111
+ raise click.UsageError("INPUT_CODE must be a directory when using --csv")
112
+ else:
113
+ # Standard Manual Mode: Expecting 2 or 3 arguments
114
+ if len(args) == 3:
115
+ change_file, input_code, input_prompt = args
116
+ # Non-CSV mode requires input_code to be a file, not a directory
117
+ if Path(input_code).is_dir():
118
+ raise click.UsageError("INPUT_CODE must be a file when not using --csv")
119
+ elif len(args) == 2:
120
+ change_file, input_code = args
121
+ input_prompt = None
122
+ # Without CSV mode, input_prompt_file is required
123
+ raise click.UsageError("INPUT_PROMPT_FILE is required when not using --csv")
124
+ else:
125
+ raise click.UsageError(
126
+ "Manual mode requires 2 or 3 arguments: CHANGE_PROMPT INPUT_CODE [INPUT_PROMPT]"
127
+ )
128
+
129
+ # Validate file existence
130
+ if not Path(change_file).exists():
131
+ raise click.UsageError(f"Change file not found: {change_file}")
132
+ if not Path(input_code).exists():
133
+ raise click.UsageError(f"Input code path not found: {input_code}")
134
+ if input_prompt and not Path(input_prompt).exists():
135
+ raise click.UsageError(f"Input prompt file not found: {input_prompt}")
136
+
137
+ # Call change_main
138
+ result, cost, model = change_main(
139
+ ctx=ctx,
140
+ change_prompt_file=change_file,
141
+ input_code=input_code,
142
+ input_prompt_file=input_prompt,
143
+ output=output,
144
+ use_csv=csv,
145
+ budget=budget
146
+ )
147
+ return result, cost, model
148
+
149
+ else:
150
+ # Agentic Mode Validation and Execution
151
+ if len(args) != 1:
152
+ raise click.UsageError("Agentic mode requires exactly 1 argument: ISSUE_URL")
153
+
154
+ issue_url = args[0]
155
+
156
+ # Call run_agentic_change
157
+ success, message, cost, model, changed_files = run_agentic_change(
158
+ issue_url=issue_url,
159
+ verbose=verbose,
160
+ quiet=quiet,
161
+ timeout_adder=timeout_adder,
162
+ use_github_state=not no_github_state
163
+ )
164
+
165
+ # Display results using click.echo as requested
166
+ if not quiet:
167
+ status = "Success" if success else "Failed"
168
+ click.echo(f"Status: {status}")
169
+ click.echo(f"Message: {message}")
170
+ click.echo(f"Cost: ${cost:.4f}")
171
+ click.echo(f"Model: {model}")
172
+ if changed_files:
173
+ click.echo("Changed files:")
174
+ for f in changed_files:
175
+ click.echo(f" - {f}")
176
+
177
+ return message, cost, model
178
+
179
+ except click.Abort:
180
+ raise
181
+ except Exception as e:
182
+ handle_error(e, "change", ctx.obj.get("quiet", False))
183
+ return None
184
+
185
+
186
+ @click.command()
187
+ @click.argument("files", nargs=-1)
188
+ @click.option("--extensions", help="Comma-separated extensions for repo mode.")
189
+ @click.option("--directory", help="Directory to scan for repo mode.")
190
+ @click.option("--git", is_flag=True, help="Use git history for original code.")
191
+ @click.option("--output", help="Output path for the updated prompt.")
192
+ @click.option("--simple", is_flag=True, default=False, help="Use legacy simple update.")
193
+ @click.pass_context
194
+ @track_cost
195
+ def update(
196
+ ctx: click.Context,
197
+ files: Tuple[str, ...],
198
+ extensions: Optional[str],
199
+ directory: Optional[str],
200
+ git: bool,
201
+ output: Optional[str],
202
+ simple: bool,
203
+ ) -> Optional[Tuple[Any, float, str]]:
204
+ """
205
+ Update the original prompt file based on code changes.
206
+
207
+ Repo-wide mode (no args): Scan entire repo.
208
+ Single-file mode (1 arg): Update prompt for specific code file.
209
+ """
210
+ ctx.ensure_object(dict)
211
+ try:
212
+ # Determine mode based on argument count
213
+ is_repo_mode = len(files) == 0
214
+
215
+ # Validate mode-specific options
216
+ if is_repo_mode:
217
+ # Repo-wide mode: --git and --output are not allowed
218
+ if git:
219
+ raise click.UsageError(
220
+ "Cannot use file-specific arguments or flags like --git in repository-wide mode"
221
+ )
222
+ else:
223
+ # Single-file mode: --extensions and --directory are not allowed
224
+ if extensions:
225
+ raise click.UsageError(
226
+ "--extensions can only be used in repository-wide mode"
227
+ )
228
+ if directory:
229
+ raise click.UsageError(
230
+ "--directory can only be used in repository-wide mode"
231
+ )
232
+
233
+ # In single-file mode, the one arg is the modified code file
234
+ modified_code_file = files[0] if len(files) > 0 else None
235
+
236
+ # Call update_main with correct parameters
237
+ result, cost, model = update_main(
238
+ ctx=ctx,
239
+ input_prompt_file=None,
240
+ modified_code_file=modified_code_file,
241
+ input_code_file=None,
242
+ output=output,
243
+ use_git=git,
244
+ repo=is_repo_mode,
245
+ extensions=extensions,
246
+ directory=directory,
247
+ simple=simple
248
+ )
249
+
250
+ return result, cost, model
251
+
252
+ except click.Abort:
253
+ raise
254
+ except Exception as e:
255
+ handle_error(e, "update", ctx.obj.get("quiet", False))
256
+ return None
pdd/commands/report.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ Report commands (report-core).
3
+ """
4
+ import os
5
+ import click
6
+ import json
7
+ import webbrowser
8
+ import urllib.parse
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from ..core.errors import handle_error, console
13
+ from ..core.dump import _build_issue_markdown, _github_config, _post_issue_to_github, _create_gist_with_files
14
+
15
+ @click.command("report-core")
16
+ @click.argument("core_file", type=click.Path(exists=True, dir_okay=False), required=False)
17
+ @click.option(
18
+ "--api",
19
+ is_flag=True,
20
+ default=False,
21
+ help="Create issue directly via GitHub API instead of opening browser. Requires authentication."
22
+ )
23
+ @click.option(
24
+ "--repo",
25
+ default=None,
26
+ help="GitHub repository in format 'owner/repo'. Defaults to 'promptdriven/pdd' or PDD_GITHUB_REPO env var."
27
+ )
28
+ @click.option(
29
+ "--description",
30
+ "-d",
31
+ default="",
32
+ help="Optional description of what happened to include in the issue."
33
+ )
34
+ @click.pass_context
35
+ def report_core(ctx: click.Context, core_file: Optional[str], api: bool, repo: Optional[str], description: str):
36
+ """Report a bug by creating a GitHub issue with the core dump file.
37
+
38
+ If CORE_FILE is not provided, the most recent core dump in .pdd/core_dumps is used.
39
+
40
+ By default, opens a browser with a pre-filled issue template. Use --api to create
41
+ the issue directly via GitHub API (requires authentication via gh CLI or GITHUB_TOKEN).
42
+ """
43
+ try:
44
+ if not core_file:
45
+ # Find latest core dump
46
+ core_dump_dir = Path.cwd() / ".pdd" / "core_dumps"
47
+ if not core_dump_dir.exists():
48
+ raise click.UsageError("No core dumps found in .pdd/core_dumps.")
49
+
50
+ dumps = sorted(core_dump_dir.glob("pdd-core-*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
51
+ if not dumps:
52
+ raise click.UsageError("No core dumps found in .pdd/core_dumps.")
53
+ core_file = str(dumps[0])
54
+ console.print(f"[info]Using most recent core dump: {core_file}[/info]")
55
+
56
+ core_path = Path(core_file)
57
+ try:
58
+ payload = json.loads(core_path.read_text(encoding="utf-8"))
59
+ except Exception as e:
60
+ raise click.UsageError(f"Failed to parse core dump: {e}")
61
+
62
+ # Determine repository
63
+ target_repo = repo or os.getenv("PDD_GITHUB_REPO", "promptdriven/pdd")
64
+
65
+ # For API submission, create a gist with all files
66
+ gist_url = None
67
+ if api:
68
+ console.print("[info]Attempting to create issue via GitHub API...[/info]")
69
+
70
+ github_config = _github_config(target_repo)
71
+ if not github_config:
72
+ console.print(
73
+ "[error]No GitHub authentication found. Please either:[/error]\n"
74
+ " 1. Install and authenticate with GitHub CLI: gh auth login\n"
75
+ " 2. Set GITHUB_TOKEN or GH_TOKEN environment variable\n"
76
+ " 3. Set PDD_GITHUB_TOKEN environment variable\n"
77
+ "\n"
78
+ "[info]Falling back to browser-based submission...[/info]"
79
+ )
80
+ api = False
81
+ else:
82
+ token, resolved_repo = github_config
83
+
84
+ # Create gist with all files
85
+ console.print(f"[info]Creating Gist with all files...[/info]")
86
+ gist_url = _create_gist_with_files(token, payload, core_path)
87
+
88
+ if gist_url:
89
+ console.print(f"[success]Gist created: {gist_url}[/success]")
90
+ else:
91
+ console.print("[warning]Failed to create Gist, including files in issue body...[/warning]")
92
+
93
+ # Build issue with gist link
94
+ title, body = _build_issue_markdown(
95
+ payload=payload,
96
+ description=description,
97
+ core_path=core_path,
98
+ replay_path=None,
99
+ attachments=[],
100
+ truncate_files=False,
101
+ gist_url=gist_url
102
+ )
103
+
104
+ console.print(f"[info]Creating issue in {resolved_repo}...[/info]")
105
+ issue_url = _post_issue_to_github(token, resolved_repo, title, body)
106
+ if issue_url:
107
+ console.print(f"[success]Issue created successfully: {issue_url}[/success]")
108
+ return
109
+ else:
110
+ console.print(
111
+ "[warning]Failed to create issue via API. Falling back to browser...[/warning]"
112
+ )
113
+ api = False
114
+
115
+ # Build issue content for browser mode (if not already built for API)
116
+ if not api:
117
+ # For browser-based submission, we'll truncate files to avoid URL length limits
118
+ title, body = _build_issue_markdown(
119
+ payload=payload,
120
+ description=description,
121
+ core_path=core_path,
122
+ replay_path=None,
123
+ attachments=[],
124
+ truncate_files=True # Truncate for browser
125
+ )
126
+
127
+ # Browser-based submission (default or fallback)
128
+ if not api:
129
+ # URL encode
130
+ encoded_title = urllib.parse.quote(title)
131
+ encoded_body = urllib.parse.quote(body)
132
+
133
+ url = f"https://github.com/{target_repo}/issues/new?title={encoded_title}&body={encoded_body}"
134
+
135
+ console.print(f"[info]Opening GitHub issue creation page for {target_repo}...[/info]")
136
+ console.print("[info]Note: File contents are truncated for browser submission. Use --api for full contents.[/info]")
137
+
138
+ if len(url) > 8000:
139
+ console.print("[warning]The issue body is large. Browser might truncate it.[/warning]")
140
+
141
+ webbrowser.open(url)
142
+
143
+ except Exception as e:
144
+ handle_error(e, "report-core", ctx.obj.get("quiet", False))
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ..core.cloud import CloudConfig
12
+ from ..remote_session import RemoteSessionManager, RemoteSessionError
13
+
14
+ console = Console()
15
+
16
+
17
+ @click.group(name="sessions")
18
+ def sessions() -> None:
19
+ """Manage remote PDD sessions."""
20
+ pass
21
+
22
+
23
+ @sessions.command(name="list")
24
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
25
+ def list_sessions(json_output: bool) -> None:
26
+ """List active remote sessions.
27
+
28
+ Retrieves a list of active remote sessions associated with the current
29
+ authenticated user and displays them in a table or as JSON.
30
+ """
31
+ jwt_token = CloudConfig.get_jwt_token()
32
+ if not jwt_token:
33
+ console.print("[red]Error: Not authenticated. Please run 'pdd auth login'.[/red]")
34
+ return
35
+
36
+ try:
37
+ sessions_list = asyncio.run(RemoteSessionManager.list_sessions(jwt_token))
38
+ except Exception as e:
39
+ console.print(f"[red]Error listing sessions: {e}[/red]")
40
+ return
41
+
42
+ if json_output:
43
+ output_data = []
44
+ for s in sessions_list:
45
+ # Handle Pydantic v1/v2 or dataclasses
46
+ if hasattr(s, "model_dump"):
47
+ output_data.append(s.model_dump())
48
+ elif hasattr(s, "dict"):
49
+ output_data.append(s.dict())
50
+ else:
51
+ output_data.append(s.__dict__)
52
+ console.print_json(data=output_data)
53
+ return
54
+
55
+ if not sessions_list:
56
+ console.print("[yellow]No active remote sessions found.[/yellow]")
57
+ return
58
+
59
+ table = Table(show_header=True, header_style="bold magenta")
60
+ table.add_column("SESSION ID", style="dim", width=12)
61
+ table.add_column("PROJECT")
62
+ table.add_column("CLOUD URL", style="blue")
63
+ table.add_column("STATUS")
64
+ table.add_column("LAST SEEN")
65
+
66
+ for session in sessions_list:
67
+ # Safely access attributes with defaults
68
+ s_id = getattr(session, "session_id", "unknown")
69
+ project = getattr(session, "project_name", "default")
70
+ url = getattr(session, "cloud_url", "")
71
+ status = getattr(session, "status", "unknown")
72
+ last_seen = getattr(session, "last_heartbeat", "never")
73
+
74
+ # Truncate ID for display
75
+ display_id = s_id[:8] if len(s_id) > 8 else s_id
76
+
77
+ # Colorize status
78
+ status_str = str(status)
79
+ if status_str.lower() == "active":
80
+ status_render = f"[green]{status_str}[/green]"
81
+ elif status_str.lower() == "stale":
82
+ status_render = f"[yellow]{status_str}[/yellow]"
83
+ else:
84
+ status_render = status_str
85
+
86
+ table.add_row(
87
+ display_id,
88
+ str(project),
89
+ str(url),
90
+ status_render,
91
+ str(last_seen)
92
+ )
93
+
94
+ console.print(table)
95
+
96
+
97
+ @sessions.command(name="info")
98
+ @click.argument("session_id")
99
+ def session_info(session_id: str) -> None:
100
+ """Display detailed info about a specific session.
101
+
102
+ Args:
103
+ session_id: The unique identifier of the session to inspect.
104
+ """
105
+ jwt_token = CloudConfig.get_jwt_token()
106
+ if not jwt_token:
107
+ console.print("[red]Error: Not authenticated. Please run 'pdd auth login'.[/red]")
108
+ return
109
+
110
+ try:
111
+ # Attempt to fetch specific session details
112
+ # Note: Assuming get_session exists on RemoteSessionManager
113
+ session = asyncio.run(RemoteSessionManager.get_session(jwt_token, session_id))
114
+ except Exception as e:
115
+ console.print(f"[red]Error fetching session: {e}[/red]")
116
+ return
117
+
118
+ if not session:
119
+ console.print(f"[red]Session '{session_id}' not found.[/red]")
120
+ return
121
+
122
+ console.print(f"[bold blue]Session Information: {session_id}[/bold blue]")
123
+
124
+ # Convert session object to dictionary for iteration
125
+ if hasattr(session, "model_dump"):
126
+ data = session.model_dump()
127
+ elif hasattr(session, "dict"):
128
+ data = session.dict()
129
+ else:
130
+ data = session.__dict__
131
+
132
+ # Display metadata in a clean table
133
+ table = Table(show_header=False, box=None, padding=(0, 2))
134
+ table.add_column("Field", style="bold cyan", justify="right")
135
+ table.add_column("Value", style="white")
136
+
137
+ # Sort keys for consistent display
138
+ for key in sorted(data.keys()):
139
+ value = data[key]
140
+ # Format key for display (snake_case to Title Case)
141
+ display_key = key.replace("_", " ").title()
142
+ table.add_row(display_key, str(value))
143
+
144
+ console.print(table)
145
+
146
+
147
+ @sessions.command(name="cleanup")
148
+ @click.option("--all", "cleanup_all", is_flag=True, help="Cleanup all sessions (including active).")
149
+ @click.option("--stale", "cleanup_stale", is_flag=True, help="Cleanup only stale sessions.")
150
+ @click.option("--force", is_flag=True, help="Skip confirmation prompt.")
151
+ def cleanup_sessions(cleanup_all: bool, cleanup_stale: bool, force: bool) -> None:
152
+ """Cleanup (deregister) remote sessions.
153
+
154
+ By default, lists sessions and prompts for cleanup.
155
+ Use --all to cleanup all sessions, or --stale to cleanup only stale sessions.
156
+ """
157
+ jwt_token = CloudConfig.get_jwt_token()
158
+ if not jwt_token:
159
+ console.print("[red]Error: Not authenticated. Please run 'pdd login'.[/red]")
160
+ return
161
+
162
+ try:
163
+ sessions_list = asyncio.run(RemoteSessionManager.list_sessions(jwt_token))
164
+ except Exception as e:
165
+ console.print(f"[red]Error listing sessions: {e}[/red]")
166
+ return
167
+
168
+ if not sessions_list:
169
+ console.print("[yellow]No active remote sessions found.[/yellow]")
170
+ return
171
+
172
+ # Filter sessions based on flags
173
+ if cleanup_stale:
174
+ sessions_to_cleanup = [s for s in sessions_list if getattr(s, "status", "").lower() == "stale"]
175
+ if not sessions_to_cleanup:
176
+ console.print("[yellow]No stale sessions found.[/yellow]")
177
+ return
178
+ elif cleanup_all:
179
+ sessions_to_cleanup = sessions_list
180
+ else:
181
+ # Interactive mode - show sessions and ask which to cleanup
182
+ console.print("[bold]Current remote sessions:[/bold]")
183
+ table = Table(show_header=True, header_style="bold magenta")
184
+ table.add_column("#", style="dim", width=3)
185
+ table.add_column("SESSION ID", style="dim", width=12)
186
+ table.add_column("PROJECT")
187
+ table.add_column("STATUS")
188
+ table.add_column("LAST SEEN")
189
+
190
+ for idx, session in enumerate(sessions_list, 1):
191
+ s_id = getattr(session, "session_id", "unknown")
192
+ project = getattr(session, "project_name", "default")
193
+ status = getattr(session, "status", "unknown")
194
+ last_seen = getattr(session, "last_heartbeat", "never")
195
+
196
+ display_id = s_id[:8] if len(s_id) > 8 else s_id
197
+
198
+ status_str = str(status)
199
+ if status_str.lower() == "active":
200
+ status_render = f"[green]{status_str}[/green]"
201
+ elif status_str.lower() == "stale":
202
+ status_render = f"[yellow]{status_str}[/yellow]"
203
+ else:
204
+ status_render = status_str
205
+
206
+ table.add_row(
207
+ str(idx),
208
+ display_id,
209
+ str(project),
210
+ status_render,
211
+ str(last_seen)
212
+ )
213
+
214
+ console.print(table)
215
+ console.print("\n[bold]Options:[/bold]")
216
+ console.print(" - Enter session numbers (comma-separated) to cleanup specific sessions")
217
+ console.print(" - Enter 'stale' to cleanup all stale sessions")
218
+ console.print(" - Enter 'all' to cleanup all sessions")
219
+ console.print(" - Press Enter to cancel")
220
+
221
+ choice = click.prompt("\nYour choice", default="", show_default=False)
222
+
223
+ if not choice:
224
+ console.print("[yellow]Cancelled.[/yellow]")
225
+ return
226
+
227
+ if choice.lower() == "all":
228
+ sessions_to_cleanup = sessions_list
229
+ elif choice.lower() == "stale":
230
+ sessions_to_cleanup = [s for s in sessions_list if getattr(s, "status", "").lower() == "stale"]
231
+ if not sessions_to_cleanup:
232
+ console.print("[yellow]No stale sessions found.[/yellow]")
233
+ return
234
+ else:
235
+ # Parse comma-separated numbers
236
+ try:
237
+ indices = [int(x.strip()) - 1 for x in choice.split(",")]
238
+ sessions_to_cleanup = [sessions_list[i] for i in indices if 0 <= i < len(sessions_list)]
239
+ if not sessions_to_cleanup:
240
+ console.print("[red]Invalid selection.[/red]")
241
+ return
242
+ except (ValueError, IndexError):
243
+ console.print("[red]Invalid input. Please enter numbers separated by commas.[/red]")
244
+ return
245
+
246
+ # Confirm cleanup
247
+ if not force:
248
+ console.print(f"\n[bold yellow]About to cleanup {len(sessions_to_cleanup)} session(s):[/bold yellow]")
249
+ for session in sessions_to_cleanup:
250
+ s_id = getattr(session, "session_id", "unknown")
251
+ project = getattr(session, "project_name", "default")
252
+ console.print(f" - {s_id[:8]} ({project})")
253
+
254
+ if not click.confirm("\nProceed with cleanup?", default=False):
255
+ console.print("[yellow]Cancelled.[/yellow]")
256
+ return
257
+
258
+ # Perform cleanup
259
+ success_count = 0
260
+ fail_count = 0
261
+
262
+ async def cleanup_session(session_id: str) -> bool:
263
+ """Helper to deregister a single session."""
264
+ from pathlib import Path
265
+ manager = RemoteSessionManager(jwt_token, project_path=Path.cwd())
266
+ manager.session_id = session_id
267
+ try:
268
+ await manager.deregister()
269
+ return True
270
+ except Exception as e:
271
+ console.print(f"[red]Failed to cleanup {session_id[:8]}: {e}[/red]")
272
+ return False
273
+
274
+ with console.status("[bold green]Cleaning up sessions..."):
275
+ for session in sessions_to_cleanup:
276
+ s_id = getattr(session, "session_id", "unknown")
277
+ if asyncio.run(cleanup_session(s_id)):
278
+ success_count += 1
279
+ else:
280
+ fail_count += 1
281
+
282
+ console.print(f"\n[bold green]✓[/bold green] Successfully cleaned up {success_count} session(s)")
283
+ if fail_count > 0:
284
+ console.print(f"[bold red]✗[/bold red] Failed to cleanup {fail_count} session(s)")