pdd-cli 0.0.90__py3-none-any.whl → 0.0.118__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +521 -786
  7. pdd/agentic_e2e_fix.py +319 -0
  8. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  9. pdd/agentic_fix.py +118 -3
  10. pdd/agentic_update.py +25 -8
  11. pdd/architecture_sync.py +565 -0
  12. pdd/auth_service.py +210 -0
  13. pdd/auto_deps_main.py +63 -53
  14. pdd/auto_include.py +185 -3
  15. pdd/auto_update.py +125 -47
  16. pdd/bug_main.py +195 -23
  17. pdd/cmd_test_main.py +345 -197
  18. pdd/code_generator.py +4 -2
  19. pdd/code_generator_main.py +118 -32
  20. pdd/commands/__init__.py +6 -0
  21. pdd/commands/analysis.py +87 -29
  22. pdd/commands/auth.py +309 -0
  23. pdd/commands/connect.py +290 -0
  24. pdd/commands/fix.py +136 -113
  25. pdd/commands/maintenance.py +3 -2
  26. pdd/commands/misc.py +8 -0
  27. pdd/commands/modify.py +190 -164
  28. pdd/commands/sessions.py +284 -0
  29. pdd/construct_paths.py +334 -32
  30. pdd/context_generator_main.py +167 -170
  31. pdd/continue_generation.py +6 -3
  32. pdd/core/__init__.py +33 -0
  33. pdd/core/cli.py +27 -3
  34. pdd/core/cloud.py +237 -0
  35. pdd/core/errors.py +4 -0
  36. pdd/core/remote_session.py +61 -0
  37. pdd/crash_main.py +219 -23
  38. pdd/data/llm_model.csv +4 -4
  39. pdd/docs/prompting_guide.md +864 -0
  40. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  41. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  42. pdd/fix_code_loop.py +208 -34
  43. pdd/fix_code_module_errors.py +6 -2
  44. pdd/fix_error_loop.py +291 -38
  45. pdd/fix_main.py +204 -4
  46. pdd/fix_verification_errors_loop.py +235 -26
  47. pdd/fix_verification_main.py +269 -83
  48. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  49. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  50. pdd/frontend/dist/index.html +376 -0
  51. pdd/frontend/dist/logo.svg +33 -0
  52. pdd/generate_output_paths.py +46 -5
  53. pdd/generate_test.py +212 -151
  54. pdd/get_comment.py +19 -44
  55. pdd/get_extension.py +8 -9
  56. pdd/get_jwt_token.py +309 -20
  57. pdd/get_language.py +8 -7
  58. pdd/get_run_command.py +7 -5
  59. pdd/insert_includes.py +2 -1
  60. pdd/llm_invoke.py +459 -95
  61. pdd/load_prompt_template.py +15 -34
  62. pdd/path_resolution.py +140 -0
  63. pdd/postprocess.py +4 -1
  64. pdd/preprocess.py +68 -12
  65. pdd/preprocess_main.py +33 -1
  66. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  67. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  68. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  69. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  70. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  71. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  72. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  73. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  74. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  75. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  76. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  77. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  78. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  79. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  80. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  81. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  82. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  83. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  84. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  85. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  86. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  87. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  88. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  89. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  90. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  91. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  92. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  93. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  94. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  95. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  96. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  97. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  98. pdd/prompts/agentic_update_LLM.prompt +192 -338
  99. pdd/prompts/auto_include_LLM.prompt +22 -0
  100. pdd/prompts/change_LLM.prompt +3093 -1
  101. pdd/prompts/detect_change_LLM.prompt +571 -14
  102. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  103. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  104. pdd/prompts/generate_test_LLM.prompt +20 -1
  105. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  106. pdd/prompts/insert_includes_LLM.prompt +262 -252
  107. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  108. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  109. pdd/remote_session.py +876 -0
  110. pdd/server/__init__.py +52 -0
  111. pdd/server/app.py +335 -0
  112. pdd/server/click_executor.py +587 -0
  113. pdd/server/executor.py +338 -0
  114. pdd/server/jobs.py +661 -0
  115. pdd/server/models.py +241 -0
  116. pdd/server/routes/__init__.py +31 -0
  117. pdd/server/routes/architecture.py +451 -0
  118. pdd/server/routes/auth.py +364 -0
  119. pdd/server/routes/commands.py +929 -0
  120. pdd/server/routes/config.py +42 -0
  121. pdd/server/routes/files.py +603 -0
  122. pdd/server/routes/prompts.py +1322 -0
  123. pdd/server/routes/websocket.py +473 -0
  124. pdd/server/security.py +243 -0
  125. pdd/server/terminal_spawner.py +209 -0
  126. pdd/server/token_counter.py +222 -0
  127. pdd/summarize_directory.py +236 -237
  128. pdd/sync_animation.py +8 -4
  129. pdd/sync_determine_operation.py +329 -47
  130. pdd/sync_main.py +272 -28
  131. pdd/sync_orchestration.py +136 -75
  132. pdd/template_expander.py +161 -0
  133. pdd/templates/architecture/architecture_json.prompt +41 -46
  134. pdd/trace.py +1 -1
  135. pdd/track_cost.py +0 -13
  136. pdd/unfinished_prompt.py +2 -1
  137. pdd/update_main.py +23 -5
  138. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
  139. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  140. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  141. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  142. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  143. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
  144. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
@@ -1,193 +1,190 @@
1
- import sys
1
+ from __future__ import annotations
2
2
  import ast
3
- from typing import Tuple, Optional
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
- from rich import print as rprint
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 _validate_python_syntax(code: str) -> Tuple[bool, Optional[str]]:
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 True, None
22
- except SyntaxError as e:
23
- return False, f"SyntaxError at line {e.lineno}: {e.msg}"
24
-
25
-
26
- def _try_fix_json_garbage(code: str) -> Optional[str]:
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
- # If we find a line that's clearly JSON garbage
43
- if any(pattern in line for pattern in json_patterns):
44
- # Go back and find where valid Python ends
45
- for j in range(i - 1, -1, -1):
46
- candidate = '\n'.join(lines[:j + 1])
47
- is_valid, _ = _validate_python_syntax(candidate)
48
- if is_valid:
49
- return candidate
50
- break
51
-
52
- # If line is just "}" or "]", likely JSON ending
53
- if line in ['}', ']', '},', '],']:
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
- # If line ends with '", (JSON string end)
57
- if line.endswith('",') or line.endswith('"'):
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
- :param ctx: Click context containing command-line parameters.
69
- :param prompt_file: Path to the prompt file that generated the code.
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
- # Construct file paths
76
- input_file_paths = {
77
- "prompt_file": prompt_file,
78
- "code_file": code_file
79
- }
80
- command_options = {
81
- "output": output
82
- }
83
- resolved_config, input_strings, output_file_paths, language = construct_paths(
84
- input_file_paths=input_file_paths,
85
- force=ctx.obj.get('force', False),
86
- quiet=ctx.obj.get('quiet', False),
87
- command="example",
88
- command_options=command_options,
89
- context_override=ctx.obj.get('context'),
90
- confirm_callback=ctx.obj.get('confirm_callback')
91
- )
92
-
93
- # Load input files
94
- prompt_content = input_strings["prompt_file"]
95
- code_content = input_strings["code_file"]
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
- # Save results - if output is a directory, use resolved file path from construct_paths
126
- if output is None:
127
- final_output_path = resolved_output
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
- try:
130
- is_dir_hint = output.endswith('/')
131
- except Exception:
132
- is_dir_hint = False
133
- if is_dir_hint or (Path(output).exists() and Path(output).is_dir()):
134
- final_output_path = resolved_output
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
- final_output_path = output
137
- if final_output_path and example_code is not None:
138
- # Validate Python syntax before saving
139
- if language == "python":
140
- is_valid, error_msg = _validate_python_syntax(example_code)
141
- if not is_valid:
142
- if not ctx.obj.get('quiet', False):
143
- rprint(f"[yellow]Warning: Generated code has syntax error: {error_msg}[/yellow]")
144
- rprint("[yellow]Attempting to fix JSON garbage pattern...[/yellow]")
145
-
146
- # Try to fix JSON garbage at end of file
147
- fixed_code = _try_fix_json_garbage(example_code)
148
- if fixed_code:
149
- is_valid_fixed, _ = _validate_python_syntax(fixed_code)
150
- if is_valid_fixed:
151
- if not ctx.obj.get('quiet', False):
152
- rprint("[green]Successfully removed garbage and fixed syntax.[/green]")
153
- example_code = fixed_code
154
- else:
155
- rprint("[red]Could not fix syntax error. Saving as-is.[/red]")
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
- rprint("[red]Could not detect fixable pattern. Saving as-is.[/red]")
158
-
159
- with open(final_output_path, 'w') as f:
160
- f.write(example_code)
161
- elif final_output_path and example_code is None:
162
- # Raise error instead of just warning
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
- # Provide user feedback
166
- if not ctx.obj.get('quiet', False):
167
- if example_code is not None:
168
- rprint("[bold green]Example code generated successfully.[/bold green]")
169
- rprint(f"[bold]Model used:[/bold] {model_name}")
170
- rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
171
- if final_output_path and example_code is not None:
172
- rprint(f"[bold]Example code saved to:[/bold] {final_output_path}")
173
- else:
174
- rprint("[bold red]Example code generation failed.[/bold red]")
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
- rprint(f"[bold red]Error:[/bold red] {str(e)}")
192
- # Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
193
- return "", 0.0, f"Error: {e}"
189
+ console.print(f"[bold red]Error:[/bold red] {str(e)}")
190
+ raise e
@@ -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="Overwrite existing files without asking for confirmation (commonly used with 'sync' to update generated outputs).",
197
+ help="Skip all interactive prompts (file overwrites, API key requests). Useful for CI/automation.",
198
198
  )
199
199
  @click.option(
200
200
  "--strength",
@@ -296,6 +296,8 @@ def cli(
296
296
 
297
297
  ctx.ensure_object(dict)
298
298
  ctx.obj["force"] = force
299
+ if force:
300
+ os.environ['PDD_FORCE'] = '1'
299
301
  # Only set strength/temperature if explicitly provided (not None)
300
302
  # This allows .get("key", default) to return the default when CLI didn't pass a value
301
303
  if strength is not None:
@@ -307,6 +309,9 @@ def cli(
307
309
  ctx.obj["output_cost"] = output_cost
308
310
  ctx.obj["review_examples"] = review_examples
309
311
  ctx.obj["local"] = local
312
+ # Propagate --local flag to environment for llm_invoke cloud detection
313
+ if local:
314
+ os.environ['PDD_FORCE_LOCAL'] = '1'
310
315
  # Use DEFAULT_TIME if time is not provided
311
316
  ctx.obj["time"] = time if time is not None else DEFAULT_TIME
312
317
  # Persist context override for downstream calls
@@ -441,7 +446,7 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
441
446
  console.print(f" [error]Step {i+1} ({command_name}):[/error] Command failed.")
442
447
  # Check if the result is the expected tuple structure from @track_cost or preprocess success
443
448
  elif isinstance(result_tuple, tuple) and len(result_tuple) == 3:
444
- _result_data, cost, model_name = result_tuple
449
+ result_data, cost, model_name = result_tuple
445
450
  total_cost += cost
446
451
  if not ctx.obj.get("quiet"):
447
452
  # Special handling for preprocess success message (check actual command name)
@@ -451,6 +456,25 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
451
456
  else:
452
457
  # Generic output using potentially "Unknown Command" name
453
458
  console.print(f" [info]Step {i+1} ({command_name}):[/info] Cost: ${cost:.6f}, Model: {model_name}")
459
+
460
+ # Display examples used for grounding
461
+ if isinstance(result_data, dict) and result_data.get("examplesUsed"):
462
+ console.print(" Examples used:")
463
+ for ex in result_data["examplesUsed"]:
464
+ slug = ex.get("slug", "unknown")
465
+ title = ex.get("title", "Untitled")
466
+ console.print(f" - {slug} (\"{title}\")")
467
+
468
+ # Handle dicts with examplesUsed (e.g. from commands not using track_cost but returning metadata)
469
+ elif isinstance(result_tuple, dict) and result_tuple.get("examplesUsed"):
470
+ if not ctx.obj.get("quiet"):
471
+ console.print(f" [info]Step {i+1} ({command_name}):[/info] Command completed.")
472
+ console.print(" Examples used:")
473
+ for ex in result_tuple["examplesUsed"]:
474
+ slug = ex.get("slug", "unknown")
475
+ title = ex.get("title", "Untitled")
476
+ console.print(f" - {slug} (\"{title}\")")
477
+
454
478
  else:
455
479
  # Handle unexpected return types if necessary
456
480
  if not ctx.obj.get("quiet"):
@@ -463,7 +487,7 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
463
487
  if any(res is not None and isinstance(res, tuple) and len(res) == 3 for res in normalized_results):
464
488
  console.print(f"[info]Total Estimated Cost:[/info] ${total_cost:.6f}")
465
489
  # 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
490
+ 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
491
  console.print("[warning]Note: Chain may have terminated early due to errors.[/warning]")
468
492
  console.print("[info]-------------------------------------[/info]")
469
493