@fortrip/agent-contract 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -74,6 +74,13 @@ interface ToolSpec {
74
74
  * text: some clients never show the model the server instructions.
75
75
  */
76
76
  declare const ORDER_NEXT_STEP = "Before you give the user an order's payment.url, show them its total and currency, the stay (hotel, room, dates, rooms and guests per room) and every term (meals, cancellation, fees payable at the property), then end your reply by asking them to confirm, and wait for a clear yes. Only after they say yes, give them payment.url exactly as returned.";
77
+ /**
78
+ * How to present hotels (the `presentation` guide, in one line). It rides as
79
+ * the last paragraph of every MCP `search_hotels`, `get_hotel` and
80
+ * `get_hotel_offers` result: a rule next to the data is followed far more
81
+ * reliably than one in the server instructions.
82
+ */
83
+ declare const PRESENTATION_HINT = "Presentation: If your interface can show rich elements, show each hotel as a card with only what Fortrip's tools returned for it in this conversation: photo, name, stars, region and price, and once you have offers every term that changes the choice (meals and who they cover, cancellation, fees payable at the property, confirmation), an unknown one as unknown. Label a search's `price.fromPerRoomNight` as an estimate per room-night and an offer's `price.total` as the whole stay for all rooms, in USD, and draw a map only from returned `location` coordinates. Offer the choices you need to ask anyway (dates, a locality, which offer) as options to pick. Otherwise use a short list. Keep hotel names as returned, and never invent photos, ratings, reviews, prices, terms or coordinates.";
77
84
  /** The answers over MCP that can hold a `payment.url`: the HTTP answer plus
78
85
  * `nextStep`, which only MCP declares. */
79
86
  declare const MCP_OUTPUT: {
@@ -1010,6 +1017,17 @@ declare const knownLocaleText: z.ZodObject<{
1010
1017
  text: z.ZodString;
1011
1018
  locale: z.ZodType<"en" | "zh" | (string & {}), unknown, z.core.$ZodTypeInternals<"en" | "zh" | (string & {}), unknown>>;
1012
1019
  }, z.core.$strip>;
1020
+ /**
1021
+ * A hotel photo on a first-party URL (3.1). `caption` is the catalogue's,
1022
+ * with its locale; `null` without one.
1023
+ */
1024
+ declare const photo: z.ZodObject<{
1025
+ url: z.ZodString;
1026
+ caption: z.ZodNullable<z.ZodObject<{
1027
+ text: z.ZodString;
1028
+ locale: z.ZodType<"en" | "zh" | (string & {}), unknown, z.core.$ZodTypeInternals<"en" | "zh" | (string & {}), unknown>>;
1029
+ }, z.core.$strip>>;
1030
+ }, z.core.$strip>;
1013
1031
  /** Free text whose language may be unknown (`locale: null`). */
1014
1032
  declare const localizedText: z.ZodObject<{
1015
1033
  text: z.ZodString;
@@ -1569,6 +1587,15 @@ declare const hotel: z.ZodObject<{
1569
1587
  text: z.ZodString;
1570
1588
  locale: z.ZodType<"en" | "zh" | (string & {}), unknown, z.core.$ZodTypeInternals<"en" | "zh" | (string & {}), unknown>>;
1571
1589
  }, z.core.$strip>>;
1590
+ /** Up to 8 hotel photos, the cover first; `null` when the catalogue lists
1591
+ * none (unknown is not no). Absent from servers before 3.1. */
1592
+ photos: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
1593
+ url: z.ZodString;
1594
+ caption: z.ZodNullable<z.ZodObject<{
1595
+ text: z.ZodString;
1596
+ locale: z.ZodType<"en" | "zh" | (string & {}), unknown, z.core.$ZodTypeInternals<"en" | "zh" | (string & {}), unknown>>;
1597
+ }, z.core.$strip>>;
1598
+ }, z.core.$strip>>>>;
1572
1599
  checkInTimes: z.ZodObject<{
1573
1600
  from: z.ZodNullable<z.ZodString>;
1574
1601
  until: z.ZodNullable<z.ZodString>;
@@ -2465,6 +2492,14 @@ declare const hotelSummary: z.ZodObject<{
2465
2492
  rateDate: z.ZodString;
2466
2493
  }, z.core.$strip>>;
2467
2494
  }, z.core.$strip>>;
2495
+ /** The cover photo; `null` without one. Absent from servers before 3.1. */
2496
+ photo: z.ZodOptional<z.ZodNullable<z.ZodObject<{
2497
+ url: z.ZodString;
2498
+ caption: z.ZodNullable<z.ZodObject<{
2499
+ text: z.ZodString;
2500
+ locale: z.ZodType<"en" | "zh" | (string & {}), unknown, z.core.$ZodTypeInternals<"en" | "zh" | (string & {}), unknown>>;
2501
+ }, z.core.$strip>>;
2502
+ }, z.core.$strip>>>;
2468
2503
  }, z.core.$strip>;
2469
2504
  type HotelSummary = z.infer<typeof hotelSummary>;
2470
2505
  /** The top locality hubs of a scope (§7.6); also `search_too_broad` details. */
@@ -2727,6 +2762,14 @@ declare const searchResult: z.ZodObject<{
2727
2762
  rateDate: z.ZodString;
2728
2763
  }, z.core.$strip>>;
2729
2764
  }, z.core.$strip>>;
2765
+ /** The cover photo; `null` without one. Absent from servers before 3.1. */
2766
+ photo: z.ZodOptional<z.ZodNullable<z.ZodObject<{
2767
+ url: z.ZodString;
2768
+ caption: z.ZodNullable<z.ZodObject<{
2769
+ text: z.ZodString;
2770
+ locale: z.ZodType<"en" | "zh" | (string & {}), unknown, z.core.$ZodTypeInternals<"en" | "zh" | (string & {}), unknown>>;
2771
+ }, z.core.$strip>>;
2772
+ }, z.core.$strip>>>;
2730
2773
  }, z.core.$strip>>;
2731
2774
  nextCursor: z.ZodNullable<z.ZodString>;
2732
2775
  notes: z.ZodArray<z.ZodObject<{
@@ -2788,4 +2831,4 @@ declare const UNKNOWN_IS_NOT_NO_EXEMPTIONS: Readonly<Record<string, string>>;
2788
2831
  /** The contract's version, from package.json, the one place a release bumps it. */
2789
2832
  declare const CONTRACT_VERSION: string;
2790
2833
 
2791
- export { AMENITY_CODES, API, type AmenityCode, type AuthConfig, CARD_NUMBER_MESSAGE, CENTER_BASES, COMMANDS, COMMAND_GROUPS, CONTRACT_VERSION, type Center, type CommandGroupSpec, type CommandSpec, type CreateOrderInput, DISPLAY_CURRENCIES, type Destination, ERROR_CODES, ERROR_DETAILS, type ErrorBody, type ErrorCode, type ErrorEnvelope, FILTERABLE_AMENITIES, type FilterableAmenity, type FlagSpec, GLOBAL_FLAGS, type GetHotelInput, type GetOrderInput, type GuideSurface, type GuideTopic, type Hotel, type HotelOffersInput, type HotelOffersResult, type HotelSummary, type IndicativePrice, type KnownErrorBody, LIMITS, type ListOrdersInput, MCP_OUTPUT, MCP_PATH, type MeResult, type Money, ORDER_NEXT_STEP, ORDER_STATUSES, type Offer, type OfferChange, type Omitted, type OptionSpec, type Order, type OrderList, type OrderStatus, PAYMENT_STATUSES, PLACE_KINDS, POLICY_TOPICS, PUBLIC_ORIGINS, type PaymentStatus, type Place, type PlaceRef, type PolicyTopic, type PositionalSpec, type RenderedTool, SEARCH_NOTE_CODES, SEARCH_SORTS, SUPPORT_EMAIL, type SearchHotelsInput, type SearchNoteCode, type SearchResult, type Stay, TOOLS, type ToolName, type ToolSpec, UNAVAILABLE_REASONS, UNKNOWN_IS_NOT_NO_EXEMPTIONS, addIsoDateDays, authConfig, cancellation, center, commandInvocation, confirmation, containsCardNumber, countIsoDateDays, createOrderInput, createSearchHotelsInput, destination, errorEnvelope, flag, formatMoney, getHotelInput, getOrderInput, hotel, hotelId, hotelOffersInput, hotelOffersResult, hotelSummary, indicativePrice, instant, isIsoDate, knownLocaleText, latLng, listOrdersInput, localDate, localTime, localities, localizedText, meResult, meals, money, names, nonEmpty, offer, offerChange, offerId, omitted, oneOf, openEnum, openObject, openUnion, order, orderId, orderList, orderReference, parseOpen, payAtProperty, place, placeId, placeRef, renderError, renderGuide, renderToolResult, roomFacts, searchHotelsInput, searchResult, stay, stayEcho, strictErrorEnvelope, strictInput, strictOutput, text, timeZone, usdMoney, wholeNumber };
2834
+ export { AMENITY_CODES, API, type AmenityCode, type AuthConfig, CARD_NUMBER_MESSAGE, CENTER_BASES, COMMANDS, COMMAND_GROUPS, CONTRACT_VERSION, type Center, type CommandGroupSpec, type CommandSpec, type CreateOrderInput, DISPLAY_CURRENCIES, type Destination, ERROR_CODES, ERROR_DETAILS, type ErrorBody, type ErrorCode, type ErrorEnvelope, FILTERABLE_AMENITIES, type FilterableAmenity, type FlagSpec, GLOBAL_FLAGS, type GetHotelInput, type GetOrderInput, type GuideSurface, type GuideTopic, type Hotel, type HotelOffersInput, type HotelOffersResult, type HotelSummary, type IndicativePrice, type KnownErrorBody, LIMITS, type ListOrdersInput, MCP_OUTPUT, MCP_PATH, type MeResult, type Money, ORDER_NEXT_STEP, ORDER_STATUSES, type Offer, type OfferChange, type Omitted, type OptionSpec, type Order, type OrderList, type OrderStatus, PAYMENT_STATUSES, PLACE_KINDS, POLICY_TOPICS, PRESENTATION_HINT, PUBLIC_ORIGINS, type PaymentStatus, type Place, type PlaceRef, type PolicyTopic, type PositionalSpec, type RenderedTool, SEARCH_NOTE_CODES, SEARCH_SORTS, SUPPORT_EMAIL, type SearchHotelsInput, type SearchNoteCode, type SearchResult, type Stay, TOOLS, type ToolName, type ToolSpec, UNAVAILABLE_REASONS, UNKNOWN_IS_NOT_NO_EXEMPTIONS, addIsoDateDays, authConfig, cancellation, center, commandInvocation, confirmation, containsCardNumber, countIsoDateDays, createOrderInput, createSearchHotelsInput, destination, errorEnvelope, flag, formatMoney, getHotelInput, getOrderInput, hotel, hotelId, hotelOffersInput, hotelOffersResult, hotelSummary, indicativePrice, instant, isIsoDate, knownLocaleText, latLng, listOrdersInput, localDate, localTime, localities, localizedText, meResult, meals, money, names, nonEmpty, offer, offerChange, offerId, omitted, oneOf, openEnum, openObject, openUnion, order, orderId, orderList, orderReference, parseOpen, payAtProperty, photo, place, placeId, placeRef, renderError, renderGuide, renderToolResult, roomFacts, searchHotelsInput, searchResult, stay, stayEcho, strictErrorEnvelope, strictInput, strictOutput, text, timeZone, usdMoney, wholeNumber };
package/dist/index.js CHANGED
@@ -1051,6 +1051,7 @@ var FRAGMENTS = {
1051
1051
  "order": '{{#if claude-skill cli}}\n## Workflow\n\n1. **Search** with `{{tool:hotels search}}` for candidate hotels: a destination, `{{arg:text}}` or both, and the stay\'s dates and rooms for indicative prices.\n{{#if cli}}\n\n ```bash\n {{tool:hotels search}} --place-name Bangkok --country TH --check-in 2026-11-10 \\\n --check-out 2026-11-12 --room 2 --stars-min 4 --sort price_asc --limit 10\n ```\n\n{{/if}}\n2. **Details** (optional) with `{{tool:hotels get}}`, when the user asks about a hotel or a stated preference needs evidence (a pool, a policy, room size). For a details-only question, call it directly when you know the hotel\'s `hotelId`; otherwise find the hotel with `{{tool:hotels search}}`.\n{{#if cli}}\n\n ```bash\n {{tool:hotels get}} <hotelId> [--rooms]\n ```\n\n{{/if}}\n3. **Offers** with `{{tool:hotels offers}}` for a few shortlisted hotels, with the dates and rooms the user will order. It takes about 15 seconds. A hotel is available only once this returns an offer for it.\n{{#if cli}}\n\n ```bash\n {{tool:hotels offers}} --hotel <hotelId> --hotel <hotelId> --check-in 2026-11-10 \\\n --check-out 2026-11-12 --room 2 --room 2 [--require-free-cancellation]\n ```\n\n{{/if}}\n4. **Order** with `{{tool:hotels book}}`, once the user has picked one offer and given the guests and the contact (see Ordering).\n{{#if cli}}\n\n ```bash\n {{tool:hotels book}} --offer <offerId> --guest 1:John:Smith --guest 2:Mary:Smith \\\n --first-name John --last-name Smith --email john@example.com \\\n --phone "+8613800000000"\n ```\n\n{{/if}}\n5. **Confirm, then give the payment link**, as in "Before giving the payment link" below.\n6. **After payment**, check the order with `{{tool:orders get}}` (see After payment).\n\n## Search\n\n### Where\n\n- **A place by name:** {{#if mcp claude-skill}}`{ kind: "place_name", name, countryCode }`{{/if}}{{#if cli}}`--place-name <name> --country <CC>`{{/if}}, in any language (Kyoto or \u4EAC\u90FD). The country is required: ask the user when it isn\'t clear. Names match exactly, ignoring only case and spacing: accents, hyphens, apostrophes and periods count ("K\xF6ln" or "Cologne", not "Koln"; "St. Moritz", not "St Moritz").\n- If several places bear the name, the answer is `destination_ambiguous` with up to {{limit:candidatesShown}} candidates and their hotel counts: ask the user which one they mean, then search it by its `placeId`; never pick one yourself.\n- `destination_not_found` with `no_match` means no place of that name in that country: check the spelling and accents, or try another name for the place. Its `nameAsText` counts the hotels that a `{{arg:text}}` search for the name would find.\n- **A place from an earlier answer:** {{#if mcp claude-skill}}`{ kind: "place", placeId }`{{/if}}{{#if cli}}`--place <placeId>`{{/if}}.\n- **Near a point:** {{#if mcp claude-skill}}`{ kind: "point", location: { lat, lng } }`{{/if}}{{#if cli}}`--point <lat,lng>`{{/if}}, for "near X" when you know where X is.\n- **A whole country:** {{#if mcp claude-skill}}`{ kind: "country", countryCode }`{{/if}}{{#if cli}}`--country-only <CC>`{{/if}}, as for the Maldives.\n- Countries, regions (states, provinces), prefecture-level cities in China and main islands cover their whole area. Towns, neighbourhoods, districts, small islands and points cover a circle of {{limit:radiusMetersDefault}} meters; `{{arg:destination.radiusMeters}}` sets another, up to {{limit:radiusMetersMax}} meters, and narrows an area to a circle around its center.\n- **Localities:** an area search lists its `localities`, the towns its hotels belong to, each with a `placeId`, its hotel `count` and the places it `includes` (Patong is part of Phuket). To search one, pass it as {{#if mcp claude-skill}}`{ kind: "locality", placeId }`{{/if}}{{#if cli}}`--locality <placeId>`{{/if}}. A town that isn\'t a locality answers `destination_not_found` with `not_a_locality` and the `hub` it belongs to: search the hub, or the town itself with {{#if mcp claude-skill}}`kind: "place"`{{/if}}{{#if cli}}`--place`{{/if}} for a circle around it.\n- A price sort or price filter over more than {{limit:poolLimit}} hotels (in an area, or for text alone) answers `search_too_broad` with `matched` and the top `localities`: show them and ask the user which they prefer, unless the request already names one of them; search that one by its `placeId` as a locality, never by its name.\n\n### Text\n\n`{{arg:text}}` matches exact words in hotel names and addresses, in Chinese or English:\n\n- Every word must match as written: plurals, accents, spacing and abbreviations count ("hostel" and "hostels", "St Regis" and "Saint Regis" find different hotels). Hyphens and apostrophes don\'t (Ritz-Carlton finds Ritz Carlton).\n- In Chinese, matching follows word segmentation, so part of a name can match unrelated hotels (\u5E0C\u5C14 is not \u5E0C\u5C14\u987F).\n- Use only distinctive words: a hotel or brand name, a district or a landmark. Not "hotel", "near", "cheap" or "best".\n- Typos return nothing: "Shangahi" finds no hotel.\n- Place names belong in the destination: "Maldives" as text finds a hotel named Maldives in China.\n\n### Filters\n\n- Stars run from 1 to 5 in half steps{{#if mcp claude-skill}} (`filters.stars`){{/if}}. "4 stars" as {{#if mcp claude-skill}}`{ min: 4, max: 4 }`{{/if}}{{#if cli}}`--stars-min 4 --stars-max 4`{{/if}} leaves out 4.5: unless the user means exactly 4, use {{#if mcp claude-skill}}`{ min: 4 }`{{/if}}{{#if cli}}`--stars-min 4`{{/if}} or ask. Unrated hotels are left out whenever stars are filtered.\n- `{{arg:filters.amenities}}` takes only the amenities that can filter (such as pool, parking, wifi); others, such as gym or spa, show in `{{tool:hotels get}}` but can\'t filter. A hotel matches only when it lists the amenity, and about half the hotels list no amenities at all (`coverage.excludedNoFacilityData` counts those left out): "not listed" is not "absent".\n- `{{arg:filters.price.maxPerRoomNight}}` bounds the indicative price per room per night, in USD; hotels without one are left out and counted.\n\n### Results\n\n- `notes` explain the answer (a limited pool, a circle around an island, more hotels just outside it): pass on what matters to the user.\n- `coverage` has the counts: `matched` hotels, how many were priced and how many have a price.\n- A next page is {{#if mcp claude-skill}}`cursor` set to `nextCursor` and, optionally, `limit`{{/if}}{{#if cli}}`--cursor <nextCursor>` and, optionally, `--limit`{{/if}}; nothing else.\n{{/if}}\n{{#if mcp}}\n- A place name needs its `countryCode` (ask the user if unclear) and matches exactly (accents and periods count). On `destination_ambiguous`, ask the user which they mean, then search its `placeId`; never pick one yourself.\n- On `search_too_broad`, show its `localities` and ask the user which, unless they already named one; search it as `{ kind: "locality", placeId }`, never by name.\n- `text` matches whole words of hotel names and addresses: use a distinctive name, brand or landmark. A typo finds nothing.\n{{/if}}\n',
1052
1052
  "payment-link": "{{#if claude-skill cli}}\n## Before giving the payment link\n\n`{{tool:hotels book}}` answers with an unpaid order: `status` `awaiting_payment`, its `reference` (the support number), `price.total`, the stay and its terms, and `payment.url`, a Fortrip page that recaps the order and continues to payment on Stripe. Nothing is charged until the user pays.\n\n1. Show the order's total and currency, the stay (hotel, room, dates, rooms and guests per room) and every term: meals, cancellation (status, deadline, penalties) and fees payable at the property, unknowns included.\n2. End that reply by asking the user to confirm, and wait for a clear yes. Urgency is not consent.\n3. Then give `payment.url` exactly as returned: don't shorten, rebuild or re-encode it.\n\n{{/if}}\n{{#if mcp}}\n- Before you give `payment.url`, show the user the order's total and currency, the stay and every term, end that reply by asking the user to confirm, and wait for a clear yes. Then give `payment.url` exactly as returned, to this user only: anyone holding it can pay.\n{{/if}}\n{{#if claude-skill cli}}\n- `payment.url` works like a payment credential: anyone holding it can pay. Give it to this user only, never in shared channels, documents, logs or tickets. With `requiresSignIn: true` (an order started on the Fortrip website), the user signs in to pay.\n- The link expires at `payment.expiresAt`; the order is then `expired`. To order the stay again, get offers again, and confirm the new order with the user before giving its link.\n- An order `awaiting_payment` whose `payment.url` is null had its link unreadable this time: read it again with `{{tool:orders get}}`.\n{{/if}}\n- Never ask for, accept or pass on payment card details; payment happens only on the payment page.{{#if claude-skill cli}} A card number in a guest name, the contact or the special requests is rejected{{#if cli}} with exit 2{{/if}}.{{/if}}\n{{#if claude-skill cli}}\n\n## After payment\n\nCheck the order with `{{tool:orders get}}`:\n\n- `confirming` while the hotel confirms, then `confirmed` with the hotel's `confirmationNumber`.\n- `failed`: no stay was booked; `payment.status` says whether the payment was released or refunded.\n- `needs_attention`: Fortrip is looking into it; the user can email {{support}} with the `reference`.\n- `manageUrl` is the user's own page for the order.\n{{/if}}\n",
1053
1053
  "planning": "# Planning a hotel stay\n\nRead this when the request is more than one hotel for fixed dates: flexible dates, a budget, split stays, several destinations, or \"find me the best\". The rules in {{#if mcp}}the server instructions{{/if}}{{#if claude-skill}}SKILL.md{{/if}}{{#if cli}}`{{tool:guide}}`{{/if}} still apply (call order, destinations, one party per offer and order, confirming the order `{{tool:hotels book}}` returns before giving the payment link); this covers what to ask, how to compare and how to present.{{#if cli}} As there, `fortrip` is short for `npx -y fortrip@latest`.{{/if}}\n\n## What to collect\n\nKeep track of: dates (or the flexible window), nights, destination(s) with their country, rooms and the guests in each room, room constraints (beds, connecting rooms, accessibility), budget, and what the user said matters most (cheapest, most flexible, closest to X).\n\nOffers and orders take one party per call, every room the same, so if the party doesn't split evenly across the rooms, ask how it splits. Rooms for different parties are priced and ordered separately, each order with its own payment link.\n\nAsk only for what changes which hotels qualify or how they rank. \"4 stars\" as {{#if mcp claude-skill}}`{ min: 4, max: 4 }`{{/if}}{{#if cli}}`--stars-min 4 --stars-max 4`{{/if}} leaves out 4.5: unless the user means exactly 4, use {{#if mcp claude-skill}}`{ min: 4 }`{{/if}}{{#if cli}}`--stars-min 4`{{/if}} or ask. If children come along, ask each child's age; `{{tool:hotels offers}}` needs it. Don't ask for trip type or style: infer it from the conversation or leave it general. When a place name is ambiguous, ask which place the user means.\n\n## Evidence\n\n- A `{{tool:hotels search}}` price is an indicative estimate per room per night, not a quote. Never call a search result available or bookable, and once you have offers, compare offers only. A hotel without a search price can still have offers.\n- Use `{{tool:hotels get}}` only when a stated preference needs evidence (a pool, a policy, room size). An amenity or policy it doesn't list is unknown, not absent.\n- A hotel is available only once `{{tool:hotels offers}}` returned an offer for it.\n- Anything {{#if mcp claude-skill}}the tools{{/if}}{{#if cli}}the CLI{{/if}} didn't return, such as reviews, style, price trends or exchange rates other than `approx`, is unknown. Say so instead of filling it in.\n\n## Ranking\n\n1. Drop options that break a hard constraint (dates, guests, budget cap, required facility). Compare a budget in the unit the user gave it: per night, per room or for the whole stay. An offer's `price.total` covers the whole stay and every room, in USD.\n2. Keep only options with a live offer.\n3. Order by the user's stated main goal. Goals they didn't state don't count.\n4. Use other preferences only to break ties.\n\nUse the cheapest comparable offer as the baseline unless the user fixed the room type. Show at most three plans, each with its price difference from the baseline and what it trades off (meals, cancellation, location, confirmation). Keep hotel names as returned and dates in the user's notation.\n\nFor a split stay or several destinations, price each leg with its own `{{tool:hotels offers}}` call and add up the legs. Each leg is a separate `{{tool:hotels book}}` call, with its own confirmation and its own payment link.\n\n## Stop and say so\n\n- `{{tool:hotels offers}}` covers up to {{limit:offersHotels}} hotels per call and takes about 15 seconds, and its offers expire after about 30 minutes. If the request needs more hotels or date combinations than a few calls can cover, narrow it with the user, or present the result as a comparison of the hotels you priced, not the best overall.\n- When a search is too broad to price, show its localities and ask the user which they prefer, unless the request already names one of them; never pick one yourself.\n- Urgency is not consent. Call `{{tool:hotels book}}` only for the offer the user picked, and give its payment link only after the user confirms the total, currency, stay and terms of the order `{{tool:hotels book}}` returned.\n",
1054
+ "presentation": "{{#if claude-skill}}\n## Presenting hotels\n\nIf your interface can show rich elements, show each hotel as a card with only what Fortrip's tools returned for it in this conversation: photo, name, stars, region and price, and once you have offers every term that changes the choice (meals and who they cover, cancellation, fees payable at the property, confirmation), an unknown one as unknown. Label a search's `price.fromPerRoomNight` as an estimate per room-night and an offer's `price.total` as the whole stay for all rooms, in USD, and draw a map only from returned `location` coordinates. Offer the choices you need to ask anyway (dates, a locality, which offer) as options to pick. Otherwise use a short list. Keep hotel names as returned, and never invent photos, ratings, reviews, prices, terms or coordinates.\n{{/if}}\n",
1054
1055
  "prices": "{{#if claude-skill cli}}\n## Prices and currency\n\n- A search price (`price.fromPerRoomNight`) is indicative and not bookable: the lowest price of one unit (a room or, at a hostel, possibly a bed) per night, for any party and often any date, and it can be well above or below the offers. Call it an estimate, quote `{{tool:hotels offers}}` prices, and never call a search result available or bookable. `price: null` means there is no indicative price, not that the hotel is full; about half the hotels have none for given dates.\n{{/if}}\n{{#if mcp}}\n- Search prices are indicative, not bookable: quote `{{tool:hotels offers}}` prices.\n{{/if}}\n- {{#if claude-skill cli}}In an offer or order, {{/if}}`price.total` is the whole stay for all rooms and is what is charged, in USD for offers and the orders made from them.{{#if claude-skill cli}} `perNightAllRooms` is the total per night; to compare with a per-room budget, also divide by the rooms.{{/if}} Stripe's page may let the user pay in their card's local currency at Stripe's rate (Adaptive Pricing){{#if claude-skill cli}}: say so plainly, and that the USD total is the price{{/if}}. `approx`{{#if claude-skill cli}}, in `{{arg:displayCurrency}}`, with its `rateDate`,{{/if}} is an estimate, never the charge.\n{{#if claude-skill cli}}\n- `{{arg:displayCurrency}}`{{#if cli}} (or `FORTRIP_DISPLAY_CURRENCY`){{/if}} is `USD` or `CNY`: `CNY` adds `approx` in yuan to suit a user who thinks in it. It never changes the charge.\n- Fees payable at the property (`payAtProperty`) are separate, in the property's own currency, not converted.\n- After payment, `payment.localCharge` is what the card paid in its own currency. Orders made on the Fortrip website can be charged in another currency: `payment.chargeCurrency` says which.\n{{/if}}\n",
1055
1056
  "scope": "{{#if claude-skill cli}}\n# Fortrip hotel stays\n\n{{/if}}\nFortrip: hotel search, live offers and orders with a payment link.{{#if claude-skill cli}} Not for flights, activities, or changing or cancelling an existing order.{{/if}}\n\n{{#if mcp}}\nCall order: `{{tool:hotels search}}` \u2192 (optional) `{{tool:hotels get}}` \u2192 `{{tool:hotels offers}}` \u2192 `{{tool:hotels book}}`; after the user pays, `{{tool:orders get}}`; `{{tool:orders list}}` lists their orders. `{{tool:guide}}` returns Fortrip's guidance for flexible dates, budgets or several destinations.\n{{/if}}\n{{#if claude-skill}}\nThe tools are on Fortrip's MCP server, named `fortrip` in this plugin (a Fortrip connector added on claude.ai can show under its own name). They're written `fortrip:<tool>` below:\n{{/if}}\n{{#if cli}}\nThe `fortrip` CLI (npm package `fortrip`) calls Fortrip as the signed-in user and prints each answer as compact text (`--json` prints its JSON):\n{{/if}}\n{{#if claude-skill cli}}\n\n| {{#if claude-skill}}Tool{{/if}}{{#if cli}}Command{{/if}} | Returns |\n|---|---|\n| `{{tool:hotels search}}` | Hotels in a place, near a point or matching a name (`{{arg:text}}`), with indicative prices |\n| `{{tool:hotels get}}` | One hotel's address, check-in and check-out times, description, policies, amenities and rooms; no prices |\n| `{{tool:hotels offers}}` | Live offers (room, meals, total in USD, cancellation terms) for up to {{limit:offersHotels}} hotels |\n| `{{tool:hotels book}}` | An unpaid order for one offer, with its terms and a payment link |\n| `{{tool:orders get}}` | One order and its status, including whether the hotel confirmed it |\n| `{{tool:orders list}}` | The user's orders, newest first, including those made on the Fortrip website |\n\nFor flexible dates, a budget, split stays, several destinations or \"find me the best\", {{#if claude-skill}}read [references/planning.md](references/planning.md){{/if}}{{#if cli}}also run `{{tool:guide}} planning`{{/if}} before searching.\n{{/if}}\n",
1056
1057
  "setup": "{{#if cli}}\n## Setup\n\nRun every command as `npx -y fortrip@latest <command>` (Node.js 22 or newer). The examples below shorten that to `fortrip`; `fortrip <command> --help` lists a command's flags. `{{tool:hotels search}}`, `{{tool:hotels offers}}` and `{{tool:hotels book}}` also take `--body '<json>'` (or `--body @file.json`), the raw request body; flags override its fields. Add `--json` to any command that calls Fortrip to get its answer as one line of JSON instead of text.\n\nWhen the user first asks about hotels, run `{{tool:whoami}}` before asking follow-up questions, so the sign-in can happen while they answer.\n\n{{/if}}\n{{#if claude-skill cli}}\n## Sign-in\n\n{{/if}}\nSign-in happens only in the browser. Never ask the user to paste a token.\n{{#if claude-skill}}\n\nIf none of these tools are available, Fortrip isn't connected: tell the user to add the Fortrip connector or plugin and sign in.\n{{/if}}\n{{#if mcp claude-skill}}\nIf a tool says the user isn't signed in, ask them to reconnect Fortrip.\n{{/if}}\n{{#if cli}}\n\nIf a command exits with code **3** and stderr has `loginUrl`, show it to the user as a link together with `userCode`, e.g. \"Sign in to Fortrip: <loginUrl> (code <userCode>), then tell me when you're done.\" When they confirm, rerun the same command; it finishes the sign-in itself. Don't run `{{tool:login}}`: it waits for up to 10 minutes. Commands that exit 3 together share one `loginUrl`; show it once. If the rerun gives a different `loginUrl`, show that one.\n\nWhen Fortrip no longer accepts the saved sign-in (it was revoked, or it expired and couldn't be renewed), the command also exits 3 with a `loginUrl`; handle it the same way. The saved sign-in is kept until the user approves the new link.\n\nIn CI or a sandbox without a browser, the user can set `FORTRIP_TOKEN` instead. Exit 3 with `tokenSource: \"FORTRIP_TOKEN\"` means Fortrip didn't accept that token: tell the user to replace or unset it.\n\n## Exit codes\n\n| Code | Meaning | What to do |\n|---|---|---|\n| 0 | Success; the answer on stdout | Continue |\n| 1 | Any other error: an error from Fortrip (see Errors), a network error or a timeout | Read the error on stderr. If `retryable` is true, wait `retryAfterSeconds` (or a few seconds) and retry once |\n| 2 | Invalid arguments, or `invalid_request` from Fortrip | Fix the flags; the error lists each bad field with a message |\n| 3 | Not signed in | Show `loginUrl` to the user, then rerun (above) |\n| 4 | Rate limited | Wait `retryAfterSeconds`, then retry once |\n\nOutput on stdout or stderr can carry `warning` (with `recoveryFile` when a sign-in couldn't be saved): pass it on to the user.\n{{/if}}\n",
@@ -1069,6 +1070,7 @@ var TOPICS = {
1069
1070
  "meals",
1070
1071
  "booking",
1071
1072
  "payment-link",
1073
+ "presentation",
1072
1074
  "errors",
1073
1075
  "support"
1074
1076
  ],
@@ -1195,6 +1197,10 @@ var knownLocaleText = openObject({
1195
1197
  text: z9.string(),
1196
1198
  locale: openEnum(LOCALES2)
1197
1199
  });
1200
+ var photo = openObject({
1201
+ url: z9.string(),
1202
+ caption: knownLocaleText.nullable()
1203
+ });
1198
1204
  var localizedText = openObject({
1199
1205
  text: z9.string(),
1200
1206
  locale: openEnum(LOCALES2).nullable()
@@ -1331,7 +1337,9 @@ var hotelSummary = openObject({
1331
1337
  location: latLng.nullable(),
1332
1338
  timeZone: z11.string().nullable(),
1333
1339
  distanceMeters: z11.number().nullable(),
1334
- price: indicativePrice.nullable()
1340
+ price: indicativePrice.nullable(),
1341
+ /** The cover photo; `null` without one. Absent from servers before 3.1. */
1342
+ photo: photo.nullable().optional()
1335
1343
  });
1336
1344
  var localities = openObject({
1337
1345
  items: z11.array(
@@ -1450,6 +1458,9 @@ var hotel = openObject({
1450
1458
  location: latLng.nullable(),
1451
1459
  timeZone: z12.string().nullable(),
1452
1460
  description: knownLocaleText.nullable(),
1461
+ /** Up to 8 hotel photos, the cover first; `null` when the catalogue lists
1462
+ * none (unknown is not no). Absent from servers before 3.1. */
1463
+ photos: z12.array(photo).nullable().optional(),
1453
1464
  checkInTimes: timeRange,
1454
1465
  checkOutTimes: timeRange,
1455
1466
  policies: openObject({
@@ -1798,6 +1809,7 @@ var MCP_PATH = "/";
1798
1809
  // src/tools.ts
1799
1810
  import { z as z16 } from "zod";
1800
1811
  var ORDER_NEXT_STEP = "Before you give the user an order's payment.url, show them its total and currency, the stay (hotel, room, dates, rooms and guests per room) and every term (meals, cancellation, fees payable at the property), then end your reply by asking them to confirm, and wait for a clear yes. Only after they say yes, give them payment.url exactly as returned.";
1812
+ var PRESENTATION_HINT = "Presentation: If your interface can show rich elements, show each hotel as a card with only what Fortrip's tools returned for it in this conversation: photo, name, stars, region and price, and once you have offers every term that changes the choice (meals and who they cover, cancellation, fees payable at the property, confirmation), an unknown one as unknown. Label a search's `price.fromPerRoomNight` as an estimate per room-night and an offer's `price.total` as the whole stay for all rooms, in USD, and draw a map only from returned `location` coordinates. Offer the choices you need to ask anyway (dates, a locality, which offer) as options to pick. Otherwise use a short list. Keep hotel names as returned, and never invent photos, ratings, reviews, prices, terms or coordinates.";
1801
1813
  var nextStep = z16.string().optional().describe(
1802
1814
  "What to show and ask before giving a payment.url. Present when the answer holds a non-null payment.url, and only then."
1803
1815
  );
@@ -1970,6 +1982,7 @@ function deposit(d) {
1970
1982
  function roomLine(r) {
1971
1983
  return [`- ${r.name.text}`, ...roomFacts2(r)].join(" \xB7 ");
1972
1984
  }
1985
+ var PHOTOS_SHOWN = 4;
1973
1986
  function renderHotel(h) {
1974
1987
  const region = h.address.region;
1975
1988
  const lines = [
@@ -1991,6 +2004,11 @@ function renderHotel(h) {
1991
2004
  deposit(h.deposit),
1992
2005
  `Phone: ${h.contact.phone ?? "not listed"} \xB7 Website: ${h.contact.website ?? "not listed"}`
1993
2006
  ];
2007
+ if (h.photos?.length) {
2008
+ const shown = h.photos.slice(0, PHOTOS_SHOWN).map((p) => p.url);
2009
+ const more = h.photos.length - shown.length;
2010
+ lines.push(`Photos: ${shown.join(" \xB7 ")}${more > 0 ? ` (+${more} more)` : ""}`);
2011
+ }
1994
2012
  if (!h.amenities.listed) {
1995
2013
  lines.push('Amenities: not listed (unknown, not "none")');
1996
2014
  } else if (h.amenities.items.length === 0) {
@@ -2370,6 +2388,7 @@ function hotelLine(h, i, r) {
2370
2388
  r.pricing.mode === "none" ? "" : priceWords(h, r.pricing.status),
2371
2389
  h.address.line ? h.address.line.text : "address not listed",
2372
2390
  where,
2391
+ h.photo ? `photo ${h.photo.url}` : "",
2373
2392
  h.hotelId
2374
2393
  ].filter(Boolean).join(" \xB7 ");
2375
2394
  }
@@ -2672,7 +2691,7 @@ var UNKNOWN_IS_NOT_NO_EXEMPTIONS = {
2672
2691
  // package.json
2673
2692
  var package_default = {
2674
2693
  name: "@fortrip/agent-contract",
2675
- version: "3.0.0",
2694
+ version: "3.1.0",
2676
2695
  license: "Apache-2.0",
2677
2696
  description: "The contract of Fortrip's agent API (https://api.fortrip.ai/v1) and its MCP server: limits, paths, request and response schemas, error codes and field rules.",
2678
2697
  type: "module",
@@ -2744,6 +2763,7 @@ export {
2744
2763
  PAYMENT_STATUSES,
2745
2764
  PLACE_KINDS,
2746
2765
  POLICY_TOPICS,
2766
+ PRESENTATION_HINT,
2747
2767
  PUBLIC_ORIGINS,
2748
2768
  SEARCH_NOTE_CODES,
2749
2769
  SEARCH_SORTS,
@@ -2803,6 +2823,7 @@ export {
2803
2823
  orderReference,
2804
2824
  parseOpen,
2805
2825
  payAtProperty,
2826
+ photo,
2806
2827
  place,
2807
2828
  placeId,
2808
2829
  placeRef,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fortrip/agent-contract",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The contract of Fortrip's agent API (https://api.fortrip.ai/v1) and its MCP server: limits, paths, request and response schemas, error codes and field rules.",
6
6
  "type": "module",