๐Ÿ“„ cli.py 8,949 bytes Apr 19, 2026 ๐Ÿ“‹ Raw

"""CLI entry point for Costco Route Optimizer."""

import argparse
import json
import sys

from costco_route.pipeline import optimize, learn_correction
from costco_route.item_memory import stats as memory_stats
from costco_route.recipe_extractor import fetch_recipe, list_recipes, get_recipe, delete_recipe, format_recipe
from costco_route.shopping_list import (
add_items as list_add_items,
add_recipe_ingredients,
format_list,
clear_list,
get_list,
check_item,
remove_item,
)

def main():
parser = argparse.ArgumentParser(
description="Costco Route Optimizer - turn a grocery list into a tactical shopping plan",
)
parser.add_argument(
"items",
nargs="*",
help="Grocery items (space-separated, or quoted for multi-word items)",
)
parser.add_argument(
"--input", "-i",
type=str,
default=None,
help="Raw input string (alternative to positional args)",
)
parser.add_argument(
"--learn",
nargs=2,
metavar=("ITEM", "ZONE"),
help="Learn an item location: --learn 'kirkland milk' 07",
)
parser.add_argument(
"--learn-notes",
type=str,
default="",
help="Add notes when learning: --learn 'milk' 07 --learn-notes 'back corner by bakery'",
)
parser.add_argument(
"--stats",
action="store_true",
help="Show learned item database stats",
)
parser.add_argument(
"--no-memory",
action="store_true",
help="Skip ChromaDB learned location overrides",
)
parser.add_argument(
"--json",
action="store_true",
help="Output raw JSON instead of formatted list",
)
parser.add_argument(
"--markdown",
action="store_true",
help="Output with markdown formatting",
)

# Recipe commands
recipe_group = parser.add_argument_group("Recipe commands")
recipe_group.add_argument(
    "--recipe",
    type=str,
    default=None,
    help="Fetch a recipe from a URL and extract ingredients",
)
recipe_group.add_argument(
    "--recipe-list",
    action="store_true",
    help="List all saved recipes in the Rolodex",
)
recipe_group.add_argument(
    "--recipe-show",
    type=str,
    default=None,
    help="Show a saved recipe by ID",
)
recipe_group.add_argument(
    "--recipe-delete",
    type=str,
    default=None,
    help="Delete a saved recipe by ID",
)

# Shopping list commands
list_group = parser.add_argument_group("Shopping list")
list_group.add_argument(
    "--list",
    action="store_true",
    help="Show the current shopping list",
)
list_group.add_argument(
    "--add",
    nargs="+",
    metavar="ITEM",
    help="Add items to the shopping list (or a recipe ID)",
)
list_group.add_argument(
    "--add-recipe",
    type=str,
    default=None,
    help="Add all ingredients from a saved recipe to the list",
)
list_group.add_argument(
    "--check",
    type=str,
    default=None,
    help="Check off an item from the list",
)
list_group.add_argument(
    "--remove",
    type=str,
    default=None,
    help="Remove an item from the list",
)
list_group.add_argument(
    "--clearlist",
    action="store_true",
    help="Clear the entire shopping list",
)

args = parser.parse_args()

# Handle --stats
if args.stats:
    s = memory_stats()
    print(f"Costco Route Memory:")
    print(f"  Items learned: {s['total_items']}")
    print(f"  Path: {s['path']}")
    return

# Handle --learn
if args.learn:
    item, zone = args.learn
    result = learn_correction(item, zone, notes=args.learn_notes)
    if "error" in result:
        print(f"โŒ {result['error']}")
        sys.exit(1)
    print(f"โœ… Learned: '{result['item']}' โ†’ Zone {result['zone']}")
    return

# Handle --recipe-list
if args.recipe_list:
    recipes = list_recipes()
    if not recipes:
        print("No saved recipes yet. Use --recipe <url> to fetch one.")
        return
    print(f"๐Ÿณ Recipe Rolodex ({len(recipes)} recipes):\n")
    for r in recipes:
        tags = ' | '.join(r.get('tags', []))
        servings = f" ยท Serves {r['servings']}" if r.get('servings') else ''
        ingredients = f" ยท {r['ingredient_count']} ingredients" if r.get('ingredient_count') else ''
        print(f"  {r['id']}  {r['title']}{servings}{ingredients}")
        if tags:
            print(f"    ๐Ÿท {tags}")
    return

# Handle --recipe-show
if args.recipe_show:
    recipe = get_recipe(args.recipe_show)
    if not recipe:
        print(f"โŒ Recipe not found: {args.recipe_show}")
        sys.exit(1)
    print(format_recipe(recipe, include_zones=True))
    return

# Handle --recipe-delete
if args.recipe_delete:
    if delete_recipe(args.recipe_delete):
        print(f"โœ… Deleted recipe: {args.recipe_delete}")
    else:
        print(f"โŒ Recipe not found: {args.recipe_delete}")
        sys.exit(1)
    return

# Handle --recipe <url>
if args.recipe:
    print(f"๐Ÿ” Fetching recipe from: {args.recipe}")
    recipe = fetch_recipe(args.recipe)
    if "error" in recipe:
        print(f"โŒ {recipe['error']}")
        sys.exit(1)
    print(format_recipe(recipe, include_zones=True))
    print(f"\n๐Ÿ’พ Saved to Rolodex as: {recipe['recipe_id']}")
    return

# Handle --list
if args.list:
    print(format_list(include_checked=True))
    return

# Handle --add
if args.add:
    # Check if it's a single recipe ID
    if len(args.add) == 1:
        recipe = get_recipe(args.add[0])
        if recipe:
            result = add_recipe_ingredients(args.add[0])
            if "error" in result:
                print(f"โŒ {result['error']}")
                sys.exit(1)
            print(f"โœ… Added {result['added']} ingredients from: {result.get('recipe_title', args.add[0])}")
            print(f"   Zones: {', '.join(result['zones'])}")
            return
    # Otherwise treat as raw items
    raw_items = " ".join(args.add)
    result = list_add_items(raw_items)
    if result['added'] == 0:
        print("All items already on the list!")
    else:
        print(f"โœ… Added {result['added']} items to the list")
        print(f"   Zones: {', '.join(result['zones'])}")
        print("   Use --list to view")
    return

# Handle --add-recipe
if args.add_recipe:
    result = add_recipe_ingredients(args.add_recipe)
    if "error" in result:
        print(f"โŒ {result['error']}")
        sys.exit(1)
    print(f"โœ… Added {result['added']} ingredients from: {result.get('recipe_title', args.add_recipe)}")
    print(f"   Zones: {', '.join(result['zones'])}")
    return

# Handle --check
if args.check:
    result = check_item(args.check)
    if "error" in result:
        print(f"โŒ {result['error']}")
        sys.exit(1)
    print(f"โœ… Checked off: {result['checked']}")
    return

# Handle --remove
if args.remove:
    result = remove_item(args.remove)
    if "error" in result:
        print(f"โŒ {result['error']}")
        sys.exit(1)
    print(f"๐Ÿ—‘ Removed: {result['removed']} (Zone {result['zone']})")
    return

# Handle --clearlist
if args.clearlist:
    result = clear_list()
    print(f"๐Ÿ—‘ Cleared {result['removed']} items from the list")
    return

# Build input from args
if args.input:
    raw_input = args.input
elif args.items:
    raw_input = " ".join(args.items)
else:
    parser.print_help()
    sys.exit(1)

# Run the pipeline
result = optimize(
    raw_input,
    use_memory=not args.no_memory,
    markdown=args.markdown,
)

if args.json:
    # JSON output โ€” strip the formatted text, include structured data
    output = {k: v for k, v in result.items() if k != "output"}
    print(json.dumps(output, indent=2, default=str))
else:
    print(result["output"])

    # Show learned overrides that were applied
    if result["learned_overrides"]:
        print(f"\n๐Ÿง  {len(result['learned_overrides'])} item(s) from learned memory")
        for item, info in result["learned_overrides"].items():
            note = f" ({info['notes']})" if info.get("notes") else ""
            print(f"  โ€ข {item} โ†’ Zone {info['zone']} [similarity: {info['similarity']}]{note}")

if name == "main":
main()