Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion domains/aa.com/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
|------|-------------|
| continue_checkout | Presses American's continue button for the current checkout step after checking the expected total, up to but never past payment selection. Purchases nothing. |
| get_checkout_step | Reports the American checkout step, its total, empty required field labels, the trip insurance answer, the selected payment method and whether a CVV is still required. |
| get_fare_options | Reads the cash or award cabin fares and the loaded nearby-date calendar on the current American results page, with pagination; never selects a fare. |
| get_flight_status | Looks up the status of one American-marketed flight number on an origin-local date with one same-origin request; public, no passenger data. |
| get_page_context | Describes the current aa.com page: its booking step, loading or pending navigation, signed-in state, total, open dialogs and suggested next tools. |
| get_trip_summary | Reads the flights, fare, base fare, tax lines and total due on Your trip summary, Passengers or Review and pay; never returns traveler details. |
| list_flight_results | Lists the departing or returning flights on American's results page with flight numbers, aircraft and each fare group's from-price, without searching again. |
| list_payment_methods | Lists American's payment methods on Review and pay, the selected one, and saved cards (brand and last four) of the signed-in member. |
| list_seat_map | Reads American's seat map for the current flight: available seats with type, features and cash price, and each passenger's seat. |
| list_trip_extras | Lists the offers on American's Trip extras page: Mileage Multiplier bonus miles (never bought) or prepaid checked bags with prices and counts. |
| login | Signs in to an American Airlines AAdvantage account, entering through a DuckDuckGo search and submitting the login form at most once. |
| search_flights | Searches American Airlines cash fares for 1–9 adults and returns the first-page outbound options with per-person from-fares. |
| prepare_itinerary | Re-reads one exact option from the current American results page as a preview for a human handoff; never selects, books or holds. |
| search_flights | Searches American Airlines cash fares, or award miles plus taxes with pay_with: miles, for 1–9 adults and returns the first-page outbound options with per-person from-fares. |
| select_fare | Opens one fare group for a flight row and returns each fare's price, bag, seat and change disclosures, refund options and whether Basic Economy consent is needed. |
| select_fare_option | Chooses one American fare (Basic Economy only with the user's agreement to its restrictions) and schedules the move to the return flights or trip summary. Purchases nothing. |
| select_payment_method | Chooses a saved AAdvantage card by last four digits or another payment method after checking the total; never types card data, holds or purchases. |
Expand Down
249 changes: 249 additions & 0 deletions domains/aa.com/get_fare_options.js

Large diffs are not rendered by default.

226 changes: 226 additions & 0 deletions domains/aa.com/get_flight_status.js

Large diffs are not rendered by default.

221 changes: 221 additions & 0 deletions domains/aa.com/prepare_itinerary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
[{
"kind": "page",
"match": {"url_patterns":["https://aa.com/*","https://*.aa.com/*"]},
"tool": {
"name": "prepare_itinerary",
"description": "Prepare a read-only outbound itinerary/fare preview for one exact option_id returned by get_fare_options on the current /booking/choose-flights/1 page. Re-reads all rendered fares and refuses stale IDs after a date, row, fare, or price change. No click, request, navigation, storage, hold, booking, or account change; safe to repeat. option_id is a snapshot identifier, NEVER human approval. Returns the observed per-person quote, explicit booking_performed=false and execution_available=false, plus a manual handoff checklist. Does not compute a party total, choose a return flight, verify complete fare terms, or persist a draft at American. Booking/hold/check-in/seat/cancel execution is deliberately unavailable because this runtime has no trusted per-action human-approval enforcement.",
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["option_id"],
"properties": {"option_id":{"type":"string","pattern":"^aa_[a-f0-9]{64}$"}}
},
"outputSchema": {
"type": "object",
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["status","option","booking_performed","execution_available","handoff_steps"],
"properties": {
"status": {"type":"string","const":"prepared"},
"option": {
"type": "object",
"additionalProperties": false,
"required": [
"option_id",
"departure_date",
"origin",
"destination",
"flight_numbers",
"transport_types",
"departure_time",
"arrival_time",
"arrival_day_offset",
"duration_minutes",
"stops",
"fare_label",
"trip_type",
"payment",
"amount",
"currency_symbol",
"tax_amount",
"per_person",
"price_is_from"
],
"properties": {
"option_id": {"type":"string","pattern":"^aa_[a-f0-9]{64}$"},
"departure_date": {"type":"string","pattern":"^20[0-9]{2}-[0-9]{2}-[0-9]{2}$"},
"origin": {"type":"string","pattern":"^[A-Z]{3}$"},
"destination": {"type":"string","pattern":"^[A-Z]{3}$"},
"flight_numbers": {
"type": "array",
"minItems": 1,
"maxItems": 6,
"items": {"type":"string","pattern":"^[A-Z0-9]{2} [0-9]{1,4}[A-Z]?$"}
},
"transport_types": {
"type": "array",
"minItems": 1,
"maxItems": 6,
"items": {"type":"string","enum":["flight","bus","train","unknown"]}
},
"departure_time": {"type":"string","pattern":"^(0[1-9]|1[0-2]):[0-5][0-9] [AP]M$"},
"arrival_time": {"type":"string","pattern":"^(0[1-9]|1[0-2]):[0-5][0-9] [AP]M$"},
"arrival_day_offset": {"type":"integer","minimum":-1,"maximum":3},
"duration_minutes": {"type":"integer","minimum":1,"maximum":10000},
"stops": {"type":"integer","minimum":0,"maximum":5},
"fare_label": {"type":"string","minLength":1,"maxLength":80},
"trip_type": {"type":"string","enum":["one_way","round_trip"]},
"payment": {"type":"string","enum":["cash","miles"]},
"amount": {"type":"number","minimum":0,"maximum":100000000},
"currency_symbol": {"type":"string","minLength":1,"maxLength":5},
"tax_amount": {"type":["number","null"],"minimum":0,"maximum":100000000},
"per_person": {"type":"boolean","const":true},
"price_is_from": {"type":"boolean"}
}
},
"booking_performed": {"type":"boolean","const":false},
"execution_available": {"type":"boolean","const":false},
"handoff_steps": {"type":"array","minItems":3,"maxItems":3,"items":{"type":"string","minLength":1,"maxLength":250}}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["status","message"],
"properties": {
"status": {
"type": "string",
"enum": [
"error",
"wrong_page",
"blocked",
"login_required",
"rate_limited",
"no_results",
"site_changed",
"stale_selection"
]
},
"message": {"type":"string","minLength":1,"maxLength":400}
}
}
]
},
"annotations": {"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true}
},
"execute": async (input, context) => {
// @xwebmcp-inline lib/aa-choose-flights.js
// American's outbound choose-flights page model shared by get_fare_options and prepare_itinerary: the page
// checks, the displayed cash and award amounts, and every enabled fare of every rendered row. Reading never clicks.
const aaFail = (status, message) => ({ status, message });
const aaText = (e) => (e?.textContent ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
const aaValidDate = (date) => /^20\d{2}-\d{2}-\d{2}$/.test(date) && Number.isFinite(Date.parse(date)) && new Date(date).toISOString().slice(0, 10) === date;
const aaPageFailure = () => {
if (location.origin !== "https://www.aa.com") return aaFail("wrong_page", "Open the outbound choose-flights page on https://www.aa.com first.");
if (/access denied|challenge validation/i.test(document.title) || document.querySelector('iframe[src*="challenge-assets"]')) return aaFail("blocked", "American is showing a human-verification or access-denied page; no availability conclusion was made.");
if (/too many requests/i.test(document.title)) return aaFail("rate_limited", "American is showing a rate limit. Do not retry automatically.");
if (/\/loyalty\/login/.test(location.pathname)) return aaFail("login_required", "The current page requires manual sign-in. No account data was read.");
if (location.pathname !== "/booking/choose-flights/1") return aaFail("wrong_page", "Run search_flights or manually open outbound choose-flights results first. This tool never navigates.");
return null;
};
// "$286" -> 286 with symbol "$"; award "16.1K" -> 16100 and "12,500" -> 12500 miles, computed from the digits so
// that no floating-point product decides whether a mileage is whole.
const aaAmount = (raw, miles) => {
const m = miles ? raw.match(/^((?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d{1,3})?)\s*(K)?$/i) : raw.match(/^([\p{Sc}A-Z]{1,5})((?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d{1,2})?)$/u);
if (!m) throw Error("Unrecognized fare amount");
let value;
if (miles) {
const [whole, fraction = ""] = m[1].replaceAll(",", "").split(".");
if (fraction && !m[2]) throw Error("Invalid fare amount");
value = m[2] ? Number(whole) * 1000 + Number(fraction.padEnd(3, "0")) : Number(whole);
} else value = Number(m[2].replaceAll(",", ""));
if (!Number.isFinite(value) || value < 0 || value > 100000000) throw Error("Invalid fare amount");
return { value, symbol: miles ? null : m[1].trim() };
};
// Every enabled fare on the page, or { failure } when the page is not an unambiguous outbound results page.
const aaReadOptions = async () => {
const visible = (e) => e.getClientRects().length > 0;
const count = aaText(document.querySelector(".result-count")).match(/^([\d,]+)\s+results?$/i);
if (!count) return { failure: aaFail("site_changed", "The results count is missing or unrecognized; the page may still be loading.") };
if (Number(count[1].replaceAll(",", "")) === 0) return { failure: aaFail("no_results", "The site explicitly reports zero flight results.") };
const dates = [...document.querySelectorAll('#weeklyCarousel button.selected-slide[id^="carousel-"]')];
if (dates.length !== 1) return { failure: aaFail("site_changed", "Exactly one selected departure date is required for an unambiguous preview.") };
const departure_date = dates[0].id.slice(9);
if (!aaValidDate(departure_date)) return { failure: aaFail("site_changed", "The selected departure date is invalid.") };
const award = !!document.querySelector(".flight-row");
const rows = [...document.querySelectorAll(award ? ".flight-row" : ".slice-details")].filter(visible);
if (!rows.length || rows.length > 100) return { failure: aaFail("site_changed", "Expected 1–100 rendered outbound flight rows; nothing was selected.") };
const options = [];
for (const [row_index, row] of rows.entries()) {
const get = (selector) => aaText(row.querySelector(selector));
const origin = get(".origin .city-code"), destination = get(".destination .city-code");
const flight_numbers = [...row.querySelectorAll(".flight-number")].map(aaText);
const time = (selector) => {
const raw = get(selector), m = raw.match(/\b(\d{1,2}):([0-5]\d)\s*([AP]M)\b/);
if (!m || +m[1] < 1 || +m[1] > 12) throw Error("Missing flight time");
const offset = Number(raw.match(/[+-]\d+/)?.[0] ?? 0);
if (!Number.isInteger(offset) || offset < -1 || offset > 3) throw Error("Invalid day offset");
return { value: `${m[1].padStart(2, "0")}:${m[2]} ${m[3]}`, offset };
};
const departure = time(award ? ".origin .flt-times" : ".origin .time");
const arrival = time(award ? ".destination .flt-times" : ".destination .time");
const duration = get(".duration"), hours = duration.match(/(\d+)h/), minutes = duration.match(/(\d+)m/);
const duration_minutes = Number(hours?.[1] ?? 0) * 60 + Number(minutes?.[1] ?? 0);
const stopsText = get(".stops"), stops = /Nonstop/i.test(stopsText) ? 0 : Number(stopsText.match(/(\d+)\s+stops?/i)?.[1] ?? NaN);
const modes = [...row.querySelectorAll(".transportation-type")].map(e => e.querySelector(".plane") ? "flight" : e.querySelector(".bus") ? "bus" : e.querySelector(".train") ? "train" : "unknown");
const transport_types = award && !modes.length ? flight_numbers.map(() => "unknown") : modes;
if (!/^[A-Z]{3}$/.test(origin) || !/^[A-Z]{3}$/.test(destination) || !flight_numbers.length || flight_numbers.length > 6 || flight_numbers.some(n => !/^[A-Z0-9]{2} [0-9]{1,4}[A-Z]?$/.test(n)) || transport_types.length !== flight_numbers.length || !Number.isInteger(stops) || stops < 0 || stops > 5 || duration_minutes < 1 || duration_minutes > 10000) throw Error("Incomplete flight row");
const buttons = [...row.querySelectorAll("button.btn-flight")];
if (buttons.length > 6) throw Error("Unexpected number of fare tiers");
for (const [fare_index, button] of buttons.entries()) {
if (button.disabled || button.getAttribute("aria-disabled") === "true") continue;
const fare_label = aaText(button.querySelector(award ? ".hidden-product-type" : ".hidden-product-group"));
const raw = aaText(button.querySelector(".per-pax-amount"));
if (!raw) continue;
const basisText = aaText(button.querySelector(award ? ".trip-type" : ".a11y-drawer-button-text"));
const trip_type = /Round trip/i.test(basisText) ? "round_trip" : /One way/i.test(basisText) ? "one_way" : null;
if (!fare_label || fare_label.length > 80 || !trip_type || (!award && !/per person/i.test(basisText))) throw Error("Unrecognized fare basis");
const parsed = aaAmount(raw, award);
const addon = award ? aaText(button.querySelector(".per-pax-addon")).match(/^\+\s*([\p{Sc}A-Z]{1,5})((?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d{1,2})?)$/u) : null;
if (award && !addon) throw Error("Missing award taxes");
const tax_amount = addon ? Number(addon[2].replaceAll(",", "")) : null;
if (tax_amount !== null && (!Number.isFinite(tax_amount) || tax_amount > 100000000)) throw Error("Invalid award taxes");
const option = {
departure_date, origin, destination, flight_numbers, transport_types,
departure_time: departure.value, arrival_time: arrival.value, arrival_day_offset: arrival.offset,
duration_minutes, stops, fare_label, trip_type, payment: award ? "miles" : "cash",
amount: parsed.value, currency_symbol: award ? addon[1].trim() : parsed.symbol, tax_amount,
per_person: true, price_is_from: !award
};
const bytes = new TextEncoder().encode(JSON.stringify({ row_index, fare_index, ...option }));
const digest = await crypto.subtle.digest("SHA-256", bytes);
const option_id = "aa_" + [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, "0")).join("");
options.push({ option_id, ...option });
}
}
return { departure_date, award, selected: dates[0], options };
};
// @xwebmcp-end
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some(k => k !== "option_id") || typeof input.option_id !== "string" || input.option_id.length !== 67 || !/^aa_[a-f0-9]{64}$/.test(input.option_id)) return aaFail("error", "Invalid input: provide only the exact option_id returned by get_fare_options. Confirmation flags are not accepted.");
if (context?.signal?.aborted) return aaFail("error", "Cancelled before reading the page.");
const page = aaPageFailure();
if (page) return page;
try {
const { failure, options } = await aaReadOptions();
if (failure) return failure;
if (context?.signal?.aborted) return aaFail("error", "Cancelled while reading; nothing was selected or reserved.");
if (!options.length) return aaFail("site_changed", "Flight rows exist but no complete fare options could be read.");
const option = options.find(option => option.option_id === input.option_id);
if (!option) return aaFail("stale_selection", "This exact option is not in the current results. Read get_fare_options again; no fare was selected.");
return { status: "prepared", option, booking_performed: false, execution_available: false, handoff_steps: [
"Review the airports, airport-local dates and times, transport types, fare and per-person price in the current American tab.",
"Manually choose outbound and any return, then review passenger details, the party total, taxes, baggage, refund/change terms and payment or miles requirements.",
"Only the human may continue with a booking or hold in the website. This preview is not approval and no execution tool is available."
] };
} catch {
return aaFail("site_changed", "The rendered flight or fare format is not recognized. Nothing was selected or reserved.");
}
}
}]
Loading
Loading