62 lines
3.5 KiB
Markdown
62 lines
3.5 KiB
Markdown
# 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('<<<RECONCILE_PY>>>', 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 `<YOUR_CALENDAR_ID>` 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_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`).
|