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