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/__init__.py
ADDED
|
@@ -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)
|
pdd/commands/analysis.py
ADDED
|
@@ -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)
|