#!/usr/bin/env python3 """Calendar collector using existing gcal skill.""" import os import sys from datetime import datetime, timedelta from pathlib import Path def fetch_events(mode: str = "today") -> list: """Fetch calendar events directly using gmail_mcp library.""" os.environ.setdefault( "GMAIL_CREDENTIALS_PATH", os.path.expanduser("~/.gmail-mcp/credentials.json") ) try: # Add gmail venv to path venv_site = Path.home() / ".claude/mcp/gmail/venv/lib/python3.14/site-packages" if str(venv_site) not in sys.path: sys.path.insert(0, str(venv_site)) from gmail_mcp.utils.GCP.gmail_auth import get_calendar_service service = get_calendar_service() now = datetime.utcnow() if mode == "today": start = now.replace(hour=0, minute=0, second=0, microsecond=0) end = start + timedelta(days=1) elif mode == "tomorrow": start = (now + timedelta(days=1)).replace( hour=0, minute=0, second=0, microsecond=0 ) end = start + timedelta(days=1) else: start = now end = now + timedelta(days=7) events_result = ( service.events() .list( calendarId="primary", timeMin=start.isoformat() + "Z", timeMax=end.isoformat() + "Z", singleEvents=True, orderBy="startTime", maxResults=20, ) .execute() ) return events_result.get("items", []) except Exception as e: return [{"error": str(e)}] def format_events(today_events: list, tomorrow_events: list = None) -> str: """Format calendar events - no LLM needed, structured data.""" lines = [] # Today's events if today_events and (len(today_events) == 0 or "error" not in today_events[0]): if not today_events: lines.append("No events today") else: for event in today_events: start = event.get("start", {}) time_str = "" if "dateTime" in start: # Timed event dt = datetime.fromisoformat( start["dateTime"].replace("Z", "+00:00") ) time_str = dt.strftime("%I:%M %p").lstrip("0") elif "date" in start: time_str = "All day" summary = event.get("summary", "(No title)") duration = "" # Calculate duration if end time available end = event.get("end", {}) if "dateTime" in start and "dateTime" in end: start_dt = datetime.fromisoformat( start["dateTime"].replace("Z", "+00:00") ) end_dt = datetime.fromisoformat( end["dateTime"].replace("Z", "+00:00") ) mins = int((end_dt - start_dt).total_seconds() / 60) if mins >= 60: hours = mins // 60 remaining = mins % 60 duration = ( f" ({hours}h{remaining}m)" if remaining else f" ({hours}h)" ) else: duration = f" ({mins}m)" lines.append(f" • {time_str} - {summary}{duration}") elif today_events and "error" in today_events[0]: error = today_events[0].get("error", "Unknown") lines.append(f"⚠️ Could not fetch calendar: {error}") else: lines.append("No events today") # Tomorrow preview if tomorrow_events is not None: if tomorrow_events and ( len(tomorrow_events) == 0 or "error" not in tomorrow_events[0] ): count = len(tomorrow_events) if count > 0: first = tomorrow_events[0] start = first.get("start", {}) if "dateTime" in start: dt = datetime.fromisoformat( start["dateTime"].replace("Z", "+00:00") ) first_time = dt.strftime("%I:%M %p").lstrip("0") else: first_time = "All day" lines.append( f"Tomorrow: {count} event{'s' if count > 1 else ''}, first at {first_time}" ) else: lines.append("Tomorrow: No events") return "\n".join(lines) if lines else "No calendar data" def collect(config: dict) -> dict: """Main collector entry point.""" cal_config = config.get("calendar", {}) show_tomorrow = cal_config.get("show_tomorrow", True) today_events = fetch_events("today") tomorrow_events = fetch_events("tomorrow") if show_tomorrow else None formatted = format_events(today_events, tomorrow_events) has_error = today_events and len(today_events) == 1 and "error" in today_events[0] return { "section": "Today", "icon": "📅", "content": formatted, "raw": {"today": today_events, "tomorrow": tomorrow_events}, "error": today_events[0].get("error") if has_error else None, } if __name__ == "__main__": config = {"calendar": {"show_tomorrow": True}} result = collect(config) print(f"## {result['icon']} {result['section']}") print(result["content"])