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.
- pdd/__init__.py +40 -8
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +497 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +526 -0
- pdd/agentic_common.py +598 -0
- pdd/agentic_crash.py +534 -0
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +1294 -0
- pdd/agentic_langtest.py +162 -0
- pdd/agentic_update.py +387 -0
- pdd/agentic_verify.py +183 -0
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +71 -51
- pdd/auto_include.py +245 -5
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +196 -23
- pdd/bug_to_unit_test.py +2 -0
- pdd/change_main.py +11 -4
- pdd/cli.py +22 -1181
- pdd/cmd_test_main.py +350 -150
- pdd/code_generator.py +60 -18
- pdd/code_generator_main.py +790 -57
- pdd/commands/__init__.py +48 -0
- pdd/commands/analysis.py +306 -0
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +163 -0
- pdd/commands/generate.py +257 -0
- pdd/commands/maintenance.py +175 -0
- pdd/commands/misc.py +87 -0
- pdd/commands/modify.py +256 -0
- pdd/commands/report.py +144 -0
- pdd/commands/sessions.py +284 -0
- pdd/commands/templates.py +215 -0
- pdd/commands/utility.py +110 -0
- pdd/config_resolution.py +58 -0
- pdd/conflicts_main.py +8 -3
- pdd/construct_paths.py +589 -111
- pdd/context_generator.py +10 -2
- pdd/context_generator_main.py +175 -76
- pdd/continue_generation.py +53 -10
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +527 -0
- pdd/core/cloud.py +237 -0
- pdd/core/dump.py +554 -0
- pdd/core/errors.py +67 -0
- pdd/core/remote_session.py +61 -0
- pdd/core/utils.py +90 -0
- pdd/crash_main.py +262 -33
- pdd/data/language_format.csv +71 -63
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +523 -95
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +491 -92
- pdd/fix_errors_from_unit_tests.py +4 -3
- pdd/fix_main.py +278 -21
- pdd/fix_verification_errors.py +12 -100
- pdd/fix_verification_errors_loop.py +529 -286
- pdd/fix_verification_main.py +294 -89
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +139 -15
- pdd/generate_test.py +218 -146
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +318 -22
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +75 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +13 -4
- pdd/llm_invoke.py +1711 -181
- pdd/load_prompt_template.py +19 -12
- pdd/path_resolution.py +140 -0
- pdd/pdd_completion.fish +25 -2
- pdd/pdd_completion.sh +30 -4
- pdd/pdd_completion.zsh +79 -4
- pdd/postprocess.py +14 -4
- pdd/preprocess.py +293 -24
- pdd/preprocess_main.py +41 -6
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
- pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
- pdd/prompts/agentic_update_LLM.prompt +925 -0
- pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
- pdd/prompts/auto_include_LLM.prompt +122 -905
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +686 -27
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
- pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
- pdd/prompts/extract_promptline_LLM.prompt +17 -11
- pdd/prompts/find_verification_errors_LLM.prompt +6 -0
- pdd/prompts/fix_code_module_errors_LLM.prompt +12 -2
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +41 -7
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/increase_tests_LLM.prompt +1 -5
- pdd/prompts/insert_includes_LLM.prompt +316 -186
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/pytest_output.py +127 -12
- pdd/remote_session.py +876 -0
- pdd/render_mermaid.py +236 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1322 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +209 -0
- pdd/server/token_counter.py +222 -0
- pdd/setup_tool.py +648 -0
- pdd/simple_math.py +2 -0
- pdd/split_main.py +3 -2
- pdd/summarize_directory.py +237 -195
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +839 -112
- pdd/sync_main.py +351 -57
- pdd/sync_orchestration.py +1400 -756
- pdd/sync_tui.py +848 -0
- pdd/template_expander.py +161 -0
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +237 -0
- pdd/templates/generic/generate_prompt.prompt +174 -0
- pdd/trace.py +168 -12
- pdd/trace_main.py +4 -3
- pdd/track_cost.py +140 -63
- pdd/unfinished_prompt.py +51 -4
- pdd/update_main.py +567 -67
- pdd/update_model_costs.py +2 -2
- pdd/update_prompt.py +19 -4
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.45.dist-info/RECORD +0 -116
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {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))
|
pdd/commands/sessions.py
ADDED
|
@@ -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)")
|