"""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()