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
Low
Low-risk findings worth noting
This skill is an action router — pick the step that matches the operator's intent and execute only that step. Do not run other steps; do not parallelize.
Every alert request is check first, alert only if absent. An alert for something already bookable is worse than useless: it delays the booking while the operator waits for an email describing space they could have taken on the spot. Report the check result either way, so it is visible why no alert was set. Only skip the check when the operator explicitly says to set the alert regardless.
The browser automation, the ExpertFlyer credential and the session live in the jbaruch/expertflyer-api service. This container holds none of them — every step below is one HTTP call through skills/expertflyer/scripts/expertflyer.py, which reads EXPERTFLYER_API_URL and EXPERTFLYER_API_TOKEN.
For "is there Z on KL642", "can I use an upgrade certificate", "check business availability".
python3 /home/node/.claude/skills/tessl__expertflyer/scripts/expertflyer.py fare-class \
--origin JFK --destination AMS --date 2026-08-31 \
--airline KL --flight 642 --class ZOutputs flight, seats, available, display_capped, alternatives (other flights that day with space), recommend_alert.
seats: 0 means the bucket exists and is empty — an answer, not a missing value. display_capped: true means at least that many. Codeshares are excluded by default because inventory lives on the operating carrier; pass --include-codeshares to see them.
Report the count plainly. When available is true, say so and do not offer an alert. When false, name any alternatives and offer the alert (Step 5).
Finish here unless the operator accepts the alert.
For "is there a non-middle seat in Comfort+ on DL2957", "any window left in premium economy".
python3 /home/node/.claude/skills/tessl__expertflyer/scripts/expertflyer.py seats \
--airline DL --flight 2957 --date 2026-08-11 \
--cabin "comfort+" --want non-middle--cabin takes the cabin the operator named — premium economy, comfort+, business, first, economy — or a bare code. Premium economy is Delta's Premium Select (A) and is a different cabin from Comfort+ (W); the service rejects an unrecognised cabin rather than falling back to economy. --want accepts non-middle (aisle and window), aisle,window, middle, or any. --origin/--destination are optional — omit them and the route is resolved from the flight number.
Outputs cabin_present, seats_in_cabin, available_total, recommend_alert, the service's own criteria filter matching, and three fields the client adds by ranking the response:
ranked — bookable seats worth taking, best first, each with a why such as 12A (window)best — the top seat's description, or null when nothing is worth takingacceptable_total — how many seats are worth takingDecide on best and acceptable_total, never on matching. Ranking drops seats the operator will not take, so matching can list a seat that ranked excludes — a middle is reported by the service and refused by the ranking. Treat matching as informational only.
A response carrying error has no best, ranked or acceptable_total. Absent is not null. Go to Step 6.
On every other response:
cabin_present is false — the aircraft has no such cabin. Say so. Offer no alert.best is set — name it and say it is open. Offer no alert.best is null — nothing in the cabin is worth taking, whatever available_total says. Offer the alert (Step 5).Ranking rules live in skills/expertflyer/scripts/seat_quality.py.
Finish here unless the operator accepts the alert.
For "is 21F the best I can do on DL2957".
Step 2 answers what is open. It does not answer whether any of it beats the seat already assigned, and those are different questions. This one runs the comparison in the script, so no seat is judged by eye.
python3 /home/node/.claude/skills/tessl__expertflyer/scripts/expertflyer.py assess \
--airline DL --flight 2957 --date 2026-08-11 \
--held 21F --held-position window--held is the seat currently assigned. --held-cabin is optional; omit it and the cabin is resolved from the aircraft's row extents. --held-position is window, aisle or middle; omit it and the column is read off the open seats in the same cabin. --scan-up sets how many cabins above the held one to include — the default and its cost are in skills/expertflyer/scripts/expertflyer.py.
Get the held seat from byAir before calling this. byair_get_flight returns it as seatNumber and seatType — camelCase on read, where byair_update_booking_info takes seat_number and seat_type on write. When byAir has no seat for the flight, ask the operator for it, write it back to byAir, then call this. Never infer the seat from a previous conversation.
Do not ask for the cabin. Never read one out of byAir. Omit --held-cabin and the cabin is resolved from the aircraft. held_cabin_from reports stated or resolved.
Pass --held-cabin only when the operator names a cabin themselves, or when a response asks for it.
Responses carrying error and detail instead of a verdict:
bad_request — an unusable argument, such as an unrecognised cabin or a seat that is not a designator. cabins_absent rides along when the aircraft has no such cabin.unrankable — a seat the ranking refused. Step 6 covers it.cabin_failed and cabins_requested naming the cabin that did not load. Go to Step 6.verdict: "no_held_seat" carries verdict and detail alone. held_cabin_unresolved adds reason and cabins_absent, plus row_in_cabins on every reason but rows_unavailable. Neither carries held.
Every other response carries verdict, held, cabins_scanned, cabins_absent, cabins_unscanned, seats_compared, held_cabin_from and held_cabin_corroborated. held carries why on every shape except held_position_unknown, which has no position to describe.
optimal and upgrade add upgrades, best_upgrade, cabin_openings, alert_recommended and alert_cabins. Every other verdict adds detail and carries none of those five. Their absence is the contract, not a malformed response.
upgrades holds seats in the held cabin. The operator selects those in the airline's app.
cabin_openings holds seats in a better cabin. Those are not a seat change — taking one is a fare change or an upgrade clearance. Never tell the operator to go select one. Step 1 answers whether the upgrade inventory exists.
alert_cabins is the authoritative list of cabins to watch. Offer the alert on those and no others, never on every cabin in cabins_scanned. An empty list with alert_recommended: false means there is nothing to watch for. Selection lives in skills/expertflyer/scripts/expertflyer.py — the ALERT_RUNGS constant and the alert block in _assess.
cabins_scanned is the whole evidence base, seats_compared is its size, and acceptable_by_cabin breaks it down per cabin into seats worth taking. cabins_unscanned lists the cabins above the sweep that were never read; widen it with --scan-up to see further, which does not widen alert_cabins.
held_cabin_corroborated is true when the held seat's row is in the cabin it was assessed as, false when it is not, and null when the service could not settle it. held_cabin_from says where the cabin came from: stated from --held-cabin, resolved from the aircraft's row extents. held_cabin_source says which evidence checked it: rows is the cabin's own extent and decides it either way, seats is the older availability-derived fallback and can only ever confirm. Derivation is in skills/expertflyer/scripts/expertflyer.py.
On null, row_seen_in names the scanned cabins where the row did turn up. A non-empty list is a reason to confirm the cabin with the operator before acting on the verdict. Report the assessment either way.
Report verdict as it comes. Do not re-derive it from upgrades:
optimal — nothing the operator can select beats the held seat.
cabins_scanned.held.why.cabins_unscanned as not checked, when it is non-empty.cabin_openings when it is non-empty.alert_cabins.Never report optimal as "nothing better exists". The sweep reads cabins_scanned and stops. A cabin in cabins_unscanned may hold a better seat and was never looked at.
Never say the held seat beat a cabin. optimal compares it against the seats that were open, never against a cabin's standing. acceptable_by_cabin gives the reason per cabin: 0 is a cabin with nothing worth taking.
upgrade — an open seat in the held cabin beats it.
best_upgrade. The operator selects it in the airline's app.cabin_openings when it is non-empty.no_held_seat — no seat was passed.
held_cabin_unresolved — the layout does not say which cabin holds that row. reason says why.
detail.shared_row — ask the operator which cabin, naming those in row_in_cabins.no_such_row — check the seat and the flight with the operator.rows_unavailable — ask the operator for the cabin.--held-cabin.held_position_unknown — the seat map does not say what the column is.
--held-position.nothing_open — no open seat was found in any scanned cabin, so nothing was compared.
--scan-up.held_cabin_mismatch — the held seat's row is outside the extent of the cabin it was assessed as.
detail. It names the cabin's row range, and the cabin the row belongs to when the sweep saw it.--held-cabin.error — go to Step 6.
cabin_failed names the cabin that did not load.no_held_seat, held_position_unknown, nothing_open, held_cabin_mismatch and held_cabin_unresolved exit non-zero. None is an answer about the seat. Report no verdict on any of them.
Ranking rules live in skills/expertflyer/scripts/seat_quality.py.
Finish here unless the operator accepts the alert.
For "make sure I have the best seats", or after a new booking appears.
python3 /home/node/.claude/skills/tessl__expertflyer/scripts/upcoming-flights.py \
--now "$(date -u +%Y-%m-%dT%H:%M:%SZ)"Outputs {"flights": [...], "count": N, "trips": [...], "excluded": [...], "excluded_count": N}. Each flight carries airline, flight, origin, destination, date, departs_utc, summary and uid, soonest first.
The pass covers the next trip. --trips N widens it to the next N; --trips 0 covers every upcoming flight. The default, the lead window and the trip-edge slack are named constants in skills/expertflyer/scripts/upcoming-flights.py.
excluded holds the upcoming flights the trip bound left out. Say how many when the operator asked about their seats generally rather than about one trip. trips names what was covered.
count: 0 with a non-empty excluded means no upcoming trip covers those flights. Report that rather than reporting nothing.
Collect the held seat for every flight in one exchange before assessing any of them. Read each from byAir. Ask the operator once, in a single message, for every flight byAir has no seat for. Write each answer back to byAir. Ask for no cabins — Step 3 resolves them.
Then run Step 3 once per flight, adding --date-fallback. Read date_fallback_applied to see which date answered. Do not pass it in Step 3 for a date the operator named.
Report only the flights that need something:
upgrade → name the flight and best_upgradecabin_openings non-empty → name the cabin and seat, and say it needs a fare change or an upgrade, never a seat selectionoptimal → one line that the seat holds up across cabins_scanned, or nothing when the operator asked only for problemsno_held_seat, held_position_unknown, nothing_open, held_cabin_mismatch or held_cabin_unresolved → name the flight as unanswered, never as finecabins_absent covering the held cabin → report it; a seat cannot be in a cabin the aircraft lacksA flight whose verdict never came back is not a flight with good seats. Say which ones were not answered.
Then offer the alert once, for every flight whose alert_recommended is true, naming those flights and their alert_cabins. Step 3's alert offer is not optional here.
Finish here unless the operator accepts an alert.
Only after Step 1, 2, 3 or 4 reported the wanted thing absent, or the operator explicitly asked for the alert regardless.
# Seat alert — needs --cabin and --want
python3 /home/node/.claude/skills/tessl__expertflyer/scripts/expertflyer.py create-alert \
--kind seat --airline DL --flight 2957 --date 2026-08-11 \
--origin ATL --destination YYZ --cabin "comfort+" --want non-middle
# Fare-class alert — needs --class
python3 /home/node/.claude/skills/tessl__expertflyer/scripts/expertflyer.py create-alert \
--kind fare-class --airline KL --flight 642 --date 2026-08-31 \
--origin JFK --destination AMS --class ZRoute is required here. Steps 1 to 4 all report it. Outputs {"created": true, "alert_id": ..., "status": "ACTIVE", "verified_in_account": true}.
The service refuses to duplicate an active alert of the same kind on the same flight and class, returning {"created": false, "reason": "already_exists", "alert_id": ...}. A seat alert and a fare-class alert on one flight are different watches, so having one never blocks the other. Relay the refusal; do not retry with --force unless the operator asks.
verified_in_account comes from the service re-reading the account after submitting. Report a failure there as not created, never as success.
Finish here.
Run when a step above reports an error field.
unrankable is not an access fault. On it:
detail. It names the seat the ranking refused.On every other value:
python3 /home/node/.claude/skills/tessl__expertflyer/scripts/expertflyer.py alertsThe error value names the fault and is not re-derivable here — relay it verbatim:
unreachable — the service is down or EXPERTFLYER_API_URL is wrong. Nothing to retry until it is up.tls — the service answered but its certificate could not be verified. The endpoint is fine; the trust store is not. The detail names the fix.auth — the service could not authenticate; its detail carries ExpertFlyer's own message. It re-tries a login itself before reporting, so this means the credentials in the service need attention.blocked — ExpertFlyer's bot wall rejected the request. Never retry it in a loop.Finish here.
.tessl-plugin
skills
check-travel-bookings
drive-engine
expertflyer
flight-assist
references
nightly-travel-sync
sync-tripit