# events-calendar-bot A weekly job that curates upcoming NYC events matching a fixed taste profile and writes them to a private Google Calendar named **"Events"**, idempotently (re-runs never create duplicates). It runs as a **Claude Code scheduled routine** (claude.ai/code/routines): the routine itself web-searches and writes to the calendar through the Google Calendar connector. There is no server, no Anthropic API key, and no Google service account — the only deployed artifact is the routine prompt. ## Layout | File | Role | |---|---| | `ROUTINE_PROMPT.md` | The paste-ready routine prompt. **This is the deliverable** — paste it into the routine. Generated from the template + `reconcile.py`. | | `ROUTINE_PROMPT.template.md` | Editable source for the prompt (everything except the embedded script). | | `reconcile.py` | Deterministic dedup + a CLI. Embedded verbatim into `ROUTINE_PROMPT.md`; the routine writes it to `/tmp` and runs it. Pure stdlib, zero deps. | | `tests/test_reconcile.py` | Unit tests, built from real calendar data. | ## How dedup works (the thing that must not break) Each run lists what's already on the Events calendar and runs `reconcile.py` to drop any candidate already present. Identity is `(normalized title, start date)` with **fuzzy** title matching — it strips a trailing `(tickets req'd)` / `(day-of option)` tag and a trailing `— Venue` segment, then compares by token overlap — so the same event reported with a slightly different title across runs still dedups. It does **not** rely on a stored key: the claude.ai Calendar connector's `create_event` can't write `extendedProperties`, and the existing hand-created events have no key, so matching works purely from title + date. ## Local test (no network, no calendar access) ```sh python3 -m unittest discover -s tests -v # or run the deduper against two JSON files (candidate list + a calendar export): python3 reconcile.py candidates.json existing.json --explain ``` ## Regenerate the prompt after editing If you change `reconcile.py` or `ROUTINE_PROMPT.template.md`, rebuild the prompt: ```sh python3 - <<'PY' import pathlib tpl = pathlib.Path('ROUTINE_PROMPT.template.md').read_text() src = pathlib.Path('reconcile.py').read_text().rstrip('\n') pathlib.Path('ROUTINE_PROMPT.md').write_text(tpl.replace('<<>>', src)) PY ``` ## Set up the routine (one time) 1. Open **claude.ai/code/routines** → create a routine. 2. **Prompt:** paste the full contents of `ROUTINE_PROMPT.md`, then replace `` with your calendar's ID and edit the curation profile to your own interests and neighborhoods. 3. **Connectors:** include the **Google Calendar** connector (it's opt-in per routine — remove the others to limit tool access). No GitHub repo is needed. 4. **Schedule:** weekly, **Thursday 11:00 America/New_York** (routines floor at 1-hour intervals; weekly is fine). 5. **Network:** default/trusted is enough (web search + the connector). 6. **Dry run first:** the prompt ships with `DRY_RUN = true`. Use **"Run now"** and review the printed plan — this is also the end-to-end check that the calendar connector works inside a scheduled run. 7. **Go live:** edit the prompt's `DRY_RUN` to `false`, save, and enable the schedule. ## Target calendar - Name: **Events** (secondary calendar) - ID: `` (your secondary calendar, e.g. `c_xxxxxxxx@group.calendar.google.com`) - Time zone: `America/New_York` - Events are written non-blocking (`availability: AVAILABILITY_FREE`) and tagged Graphite (`colorId 8`).