Travel assistant for NanoClaw: byAir flight notifications (delay, gate, connection risk, inbound aircraft delay, time-to-leave, arrival logistics), travel-booking gap checks, and nightly TripIt sync. Per-chat overlay tile.
75
93%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
"""Normalize raw Google Calendar events into the planner's event shape.
The reconcile script (PR3b) fetches events via Composio's
`GOOGLECALENDAR_FIND_EVENT`, which returns Google Calendar event resources.
`plan_reconciliation` (see `calendar_plan.py`) consumes a flat normalized
shape: `{event_id, calendar_id, summary, start, end, private_props,
is_reclaim_travel}`. This module is the deterministic adapter between the
two — pure, stdlib-only, unit-tested against the real event shapes.
is_reclaim_travel is content-based, not calendar-based. Reclaim writes its
travel blocks onto the user's primary calendar interleaved with real
meetings (there is no dedicated Reclaim calendar — see #55), so the only
safe delete discriminator is the event's own content: the Reclaim
authorship signature in the description plus a travel marker in the
summary. The planner further bounds every delete to a same-airport layover
gap, so a genuine meeting is never a delete candidate even if it somehow
carried the signature.
A real Reclaim travel block looks like:
summary: "🚌 Travel"
description: "...created by <a href='https://app.reclaim.ai/...'>Reclaim</a>...
Baruch is traveling to/from the airport for a flight..."
A real Flighty flight event looks like:
summary: "✈ BNA→YYZ • UA 8018"
start/end: {"dateTime": "2026-06-26T10:05:00-05:00", "timeZone": "..."}
(no extendedProperties — untagged, an adoption candidate)
stdlib-only per `coding-policy: dependency-management`.
"""
from __future__ import annotations
# Distinctive substring of the authorship link Reclaim stamps into every
# event description. Two-factor with the travel marker below so a user
# event is never misread as a Reclaim travel block.
RECLAIM_SIGNATURE = "app.reclaim.ai"
# Reclaim names every travel block "🚌 Travel"; its habit / focus / task
# blocks carry other summaries. Matching the word travel in the summary (on
# a Reclaim-signed event) separates travel from those non-travel blocks.
_TRAVEL_MARKER = "travel"
class NormalizeError(ValueError):
"""Raised when a raw event lacks a field normalization needs (e.g. id).
A ValueError subclass: the fix is "pass a well-formed Google event",
not "retry".
"""
def is_reclaim_travel(raw_event: dict) -> bool:
"""True when the event is a Reclaim-generated travel block.
Two factors, both required:
1. the description carries Reclaim's authorship signature, AND
2. the summary names it a travel block.
Reclaim's non-travel blocks (habit / focus / task) carry the signature
but a different summary, so they return False. A user's own event
carries neither and returns False.
"""
description = (raw_event.get("description") or "").lower()
summary = (raw_event.get("summary") or "").lower()
return RECLAIM_SIGNATURE in description and _TRAVEL_MARKER in summary
def _extract_instant(endpoint: dict | None) -> str | None:
"""Pull the RFC 3339 instant from a Google event start/end block.
Google uses `{"dateTime": "...+offset", "timeZone": "..."}` for timed
events and `{"date": "YYYY-MM-DD"}` for all-day events. Returns the
`dateTime` when present (it carries the offset the planner needs), else
the bare `date` (which the planner will reject — all-day events are not
flights or travel blocks and the reconcile filters them out before
planning), else None.
"""
if not isinstance(endpoint, dict):
return None
return endpoint.get("dateTime") or endpoint.get("date")
def normalize_event(raw_event: dict, *, calendar_id: str, classify_reclaim: bool = False) -> dict:
"""Flatten one Google Calendar event into the planner's event shape.
Args:
raw_event: a Google Calendar event resource.
calendar_id: the calendar the event was fetched from (the planner
classifies by calendar ID, so this is authoritative — not read
from the event body).
classify_reclaim: when True, run the Reclaim-travel content
classifier and set `is_reclaim_travel`. Pass True only for
events fetched from the calendar Reclaim writes to (the user's
primary); False for the byAir flight calendar.
Returns the flat shape `plan_reconciliation` consumes. `private_props`
is `extendedProperties.private` (the tags flight-assist stamps), `{}`
when absent.
Raises NormalizeError when the event has no `id`.
"""
event_id = raw_event.get("id")
if not event_id:
# Name only the keys present, never the values — the event body
# carries summary / description / attendees we must not leak into
# logs (per `coding-policy: no-secrets` and error-handling).
raise NormalizeError(
f"event is missing required 'id' field — keys present: {sorted(raw_event.keys())}"
)
extended = raw_event.get("extendedProperties") or {}
private = extended.get("private") if isinstance(extended, dict) else None
# Force a dict: the planner calls `.get()` on private_props, so a
# malformed non-mapping value (list/string from an API or client bug)
# must normalize to {} rather than crash downstream.
private_props = private if isinstance(private, dict) else {}
return {
"event_id": event_id,
"calendar_id": calendar_id,
"summary": raw_event.get("summary") or "",
"start": _extract_instant(raw_event.get("start")),
"end": _extract_instant(raw_event.get("end")),
"private_props": private_props,
"is_reclaim_travel": is_reclaim_travel(raw_event) if classify_reclaim else False,
}skills
check-travel-bookings
flight-assist
nightly-travel-sync
sync-tripit