pdd-cli 0.0.90__py3-none-any.whl → 0.0.121__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +506 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +537 -0
- pdd/agentic_common.py +533 -770
- pdd/agentic_crash.py +2 -1
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +582 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +27 -9
- pdd/agentic_verify.py +3 -2
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +236 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +113 -48
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +358 -0
- pdd/commands/fix.py +155 -114
- pdd/commands/generate.py +5 -0
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +225 -163
- pdd/commands/sessions.py +284 -0
- pdd/commands/utility.py +12 -7
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +44 -7
- pdd/core/cloud.py +237 -0
- pdd/core/dump.py +68 -20
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -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 +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +208 -6
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +531 -97
- pdd/load_prompt_template.py +15 -34
- pdd/operation_log.py +342 -0
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +122 -97
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- 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 +140 -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_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_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +19 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +366 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +123 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -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 +1347 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +217 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +289 -211
- pdd/sync_order.py +304 -0
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +68 -26
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +15 -10
- pdd_cli-0.0.121.dist-info/RECORD +229 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/top_level.txt +0 -0
pdd/context_generator_main.py
CHANGED
|
@@ -1,193 +1,190 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
-
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
4
7
|
from pathlib import Path
|
|
8
|
+
from typing import Optional, Tuple, Dict, Any
|
|
5
9
|
import click
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
import httpx
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.text import Text
|
|
8
14
|
from .construct_paths import construct_paths
|
|
9
15
|
from .context_generator import context_generator
|
|
16
|
+
from .core.cloud import CloudConfig
|
|
17
|
+
# get_jwt_token imports removed - using CloudConfig.get_jwt_token() instead
|
|
18
|
+
from .preprocess import preprocess
|
|
19
|
+
from . import DEFAULT_STRENGTH, DEFAULT_TEMPERATURE
|
|
10
20
|
|
|
21
|
+
console = Console()
|
|
22
|
+
CLOUD_TIMEOUT_SECONDS = 400.0
|
|
11
23
|
|
|
12
|
-
def
|
|
13
|
-
"""
|
|
14
|
-
Validate that the code is valid Python syntax.
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
Tuple of (is_valid, error_message)
|
|
18
|
-
"""
|
|
24
|
+
def _validate_and_fix_python_syntax(code: str, quiet: bool) -> str:
|
|
19
25
|
try:
|
|
20
26
|
ast.parse(code)
|
|
21
|
-
return
|
|
22
|
-
except SyntaxError
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Attempt to fix code that has JSON metadata garbage appended at the end.
|
|
29
|
-
This is a common LLM extraction failure pattern.
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
Fixed code if successful, None if not fixable.
|
|
33
|
-
"""
|
|
34
|
-
lines = code.split('\n')
|
|
35
|
-
|
|
36
|
-
# Look for JSON-like patterns at the end
|
|
37
|
-
json_patterns = ['"explanation":', '"focus":', '"description":', '"code":']
|
|
38
|
-
|
|
27
|
+
return code
|
|
28
|
+
except SyntaxError:
|
|
29
|
+
if not quiet:
|
|
30
|
+
console.print("[yellow]Warning: Generated code has syntax errors. Attempting to fix...[/yellow]")
|
|
31
|
+
lines = code.splitlines()
|
|
32
|
+
json_markers = ['"explanation":', '"focus":', '"description":', '"code":', '"filename":']
|
|
33
|
+
cut_index = -1
|
|
39
34
|
for i in range(len(lines) - 1, -1, -1):
|
|
40
35
|
line = lines[i].strip()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
36
|
+
if any(marker in line for marker in json_markers) or line == "}" or line == "},":
|
|
37
|
+
cut_index = i
|
|
38
|
+
if cut_index != -1:
|
|
39
|
+
candidate = "\n".join(lines[:cut_index])
|
|
40
|
+
try:
|
|
41
|
+
ast.parse(candidate)
|
|
42
|
+
if not quiet:
|
|
43
|
+
console.print("[green]Fix successful: Removed trailing metadata.[/green]")
|
|
44
|
+
return candidate
|
|
45
|
+
except SyntaxError:
|
|
46
|
+
pass
|
|
47
|
+
low = 0
|
|
48
|
+
high = cut_index if cut_index != -1 else len(lines)
|
|
49
|
+
valid_len = 0
|
|
50
|
+
while low < high:
|
|
51
|
+
mid = (low + high + 1) // 2
|
|
52
|
+
candidate = "\n".join(lines[:mid])
|
|
53
|
+
try:
|
|
54
|
+
ast.parse(candidate)
|
|
55
|
+
valid_len = mid
|
|
56
|
+
low = mid
|
|
57
|
+
except SyntaxError:
|
|
58
|
+
high = mid - 1
|
|
59
|
+
for i in range(len(lines), max(0, len(lines) - 50), -1):
|
|
60
|
+
candidate = "\n".join(lines[:i])
|
|
61
|
+
try:
|
|
62
|
+
ast.parse(candidate)
|
|
63
|
+
if not quiet:
|
|
64
|
+
console.print("[green]Fix successful: Truncated invalid tail content.[/green]")
|
|
65
|
+
return candidate
|
|
66
|
+
except SyntaxError:
|
|
54
67
|
continue
|
|
68
|
+
if not quiet:
|
|
69
|
+
console.print("[red]Fix failed: Could not automatically repair syntax.[/red]")
|
|
70
|
+
return code
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Check if this looks like inside a JSON object
|
|
59
|
-
if any(pattern in lines[i] if i < len(lines) else '' for pattern in json_patterns):
|
|
60
|
-
continue
|
|
61
|
-
|
|
62
|
-
return None
|
|
63
|
-
|
|
64
|
-
def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str, output: Optional[str]) -> Tuple[str, float, str]:
|
|
65
|
-
"""
|
|
66
|
-
Main function to generate example code from a prompt file and an existing code file.
|
|
72
|
+
async def _run_cloud_generation(prompt_content: str, code_content: str, language: str, strength: float, temperature: float, verbose: bool, pdd_env: str, token: str) -> Tuple[Optional[str], float, str]:
|
|
73
|
+
"""Run cloud generation with the provided JWT token.
|
|
67
74
|
|
|
68
|
-
:
|
|
69
|
-
|
|
70
|
-
:param code_file: Path to the existing code file.
|
|
71
|
-
:param output: Optional path to save the generated example code.
|
|
72
|
-
:return: A tuple containing the generated example code, total cost, and model name used.
|
|
75
|
+
Note: JWT token must be obtained BEFORE calling this async function to avoid
|
|
76
|
+
nested asyncio.run() calls (CloudConfig.get_jwt_token() uses asyncio.run internally).
|
|
73
77
|
"""
|
|
74
78
|
try:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# Get resolved output path for file path information
|
|
98
|
-
resolved_output = output_file_paths["output"]
|
|
99
|
-
|
|
100
|
-
# Determine file path information for correct imports
|
|
101
|
-
from pathlib import Path
|
|
102
|
-
source_file_path = str(Path(code_file).resolve())
|
|
103
|
-
example_file_path = str(Path(resolved_output).resolve()) if resolved_output else ""
|
|
104
|
-
|
|
105
|
-
# Extract module name from the code file
|
|
106
|
-
module_name = Path(code_file).stem
|
|
107
|
-
|
|
108
|
-
# Generate example code
|
|
109
|
-
strength = ctx.obj.get('strength', 0.5)
|
|
110
|
-
temperature = ctx.obj.get('temperature', 0)
|
|
111
|
-
time = ctx.obj.get('time')
|
|
112
|
-
example_code, total_cost, model_name = context_generator(
|
|
113
|
-
language=language,
|
|
114
|
-
code_module=code_content,
|
|
115
|
-
prompt=prompt_content,
|
|
116
|
-
strength=strength,
|
|
117
|
-
temperature=temperature,
|
|
118
|
-
time=time,
|
|
119
|
-
verbose=ctx.obj.get('verbose', False),
|
|
120
|
-
source_file_path=source_file_path,
|
|
121
|
-
example_file_path=example_file_path,
|
|
122
|
-
module_name=module_name
|
|
123
|
-
)
|
|
79
|
+
processed_prompt = preprocess(prompt_content, recursive=True, double_curly_brackets=False)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return None, 0.0, f"Preprocessing failed: {e}"
|
|
82
|
+
if verbose:
|
|
83
|
+
console.print(Panel(Text(processed_prompt[:500] + "..." if len(processed_prompt) > 500 else processed_prompt, overflow="fold"), title="[cyan]Preprocessed Prompt for Cloud[/cyan]", expand=False))
|
|
84
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
85
|
+
payload = {"promptContent": processed_prompt, "codeContent": code_content, "language": language, "strength": strength, "temperature": temperature, "verbose": verbose}
|
|
86
|
+
async with httpx.AsyncClient(timeout=CLOUD_TIMEOUT_SECONDS) as client:
|
|
87
|
+
try:
|
|
88
|
+
cloud_url = CloudConfig.get_endpoint_url("generateExample")
|
|
89
|
+
response = await client.post(cloud_url, json=payload, headers=headers)
|
|
90
|
+
response.raise_for_status()
|
|
91
|
+
data = response.json()
|
|
92
|
+
generated_code = data.get("generatedExample", "")
|
|
93
|
+
total_cost = float(data.get("totalCost", 0.0))
|
|
94
|
+
model_name = data.get("modelName", "cloud-model")
|
|
95
|
+
if not generated_code:
|
|
96
|
+
return None, 0.0, "Cloud function returned empty code."
|
|
97
|
+
return generated_code, total_cost, model_name
|
|
98
|
+
except Exception as e:
|
|
99
|
+
return None, 0.0, f"Cloud error: {e}"
|
|
124
100
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
101
|
+
def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str, output: Optional[str]) -> Tuple[str, float, str]:
|
|
102
|
+
try:
|
|
103
|
+
input_file_paths = {"prompt_file": prompt_file, "code_file": code_file}
|
|
104
|
+
command_options = {"output": output}
|
|
105
|
+
resolved_config, input_strings, output_file_paths, language = construct_paths(input_file_paths=input_file_paths, force=ctx.obj.get('force', False), quiet=ctx.obj.get('quiet', False), command="example", command_options=command_options, context_override=ctx.obj.get('context'), confirm_callback=ctx.obj.get('confirm_callback'))
|
|
106
|
+
prompt_content = input_strings.get("prompt_file", "")
|
|
107
|
+
code_content = input_strings.get("code_file", "")
|
|
108
|
+
if output and not output.endswith("/") and not Path(output).is_dir():
|
|
109
|
+
resolved_output = output
|
|
128
110
|
else:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
111
|
+
resolved_output = output_file_paths.get("output")
|
|
112
|
+
is_local = ctx.obj.get("local", False)
|
|
113
|
+
strength = ctx.obj.get('strength', DEFAULT_STRENGTH)
|
|
114
|
+
temperature = ctx.obj.get('temperature', DEFAULT_TEMPERATURE)
|
|
115
|
+
verbose = ctx.obj.get('verbose', False)
|
|
116
|
+
quiet = ctx.obj.get('quiet', False)
|
|
117
|
+
pdd_env = os.environ.get("PDD_ENV", "local")
|
|
118
|
+
generated_code = None
|
|
119
|
+
total_cost = 0.0
|
|
120
|
+
model_name = ""
|
|
121
|
+
if not is_local:
|
|
122
|
+
if verbose:
|
|
123
|
+
console.print("Attempting cloud example generation...")
|
|
124
|
+
|
|
125
|
+
# Get JWT token BEFORE entering async context to avoid nested asyncio.run() calls
|
|
126
|
+
# (CloudConfig.get_jwt_token() uses asyncio.run internally for device flow auth)
|
|
127
|
+
jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
|
|
128
|
+
if not jwt_token:
|
|
129
|
+
if not quiet:
|
|
130
|
+
console.print("[yellow]Cloud authentication failed. Falling back to local.[/yellow]")
|
|
131
|
+
is_local = True
|
|
135
132
|
else:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if not
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
133
|
+
try:
|
|
134
|
+
generated_code, total_cost, model_name = asyncio.run(_run_cloud_generation(prompt_content, code_content, language, strength, temperature, verbose, pdd_env, jwt_token))
|
|
135
|
+
if generated_code:
|
|
136
|
+
if verbose:
|
|
137
|
+
console.print(Panel(f"Cloud generation successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Cloud Success[/green]", expand=False))
|
|
138
|
+
except httpx.TimeoutException:
|
|
139
|
+
if not quiet:
|
|
140
|
+
console.print(f"[yellow]Cloud execution timed out ({CLOUD_TIMEOUT_SECONDS}s). Falling back to local.[/yellow]")
|
|
141
|
+
generated_code = None
|
|
142
|
+
except httpx.HTTPStatusError as e:
|
|
143
|
+
status_code = e.response.status_code
|
|
144
|
+
response_text = e.response.text or ""
|
|
145
|
+
err_content = response_text[:200] if response_text else "No response content"
|
|
146
|
+
if status_code == 402:
|
|
147
|
+
console.print(f"[red]Insufficient credits: {err_content}[/red]")
|
|
148
|
+
raise click.UsageError("Insufficient credits for cloud example generation")
|
|
149
|
+
elif status_code == 401:
|
|
150
|
+
console.print(f"[red]Authentication failed: {err_content}[/red]")
|
|
151
|
+
raise click.UsageError("Cloud authentication failed")
|
|
152
|
+
elif status_code == 403:
|
|
153
|
+
console.print(f"[red]Access denied: {err_content}[/red]")
|
|
154
|
+
raise click.UsageError("Access denied - user not approved")
|
|
156
155
|
else:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
if not quiet:
|
|
157
|
+
console.print(f"[yellow]Cloud HTTP error ({status_code}): {err_content}. Falling back to local.[/yellow]")
|
|
158
|
+
generated_code = None
|
|
159
|
+
except Exception as e:
|
|
160
|
+
if verbose:
|
|
161
|
+
console.print(f"[yellow]Cloud error: {e}. Falling back to local.[/yellow]")
|
|
162
|
+
generated_code = None
|
|
163
|
+
|
|
164
|
+
if generated_code is None:
|
|
165
|
+
if not quiet:
|
|
166
|
+
console.print("[yellow]Cloud execution failed. Falling back to local.[/yellow]")
|
|
167
|
+
is_local = True
|
|
168
|
+
if is_local:
|
|
169
|
+
# Compute file path info if not already computed (when --local flag is used from start)
|
|
170
|
+
source_file_path = str(Path(code_file).resolve())
|
|
171
|
+
example_file_path = str(Path(resolved_output).resolve()) if resolved_output else ""
|
|
172
|
+
module_name = Path(code_file).stem
|
|
173
|
+
generated_code, total_cost, model_name = context_generator(code_module=code_content, prompt=prompt_content, language=language, strength=strength, temperature=temperature, verbose=not quiet, source_file_path=source_file_path, example_file_path=example_file_path, module_name=module_name, time=ctx.obj.get('time'))
|
|
174
|
+
if not generated_code:
|
|
163
175
|
raise click.UsageError("Example generation failed, no code produced.")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
|
|
176
|
-
|
|
177
|
-
# Always print example code, even in quiet mode (if it exists)
|
|
178
|
-
if example_code is not None:
|
|
179
|
-
rprint("[bold]Generated Example Code:[/bold]")
|
|
180
|
-
rprint(example_code)
|
|
181
|
-
else:
|
|
182
|
-
rprint("[bold red]No example code generated due to errors.[/bold red]")
|
|
183
|
-
|
|
184
|
-
return example_code, total_cost, model_name
|
|
185
|
-
|
|
186
|
-
except click.Abort:
|
|
187
|
-
# User cancelled - re-raise to stop the sync loop
|
|
188
|
-
raise
|
|
176
|
+
if language and language.lower() == "python":
|
|
177
|
+
generated_code = _validate_and_fix_python_syntax(generated_code, quiet)
|
|
178
|
+
if resolved_output:
|
|
179
|
+
out_path = Path(resolved_output)
|
|
180
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
out_path.write_text(generated_code, encoding="utf-8")
|
|
182
|
+
if not quiet:
|
|
183
|
+
console.print("[bold green]Example generation completed successfully.[/bold green]")
|
|
184
|
+
console.print(f"[bold]Model used:[/bold] {model_name}")
|
|
185
|
+
console.print(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
|
|
186
|
+
return generated_code, total_cost, model_name
|
|
189
187
|
except Exception as e:
|
|
190
188
|
if not ctx.obj.get('quiet', False):
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return "", 0.0, f"Error: {e}"
|
|
189
|
+
console.print(f"[bold red]Error:[/bold red] {str(e)}")
|
|
190
|
+
raise e
|
pdd/continue_generation.py
CHANGED
|
@@ -84,7 +84,8 @@ def continue_generation(
|
|
|
84
84
|
temperature=0,
|
|
85
85
|
time=time,
|
|
86
86
|
output_pydantic=TrimResultsStartOutput,
|
|
87
|
-
verbose=verbose
|
|
87
|
+
verbose=verbose,
|
|
88
|
+
language=language,
|
|
88
89
|
)
|
|
89
90
|
total_cost += trim_start_response['cost']
|
|
90
91
|
code_block = trim_start_response['result'].code_block
|
|
@@ -111,7 +112,8 @@ def continue_generation(
|
|
|
111
112
|
strength=strength,
|
|
112
113
|
temperature=temperature,
|
|
113
114
|
time=time,
|
|
114
|
-
verbose=verbose
|
|
115
|
+
verbose=verbose,
|
|
116
|
+
language=language,
|
|
115
117
|
)
|
|
116
118
|
|
|
117
119
|
total_cost += continue_response['cost']
|
|
@@ -171,7 +173,8 @@ def continue_generation(
|
|
|
171
173
|
temperature=0,
|
|
172
174
|
time=time,
|
|
173
175
|
output_pydantic=TrimResultsOutput,
|
|
174
|
-
verbose=verbose
|
|
176
|
+
verbose=verbose,
|
|
177
|
+
language=language,
|
|
175
178
|
)
|
|
176
179
|
total_cost += trim_response['cost']
|
|
177
180
|
code_block += trim_response['result'].trimmed_continued_generation
|
pdd/core/__init__.py
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core utilities and configuration for PDD CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .cloud import (
|
|
6
|
+
CloudConfig,
|
|
7
|
+
AuthError,
|
|
8
|
+
NetworkError,
|
|
9
|
+
TokenError,
|
|
10
|
+
UserCancelledError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
FIREBASE_API_KEY_ENV,
|
|
13
|
+
GITHUB_CLIENT_ID_ENV,
|
|
14
|
+
PDD_CLOUD_URL_ENV,
|
|
15
|
+
PDD_JWT_TOKEN_ENV,
|
|
16
|
+
DEFAULT_BASE_URL,
|
|
17
|
+
CLOUD_ENDPOINTS,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
'CloudConfig',
|
|
22
|
+
'AuthError',
|
|
23
|
+
'NetworkError',
|
|
24
|
+
'TokenError',
|
|
25
|
+
'UserCancelledError',
|
|
26
|
+
'RateLimitError',
|
|
27
|
+
'FIREBASE_API_KEY_ENV',
|
|
28
|
+
'GITHUB_CLIENT_ID_ENV',
|
|
29
|
+
'PDD_CLOUD_URL_ENV',
|
|
30
|
+
'PDD_JWT_TOKEN_ENV',
|
|
31
|
+
'DEFAULT_BASE_URL',
|
|
32
|
+
'CLOUD_ENDPOINTS',
|
|
33
|
+
]
|
pdd/core/cli.py
CHANGED
|
@@ -194,7 +194,7 @@ class PDDCLI(click.Group):
|
|
|
194
194
|
"--force",
|
|
195
195
|
is_flag=True,
|
|
196
196
|
default=False,
|
|
197
|
-
help="
|
|
197
|
+
help="Skip all interactive prompts (file overwrites, API key requests). Useful for CI/automation.",
|
|
198
198
|
)
|
|
199
199
|
@click.option(
|
|
200
200
|
"--strength",
|
|
@@ -262,11 +262,17 @@ class PDDCLI(click.Group):
|
|
|
262
262
|
help="List available contexts from .pddrc and exit.",
|
|
263
263
|
)
|
|
264
264
|
@click.option(
|
|
265
|
-
"--core-dump",
|
|
265
|
+
"--core-dump/--no-core-dump",
|
|
266
266
|
"core_dump",
|
|
267
|
-
|
|
268
|
-
default
|
|
269
|
-
|
|
267
|
+
default=True,
|
|
268
|
+
help="Write a JSON core dump for this run into .pdd/core_dumps (default: on). Use --no-core-dump to disable.",
|
|
269
|
+
)
|
|
270
|
+
@click.option(
|
|
271
|
+
"--keep-core-dumps",
|
|
272
|
+
"keep_core_dumps",
|
|
273
|
+
type=click.IntRange(min=0),
|
|
274
|
+
default=10,
|
|
275
|
+
help="Number of core dumps to keep (default: 10, min: 0). Older dumps are garbage collected after each dump write.",
|
|
270
276
|
)
|
|
271
277
|
@click.version_option(version=__version__, package_name="pdd-cli")
|
|
272
278
|
@click.pass_context
|
|
@@ -284,6 +290,7 @@ def cli(
|
|
|
284
290
|
context_override: Optional[str],
|
|
285
291
|
list_contexts: bool,
|
|
286
292
|
core_dump: bool,
|
|
293
|
+
keep_core_dumps: int,
|
|
287
294
|
):
|
|
288
295
|
"""
|
|
289
296
|
Main entry point for the PDD CLI. Handles global options and initializes context.
|
|
@@ -296,6 +303,8 @@ def cli(
|
|
|
296
303
|
|
|
297
304
|
ctx.ensure_object(dict)
|
|
298
305
|
ctx.obj["force"] = force
|
|
306
|
+
if force:
|
|
307
|
+
os.environ['PDD_FORCE'] = '1'
|
|
299
308
|
# Only set strength/temperature if explicitly provided (not None)
|
|
300
309
|
# This allows .get("key", default) to return the default when CLI didn't pass a value
|
|
301
310
|
if strength is not None:
|
|
@@ -307,11 +316,20 @@ def cli(
|
|
|
307
316
|
ctx.obj["output_cost"] = output_cost
|
|
308
317
|
ctx.obj["review_examples"] = review_examples
|
|
309
318
|
ctx.obj["local"] = local
|
|
319
|
+
# Propagate --local flag to environment for llm_invoke cloud detection
|
|
320
|
+
if local:
|
|
321
|
+
os.environ['PDD_FORCE_LOCAL'] = '1'
|
|
310
322
|
# Use DEFAULT_TIME if time is not provided
|
|
311
323
|
ctx.obj["time"] = time if time is not None else DEFAULT_TIME
|
|
312
324
|
# Persist context override for downstream calls
|
|
313
325
|
ctx.obj["context"] = context_override
|
|
314
326
|
ctx.obj["core_dump"] = core_dump
|
|
327
|
+
ctx.obj["keep_core_dumps"] = keep_core_dumps
|
|
328
|
+
|
|
329
|
+
# Garbage collect old core dumps on every CLI invocation (Issue #231)
|
|
330
|
+
# This runs regardless of --no-core-dump to ensure cleanup always happens
|
|
331
|
+
from .dump import garbage_collect_core_dumps
|
|
332
|
+
garbage_collect_core_dumps(keep=keep_core_dumps)
|
|
315
333
|
|
|
316
334
|
# Set up terminal output capture if core_dump is enabled
|
|
317
335
|
if core_dump:
|
|
@@ -441,7 +459,7 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
441
459
|
console.print(f" [error]Step {i+1} ({command_name}):[/error] Command failed.")
|
|
442
460
|
# Check if the result is the expected tuple structure from @track_cost or preprocess success
|
|
443
461
|
elif isinstance(result_tuple, tuple) and len(result_tuple) == 3:
|
|
444
|
-
|
|
462
|
+
result_data, cost, model_name = result_tuple
|
|
445
463
|
total_cost += cost
|
|
446
464
|
if not ctx.obj.get("quiet"):
|
|
447
465
|
# Special handling for preprocess success message (check actual command name)
|
|
@@ -451,6 +469,25 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
451
469
|
else:
|
|
452
470
|
# Generic output using potentially "Unknown Command" name
|
|
453
471
|
console.print(f" [info]Step {i+1} ({command_name}):[/info] Cost: ${cost:.6f}, Model: {model_name}")
|
|
472
|
+
|
|
473
|
+
# Display examples used for grounding
|
|
474
|
+
if isinstance(result_data, dict) and result_data.get("examplesUsed"):
|
|
475
|
+
console.print(" Examples used:")
|
|
476
|
+
for ex in result_data["examplesUsed"]:
|
|
477
|
+
slug = ex.get("slug", "unknown")
|
|
478
|
+
title = ex.get("title", "Untitled")
|
|
479
|
+
console.print(f" - {slug} (\"{title}\")")
|
|
480
|
+
|
|
481
|
+
# Handle dicts with examplesUsed (e.g. from commands not using track_cost but returning metadata)
|
|
482
|
+
elif isinstance(result_tuple, dict) and result_tuple.get("examplesUsed"):
|
|
483
|
+
if not ctx.obj.get("quiet"):
|
|
484
|
+
console.print(f" [info]Step {i+1} ({command_name}):[/info] Command completed.")
|
|
485
|
+
console.print(" Examples used:")
|
|
486
|
+
for ex in result_tuple["examplesUsed"]:
|
|
487
|
+
slug = ex.get("slug", "unknown")
|
|
488
|
+
title = ex.get("title", "Untitled")
|
|
489
|
+
console.print(f" - {slug} (\"{title}\")")
|
|
490
|
+
|
|
454
491
|
else:
|
|
455
492
|
# Handle unexpected return types if necessary
|
|
456
493
|
if not ctx.obj.get("quiet"):
|
|
@@ -463,7 +500,7 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
463
500
|
if any(res is not None and isinstance(res, tuple) and len(res) == 3 for res in normalized_results):
|
|
464
501
|
console.print(f"[info]Total Estimated Cost:[/info] ${total_cost:.6f}")
|
|
465
502
|
# Indicate if the chain might have been incomplete due to errors
|
|
466
|
-
if num_results < num_commands and not all(res is None for res in results): # Avoid printing if all failed
|
|
503
|
+
if num_results < num_commands and results is not None and not all(res is None for res in results): # Avoid printing if all failed
|
|
467
504
|
console.print("[warning]Note: Chain may have terminated early due to errors.[/warning]")
|
|
468
505
|
console.print("[info]-------------------------------------[/info]")
|
|
469
506
|
|