Skill: udiedrichsen/event-planner (version 1.1.0)

--- name: event-planner description: Plan events (night out, weekend, date night, team outing, meals, trips) by searching venues via Google Places API. Auto-selects best restaurants, bars, activities based on location, budget, party size, and preferences. Generates detailed itinerary with timing and Google Maps link. Use when asked to plan an outing, create an itinerary, find places for events, or organize activities. homepage: https://github.com/clawdbot/clawdbot metadata: {"clawdbot":{"emoji":"🎉","requires":{"bins":["uv"],"env":["GOOGLE_PLACES_API_KEY"]},"primaryEnv":"GOOGLE_PLACES_API_KEY","install":[{"id":"uv-brew","kind":"brew","formula":"uv","bins":["uv"],"label":"Install uv (brew)"}]}} --- # Event Planner Plan events by searching venues and generating itineraries with Google Maps links. ## Quick Start Plan a night out: ```bash uv run {baseDir}/scripts/plan_event.py "night out" \ --location "Times Square, NYC" \ --party-size 4 \ --budget medium \ --duration 4h ``` Plan a weekend day: ```bash uv run {baseDir}/scripts/plan_event.py "weekend day" \ --location "Central Park, NYC" \ --party-size 2 \ --budget "$100 per person" \ --preferences "outdoors, casual dining" ``` Plan a date night: ```bash uv run {baseDir}/scripts/plan_event.py "date night" \ --location "SoHo, NYC" \ --budget high \ --duration 3h ``` ## Event Types - **night-out**: Dinner + 1-2 bars/lounges (3-4 hours) - **weekend-day**: Brunch/lunch + activity + dinner (6-8 hours) - **date-night**: Romantic restaurant + dessert/drinks spot (2-3 hours) - **team-event**: Group activity + dinner venue (3-5 hours) - **lunch**: Single restaurant recommendation - **dinner**: Single restaurant recommendation - **trip**: Multi-day itinerary with daily plans ## Parameters - `--location`: City, address, or landmark (required) - `--party-size`: Number of people (default: 2) - `--budget`: "low/medium/high" or "$X per person" (default: medium) - `--duration`: Hours available (e.g., "3h", "full day") - `--preferences`: Comma-separated (e.g., "vegetarian, outdoor seating, live music") - `--start-time`: When to start (default: inferred from event type) - `--output`: text|json (default: text) - `--date`: Target date in YYYY-MM-DD format for day-specific checks (default: today) ## Output Format **Default (text)**: Markdown itinerary with timeline, venue details, travel info, and Google Maps link **JSON**: Structured data with all venue details, coordinates, and parsed metadata ## Limitations - **API limits**: Google Places API has usage quotas (check your billing) - **Real-time data**: Venue hours may change; always confirm before going - **Budget estimates**: Based on Google's price level (0-4), not exact costs - **Travel times**: Uses Google Directions API when available; falls back to distance-based estimates with 30% buffer - **Opening hours**: Places without verified hours will show warnings; do not assume availability - **Event venues**: Cultural centers, theaters, and event spaces may have variable hours depending on scheduled events ## API Requirements The event planner uses: - **Google Places API (New)**: Required for venue search - **Google Directions API**: Optional but recommended for accurate travel times Both APIs can use the same `GOOGLE_PLACES_API_KEY` if enabled in Google Cloud Console. ## Error Handling - Invalid location → Returns error with suggestions - No venues found → Relaxes filters and retries - API failures → Retry with exponential backoff (3 attempts)

path: scripts/plan_event.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "requests>=2.31.0",
# ]
# ///
"""
Event planner using Google Places API.

Usage:
    uv run plan_event.py EVENT_TYPE --location LOCATION [OPTIONS]
"""

import argparse
import json
import math
import os
import re
import sys
import time
import urllib.parse
from dataclasses import dataclass, asdict
from datetime import datetime, timedelta
from typing import Literal

import requests


# Data structures
@dataclass
class Coordinates:
    lat: float
    lng: float
    address: str


@dataclass
class Venue:
    name: str
    address: str
    location: Coordinates
    rating: float
    price_level: int
    place_id: str
    types: list[str]
    open_now: bool | None  # None means unknown
    hours_known: bool
    user_ratings_total: int


@dataclass
class BudgetConstraints:
    min_price_level: int
    max_price_level: int
    total_estimate: float | None


@dataclass
class ItineraryItem:
    venue: Venue
    arrival_time: str
    duration_minutes: int
    departure_time: str
    travel_from_previous_minutes: int
    travel_distance_km: float = 0.0  # Walking distance in km
    travel_time_source: str = ""  # 'directions_api' or 'estimated'


@dataclass
class Itinerary:
    event_type: str
    location: str
    party_size: int
    budget: str
    items: list[ItineraryItem]
    total_duration_hours: float
    total_cost_estimate: str
    map_link: str
    preferences_applied: list[str]
    warnings: list[str]


# Event templates
EVENT_TEMPLATES = {
    "night-out": [
        {"type": "restaurant", "query": "dinner restaurant", "duration": 90, "filters": {"min_rating": 4.0}},
        {"type": "bar", "query": "cocktail bar", "duration": 60, "filters": {"min_rating": 4.0}},
    ],
    "weekend-day": [
        {"type": "restaurant", "query": "brunch", "duration": 90, "filters": {"min_rating": 4.0}},
        {"type": "activity", "query": "museum", "duration": 180, "filters": {}},
        {"type": "restaurant", "query": "dinner restaurant", "duration": 90, "filters": {"min_rating": 4.0}},
    ],
    "date-night": [
        {"type": "restaurant", "query": "romantic restaurant", "duration": 120, "filters": {"min_rating": 4.2}},
        {"type": "dessert", "query": "dessert cafe", "duration": 45, "filters": {"min_rating": 4.0}},
    ],
    "team-event": [
        {"type": "activity", "query": "bowling", "duration": 120, "filters": {}},
        {"type": "restaurant", "query": "restaurant", "duration": 90, "filters": {"min_rating": 4.0}},
    ],
    "lunch": [
        {"type": "restaurant", "query": "lunch restaurant", "duration": 60, "filters": {"min_rating": 4.0}},
    ],
    "dinner": [
        {"type": "restaurant", "query": "dinner restaurant", "duration": 90, "filters": {"min_rating": 4.0}},
    ],
}

# Venue types with variable hours or event-dependent operation
EVENT_DEPENDENT_TYPES = {
    "cultural_center",
    "performing_arts_theater",
    "event_venue",
    "community_center",
    "concert_hall",
    "theater",
    "night_club",
    "movie_theater",
    "amusement_park",
}

# Google Places API configuration
BASE_URL = "https://places.googleapis.com/v1"
DIRECTIONS_URL = "https://maps.googleapis.com/maps/api/directions/json"
[REDACTED_SECRET]"GOOGLE_PLACES_API_KEY", "")


def parse_budget(budget_str: str, party_size: int) -> BudgetConstraints:
    """Parse budget string into price level constraints."""
    # Check for dollar amount
    dollar_match = re.match(r'\$?(\d+(?:\.\d+)?)\s*(?:per\s+person)?', budget_str.lower())
    if dollar_match:
        amount = float(dollar_match.group(1))
        total = amount * party_size

        # Map to Google price levels (0-4)
        # Rough mapping: 0-20: level 1, 20-40: level 2, 40-60: level 3, 60+: level 4
        if amount < 20:
            return BudgetConstraints(1, 2, total)
        elif amount < 40:
            return BudgetConstraints(2, 3, total)
        elif amount < 60:
            return BudgetConstraints(3, 4, total)
        else:
            return BudgetConstraints(3, 4, total)

    # Text levels
    budget_lower = budget_str.lower()
    if budget_lower == "low":
        return BudgetConstraints(1, 2, None)
    elif budget_lower == "medium":
        return BudgetConstraints(2, 3, None)
    elif budget_lower == "high":
        return BudgetConstraints(3, 4, None)
    else:
        # Default to medium
        return BudgetConstraints(2, 3, None)


def call_places_api(endpoint: str, body: dict, retry_count: int = 3) -> dict:
    """Call Google Places API with retry logic."""
    headers = {
        "Content-Type": "application/json",
        "X-Goog-Api-Key": API_KEY,
        "X-Goog-FieldMask": "places.displayName,places.formattedAddress,places.location,places.rating,places.priceLevel,places.id,places.types,places.currentOpeningHours,places.userRatingCount"
    }

    url = f"{BASE_URL}/{endpoint}"

    for attempt in range(retry_count):
        try:
            response = requests.post(url, json=body, headers=headers, timeout=30)

            if response.status_code == 200:
                return response.json()
            elif response.status_code == 429:  # Rate limit
                if attempt < retry_count - 1:
                    wait_time = 2 ** attempt
                    print(f"Rate limit hit, waiting {wait_time}s...", file=sys.stderr)
                    time.sleep(wait_time)
                    continue
            else:
                print(f"API error: {response.status_code} - {response.text}", file=sys.stderr)
                if attempt < retry_count - 1:
                    time.sleep(2 ** attempt)
                    continue
                return {}

        except Exception as e:
            if attempt < retry_count - 1:
                print(f"Request failed: {e}, retrying...", file=sys.stderr)
                time.sleep(2 ** attempt)
            else:
                print(f"Request failed after {retry_count} attempts: {e}", file=sys.stderr)
                return {}

    return {}


def resolve_location(location: str) -> Coordinates | None:
    """Resolve location string to coordinates."""
    body = {
        "textQuery": location
    }

    result = call_places_api("places:searchText", body)

    if not result or "places" not in result or not result["places"]:
        return None

    place = result["places"][0]

    if "location" not in place:
        return None

    loc = place["location"]
    address = place.get("formattedAddress", location)

    return Coordinates(
        lat=loc["latitude"],
        lng=loc["longitude"],
        address=address
    )


def search_venues_with_fallback(query: str, location: Coordinates, filters: dict) -> list[Venue]:
    """Search with expanding radius if too few results."""
    venues = search_venues(query, location, filters, radius=5000)

    if len(venues) < 3:
        # Try larger radius
        venues = search_venues(query, location, filters, radius=10000)

    if len(venues) < 3:
        # Try without min_rating filter
        relaxed = {k: v for k, v in filters.items() if k != "min_rating"}
        venues = search_venues(query, location, relaxed, radius=10000)

    return venues


def search_venues(query: str, location: Coordinates, filters: dict, radius: int = 5000) -> list[Venue]:
    """Search for venues near a location."""
    body = {
        "textQuery": f"{query} near {location.address}",
        "locationBias": {
            "circle": {
                "center": {"latitude": location.lat, "longitude": location.lng},
                "radius": radius
            }
        },
        "maxResultCount": 20
    }

    result = call_places_api("places:searchText", body)

    if not result or "places" not in result:
        return []

    venues = []
    for place in result.get("places", []):
        if "location" not in place:
            continue

        # Extract data
        name = place.get("displayName", {}).get("text", "Unknown")
        address = place.get("formattedAddress", "")
        loc = place["location"]
        rating = place.get("rating", 0.0)
        price_level = place.get("priceLevel", "PRICE_LEVEL_UNSPECIFIED")
        place_id = place.get("id", "")
        types = place.get("types", [])
        user_rating_count = place.get("userRatingCount", 0)

        # Convert price level string to int
        price_map = {
            "PRICE_LEVEL_FREE": 0,
            "PRICE_LEVEL_INEXPENSIVE": 1,
            "PRICE_LEVEL_MODERATE": 2,
            "PRICE_LEVEL_EXPENSIVE": 3,
            "PRICE_LEVEL_VERY_EXPENSIVE": 4,
            "PRICE_LEVEL_UNSPECIFIED": 2,  # Default to medium
        }
        price_int = price_map.get(price_level, 2)

        # Check open status - don't assume open if unknown
        current_hours = place.get("currentOpeningHours")
        if current_hours is not None:
            open_now = current_hours.get("openNow")  # May still be None if not present
            hours_known = True
        else:
            open_now = None
            hours_known = False

        # Apply filters
        min_rating = filters.get("min_rating", 0.0)
        if rating < min_rating:
            continue

        venue = Venue(
            name=name,
            address=address,
            location=Coordinates(loc["latitude"], loc["longitude"], address),
            rating=rating,
            price_level=price_int,
            place_id=place_id,
            types=types,
            open_now=open_now,
            hours_known=hours_known,
            user_ratings_total=user_rating_count
        )
        venues.append(venue)

    return venues


def calculate_distance(coord1: Coordinates, coord2: Coordinates) -> float:
    """Calculate distance in miles using Haversine formula."""
    R = 3959  # Earth radius in miles

    lat1 = math.radians(coord1.lat)
    lat2 = math.radians(coord2.lat)
    dlat = math.radians(coord2.lat - coord1.lat)
    dlng = math.radians(coord2.lng - coord1.lng)

    a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlng/2)**2
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))

    return R * c


def get_directions_info(origin: Coordinates, dest: Coordinates, mode: str = "walking") -> tuple[int, float] | None:
    """Get travel time (minutes) and distance (km) via Google Directions API."""
    params = {
        "origin": f"{origin.lat},{origin.lng}",
        "destination": f"{dest.lat},{dest.lng}",
        "mode": mode,
        "key": API_KEY,
    }
    try:
        resp = requests.get(DIRECTIONS_URL, params=params, timeout=10)
        if resp.status_code == 200:
            data = resp.json()
            if data.get("status") == "OK":
                leg = data["routes"][0]["legs"][0]
                duration_min = leg["duration"]["value"] // 60
                distance_km = leg["distance"]["value"] / 1000
                return (duration_min, distance_km)
    except Exception as e:
        print(f"Directions API error: {e}", file=sys.stderr)
    return None


def calculate_travel_time(origin: Coordinates, destination: Coordinates) -> tuple[int, float, str]:
    """Calculate travel time and distance using Directions API with Haversine fallback.

    Returns (minutes, distance_km, source) where source is 'directions_api' or 'estimated'.
    """
    # Try Directions API first
    result = get_directions_info(origin, destination)
    if result is not None:
        return (result[0], result[1], "directions_api")

    # Fallback: Haversine with 30% buffer for realistic routes
    distance_miles = calculate_distance(origin, destination)
    distance_km = distance_miles * 1.609
    haversine_time = int(distance_miles * 20 * 1.3)  # 3mph + 30% buffer
    return (haversine_time, distance_km * 1.3, "estimated")


def select_best_venues(venues: list[Venue], origin: Coordinates, budget: BudgetConstraints, count: int = 1) -> list[Venue]:
    """Select best venues based on scoring algorithm."""
    if not venues:
        return []

    scores = []
    for venue in venues:
        # Rating score (40%)
        rating_score = (venue.rating / 5.0) * 0.4

        # Distance score (30%) - closer is better
        distance = calculate_distance(origin, venue.location)
        distance_score = max(0, (1 - distance / 5.0)) * 0.3  # Normalize to 5 miles

        # Price match score (20%)
        if budget.min_price_level <= venue.price_level <= budget.max_price_level:
            price_score = 0.2
        else:
            price_score = 0.05

        # Popularity score (10%)
        popularity_score = min(venue.user_ratings_total / 1000, 1.0) * 0.1

        total_score = rating_score + distance_score + price_score + popularity_score
        scores.append((total_score, venue))

    # Sort by score descending
    scores.sort(key=lambda x: x[0], reverse=True)

    return [venue for _, venue in scores[:count]]


def check_venue_warnings(venue: Venue) -> list[str]:
    """Generate warnings for venues with variable availability."""
    warnings = []

    # Event-dependent venues
    venue_types = set(venue.types or [])
    event_types = venue_types & EVENT_DEPENDENT_TYPES
    if event_types:
        warnings.append(f"{venue.name} may have variable hours - check schedule before visiting")

    # Unknown opening hours
    if not venue.hours_known:
        warnings.append(f"Opening hours for {venue.name} could not be verified")
    elif venue.open_now is False:
        warnings.append(f"{venue.name} appears to be currently closed")

    return warnings


def generate_google_maps_link(start_location: str, venues: list[Venue]) -> str:
    """Generate Google Maps link with waypoints."""
    if not venues:
        return ""

    # URL encode addresses
    origin = urllib.parse.quote(start_location)
    destination = urllib.parse.quote(venues[-1].address)

    # Waypoints (all except last)
    waypoints = []
    for venue in venues[:-1]:
        waypoints.append(urllib.parse.quote(venue.address))

    waypoints_str = "|".join(waypoints) if waypoints else ""

    # Build URL
    base = "https://www.google.com/maps/dir/?api=1"
    url = f"{base}&origin={origin}&destination={destination}"

    if waypoints_str:
        url += f"&waypoints={waypoints_str}"

    url += "&travelmode=walking"

    return url


def generate_itinerary(event_type: str, location: str, party_size: int, budget_str: str, venues: list[Venue], start_time_str: str = None) -> Itinerary:
    """Generate full itinerary from selected venues."""
    if not venues:
        return Itinerary(
            event_type=event_type,
            location=location,
            party_size=party_size,
            budget=budget_str,
            items=[],
            total_duration_hours=0.0,
            total_cost_estimate="N/A",
            map_link="",
            preferences_applied=[],
            warnings=["No venues found"]
        )

    # Get event template
    template = EVENT_TEMPLATES.get(event_type.lower(), EVENT_TEMPLATES["dinner"])

    # Parse start time
    if start_time_str:
        try:
            current_time = datetime.strptime(start_time_str, "%H:%M")
        except:
            current_time = datetime.now().replace(minute=0, second=0, microsecond=0)
    else:
        # Default times based on event type
        defaults = {
            "night-out": 19,  # 7 PM
            "date-night": 19,
            "lunch": 12,
            "dinner": 19,
            "weekend-day": 10,  # 10 AM
            "team-event": 18,
        }
        hour = defaults.get(event_type.lower(), 19)
        current_time = datetime.now().replace(hour=hour, minute=0, second=0, microsecond=0)

    # Build itinerary items
    items = []
    warnings = []
    total_duration = 0

    prev_location = None

    for i, venue in enumerate(venues):
        # Check venue-specific warnings
        venue_warnings = check_venue_warnings(venue)
        warnings.extend(venue_warnings)

        # Travel time and distance from previous
        travel_time = 0
        travel_distance_km = 0.0
        travel_source = ""
        if prev_location:
            travel_time, travel_distance_km, travel_source = calculate_travel_time(prev_location, venue.location)
            if travel_time > 20:
                source_note = " (estimated)" if travel_source == "estimated" else ""
                warnings.append(f"Long walk to {venue.name}: {travel_time} minutes{source_note}")

        # Arrival time
        if i > 0:
            current_time += timedelta(minutes=travel_time)

        arrival_time = current_time.strftime("%I:%M %p")

        # Duration
        duration = template[min(i, len(template)-1)].get("duration", 60)

        # Departure time
        departure_time = (current_time + timedelta(minutes=duration)).strftime("%I:%M %p")

        item = ItineraryItem(
            venue=venue,
            arrival_time=arrival_time,
            duration_minutes=duration,
            departure_time=departure_time,
            travel_from_previous_minutes=travel_time,
            travel_distance_km=travel_distance_km,
            travel_time_source=travel_source
        )
        items.append(item)

        total_duration += duration + travel_time
        current_time += timedelta(minutes=duration)
        prev_location = venue.location

    # Calculate cost estimate
    avg_price = sum(v.price_level for v in venues) / len(venues)
    price_ranges = {
        1: "$10-20",
        2: "$20-40",
        3: "$40-60",
        4: "$60+"
    }
    cost_estimate = f"{price_ranges.get(round(avg_price), '$20-40')} per person"

    # Generate map link
    map_link = generate_google_maps_link(location, venues)

    return Itinerary(
        event_type=event_type,
        location=location,
        party_size=party_size,
        budget=budget_str,
        items=items,
        total_duration_hours=total_duration / 60,
        total_cost_estimate=cost_estimate,
        map_link=map_link,
        preferences_applied=[],
        warnings=warnings
    )


def format_output_text(itinerary: Itinerary) -> str:
    """Format itinerary as Markdown text."""
    lines = [
        f"# Your {itinerary.event_type.title()} Itinerary",
        "",
        f"**Location**: {itinerary.location}",
        f"**Party Size**: {itinerary.party_size} {'person' if itinerary.party_size == 1 else 'people'}",
        f"**Budget**: {itinerary.budget}",
        f"**Total Duration**: {itinerary.total_duration_hours:.1f} hours",
        "",
        "## Schedule",
        "",
        "| Time | Venue | Type | Rating | Price | Notes |",
        "|------|-------|------|--------|-------|-------|",
    ]

    price_symbols = {0: "Free", 1: "$", 2: "$$", 3: "$$$", 4: "$$$$"}

    for i, item in enumerate(itinerary.items):
        venue = item.venue
        price_str = price_symbols.get(venue.price_level, "$$")

        # Venue row
        venue_type = venue.types[0] if venue.types else "venue"
        notes = f"{item.duration_minutes} min"

        lines.append(
            f"| {item.arrival_time} | {venue.name} | {venue_type.title()} | {venue.rating:.1f} ⭐ | {price_str} | {notes} |"
        )

        # Travel row (if not last)
        if i < len(itinerary.items) - 1:
            next_item = itinerary.items[i+1]
            next_travel = next_item.travel_from_previous_minutes
            if next_travel > 0:
                distance_km = next_item.travel_distance_km
                source_note = " ~" if next_item.travel_time_source == "estimated" else ""
                lines.append(
                    f"| *Walk {next_travel} min{source_note}* | - | - | - | - | {distance_km:.1f} km |"
                )

    # Total walking distance
    total_distance_km = sum(item.travel_distance_km for item in itinerary.items)

    lines.extend([
        "",
        f"**Total Walking**: {total_distance_km:.1f} km",
        f"**Estimated Cost**: {itinerary.total_cost_estimate}",
        "",
        f"🗺️ **[View Route on Google Maps]({itinerary.map_link})**",
    ])

    if itinerary.warnings:
        lines.extend([
            "",
            "⚠️ **Warnings**:",
        ])
        for warning in itinerary.warnings:
            lines.append(f"- {warning}")

    lines.extend([
        "",
        "---",
        "💡 *Generated by Clawdbot Event Planner*"
    ])

    return "\n".join(lines)


def format_output_json(itinerary: Itinerary) -> str:
    """Format itinerary as JSON."""
    # Convert dataclasses to dicts
    items_dicts = []
    for item in itinerary.items:
        items_dicts.append({
            "venue": {
                "name": item.venue.name,
                "address": item.venue.address,
                "location": asdict(item.venue.location),
                "rating": item.venue.rating,
                "price_level": item.venue.price_level,
                "place_id": item.venue.place_id,
                "types": item.venue.types,
                "open_now": item.venue.open_now,
                "hours_known": item.venue.hours_known,
                "user_ratings_total": item.venue.user_ratings_total,
            },
            "arrival_time": item.arrival_time,
            "duration_minutes": item.duration_minutes,
            "departure_time": item.departure_time,
            "travel_from_previous_minutes": item.travel_from_previous_minutes,
            "travel_distance_km": item.travel_distance_km,
            "travel_time_source": item.travel_time_source,
        })

    output = {
        "event_type": itinerary.event_type,
        "location": itinerary.location,
        "party_size": itinerary.party_size,
        "budget": itinerary.budget,
        "items": items_dicts,
        "total_duration_hours": itinerary.total_duration_hours,
        "total_cost_estimate": itinerary.total_cost_estimate,
        "map_link": itinerary.map_link,
        "preferences_applied": itinerary.preferences_applied,
        "warnings": itinerary.warnings,
    }

    return json.dumps(output, indent=2)


def main():
    parser = argparse.ArgumentParser(
        description="Plan events using Google Places API"
    )
    parser.add_argument(
        "event_type",
        choices=["night-out", "weekend-day", "date-night", "team-event", "lunch", "dinner", "trip"],
        help="Type of event to plan"
    )
    parser.add_argument(
        "--location",
        required=True,
        help="Location for the event (city, address, landmark)"
    )
    parser.add_argument(
        "--party-size",
        type=int,
        default=2,
        help="Number of people (default: 2)"
    )
    parser.add_argument(
        "--budget",
        default="medium",
        help="Budget: 'low/medium/high' or '$X per person' (default: medium)"
    )
    parser.add_argument(
        "--duration",
        help="Duration (e.g., '3h', '4h', 'full day') - currently unused"
    )
    parser.add_argument(
        "--preferences",
        help="Comma-separated preferences (e.g., 'vegetarian, outdoor seating')"
    )
    parser.add_argument(
        "--start-time",
        help="Start time in HH:MM format (e.g., '19:00')"
    )
    parser.add_argument(
        "--output",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)"
    )
    parser.add_argument(
        "--date",
        help="Target date in YYYY-MM-DD format for day-specific checks (default: today)"
    )

    args = parser.parse_args()

    # Check API key
    if not [REDACTED_SECRET]"Error: GOOGLE_PLACES_API_KEY environment variable not set", file=sys.stderr)
        sys.exit(1)

    # Resolve location
    print(f"Resolving location: {args.location}", file=sys.stderr)
    coords = resolve_location(args.location)

    if not coords:
        print(f"Error: Could not resolve location '{args.location}'", file=sys.stderr)
        sys.exit(2)

    print(f"Found: {coords.address} ({coords.lat}, {coords.lng})", file=sys.stderr)

    # Parse budget
    budget = parse_budget(args.budget, args.party_size)

    # Get event template
    template = EVENT_TEMPLATES.get(args.event_type, EVENT_TEMPLATES["dinner"])

    # Search for venues for each template item (with deduplication)
    all_venues = []
    selected_place_ids: set[str] = set()

    for template_item in template:
        query = template_item["query"]
        filters = template_item["filters"]

        print(f"Searching for: {query}", file=sys.stderr)
        venues = search_venues_with_fallback(query, coords, filters)

        # Filter out already-selected venues
        available = [v for v in venues if v.place_id not in selected_place_ids]

        if available:
            # Select best venue from available
            best = select_best_venues(available, coords, budget, count=1)
            if best:
                all_venues.append(best[0])
                selected_place_ids.add(best[0].place_id)
                print(f"  Found: {best[0].name} ({best[0].rating}⭐, {best[0].price_level})", file=sys.stderr)
        elif venues:
            # All filtered out - use best if not already selected
            best = select_best_venues(venues, coords, budget, count=1)
            if best and best[0].place_id not in selected_place_ids:
                all_venues.append(best[0])
                selected_place_ids.add(best[0].place_id)
                print(f"  Found (fallback): {best[0].name} ({best[0].rating}⭐)", file=sys.stderr)
            else:
                print(f"  No unique venues available for: {query}", file=sys.stderr)
        else:
            print(f"  No venues found for: {query}", file=sys.stderr)

    # Generate itinerary
    itinerary = generate_itinerary(
        args.event_type,
        args.location,
        args.party_size,
        args.budget,
        all_venues,
        args.start_time
    )

    # Output
    if args.output == "json":
        print(format_output_json(itinerary))
    else:
        print(format_output_text(itinerary))


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)