Week 15 of 16

Build: The DVP CLI Tool

Build dvp.py — a proper command-line tool that wraps all your Prompt Vault functionality.

Day 73 75 minutes Build

Day 73 of 80

What dvp.py Will Do

You're building a single CLI tool that replaces all your separate scripts. Instead of remembering which file does what, you run:

python dvp.py list                              # see all prompts
python dvp.py generate "Golf swing at sunset"   # generate for all platforms
python dvp.py search golf                       # find prompts by keyword
python dvp.py delete 5                          # remove a specific prompt
python dvp.py stats                             # platform breakdown

One entry point. All functionality. Clean help text on every command.

Create dvp.py

Create dvp.py in your prompt-vault/ folder:

dvp.py — Part 1: setup and list/add/search/delete Python
# dvp.py — the DVP Prompt Vault CLI tool.
# Run: python dvp.py --help
import asyncio
import json
import typer
import anthropic
from dotenv import load_dotenv
from pathlib import Path
from database import Database
from logger import get_logger

load_dotenv()
logger = get_logger(__name__)

app = typer.Typer(
    name="dvp",
    help="DVP Prompt Vault — manage and generate AI video prompts.",
)

# Open the database once. It stays open for the duration of the command.
db = Database("prompt_vault.db")


@app.command()
def list(
    platform: str = typer.Option(None, "--platform", "-p", help="Filter by platform"),
):
    """List all saved prompts, optionally filtered by platform."""
    if platform:
        rows = db.get_by_platform(platform)
        typer.echo(f"\n  [{platform}] — {len(rows)} prompts\n")
    else:
        rows = db.get_all()
        typer.echo(f"\n  All prompts — {len(rows)} total\n")

    if not rows:
        typer.echo("  (empty — generate some prompts first)")
        return

    for row in rows:
        typer.echo(f"  #{row['id']} [{row['platform']}] {row['shot']}")
        preview = row["prompt"][:70] + "..." if len(row["prompt"]) > 70 else row["prompt"]
        typer.echo(f"      {preview}\n")


@app.command()
def add(
    platform: str = typer.Argument(..., help="Platform (Kling/Runway/Veo)"),
    shot: str = typer.Argument(..., help="Shot description"),
    prompt_text: str = typer.Argument(..., help="Full prompt text"),
):
    """Add a prompt manually to the vault."""
    valid = ["Kling", "Runway", "Veo"]
    cleaned = platform.strip().title()
    if cleaned not in valid:
        typer.echo(f"  Unknown platform '{platform}'. Valid: {', '.join(valid)}")
        raise typer.Exit(1)

    prompt_id = db.add_prompt(cleaned, shot, prompt_text)
    typer.echo(f"  Added prompt #{prompt_id}: [{cleaned}] {shot}")


@app.command()
def search(
    query: str = typer.Argument(..., help="Search keyword"),
):
    """Search prompts by keyword (shot description or prompt text)."""
    rows = db.search(query)
    typer.echo(f"\n  Found {len(rows)} match{'es' if len(rows) != 1 else ''} for '{query}':\n")
    for row in rows:
        typer.echo(f"  #{row['id']} [{row['platform']}] {row['shot']}")
    typer.echo()


@app.command()
def delete(
    prompt_id: int = typer.Argument(..., help="Prompt ID to delete"),
    force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
):
    """Delete a prompt by ID."""
    if not force:
        confirmed = typer.confirm(f"Delete prompt #{prompt_id}?")
        if not confirmed:
            typer.echo("Cancelled.")
            return

    if db.delete(prompt_id):
        typer.echo(f"  Deleted prompt #{prompt_id}")
    else:
        typer.echo(f"  No prompt with ID {prompt_id}")
        raise typer.Exit(1)


@app.command()
def stats():
    """Show prompt library statistics."""
    counts = db.count_by_platform()
    total = sum(counts.values())

    typer.echo(f"\n  DVP Prompt Vault — {total} prompts total\n")
    for platform, count in sorted(counts.items()):
        bar = "█" * count
        typer.echo(f"  {platform:8} {bar} {count}")
    typer.echo()


if __name__ == "__main__":
    app()

typer.Argument(...) — the ... means "required." Compare this to typer.Option(None) which has None as a default (making it optional). The ellipsis is Python's standard "required" marker.

raise typer.Exit(1) exits with a non-zero status code. In shell scripting, exit code 0 means success, anything else means failure. This lets other tools detect if your command failed.

The stats bar uses Unicode block character repeated count times. Simple but effective — you can see the distribution at a glance.

Test the Commands

python dvp.py --help
python dvp.py list
python dvp.py search golf
python dvp.py stats
python dvp.py delete 1     # will ask for confirmation
python dvp.py delete 1 -f  # skips confirmation

End of Day Checklist

Tomorrow

Day 74 adds the generate and batch commands — the async generation commands that make this tool genuinely useful for your workflow.