pdd-cli 0.0.45__py3-none-any.whl → 0.0.90__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 (114) hide show
  1. pdd/__init__.py +4 -4
  2. pdd/agentic_common.py +863 -0
  3. pdd/agentic_crash.py +534 -0
  4. pdd/agentic_fix.py +1179 -0
  5. pdd/agentic_langtest.py +162 -0
  6. pdd/agentic_update.py +370 -0
  7. pdd/agentic_verify.py +183 -0
  8. pdd/auto_deps_main.py +15 -5
  9. pdd/auto_include.py +63 -5
  10. pdd/bug_main.py +3 -2
  11. pdd/bug_to_unit_test.py +2 -0
  12. pdd/change_main.py +11 -4
  13. pdd/cli.py +22 -1181
  14. pdd/cmd_test_main.py +73 -21
  15. pdd/code_generator.py +58 -18
  16. pdd/code_generator_main.py +672 -25
  17. pdd/commands/__init__.py +42 -0
  18. pdd/commands/analysis.py +248 -0
  19. pdd/commands/fix.py +140 -0
  20. pdd/commands/generate.py +257 -0
  21. pdd/commands/maintenance.py +174 -0
  22. pdd/commands/misc.py +79 -0
  23. pdd/commands/modify.py +230 -0
  24. pdd/commands/report.py +144 -0
  25. pdd/commands/templates.py +215 -0
  26. pdd/commands/utility.py +110 -0
  27. pdd/config_resolution.py +58 -0
  28. pdd/conflicts_main.py +8 -3
  29. pdd/construct_paths.py +258 -82
  30. pdd/context_generator.py +10 -2
  31. pdd/context_generator_main.py +113 -11
  32. pdd/continue_generation.py +47 -7
  33. pdd/core/__init__.py +0 -0
  34. pdd/core/cli.py +503 -0
  35. pdd/core/dump.py +554 -0
  36. pdd/core/errors.py +63 -0
  37. pdd/core/utils.py +90 -0
  38. pdd/crash_main.py +44 -11
  39. pdd/data/language_format.csv +71 -63
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +330 -76
  43. pdd/fix_error_loop.py +207 -61
  44. pdd/fix_errors_from_unit_tests.py +4 -3
  45. pdd/fix_main.py +75 -18
  46. pdd/fix_verification_errors.py +12 -100
  47. pdd/fix_verification_errors_loop.py +306 -272
  48. pdd/fix_verification_main.py +28 -9
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +9 -2
  52. pdd/get_run_command.py +73 -0
  53. pdd/get_test_command.py +68 -0
  54. pdd/git_update.py +70 -19
  55. pdd/incremental_code_generator.py +2 -2
  56. pdd/insert_includes.py +11 -3
  57. pdd/llm_invoke.py +1269 -103
  58. pdd/load_prompt_template.py +36 -10
  59. pdd/pdd_completion.fish +25 -2
  60. pdd/pdd_completion.sh +30 -4
  61. pdd/pdd_completion.zsh +79 -4
  62. pdd/postprocess.py +10 -3
  63. pdd/preprocess.py +228 -15
  64. pdd/preprocess_main.py +8 -5
  65. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  66. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  67. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  68. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  69. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  70. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  71. pdd/prompts/auto_include_LLM.prompt +100 -905
  72. pdd/prompts/detect_change_LLM.prompt +122 -20
  73. pdd/prompts/example_generator_LLM.prompt +22 -1
  74. pdd/prompts/extract_code_LLM.prompt +5 -1
  75. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  76. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  77. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  78. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  79. pdd/prompts/fix_code_module_errors_LLM.prompt +4 -2
  80. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +8 -0
  81. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  82. pdd/prompts/generate_test_LLM.prompt +21 -6
  83. pdd/prompts/increase_tests_LLM.prompt +1 -5
  84. pdd/prompts/insert_includes_LLM.prompt +228 -108
  85. pdd/prompts/trace_LLM.prompt +25 -22
  86. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  87. pdd/prompts/update_prompt_LLM.prompt +22 -1
  88. pdd/pytest_output.py +127 -12
  89. pdd/render_mermaid.py +236 -0
  90. pdd/setup_tool.py +648 -0
  91. pdd/simple_math.py +2 -0
  92. pdd/split_main.py +3 -2
  93. pdd/summarize_directory.py +49 -6
  94. pdd/sync_determine_operation.py +543 -98
  95. pdd/sync_main.py +81 -31
  96. pdd/sync_orchestration.py +1334 -751
  97. pdd/sync_tui.py +848 -0
  98. pdd/template_registry.py +264 -0
  99. pdd/templates/architecture/architecture_json.prompt +242 -0
  100. pdd/templates/generic/generate_prompt.prompt +174 -0
  101. pdd/trace.py +168 -12
  102. pdd/trace_main.py +4 -3
  103. pdd/track_cost.py +151 -61
  104. pdd/unfinished_prompt.py +49 -3
  105. pdd/update_main.py +549 -67
  106. pdd/update_model_costs.py +2 -2
  107. pdd/update_prompt.py +19 -4
  108. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +19 -6
  109. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  110. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  111. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  112. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  113. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  114. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ import os
3
3
  import subprocess
4
4
  import click
5
5
  import logging
6
+ from pathlib import Path
6
7
  from typing import Optional, Tuple, List, Dict, Any
7
8
 
8
9
  # Use Rich for pretty printing to the console
@@ -116,6 +117,9 @@ def fix_verification_main(
116
117
  verification_program: Optional[str], # Only used if loop=True
117
118
  max_attempts: int = DEFAULT_MAX_ATTEMPTS,
118
119
  budget: float = DEFAULT_BUDGET,
120
+ agentic_fallback: bool = True,
121
+ strength: Optional[float] = None,
122
+ temperature: Optional[float] = None,
119
123
  ) -> Tuple[bool, str, str, int, float, str]:
120
124
  """
121
125
  CLI wrapper for the 'verify' command. Verifies code correctness by running
@@ -144,9 +148,9 @@ def fix_verification_main(
144
148
  - total_cost (float): Total cost incurred.
145
149
  - model_name (str): Name of the LLM used.
146
150
  """
147
- # Extract global options from context
148
- strength: float = ctx.obj.get('strength', DEFAULT_STRENGTH)
149
- temperature: float = ctx.obj.get('temperature', DEFAULT_TEMPERATURE)
151
+ # Extract global options from context (prefer passed parameters over ctx.obj)
152
+ strength: float = strength if strength is not None else ctx.obj.get('strength', DEFAULT_STRENGTH)
153
+ temperature: float = temperature if temperature is not None else ctx.obj.get('temperature', DEFAULT_TEMPERATURE)
150
154
  force: bool = ctx.obj.get('force', False)
151
155
  quiet: bool = ctx.obj.get('quiet', False)
152
156
  verbose: bool = ctx.obj.get('verbose', False)
@@ -204,6 +208,8 @@ def fix_verification_main(
204
208
  quiet=quiet,
205
209
  command="verify",
206
210
  command_options=command_options,
211
+ context_override=ctx.obj.get('context'),
212
+ confirm_callback=ctx.obj.get('confirm_callback')
207
213
  )
208
214
  output_code_path = output_file_paths.get("output_code")
209
215
  output_results_path = output_file_paths.get("output_results")
@@ -212,6 +218,9 @@ def fix_verification_main(
212
218
  if verbose:
213
219
  rich_print("[dim]Resolved output paths via construct_paths.[/dim]")
214
220
 
221
+ except click.Abort:
222
+ # User cancelled - re-raise to stop the sync loop
223
+ raise
215
224
  except Exception as e:
216
225
  # If the helper does not understand the "verify" command fall back.
217
226
  if "invalid command" in str(e).lower():
@@ -230,7 +239,8 @@ def fix_verification_main(
230
239
  input_strings["program_file"] = f.read()
231
240
  except FileNotFoundError as fe:
232
241
  rich_print(f"[bold red]Error:[/bold red] {fe}")
233
- sys.exit(1)
242
+ # Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
243
+ return False, "", "", 0, 0.0, f"FileNotFoundError: {fe}"
234
244
 
235
245
  # Pick or build output paths
236
246
  if output_code_path is None:
@@ -252,7 +262,8 @@ def fix_verification_main(
252
262
  if verbose:
253
263
  import traceback
254
264
  rich_print(Panel(traceback.format_exc(), title="Traceback", border_style="red"))
255
- sys.exit(1)
265
+ # Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
266
+ return False, "", "", 0, 0.0, f"Error: {e}"
256
267
 
257
268
  # --- Core Logic ---
258
269
  success: bool = False
@@ -273,6 +284,7 @@ def fix_verification_main(
273
284
  program_file=program_file, # Changed to pass the program_file path
274
285
  code_file=code_file, # Changed to pass the code_file path
275
286
  prompt=input_strings["prompt_file"], # Correctly passing prompt content
287
+ prompt_file=prompt_file,
276
288
  verification_program=verification_program, # Path to the verifier program
277
289
  strength=strength,
278
290
  temperature=temperature,
@@ -283,7 +295,8 @@ def fix_verification_main(
283
295
  # output_code_path should not be passed here
284
296
  # output_program_path should not be passed here
285
297
  verbose=verbose,
286
- program_args=[] # Pass an empty list for program_args
298
+ program_args=[], # Pass an empty list for program_args
299
+ agentic_fallback=agentic_fallback,
287
300
  )
288
301
  success = loop_results.get('success', False)
289
302
  final_program = loop_results.get('final_program', "") # Use .get for safety
@@ -407,7 +420,9 @@ def fix_verification_main(
407
420
  try:
408
421
  if verbose:
409
422
  rich_print(f"[cyan bold DEBUG] In fix_verification_main, ATTEMPTING to write code to: {output_code_path!r}")
410
- with open(output_code_path, "w") as f:
423
+ output_code_path_obj = Path(output_code_path)
424
+ output_code_path_obj.parent.mkdir(parents=True, exist_ok=True)
425
+ with open(output_code_path_obj, "w") as f:
411
426
  f.write(final_code)
412
427
  saved_code_path = output_code_path
413
428
  if not quiet:
@@ -427,7 +442,9 @@ def fix_verification_main(
427
442
  try:
428
443
  if verbose:
429
444
  rich_print(f"[cyan bold DEBUG] In fix_verification_main, ATTEMPTING to write program to: {output_program_path!r}")
430
- with open(output_program_path, "w") as f:
445
+ output_program_path_obj = Path(output_program_path)
446
+ output_program_path_obj.parent.mkdir(parents=True, exist_ok=True)
447
+ with open(output_program_path_obj, "w") as f:
431
448
  f.write(final_program)
432
449
  saved_program_path = output_program_path
433
450
  if not quiet:
@@ -437,7 +454,9 @@ def fix_verification_main(
437
454
 
438
455
  if not loop and output_results_path:
439
456
  try:
440
- with open(output_results_path, "w") as f:
457
+ output_results_path_obj = Path(output_results_path)
458
+ output_results_path_obj.parent.mkdir(parents=True, exist_ok=True)
459
+ with open(output_results_path_obj, "w") as f:
441
460
  f.write(results_log_content)
442
461
  saved_results_path = output_results_path
443
462
  if not quiet:
@@ -1,13 +1,18 @@
1
1
  import os
2
2
  import logging
3
- from typing import Dict, List, Optional
3
+ from typing import Dict, List, Literal, Optional
4
+
5
+ # Type alias for path resolution mode
6
+ PathResolutionMode = Literal["config_base", "cwd"]
4
7
 
5
8
  # Configure logging
6
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
7
9
  logger = logging.getLogger(__name__)
8
10
 
9
11
  # --- Configuration Data ---
10
12
 
13
+ # Default directory names
14
+ EXAMPLES_DIR = "examples"
15
+
11
16
  # Define the expected output keys for each command
12
17
  # Use underscores for keys as requested
13
18
  COMMAND_OUTPUT_KEYS: Dict[str, List[str]] = {
@@ -178,14 +183,19 @@ def generate_output_paths(
178
183
  basename: str,
179
184
  language: str,
180
185
  file_extension: str,
181
- context_config: Optional[Dict[str, str]] = None
186
+ context_config: Optional[Dict[str, str]] = None,
187
+ input_file_dir: Optional[str] = None,
188
+ input_file_dirs: Optional[Dict[str, str]] = None,
189
+ config_base_dir: Optional[str] = None,
190
+ path_resolution_mode: PathResolutionMode = "config_base",
182
191
  ) -> Dict[str, str]:
183
192
  """
184
193
  Generates the full, absolute output paths for a given PDD command.
185
194
 
186
- It prioritizes user-specified paths (--output options), then context
187
- configuration from .pddrc, then environment variables, and finally
188
- falls back to default naming conventions in the current working directory.
195
+ It prioritizes user-specified paths (--output options), then context
196
+ configuration from .pddrc, then environment variables, and finally
197
+ falls back to default naming conventions in the input file's directory
198
+ (or current working directory if input_file_dir is not provided).
189
199
 
190
200
  Args:
191
201
  command: The PDD command being executed (e.g., 'generate', 'fix').
@@ -199,6 +209,23 @@ def generate_output_paths(
199
209
  used when default patterns require it.
200
210
  context_config: Optional dictionary with context-specific paths from .pddrc
201
211
  configuration (e.g., {'generate_output_path': 'src/'}).
212
+ input_file_dir: Optional path to the input file's directory. When provided,
213
+ default output files will be placed in this directory instead
214
+ of the current working directory.
215
+ input_file_dirs: Optional dictionary mapping output keys to specific input
216
+ file directories. When provided, each output will use its
217
+ corresponding input file directory (e.g., {'output_code': 'src/main/java'}).
218
+ config_base_dir: Optional base directory to resolve relative `.pddrc` and
219
+ environment variable output paths. When set, relative
220
+ config paths resolve under this directory (typically the
221
+ directory containing `.pddrc`) instead of the input file
222
+ directory.
223
+ path_resolution_mode: Controls how relative paths from `.pddrc` and
224
+ environment variables are resolved. "config_base"
225
+ (default) resolves relative to config_base_dir,
226
+ "cwd" resolves relative to the current working
227
+ directory. Use "cwd" for sync command to ensure
228
+ output files are created where the user is.
202
229
 
203
230
  Returns:
204
231
  A dictionary where keys are the standardized output identifiers
@@ -209,9 +236,14 @@ def generate_output_paths(
209
236
  logger.debug(f"Generating output paths for command: {command}")
210
237
  logger.debug(f"User output locations: {output_locations}")
211
238
  logger.debug(f"Context config: {context_config}")
239
+ logger.debug(f"Input file dirs: {input_file_dirs}")
240
+ logger.debug(f"Config base dir: {config_base_dir}")
241
+ logger.debug(f"Path resolution mode: {path_resolution_mode}")
212
242
  logger.debug(f"Basename: {basename}, Language: {language}, Extension: {file_extension}")
213
243
 
214
244
  context_config = context_config or {}
245
+ input_file_dirs = input_file_dirs or {}
246
+ config_base_dir_abs = os.path.abspath(config_base_dir) if config_base_dir else None
215
247
  result_paths: Dict[str, str] = {}
216
248
 
217
249
  if not basename:
@@ -278,6 +310,20 @@ def generate_output_paths(
278
310
  # 2. Check Context Configuration Path (.pddrc)
279
311
  elif context_path:
280
312
  source = "context"
313
+
314
+ # Resolve relative `.pddrc` paths based on path_resolution_mode.
315
+ # "cwd" mode: resolve relative to current working directory (for sync)
316
+ # "config_base" mode: resolve relative to config_base_dir (for fix, etc.)
317
+ # Fall back to the input file directory for backwards compatibility.
318
+ if not os.path.isabs(context_path):
319
+ if path_resolution_mode == "cwd":
320
+ context_path = os.path.join(os.getcwd(), context_path)
321
+ elif config_base_dir_abs:
322
+ context_path = os.path.join(config_base_dir_abs, context_path)
323
+ elif input_file_dir:
324
+ context_path = os.path.join(input_file_dir, context_path)
325
+ logger.debug(f"Resolved relative context path to: {context_path}")
326
+
281
327
  # Check if the context path is a directory
282
328
  is_dir = context_path.endswith(os.path.sep) or context_path.endswith('/')
283
329
  if not is_dir:
@@ -297,6 +343,18 @@ def generate_output_paths(
297
343
  # 3. Check Environment Variable Path
298
344
  elif env_path:
299
345
  source = "environment"
346
+
347
+ # Resolve relative env paths based on path_resolution_mode.
348
+ # Same logic as .pddrc paths for consistency.
349
+ if not os.path.isabs(env_path):
350
+ if path_resolution_mode == "cwd":
351
+ env_path = os.path.join(os.getcwd(), env_path)
352
+ elif config_base_dir_abs:
353
+ env_path = os.path.join(config_base_dir_abs, env_path)
354
+ elif input_file_dir:
355
+ env_path = os.path.join(input_file_dir, env_path)
356
+ logger.debug(f"Resolved relative env path to: {env_path}")
357
+
300
358
  # Check if the environment variable points to a directory
301
359
  is_dir = env_path.endswith(os.path.sep)
302
360
  if not is_dir:
@@ -313,11 +371,33 @@ def generate_output_paths(
313
371
  logger.debug(f"Env path '{env_path}' identified as a specific file path.")
314
372
  final_path = env_path # Assume it's a full path or filename
315
373
 
316
- # 4. Use Default Naming Convention in CWD
374
+ # 4. Use Default Naming Convention
317
375
  else:
318
376
  source = "default"
319
- logger.debug(f"Using default filename '{default_filename}' in current directory.")
320
- final_path = default_filename # Relative to CWD initially
377
+ # For example command, default to examples/ directory if no .pddrc config
378
+ if command == "example":
379
+ examples_dir = EXAMPLES_DIR # Fallback constant
380
+ # Create examples directory if it doesn't exist
381
+ try:
382
+ os.makedirs(examples_dir, exist_ok=True)
383
+ logger.debug(f"Created examples directory: {examples_dir}")
384
+ except OSError as e:
385
+ logger.warning(f"Could not create examples directory: {e}")
386
+ final_path = os.path.join(examples_dir, default_filename)
387
+ logger.debug(f"Using default filename '{default_filename}' in examples directory.")
388
+ else:
389
+ # First check if there's a specific directory for this output key
390
+ specific_dir = input_file_dirs.get(output_key)
391
+ if specific_dir:
392
+ final_path = os.path.join(specific_dir, default_filename)
393
+ logger.debug(f"Using default filename '{default_filename}' in specific input file directory: {specific_dir}")
394
+ # Otherwise use the general input file directory if provided
395
+ elif input_file_dir:
396
+ final_path = os.path.join(input_file_dir, default_filename)
397
+ logger.debug(f"Using default filename '{default_filename}' in input file directory: {input_file_dir}")
398
+ else:
399
+ final_path = default_filename # Relative to CWD initially
400
+ logger.debug(f"Using default filename '{default_filename}' in current directory.")
321
401
 
322
402
  # Resolve to absolute path
323
403
  if final_path:
@@ -340,6 +420,9 @@ def generate_output_paths(
340
420
 
341
421
  # --- Example Usage (for testing) ---
342
422
  if __name__ == '__main__':
423
+ # Configure logging for standalone execution
424
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
425
+
343
426
  # Mock inputs
344
427
  mock_basename = "my_module"
345
428
  mock_language = "python"
@@ -568,4 +651,4 @@ if __name__ == '__main__':
568
651
  # 'output_code': '/path/to/cwd/another_module_verify_verified.py',
569
652
  # 'output_program': f'/path/to/cwd/{env_verify_prog_path}'
570
653
  # }
571
- del os.environ['PDD_VERIFY_PROGRAM_OUTPUT_PATH'] # Clean up
654
+ del os.environ['PDD_VERIFY_PROGRAM_OUTPUT_PATH'] # Clean up
pdd/generate_test.py CHANGED
@@ -15,11 +15,14 @@ console = Console()
15
15
  def generate_test(
16
16
  prompt: str,
17
17
  code: str,
18
- strength: float=DEFAULT_STRENGTH,
19
- temperature: float=0.0,
18
+ strength: float = DEFAULT_STRENGTH,
19
+ temperature: float = 0.0,
20
20
  time: float = DEFAULT_TIME,
21
21
  language: str = "python",
22
- verbose: bool = False
22
+ verbose: bool = False,
23
+ source_file_path: Optional[str] = None,
24
+ test_file_path: Optional[str] = None,
25
+ module_name: Optional[str] = None
23
26
  ) -> Tuple[str, float, str]:
24
27
  """
25
28
  Generate a unit test from a code file using LLM.
@@ -32,6 +35,9 @@ def generate_test(
32
35
  language (str): The programming language for the unit test.
33
36
  time (float, optional): Time budget for LLM calls. Defaults to DEFAULT_TIME.
34
37
  verbose (bool): Whether to print detailed information.
38
+ source_file_path (Optional[str]): Absolute or relative path to the code under test.
39
+ test_file_path (Optional[str]): Destination path for the generated test file.
40
+ module_name (Optional[str]): Module name (without extension) for proper imports.
35
41
 
36
42
  Returns:
37
43
  Tuple[str, float, str]: (unit_test, total_cost, model_name)
@@ -53,7 +59,10 @@ def generate_test(
53
59
  input_json = {
54
60
  "prompt_that_generated_code": processed_prompt,
55
61
  "code": code,
56
- "language": language
62
+ "language": language,
63
+ "source_file_path": source_file_path or "",
64
+ "test_file_path": test_file_path or "",
65
+ "module_name": module_name or ""
57
66
  }
58
67
 
59
68
  if verbose:
@@ -98,6 +107,7 @@ def generate_test(
98
107
  strength=strength,
99
108
  temperature=temperature,
100
109
  time=time,
110
+ language=language,
101
111
  verbose=verbose
102
112
  )
103
113
  total_cost += check_cost
@@ -112,6 +122,7 @@ def generate_test(
112
122
  strength=strength,
113
123
  temperature=temperature,
114
124
  time=time,
125
+ language=language,
115
126
  verbose=verbose
116
127
  )
117
128
  total_cost += continue_cost
@@ -181,4 +192,4 @@ def _validate_inputs(
181
192
  if not isinstance(temperature, float):
182
193
  raise ValueError("Temperature must be a float")
183
194
  if not language or not isinstance(language, str):
184
- raise ValueError("Language must be a non-empty string")
195
+ raise ValueError("Language must be a non-empty string")
pdd/get_jwt_token.py CHANGED
@@ -248,7 +248,14 @@ class FirebaseAuthenticator:
248
248
  except requests.exceptions.ConnectionError as e:
249
249
  raise NetworkError(f"Failed to connect to Firebase: {e}")
250
250
  except requests.exceptions.RequestException as e:
251
- raise TokenError(f"Error exchanging GitHub token for Firebase token: {e}")
251
+ # Capture more detail to help diagnose provider configuration or audience mismatches
252
+ extra = ""
253
+ if getattr(e, "response", None) is not None:
254
+ try:
255
+ extra = f" | response: {e.response.text}"
256
+ except Exception:
257
+ pass
258
+ raise TokenError(f"Error exchanging GitHub token for Firebase token: {e}{extra}")
252
259
 
253
260
  def verify_firebase_token(self, id_token: str) -> bool:
254
261
  """
@@ -319,4 +326,4 @@ async def get_jwt_token(firebase_api_key: str, github_client_id: str, app_name:
319
326
  # Store refresh token
320
327
  firebase_auth._store_refresh_token(refresh_token)
321
328
 
322
- return id_token
329
+ return id_token
pdd/get_run_command.py ADDED
@@ -0,0 +1,73 @@
1
+ """Module to retrieve run commands for programming languages."""
2
+
3
+ import os
4
+ import csv
5
+
6
+
7
+ def get_run_command(extension: str) -> str:
8
+ """
9
+ Retrieves the run command for a given file extension.
10
+
11
+ Args:
12
+ extension: The file extension (e.g., ".py", ".js").
13
+
14
+ Returns:
15
+ The run command template with {file} placeholder (e.g., "python {file}"),
16
+ or an empty string if not found or not executable.
17
+
18
+ Raises:
19
+ ValueError: If the PDD_PATH environment variable is not set.
20
+ """
21
+ # Step 1: Load environment variable PDD_PATH
22
+ pdd_path = os.environ.get('PDD_PATH')
23
+ if not pdd_path:
24
+ raise ValueError("PDD_PATH environment variable is not set")
25
+
26
+ # Step 2: Ensure the extension starts with a dot and convert to lowercase
27
+ if not extension.startswith('.'):
28
+ extension = '.' + extension
29
+ extension = extension.lower()
30
+
31
+ # Step 3: Look up the run command
32
+ csv_path = os.path.join(pdd_path, 'data', 'language_format.csv')
33
+ try:
34
+ with open(csv_path, 'r') as csvfile:
35
+ reader = csv.DictReader(csvfile)
36
+ for row in reader:
37
+ if row['extension'].lower() == extension:
38
+ run_command = row.get('run_command', '').strip()
39
+ return run_command if run_command else ''
40
+ except FileNotFoundError:
41
+ print(f"CSV file not found at {csv_path}")
42
+ except csv.Error as e:
43
+ print(f"Error reading CSV file: {e}")
44
+ except KeyError:
45
+ # run_command column doesn't exist
46
+ pass
47
+
48
+ return ''
49
+
50
+
51
+ def get_run_command_for_file(file_path: str) -> str:
52
+ """
53
+ Retrieves the run command for a given file, with the {file} placeholder replaced.
54
+
55
+ Args:
56
+ file_path: The path to the file to run.
57
+
58
+ Returns:
59
+ The complete run command (e.g., "python /path/to/script.py"),
60
+ or an empty string if no run command is available for this file type.
61
+
62
+ Raises:
63
+ ValueError: If the PDD_PATH environment variable is not set.
64
+ """
65
+ _, extension = os.path.splitext(file_path)
66
+ if not extension:
67
+ return ''
68
+
69
+ run_command_template = get_run_command(extension)
70
+ if not run_command_template:
71
+ return ''
72
+
73
+ return run_command_template.replace('{file}', file_path)
@@ -0,0 +1,68 @@
1
+ # pdd/get_test_command.py
2
+ """Get language-appropriate test commands.
3
+
4
+ This module provides functions to resolve the appropriate test command
5
+ for a given test file based on:
6
+ 1. CSV run_test_command (if non-empty)
7
+ 2. Smart detection via default_verify_cmd_for()
8
+ 3. None (triggers agentic fallback)
9
+ """
10
+ from pathlib import Path
11
+ from typing import Optional
12
+ import csv
13
+
14
+ from .agentic_langtest import default_verify_cmd_for
15
+ from .get_language import get_language
16
+
17
+
18
+ def _load_language_format() -> dict:
19
+ """Load language_format.csv into a dict keyed by extension."""
20
+ csv_path = Path(__file__).parent.parent / "data" / "language_format.csv"
21
+ result = {}
22
+ with open(csv_path, 'r') as f:
23
+ reader = csv.DictReader(f)
24
+ for row in reader:
25
+ ext = row.get('extension', '')
26
+ if ext:
27
+ result[ext] = row
28
+ return result
29
+
30
+
31
+ def get_test_command_for_file(test_file: str, language: Optional[str] = None) -> Optional[str]:
32
+ """
33
+ Get the appropriate test command for a test file.
34
+
35
+ Resolution order:
36
+ 1. CSV run_test_command (if non-empty)
37
+ 2. Smart detection via default_verify_cmd_for()
38
+ 3. None (triggers agentic fallback)
39
+
40
+ Args:
41
+ test_file: Path to the test file
42
+ language: Optional language override
43
+
44
+ Returns:
45
+ Test command string with {file} placeholder replaced, or None
46
+ """
47
+ test_path = Path(test_file)
48
+ ext = test_path.suffix
49
+
50
+ resolved_language = language
51
+ if resolved_language is None:
52
+ resolved_language = get_language(ext)
53
+
54
+ # 1. Check CSV for run_test_command
55
+ lang_formats = _load_language_format()
56
+ if ext in lang_formats:
57
+ csv_cmd = lang_formats[ext].get('run_test_command', '').strip()
58
+ if csv_cmd:
59
+ return csv_cmd.replace('{file}', str(test_file))
60
+
61
+ # 2. Smart detection
62
+ if resolved_language:
63
+ smart_cmd = default_verify_cmd_for(resolved_language.lower(), str(test_file))
64
+ if smart_cmd:
65
+ return smart_cmd
66
+
67
+ # 3. No command available
68
+ return None
pdd/git_update.py CHANGED
@@ -4,6 +4,8 @@ from rich import print
4
4
  from rich.console import Console
5
5
  from rich.panel import Panel
6
6
  from .update_prompt import update_prompt
7
+ from .agentic_common import get_available_agents
8
+ from .agentic_update import run_agentic_update
7
9
  import git
8
10
  from . import DEFAULT_TIME
9
11
  console = Console()
@@ -14,21 +16,33 @@ def git_update(
14
16
  strength: float,
15
17
  temperature: float,
16
18
  verbose: bool = False,
17
- time: float = DEFAULT_TIME
19
+ time: float = DEFAULT_TIME,
20
+ simple: bool = False,
21
+ quiet: bool = False,
22
+ prompt_file: Optional[str] = None
18
23
  ) -> Tuple[Optional[str], float, str]:
19
24
  """
20
- Read in modified code, restore the prior checked-in version from GitHub,
21
- update the prompt, write back the modified code, and return outputs.
25
+ Read in modified code, restore the prior checked-in version from Git,
26
+ update the prompt (via agentic or legacy path), write back the modified code,
27
+ and return outputs.
22
28
 
23
29
  Args:
24
- input_prompt (str): The prompt that generated the original code.
30
+ input_prompt (str): The prompt TEXT content (not a file path).
25
31
  modified_code_file (str): Filepath of the modified code.
26
32
  strength (float): Strength parameter for the LLM model.
27
33
  temperature (float): Temperature parameter for the LLM model.
34
+ verbose (bool): Enable verbose logging.
35
+ time (float): Time parameter for the LLM model.
36
+ simple (bool): If True, skip agentic and use legacy update_prompt().
37
+ quiet (bool): Suppress non-error logging.
38
+ prompt_file (Optional[str]): Path to prompt file (required for agentic path).
28
39
 
29
40
  Returns:
30
- Tuple[Optional[str], float, str]: Modified prompt, total cost, and model name.
41
+ Tuple[Optional[str], float, str]: Updated prompt content, total cost, and model name.
31
42
  """
43
+ modified_code: Optional[str] = None
44
+ agentic_cost = 0.0
45
+
32
46
  try:
33
47
  # Check if inputs are valid
34
48
  if not input_prompt or not modified_code_file:
@@ -37,26 +51,57 @@ def git_update(
37
51
  if not os.path.exists(modified_code_file):
38
52
  raise FileNotFoundError(f"Modified code file not found: {modified_code_file}")
39
53
 
40
- # Initialize git repository
41
- repo = git.Repo(search_parent_directories=True)
54
+ # Initialize git repository object once
55
+ repo = git.Repo(modified_code_file, search_parent_directories=True)
56
+ repo_root = repo.working_tree_dir
42
57
 
43
58
  # Get the file's relative path to the repo root
44
- repo_root = repo.git.rev_parse("--show-toplevel")
45
59
  relative_path = os.path.relpath(modified_code_file, repo_root)
46
60
 
47
- # Read the modified code
61
+ # Read the modified code FIRST (before any git operations)
48
62
  with open(modified_code_file, 'r') as file:
49
63
  modified_code = file.read()
50
64
 
51
- # Restore the prior checked-in version
65
+ # Restore the prior checked-in version using the relative path
52
66
  repo.git.checkout('HEAD', '--', relative_path)
53
67
 
54
68
  # Read the original input code
55
69
  with open(modified_code_file, 'r') as file:
56
70
  original_input_code = file.read()
57
71
 
58
- # Call update_prompt function
59
- modified_prompt, total_cost, model_name = update_prompt(
72
+ # Routing decision: agentic vs legacy
73
+ use_agentic = (
74
+ not simple
75
+ and prompt_file is not None
76
+ and get_available_agents()
77
+ )
78
+
79
+ if use_agentic:
80
+ # Agentic path
81
+ success, message, agentic_cost, provider, changed_files = run_agentic_update(
82
+ prompt_file=prompt_file,
83
+ code_file=modified_code_file,
84
+ verbose=verbose,
85
+ quiet=quiet
86
+ )
87
+ if success:
88
+ # Read updated prompt content from file
89
+ with open(prompt_file, 'r') as file:
90
+ updated_prompt = file.read()
91
+
92
+ # Pretty print the results
93
+ console.print(Panel.fit(
94
+ f"[bold green]Success (agentic):[/bold green]\n"
95
+ f"Provider: {provider}\n"
96
+ f"Total cost: ${agentic_cost:.6f}\n"
97
+ f"Changed files: {', '.join(changed_files)}"
98
+ ))
99
+
100
+ return updated_prompt, agentic_cost, provider
101
+ # Fall through to legacy on agentic failure
102
+
103
+ # Legacy path
104
+ result_prompt, legacy_cost, model_name = update_prompt(
60
105
  input_prompt=input_prompt,
61
106
  input_code=original_input_code,
62
107
  modified_code=modified_code,
@@ -66,21 +111,27 @@ def git_update(
66
111
  time=time
67
112
  )
68
113
 
69
- # Write back the modified code
70
- with open(modified_code_file, 'w') as file:
71
- file.write(modified_code)
72
-
114
+ total_cost = agentic_cost + legacy_cost
73
115
 
74
116
  # Pretty print the results
75
117
  console.print(Panel.fit(
76
118
  f"[bold green]Success:[/bold green]\n"
77
- f"Modified prompt: {modified_prompt}\n"
119
+ f"Modified prompt: {result_prompt}\n"
78
120
  f"Total cost: ${total_cost:.6f}\n"
79
121
  f"Model name: {model_name}"
80
122
  ))
81
123
 
82
- return modified_prompt, total_cost, model_name
124
+ return result_prompt, total_cost, model_name
83
125
 
84
126
  except Exception as e:
85
127
  console.print(Panel(f"[bold red]Error:[/bold red] {str(e)}", title="Error", expand=False))
86
- return None, 0.0, ""
128
+ return None, agentic_cost, ""
129
+
130
+ finally:
131
+ # Always restore user's modified code to disk before returning
132
+ if modified_code is not None and modified_code_file:
133
+ try:
134
+ with open(modified_code_file, 'w') as file:
135
+ file.write(modified_code)
136
+ except Exception:
137
+ pass # Best effort restoration
@@ -59,8 +59,8 @@ def incremental_code_generator(
59
59
  if not original_prompt or not new_prompt or not existing_code or not language:
60
60
  raise ValueError("All required inputs (original_prompt, new_prompt, existing_code, language) must be provided.")
61
61
 
62
- if not 0 <= strength <= 1 or not 0 <= temperature <= 1 or not 0 <= time <= 1:
63
- raise ValueError("Strength, temperature, and time must be between 0 and 1.")
62
+ if not 0 <= strength <= 1 or not 0 <= temperature <= 2 or not 0 <= time <= 1:
63
+ raise ValueError("Strength and time must be between 0 and 1. Temperature must be between 0 and 2.")
64
64
 
65
65
  try:
66
66
  total_cost = 0.0