pdd-cli 0.0.60__py3-none-any.whl → 0.0.62__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,6 +1,6 @@
1
1
  """PDD - Prompt Driven Development"""
2
2
 
3
- __version__ = "0.0.60"
3
+ __version__ = "0.0.62"
4
4
 
5
5
  # Strength parameter used for LLM extraction across the codebase
6
6
  # Used in postprocessing, XML tagging, code generation, and other extraction
pdd/cli.py CHANGED
@@ -14,9 +14,11 @@ from typing import Any, Dict, List, Optional, Tuple
14
14
  from pathlib import Path # Import Path
15
15
 
16
16
  import click
17
+ from rich import box
17
18
  from rich.console import Console
18
- from rich.theme import Theme
19
19
  from rich.markup import MarkupError, escape
20
+ from rich.table import Table
21
+ from rich.theme import Theme
20
22
 
21
23
  # --- Relative Imports for Internal Modules ---
22
24
  from . import DEFAULT_STRENGTH, __version__, DEFAULT_TIME
@@ -348,14 +350,26 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
348
350
  invoked_subcommands = ctx.obj.get('invoked_subcommands', []) or []
349
351
  except Exception:
350
352
  invoked_subcommands = []
351
- results = results or []
353
+ # Normalize results: Click may pass a single return value (e.g., a 3-tuple)
354
+ # rather than a list of results. Wrap single 3-tuples so we treat them as
355
+ # one step in the summary instead of three separate items.
356
+ if results is None:
357
+ normalized_results: List[Any] = []
358
+ elif isinstance(results, list):
359
+ normalized_results = results
360
+ elif isinstance(results, tuple) and len(results) == 3:
361
+ normalized_results = [results]
362
+ else:
363
+ # Fallback: wrap any other scalar/iterable as a single result
364
+ normalized_results = [results]
365
+
352
366
  num_commands = len(invoked_subcommands)
353
- num_results = len(results) # Number of results actually received
367
+ num_results = len(normalized_results) # Number of results actually received
354
368
 
355
369
  if not ctx.obj.get("quiet"):
356
370
  console.print("\n[info]--- Command Execution Summary ---[/info]")
357
371
 
358
- for i, result_tuple in enumerate(results):
372
+ for i, result_tuple in enumerate(normalized_results):
359
373
  # Use the retrieved subcommand name (might be "Unknown Command X" in tests)
360
374
  command_name = invoked_subcommands[i] if i < num_commands else f"Unknown Command {i+1}"
361
375
 
@@ -395,7 +409,7 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
395
409
 
396
410
  if not ctx.obj.get("quiet"):
397
411
  # Only print total cost if at least one command potentially contributed cost
398
- if any(res is not None and isinstance(res, tuple) and len(res) == 3 for res in results):
412
+ if any(res is not None and isinstance(res, tuple) and len(res) == 3 for res in normalized_results):
399
413
  console.print(f"[info]Total Estimated Cost:[/info] ${total_cost:.6f}")
400
414
  # Indicate if the chain might have been incomplete due to errors
401
415
  if num_results < num_commands and not all(res is None for res in results): # Avoid printing if all failed
@@ -437,20 +451,138 @@ def templates_show(name: str):
437
451
  try:
438
452
  data = template_registry.show_template(name)
439
453
  summary = data.get("summary", {})
440
- console.print(f"[bold]{summary.get('name','')}[/bold] — {summary.get('description','')}")
441
- console.print(f"Version: {summary.get('version','')} Tags: {', '.join(summary.get('tags',[]))}")
442
- console.print(f"Language: {summary.get('language','')} Output: {summary.get('output','')}")
443
- console.print(f"Path: {summary.get('path','')}")
454
+
455
+ def _render_key_value_table(title: Optional[str], items: List[Tuple[str, Any]], *, highlight_path: bool = False):
456
+ """Render a 2-column Rich table for key/value pairs."""
457
+
458
+ table = Table(show_header=False, box=box.SIMPLE, expand=True)
459
+ table.add_column("Field", style="info", no_wrap=True)
460
+ table.add_column("Value", overflow="fold")
461
+
462
+ added_rows = False
463
+ for label, value in items:
464
+ if value in (None, "", [], {}):
465
+ continue
466
+ if isinstance(value, (list, tuple)):
467
+ value_str = ", ".join(str(v) for v in value)
468
+ else:
469
+ value_str = str(value)
470
+
471
+ if highlight_path and label.lower() == "path":
472
+ value_markup = f"[path]{escape(value_str)}[/path]"
473
+ else:
474
+ value_markup = escape(value_str)
475
+
476
+ table.add_row(label, value_markup)
477
+ added_rows = True
478
+
479
+ if added_rows:
480
+ if title:
481
+ console.print(f"[info]{title}[/info]")
482
+ console.print(table)
483
+
484
+ summary_items = [
485
+ ("Name", summary.get("name")),
486
+ ("Description", summary.get("description")),
487
+ ("Version", summary.get("version")),
488
+ ("Tags", summary.get("tags", [])),
489
+ ("Language", summary.get("language")),
490
+ ("Output", summary.get("output")),
491
+ ("Path", summary.get("path")),
492
+ ]
493
+ _render_key_value_table("Template Summary:", summary_items, highlight_path=True)
494
+
444
495
  if data.get("variables"):
445
496
  console.print("\n[info]Variables:[/info]")
446
- for k, v in data["variables"].items():
447
- console.print(f"- {k}: {v}")
497
+ variables_table = Table(box=box.SIMPLE_HEAD, show_lines=False, expand=True)
498
+ variables_table.add_column("Name", style="bold", no_wrap=True)
499
+ variables_table.add_column("Required", style="info", no_wrap=True)
500
+ variables_table.add_column("Type", no_wrap=True)
501
+ variables_table.add_column("Description", overflow="fold")
502
+ variables_table.add_column("Default/Examples", overflow="fold")
503
+
504
+ for var_name, var_meta in data["variables"].items():
505
+ required = var_meta.get("required")
506
+ if required is True:
507
+ required_str = "Yes"
508
+ elif required is False:
509
+ required_str = "No"
510
+ else:
511
+ required_str = "-"
512
+
513
+ var_type = escape(str(var_meta.get("type", "-")))
514
+ description = escape(str(var_meta.get("description", "")))
515
+
516
+ default_parts: List[str] = []
517
+ default_value = var_meta.get("default")
518
+ if default_value not in (None, ""):
519
+ default_parts.append(f"default: {default_value}")
520
+
521
+ examples_value = var_meta.get("examples")
522
+ if examples_value:
523
+ if isinstance(examples_value, (list, tuple)):
524
+ examples_str = ", ".join(str(example) for example in examples_value)
525
+ else:
526
+ examples_str = str(examples_value)
527
+ default_parts.append(f"examples: {examples_str}")
528
+
529
+ example_paths_value = var_meta.get("example_paths")
530
+ if example_paths_value:
531
+ if isinstance(example_paths_value, (list, tuple)):
532
+ example_paths_str = ", ".join(str(example) for example in example_paths_value)
533
+ else:
534
+ example_paths_str = str(example_paths_value)
535
+ default_parts.append(f"paths: {example_paths_str}")
536
+
537
+ default_examples = "\n".join(default_parts) if default_parts else "-"
538
+
539
+ variables_table.add_row(
540
+ escape(str(var_name)),
541
+ required_str,
542
+ var_type,
543
+ description,
544
+ escape(default_examples),
545
+ )
546
+
547
+ console.print(variables_table)
548
+
448
549
  if data.get("usage"):
449
550
  console.print("\n[info]Usage:[/info]")
450
- console.print(data["usage"]) # raw; CLI may format later
551
+ usage = data["usage"]
552
+ if isinstance(usage, dict):
553
+ for group_name, entries in usage.items():
554
+ console.print(f"[bold]{escape(str(group_name))}[/bold]")
555
+ usage_table = Table(box=box.SIMPLE, show_lines=False, expand=True)
556
+ usage_table.add_column("Name", style="bold", no_wrap=True)
557
+ usage_table.add_column("Command", overflow="fold")
558
+
559
+ if isinstance(entries, (list, tuple)):
560
+ iterable_entries = entries
561
+ else:
562
+ iterable_entries = [entries]
563
+
564
+ for entry in iterable_entries:
565
+ if isinstance(entry, dict):
566
+ name_value = escape(str(entry.get("name", "")))
567
+ command_value = escape(str(entry.get("command", "")))
568
+ else:
569
+ name_value = "-"
570
+ command_value = escape(str(entry))
571
+ usage_table.add_row(name_value, f"[command]{command_value}[/command]")
572
+
573
+ if usage_table.row_count:
574
+ console.print(usage_table)
575
+ else:
576
+ console.print(usage)
577
+
451
578
  if data.get("discover"):
452
579
  console.print("\n[info]Discover:[/info]")
453
- console.print(data["discover"]) # raw dict
580
+ discover = data["discover"]
581
+ if isinstance(discover, dict):
582
+ discover_items = [(str(key), value) for key, value in discover.items()]
583
+ _render_key_value_table(None, discover_items)
584
+ else:
585
+ console.print(discover)
454
586
  if data.get("output_schema"):
455
587
  console.print("\n[info]Output Schema:[/info]")
456
588
  try:
@@ -64,3 +64,6 @@ Log,del,.log
64
64
  reStructuredText,del,.rst
65
65
  Text,del,.txt
66
66
  INI,;,.ini
67
+ Verilog,//,.v
68
+ Systemverilog,//,.sv
69
+ Prisma,///,.prisma
pdd/data/llm_model.csv CHANGED
@@ -2,7 +2,7 @@ provider,model,input,output,coding_arena_elo,base_url,api_key,max_reasoning_toke
2
2
  OpenAI,gpt-5-nano,0.05,0.4,1249,,OPENAI_API_KEY,0,True,none
3
3
  Google,vertex_ai/gemini-2.5-flash,0.15,0.6,1290,,VERTEX_CREDENTIALS,0,True,effort
4
4
  Google,gemini/gemini-2.5-pro,1.25,10.0,1360,,GEMINI_API_KEY,0,True,none
5
- Google,vertex_ai/claude-sonnet-4,3.0,15.0,1359,,VERTEX_CREDENTIALS,64000,True,budget
5
+ Google,vertex_ai/claude-sonnet-4-5,3.0,15.0,1359,,VERTEX_CREDENTIALS,64000,True,budget
6
6
  Google,vertex_ai/gemini-2.5-pro,1.25,10.0,1405,,VERTEX_CREDENTIALS,0,True,none
7
7
  OpenAI,gpt-5-mini,0.25,2.0,1325,,OPENAI_API_KEY,0,True,effort
8
8
  OpenAI,gpt-5,1.25,10.0,1482,,OPENAI_API_KEY,0,True,effort
@@ -15,6 +15,6 @@ OpenAI,openai/mlx-community/Qwen3-30B-A3B-4bit,0,0,1040,http://localhost:8080,,0
15
15
  OpenAI,lm_studio/openai-gpt-oss-120b-mlx-6,0.0001,0,1082,http://localhost:1234/v1,,0,True,none
16
16
  Fireworks,fireworks_ai/accounts/fireworks/models/glm-4p5,3.0,8.0,1364,,FIREWORKS_API_KEY,0,False,none
17
17
  OpenAI,groq/moonshotai/kimi-k2-instruct,1.0,3.0,1330,,GROQ_API_KEY,0,True,none
18
- Anthropic,anthropic/claude-sonnet-4-20250514,3.0,15.0,1356,,ANTHROPIC_API_KEY,64000,True,budget
18
+ Anthropic,anthropic/claude-sonnet-4-5-20250929,3.0,15.0,1356,,ANTHROPIC_API_KEY,64000,True,budget
19
19
  Anthropic,anthropic/claude-opus-4-1-20250805,3.0,15.0,1474,,ANTHROPIC_API_KEY,32000,True,budget
20
20
  Anthropic,anthropic/claude-3-5-haiku-20241022,3.0,15.0,1133,,ANTHROPIC_API_KEY,8192,True,budget
pdd/preprocess.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import re
3
3
  import subprocess
4
- from typing import List, Optional
4
+ from typing import List, Optional, Tuple
5
5
  import traceback
6
6
  from rich.console import Console
7
7
  from rich.panel import Panel
@@ -11,22 +11,107 @@ from rich.traceback import install
11
11
  install()
12
12
  console = Console()
13
13
 
14
+ # Debug/Instrumentation controls
15
+ _DEBUG_PREPROCESS = str(os.getenv("PDD_PREPROCESS_DEBUG", "")).lower() in ("1", "true", "yes", "on")
16
+ _DEBUG_OUTPUT_FILE = os.getenv("PDD_PREPROCESS_DEBUG_FILE") # Optional path to write a debug report
17
+ _DEBUG_EVENTS: List[str] = []
18
+
19
+ def _dbg(msg: str) -> None:
20
+ if _DEBUG_PREPROCESS:
21
+ console.print(f"[dim][PPD][preprocess][/dim] {escape(msg)}")
22
+ _DEBUG_EVENTS.append(msg)
23
+
24
+ def _write_debug_report() -> None:
25
+ if _DEBUG_PREPROCESS and _DEBUG_OUTPUT_FILE:
26
+ try:
27
+ with open(_DEBUG_OUTPUT_FILE, "w", encoding="utf-8") as fh:
28
+ fh.write("Preprocess Debug Report\n\n")
29
+ for line in _DEBUG_EVENTS:
30
+ fh.write(line + "\n")
31
+ except Exception:
32
+ # Avoid interfering with normal flow if writing fails
33
+ pass
34
+
35
+ def _extract_fence_spans(text: str) -> List[Tuple[int, int]]:
36
+ """Return list of (start, end) spans for fenced code blocks ```...```.
37
+
38
+ The spans are [start, end) indices in the original text.
39
+ """
40
+ spans: List[Tuple[int, int]] = []
41
+ try:
42
+ for m in re.finditer(r"```[\w\s]*\n[\s\S]*?```", text):
43
+ spans.append((m.start(), m.end()))
44
+ except Exception:
45
+ pass
46
+ return spans
47
+
48
+ def _is_inside_any_span(idx: int, spans: List[Tuple[int, int]]) -> bool:
49
+ for s, e in spans:
50
+ if s <= idx < e:
51
+ return True
52
+ return False
53
+
54
+ def _scan_risky_placeholders(text: str) -> Tuple[List[Tuple[int, str]], List[Tuple[int, str]]]:
55
+ """Scan for risky placeholders outside code fences.
56
+
57
+ Returns two lists of (line_no, snippet):
58
+ - single_brace: matches like {name} not doubled and not part of {{...}}
59
+ - template_brace: `${...}` occurrences (which include single { ... })
60
+ """
61
+ single_brace: List[Tuple[int, str]] = []
62
+ template_brace: List[Tuple[int, str]] = []
63
+ try:
64
+ fence_spans = _extract_fence_spans(text)
65
+ # Single-brace variable placeholders (avoid matching {{ or }})
66
+ for m in re.finditer(r"(?<!\{)\{([A-Za-z_][A-Za-z0-9_]*)\}(?!\})", text):
67
+ if not _is_inside_any_span(m.start(), fence_spans):
68
+ line_no = text.count("\n", 0, m.start()) + 1
69
+ single_brace.append((line_no, m.group(0)))
70
+ # JavaScript template placeholders like ${...}
71
+ for m in re.finditer(r"\$\{[^\}]+\}", text):
72
+ if not _is_inside_any_span(m.start(), fence_spans):
73
+ line_no = text.count("\n", 0, m.start()) + 1
74
+ template_brace.append((line_no, m.group(0)))
75
+ except Exception:
76
+ pass
77
+ return single_brace, template_brace
78
+
14
79
  def preprocess(prompt: str, recursive: bool = False, double_curly_brackets: bool = True, exclude_keys: Optional[List[str]] = None) -> str:
15
80
  try:
16
81
  if not prompt:
17
82
  console.print("[bold red]Error:[/bold red] Empty prompt provided")
18
83
  return ""
84
+ _DEBUG_EVENTS.clear()
85
+ _dbg(f"Start preprocess(recursive={recursive}, double_curly={double_curly_brackets}, exclude_keys={exclude_keys})")
86
+ _dbg(f"Initial length: {len(prompt)} characters")
19
87
  console.print(Panel("Starting prompt preprocessing", style="bold blue"))
20
88
  prompt = process_backtick_includes(prompt, recursive)
89
+ _dbg("After backtick includes processed")
21
90
  prompt = process_xml_tags(prompt, recursive)
91
+ _dbg("After XML-like tags processed")
22
92
  if double_curly_brackets:
23
93
  prompt = double_curly(prompt, exclude_keys)
94
+ _dbg("After double_curly execution")
95
+ # Scan for risky placeholders remaining outside code fences
96
+ singles, templates = _scan_risky_placeholders(prompt)
97
+ if singles:
98
+ _dbg(f"WARNING: Found {len(singles)} single-brace placeholders outside code fences (examples):")
99
+ for ln, frag in singles[:5]:
100
+ _dbg(f" line {ln}: {frag}")
101
+ if templates:
102
+ _dbg(f"INFO: Found {len(templates)} template literals ${'{...'} outside code fences (examples):")
103
+ for ln, frag in templates[:5]:
104
+ _dbg(f" line {ln}: {frag}")
24
105
  # Don't trim whitespace that might be significant for the tests
25
106
  console.print(Panel("Preprocessing complete", style="bold green"))
107
+ _dbg(f"Final length: {len(prompt)} characters")
108
+ _write_debug_report()
26
109
  return prompt
27
110
  except Exception as e:
28
111
  console.print(f"[bold red]Error during preprocessing:[/bold red] {str(e)}")
29
112
  console.print(Panel(traceback.format_exc(), title="Error Details", style="red"))
113
+ _dbg(f"Exception: {str(e)}")
114
+ _write_debug_report()
30
115
  return prompt
31
116
 
32
117
  def get_file_path(file_name: str) -> str:
@@ -45,14 +130,17 @@ def process_backtick_includes(text: str, recursive: bool) -> str:
45
130
  content = file.read()
46
131
  if recursive:
47
132
  content = preprocess(content, recursive=True, double_curly_brackets=False)
133
+ _dbg(f"Included via backticks: {file_path} (len={len(content)})")
48
134
  return f"```{content}```"
49
135
  except FileNotFoundError:
50
136
  console.print(f"[bold red]Warning:[/bold red] File not found: {file_path}")
137
+ _dbg(f"Missing backtick include: {file_path}")
51
138
  # First pass (recursive=True): leave the tag so a later env expansion can resolve it
52
139
  # Second pass (recursive=False): replace with a visible placeholder
53
140
  return match.group(0) if recursive else f"```[File not found: {file_path}]```"
54
141
  except Exception as e:
55
142
  console.print(f"[bold red]Error processing include:[/bold red] {str(e)}")
143
+ _dbg(f"Error processing backtick include {file_path}: {e}")
56
144
  return f"```[Error processing include: {file_path}]```"
57
145
  prev_text = ""
58
146
  current_text = text
@@ -80,14 +168,17 @@ def process_include_tags(text: str, recursive: bool) -> str:
80
168
  content = file.read()
81
169
  if recursive:
82
170
  content = preprocess(content, recursive=True, double_curly_brackets=False)
171
+ _dbg(f"Included via XML tag: {file_path} (len={len(content)})")
83
172
  return content
84
173
  except FileNotFoundError:
85
174
  console.print(f"[bold red]Warning:[/bold red] File not found: {file_path}")
175
+ _dbg(f"Missing XML include: {file_path}")
86
176
  # First pass (recursive=True): leave the tag so a later env expansion can resolve it
87
177
  # Second pass (recursive=False): replace with a visible placeholder
88
178
  return match.group(0) if recursive else f"[File not found: {file_path}]"
89
179
  except Exception as e:
90
180
  console.print(f"[bold red]Error processing include:[/bold red] {str(e)}")
181
+ _dbg(f"Error processing XML include {file_path}: {e}")
91
182
  return f"[Error processing include: {file_path}]"
92
183
  prev_text = ""
93
184
  current_text = text
@@ -113,15 +204,18 @@ def process_shell_tags(text: str, recursive: bool) -> str:
113
204
  # Defer execution until after env var expansion
114
205
  return match.group(0)
115
206
  console.print(f"Executing shell command: [cyan]{escape(command)}[/cyan]")
207
+ _dbg(f"Shell tag command: {command}")
116
208
  try:
117
209
  result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
118
210
  return result.stdout
119
211
  except subprocess.CalledProcessError as e:
120
212
  error_msg = f"Command '{command}' returned non-zero exit status {e.returncode}."
121
213
  console.print(f"[bold red]Error:[/bold red] {error_msg}")
214
+ _dbg(f"Shell command error: {error_msg}")
122
215
  return f"Error: {error_msg}"
123
216
  except Exception as e:
124
217
  console.print(f"[bold red]Error executing shell command:[/bold red] {str(e)}")
218
+ _dbg(f"Shell execution exception: {e}")
125
219
  return f"[Shell execution error: {str(e)}]"
126
220
  return re.sub(pattern, replace_shell, text, flags=re.DOTALL)
127
221
 
@@ -133,24 +227,30 @@ def process_web_tags(text: str, recursive: bool) -> str:
133
227
  # Defer network operations until after env var expansion
134
228
  return match.group(0)
135
229
  console.print(f"Scraping web content from: [cyan]{url}[/cyan]")
230
+ _dbg(f"Web tag URL: {url}")
136
231
  try:
137
232
  try:
138
233
  from firecrawl import FirecrawlApp
139
234
  except ImportError:
235
+ _dbg("firecrawl import failed; package not installed")
140
236
  return f"[Error: firecrawl-py package not installed. Cannot scrape {url}]"
141
237
  api_key = os.environ.get('FIRECRAWL_API_KEY')
142
238
  if not api_key:
143
239
  console.print("[bold yellow]Warning:[/bold yellow] FIRECRAWL_API_KEY not found in environment")
240
+ _dbg("FIRECRAWL_API_KEY not set")
144
241
  return f"[Error: FIRECRAWL_API_KEY not set. Cannot scrape {url}]"
145
242
  app = FirecrawlApp(api_key=api_key)
146
243
  response = app.scrape_url(url, formats=['markdown'])
147
244
  if hasattr(response, 'markdown'):
245
+ _dbg(f"Web scrape returned markdown (len={len(response.markdown)})")
148
246
  return response.markdown
149
247
  else:
150
248
  console.print(f"[bold yellow]Warning:[/bold yellow] No markdown content returned for {url}")
249
+ _dbg("Web scrape returned no markdown content")
151
250
  return f"[No content available for {url}]"
152
251
  except Exception as e:
153
252
  console.print(f"[bold red]Error scraping web content:[/bold red] {str(e)}")
253
+ _dbg(f"Web scraping exception: {e}")
154
254
  return f"[Web scraping error: {str(e)}]"
155
255
  return re.sub(pattern, replace_web, text, flags=re.DOTALL)
156
256
 
@@ -173,11 +273,14 @@ def process_include_many_tags(text: str, recursive: bool) -> str:
173
273
  console.print(f"Including (many): [cyan]{full_path}[/cyan]")
174
274
  with open(full_path, 'r', encoding='utf-8') as fh:
175
275
  contents.append(fh.read())
276
+ _dbg(f"Included (many): {p}")
176
277
  except FileNotFoundError:
177
278
  console.print(f"[bold red]Warning:[/bold red] File not found: {p}")
279
+ _dbg(f"Missing include-many: {p}")
178
280
  contents.append(f"[File not found: {p}]")
179
281
  except Exception as e:
180
282
  console.print(f"[bold red]Error processing include-many:[/bold red] {str(e)}")
283
+ _dbg(f"Error processing include-many {p}: {e}")
181
284
  contents.append(f"[Error processing include: {p}]")
182
285
  return "\n".join(contents)
183
286
  return re.sub(pattern, replace_many, text, flags=re.DOTALL)
@@ -187,6 +290,7 @@ def double_curly(text: str, exclude_keys: Optional[List[str]] = None) -> str:
187
290
  exclude_keys = []
188
291
 
189
292
  console.print("Doubling curly brackets...")
293
+ _dbg("double_curly invoked")
190
294
 
191
295
  # Special case handling for specific test patterns
192
296
  if "Mix of {excluded{inner}} nesting" in text and "excluded" in exclude_keys:
@@ -210,8 +314,8 @@ def double_curly(text: str, exclude_keys: Optional[List[str]] = None) -> str:
210
314
  "2": {{"id": "2", "name": "Resource Two"}}
211
315
  }}"""
212
316
 
213
- # Protect ${IDENT} placeholders so they remain unchanged
214
- # Use placeholders that won't collide with typical content
317
+ # Protect ${IDENT} placeholders so we can safely double braces, then restore
318
+ # them as ${{IDENT}} to avoid PromptTemplate interpreting {IDENT}.
215
319
  protected_vars: List[str] = []
216
320
  def _protect_var(m):
217
321
  protected_vars.append(m.group(0))
@@ -235,10 +339,22 @@ def double_curly(text: str, exclude_keys: Optional[List[str]] = None) -> str:
235
339
  # Restore already doubled brackets
236
340
  text = re.sub(r'__ALREADY_DOUBLED__(.*?)__END_ALREADY__', r'{{\1}}', text)
237
341
 
238
- # Restore protected ${IDENT} placeholders
342
+ # Restore protected ${IDENT} placeholders as ${{IDENT}} so single braces
343
+ # don't leak into PromptTemplate formatting. This is safe for JS template
344
+ # literals and prevents missing-key errors in later formatting steps.
239
345
  def _restore_var(m):
240
346
  idx = int(m.group(1))
241
- return protected_vars[idx] if 0 <= idx < len(protected_vars) else m.group(0)
347
+ if 0 <= idx < len(protected_vars):
348
+ original = protected_vars[idx] # e.g., ${FOO}
349
+ try:
350
+ inner = re.match(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}", original)
351
+ if inner:
352
+ # Build as concatenation to avoid f-string brace escaping confusion
353
+ return "${{" + inner.group(1) + "}}" # -> ${{FOO}}
354
+ except Exception:
355
+ pass
356
+ return original
357
+ return m.group(0)
242
358
  text = re.sub(r"__PDD_VAR_(\d+)__", _restore_var, text)
243
359
 
244
360
  # Special handling for code blocks
@@ -84,7 +84,21 @@ output_schema:
84
84
  properties:
85
85
  route: { type: string }
86
86
  params: { type: array, items: { type: object } }
87
- dataSources: { type: array, items: { type: object } }
87
+ dataSources:
88
+ type: array
89
+ items:
90
+ type: object
91
+ required: [kind, source]
92
+ properties:
93
+ kind: { enum: [api, query, stream, file, cache, message, job, other] }
94
+ source: { type: string }
95
+ method: { type: string }
96
+ description: { type: string }
97
+ auth: { type: string }
98
+ inputs: { type: array, items: { type: string } }
99
+ outputs: { type: array, items: { type: string } }
100
+ refreshInterval: { type: string }
101
+ notes: { type: string }
88
102
  layout: { type: object }
89
103
  module: { type: object }
90
104
  api: { type: object }
@@ -109,6 +123,7 @@ INSTRUCTIONS:
109
123
  - Output a single top-level JSON array of items. Each item must include:
110
124
  - reason, description, dependencies (filenames), priority (1 = highest), filename, optional tags.
111
125
  - interface: include only the applicable sub-object (component, page, module, api, graphql, cli, job, message, or config). Omit all non-applicable sub-objects entirely.
126
+ - When interface.type is "page", each entry in `dataSources` must be an object with at least `kind` (e.g., api/query) and `source` (e.g., URL or identifier). Provide `method`, `description`, and any other useful metadata when known.
112
127
  - Valid JSON only. No comments or trailing commas.
113
128
 
114
129
  OUTPUT FORMAT (authoritative):
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: generic/generate_prompt
3
+ description: Generate a module prompt (.prompt) for any stack (backend, frontend, CLI, jobs) using project docs and context
4
+ version: 1.0.0
5
+ tags: [template, prompt, generic]
6
+ language: prompt
7
+ output: prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt
8
+ variables:
9
+ MODULE:
10
+ required: true
11
+ type: string
12
+ description: Module/component basename to generate a prompt for.
13
+ examples: [orders, auth, users]
14
+ LANG_OR_FRAMEWORK:
15
+ required: false
16
+ type: string
17
+ description: Target language or framework suffix used in prompt naming (matches your stack conventions).
18
+ examples: [Python, TypeScriptReact, Go, Java, Ruby]
19
+ default: Python
20
+ LAYER:
21
+ required: false
22
+ type: string
23
+ description: System layer or interface type for context.
24
+ examples: [backend, frontend, api, graphql, cli, job, message, config, module, component, page]
25
+ PRD_FILE:
26
+ required: false
27
+ type: path
28
+ description: Product requirements document providing overall context.
29
+ example_paths: [PRD.md, docs/product/prd.md]
30
+ API_DOC_FILE:
31
+ required: false
32
+ type: path
33
+ description: API documentation describing endpoints and conventions.
34
+ example_paths: [docs/api-documentation.md, docs/api.md]
35
+ DB_SCHEMA_FILE:
36
+ required: false
37
+ type: path
38
+ description: Database schema or ERD for backend data models.
39
+ example_paths: [context/database-schema.md, docs/db/schema.md]
40
+ BACKEND_FILES_CSV:
41
+ required: false
42
+ type: path
43
+ description: CSV listing backend Python files/modules (for context/reference).
44
+ example_paths: [prompts/backend/python_architecture.csv]
45
+ IO_DEPENDENCIES_CSV:
46
+ required: false
47
+ type: path
48
+ description: CSV of function inputs/outputs and dependencies for backend modules.
49
+ example_paths: [prompts/backend/io_dependencies.csv]
50
+ ARCHITECTURE_FILE:
51
+ required: true
52
+ type: path
53
+ description: Architecture JSON (from architecture/architecture_json) to drive module scope, dependencies, and interface.
54
+ example_paths: [architecture.json]
55
+ TECH_STACK_FILE:
56
+ required: false
57
+ type: path
58
+ description: Tech stack overview (languages, frameworks, infrastructure, tools) for shaping conventions.
59
+ example_paths: [docs/tech_stack.md, docs/architecture/stack.md]
60
+ CODE_GENERATOR_PROMPT:
61
+ required: false
62
+ type: path
63
+ description: Reference code generator prompt to mirror style and expectations.
64
+ example_paths: [prompts/code_generator_python.prompt, prompts/code_generator_main_python.prompt]
65
+ EXISTING_PROMPTS:
66
+ required: false
67
+ type: list
68
+ description: Existing prompt files to use as reference (comma/newline-separated).
69
+ example_paths: [prompts/orders_python.prompt, prompts/auth_python.prompt]
70
+ DEP_EXAMPLE_EXT:
71
+ required: false
72
+ type: string
73
+ description: File extension for dependency examples under context/ (for non-Python stacks).
74
+ examples: [py, ts, tsx, go, java]
75
+ default: py
76
+ usage:
77
+ generate:
78
+ - name: Minimal (architecture only)
79
+ command: pdd generate -e MODULE=orders -e LANG_OR_FRAMEWORK=Python -e ARCHITECTURE_FILE=architecture.json --output 'prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt' pdd/templates/generic/generate_prompt.prompt
80
+ - name: With project docs
81
+ command: pdd generate -e MODULE=orders -e LANG_OR_FRAMEWORK=Python -e ARCHITECTURE_FILE=architecture.json -e PRD_FILE=docs/PRD.md -e API_DOC_FILE=docs/api-documentation.md -e DB_SCHEMA_FILE=context/database-schema.md --output 'prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt' pdd/templates/generic/generate_prompt.prompt
82
+ - name: With CSVs and references (backend/Python)
83
+ command: pdd generate -e MODULE=orders -e LANG_OR_FRAMEWORK=Python -e ARCHITECTURE_FILE=architecture.json -e PRD_FILE=docs/PRD.md -e API_DOC_FILE=docs/api-documentation.md -e DB_SCHEMA_FILE=context/database-schema.md -e BACKEND_FILES_CSV=prompts/backend/python_architecture.csv -e IO_DEPENDENCIES_CSV=prompts/backend/io_dependencies.csv -e CODE_GENERATOR_PROMPT=prompts/code_generator_python.prompt --output 'prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt' pdd/templates/generic/generate_prompt.prompt
84
+ - name: Frontend (TypeScriptReact) variant
85
+ command: pdd generate -e MODULE=profile_page -e LANG_OR_FRAMEWORK=TypeScriptReact -e LAYER=frontend -e ARCHITECTURE_FILE=architecture.json -e PRD_FILE=docs/PRD.md --output 'prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt' pdd/templates/generic/generate_prompt.prompt
86
+ - name: From architecture.json
87
+ command: pdd generate -e MODULE=orders_api -e LANG_OR_FRAMEWORK=Python -e LAYER=api -e ARCHITECTURE_FILE=architecture.json --output 'prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt' pdd/templates/generic/generate_prompt.prompt
88
+
89
+ discover:
90
+ enabled: false
91
+ max_per_pattern: 5
92
+ max_total: 10
93
+ ---
94
+
95
+ % You are an expert prompt writer and software architect for PDD. Your goal is to write a high-quality prompt that will generate the code for the ${MODULE} module/component. The prompt you create will be used to produce a detailed implementation specification in a file named ${MODULE}_${LANG_OR_FRAMEWORK}.prompt, suitable for the specified stack and layer.
96
+
97
+ IMPORTANT: Your reply MUST begin with `<prompt>` on the very first line and end with `</prompt>` on the final line. Do not include any text, whitespace, or code fences outside this block.
98
+
99
+ % Project context (architecture required, others optional):
100
+ <prd><include>${PRD_FILE}</include></prd>
101
+ <api><include>${API_DOC_FILE}</include></api>
102
+ <database><include>${DB_SCHEMA_FILE}</include></database>
103
+ <backend_files_csv><include>${BACKEND_FILES_CSV}</include></backend_files_csv>
104
+ <io_dependencies_csv><include>${IO_DEPENDENCIES_CSV}</include></io_dependencies_csv>
105
+ <architecture><include>${ARCHITECTURE_FILE}</include></architecture>
106
+ <tech_stack><include>${TECH_STACK_FILE}</include></tech_stack>
107
+ <generate_code_cli_example><include>${CODE_GENERATOR_PROMPT}</include></generate_code_cli_example>
108
+
109
+ % Existing prompt references (optional):
110
+ <existing_backend_prompts><include-many>${EXISTING_PROMPTS}</include-many></existing_backend_prompts>
111
+
112
+ % Do the following:
113
+ - Explain concisely what you are going to do (create a prompt for the ${MODULE} module/component for the specified layer and stack).
114
+ - Analyze any difficulties this prompt might encounter for ${MODULE} (e.g., data modeling, API or UI contracts, transactions, idempotency, auth, state management, error handling) and briefly state mitigation strategies tailored to the given LAYER and LANG_OR_FRAMEWORK.
115
+ - Use the ARCHITECTURE_FILE to identify the item that corresponds to this prompt by matching `filename` to `${MODULE}_${LANG_OR_FRAMEWORK}.prompt` (or best match by basename and layer). Use that item’s `reason`, `description`, `dependencies`, `interface`, and `tags` to shape the sections below.
116
+ - Then create the prompt content for ${MODULE} inside XML tags named prompt, ensuring conventions fit the stack and layer.
117
+ - Ensure the final response consists solely of the `<prompt>...</prompt>` block; nothing else (including whitespace) may appear before `<prompt>` or after `</prompt>`.
118
+
119
+ % The prompt you generate must follow this structure:
120
+ 1) First paragraph: describe the role and responsibility of the ${MODULE} module/component within the system (consider the LAYER if provided).
121
+ 2) A "Requirements" section with numbered points covering functionality, contracts, error handling, validation, logging, performance, and security.
122
+ 3) A "Dependencies" section using XML include tags for each dependency (see format below).
123
+ 4) An "Instructions" section with precise implementation guidance (clarify inputs/outputs, function/class responsibilities, edge cases, and testing notes).
124
+ 5) A clear "Deliverable" section describing the expected code artifacts and entry points.
125
+
126
+ % Dependencies format and conventions:
127
+ - Represent each dependency using an XML tag with the dependency name, and put the file path inside an <include> tag, e.g.:
128
+ <orders_service>
129
+ <include>context/orders_service_example.${DEP_EXAMPLE_EXT}</include>
130
+ </orders_service>
131
+ - Prefer real example files available in the provided context (use <include-many> when listing multiple). If examples are not provided, assume dependency examples live under context/ using the pattern context/[dependency_name]_example.${DEP_EXAMPLE_EXT}.
132
+ - Include all necessary dependencies for the module/component (based on the provided context and references).
133
+ - The ARCHITECTURE_FILE lists `dependencies` referencing other prompt filenames. Convert each dependency prompt filename into a sensible dependency name (strip language suffix and `_prompt`), and map to context files with the `${DEP_EXAMPLE_EXT}` extension if present; otherwise, list the prompt filename explicitly in a "Prompt Dependencies" subsection.
134
+
135
+ % Architecture awareness (ARCHITECTURE_FILE is required):
136
+ - Align the "Requirements" and "Instructions" with the selected item’s `interface.type` (e.g., page, component, module, api, graphql, cli, job, message, config).
137
+ - For `api`, outline endpoints (method, path, auth) consistent with the architecture description; for `page`/`component`, describe route/props/data sources; for `job`, include trigger and retry policy; for `config`, list keys and sources.
138
+
139
+ % Style and quality requirements:
140
+ - The generated prompt must be detailed enough to yield production-ready code.
141
+ - Match the style and patterns of existing *_${LANG_OR_FRAMEWORK}.prompt files when present.
142
+ - Do not invent technologies or files; rely on the included context. If assumptions are necessary, state them explicitly and conservatively.
143
+
144
+ % Output contract:
145
+ - Start the output with `<prompt>` on its own line and end with `</prompt>` on its own line.
146
+ - Do not emit any characters (including whitespace, markdown fences, or commentary) outside the `<prompt>...</prompt>` block.
147
+ - Within the tags, include the sections described above as plain text.
148
+ - OUTPUT FORMAT (authoritative – copy/paste and replace the bracketed placeholders, keeping every literal token):
149
+ ```text
150
+ <prompt>
151
+ {ROLE_PARAGRAPH}
152
+ Requirements
153
+ 1. {REQ_ITEM_1}
154
+ 2. {REQ_ITEM_2}
155
+ Dependencies
156
+ <{DEPENDENCY_TAG_1}>
157
+ <include>{DEPENDENCY_INCLUDE_1}</include>
158
+ </{DEPENDENCY_TAG_1}>
159
+ {OPTIONAL_ADDITIONAL_DEPENDENCY_TAGS}
160
+ Prompt Dependencies:
161
+ {PROMPT_DEPENDENCIES_SECTION}
162
+ Instructions
163
+ - {INSTRUCTION_1}
164
+ - {INSTRUCTION_2}
165
+ Deliverable
166
+ - {DELIVERABLE_1}
167
+ - {DELIVERABLE_2}
168
+ Implementation assumptions (explicit)
169
+ - {ASSUMPTION_1}
170
+ - {ASSUMPTION_2}
171
+ Please produce production-ready prompt content that will generate the module consistent with the above.
172
+ </prompt>
173
+ ```
174
+ Replace each `{PLACEHOLDER}` with concrete content while preserving the surrounding structure and literal `<prompt>` / `<include>` tags.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdd-cli
3
- Version: 0.0.60
3
+ Version: 0.0.62
4
4
  Summary: PDD (Prompt-Driven Development) Command Line Interface
5
5
  Author: Greg Tanaka
6
6
  Author-email: glt@alumni.caltech.edu
@@ -53,7 +53,7 @@ Requires-Dist: build; extra == "dev"
53
53
  Requires-Dist: twine; extra == "dev"
54
54
  Dynamic: license-file
55
55
 
56
- .. image:: https://img.shields.io/badge/pdd--cli-v0.0.60-blue
56
+ .. image:: https://img.shields.io/badge/pdd--cli-v0.0.62-blue
57
57
  :alt: PDD-CLI Version
58
58
 
59
59
  .. image:: https://img.shields.io/badge/Discord-join%20chat-7289DA.svg?logo=discord&logoColor=white&link=https://discord.gg/Yp4RTh8bG7
@@ -130,7 +130,7 @@ After installation, verify:
130
130
 
131
131
  pdd --version
132
132
 
133
- You'll see the current PDD version (e.g., 0.0.60).
133
+ You'll see the current PDD version (e.g., 0.0.62).
134
134
 
135
135
  Getting Started with Examples
136
136
  -----------------------------
@@ -1,4 +1,4 @@
1
- pdd/__init__.py,sha256=DkLNT664TDBz1q2bKJ1z7tY7zRdVTF6dTex1j5DokZU,633
1
+ pdd/__init__.py,sha256=41MUueYvzpXybf55GOfmYTViLeVAZihThf7Y-8g_YI4,633
2
2
  pdd/auto_deps_main.py,sha256=cpP3bbzVL3jomrGinpzTxzIDIC8tmDDYOwUAC1TKRaw,3970
3
3
  pdd/auto_include.py,sha256=OJcdcwTwJNqHPHKG9P4m9Ij-PiLex0EbuwJP0uiQi_Y,7484
4
4
  pdd/auto_update.py,sha256=w6jzTnMiYRNpwQHQxWNiIAwQ0d6xh1iOB3xgDsabWtc,5236
@@ -6,7 +6,7 @@ pdd/bug_main.py,sha256=EtaGTuucQ7VgqOhyg4o6GFG7_QtTsDPTrRdGJWT648M,4841
6
6
  pdd/bug_to_unit_test.py,sha256=BoQqNyKQpBQDW8-JwBH_RX4RHRSiU8Kk3EplFrkECt0,6665
7
7
  pdd/change.py,sha256=Hg_x0pa370-e6oDiczaTgFAy3Am9ReCPkqFrvqv4U38,6114
8
8
  pdd/change_main.py,sha256=04VHiO_D-jlfeRn6rrVH7ZTA5agXPoJGm1StGI8--XY,27804
9
- pdd/cli.py,sha256=c5Gco_Ra1ZCZf1MtxrNZdySDhZqICHBQMSH5VBqVPEw,55162
9
+ pdd/cli.py,sha256=qjDBwwwE-sTWFqKTJOIiYh2nuimlTTgXtMDE0RUuVaU,60805
10
10
  pdd/cmd_test_main.py,sha256=M-i5x26ORXurt_pu8x1sgLAyVIItbuRThiux4wBg3Ls,7768
11
11
  pdd/code_generator.py,sha256=AxMRZKGIlLh9xWdn2FA6b3zSoZ-5TIZNIAzqjFboAQs,4718
12
12
  pdd/code_generator_main.py,sha256=UtoskalEPpMAvCO-zd6xmr1lbQqSWQ7BvYgNJCybqok,35151
@@ -50,7 +50,7 @@ pdd/pdd_completion.sh,sha256=xgx-g6aeCCrlh6loeLyJN5jCsX15YXrWyT1U499p3C0,6490
50
50
  pdd/pdd_completion.zsh,sha256=V9-V8jqw3osjlXNOvjYMJf0E771-_EQe-Cboo1xzPvY,17090
51
51
  pdd/postprocess.py,sha256=mNw3iSDxE-eTYo3QwJCj_EmdEnnB5ysUN62YPapC_IM,4433
52
52
  pdd/postprocess_0.py,sha256=OW17GyCFLYErCyWh2tL4syuho3q2yFf2wyekQ4BLdPM,2168
53
- pdd/preprocess.py,sha256=75-J1smdi1Uq7gRQRLtVdkIfwltkeIvIZE-TkxxxCz0,12326
53
+ pdd/preprocess.py,sha256=STmC_e4ST253jrhYtya1GfGBDIECu2n3P68S0qyGIxs,17779
54
54
  pdd/preprocess_main.py,sha256=WGhOB9qEu7MmFoyXNml_AmqGii73LJWngx4kTlZ526k,3262
55
55
  pdd/process_csv_change.py,sha256=ckNqVPRooWVyIvmqjdEgo2PDLnpoQ6Taa2dUaWGRlzU,27926
56
56
  pdd/pytest_output.py,sha256=IrRKYneW_F6zv9WaJwKFGnOBLFBFjk1CnhO_EVAjb9E,6612
@@ -72,8 +72,8 @@ pdd/update_main.py,sha256=SWCd7Us3YatrDR7B66dQCpRCIgQoMHysPzxa4dedVmk,4385
72
72
  pdd/update_model_costs.py,sha256=RfeOlAHtc1FCx47A7CjrH2t5WXQclQ_9uYtNjtQh75I,22998
73
73
  pdd/update_prompt.py,sha256=zc-HiI1cwGBkJHVmNDyoSZa13lZH90VdB9l8ajdj6Kk,4543
74
74
  pdd/xml_tagger.py,sha256=5Bc3HRm7iz_XjBdzQIcMb8KocUQ8PELI2NN5Gw4amd4,4825
75
- pdd/data/language_format.csv,sha256=shimgYTHMJtfKQ1wpjjLto27TaMKU4OzNJ4hLeoXHms,957
76
- pdd/data/llm_model.csv,sha256=UxllgS0btSpCKpPgPnaTFAtZsAynBhLeZyoIVo0Tpwc,1698
75
+ pdd/data/language_format.csv,sha256=i4AfibdhmMnx_xAnv5jHA8TGSftjOJnLcKE53kS7kLY,1010
76
+ pdd/data/llm_model.csv,sha256=u7naNW110fejsV443qlzs0_TmCAzxa8EJogjmmJSAZs,1702
77
77
  pdd/prompts/auto_include_LLM.prompt,sha256=sNF2rdJu9wJ8c0lwjCfZ9ZReX8zGXRUNehRs1ZiyDoc,12108
78
78
  pdd/prompts/bug_to_unit_test_LLM.prompt,sha256=KdMkvRVnjVSf0NTYIaDXIMT93xPttXEwkMpjWx5leLs,1588
79
79
  pdd/prompts/change_LLM.prompt,sha256=5rgWIL16p3VRURd2_lNtcbu_MVRqPhI8gFIBt1gkzDQ,2164
@@ -110,10 +110,11 @@ pdd/prompts/trim_results_start_LLM.prompt,sha256=OKz8fAf1cYWKWgslFOHEkUpfaUDARh3
110
110
  pdd/prompts/unfinished_prompt_LLM.prompt,sha256=vud_G9PlVv9Ig64uBC-hPEVFRk5lwpc8pW6tOIxJM4I,5082
111
111
  pdd/prompts/update_prompt_LLM.prompt,sha256=prIc8uLp2jqnLTHt6JvWDZGanPZipivhhYeXe0lVaYw,1328
112
112
  pdd/prompts/xml_convertor_LLM.prompt,sha256=YGRGXJeg6EhM9690f-SKqQrKqSJjLFD51UrPOlO0Frg,2786
113
- pdd/templates/architecture/architecture_json.prompt,sha256=uSNSsKTL-cuMMhi5a4GSpC94DKkOFAlXh7R0CUlo-hg,8126
114
- pdd_cli-0.0.60.dist-info/licenses/LICENSE,sha256=kvTJnnxPVTYlGKSY4ZN1kzdmJ0lxRdNWxgupaB27zsU,1066
115
- pdd_cli-0.0.60.dist-info/METADATA,sha256=4vSMytmX5LPnADjAlOVkZPCSf--g5FWbATtu9zo7Qqs,12687
116
- pdd_cli-0.0.60.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
117
- pdd_cli-0.0.60.dist-info/entry_points.txt,sha256=Kr8HtNVb8uHZtQJNH4DnF8j7WNgWQbb7_Pw5hECSR-I,36
118
- pdd_cli-0.0.60.dist-info/top_level.txt,sha256=xjnhIACeMcMeDfVNREgQZl4EbTni2T11QkL5r7E-sbE,4
119
- pdd_cli-0.0.60.dist-info/RECORD,,
113
+ pdd/templates/architecture/architecture_json.prompt,sha256=omwivayKRP87_PJXqmUPEOwWItZ10b42tnn1aLoslGE,8986
114
+ pdd/templates/generic/generate_prompt.prompt,sha256=4PhcNczpYpwSiaGt0r2f-vhSO3JFqeU1fTEy6YpPudQ,10758
115
+ pdd_cli-0.0.62.dist-info/licenses/LICENSE,sha256=kvTJnnxPVTYlGKSY4ZN1kzdmJ0lxRdNWxgupaB27zsU,1066
116
+ pdd_cli-0.0.62.dist-info/METADATA,sha256=Ha871-qxdSiVohQUS1P-kfhePM1SBYMzYWmtGojHaMw,12687
117
+ pdd_cli-0.0.62.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
118
+ pdd_cli-0.0.62.dist-info/entry_points.txt,sha256=Kr8HtNVb8uHZtQJNH4DnF8j7WNgWQbb7_Pw5hECSR-I,36
119
+ pdd_cli-0.0.62.dist-info/top_level.txt,sha256=xjnhIACeMcMeDfVNREgQZl4EbTni2T11QkL5r7E-sbE,4
120
+ pdd_cli-0.0.62.dist-info/RECORD,,