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.
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
#!/usr/bin/env python3
"""Flag upcoming hotel stays whose TripIt `location` is not a usable address.
TripIt sometimes drops a non-address into a Lodging's `location` — most often a
resort-fee / nightly-rate note (`"Stay resort fee: $72.03"`) or a blank. A garbage
location can't anchor a drive (the drive engine resolves the wrong place or
suppresses the block) and reads as nonsense on the calendar. The ONLY fix is a
manual TripIt edit, so this script only DETECTS; the `check-travel-bookings` skill
ALERTS the operator. Nothing is auto-corrected.
Source is `/workspace/group/travel-schedule.json` — the raw TripIt-derived feed,
the only artifact that carries each Lodging's `location` (the day-keyed
`travel-db.json` drops it). One warning per upcoming stay, keyed on the
`Check-in:` record, naming the hotel, the bad location text, and the check-in
date. Silent when every upcoming stay has a plausible address.
Detection is deliberately conservative and fully enumerable per
`coding-policy: script-delegation` (the Regex Trap): it flags KNOWN non-address
shapes — a blank, a currency amount, or a rate/fee keyword — never a general
"does this look like an address?" judgment (that is reasoning, not scripting).
Real street addresses carry none of these, so false positives are near zero;
novel garbage shapes (a phone number, a booking code) are out of scope and pass
through silently rather than risk flagging a real address.
Scheduled-task-adjacent contract: single-line-free JSON on stdout, exit 0 on a
clean read (including when the schedule is missing/unreadable — schedule
freshness is `nightly-travel-sync`'s own alert surface, not this check's, so a
missing file yields an empty warning list, never a hard failure). A stderr
diagnostic records a degraded read.
stdlib-only per `coding-policy: dependency-management`.
"""
from __future__ import annotations
import json
import re
import sys
from datetime import date, datetime, timezone
SCHEDULE_PATH = "/workspace/group/travel-schedule.json"
_CHECKIN_PREFIX = "Check-in:"
# --- garbage signals (enumerable non-address shapes only) -------------------
# A currency symbol next to a number — a price/fee, never part of a street
# address. Covers `$72.03`, `€25`, `₪500`, etc.
_CURRENCY_RE = re.compile(r"[$€£₪¥₹]\s*\d")
# Rate / fee wording TripIt drops into the address field. Word-bounded so it
# can't fire on an address that merely contains one of the substrings. A bare
# "deposit" is deliberately excluded — it is a real place name ("Deposit, NY"),
# and a monetary deposit is already caught by `_CURRENCY_RE`.
_FEE_KEYWORDS_RE = re.compile(
r"\b(?:resort fee|nightly rate|room rate|per night)\b|/\s*night",
re.IGNORECASE,
)
def garbage_reason(location: object) -> str | None:
"""A short reason when `location` is not a usable address, else None.
Blank / non-string → "no address in TripIt". A currency amount or a
rate/fee keyword → the field holds a fee note, not an address. Anything else
is treated as a plausible address and passes.
"""
if not isinstance(location, str) or not location.strip():
return "no address in TripIt"
if _CURRENCY_RE.search(location) or _FEE_KEYWORDS_RE.search(location):
return "location looks like a fee/rate note, not an address"
return None
def _parse_day(value: object) -> date | None:
"""A schedule `start` string as a calendar date, else None.
Tolerates both the date-only `YYYY-MM-DD` and the timed `YYYY-MM-DDTHH:MM:SSZ`
shapes `refresh-travel-schedule.py` emits — the `[:10]` slice keeps only the
day, matching `check-travel-bookings.py`.
"""
if not isinstance(value, str) or len(value) < 10:
return None
try:
return date.fromisoformat(value[:10])
except ValueError:
return None
def find_garbage_lodging(schedule: list | None, today: date) -> list[dict]:
"""Upcoming stays whose location is garbage — one entry per Check-in record.
`today` is injected (not read from the clock) so the scan stays pure and
testable. Only stays checking in on or after `today` are considered — a past
or in-progress stay is not worth nagging about. Entries are sorted by
check-in date.
"""
warnings: list[dict] = []
for record in schedule or []:
if not isinstance(record, dict) or record.get("type") != "Lodging":
continue
summary = record.get("summary")
if not (isinstance(summary, str) and summary.startswith(_CHECKIN_PREFIX)):
continue
checkin = _parse_day(record.get("start"))
if checkin is None or checkin < today:
continue
reason = garbage_reason(record.get("location"))
if reason is None:
continue
warnings.append(
{
"hotel": summary[len(_CHECKIN_PREFIX) :].strip(),
"location": record.get("location")
if isinstance(record.get("location"), str)
else "",
"checkin": checkin.isoformat(),
"reason": reason,
}
)
warnings.sort(key=lambda w: w["checkin"])
return warnings
def load_schedule(path: str) -> list | None:
"""Read travel-schedule.json's record list, or None on any degraded read.
Missing / unreadable / non-UTF-8 / malformed / non-list root all resolve to
None — the caller emits no warnings and exits 0, because schedule freshness
is `nightly-travel-sync`'s alert surface, not this location check's. A stderr
diagnostic records the cause.
"""
try:
with open(path, encoding="utf-8") as f:
payload = json.load(f)
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
print(
f"check-lodging-locations: no usable travel schedule at {path} "
f"({type(exc).__name__}) — no location check this run",
file=sys.stderr,
)
return None
if not isinstance(payload, list):
print(
f"check-lodging-locations: travel schedule at {path} has a non-list "
"root — no location check this run",
file=sys.stderr,
)
return None
return payload
def main() -> None:
today = date.today()
schedule = load_schedule(SCHEDULE_PATH)
warnings = find_garbage_lodging(schedule, today)
output = {
"garbage_lodging": warnings,
"checked_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
print(json.dumps(output, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main().tessl-plugin
skills
check-travel-bookings
drive-engine
flight-assist
references
nightly-travel-sync
sync-tripit
travel-core