Travel assistant for NanoClaw: byAir flight notifications (delay, gate, connection risk, inbound aircraft delay, time-to-leave, arrival logistics), traffic-aware drive planning for in-person meetings (auto drive blocks + leave-by traffic rechecks), travel-booking gap checks, and nightly TripIt sync. Per-chat overlay plugin.
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
"""Calendar events around a flight, for the day-before sanity check (#300).
The `day_before` compose lists the operator's calendar events near a flight.
Two parts of that are fixed predicates, not judgment, so they live here rather
than in the skill prose (`coding-policy: script-delegation`):
- Which events count. An event the operator declined (their own attendee row
carries `responseStatus: declined`) or one its organizer cancelled is not a
conflict. #300's notification framed a declined football practice as one.
- What clock an event reads on. Each event's instant is rendered in the
operator's current zone, never on the event's raw offset. A source event can
come home from a trip still carrying a `+02:00` offset while labelled
`America/Chicago` (#300, #301): the instant is right, the offset is not the
clock the operator is reading.
The window is `CONFLICT_MARGIN` either side of the flight's effective
departure and arrival: byAir truth when published, else scheduled — the same
instants the boarding block uses (`calendar_reconcile._effective_times`).
Pure: the caller fetches the events and resolves the operator's zone.
`scripts/day-before-calendar.py` is the caller.
stdlib-only per `coding-policy: dependency-management`.
"""
from __future__ import annotations
import sys
from datetime import date, datetime, timedelta, tzinfo
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from calendar_reconcile import _effective_times
# How far either side of the flight an event still counts as worth a mention.
CONFLICT_MARGIN = timedelta(hours=3)
_DISPLAY_FORMAT = "%a %b %d, %H:%M"
def _parse_instant(raw: object) -> datetime | None:
"""An RFC 3339 string as a tz-aware datetime, or None.
A trailing `Z` is normalized first; a naive result is not an instant and
reads as None rather than being guessed into a zone.
"""
if not isinstance(raw, str) or not raw:
return None
try:
parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
except ValueError:
return None
if parsed.tzinfo is None or parsed.utcoffset() is None:
return None
return parsed
def conflict_window(state: dict) -> tuple[datetime, datetime]:
"""The `[start, end]` span an event must overlap to be listed.
Raises `ValueError` when the flight's effective departure or arrival does
not parse: a window anchored on nothing would list the wrong day.
"""
dep_raw, arr_raw = _effective_times(state)
dep, arr = _parse_instant(dep_raw), _parse_instant(arr_raw)
if dep is None or arr is None:
raise ValueError(
f"flight {state.get('flight_id')!r} has no parseable departure/arrival "
f"({dep_raw!r} / {arr_raw!r}) — re-run the precheck to refresh its state"
)
return dep - CONFLICT_MARGIN, arr + CONFLICT_MARGIN
def resolve_zone(operator_tz: str | None) -> ZoneInfo | None:
"""The operator's zone as a `ZoneInfo`, or None.
None for no name, and for a name `ZoneInfo` cannot resolve — that one with
a stderr line naming it. The caller reports the zone it actually used, so
an unresolvable name is never passed on as though it were the operator's.
"""
if not operator_tz:
return None
try:
return ZoneInfo(operator_tz)
except (ZoneInfoNotFoundError, ValueError) as exc:
print(
f"day_before_calendar: operator zone {operator_tz!r} does not resolve ({exc}); "
"event times keep their own offsets — check the host tz_state row the core "
"current-tz reader serves",
file=sys.stderr,
)
return None
def _offset_label(when: datetime) -> str:
"""`UTC-05:00` for a tz-aware datetime: the clock a no-zone time is on."""
offset = when.utcoffset()
assert offset is not None # _parse_instant only returns aware datetimes
minutes = int(offset.total_seconds() // 60)
sign = "-" if minutes < 0 else "+"
hours, mins = divmod(abs(minutes), 60)
return f"UTC{sign}{hours:02d}:{mins:02d}"
def _self_declined(attendees: object) -> bool:
"""True when the operator's own attendee row (`self: true`) declined."""
if not isinstance(attendees, list):
return False
return any(
isinstance(a, dict) and a.get("self") is True and a.get("responseStatus") == "declined"
for a in attendees
)
def _bounds(event: dict, date_zone: tzinfo) -> tuple[datetime, datetime, bool] | None:
"""`(start, end, all_day)` for an event, or None when its times do not parse.
An all-day event's `date` is a calendar-local day, not a UTC one: it spans
midnight to its exclusive end `date`'s midnight in `date_zone`. At UTC
midnight instead, a Sep 10 all-day event in Chicago would end at 19:00 on
Sep 10 and miss a flight that evening.
"""
start, end = event.get("start"), event.get("end")
if not isinstance(start, dict) or not isinstance(end, dict):
return None
if "date" in start and "dateTime" not in start:
try:
first = date.fromisoformat(str(start["date"]))
after = date.fromisoformat(str(end.get("date", start["date"])))
except ValueError:
return None
return (
datetime(first.year, first.month, first.day, tzinfo=date_zone),
datetime(after.year, after.month, after.day, tzinfo=date_zone),
True,
)
begins, ends = _parse_instant(start.get("dateTime")), _parse_instant(end.get("dateTime"))
if begins is None or ends is None:
return None
return begins, ends, False
def calendar_conflicts(
events: list[dict],
*,
window: tuple[datetime, datetime],
zone: ZoneInfo | None,
) -> tuple[list[dict], int]:
"""The events worth listing, and how many were skipped as declined/cancelled.
Returns `(conflicts, skipped)`. A conflict is an event overlapping `window`
that the operator has not declined and its organizer has not cancelled,
ordered by start. Each is `{"summary", "location", "all_day", "start",
"end", "display"}`: `start` / `end` are ISO instants on the operator's
clock (`zone`, from `resolve_zone`), or on the event's own offset when
`zone` is None; `display` is the human form (`Thu Sep 10, 18:00–20:10`, or
the date for an all-day event), with the event's own UTC offset appended
when `zone` is None (`Thu Sep 10, 18:00–20:10 (UTC-05:00)`). An all-day
event's days are read in `zone`, else in the window's own offset. An event
whose times do not parse is left out — there is no instant to place it by.
"""
window_start, window_end = window
date_zone = zone or window_start.tzinfo
assert date_zone is not None # conflict_window returns tz-aware bounds
listed: list[tuple[datetime, dict]] = []
skipped = 0
for event in events:
if not isinstance(event, dict):
continue
bounds = _bounds(event, date_zone)
if bounds is None:
continue
begins, ends, all_day = bounds
if not (begins < window_end and window_start < ends):
continue
if event.get("status") == "cancelled" or _self_declined(event.get("attendees")):
skipped += 1
continue
if all_day:
display = begins.date().strftime("%a %b %d") + " (all day)"
start_out, end_out = begins.date().isoformat(), ends.date().isoformat()
else:
local_begin = begins.astimezone(zone) if zone else begins
local_end = ends.astimezone(zone) if zone else ends
display = f"{local_begin.strftime(_DISPLAY_FORMAT)}–{local_end.strftime('%H:%M')}"
if zone is None:
# Not the operator's clock: say whose it is, so a stale source
# offset is never read as operator-local.
display += f" ({_offset_label(local_begin)})"
start_out, end_out = local_begin.isoformat(), local_end.isoformat()
location = event.get("location")
listed.append(
(
begins,
{
"summary": str(event.get("summary") or ""),
"location": location if isinstance(location, str) and location else None,
"all_day": all_day,
"start": start_out,
"end": end_out,
"display": display,
},
)
)
listed.sort(key=lambda pair: pair[0])
return [entry for _, entry in listed], skipped.tessl-plugin
skills
check-travel-bookings
drive-engine
expertflyer
flight-assist
references
nightly-travel-sync
sync-tripit