travel-agent
A travel desk that does the two things a good agent does: find and price the trip, then produce the trip as documents someone can actually travel on.
Read references/traveller-profile.md first. That file holds the traveller's standing
preferences (loyalty programmes, fare rules, how they like to sleep and move). Treat what is
in it as a hard default, not a suggestion. If it is still the unfilled template, ask for the
handful of answers that change the recommendation rather than guessing.
Honest scope
- Can do: real bookable fare search, arbitrary multi-city and open-jaw routings, carrier and connection forcing, cheapest-date scans, fare class / RBD / aircraft detail, driving and walking times, public transport and rail itineraries, itinerary building, and the trip documents.
- Cannot do: ticket anything. Matrix prices, it does not sell. There is also no live hotel, hostel, rail-fare or car inventory here, and no live traffic. Those are researched on the web and priced on the operator's own site, and a Matrix fare can be a few units of currency off what the airline finally charges.
- Never invent a flight number, fare, aircraft type, hotel price, opening hour or visa rule. It comes from a search result or a named source, or it is labelled as an assumption out loud.
Setup, once
Configuration lives in ~/.config/travel-agent/config.env, written by travel-agent init.
It holds ITA_MATRIX_MCP (fare search), TRAVEL_CONTACT, TRIPS_DIR, HOME_TZ,
HOME_CURRENCY and HOME_AIRPORTS. Read it when you need any of those.
Fare search needs an ITA Matrix MCP server of the user's own, from
github.com/Filip-Kin/ita-matrix-mcp. If
ITA_MATRIX_MCP is unset, say so plainly and carry on: everything else (driving, walking,
transit, geocoding) needs no key and works regardless.
If the profile is still full of "not answered" lines, travel-agent profile re-runs the
setup questions. Suggest it once, do not nag.
1. Pin the constraints first
Before searching, get these straight, asking only about what actually changes the answer:
- Dates, and which of them are immovable. Most trips have one hard anchor and a lot of soft edges.
- Which part of the trip matters. Optimise that part properly and keep the rest simple.
- Cabin, budget ceiling, and whether a positioning leg or a deliberately long layover is welcome. A long layover in a city worth seeing is a feature for some travellers and a punishment for others; the profile says which.
2. Search fares
M=~/.claude/skills/travel-agent/scripts/matrix.py
"$M" airports "poznan" # resolve names to IATA / metro codes
"$M" search '<json>' # search_flights arguments as JSON (or @file, or -)
"$M" flexdates '<json>' # cheapest departure date across a range
"$M" details <searchId> <rank> # per-segment flight numbers, RBD, fare basis, aircraft
"$M" routing-ref # full routing language (offline copy in references/)
Searches take 20-60 seconds, multi-city longer, so give them a generous timeout or run them in the background, and fire independent searches together rather than one after another.
One slice per leg (2 slices = return, 3+ = multi-city or open-jaw):
{"slices":[{"origin":"DTW","destination":"VIE","date":"2026-10-18",
"routing":"UA+ IAD LH+","extraCodes":"alliance star-alliance minconnect 90"}],
"cabin":"COACH","maxStops":1,"limit":10,"currency":"USD","adults":1}
Routing recipes (full syntax in references/routing-language.md):
| Goal | Fields |
|---|---|
| One alliance only, for status and mileage credit | extraCodes: "alliance star-alliance" (or oneworld, skyteam) |
| Force a hub, because the layover is the point | routing: "UA+ IAD LH+" |
| Chase a specific aircraft | force the hub that type flies from, then confirm with details. Matrix cannot filter by type |
| Avoid basic economy | search normally, then details, then reject the fare basis / RBD that maps to basic on that carrier |
| Keep a connection humane | extraCodes: "minconnect 90 -overnight" |
| Cheaper point of sale | salesCity, an IATA city code |
| One-carrier feed for status | routing: "LH+", or C:UA to pin the marketing carrier |
maxStops is relative to the route minimum, not absolute. Empty results almost always mean
over-constraining: drop extraCodes, then loosen the carrier +, then raise maxStops, then
widen the dates.
If a call hangs: the client pins IPv4 deliberately (a host with a dead AAAA otherwise burns the
whole timeout) and caches its MCP session in ~/.cache/travel-agent/mcp-session, refreshing it
when the server has forgotten it. Check the server is up before debugging anything else.
Always run details on the shortlist before presenting it. Booking class decides mileage and
status earning, and aircraft type decides whether it is the flight they wanted.
3. Present options like a person, not a dump
Three to five options maximum. Cheapest and best clearly labelled, one short line each with price, routing, total duration, cabin, aircraft and earning class, then two lines on the real tradeoff. Lead with a recommendation. Offer to expand rather than pre-expanding.
Say plainly when an option is worse in one dimension. Do not oversell a routing because it is clever.
4. The non-flight legs: trains, beds, cars, driving
Detail in references/ground-and-beds.md. Shape comes from the routing script; price and
booking come from the operator.
R=~/.claude/skills/travel-agent/scripts/routes.py
"$R" transit "Wien Hauptbahnhof" "Praha hlavni nadrazi" --date 2026-10-21 --time 09:00
"$R" drive "Seattle, WA" "Mount Rainier National Park" "Portland, OR"
"$R" walk "Wien Hauptbahnhof" "Hostel Ruthensteiner, Vienna"
"$R" stations "Poznan Glowny" # transit stops, with their timezone
"$R" geocode "Sir Toby's Prague" # addresses, hotels, trailheads
Three things about that tool that must reach the user, not just sit in the code:
- Driving times are free-flow. No traffic, no roadworks, no border queue. Add a buffer for a peak-hour city arrival, and say that you added it instead of quoting a padded number as if the engine produced it.
- Transit and rail come from published GTFS feeds. Nothing back for a far-future date means the timetable is not published yet, not that there is no train.
- Neither returns fares. Price on the operator's site.
Always work out whether a journey is one ticket or two. Two tickets means a missed connection is the traveller's money, not the operator's problem. Say it before they book, and record it in the dossier.
5. Verify anything time-sensitive
Before an attraction, viewpoint, museum or road goes into a plan, check it: closed days,
renovation and scaffolding, timed-entry slots that sell out weeks ahead, seasonal closures,
last bus back, strikes. Method and sources in references/verify-before-you-go.md.
Every checked fact carries the date it was checked. Unchecked items are marked TBC, never
quietly filled in with what is usually true.
6. Hand the trip over to the in-trip agent
Planning ends with a dossier: one markdown file holding the legs, beds, refs, walking
times, money, contingencies and open items, in local time with the zones named. Spec in
references/dossier.md, template in assets/dossier-template.md.
It lives at <trips dir>/<YYYY-MM Title>/trip-dossier.md, and the active trip is pointed to by
~/.claude/trips/CURRENT. The companion skill reads it in the field, on a phone, with none of
this session's context. Write it for that reader: what happens when a leg is missed matters
more than what the plan was.
The dossier is private and carries confirmation refs. The field guide (trip-documents.md) is
share-safe and carries none. Two files, two jobs, never merged.
7. Deliverables and follow-through
- Trip field guide PDF and confirmation PDFs:
references/trip-documents.md, including the two renderer traps that cost real time to find. - When something is booked, update the dossier and the traveller profile with what was decided and why, so the next session does not re-litigate it.
- Confirmation references, PINs and payment details live in the private files only.
Related
trip-companionskill: the second agent, used during the trip, reads the dossier.