pdd-cli 0.0.30__py3-none-any.whl → 0.0.32__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.

Potentially problematic release.


This version of pdd-cli might be problematic. Click here for more details.

pdd/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.0.30"
1
+ __version__ = "0.0.32"
2
2
 
3
3
  # Strength parameter used for LLM extraction across the codebase
4
4
  # Used in postprocessing, XML tagging, code generation, and other extraction operations. The module should have a large context window and be affordable.
@@ -6,9 +6,17 @@ EXTRACTION_STRENGTH = 0.9
6
6
 
7
7
  DEFAULT_STRENGTH = 0.9
8
8
 
9
+ DEFAULT_TEMPERATURE = 0.0
10
+
11
+ DEFAULT_TIME = 0.25
12
+
9
13
  """PDD - Prompt Driven Development"""
10
14
 
11
15
  # Define constants used across the package
12
16
  DEFAULT_LLM_MODEL = "gpt-4.1-nano"
17
+ # When going to production, set the following constants:
18
+ # REACT_APP_FIREBASE_API_KEY
19
+ # GITHUB_CLIENT_ID
20
+
21
+ # You can add other package-level initializations or imports here
13
22
 
14
- # You can add other package-level initializations or imports here
pdd/change_main.py CHANGED
@@ -31,6 +31,7 @@ def change_main(
31
31
  input_prompt_file: Optional[str],
32
32
  output: Optional[str],
33
33
  use_csv: bool,
34
+ budget: float,
34
35
  ) -> Tuple[str, float, str]:
35
36
  """
36
37
  Handles the core logic for the 'change' command.
@@ -46,6 +47,7 @@ def change_main(
46
47
  input_prompt_file: Path to the input prompt file (required in non-CSV mode).
47
48
  output: Optional output path (file or directory).
48
49
  use_csv: Flag indicating whether to use CSV mode.
50
+ budget: Budget for the operation.
49
51
 
50
52
  Returns:
51
53
  A tuple containing:
@@ -64,8 +66,6 @@ def change_main(
64
66
  quiet: bool = ctx.obj.get("quiet", False)
65
67
  strength: float = ctx.obj.get("strength", DEFAULT_STRENGTH)
66
68
  temperature: float = ctx.obj.get("temperature", 0.0)
67
- # Default budget to 5.0 if not specified - needed for process_csv_change
68
- budget: float = ctx.obj.get("budget", 5.0)
69
69
  # --- Get language and extension from context ---
70
70
  # These are crucial for knowing the target code file types, especially in CSV mode
71
71
  target_language: str = ctx.obj.get("language", "") # Get from context
pdd/cli.py CHANGED
@@ -239,17 +239,39 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
239
239
  default=None,
240
240
  help="Specify where to save the generated code (file or directory).",
241
241
  )
242
+ @click.option(
243
+ "--original-prompt",
244
+ "original_prompt_file_path",
245
+ type=click.Path(exists=True, dir_okay=False),
246
+ default=None,
247
+ help="Path to the original prompt file for incremental generation.",
248
+ )
249
+ @click.option(
250
+ "--force-incremental",
251
+ "force_incremental_flag",
252
+ is_flag=True,
253
+ default=False,
254
+ help="Force incremental generation even if full regeneration is suggested.",
255
+ )
242
256
  @click.pass_context
243
257
  @track_cost
244
- def generate(ctx: click.Context, prompt_file: str, output: Optional[str]) -> Optional[Tuple[str, float, str]]: # Modified return type
258
+ def generate(
259
+ ctx: click.Context,
260
+ prompt_file: str,
261
+ output: Optional[str],
262
+ original_prompt_file_path: Optional[str],
263
+ force_incremental_flag: bool,
264
+ ) -> Optional[Tuple[str, float, str]]: # Modified return type
245
265
  """Create runnable code from a prompt file."""
246
266
  quiet = ctx.obj.get("quiet", False)
247
267
  command_name = "generate"
248
268
  try:
249
- generated_code, total_cost, model_name = code_generator_main(
269
+ generated_code, incremental, total_cost, model_name = code_generator_main(
250
270
  ctx=ctx,
251
271
  prompt_file=prompt_file,
252
272
  output=output,
273
+ original_prompt_file_path=original_prompt_file_path,
274
+ force_incremental_flag=force_incremental_flag,
253
275
  )
254
276
  return generated_code, total_cost, model_name
255
277
  except Exception as e:
@@ -567,6 +589,13 @@ def split(
567
589
  @click.argument("change_prompt_file", type=click.Path(exists=True, dir_okay=False))
568
590
  @click.argument("input_code", type=click.Path(exists=True)) # Can be file or dir
569
591
  @click.argument("input_prompt_file", type=click.Path(exists=True, dir_okay=False), required=False)
592
+ @click.option(
593
+ "--budget",
594
+ type=float,
595
+ default=5.0,
596
+ show_default=True,
597
+ help="Maximum cost allowed for the change process.",
598
+ )
570
599
  @click.option(
571
600
  "--output",
572
601
  type=click.Path(writable=True),
@@ -589,6 +618,7 @@ def change(
589
618
  input_prompt_file: Optional[str],
590
619
  output: Optional[str],
591
620
  use_csv: bool,
621
+ budget: float,
592
622
  ) -> Optional[Tuple[str | Dict, float, str]]: # Modified return type
593
623
  """Modify prompt(s) based on change instructions."""
594
624
  quiet = ctx.obj.get("quiet", False)
@@ -617,6 +647,7 @@ def change(
617
647
  input_prompt_file=input_prompt_file,
618
648
  output=output,
619
649
  use_csv=use_csv,
650
+ budget=budget,
620
651
  )
621
652
  return result_data, total_cost, model_name
622
653
  except (click.UsageError, Exception) as e: # Catch specific and general exceptions
@@ -1009,7 +1040,7 @@ def verify(
1009
1040
  quiet = ctx.obj.get("quiet", False)
1010
1041
  command_name = "verify"
1011
1042
  try:
1012
- success, final_program, final_code, attempts, cost, model = fix_verification_main(
1043
+ success, final_program, final_code, attempts, total_cost_value, model_name_value = fix_verification_main(
1013
1044
  ctx=ctx,
1014
1045
  prompt_file=prompt_file,
1015
1046
  code_file=code_file,
@@ -1029,7 +1060,7 @@ def verify(
1029
1060
  "verified_program_path": output_program,
1030
1061
  "results_log_path": output_results,
1031
1062
  }
1032
- return result_data, cost, model
1063
+ return result_data, total_cost_value, model_name_value
1033
1064
  except Exception as e:
1034
1065
  handle_error(e, command_name, quiet)
1035
1066
  return None # Return None on failure
@@ -1,124 +1,354 @@
1
- import sys
2
- from typing import Tuple, Optional
3
- import click
4
- from rich import print as rprint
1
+ import os
2
+ import asyncio
3
+ import json
4
+ import pathlib
5
+ import shlex
6
+ import subprocess
7
+ import requests
8
+ from typing import Optional, Tuple, Dict, Any, List
5
9
 
6
- import requests # <── Added at top level so the tests can patch pdd.code_generator_main.requests
10
+ import click
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.text import Text
7
14
 
15
+ # Relative imports for PDD package structure
16
+ from . import DEFAULT_STRENGTH, DEFAULT_TIME, EXTRACTION_STRENGTH # Assuming these are in __init__.py
8
17
  from .construct_paths import construct_paths
9
- from .code_generator import code_generator
10
- from .get_jwt_token import get_jwt_token
11
- from .preprocess import preprocess
18
+ from .preprocess import preprocess as pdd_preprocess
19
+ from .code_generator import code_generator as local_code_generator_func
20
+ from .incremental_code_generator import incremental_code_generator as incremental_code_generator_func
21
+ from .get_jwt_token import get_jwt_token, AuthError, NetworkError, TokenError, UserCancelledError, RateLimitError
22
+
23
+ # Environment variable names for Firebase/GitHub auth
24
+ FIREBASE_API_KEY_ENV_VAR = "NEXT_PUBLIC_FIREBASE_API_KEY"
25
+ GITHUB_CLIENT_ID_ENV_VAR = "GITHUB_CLIENT_ID"
26
+ PDD_APP_NAME = "PDD Code Generator"
27
+
28
+ # Cloud function URL
29
+ CLOUD_GENERATE_URL = "https://us-central1-prompt-driven-development.cloudfunctions.net/generateCode"
30
+ CLOUD_REQUEST_TIMEOUT = 200 # seconds
31
+
32
+ console = Console()
33
+
34
+ # --- Git Helper Functions ---
35
+ def _run_git_command(command: List[str], cwd: Optional[str] = None) -> Tuple[int, str, str]:
36
+ """Runs a git command and returns (return_code, stdout, stderr)."""
37
+ try:
38
+ process = subprocess.run(command, capture_output=True, text=True, check=False, cwd=cwd, encoding='utf-8')
39
+ return process.returncode, process.stdout.strip(), process.stderr.strip()
40
+ except FileNotFoundError:
41
+ return -1, "", "Git command not found. Ensure git is installed and in your PATH."
42
+ except Exception as e:
43
+ return -2, "", f"Error running git command {' '.join(command)}: {e}"
44
+
45
+ def is_git_repository(path: Optional[str] = None) -> bool:
46
+ """Checks if the given path (or current dir) is a git repository."""
47
+ start_path = pathlib.Path(path).resolve() if path else pathlib.Path.cwd()
48
+ # Check for .git in current or any parent directory
49
+ current_path = start_path
50
+ while True:
51
+ if (current_path / ".git").is_dir():
52
+ # Verify it's the root of the work tree or inside it
53
+ returncode, stdout, _ = _run_git_command(["git", "rev-parse", "--is-inside-work-tree"], cwd=str(start_path))
54
+ return returncode == 0 and stdout == "true"
55
+ parent = current_path.parent
56
+ if parent == current_path: # Reached root directory
57
+ break
58
+ current_path = parent
59
+ return False
12
60
 
13
- def code_generator_main(ctx: click.Context, prompt_file: str, output: Optional[str]) -> Tuple[str, float, str]:
14
- """
15
- Main function to generate code from a prompt file.
16
61
 
17
- :param ctx: Click context containing command-line parameters.
18
- :param prompt_file: Path to the prompt file used to generate the code.
19
- :param output: Optional path to save the generated code.
20
- :return: A tuple containing the generated code, total cost, and model name used.
62
+ def get_git_committed_content(file_path: str) -> Optional[str]:
63
+ """Gets the content of the file as it was in the last commit (HEAD)."""
64
+ abs_file_path = pathlib.Path(file_path).resolve()
65
+ if not is_git_repository(str(abs_file_path.parent)):
66
+ return None
67
+
68
+ returncode_rev, git_root_str, stderr_rev = _run_git_command(["git", "rev-parse", "--show-toplevel"], cwd=str(abs_file_path.parent))
69
+ if returncode_rev != 0:
70
+ # console.print(f"[yellow]Git (rev-parse) warning for {file_path}: {stderr_rev}[/yellow]")
71
+ return None
72
+
73
+ git_root = pathlib.Path(git_root_str)
74
+ try:
75
+ relative_path = abs_file_path.relative_to(git_root)
76
+ except ValueError:
77
+ # console.print(f"[yellow]File {file_path} is not under git root {git_root}.[/yellow]")
78
+ return None
79
+
80
+ returncode, stdout, stderr = _run_git_command(["git", "show", f"HEAD:{relative_path.as_posix()}"], cwd=str(git_root))
81
+ if returncode == 0:
82
+ return stdout
83
+ else:
84
+ # File might be new, or other git error. Not necessarily an error for this function's purpose.
85
+ # if "does not exist" not in stderr and "exists on disk, but not in 'HEAD'" not in stderr and console.is_terminal:
86
+ # console.print(f"[yellow]Git (show) warning for {file_path}: {stderr}[/yellow]")
87
+ return None
88
+
89
+ def get_file_git_status(file_path: str) -> str:
90
+ """Gets the git status of a single file (e.g., ' M', '??', 'A '). Empty if clean."""
91
+ abs_file_path = pathlib.Path(file_path).resolve()
92
+ if not is_git_repository(str(abs_file_path.parent)) or not abs_file_path.exists():
93
+ return ""
94
+ returncode, stdout, _ = _run_git_command(["git", "status", "--porcelain", str(abs_file_path)], cwd=str(abs_file_path.parent))
95
+ if returncode == 0:
96
+ # stdout might be " M path/to/file" or "?? path/to/file"
97
+ # We only want the status codes part
98
+ status_part = stdout.split(str(abs_file_path.name))[0].strip() if str(abs_file_path.name) in stdout else stdout.strip()
99
+ return status_part
100
+ return ""
101
+
102
+ def git_add_files(file_paths: List[str], verbose: bool = False) -> bool:
103
+ """Stages the given files using 'git add'."""
104
+ if not file_paths:
105
+ return True
106
+
107
+ # Resolve paths and ensure they are absolute for git command
108
+ abs_paths = [str(pathlib.Path(fp).resolve()) for fp in file_paths]
109
+
110
+ # Determine common parent directory to run git command from, or git root
111
+ # For simplicity, assume they are in the same repo and run from one of their parents
112
+ if not is_git_repository(str(pathlib.Path(abs_paths[0]).parent)):
113
+ if verbose:
114
+ console.print(f"[yellow]Cannot stage files: {abs_paths[0]} is not in a git repository.[/yellow]")
115
+ return False
116
+
117
+ returncode, _, stderr = _run_git_command(["git", "add"] + abs_paths, cwd=str(pathlib.Path(abs_paths[0]).parent))
118
+ if returncode == 0:
119
+ if verbose:
120
+ console.print(f"Successfully staged: [cyan]{', '.join(abs_paths)}[/cyan]")
121
+ return True
122
+ else:
123
+ console.print(f"[red]Error staging files with git:[/red] {stderr}")
124
+ return False
125
+ # --- End Git Helper Functions ---
126
+
127
+
128
+ def code_generator_main(
129
+ ctx: click.Context,
130
+ prompt_file: str,
131
+ output: Optional[str],
132
+ original_prompt_file_path: Optional[str],
133
+ force_incremental_flag: bool,
134
+ ) -> Tuple[str, bool, float, str]:
135
+ """
136
+ CLI wrapper for generating code from prompts. Handles full and incremental generation,
137
+ local vs. cloud execution, and output.
21
138
  """
139
+ cli_params = ctx.obj or {}
140
+ is_local_execution_preferred = cli_params.get('local', False)
141
+ strength = cli_params.get('strength', DEFAULT_STRENGTH)
142
+ temperature = cli_params.get('temperature', 0.0)
143
+ time_budget = cli_params.get('time', DEFAULT_TIME)
144
+ verbose = cli_params.get('verbose', False)
145
+ force_overwrite = cli_params.get('force', False)
146
+ quiet = cli_params.get('quiet', False)
147
+
148
+ generated_code_content: Optional[str] = None
149
+ was_incremental_operation = False
150
+ total_cost = 0.0
151
+ model_name = "unknown"
152
+
153
+ input_file_paths_dict: Dict[str, str] = {"prompt_file": prompt_file}
154
+ if original_prompt_file_path:
155
+ input_file_paths_dict["original_prompt_file"] = original_prompt_file_path
156
+
157
+ command_options: Dict[str, Any] = {"output": output}
158
+
22
159
  try:
23
- # Construct file paths
24
- input_file_paths = {
25
- "prompt_file": prompt_file
26
- }
27
- command_options = {
28
- "output": output
29
- }
30
160
  input_strings, output_file_paths, language = construct_paths(
31
- input_file_paths=input_file_paths,
32
- force=ctx.obj.get('force', False),
33
- quiet=ctx.obj.get('quiet', False),
161
+ input_file_paths=input_file_paths_dict,
162
+ force=force_overwrite,
163
+ quiet=quiet,
34
164
  command="generate",
35
- command_options=command_options
165
+ command_options=command_options,
36
166
  )
37
-
38
- # Load input file
39
167
  prompt_content = input_strings["prompt_file"]
168
+ output_path = output_file_paths.get("output")
169
+
170
+ except FileNotFoundError as e:
171
+ console.print(f"[red]Error: Input file not found: {e.filename}[/red]")
172
+ return "", False, 0.0, "error"
173
+ except Exception as e:
174
+ console.print(f"[red]Error during path construction: {e}[/red]")
175
+ return "", False, 0.0, "error"
176
+
177
+ can_attempt_incremental = False
178
+ existing_code_content: Optional[str] = None
179
+ original_prompt_content_for_incremental: Optional[str] = None
180
+
181
+ if output_path and pathlib.Path(output_path).exists():
182
+ try:
183
+ existing_code_content = pathlib.Path(output_path).read_text(encoding="utf-8")
184
+ except Exception as e:
185
+ console.print(f"[yellow]Warning: Could not read existing output file {output_path}: {e}[/yellow]")
186
+ existing_code_content = None
187
+
188
+ if existing_code_content is not None:
189
+ if "original_prompt_file" in input_strings:
190
+ original_prompt_content_for_incremental = input_strings["original_prompt_file"]
191
+ can_attempt_incremental = True
192
+ if verbose:
193
+ console.print(f"Using specified original prompt: [cyan]{original_prompt_file_path}[/cyan]")
194
+ elif is_git_repository(str(pathlib.Path(prompt_file).parent)):
195
+ original_prompt_git_content = get_git_committed_content(prompt_file)
196
+ if original_prompt_git_content is not None:
197
+ original_prompt_content_for_incremental = original_prompt_git_content
198
+ can_attempt_incremental = True
199
+ if verbose:
200
+ console.print(f"Using last committed version of [cyan]{prompt_file}[/cyan] as original prompt.")
201
+ elif verbose:
202
+ console.print(f"[yellow]Warning: Could not find committed version of {prompt_file} in git for incremental generation.[/yellow]")
203
+
204
+ if force_incremental_flag and existing_code_content:
205
+ if not (original_prompt_content_for_incremental or "original_prompt_file" in input_strings): # Check if original prompt is actually available
206
+ console.print(
207
+ "[yellow]Warning: --incremental flag used, but original prompt could not be determined. "
208
+ "Falling back to full generation.[/yellow]"
209
+ )
210
+ else:
211
+ can_attempt_incremental = True
212
+
213
+ if force_incremental_flag and (not output_path or not pathlib.Path(output_path).exists()):
214
+ console.print(
215
+ "[yellow]Warning: --incremental flag used, but output file does not exist or path not specified. "
216
+ "Performing full generation.[/yellow]"
217
+ )
218
+ can_attempt_incremental = False
219
+
220
+ try:
221
+ if can_attempt_incremental and existing_code_content is not None and original_prompt_content_for_incremental is not None:
222
+ if verbose:
223
+ console.print(Panel("Attempting incremental code generation...", title="[blue]Mode[/blue]", expand=False))
40
224
 
41
- # Generate code
42
- strength = ctx.obj.get('strength', 0.5)
43
- temperature = ctx.obj.get('temperature', 0.0)
44
- verbose = not ctx.obj.get('quiet', False)
45
- local = ctx.obj.get('local', False)
46
-
47
- if local:
48
- print("Running in local mode")
49
- # Local execution
50
- generated_code, total_cost, model_name = code_generator(
51
- prompt_content,
52
- language,
53
- strength,
54
- temperature,
55
- verbose=verbose
225
+ if is_git_repository(str(pathlib.Path(prompt_file).parent)):
226
+ files_to_stage_for_rollback: List[str] = []
227
+ paths_to_check = [pathlib.Path(prompt_file).resolve()]
228
+ if output_path and pathlib.Path(output_path).exists():
229
+ paths_to_check.append(pathlib.Path(output_path).resolve())
230
+
231
+ for p_to_check in paths_to_check:
232
+ if not p_to_check.exists(): continue
233
+
234
+ is_untracked = get_file_git_status(str(p_to_check)).startswith("??")
235
+ # Check if different from HEAD or untracked
236
+ is_different_from_head_rc = 1 if is_untracked else _run_git_command(["git", "diff", "--quiet", "HEAD", "--", str(p_to_check)], cwd=str(p_to_check.parent))[0]
237
+
238
+ if is_different_from_head_rc != 0: # Different from HEAD or untracked
239
+ files_to_stage_for_rollback.append(str(p_to_check))
240
+
241
+ if files_to_stage_for_rollback:
242
+ git_add_files(files_to_stage_for_rollback, verbose=verbose)
243
+
244
+ generated_code_content, was_incremental_operation, total_cost, model_name = incremental_code_generator_func(
245
+ original_prompt=original_prompt_content_for_incremental,
246
+ new_prompt=prompt_content,
247
+ existing_code=existing_code_content,
248
+ language=language,
249
+ strength=strength,
250
+ temperature=temperature,
251
+ time=time_budget,
252
+ force_incremental=force_incremental_flag,
253
+ verbose=verbose,
254
+ preprocess_prompt=True
56
255
  )
57
- else:
58
- # Cloud execution
59
- try:
60
- import asyncio
61
- import os
62
- # Get JWT token for cloud authentication
63
- jwt_token = asyncio.run(get_jwt_token(
64
- firebase_api_key=os.environ.get("REACT_APP_FIREBASE_API_KEY"),
65
- github_client_id=os.environ.get("GITHUB_CLIENT_ID"),
66
- app_name="PDD Code Generator"
67
- ))
68
- # Call cloud code generator
69
- headers = {
70
- "Authorization": f"Bearer {jwt_token}",
71
- "Content-Type": "application/json"
72
- }
73
- # Preprocess the prompt
74
- processed_prompt = preprocess(prompt_content, recursive=False, double_curly_brackets=True)
256
+
257
+ if not was_incremental_operation:
75
258
  if verbose:
76
- print(f"Processed prompt: {processed_prompt}")
77
- data = {
78
- "promptContent": processed_prompt,
79
- "language": language,
80
- "strength": strength,
81
- "temperature": temperature,
82
- "verbose": verbose
83
- }
84
- response = requests.post(
85
- "https://us-central1-prompt-driven-development.cloudfunctions.net/generateCode",
86
- headers=headers,
87
- json=data
88
- )
89
- response.raise_for_status()
90
- result = response.json()
91
- generated_code = result["generatedCode"]
92
- total_cost = result["totalCost"]
93
- model_name = result["modelName"]
94
-
95
- except Exception as e:
96
- if not ctx.obj.get('quiet', False):
97
- rprint("[bold red]Cloud execution failed, falling back to local mode[/bold red]")
98
- generated_code, total_cost, model_name = code_generator(
99
- prompt_content,
100
- language,
101
- strength,
102
- temperature,
103
- verbose=verbose
104
- )
259
+ console.print(Panel("Incremental generator suggested full regeneration. Falling back.", title="[yellow]Fallback[/yellow]", expand=False))
260
+ elif verbose:
261
+ console.print(Panel(f"Incremental update successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Incremental Success[/green]", expand=False))
105
262
 
106
- # Save results
107
- if output_file_paths["output"]:
108
- with open(output_file_paths["output"], 'w') as f:
109
- f.write(generated_code)
263
+ if not was_incremental_operation: # Full generation path
264
+ if verbose:
265
+ console.print(Panel("Performing full code generation...", title="[blue]Mode[/blue]", expand=False))
266
+
267
+ current_execution_is_local = is_local_execution_preferred
268
+
269
+ if not current_execution_is_local:
270
+ if verbose: console.print("Attempting cloud code generation...")
271
+
272
+ processed_prompt_for_cloud = pdd_preprocess(prompt_content, recursive=True, double_curly_brackets=True, exclude_keys=[])
273
+ if verbose: console.print(Panel(Text(processed_prompt_for_cloud, overflow="fold"), title="[cyan]Preprocessed Prompt for Cloud[/cyan]", expand=False))
274
+
275
+ jwt_token: Optional[str] = None
276
+ try:
277
+ firebase_api_key_val = os.environ.get(FIREBASE_API_KEY_ENV_VAR)
278
+ github_client_id_val = os.environ.get(GITHUB_CLIENT_ID_ENV_VAR)
110
279
 
111
- # Provide user feedback
112
- if not ctx.obj.get('quiet', False):
113
- rprint("[bold green]Code generation completed successfully.[/bold green]")
114
- rprint(f"[bold]Model used:[/bold] {model_name}")
115
- rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
116
- if output:
117
- rprint(f"[bold]Code saved to:[/bold] {output_file_paths['output']}")
280
+ if not firebase_api_key_val: raise AuthError(f"{FIREBASE_API_KEY_ENV_VAR} not set.")
281
+ if not github_client_id_val: raise AuthError(f"{GITHUB_CLIENT_ID_ENV_VAR} not set.")
118
282
 
119
- return generated_code, total_cost, model_name
283
+ jwt_token = asyncio.run(get_jwt_token(
284
+ firebase_api_key=firebase_api_key_val,
285
+ github_client_id=github_client_id_val,
286
+ app_name=PDD_APP_NAME
287
+ ))
288
+ except (AuthError, NetworkError, TokenError, UserCancelledError, RateLimitError) as e:
289
+ console.print(f"[yellow]Cloud authentication/token error: {e}. Falling back to local execution.[/yellow]")
290
+ current_execution_is_local = True
291
+ except Exception as e:
292
+ console.print(f"[yellow]Unexpected error during cloud authentication: {e}. Falling back to local execution.[/yellow]")
293
+ current_execution_is_local = True
294
+
295
+ if jwt_token and not current_execution_is_local:
296
+ payload = {"promptContent": processed_prompt_for_cloud, "language": language, "strength": strength, "temperature": temperature, "verbose": verbose}
297
+ headers = {"Authorization": f"Bearer {jwt_token}", "Content-Type": "application/json"}
298
+ try:
299
+ response = requests.post(CLOUD_GENERATE_URL, json=payload, headers=headers, timeout=CLOUD_REQUEST_TIMEOUT)
300
+ response.raise_for_status()
301
+
302
+ response_data = response.json()
303
+ generated_code_content = response_data.get("generatedCode")
304
+ total_cost = float(response_data.get("totalCost", 0.0))
305
+ model_name = response_data.get("modelName", "cloud_model")
306
+
307
+ if generated_code_content is None:
308
+ console.print("[yellow]Cloud execution returned no code. Falling back to local.[/yellow]")
309
+ current_execution_is_local = True
310
+ elif verbose:
311
+ console.print(Panel(f"Cloud generation successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Cloud Success[/green]", expand=False))
312
+ except requests.exceptions.Timeout:
313
+ console.print(f"[yellow]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s). Falling back to local.[/yellow]")
314
+ current_execution_is_local = True
315
+ except requests.exceptions.HTTPError as e:
316
+ err_content = e.response.text[:200] if e.response else "No response content"
317
+ console.print(f"[yellow]Cloud HTTP error ({e.response.status_code}): {err_content}. Falling back to local.[/yellow]")
318
+ current_execution_is_local = True
319
+ except requests.exceptions.RequestException as e:
320
+ console.print(f"[yellow]Cloud network error: {e}. Falling back to local.[/yellow]")
321
+ current_execution_is_local = True
322
+ except json.JSONDecodeError:
323
+ console.print("[yellow]Cloud returned invalid JSON. Falling back to local.[/yellow]")
324
+ current_execution_is_local = True
325
+
326
+ if current_execution_is_local:
327
+ if verbose: console.print("Executing code generator locally...")
328
+ generated_code_content, total_cost, model_name = local_code_generator_func(
329
+ prompt=prompt_content, language=language, strength=strength,
330
+ temperature=temperature, verbose=verbose
331
+ )
332
+ if verbose:
333
+ console.print(Panel(f"Local generation successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Local Success[/green]", expand=False))
334
+
335
+ if generated_code_content is not None:
336
+ if output_path:
337
+ p_output = pathlib.Path(output_path)
338
+ p_output.parent.mkdir(parents=True, exist_ok=True)
339
+ p_output.write_text(generated_code_content, encoding="utf-8")
340
+ if verbose or not quiet:
341
+ console.print(f"Generated code saved to: [green]{p_output.resolve()}[/green]")
342
+ elif not quiet: # No output path, print to console if not quiet
343
+ console.print(Panel(Text(generated_code_content, overflow="fold"), title="[cyan]Generated Code[/cyan]", expand=True))
344
+ else:
345
+ console.print("[red]Error: Code generation failed. No code was produced.[/red]")
346
+ return "", was_incremental_operation, total_cost, model_name or "error"
120
347
 
121
348
  except Exception as e:
122
- if not ctx.obj.get('quiet', False):
123
- rprint(f"[bold red]Error:[/bold red] {str(e)}")
124
- sys.exit(1)
349
+ console.print(f"[red]An unexpected error occurred: {e}[/red]")
350
+ import traceback
351
+ if verbose: console.print(traceback.format_exc())
352
+ return "", was_incremental_operation, total_cost, "error"
353
+
354
+ return generated_code_content or "", was_incremental_operation, total_cost, model_name
pdd/data/llm_model.csv CHANGED
@@ -5,7 +5,7 @@ Anthropic,claude-3-5-haiku-20241022,.8,4,1261,,ANTHROPIC_API_KEY,0,True,none
5
5
  OpenAI,deepseek/deepseek-chat,.27,1.1,1353,https://api.deepseek.com/beta,DEEPSEEK_API_KEY,0,False,none
6
6
  Google,vertex_ai/gemini-2.5-flash-preview-04-17,0.15,0.6,1330,,VERTEX_CREDENTIALS,0,True,effort
7
7
  Google,gemini-2.5-pro-exp-03-25,1.25,10.0,1360,,GOOGLE_API_KEY,0,True,none
8
- Anthropic,claude-3-7-sonnet-20250219,3.0,15.0,1340,,ANTHROPIC_API_KEY,64000,True,budget
8
+ Anthropic,claude-sonnet-4-20250514,3.0,15.0,1340,,ANTHROPIC_API_KEY,64000,True,budget
9
9
  Google,vertex_ai/gemini-2.5-pro-preview-05-06,1.25,10.0,1361,,VERTEX_CREDENTIALS,0,True,none
10
10
  OpenAI,o4-mini,1.1,4.4,1333,,OPENAI_API_KEY,0,True,effort
11
11
  OpenAI,o3,10.0,40.0,1389,,OPENAI_API_KEY,0,True,effort