feat(events): recurring expansion, CalDAV pull sync, past search, reminders

- RRULE expansion: list_events now expands recurring events into
  individual occurrences within the query window using python-dateutil
- CalDAV pull sync: new caldav_sync.py + POST /api/events/sync route;
  imports remote events into the internal store by caldav_uid
- Past event search: search_events accepts include_past=true to search
  historical events; exposed in the LLM tool definition
- Internal reminders: migration 0037 adds reminder_minutes +
  reminder_sent_at columns; event_scheduler.py checks every 5 min and
  fires push notifications; CalDAV sync job runs hourly
- reminder_minutes now stored and returned in create/update routes + tools

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 12:15:37 -04:00
parent 358534efbf
commit edfed6b5bb
8 changed files with 454 additions and 23 deletions
+13 -1
View File
@@ -695,6 +695,10 @@ _CORE_TOOLS = [
"type": "string",
"description": "Search keyword to match against event titles, locations, and descriptions",
},
"include_past": {
"type": "boolean",
"description": "Set to true to include past events in results (default: future events only)",
},
},
"required": ["query"],
},
@@ -744,6 +748,10 @@ _CORE_TOOLS = [
"type": "string",
"description": "New iCalendar RRULE",
},
"reminder_minutes": {
"type": "integer",
"description": "Reminder N minutes before the event. Pass 0 to remove an existing reminder.",
},
},
"required": ["query"],
},
@@ -1471,7 +1479,7 @@ async def execute_tool(
"type": "events",
"data": {
"count": len(events),
"events": [e.to_dict() for e in events],
"events": events,
},
}
@@ -1479,6 +1487,7 @@ async def execute_tool(
events = await events_search_events(
user_id=user_id,
query=arguments.get("query", ""),
include_past=arguments.get("include_past", False),
)
return {
"success": True,
@@ -1502,6 +1511,9 @@ async def execute_tool(
fields[str_field] = arguments[str_field]
if arguments.get("all_day") is not None:
fields["all_day"] = arguments["all_day"]
if "reminder_minutes" in arguments:
rm = arguments["reminder_minutes"]
fields["reminder_minutes"] = None if rm == 0 else rm
for dt_field, key in (("start_dt", "start"), ("end_dt", "end")):
val = arguments.get(key)
if val: