fortrip 2.0.0 → 3.0.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/README.md +131 -73
- package/dist/{chunk-53656LB2.js → chunk-26IHFJYT.js} +2223 -569
- package/dist/cli.js +262 -107
- package/dist/{mcp-bridge-LPQBHWLD.js → mcp-bridge-3WLWV2KY.js} +87 -24
- package/package.json +2 -2
|
@@ -2671,10 +2671,10 @@ function isValidJWT(token, algorithm = null) {
|
|
|
2671
2671
|
const tokensParts = token.split(".");
|
|
2672
2672
|
if (tokensParts.length !== 3)
|
|
2673
2673
|
return false;
|
|
2674
|
-
const [
|
|
2675
|
-
if (!
|
|
2674
|
+
const [header2] = tokensParts;
|
|
2675
|
+
if (!header2)
|
|
2676
2676
|
return false;
|
|
2677
|
-
const parsedHeader = JSON.parse(atob(
|
|
2677
|
+
const parsedHeader = JSON.parse(atob(header2));
|
|
2678
2678
|
if ("typ" in parsedHeader && parsedHeader?.typ !== "JWT")
|
|
2679
2679
|
return false;
|
|
2680
2680
|
if (!parsedHeader.alg)
|
|
@@ -3043,13 +3043,13 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
3043
3043
|
}
|
|
3044
3044
|
return propValues;
|
|
3045
3045
|
});
|
|
3046
|
-
const
|
|
3046
|
+
const isObject4 = isObject;
|
|
3047
3047
|
const catchall = def.catchall;
|
|
3048
3048
|
let value;
|
|
3049
3049
|
inst._zod.parse = (payload, ctx) => {
|
|
3050
3050
|
value ?? (value = _normalized.value);
|
|
3051
3051
|
const input = payload.value;
|
|
3052
|
-
if (!
|
|
3052
|
+
if (!isObject4(input)) {
|
|
3053
3053
|
payload.issues.push({
|
|
3054
3054
|
expected: "object",
|
|
3055
3055
|
code: "invalid_type",
|
|
@@ -3176,7 +3176,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
3176
3176
|
return (payload, ctx) => fn(shape, payload, ctx);
|
|
3177
3177
|
};
|
|
3178
3178
|
let fastpass;
|
|
3179
|
-
const
|
|
3179
|
+
const isObject4 = isObject;
|
|
3180
3180
|
const jit = !globalConfig.jitless;
|
|
3181
3181
|
const allowsEval2 = allowsEval;
|
|
3182
3182
|
const fastEnabled = jit && allowsEval2.value;
|
|
@@ -3185,7 +3185,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
3185
3185
|
inst._zod.parse = (payload, ctx) => {
|
|
3186
3186
|
value ?? (value = _normalized.value);
|
|
3187
3187
|
const input = payload.value;
|
|
3188
|
-
if (!
|
|
3188
|
+
if (!isObject4(input)) {
|
|
3189
3189
|
payload.issues.push({
|
|
3190
3190
|
expected: "object",
|
|
3191
3191
|
code: "invalid_type",
|
|
@@ -4641,8 +4641,8 @@ function az_default() {
|
|
|
4641
4641
|
}
|
|
4642
4642
|
|
|
4643
4643
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/be.js
|
|
4644
|
-
function getBelarusianPlural(
|
|
4645
|
-
const absCount = Math.abs(
|
|
4644
|
+
function getBelarusianPlural(count3, one, few, many) {
|
|
4645
|
+
const absCount = Math.abs(count3);
|
|
4646
4646
|
const lastDigit = absCount % 10;
|
|
4647
4647
|
const lastTwoDigits = absCount % 100;
|
|
4648
4648
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
|
@@ -6473,8 +6473,8 @@ var error17 = () => {
|
|
|
6473
6473
|
case "invalid_union":
|
|
6474
6474
|
return "\u05E7\u05DC\u05D8 \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF";
|
|
6475
6475
|
case "invalid_element": {
|
|
6476
|
-
const
|
|
6477
|
-
return `\u05E2\u05E8\u05DA \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF \u05D1${
|
|
6476
|
+
const place2 = withDefinite(issue2.origin ?? "array");
|
|
6477
|
+
return `\u05E2\u05E8\u05DA \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF \u05D1${place2}`;
|
|
6478
6478
|
}
|
|
6479
6479
|
default:
|
|
6480
6480
|
return `\u05E7\u05DC\u05D8 \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF`;
|
|
@@ -6720,8 +6720,8 @@ function hu_default() {
|
|
|
6720
6720
|
}
|
|
6721
6721
|
|
|
6722
6722
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/hy.js
|
|
6723
|
-
function getArmenianPlural(
|
|
6724
|
-
return Math.abs(
|
|
6723
|
+
function getArmenianPlural(count3, one, many) {
|
|
6724
|
+
return Math.abs(count3) === 1 ? one : many;
|
|
6725
6725
|
}
|
|
6726
6726
|
function withDefiniteArticle(word) {
|
|
6727
6727
|
if (!word)
|
|
@@ -8849,8 +8849,8 @@ function ro_default() {
|
|
|
8849
8849
|
}
|
|
8850
8850
|
|
|
8851
8851
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ru.js
|
|
8852
|
-
function getRussianPlural(
|
|
8853
|
-
const absCount = Math.abs(
|
|
8852
|
+
function getRussianPlural(count3, one, few, many) {
|
|
8853
|
+
const absCount = Math.abs(count3);
|
|
8854
8854
|
const lastDigit = absCount % 10;
|
|
8855
8855
|
const lastTwoDigits = absCount % 100;
|
|
8856
8856
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
|
@@ -14580,11 +14580,58 @@ function countIsoDateDays(start, end) {
|
|
|
14580
14580
|
(Date.parse(`${end}T00:00:00Z`) - Date.parse(`${start}T00:00:00Z`)) / DAY_MS
|
|
14581
14581
|
);
|
|
14582
14582
|
}
|
|
14583
|
-
|
|
14584
|
-
|
|
14585
|
-
|
|
14586
|
-
|
|
14583
|
+
|
|
14584
|
+
// ../contract/src/conventions/ids.ts
|
|
14585
|
+
function prefixed(prefix, name) {
|
|
14586
|
+
const error51 = `${name} must be ${prefix}_ followed by 20 letters or digits`;
|
|
14587
|
+
return external_exports.string({ error: error51 }).regex(new RegExp(`^${prefix}_[0-9A-Za-z]{20}$`), { error: error51 });
|
|
14588
|
+
}
|
|
14589
|
+
var hotelId = prefixed("htl", "hotelId");
|
|
14590
|
+
var offerId = prefixed("off", "offerId");
|
|
14591
|
+
var orderId = prefixed("ord", "orderId");
|
|
14592
|
+
var placeId = external_exports.string({ error: "placeId must be plc_ followed by 1 to 12 digits" }).regex(/^plc_\d{1,12}$/, {
|
|
14593
|
+
error: "placeId must be plc_ followed by 1 to 12 digits"
|
|
14594
|
+
});
|
|
14595
|
+
|
|
14596
|
+
// ../contract/src/conventions/money.ts
|
|
14597
|
+
var amountError = "amount must be a decimal string such as 132.40";
|
|
14598
|
+
var currencyError = "currency must be a 3-letter uppercase ISO 4217 code";
|
|
14599
|
+
var money = external_exports.looseObject({
|
|
14600
|
+
amount: external_exports.string({ error: amountError }).regex(/^\d+(\.\d+)?$/, {
|
|
14601
|
+
error: amountError
|
|
14602
|
+
}),
|
|
14603
|
+
currency: external_exports.string({ error: currencyError }).regex(/^[A-Z]{3}$/, {
|
|
14604
|
+
error: currencyError
|
|
14605
|
+
})
|
|
14606
|
+
});
|
|
14607
|
+
var usdMoney = external_exports.strictObject(
|
|
14608
|
+
{
|
|
14609
|
+
amount: external_exports.string({ error: amountError }).regex(/^\d+(\.\d+)?$/, { error: amountError }).describe("Decimal string such as 132.40, never a number."),
|
|
14610
|
+
currency: external_exports.literal("USD", { error: "currency must be USD" }).describe("Always USD in a request.")
|
|
14611
|
+
},
|
|
14612
|
+
{
|
|
14613
|
+
error: (issue2) => issue2.code === "unrecognized_keys" ? `Unknown field(s): ${issue2.keys.join(", ")}` : "Expected a Money object"
|
|
14614
|
+
}
|
|
14615
|
+
);
|
|
14616
|
+
|
|
14617
|
+
// ../contract/src/conventions/time.ts
|
|
14618
|
+
var instant = external_exports.string({ error: "must be an RFC 3339 UTC instant such as 2026-10-23T08:00:00Z" }).regex(/^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?Z$/, {
|
|
14619
|
+
error: "must be an RFC 3339 UTC instant such as 2026-10-23T08:00:00Z"
|
|
14620
|
+
}).refine((s) => isIsoDate(s.slice(0, 10)), { error: "must be a real instant" });
|
|
14621
|
+
var localDate = external_exports.string({ error: "must be a real yyyy-MM-dd calendar date" }).refine(isIsoDate, { error: "must be a real yyyy-MM-dd calendar date" });
|
|
14622
|
+
var localTime = external_exports.string({ error: "must be a local time such as 14:00" }).regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/, {
|
|
14623
|
+
error: "must be a local time such as 14:00"
|
|
14624
|
+
});
|
|
14625
|
+
function isIanaZone(s) {
|
|
14626
|
+
if (!/^[A-Za-z][A-Za-z0-9_+-]*(?:\/[A-Za-z0-9_+-]+)*$/.test(s)) return false;
|
|
14627
|
+
try {
|
|
14628
|
+
new Intl.DateTimeFormat("en", { timeZone: s });
|
|
14629
|
+
return true;
|
|
14630
|
+
} catch {
|
|
14631
|
+
return false;
|
|
14632
|
+
}
|
|
14587
14633
|
}
|
|
14634
|
+
var timeZone = external_exports.string({ error: "must be an IANA time zone such as Asia/Shanghai" }).refine(isIanaZone, { error: "must be an IANA time zone such as Asia/Shanghai" });
|
|
14588
14635
|
|
|
14589
14636
|
// ../contract/src/fields.ts
|
|
14590
14637
|
function strictInput(shape) {
|
|
@@ -14596,14 +14643,6 @@ function wholeNumber(name, min, max) {
|
|
|
14596
14643
|
const error51 = `${name} must be a whole number from ${min} to ${max}`;
|
|
14597
14644
|
return external_exports.number({ error: error51 }).int({ error: error51 }).min(min, { error: error51 }).max(max, { error: error51 });
|
|
14598
14645
|
}
|
|
14599
|
-
function positiveId(name) {
|
|
14600
|
-
const error51 = `${name} must be a positive whole number`;
|
|
14601
|
-
return external_exports.number({ error: error51 }).int({ error: error51 }).positive({ error: error51 });
|
|
14602
|
-
}
|
|
14603
|
-
function isoDate(name) {
|
|
14604
|
-
const error51 = `${name} must be a real yyyy-MM-dd calendar date`;
|
|
14605
|
-
return external_exports.string({ error: error51 }).refine(isIsoDate, { error: error51 });
|
|
14606
|
-
}
|
|
14607
14646
|
function nonEmpty(name) {
|
|
14608
14647
|
const error51 = `${name} must be a non-empty string`;
|
|
14609
14648
|
return external_exports.string({ error: error51 }).min(1, { error: error51 });
|
|
@@ -14620,10 +14659,6 @@ function oneOf(name, values) {
|
|
|
14620
14659
|
function flag(name) {
|
|
14621
14660
|
return external_exports.boolean({ error: `${name} must be true or false` });
|
|
14622
14661
|
}
|
|
14623
|
-
function childrenAges(max) {
|
|
14624
|
-
const error51 = `childrenAges must list each child's age (0 to 17) in one room, at most ${max}`;
|
|
14625
|
-
return external_exports.array(wholeNumber("Each child's age", 0, 17), { error: error51 }).max(max, { error: error51 });
|
|
14626
|
-
}
|
|
14627
14662
|
|
|
14628
14663
|
// ../contract/src/guest-name.ts
|
|
14629
14664
|
var CURLY_APOSTROPHES = /[‘’]/g;
|
|
@@ -14639,199 +14674,301 @@ function normalizeGuestName(value) {
|
|
|
14639
14674
|
var LIMITS = {
|
|
14640
14675
|
rooms: 8,
|
|
14641
14676
|
adultsPerRoom: 8,
|
|
14642
|
-
childrenPerRoom:
|
|
14677
|
+
childrenPerRoom: 4,
|
|
14643
14678
|
childAgeMax: 17,
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
|
|
14679
|
+
nightsFortrip: 30,
|
|
14680
|
+
nightsSupplier: 15,
|
|
14681
|
+
offersHotels: 15,
|
|
14682
|
+
offersPerHotelDefault: 5,
|
|
14683
|
+
offersPerHotelMax: 20,
|
|
14684
|
+
pageSizeDefault: 20,
|
|
14685
|
+
pageSizeMax: 50,
|
|
14686
|
+
textLength: 128,
|
|
14687
|
+
radiusMetersMax: 8e4,
|
|
14688
|
+
radiusMetersDefault: 8e3,
|
|
14689
|
+
amenityFilters: 9,
|
|
14651
14690
|
guestName: 60,
|
|
14652
|
-
|
|
14691
|
+
contactFirstName: 59,
|
|
14692
|
+
contactLastName: 59,
|
|
14653
14693
|
contactPhoneMin: 3,
|
|
14654
14694
|
contactPhoneMax: 40,
|
|
14655
14695
|
specialRequests: 500,
|
|
14656
|
-
|
|
14696
|
+
poolLimit: 3e3,
|
|
14697
|
+
candidatesShown: 8,
|
|
14698
|
+
candidatesCounted: 200,
|
|
14699
|
+
localitiesShown: 8
|
|
14657
14700
|
};
|
|
14658
|
-
var
|
|
14659
|
-
|
|
14660
|
-
|
|
14661
|
-
|
|
14662
|
-
|
|
14663
|
-
"
|
|
14664
|
-
"
|
|
14665
|
-
"
|
|
14666
|
-
"
|
|
14667
|
-
"
|
|
14701
|
+
var DISPLAY_CURRENCIES = ["USD", "CNY"];
|
|
14702
|
+
var SUPPORT_EMAIL = "info@fortrip.ai";
|
|
14703
|
+
|
|
14704
|
+
// ../contract/src/vocabulary.ts
|
|
14705
|
+
var FILTERABLE_AMENITIES = [
|
|
14706
|
+
"parking",
|
|
14707
|
+
"wifi",
|
|
14708
|
+
"laundry",
|
|
14709
|
+
"restaurant",
|
|
14710
|
+
"room_service",
|
|
14711
|
+
"bar",
|
|
14712
|
+
"non_smoking",
|
|
14713
|
+
"business_center",
|
|
14714
|
+
"pool"
|
|
14715
|
+
];
|
|
14716
|
+
var AMENITY_CODES = [
|
|
14717
|
+
...FILTERABLE_AMENITIES,
|
|
14718
|
+
"airport_shuttle",
|
|
14719
|
+
"spa",
|
|
14720
|
+
"gym",
|
|
14721
|
+
"accessible",
|
|
14722
|
+
"ev_charging",
|
|
14723
|
+
"air_conditioning",
|
|
14724
|
+
"pets_allowed",
|
|
14725
|
+
"kitchen",
|
|
14726
|
+
"other"
|
|
14668
14727
|
];
|
|
14669
|
-
var CURRENCIES = ["CNY", "USD"];
|
|
14670
14728
|
var SEARCH_SORTS = [
|
|
14671
|
-
"
|
|
14672
|
-
"
|
|
14673
|
-
"
|
|
14674
|
-
"
|
|
14675
|
-
"
|
|
14729
|
+
"distance",
|
|
14730
|
+
"relevance",
|
|
14731
|
+
"stars_desc",
|
|
14732
|
+
"price_asc",
|
|
14733
|
+
"price_desc"
|
|
14676
14734
|
];
|
|
14677
|
-
var SUPPORT_EMAIL = "info@fortrip.ai";
|
|
14678
14735
|
|
|
14679
14736
|
// ../contract/src/inputs.ts
|
|
14680
|
-
var
|
|
14681
|
-
|
|
14682
|
-
var DEFAULT_MAX_ROOMS_PER_HOTEL = 10;
|
|
14683
|
-
var stayDescriptions = {
|
|
14684
|
-
checkIn: "Check-in date, yyyy-MM-dd.",
|
|
14685
|
-
checkOut: `Check-out date, yyyy-MM-dd. Stays are 1 to ${LIMITS.nights} nights.`
|
|
14686
|
-
};
|
|
14687
|
-
var displayCurrency = oneOf("currency", CURRENCIES).default("CNY").describe(
|
|
14688
|
-
"Currency to show prices in. Prices are converted from the supplier's at Stripe's exchange rate; the response's `fx` says how."
|
|
14737
|
+
var locale = oneOf("locale", ["en", "zh"]).describe(
|
|
14738
|
+
"Language for names and text in the response: en or zh."
|
|
14689
14739
|
);
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
var
|
|
14697
|
-
var
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
cityCode: positiveId("cityCode").optional().describe("Catalogue city code, as returned in `hotels[].cityCode`."),
|
|
14702
|
-
near: strictInput({
|
|
14703
|
-
lat: bounded("near.lat", -90, 90).describe("Latitude."),
|
|
14704
|
-
lng: bounded("near.lng", -180, 180).describe("Longitude."),
|
|
14705
|
-
radiusKm: external_exports.number({ error: radiusError }).positive({ error: radiusError }).max(LIMITS.nearRadiusKm, { error: radiusError }).describe("Radius in kilometres.")
|
|
14706
|
-
}).optional().describe("Search around a point."),
|
|
14707
|
-
starMin: bounded("starMin", 0, 5).optional().describe("Lowest star rating to include."),
|
|
14708
|
-
starMax: bounded("starMax", 0, 5).optional().describe("Highest star rating to include."),
|
|
14709
|
-
facilities: external_exports.array(
|
|
14710
|
-
external_exports.string({ error: facilitiesError }).trim().min(1, { error: facilitiesError }).max(FACILITY_NAME_MAX, { error: facilitiesError }),
|
|
14711
|
-
{ error: facilitiesError }
|
|
14712
|
-
).min(1, { error: facilitiesError }).max(LIMITS.facilities, { error: facilitiesError }).transform((names) => [...new Set(names)]).optional().describe(
|
|
14713
|
-
"Facilities every hotel must have. Names must match the catalogue exactly (case-insensitive), e.g. `Swimming pool`, `Parking`."
|
|
14714
|
-
),
|
|
14715
|
-
checkIn: isoDate("checkIn").describe(stayDescriptions.checkIn),
|
|
14716
|
-
checkOut: isoDate("checkOut").describe(stayDescriptions.checkOut),
|
|
14717
|
-
currency: displayCurrency,
|
|
14718
|
-
priceMin: external_exports.number({ error: "priceMin must be a number, 0 or more" }).nonnegative({ error: "priceMin must be a number, 0 or more" }).optional().describe(
|
|
14719
|
-
"Lowest average nightly price, in `currency`. Applied to indicative prices."
|
|
14740
|
+
var displayCurrency = oneOf("displayCurrency", [
|
|
14741
|
+
"USD",
|
|
14742
|
+
"CNY"
|
|
14743
|
+
]).describe(
|
|
14744
|
+
"Currency to show prices in, USD or CNY. It never changes what is charged: payment is in USD."
|
|
14745
|
+
);
|
|
14746
|
+
var cursor = nonEmpty("cursor");
|
|
14747
|
+
var nightsError = (name) => `${name} must be after the check-in date and at most ${LIMITS.nightsFortrip} nights later`;
|
|
14748
|
+
var stay = strictInput({
|
|
14749
|
+
checkIn: localDate.describe(
|
|
14750
|
+
"Check-in date in the hotel's calendar, yyyy-MM-dd."
|
|
14720
14751
|
),
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
"Result order. `recommended` sorts by distance with `near`, by relevance with `query`, otherwise by catalogue weight. `distance` requires `near`."
|
|
14752
|
+
checkOut: localDate.describe(
|
|
14753
|
+
`Check-out date in the hotel's calendar, yyyy-MM-dd. Stays are 1 to ${LIMITS.nightsFortrip} nights.`
|
|
14724
14754
|
),
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
}
|
|
14741
|
-
}
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
}
|
|
14749
|
-
if (input.priceMin !== void 0 && input.priceMax !== void 0 && input.priceMin > input.priceMax) {
|
|
14750
|
-
ctx.addIssue({
|
|
14751
|
-
code: "custom",
|
|
14752
|
-
path: ["priceMax"],
|
|
14753
|
-
message: "priceMax must be >= priceMin"
|
|
14754
|
-
});
|
|
14755
|
-
}
|
|
14756
|
-
if (!isIsoDate(input.checkIn) || !isIsoDate(input.checkOut)) return;
|
|
14757
|
-
const earliest = addIsoDateDays((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), -1);
|
|
14758
|
-
if (input.checkIn < earliest) {
|
|
14759
|
-
ctx.addIssue({
|
|
14760
|
-
code: "custom",
|
|
14761
|
-
path: ["checkIn"],
|
|
14762
|
-
message: "checkIn must not be in the past"
|
|
14763
|
-
});
|
|
14764
|
-
}
|
|
14765
|
-
const nights = countIsoDateDays(input.checkIn, input.checkOut);
|
|
14766
|
-
if (nights < 1) {
|
|
14767
|
-
ctx.addIssue({
|
|
14768
|
-
code: "custom",
|
|
14769
|
-
path: ["checkOut"],
|
|
14770
|
-
message: "checkOut must be after checkIn"
|
|
14771
|
-
});
|
|
14772
|
-
} else if (nights > LIMITS.nights) {
|
|
14755
|
+
rooms: external_exports.array(
|
|
14756
|
+
strictInput({
|
|
14757
|
+
adults: wholeNumber("adults", 1, LIMITS.adultsPerRoom).describe(
|
|
14758
|
+
"Adults in this room."
|
|
14759
|
+
),
|
|
14760
|
+
childrenAges: external_exports.array(wholeNumber("Each child's age", 0, LIMITS.childAgeMax), {
|
|
14761
|
+
error: `childrenAges must list each child's age (0 to ${LIMITS.childAgeMax}) in this room, at most ${LIMITS.childrenPerRoom}`
|
|
14762
|
+
}).max(LIMITS.childrenPerRoom, {
|
|
14763
|
+
error: `childrenAges must list each child's age (0 to ${LIMITS.childAgeMax}) in this room, at most ${LIMITS.childrenPerRoom}`
|
|
14764
|
+
}).describe(
|
|
14765
|
+
"Age of each child in this room, 0 to 17. An empty list means no children."
|
|
14766
|
+
)
|
|
14767
|
+
}),
|
|
14768
|
+
{ error: `rooms must list 1 to ${LIMITS.rooms} rooms` }
|
|
14769
|
+
).min(1, { error: `rooms must list 1 to ${LIMITS.rooms} rooms` }).max(LIMITS.rooms, {
|
|
14770
|
+
error: `rooms must list 1 to ${LIMITS.rooms} rooms`
|
|
14771
|
+
}).describe(
|
|
14772
|
+
`The rooms wanted, 1 to ${LIMITS.rooms}, each with its own party.`
|
|
14773
|
+
)
|
|
14774
|
+
}).superRefine((value, ctx) => {
|
|
14775
|
+
if (!isIsoDate(value.checkIn) || !isIsoDate(value.checkOut)) return;
|
|
14776
|
+
const nights = countIsoDateDays(value.checkIn, value.checkOut);
|
|
14777
|
+
if (nights < 1 || nights > LIMITS.nightsFortrip) {
|
|
14773
14778
|
ctx.addIssue({
|
|
14774
14779
|
code: "custom",
|
|
14775
14780
|
path: ["checkOut"],
|
|
14776
|
-
message:
|
|
14781
|
+
message: nightsError("checkOut")
|
|
14777
14782
|
});
|
|
14778
14783
|
}
|
|
14779
14784
|
});
|
|
14780
|
-
var
|
|
14781
|
-
|
|
14785
|
+
var countryCode = external_exports.string({ error: "countryCode must be an ISO 3166-1 alpha-2 code" }).regex(/^[A-Z]{2}$/, {
|
|
14786
|
+
error: "countryCode must be 2 uppercase letters (ISO 3166-1 alpha-2)"
|
|
14787
|
+
});
|
|
14788
|
+
var radiusMeters = wholeNumber("radiusMeters", 1, LIMITS.radiusMetersMax).optional().describe(
|
|
14789
|
+
`Search radius around the destination in meters, 1 to ${LIMITS.radiusMetersMax}. Left out, the service picks one.`
|
|
14790
|
+
);
|
|
14791
|
+
function bounded(name, min, max) {
|
|
14792
|
+
const error51 = `${name} must be a number from ${min} to ${max}`;
|
|
14793
|
+
return external_exports.number({ error: error51 }).min(min, { error: error51 }).max(max, { error: error51 });
|
|
14794
|
+
}
|
|
14795
|
+
var KINDS = ["place_name", "place", "point", "country", "locality"];
|
|
14796
|
+
var destination = external_exports.discriminatedUnion(
|
|
14797
|
+
"kind",
|
|
14798
|
+
[
|
|
14799
|
+
strictInput({
|
|
14800
|
+
kind: external_exports.literal("place_name", { error: "kind must be place_name" }).describe("A place named in words, with its country."),
|
|
14801
|
+
name: text("name", 1, LIMITS.textLength).describe(
|
|
14802
|
+
"The place name, in any language, e.g. Tokyo or \u4E1C\u4EAC."
|
|
14803
|
+
),
|
|
14804
|
+
countryCode: countryCode.describe(
|
|
14805
|
+
"ISO 3166-1 alpha-2 country the place is in, e.g. JP."
|
|
14806
|
+
),
|
|
14807
|
+
radiusMeters
|
|
14808
|
+
}),
|
|
14809
|
+
strictInput({
|
|
14810
|
+
kind: external_exports.literal("place", { error: "kind must be place" }).describe("A place id from an earlier response."),
|
|
14811
|
+
placeId: placeId.describe("placeId from an earlier response."),
|
|
14812
|
+
radiusMeters
|
|
14813
|
+
}),
|
|
14814
|
+
strictInput({
|
|
14815
|
+
kind: external_exports.literal("point", { error: "kind must be point" }).describe("A point on the map."),
|
|
14816
|
+
location: strictInput({
|
|
14817
|
+
lat: bounded("lat", -90, 90).describe("Latitude, WGS84."),
|
|
14818
|
+
lng: bounded("lng", -180, 180).describe("Longitude, WGS84.")
|
|
14819
|
+
}).describe("The point, WGS84."),
|
|
14820
|
+
radiusMeters
|
|
14821
|
+
}),
|
|
14822
|
+
strictInput({
|
|
14823
|
+
kind: external_exports.literal("country", { error: "kind must be country" }).describe("A whole country."),
|
|
14824
|
+
countryCode: countryCode.describe("ISO 3166-1 alpha-2 country, e.g. JP.")
|
|
14825
|
+
}),
|
|
14826
|
+
strictInput({
|
|
14827
|
+
kind: external_exports.literal("locality", { error: "kind must be locality" }).describe("A locality (hub) listed in an earlier search response."),
|
|
14828
|
+
placeId: placeId.describe("placeId of the locality."),
|
|
14829
|
+
radiusMeters
|
|
14830
|
+
})
|
|
14831
|
+
],
|
|
14782
14832
|
{
|
|
14783
|
-
|
|
14784
|
-
|
|
14785
|
-
},
|
|
14786
|
-
{ error: "Expected a JSON object" }
|
|
14833
|
+
error: `destination.kind must be one of: ${KINDS.join(", ")}`
|
|
14834
|
+
}
|
|
14787
14835
|
);
|
|
14788
|
-
var
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
)
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14836
|
+
var textQuery = external_exports.string({
|
|
14837
|
+
error: `text must be text of 1 to ${LIMITS.textLength} characters`
|
|
14838
|
+
}).trim().min(1, {
|
|
14839
|
+
error: `text must be text of 1 to ${LIMITS.textLength} characters`
|
|
14840
|
+
}).max(LIMITS.textLength, {
|
|
14841
|
+
error: `text must be text of 1 to ${LIMITS.textLength} characters`
|
|
14842
|
+
});
|
|
14843
|
+
var starsError = "must be from 1 to 5 in steps of 0.5";
|
|
14844
|
+
function starBound(name) {
|
|
14845
|
+
return external_exports.number({ error: `${name} ${starsError}` }).min(1, { error: `${name} ${starsError}` }).max(5, { error: `${name} ${starsError}` }).refine((value) => Number.isInteger(value * 2), {
|
|
14846
|
+
error: `${name} ${starsError}`
|
|
14847
|
+
});
|
|
14848
|
+
}
|
|
14849
|
+
var amenitiesError = `amenities must list 1 to ${LIMITS.amenityFilters} codes from: ${AMENITY_CODES.join(", ")}`;
|
|
14850
|
+
var searchFilters = strictInput({
|
|
14851
|
+
stars: strictInput({
|
|
14852
|
+
min: starBound("stars.min").optional().describe("Lowest star rating, 1 to 5 in steps of 0.5."),
|
|
14853
|
+
max: starBound("stars.max").optional().describe("Highest star rating, 1 to 5 in steps of 0.5.")
|
|
14854
|
+
}).superRefine((value, ctx) => {
|
|
14855
|
+
if (value.min !== void 0 && value.max !== void 0 && value.min > value.max) {
|
|
14856
|
+
ctx.addIssue({
|
|
14857
|
+
code: "custom",
|
|
14858
|
+
path: ["max"],
|
|
14859
|
+
message: "stars.max must be at least stars.min"
|
|
14860
|
+
});
|
|
14861
|
+
}
|
|
14803
14862
|
}).optional().describe(
|
|
14804
|
-
"
|
|
14863
|
+
"Star rating range. Unrated hotels are left out whenever it is set."
|
|
14864
|
+
),
|
|
14865
|
+
amenities: external_exports.array(oneOf("Each amenity", AMENITY_CODES), { error: amenitiesError }).min(1, { error: amenitiesError }).max(LIMITS.amenityFilters, { error: amenitiesError }).optional().describe(
|
|
14866
|
+
"Amenities every hotel must list. Only some codes can filter; asking for one that cannot is answered with the list that can."
|
|
14805
14867
|
),
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14868
|
+
price: strictInput({
|
|
14869
|
+
maxPerRoomNight: usdMoney.describe(
|
|
14870
|
+
"Highest price per room per night, in USD."
|
|
14871
|
+
)
|
|
14872
|
+
}).optional().describe("Price limit. Needs indicative pricing, so a stay.")
|
|
14873
|
+
}).optional().describe("Filters every hotel in the answer must pass.");
|
|
14874
|
+
var NEXT_PAGE_KEYS = /* @__PURE__ */ new Set(["cursor", "limit"]);
|
|
14875
|
+
function createSearchHotelsInput(now = Date.now) {
|
|
14876
|
+
return strictInput({
|
|
14877
|
+
destination: destination.optional().describe("Where to search. A first page needs this or `text`."),
|
|
14878
|
+
text: textQuery.optional().describe(
|
|
14879
|
+
`Hotel name, brand or area to match, 1 to ${LIMITS.textLength} characters. A first page needs this or \`destination\`.`
|
|
14880
|
+
),
|
|
14881
|
+
stay: stay.optional().describe(
|
|
14882
|
+
"Dates and party. Needed for indicative prices. Search uses only the dates when the rooms differ."
|
|
14883
|
+
),
|
|
14884
|
+
pricing: oneOf("pricing", ["indicative", "none"]).optional().describe(
|
|
14885
|
+
"`indicative` adds an approximate price per hotel and needs a stay; `none` skips prices. Default: indicative with a stay, otherwise none."
|
|
14886
|
+
),
|
|
14887
|
+
filters: searchFilters,
|
|
14888
|
+
sort: oneOf("sort", SEARCH_SORTS).optional().describe(
|
|
14889
|
+
"Result order. `distance` needs a destination, `relevance` needs `text`, `price_asc` and `price_desc` need indicative pricing. Default: relevance with `text`, otherwise distance."
|
|
14890
|
+
),
|
|
14891
|
+
limit: wholeNumber("limit", 1, LIMITS.pageSizeMax).optional().describe(
|
|
14892
|
+
`Hotels per page, 1 to ${LIMITS.pageSizeMax}. Default ${LIMITS.pageSizeDefault}.`
|
|
14893
|
+
),
|
|
14894
|
+
cursor: cursor.optional().describe(
|
|
14895
|
+
"`nextCursor` from the previous response. A next page is the cursor and, optionally, `limit`; nothing else."
|
|
14896
|
+
),
|
|
14897
|
+
locale: locale.optional(),
|
|
14898
|
+
displayCurrency: displayCurrency.optional()
|
|
14899
|
+
}).superRefine((input, ctx) => {
|
|
14900
|
+
const issue2 = (path, message) => ctx.addIssue({ code: "custom", path, message });
|
|
14901
|
+
if (input.cursor !== void 0) {
|
|
14902
|
+
for (const [key, value] of Object.entries(input)) {
|
|
14903
|
+
if (value !== void 0 && !NEXT_PAGE_KEYS.has(key)) {
|
|
14904
|
+
issue2(
|
|
14905
|
+
[key],
|
|
14906
|
+
`${key} cannot be sent with cursor: a next page is the cursor and limit only`
|
|
14907
|
+
);
|
|
14908
|
+
}
|
|
14909
|
+
}
|
|
14910
|
+
return;
|
|
14911
|
+
}
|
|
14912
|
+
if (input.destination === void 0 && input.text === void 0) {
|
|
14913
|
+
issue2(["destination"], "Provide a destination or text");
|
|
14914
|
+
}
|
|
14915
|
+
if (input.sort === "distance" && input.destination === void 0) {
|
|
14916
|
+
issue2(["sort"], "sort distance needs a destination");
|
|
14917
|
+
}
|
|
14918
|
+
if (input.sort === "relevance" && input.text === void 0) {
|
|
14919
|
+
issue2(["sort"], "sort relevance needs text");
|
|
14920
|
+
}
|
|
14921
|
+
const indicative = (input.pricing ?? (input.stay ? "indicative" : "none")) === "indicative";
|
|
14922
|
+
if (input.pricing === "indicative" && input.stay === void 0) {
|
|
14923
|
+
issue2(["stay"], "pricing indicative needs a stay");
|
|
14924
|
+
}
|
|
14925
|
+
if (!indicative) {
|
|
14926
|
+
const reason = input.pricing === "none" ? 'pricing is "none"' : "there is no stay";
|
|
14927
|
+
if (input.sort === "price_asc" || input.sort === "price_desc") {
|
|
14928
|
+
issue2(
|
|
14929
|
+
["sort"],
|
|
14930
|
+
`${input.sort} needs indicative pricing, but ${reason}`
|
|
14931
|
+
);
|
|
14932
|
+
}
|
|
14933
|
+
if (input.filters?.price !== void 0) {
|
|
14934
|
+
issue2(
|
|
14935
|
+
["filters", "price"],
|
|
14936
|
+
`filters.price needs indicative pricing, but ${reason}`
|
|
14937
|
+
);
|
|
14938
|
+
}
|
|
14939
|
+
}
|
|
14940
|
+
const checkIn = input.stay?.checkIn;
|
|
14941
|
+
if (checkIn !== void 0 && isIsoDate(checkIn)) {
|
|
14942
|
+
const earliest = new Date(now() - 12 * 36e5).toISOString().slice(0, 10);
|
|
14943
|
+
if (checkIn < earliest) {
|
|
14944
|
+
issue2(["stay", "checkIn"], "stay.checkIn must not be in the past");
|
|
14945
|
+
}
|
|
14946
|
+
}
|
|
14947
|
+
});
|
|
14948
|
+
}
|
|
14949
|
+
var searchHotelsInput = createSearchHotelsInput();
|
|
14950
|
+
var getHotelInput = strictInput({
|
|
14951
|
+
hotelId: hotelId.describe("hotelId from a search response."),
|
|
14952
|
+
include: oneOf("include", ["rooms"]).optional().describe("`rooms` adds the room list."),
|
|
14953
|
+
locale: locale.optional()
|
|
14954
|
+
});
|
|
14955
|
+
var hotelIdsError = `hotelIds must list 1 to ${LIMITS.offersHotels} distinct hotelIds`;
|
|
14956
|
+
var hotelOffersInput = strictInput({
|
|
14957
|
+
hotelIds: external_exports.array(hotelId, { error: hotelIdsError }).min(1, { error: hotelIdsError }).max(LIMITS.offersHotels, { error: hotelIdsError }).refine((ids) => new Set(ids).size === ids.length, {
|
|
14958
|
+
error: hotelIdsError
|
|
14959
|
+
}).describe(`Hotels to price, 1 to ${LIMITS.offersHotels}, no repeats.`),
|
|
14960
|
+
stay: stay.describe(
|
|
14961
|
+
"Dates and party. One party per call: every room must be the same."
|
|
14812
14962
|
),
|
|
14813
|
-
|
|
14814
|
-
"Keep only
|
|
14963
|
+
filters: strictInput({
|
|
14964
|
+
requireBreakfast: flag("requireBreakfast").optional().describe("Keep only offers that include breakfast."),
|
|
14965
|
+
requireFreeCancellation: flag("requireFreeCancellation").optional().describe("Keep only offers that can still be cancelled free of charge.")
|
|
14966
|
+
}).optional().describe("Filters every offer must pass."),
|
|
14967
|
+
limitPerHotel: wholeNumber("limitPerHotel", 1, LIMITS.offersPerHotelMax).default(LIMITS.offersPerHotelDefault).describe(
|
|
14968
|
+
`Offers per hotel, 1 to ${LIMITS.offersPerHotelMax}. Default ${LIMITS.offersPerHotelDefault}.`
|
|
14815
14969
|
),
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
)
|
|
14819
|
-
}).superRefine((input, ctx) => {
|
|
14820
|
-
if (!isIsoDate(input.checkIn) || !isIsoDate(input.checkOut)) return;
|
|
14821
|
-
const nights = countIsoDateDays(input.checkIn, input.checkOut);
|
|
14822
|
-
if (nights < 1) {
|
|
14823
|
-
ctx.addIssue({
|
|
14824
|
-
code: "custom",
|
|
14825
|
-
path: ["checkOut"],
|
|
14826
|
-
message: "checkOut must be after checkIn"
|
|
14827
|
-
});
|
|
14828
|
-
} else if (nights > LIMITS.nights) {
|
|
14829
|
-
ctx.addIssue({
|
|
14830
|
-
code: "custom",
|
|
14831
|
-
path: ["checkOut"],
|
|
14832
|
-
message: `rates support at most ${LIMITS.nights} nights`
|
|
14833
|
-
});
|
|
14834
|
-
}
|
|
14970
|
+
locale: locale.optional(),
|
|
14971
|
+
displayCurrency: displayCurrency.optional()
|
|
14835
14972
|
});
|
|
14836
14973
|
function cardFreeText(name, min, max, minDigits) {
|
|
14837
14974
|
return text(name, min, max).refine(
|
|
@@ -14850,76 +14987,101 @@ function guestName(name) {
|
|
|
14850
14987
|
{ error: GUEST_NAME_MESSAGE }
|
|
14851
14988
|
);
|
|
14852
14989
|
}
|
|
14990
|
+
function contactName(name, max) {
|
|
14991
|
+
return external_exports.string({ error: `${name} must be text of 1 to ${max} characters` }).refine((value) => !containsCardNumber(value), {
|
|
14992
|
+
error: CARD_NUMBER_MESSAGE,
|
|
14993
|
+
abort: true
|
|
14994
|
+
}).refine(
|
|
14995
|
+
(value) => {
|
|
14996
|
+
const length = normalizeGuestName(value).length;
|
|
14997
|
+
return length >= 1 && length <= max;
|
|
14998
|
+
},
|
|
14999
|
+
{ error: `${name} must be text of 1 to ${max} characters` }
|
|
15000
|
+
);
|
|
15001
|
+
}
|
|
15002
|
+
var emailError = "email must be an email address";
|
|
14853
15003
|
var guestsError = "guests must list at least one guest per room";
|
|
14854
|
-
var
|
|
14855
|
-
var
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
"
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
guests: external_exports.array(
|
|
15004
|
+
var maxGuestsPerRoom = LIMITS.adultsPerRoom + LIMITS.childrenPerRoom;
|
|
15005
|
+
var createOrderInput = strictInput({
|
|
15006
|
+
contact: strictInput({
|
|
15007
|
+
firstName: contactName("firstName", LIMITS.contactFirstName).describe(
|
|
15008
|
+
`Contact person's first name, at most ${LIMITS.contactFirstName} characters.`
|
|
15009
|
+
),
|
|
15010
|
+
lastName: contactName("lastName", LIMITS.contactLastName).describe(
|
|
15011
|
+
`Contact person's last name, at most ${LIMITS.contactLastName} characters.`
|
|
15012
|
+
),
|
|
15013
|
+
email: external_exports.string({ error: emailError }).trim().pipe(external_exports.email({ error: emailError })).describe("Contact email; the confirmation is sent here."),
|
|
15014
|
+
// Only cards of 16+ digits: a phone number has at most 15 (E.164).
|
|
15015
|
+
phone: cardFreeText(
|
|
15016
|
+
"phone",
|
|
15017
|
+
LIMITS.contactPhoneMin,
|
|
15018
|
+
LIMITS.contactPhoneMax,
|
|
15019
|
+
16
|
|
15020
|
+
).describe("Contact phone number, with country code.")
|
|
15021
|
+
}).describe("Who the hotel and the confirmation reach."),
|
|
15022
|
+
items: external_exports.array(
|
|
14874
15023
|
strictInput({
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
15024
|
+
offerId: offerId.describe("offerId from an offers response."),
|
|
15025
|
+
hotel: strictInput({
|
|
15026
|
+
rooms: external_exports.array(
|
|
15027
|
+
strictInput({
|
|
15028
|
+
guests: external_exports.array(
|
|
15029
|
+
strictInput({
|
|
15030
|
+
firstName: guestName("firstName").describe(
|
|
15031
|
+
"Guest's first name, in Latin letters as on the passport; transliterate accents (M\xFCller \u2192 Mueller)."
|
|
15032
|
+
),
|
|
15033
|
+
lastName: guestName("lastName").describe(
|
|
15034
|
+
"Guest's last name, in Latin letters as on the passport."
|
|
15035
|
+
)
|
|
15036
|
+
}),
|
|
15037
|
+
{ error: guestsError }
|
|
15038
|
+
).min(1, { error: guestsError }).max(maxGuestsPerRoom, {
|
|
15039
|
+
error: `guests must list at most ${maxGuestsPerRoom} guests per room`
|
|
15040
|
+
}).describe(
|
|
15041
|
+
"Guests staying in this room: at least one, no more than the room sleeps."
|
|
15042
|
+
)
|
|
15043
|
+
}),
|
|
15044
|
+
{ error: `rooms must list 1 to ${LIMITS.rooms} rooms` }
|
|
15045
|
+
).min(1, { error: `rooms must list 1 to ${LIMITS.rooms} rooms` }).max(LIMITS.rooms, {
|
|
15046
|
+
error: `rooms must list 1 to ${LIMITS.rooms} rooms`
|
|
15047
|
+
}).describe("One entry per room of the offer, in order."),
|
|
15048
|
+
specialRequests: cardFreeText(
|
|
15049
|
+
"specialRequests",
|
|
15050
|
+
0,
|
|
15051
|
+
LIMITS.specialRequests
|
|
15052
|
+
).optional().describe("Requests for the hotel, in English. Not guaranteed.")
|
|
15053
|
+
}).describe("What the hotel needs to know.")
|
|
14879
15054
|
}),
|
|
14880
|
-
{ error:
|
|
14881
|
-
).min(1, { error:
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
15055
|
+
{ error: "items must list exactly one offer" }
|
|
15056
|
+
).min(1, { error: "items must list exactly one offer" }).max(1, { error: "items must list exactly one offer" }).describe("The offer to book. Exactly one in v1.")
|
|
15057
|
+
});
|
|
15058
|
+
var getOrderInput = strictInput({
|
|
15059
|
+
orderId: orderId.describe("orderId from create order or list orders."),
|
|
15060
|
+
locale: locale.optional()
|
|
15061
|
+
});
|
|
15062
|
+
var listLimitError = `limit must be a whole number from 1 to ${LIMITS.pageSizeMax}`;
|
|
15063
|
+
var listOrdersInput = strictInput({
|
|
15064
|
+
cursor: cursor.optional().describe("`nextCursor` from the previous page of orders."),
|
|
15065
|
+
limit: external_exports.coerce.number({ error: listLimitError }).int({ error: listLimitError }).min(1, { error: listLimitError }).max(LIMITS.pageSizeMax, { error: listLimitError }).optional().describe(
|
|
15066
|
+
`Orders per page, 1 to ${LIMITS.pageSizeMax}. Default ${LIMITS.pageSizeDefault}.`
|
|
14886
15067
|
),
|
|
14887
|
-
|
|
14888
|
-
// its digits aren't mistaken for a card.
|
|
14889
|
-
contactPhone: cardFreeText(
|
|
14890
|
-
"contactPhone",
|
|
14891
|
-
LIMITS.contactPhoneMin,
|
|
14892
|
-
LIMITS.contactPhoneMax,
|
|
14893
|
-
16
|
|
14894
|
-
).describe("Contact phone number."),
|
|
14895
|
-
// Blank means none, as it did before: the booking sends no email then.
|
|
14896
|
-
contactEmail: external_exports.string({ error: contactEmailError }).trim().refine((value) => value === "" || external_exports.email().safeParse(value).success, {
|
|
14897
|
-
error: contactEmailError
|
|
14898
|
-
}).optional().describe("Contact email."),
|
|
14899
|
-
specialRequests: cardFreeText("specialRequests", 0, LIMITS.specialRequests).optional().describe("Requests for the hotel, in English. Not guaranteed.")
|
|
14900
|
-
}).superRefine((input, ctx) => {
|
|
14901
|
-
if (!isIsoDate(input.checkIn) || !isIsoDate(input.checkOut)) return;
|
|
14902
|
-
const nights = countIsoDateDays(input.checkIn, input.checkOut);
|
|
14903
|
-
if (nights < 1) {
|
|
14904
|
-
ctx.addIssue({
|
|
14905
|
-
code: "custom",
|
|
14906
|
-
path: ["checkOut"],
|
|
14907
|
-
message: "checkOut must be after checkIn"
|
|
14908
|
-
});
|
|
14909
|
-
} else if (nights > LIMITS.nights) {
|
|
14910
|
-
ctx.addIssue({
|
|
14911
|
-
code: "custom",
|
|
14912
|
-
path: ["checkOut"],
|
|
14913
|
-
message: `A booking covers at most ${LIMITS.nights} nights; book a longer stay as separate bookings`
|
|
14914
|
-
});
|
|
14915
|
-
}
|
|
15068
|
+
locale: locale.optional()
|
|
14916
15069
|
});
|
|
14917
15070
|
|
|
14918
15071
|
// ../contract/src/commands.ts
|
|
14919
|
-
function fieldDescription(json2, field) {
|
|
15072
|
+
function fieldDescription(json2, field, sets = {}) {
|
|
14920
15073
|
let node = json2;
|
|
15074
|
+
const path = [];
|
|
14921
15075
|
for (const part of field.split(".")) {
|
|
14922
|
-
|
|
15076
|
+
if (node?.type === "array") node = node.items;
|
|
15077
|
+
const at = path.join(".");
|
|
15078
|
+
const variants = node?.oneOf ?? node?.anyOf ?? (node ? [node] : []);
|
|
15079
|
+
node = variants.find(
|
|
15080
|
+
(variant) => variant.properties?.[part] && Object.entries(sets).every(
|
|
15081
|
+
([key, value]) => !key.startsWith(`${at}.`) || variant.properties?.[key.slice(at.length + 1)]?.const === value
|
|
15082
|
+
)
|
|
15083
|
+
)?.properties?.[part];
|
|
15084
|
+
path.push(part);
|
|
14923
15085
|
}
|
|
14924
15086
|
if (!node?.description) throw new Error(`No description for field ${field}`);
|
|
14925
15087
|
return node.description;
|
|
@@ -14928,193 +15090,219 @@ function flagsOf(input, defs) {
|
|
|
14928
15090
|
const json2 = external_exports.toJSONSchema(input, { io: "input" });
|
|
14929
15091
|
return defs.map((def) => ({
|
|
14930
15092
|
...def,
|
|
14931
|
-
description: def.description ?? fieldDescription(json2, def.field)
|
|
15093
|
+
description: def.description ?? fieldDescription(json2, def.field, def.sets)
|
|
14932
15094
|
}));
|
|
14933
15095
|
}
|
|
14934
|
-
|
|
15096
|
+
function positional(input, name, description) {
|
|
15097
|
+
const json2 = external_exports.toJSONSchema(input, { io: "input" });
|
|
15098
|
+
return {
|
|
15099
|
+
name,
|
|
15100
|
+
field: name,
|
|
15101
|
+
description: description ?? fieldDescription(json2, name)
|
|
15102
|
+
};
|
|
15103
|
+
}
|
|
14935
15104
|
var COUNT = "<n>";
|
|
14936
15105
|
var DATE = "YYYY-MM-DD";
|
|
15106
|
+
var ROOM = "<adults[:age,\u2026]>";
|
|
15107
|
+
var LOCALES = "en|zh";
|
|
15108
|
+
var kind = (value) => ({ "destination.kind": value });
|
|
15109
|
+
var localeFlag = {
|
|
15110
|
+
flag: "locale",
|
|
15111
|
+
field: "locale",
|
|
15112
|
+
type: "string",
|
|
15113
|
+
placeholder: LOCALES
|
|
15114
|
+
};
|
|
15115
|
+
var displayCurrencyFlag = {
|
|
15116
|
+
flag: "display-currency",
|
|
15117
|
+
field: "displayCurrency",
|
|
15118
|
+
type: "string",
|
|
15119
|
+
placeholder: DISPLAY_CURRENCIES.join("|")
|
|
15120
|
+
};
|
|
15121
|
+
function stayFlags(roomDescription) {
|
|
15122
|
+
return [
|
|
15123
|
+
{ flag: "check-in", field: "stay.checkIn", type: "string", placeholder: DATE },
|
|
15124
|
+
{ flag: "check-out", field: "stay.checkOut", type: "string", placeholder: DATE },
|
|
15125
|
+
{
|
|
15126
|
+
flag: "room",
|
|
15127
|
+
field: "stay.rooms",
|
|
15128
|
+
type: "string",
|
|
15129
|
+
multiple: true,
|
|
15130
|
+
placeholder: ROOM,
|
|
15131
|
+
description: roomDescription
|
|
15132
|
+
}
|
|
15133
|
+
];
|
|
15134
|
+
}
|
|
15135
|
+
var ROOM_FORMAT = "as `adults` or `adults:age,age` with each child's age, e.g. `2` or `2:6,9`";
|
|
14937
15136
|
var searchFlags = flagsOf(searchHotelsInput, [
|
|
14938
15137
|
{
|
|
14939
|
-
flag: "
|
|
14940
|
-
|
|
14941
|
-
field: "query",
|
|
15138
|
+
flag: "place-name",
|
|
15139
|
+
field: "destination.name",
|
|
14942
15140
|
type: "string",
|
|
14943
|
-
|
|
14944
|
-
|
|
15141
|
+
sets: kind("place_name"),
|
|
15142
|
+
placeholder: "<name>",
|
|
15143
|
+
description: "Search a place by its name, in any language (e.g. Tokyo or \u4E1C\u4EAC), with its country in `--country`."
|
|
14945
15144
|
},
|
|
14946
|
-
{ flag: "city-code", field: "cityCode", type: "id", placeholder: COUNT },
|
|
14947
15145
|
{
|
|
14948
|
-
flag: "
|
|
14949
|
-
field: "
|
|
15146
|
+
flag: "country",
|
|
15147
|
+
field: "destination.countryCode",
|
|
14950
15148
|
type: "string",
|
|
14951
|
-
|
|
14952
|
-
|
|
15149
|
+
sets: kind("place_name"),
|
|
15150
|
+
placeholder: "<CC>",
|
|
15151
|
+
description: "The country of `--place-name`, ISO 3166-1 alpha-2, e.g. JP."
|
|
14953
15152
|
},
|
|
14954
|
-
{ flag: "check-in", field: "checkIn", type: "string", placeholder: DATE },
|
|
14955
|
-
{ flag: "check-out", field: "checkOut", type: "string", placeholder: DATE },
|
|
14956
|
-
{ flag: "star-min", field: "starMin", type: "number", placeholder: COUNT },
|
|
14957
|
-
{ flag: "star-max", field: "starMax", type: "number", placeholder: COUNT },
|
|
14958
15153
|
{
|
|
14959
|
-
flag: "
|
|
14960
|
-
field: "
|
|
15154
|
+
flag: "place",
|
|
15155
|
+
field: "destination.placeId",
|
|
14961
15156
|
type: "string",
|
|
14962
|
-
|
|
14963
|
-
placeholder: "<
|
|
15157
|
+
sets: kind("place"),
|
|
15158
|
+
placeholder: "<plc_>",
|
|
15159
|
+
description: "Search a place by its placeId from an earlier answer."
|
|
14964
15160
|
},
|
|
14965
|
-
{ flag: "price-min", field: "priceMin", type: "number", placeholder: COUNT },
|
|
14966
|
-
{ flag: "price-max", field: "priceMax", type: "number", placeholder: COUNT },
|
|
14967
15161
|
{
|
|
14968
|
-
flag: "
|
|
14969
|
-
field: "
|
|
15162
|
+
flag: "locality",
|
|
15163
|
+
field: "destination.placeId",
|
|
14970
15164
|
type: "string",
|
|
14971
|
-
|
|
15165
|
+
sets: kind("locality"),
|
|
15166
|
+
placeholder: "<plc_>",
|
|
15167
|
+
description: "Search a locality by its placeId, as an earlier search listed it under localities."
|
|
14972
15168
|
},
|
|
14973
|
-
{ flag: "page", field: "page", type: "number", placeholder: COUNT },
|
|
14974
|
-
{ flag: "page-size", field: "pageSize", type: "number", placeholder: COUNT },
|
|
14975
15169
|
{
|
|
14976
|
-
flag: "
|
|
14977
|
-
field: "
|
|
15170
|
+
flag: "point",
|
|
15171
|
+
field: "destination.location",
|
|
14978
15172
|
type: "string",
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
var hotelFlags = flagsOf(getHotelInput, [
|
|
14983
|
-
{ flag: "with-rooms", field: "includeRooms", type: "boolean" }
|
|
14984
|
-
]);
|
|
14985
|
-
var ratesFlags = flagsOf(getRatesInput, [
|
|
14986
|
-
{
|
|
14987
|
-
flag: "hotel",
|
|
14988
|
-
field: "hotelIds",
|
|
14989
|
-
type: "id",
|
|
14990
|
-
multiple: true,
|
|
14991
|
-
placeholder: ID,
|
|
14992
|
-
description: `Hotels to price, by id from \`hotels search\`: repeat the flag or comma-separate, up to ${LIMITS.ratesHotels}. Each call takes about 15 seconds.`
|
|
15173
|
+
sets: kind("point"),
|
|
15174
|
+
placeholder: "<lat,lng>",
|
|
15175
|
+
description: "Search around a point, as `lat,lng` (WGS84), e.g. `13.75,100.5`."
|
|
14993
15176
|
},
|
|
14994
|
-
{ flag: "check-in", field: "checkIn", type: "string", placeholder: DATE },
|
|
14995
|
-
{ flag: "check-out", field: "checkOut", type: "string", placeholder: DATE },
|
|
14996
|
-
{ flag: "rooms", field: "rooms", type: "number", placeholder: COUNT },
|
|
14997
15177
|
{
|
|
14998
|
-
flag: "
|
|
14999
|
-
field: "
|
|
15000
|
-
type: "
|
|
15001
|
-
|
|
15002
|
-
|
|
15178
|
+
flag: "country-only",
|
|
15179
|
+
field: "destination.countryCode",
|
|
15180
|
+
type: "string",
|
|
15181
|
+
sets: kind("country"),
|
|
15182
|
+
placeholder: "<CC>",
|
|
15183
|
+
description: "Search a whole country, ISO 3166-1 alpha-2, e.g. JP."
|
|
15003
15184
|
},
|
|
15004
15185
|
{
|
|
15005
|
-
flag: "
|
|
15006
|
-
field: "
|
|
15186
|
+
flag: "radius-meters",
|
|
15187
|
+
field: "destination.radiusMeters",
|
|
15007
15188
|
type: "number",
|
|
15008
|
-
|
|
15009
|
-
placeholder: "<age>"
|
|
15189
|
+
placeholder: COUNT
|
|
15010
15190
|
},
|
|
15011
15191
|
{
|
|
15012
|
-
flag: "
|
|
15013
|
-
field: "
|
|
15192
|
+
flag: "text",
|
|
15193
|
+
field: "text",
|
|
15014
15194
|
type: "string",
|
|
15015
|
-
placeholder: "<
|
|
15195
|
+
placeholder: "<text>",
|
|
15196
|
+
description: `Hotel name, brand or area to match, 1 to ${LIMITS.textLength} characters. A first page needs this or a destination.`
|
|
15016
15197
|
},
|
|
15198
|
+
...stayFlags(
|
|
15199
|
+
`A room, ${ROOM_FORMAT}. Repeat for each room, up to ${LIMITS.rooms}; rooms may differ.`
|
|
15200
|
+
),
|
|
15017
15201
|
{
|
|
15018
|
-
flag: "
|
|
15019
|
-
field: "
|
|
15202
|
+
flag: "pricing",
|
|
15203
|
+
field: "pricing",
|
|
15020
15204
|
type: "string",
|
|
15021
|
-
|
|
15022
|
-
placeholder: BOARD_CODES.join("|")
|
|
15023
|
-
},
|
|
15024
|
-
{ flag: "free-cancellation", field: "freeCancellation", type: "boolean" },
|
|
15025
|
-
{
|
|
15026
|
-
flag: "include-unbookable",
|
|
15027
|
-
field: "bookableOnly",
|
|
15028
|
-
type: "boolean",
|
|
15029
|
-
description: "Also keep rates that are not bookable on every night (by default only bookable rates are returned)."
|
|
15205
|
+
placeholder: "indicative|none"
|
|
15030
15206
|
},
|
|
15207
|
+
{ flag: "stars-min", field: "filters.stars.min", type: "number", placeholder: COUNT },
|
|
15208
|
+
{ flag: "stars-max", field: "filters.stars.max", type: "number", placeholder: COUNT },
|
|
15031
15209
|
{
|
|
15032
|
-
flag: "
|
|
15033
|
-
field: "
|
|
15034
|
-
type: "
|
|
15035
|
-
|
|
15036
|
-
|
|
15210
|
+
flag: "amenity",
|
|
15211
|
+
field: "filters.amenities",
|
|
15212
|
+
type: "string",
|
|
15213
|
+
multiple: true,
|
|
15214
|
+
placeholder: FILTERABLE_AMENITIES.join("|"),
|
|
15215
|
+
description: `An amenity every hotel must list; repeat for each, up to ${LIMITS.amenityFilters}.`
|
|
15037
15216
|
},
|
|
15038
15217
|
{
|
|
15039
|
-
flag: "
|
|
15040
|
-
field: "
|
|
15218
|
+
flag: "price-max-per-room-night",
|
|
15219
|
+
field: "filters.price.maxPerRoomNight",
|
|
15041
15220
|
type: "string",
|
|
15042
|
-
placeholder:
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
var bookFlags = flagsOf(createBookingInput, [
|
|
15046
|
-
{ flag: "hotel", field: "hotelId", type: "id", placeholder: ID },
|
|
15221
|
+
placeholder: "<USD decimal>",
|
|
15222
|
+
description: "Highest indicative price per room per night, in USD, e.g. `150` or `149.50`. Needs a stay."
|
|
15223
|
+
},
|
|
15047
15224
|
{
|
|
15048
|
-
flag: "
|
|
15049
|
-
field: "
|
|
15225
|
+
flag: "sort",
|
|
15226
|
+
field: "sort",
|
|
15050
15227
|
type: "string",
|
|
15051
|
-
placeholder:
|
|
15052
|
-
description: "`roomId` from `hotels rates`"
|
|
15228
|
+
placeholder: SEARCH_SORTS.join("|")
|
|
15053
15229
|
},
|
|
15230
|
+
{ flag: "limit", field: "limit", type: "number", placeholder: COUNT },
|
|
15054
15231
|
{
|
|
15055
|
-
flag: "
|
|
15056
|
-
field: "
|
|
15232
|
+
flag: "cursor",
|
|
15233
|
+
field: "cursor",
|
|
15057
15234
|
type: "string",
|
|
15058
|
-
placeholder: "<
|
|
15059
|
-
description: "`
|
|
15235
|
+
placeholder: "<cursor>",
|
|
15236
|
+
description: "`nextCursor` from the previous page. A next page takes only `--cursor` and `--limit`."
|
|
15060
15237
|
},
|
|
15061
|
-
|
|
15062
|
-
|
|
15238
|
+
localeFlag,
|
|
15239
|
+
displayCurrencyFlag
|
|
15240
|
+
]);
|
|
15241
|
+
var hotelFlags = flagsOf(getHotelInput, [
|
|
15063
15242
|
{
|
|
15064
15243
|
flag: "rooms",
|
|
15065
|
-
field: "
|
|
15066
|
-
type: "
|
|
15067
|
-
|
|
15068
|
-
description: "
|
|
15069
|
-
},
|
|
15070
|
-
{
|
|
15071
|
-
flag: "adults",
|
|
15072
|
-
field: "adults",
|
|
15073
|
-
type: "number",
|
|
15074
|
-
placeholder: COUNT,
|
|
15075
|
-
description: "Adults per room, not in total, as in `hotels rates` (2 rooms for 4 adults = `--rooms 2 --adults 2`)"
|
|
15076
|
-
},
|
|
15077
|
-
{
|
|
15078
|
-
flag: "child-age",
|
|
15079
|
-
field: "childrenAges",
|
|
15080
|
-
type: "number",
|
|
15081
|
-
multiple: true,
|
|
15082
|
-
placeholder: "<age>",
|
|
15083
|
-
description: "Age of each child in one room (0-17), per room, as in `hotels rates`"
|
|
15244
|
+
field: "include",
|
|
15245
|
+
type: "boolean",
|
|
15246
|
+
value: "rooms",
|
|
15247
|
+
description: "Add the hotel's rooms."
|
|
15084
15248
|
},
|
|
15249
|
+
localeFlag
|
|
15250
|
+
]);
|
|
15251
|
+
var offersFlags = flagsOf(hotelOffersInput, [
|
|
15085
15252
|
{
|
|
15086
|
-
flag: "
|
|
15087
|
-
field: "
|
|
15253
|
+
flag: "hotel",
|
|
15254
|
+
field: "hotelIds",
|
|
15088
15255
|
type: "string",
|
|
15089
15256
|
multiple: true,
|
|
15090
|
-
placeholder:
|
|
15091
|
-
description:
|
|
15257
|
+
placeholder: "<htl_>",
|
|
15258
|
+
description: `A hotel to price, by hotelId from \`hotels search\`; repeat for each, up to ${LIMITS.offersHotels}.`
|
|
15092
15259
|
},
|
|
15260
|
+
...stayFlags(
|
|
15261
|
+
`A room, ${ROOM_FORMAT}. Repeat for each room, up to ${LIMITS.rooms}; every room must be the same.`
|
|
15262
|
+
),
|
|
15263
|
+
{ flag: "require-breakfast", field: "filters.requireBreakfast", type: "boolean" },
|
|
15093
15264
|
{
|
|
15094
|
-
flag: "
|
|
15095
|
-
field: "
|
|
15096
|
-
type: "
|
|
15097
|
-
placeholder: "<name>"
|
|
15265
|
+
flag: "require-free-cancellation",
|
|
15266
|
+
field: "filters.requireFreeCancellation",
|
|
15267
|
+
type: "boolean"
|
|
15098
15268
|
},
|
|
15269
|
+
{ flag: "limit-per-hotel", field: "limitPerHotel", type: "number", placeholder: COUNT },
|
|
15270
|
+
localeFlag,
|
|
15271
|
+
displayCurrencyFlag
|
|
15272
|
+
]);
|
|
15273
|
+
var bookFlags = flagsOf(createOrderInput, [
|
|
15099
15274
|
{
|
|
15100
|
-
flag: "
|
|
15101
|
-
field: "
|
|
15275
|
+
flag: "offer",
|
|
15276
|
+
field: "items.offerId",
|
|
15102
15277
|
type: "string",
|
|
15103
|
-
placeholder: "<
|
|
15278
|
+
placeholder: "<off_>",
|
|
15279
|
+
description: "The offer to order, by offerId from `hotels offers`."
|
|
15104
15280
|
},
|
|
15105
15281
|
{
|
|
15106
|
-
flag: "
|
|
15107
|
-
field: "
|
|
15282
|
+
flag: "guest",
|
|
15283
|
+
field: "items.hotel.rooms.guests",
|
|
15108
15284
|
type: "string",
|
|
15109
|
-
|
|
15285
|
+
multiple: true,
|
|
15286
|
+
placeholder: "<room>:<first>:<last>",
|
|
15287
|
+
description: "A guest, as `room:first:last`, e.g. `1:John:Smith` for room 1. Repeat for each guest: at least one per room of the offer, no more than the room sleeps. Names in Latin letters as on the passport; transliterate accents (M\xFCller \u2192 Mueller)."
|
|
15110
15288
|
},
|
|
15289
|
+
{ flag: "first-name", field: "contact.firstName", type: "string", placeholder: "<name>" },
|
|
15290
|
+
{ flag: "last-name", field: "contact.lastName", type: "string", placeholder: "<name>" },
|
|
15291
|
+
{ flag: "email", field: "contact.email", type: "string", placeholder: "<email>" },
|
|
15292
|
+
{ flag: "phone", field: "contact.phone", type: "string", placeholder: "<phone>" },
|
|
15111
15293
|
{
|
|
15112
15294
|
flag: "special-requests",
|
|
15113
|
-
field: "specialRequests",
|
|
15295
|
+
field: "items.hotel.specialRequests",
|
|
15114
15296
|
type: "string",
|
|
15115
15297
|
placeholder: "<english text>"
|
|
15116
15298
|
}
|
|
15117
15299
|
]);
|
|
15300
|
+
var orderFlags = flagsOf(getOrderInput, [localeFlag]);
|
|
15301
|
+
var ordersListFlags = flagsOf(listOrdersInput, [
|
|
15302
|
+
{ flag: "cursor", field: "cursor", type: "string", placeholder: "<cursor>" },
|
|
15303
|
+
{ flag: "limit", field: "limit", type: "number", placeholder: COUNT },
|
|
15304
|
+
localeFlag
|
|
15305
|
+
]);
|
|
15118
15306
|
var BODY_OPTION = {
|
|
15119
15307
|
flag: "body",
|
|
15120
15308
|
type: "string",
|
|
@@ -15122,13 +15310,14 @@ var BODY_OPTION = {
|
|
|
15122
15310
|
description: "The raw request body as a JSON object, or `@file.json` to read it from a file. Flags override its fields."
|
|
15123
15311
|
};
|
|
15124
15312
|
var COMMAND_GROUPS = [
|
|
15125
|
-
{ name: "hotels", summary: "Search,
|
|
15313
|
+
{ name: "hotels", summary: "Search hotels, get offers and order one" },
|
|
15314
|
+
{ name: "orders", summary: "Your orders and their status" }
|
|
15126
15315
|
];
|
|
15127
15316
|
var COMMANDS = [
|
|
15128
15317
|
{
|
|
15129
15318
|
group: "hotels",
|
|
15130
15319
|
name: "search",
|
|
15131
|
-
summary: "Find hotels with indicative prices",
|
|
15320
|
+
summary: "Find hotels in a place, near a point or by name, with indicative prices",
|
|
15132
15321
|
tool: "search_hotels",
|
|
15133
15322
|
flags: searchFlags,
|
|
15134
15323
|
options: [BODY_OPTION]
|
|
@@ -15136,27 +15325,42 @@ var COMMANDS = [
|
|
|
15136
15325
|
{
|
|
15137
15326
|
group: "hotels",
|
|
15138
15327
|
name: "get",
|
|
15139
|
-
summary: "
|
|
15328
|
+
summary: "Hotel details: description, policies, amenities and rooms",
|
|
15140
15329
|
tool: "get_hotel",
|
|
15141
15330
|
flags: hotelFlags,
|
|
15142
|
-
positionals: ["hotelId"]
|
|
15331
|
+
positionals: [positional(getHotelInput, "hotelId")]
|
|
15143
15332
|
},
|
|
15144
15333
|
{
|
|
15145
15334
|
group: "hotels",
|
|
15146
|
-
name: "
|
|
15147
|
-
summary: `Live
|
|
15148
|
-
tool: "
|
|
15149
|
-
flags:
|
|
15335
|
+
name: "offers",
|
|
15336
|
+
summary: `Live bookable offers for up to ${LIMITS.offersHotels} hotels`,
|
|
15337
|
+
tool: "get_hotel_offers",
|
|
15338
|
+
flags: offersFlags,
|
|
15150
15339
|
options: [BODY_OPTION]
|
|
15151
15340
|
},
|
|
15152
15341
|
{
|
|
15153
15342
|
group: "hotels",
|
|
15154
15343
|
name: "book",
|
|
15155
|
-
summary: "
|
|
15344
|
+
summary: "Order an offer: an unpaid order with a payment link",
|
|
15156
15345
|
tool: "book_hotel",
|
|
15157
15346
|
flags: bookFlags,
|
|
15158
15347
|
options: [BODY_OPTION]
|
|
15159
15348
|
},
|
|
15349
|
+
{
|
|
15350
|
+
group: "orders",
|
|
15351
|
+
name: "get",
|
|
15352
|
+
summary: "Show an order and its status",
|
|
15353
|
+
tool: "get_order",
|
|
15354
|
+
flags: orderFlags,
|
|
15355
|
+
positionals: [positional(getOrderInput, "orderId")]
|
|
15356
|
+
},
|
|
15357
|
+
{
|
|
15358
|
+
group: "orders",
|
|
15359
|
+
name: "list",
|
|
15360
|
+
summary: "List your orders, newest first",
|
|
15361
|
+
tool: "list_orders",
|
|
15362
|
+
flags: ordersListFlags
|
|
15363
|
+
},
|
|
15160
15364
|
{
|
|
15161
15365
|
group: null,
|
|
15162
15366
|
name: "login",
|
|
@@ -15191,7 +15395,14 @@ var COMMANDS = [
|
|
|
15191
15395
|
summary: "Print the guide for AI agents (Markdown)",
|
|
15192
15396
|
tool: "get_planning_guide",
|
|
15193
15397
|
flags: [],
|
|
15194
|
-
positionals: [
|
|
15398
|
+
positionals: [
|
|
15399
|
+
{
|
|
15400
|
+
name: "topic",
|
|
15401
|
+
field: null,
|
|
15402
|
+
optional: true,
|
|
15403
|
+
description: "`planning` for the planning guide; left out, the main guide."
|
|
15404
|
+
}
|
|
15405
|
+
]
|
|
15195
15406
|
},
|
|
15196
15407
|
{
|
|
15197
15408
|
group: null,
|
|
@@ -15216,22 +15427,27 @@ var GLOBAL_FLAGS = [
|
|
|
15216
15427
|
short: "v",
|
|
15217
15428
|
type: "boolean",
|
|
15218
15429
|
description: "Print the CLI version."
|
|
15430
|
+
},
|
|
15431
|
+
{
|
|
15432
|
+
flag: "json",
|
|
15433
|
+
type: "boolean",
|
|
15434
|
+
description: "Print the answer as its JSON on one line instead of text; an error prints its JSON envelope, on stderr."
|
|
15219
15435
|
}
|
|
15220
15436
|
];
|
|
15221
15437
|
|
|
15222
15438
|
// ../contract/src/guide-data.generated.ts
|
|
15223
15439
|
var FRAGMENTS = {
|
|
15224
|
-
"booking":
|
|
15225
|
-
"errors": "{{#if claude-skill cli}}\n## Errors\n\n{{#if claude-skill}}A tool error{{/if}}{{#if cli}}An error on stderr{{/if}} carries `
|
|
15226
|
-
"meals":
|
|
15227
|
-
"occupancy": "{{#if claude-skill cli}}\n##
|
|
15228
|
-
"order": '{{#if claude-skill cli}}\n## Workflow\n\n1. **Search** with `{{tool:hotels search}}` for candidate hotels
|
|
15229
|
-
"payment-link": "{{#if claude-skill cli}}\n## Before giving the payment link\n\n`{{tool:hotels book}}`
|
|
15230
|
-
"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,
|
|
15231
|
-
"prices": "{{#if claude-skill cli}}\n## Prices and currency\n\n
|
|
15232
|
-
"scope": "{{#if claude-skill cli}}\n# Fortrip hotel stays\n\n{{/if}}\nFortrip: hotel search, live
|
|
15233
|
-
"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
|
|
15234
|
-
"support": "{{#if claude-skill cli}}\n## Changes and support\n\n{{/if}}\n{{#if mcp claude-skill}}These tools{{/if}}{{#if cli}}The CLI{{/if}} can't change or cancel
|
|
15440
|
+
"booking": "{{#if claude-skill cli}}\n## Ordering\n\n{{/if}}\n- Call `{{tool:hotels book}}` only for the offer the user picked, with at least one guest per room{{#if claude-skill cli}}, named as the user gave them{{/if}}; never invent guests.{{#if claude-skill cli}} Room 1 is the offer's first room, and a room takes no more guests than it sleeps.{{/if}}\n- Guest names: Latin letters as on the passport (M\xFCller \u2192 Mueller), first and last name 1\u2013{{limit:guestName}} characters each{{#if cli}}, as `--guest 1:John:Smith` (room 1, first name, last name){{/if}}.\n{{#if claude-skill cli}}\n- The contact's first and last name (up to {{limit:contactFirstName}} characters each), email and phone (with its country code) are all required. Special requests must be in English and aren't guaranteed.\n{{/if}}\n- `{{tool:hotels book}}` can take up to about two minutes{{#if cli}}: allow the command at least 3 minutes{{/if}}. If it times out, {{#if cli}}run it again with identical flags{{/if}}{{#if mcp claude-skill}}call it again with the same details{{/if}}: it returns the same order.\n{{#if mcp}}\n- On `offer_changed`, show the user every change and ask again before ordering the new offer.\n{{/if}}\n{{#if cli}}\n- `{{tool:hotels book}}` takes only these fields: a `--body` with any other field (such as `nationality`) is rejected with exit 2.\n{{/if}}\n",
|
|
15441
|
+
"errors": "{{#if claude-skill cli}}\n## Errors\n\n{{#if claude-skill}}A tool error{{/if}}{{#if cli}}An error on stderr{{/if}} carries `code`, `message` and `retryable`, and `retryAfterSeconds` when waiting helps; many carry `details`. Search errors are under Search. Ordering errors:\n\n| `code` | Do this |\n|---|---|\n| `offer_changed` (409) | The price or terms changed since the offer: `changes` lists every change (`worse` says which are worse for the user) and `offer` is the new offer. Show the user every change and ask again; if they still want it, order the new offer, and confirm the new order before giving its link |\n| `offer_expired` (409) | Get offers again and let the user choose again |\n| `offer_unavailable` (422) | The offer is sold out or was refused: get offers again and let the user choose again |\n| `order_in_progress` (409) | The same order is still being created: wait `retryAfterSeconds`, then {{#if claude-skill}}call again with the same details{{/if}}{{#if cli}}run the command again with identical flags{{/if}} |\n| `offer_already_ordered` (409) | This offer was already ordered with other details: show that order (`orderId`) instead. An offer is ordered once |\n| `order_exists` (409) | This stay already has an order (`orderId`, `status`): show it instead of ordering again. This also blocks ordering the same offer, dates and rooms again for another party, ordering a stay again after it was cancelled, ordering while the user's own website checkout for the stay is open (up to 3 hours 45 minutes) and ordering again a stay whose earlier order failed at the hotel. For those, the user emails {{support}} |\n| `order_attempts_exhausted` (409) | This order can't be tried again: the user emails {{support}} with its `reference` |\n| `order_removed` (410) | The order was never completed and no payment is held: get offers again and order the new offer (the order keeps its `orderId`) |\n| `stay_unsupported` (422) | `mixed_occupancy`: order once per party; `max_nights`: at most `max` nights per order |\n| `stay_in_past` (422) | Check-in has passed in the hotel's time zone: ask for new dates |\n| `ordering_paused` (503) | Ordering is paused on Fortrip's side: tell the user, and try again only after `retryAfterSeconds` (10 minutes or more) |\n\nAny code:\n\n- `invalid_request` (400): `details.issues` lists each bad field; fix them. For amenities, `details.filterable` lists those that can filter.\n- `cursor_invalid` (400): the page is gone; search again from the first page.\n- `not_found` (404): no such hotel, or no such order for this user.\n- `rate_limited` (429): wait `retryAfterSeconds`, then retry once.\n- `service_unavailable` (503) and `supplier_unavailable` (503 or 504): wait `retryAfterSeconds`, then retry once with the same details; if it fails again, tell the user.\n- Any other code: read `message`. If `retryable` is true, wait `retryAfterSeconds` and retry once; otherwise tell the user instead of guessing.\n{{/if}}\n",
|
|
15442
|
+
"meals": '{{#if claude-skill cli}}\n## Presenting an offer\n\nGive the user, for each offer:\n\n- the room (`room.name`, beds, size) and the total, as above;\n- the meals: `meals.included` (breakfast, lunch, dinner) or `allInclusive`, with `guestsCoveredPerRoom`. Fewer than the guests per room means not everyone gets the meals (breakfast for 1 with 2 adults in the room): say who is covered. `unspecifiedMainMeals` counts main meals the hotel didn\'t name. With `known: false`, the meals are unknown: say so, and don\'t call it room only;\n- cancellation: `status` (`free`, `partial_penalty`, `non_refundable` or `unknown`), `freeCancellationEndsAt` (null with `free`: free until check-in) and the `penalties`. Times are in UTC: also give them in the hotel\'s `timeZone`;\n- confirmation: `mode` `instant` or `on_request` (within `withinMinutes`);\n- fees payable at the property (`payAtProperty.items`), in the property\'s currency. `disclosure: "incomplete"` means there can be fees not listed: say so.\n\nAn offer expires at `expiresAt`, about 30 minutes after it was made; after that, get offers again.\n\n"Not listed" is not "no": `listed: false` (amenities, policies, rooms), `null` and `unknown` mean the source didn\'t say. Tell the user it\'s unknown instead of turning it into "no".\n{{/if}}\n{{#if mcp}}\n- "Not listed" or "unknown" is not "no".\n{{/if}}\n',
|
|
15443
|
+
"occupancy": "{{#if claude-skill cli}}\n## Rooms and guests\n\n- {{#if mcp claude-skill}}`stay.rooms` lists each room's party as `{ adults, childrenAges }`: two rooms of 2 adults, one with a child of 6, is `[{ adults: 2, childrenAges: [6] }, { adults: 2, childrenAges: [] }]`{{/if}}{{#if cli}}`--room <adults[:age,\u2026]>`, once per room, gives each room's party: two rooms of 2 adults, one with a child of 6, is `--room 2:6 --room 2`{{/if}}.\n- Ask each child's age; offers need it.\n- If the party doesn't split evenly across the rooms (3 adults in 2 rooms), ask the user how it splits.\n{{/if}}\n- Offers and orders take one party per call, every room the same: for different parties, order once per party.{{#if claude-skill cli}} `{{tool:hotels offers}}` answers `stay_unsupported` (`mixed_occupancy`) for mixed rooms; `{{tool:hotels search}}` accepts them but prices only from the dates, with the note `party_split_required`.{{/if}}\n{{#if claude-skill cli}}\n- Limits: 1\u2013{{limit:rooms}} rooms, 1\u2013{{limit:adultsPerRoom}} adults and 0\u2013{{limit:childrenPerRoom}} children (aged 0\u2013{{limit:childAgeMax}}) per room, 1\u2013{{limit:nightsSupplier}} nights, up to {{limit:offersHotels}} hotels per `{{tool:hotels offers}}` call. A longer stay answers `stay_unsupported` (`max_nights`): ask the user whether to split it into consecutive stays, each its own order.\n{{/if}}\n",
|
|
15444
|
+
"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',
|
|
15445
|
+
"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",
|
|
15446
|
+
"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",
|
|
15447
|
+
"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",
|
|
15448
|
+
"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",
|
|
15449
|
+
"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",
|
|
15450
|
+
"support": "{{#if claude-skill cli}}\n## Changes and support\n\n{{/if}}\n{{#if mcp claude-skill}}These tools{{/if}}{{#if cli}}The CLI{{/if}} can't change or cancel an order: for changes, cancellations, refunds or other problems, the user emails {{support}} with the order's `reference` (never `payment.url`).\n"
|
|
15235
15451
|
};
|
|
15236
15452
|
|
|
15237
15453
|
// ../contract/src/guide.ts
|
|
@@ -15299,13 +15515,13 @@ function select(text2, surface) {
|
|
|
15299
15515
|
}
|
|
15300
15516
|
continue;
|
|
15301
15517
|
}
|
|
15302
|
-
const
|
|
15303
|
-
for (const name of
|
|
15518
|
+
const names2 = tag[1].trim().split(/\s+/);
|
|
15519
|
+
for (const name of names2) {
|
|
15304
15520
|
if (!SURFACES.includes(name)) {
|
|
15305
15521
|
throw new Error(`Unknown guide surface "${name}" in ${tag[0]}`);
|
|
15306
15522
|
}
|
|
15307
15523
|
}
|
|
15308
|
-
open.push(
|
|
15524
|
+
open.push(names2.includes(surface));
|
|
15309
15525
|
}
|
|
15310
15526
|
if (open.length > 0) throw new Error("Guide has {{#if}} without {{/if}}");
|
|
15311
15527
|
return out + tagged.slice(from);
|
|
@@ -15313,18 +15529,18 @@ function select(text2, surface) {
|
|
|
15313
15529
|
function resolve(token, surface, shown) {
|
|
15314
15530
|
const body = token.slice(2, -2);
|
|
15315
15531
|
if (body === "support") return SUPPORT_EMAIL;
|
|
15316
|
-
const [
|
|
15532
|
+
const [kind2, name, ...rest] = body.split(":");
|
|
15317
15533
|
if (name !== void 0 && rest.length === 0) {
|
|
15318
|
-
if (
|
|
15534
|
+
if (kind2 === "limit" && Object.hasOwn(LIMITS, name)) {
|
|
15319
15535
|
return String(LIMITS[name]);
|
|
15320
15536
|
}
|
|
15321
|
-
if (
|
|
15537
|
+
if (kind2 === "tool") {
|
|
15322
15538
|
const spec = COMMANDS.find(
|
|
15323
15539
|
(command) => commandInvocation(command) === `fortrip ${name}`
|
|
15324
15540
|
);
|
|
15325
15541
|
if (spec) return shown ? toolName(spec, surface, token) : token;
|
|
15326
15542
|
}
|
|
15327
|
-
if (
|
|
15543
|
+
if (kind2 === "arg") {
|
|
15328
15544
|
const flags = new Set(
|
|
15329
15545
|
COMMANDS.flatMap((command) => command.flags).filter((flag3) => flag3.field === name).map((flag3) => flag3.flag)
|
|
15330
15546
|
);
|
|
@@ -15344,157 +15560,1462 @@ function toolName(spec, surface, token) {
|
|
|
15344
15560
|
return surface === "mcp" ? spec.tool : `fortrip:${spec.tool}`;
|
|
15345
15561
|
}
|
|
15346
15562
|
|
|
15347
|
-
// ../contract/src/
|
|
15348
|
-
var
|
|
15349
|
-
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15356
|
-
|
|
15563
|
+
// ../contract/src/conventions/open.ts
|
|
15564
|
+
var OPEN = /* @__PURE__ */ Symbol("fortrip.open");
|
|
15565
|
+
function mark(schema, kind2) {
|
|
15566
|
+
schema._zod.def[OPEN] = kind2;
|
|
15567
|
+
}
|
|
15568
|
+
function openObject(shape) {
|
|
15569
|
+
return external_exports.looseObject(shape);
|
|
15570
|
+
}
|
|
15571
|
+
function openEnum(values) {
|
|
15572
|
+
const schema = external_exports.union([
|
|
15573
|
+
external_exports.enum(values),
|
|
15574
|
+
external_exports.string()
|
|
15575
|
+
]);
|
|
15576
|
+
mark(schema, "enum");
|
|
15577
|
+
return schema;
|
|
15578
|
+
}
|
|
15579
|
+
function openUnion(discriminator, branches) {
|
|
15580
|
+
const known = /* @__PURE__ */ new Set();
|
|
15581
|
+
for (const branch of branches) {
|
|
15582
|
+
const field = branch.shape[discriminator];
|
|
15583
|
+
const values = field?._zod.values;
|
|
15584
|
+
if (!values) {
|
|
15585
|
+
throw new Error(`openUnion: branch has no literal "${discriminator}"`);
|
|
15586
|
+
}
|
|
15587
|
+
for (const v of values) known.add(v);
|
|
15357
15588
|
}
|
|
15358
|
-
|
|
15589
|
+
const fallback = external_exports.looseObject({
|
|
15590
|
+
[discriminator]: external_exports.string().refine((v) => !known.has(v), { error: "malformed value for a known type" })
|
|
15591
|
+
});
|
|
15592
|
+
const schema = external_exports.union([...branches, fallback]);
|
|
15593
|
+
mark(schema, "union");
|
|
15594
|
+
return schema;
|
|
15595
|
+
}
|
|
15596
|
+
function parseOpen(schema, value) {
|
|
15597
|
+
return schema.parse(value);
|
|
15598
|
+
}
|
|
15359
15599
|
|
|
15360
|
-
// ../contract/src/outputs.ts
|
|
15361
|
-
var
|
|
15362
|
-
|
|
15363
|
-
|
|
15364
|
-
|
|
15600
|
+
// ../contract/src/outputs/common.ts
|
|
15601
|
+
var names = openObject({
|
|
15602
|
+
en: external_exports.string().nullable(),
|
|
15603
|
+
zh: external_exports.string().nullable()
|
|
15604
|
+
});
|
|
15605
|
+
var latLng = openObject({ lat: external_exports.number(), lng: external_exports.number() });
|
|
15606
|
+
var LOCALES2 = ["en", "zh"];
|
|
15607
|
+
var knownLocaleText = openObject({
|
|
15608
|
+
text: external_exports.string(),
|
|
15609
|
+
locale: openEnum(LOCALES2)
|
|
15610
|
+
});
|
|
15611
|
+
var localizedText = openObject({
|
|
15612
|
+
text: external_exports.string(),
|
|
15613
|
+
locale: openEnum(LOCALES2).nullable()
|
|
15614
|
+
});
|
|
15615
|
+
var stayEcho = openObject({
|
|
15616
|
+
checkIn: localDate,
|
|
15617
|
+
checkOut: localDate,
|
|
15618
|
+
rooms: external_exports.array(
|
|
15619
|
+
openObject({
|
|
15620
|
+
adults: external_exports.number().int(),
|
|
15621
|
+
childrenAges: external_exports.array(external_exports.number().int())
|
|
15622
|
+
})
|
|
15623
|
+
).min(1)
|
|
15624
|
+
});
|
|
15625
|
+
|
|
15626
|
+
// ../contract/src/outputs/place.ts
|
|
15627
|
+
var PLACE_KINDS = [
|
|
15628
|
+
"country",
|
|
15629
|
+
"region",
|
|
15630
|
+
"district",
|
|
15631
|
+
"settlement",
|
|
15632
|
+
"neighbourhood",
|
|
15633
|
+
"island"
|
|
15634
|
+
];
|
|
15635
|
+
var placeRef = openObject({
|
|
15636
|
+
placeId,
|
|
15637
|
+
name: external_exports.string(),
|
|
15638
|
+
names
|
|
15639
|
+
});
|
|
15640
|
+
var place = openObject({
|
|
15641
|
+
placeId,
|
|
15642
|
+
name: external_exports.string(),
|
|
15643
|
+
names,
|
|
15644
|
+
kind: openEnum(PLACE_KINDS),
|
|
15645
|
+
scope: openEnum(["area", "radius"]),
|
|
15646
|
+
cityLike: external_exports.boolean().nullable(),
|
|
15647
|
+
population: external_exports.number().int().nullable(),
|
|
15648
|
+
region: placeRef.nullable(),
|
|
15649
|
+
countryCode: external_exports.string(),
|
|
15650
|
+
location: latLng
|
|
15651
|
+
});
|
|
15652
|
+
var CENTER_BASES = [
|
|
15653
|
+
"seat",
|
|
15654
|
+
"capital",
|
|
15655
|
+
"busiest_settlement",
|
|
15656
|
+
"hub",
|
|
15657
|
+
"point"
|
|
15658
|
+
];
|
|
15659
|
+
var center = openObject({
|
|
15660
|
+
location: latLng,
|
|
15661
|
+
place: placeRef.nullable(),
|
|
15662
|
+
basis: openEnum(CENTER_BASES)
|
|
15663
|
+
});
|
|
15664
|
+
|
|
15665
|
+
// ../contract/src/outputs/search.ts
|
|
15666
|
+
var SEARCH_NOTE_CODES = [
|
|
15667
|
+
"indicative_prices",
|
|
15668
|
+
"prices_unavailable",
|
|
15669
|
+
"pool_limited",
|
|
15670
|
+
"amenities_listed_only",
|
|
15671
|
+
"place_resolved_dominant",
|
|
15672
|
+
"area_absorbed",
|
|
15673
|
+
"broad_area",
|
|
15674
|
+
"island_radius",
|
|
15675
|
+
"area_retired",
|
|
15676
|
+
"more_outside_radius",
|
|
15677
|
+
"more_elsewhere",
|
|
15678
|
+
"few_hotels_in_scope",
|
|
15679
|
+
"party_split_required",
|
|
15680
|
+
"no_results",
|
|
15681
|
+
"locality_available"
|
|
15682
|
+
];
|
|
15683
|
+
var indicativePrice = openObject({
|
|
15684
|
+
kind: openEnum(["indicative"]),
|
|
15685
|
+
basis: openEnum(["lowest_unit"]),
|
|
15686
|
+
fromPerRoomNight: money,
|
|
15687
|
+
fromPerRoomStay: money,
|
|
15688
|
+
approx: openObject({
|
|
15689
|
+
fromPerRoomNight: money,
|
|
15690
|
+
fromPerRoomStay: money,
|
|
15691
|
+
rateDate: localDate
|
|
15692
|
+
}).nullable()
|
|
15693
|
+
});
|
|
15694
|
+
var hotelSummary = openObject({
|
|
15695
|
+
hotelId,
|
|
15696
|
+
name: external_exports.string(),
|
|
15697
|
+
names,
|
|
15698
|
+
stars: external_exports.number().nullable(),
|
|
15699
|
+
address: openObject({
|
|
15700
|
+
line: knownLocaleText.nullable(),
|
|
15701
|
+
countryCode: external_exports.string().nullable(),
|
|
15702
|
+
region: placeRef.nullable()
|
|
15703
|
+
}),
|
|
15704
|
+
location: latLng.nullable(),
|
|
15705
|
+
timeZone: external_exports.string().nullable(),
|
|
15706
|
+
distanceMeters: external_exports.number().nullable(),
|
|
15707
|
+
price: indicativePrice.nullable()
|
|
15708
|
+
});
|
|
15709
|
+
var localities = openObject({
|
|
15710
|
+
items: external_exports.array(
|
|
15711
|
+
openObject({
|
|
15712
|
+
place: placeRef,
|
|
15713
|
+
count: external_exports.number().int(),
|
|
15714
|
+
total: external_exports.number().int(),
|
|
15715
|
+
includes: external_exports.array(
|
|
15716
|
+
openObject({ place: placeRef, members: external_exports.number().int() })
|
|
15717
|
+
)
|
|
15718
|
+
})
|
|
15365
15719
|
),
|
|
15366
|
-
|
|
15367
|
-
|
|
15368
|
-
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
|
|
15372
|
-
|
|
15373
|
-
|
|
15374
|
-
|
|
15375
|
-
|
|
15376
|
-
|
|
15377
|
-
|
|
15378
|
-
|
|
15379
|
-
|
|
15380
|
-
|
|
15381
|
-
|
|
15382
|
-
|
|
15383
|
-
|
|
15384
|
-
|
|
15385
|
-
|
|
15386
|
-
|
|
15387
|
-
|
|
15388
|
-
|
|
15389
|
-
|
|
15390
|
-
|
|
15391
|
-
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
15720
|
+
unassigned: external_exports.number().int(),
|
|
15721
|
+
more: external_exports.number().int()
|
|
15722
|
+
});
|
|
15723
|
+
var radiusSource = openEnum(["default", "requested"]);
|
|
15724
|
+
var destination2 = openUnion("kind", [
|
|
15725
|
+
openObject({
|
|
15726
|
+
kind: external_exports.literal("place"),
|
|
15727
|
+
place,
|
|
15728
|
+
resolution: openEnum([
|
|
15729
|
+
"only_match",
|
|
15730
|
+
"dominant",
|
|
15731
|
+
"by_id",
|
|
15732
|
+
"by_country_code",
|
|
15733
|
+
"by_locality"
|
|
15734
|
+
]),
|
|
15735
|
+
otherMatches: external_exports.number().int(),
|
|
15736
|
+
scope: openEnum(["area", "radius", "locality"]),
|
|
15737
|
+
center,
|
|
15738
|
+
radiusMeters: external_exports.number().nullable(),
|
|
15739
|
+
radiusSource: radiusSource.nullable()
|
|
15740
|
+
}),
|
|
15741
|
+
openObject({
|
|
15742
|
+
kind: external_exports.literal("point"),
|
|
15743
|
+
center,
|
|
15744
|
+
scope: openEnum(["radius"]),
|
|
15745
|
+
radiusMeters: external_exports.number(),
|
|
15746
|
+
radiusSource
|
|
15747
|
+
})
|
|
15748
|
+
]);
|
|
15749
|
+
var count = external_exports.number().int();
|
|
15750
|
+
var searchResult = openObject({
|
|
15751
|
+
destination: destination2.nullable(),
|
|
15752
|
+
text: openObject({
|
|
15753
|
+
value: external_exports.string(),
|
|
15754
|
+
match: openEnum(["exact"])
|
|
15755
|
+
}).nullable(),
|
|
15756
|
+
stay: stayEcho.nullable(),
|
|
15757
|
+
pricing: openObject({
|
|
15758
|
+
mode: openEnum(["indicative", "none"]),
|
|
15759
|
+
scope: openEnum(["page", "pool"]).nullable(),
|
|
15760
|
+
status: openEnum(["ok", "unavailable"]).nullable()
|
|
15761
|
+
}),
|
|
15762
|
+
sort: openEnum([
|
|
15763
|
+
"distance",
|
|
15764
|
+
"relevance",
|
|
15765
|
+
"stars_desc",
|
|
15766
|
+
"price_asc",
|
|
15767
|
+
"price_desc"
|
|
15768
|
+
]),
|
|
15769
|
+
coverage: openObject({
|
|
15770
|
+
matched: count,
|
|
15771
|
+
priced: count.nullable(),
|
|
15772
|
+
withPrice: count.nullable(),
|
|
15773
|
+
poolRadiusMeters: external_exports.number().nullable(),
|
|
15774
|
+
excludedNoPrice: count.nullable(),
|
|
15775
|
+
excludedNoFacilityData: count.nullable(),
|
|
15776
|
+
withinMaxRadius: count.nullable(),
|
|
15777
|
+
textAnywhere: count.nullable(),
|
|
15778
|
+
localities: localities.nullable(),
|
|
15779
|
+
nameAsText: count.nullable()
|
|
15780
|
+
}),
|
|
15781
|
+
data: external_exports.array(hotelSummary),
|
|
15782
|
+
nextCursor: external_exports.string().nullable(),
|
|
15783
|
+
notes: external_exports.array(
|
|
15784
|
+
openObject({ code: openEnum(SEARCH_NOTE_CODES), message: external_exports.string() })
|
|
15785
|
+
)
|
|
15786
|
+
});
|
|
15787
|
+
|
|
15788
|
+
// ../contract/src/outputs/hotel.ts
|
|
15789
|
+
var POLICY_TOPICS = [
|
|
15790
|
+
"check_in",
|
|
15791
|
+
"important_notices",
|
|
15792
|
+
"booking",
|
|
15793
|
+
"booking_tips",
|
|
15794
|
+
"check_in_out",
|
|
15795
|
+
"check_in_tips",
|
|
15796
|
+
"reception",
|
|
15797
|
+
"deposit",
|
|
15798
|
+
"meal",
|
|
15799
|
+
"extra_bed",
|
|
15800
|
+
"pets",
|
|
15801
|
+
"surroundings",
|
|
15802
|
+
"meetings",
|
|
15803
|
+
"non_smoking",
|
|
15804
|
+
"other"
|
|
15805
|
+
];
|
|
15806
|
+
var roomFacts = {
|
|
15807
|
+
beds: localizedText.nullable(),
|
|
15808
|
+
areaSqm: openObject({ min: external_exports.number(), max: external_exports.number() }).nullable(),
|
|
15809
|
+
maxOccupancy: external_exports.number().int().nullable(),
|
|
15810
|
+
window: openEnum(["yes", "no", "partial"]).nullable()
|
|
15811
|
+
};
|
|
15812
|
+
var timeRange = openObject({
|
|
15813
|
+
from: localTime.nullable(),
|
|
15814
|
+
until: localTime.nullable()
|
|
15815
|
+
});
|
|
15816
|
+
var hotel = openObject({
|
|
15817
|
+
hotelId,
|
|
15818
|
+
name: external_exports.string(),
|
|
15819
|
+
names,
|
|
15820
|
+
bookable: external_exports.boolean(),
|
|
15821
|
+
stars: external_exports.number().nullable(),
|
|
15822
|
+
address: hotelSummary.shape.address,
|
|
15823
|
+
location: latLng.nullable(),
|
|
15824
|
+
timeZone: external_exports.string().nullable(),
|
|
15825
|
+
description: knownLocaleText.nullable(),
|
|
15826
|
+
checkInTimes: timeRange,
|
|
15827
|
+
checkOutTimes: timeRange,
|
|
15828
|
+
policies: openObject({
|
|
15829
|
+
listed: external_exports.boolean(),
|
|
15830
|
+
items: external_exports.array(
|
|
15831
|
+
openObject({
|
|
15832
|
+
topic: openEnum(POLICY_TOPICS),
|
|
15833
|
+
text: external_exports.string(),
|
|
15834
|
+
locale: openEnum(["en", "zh"]).nullable()
|
|
15395
15835
|
})
|
|
15396
15836
|
)
|
|
15397
|
-
})
|
|
15837
|
+
}),
|
|
15838
|
+
amenities: openObject({
|
|
15839
|
+
listed: external_exports.boolean(),
|
|
15840
|
+
items: external_exports.array(
|
|
15841
|
+
openObject({ code: openEnum(AMENITY_CODES), name: knownLocaleText })
|
|
15842
|
+
)
|
|
15843
|
+
}),
|
|
15844
|
+
breakfastServed: external_exports.boolean().nullable(),
|
|
15845
|
+
deposit: openObject({
|
|
15846
|
+
required: external_exports.boolean().nullable(),
|
|
15847
|
+
amount: money.nullable()
|
|
15848
|
+
}),
|
|
15849
|
+
contact: openObject({
|
|
15850
|
+
phone: external_exports.string().nullable(),
|
|
15851
|
+
website: external_exports.string().nullable()
|
|
15852
|
+
}),
|
|
15853
|
+
rooms: openObject({
|
|
15854
|
+
listed: external_exports.boolean(),
|
|
15855
|
+
items: external_exports.array(openObject({ name: localizedText, ...roomFacts }))
|
|
15856
|
+
}).optional()
|
|
15398
15857
|
});
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
15402
|
-
|
|
15858
|
+
|
|
15859
|
+
// ../contract/src/outputs/offer.ts
|
|
15860
|
+
var meals = openObject({
|
|
15861
|
+
known: external_exports.boolean(),
|
|
15862
|
+
included: external_exports.array(openEnum(["breakfast", "lunch", "dinner"])).nullable(),
|
|
15863
|
+
unspecifiedMainMeals: external_exports.number().int().nullable(),
|
|
15864
|
+
allInclusive: external_exports.boolean().nullable(),
|
|
15865
|
+
guestsCoveredPerRoom: external_exports.number().int().nullable(),
|
|
15866
|
+
label: localizedText.nullable()
|
|
15403
15867
|
});
|
|
15404
|
-
var
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
|
|
15410
|
-
|
|
15868
|
+
var payAtProperty = openObject({
|
|
15869
|
+
disclosure: openEnum(["complete", "incomplete"]),
|
|
15870
|
+
items: external_exports.array(
|
|
15871
|
+
openObject({
|
|
15872
|
+
label: localizedText.nullable(),
|
|
15873
|
+
amount: money.nullable(),
|
|
15874
|
+
mandatory: external_exports.boolean(),
|
|
15875
|
+
basis: openEnum([
|
|
15876
|
+
"per_stay",
|
|
15877
|
+
"per_night",
|
|
15878
|
+
"per_person",
|
|
15879
|
+
"per_person_per_night",
|
|
15880
|
+
"unknown"
|
|
15881
|
+
]),
|
|
15882
|
+
collectedAt: openEnum(["property", "unknown"])
|
|
15883
|
+
})
|
|
15884
|
+
)
|
|
15411
15885
|
});
|
|
15412
|
-
|
|
15413
|
-
|
|
15414
|
-
|
|
15415
|
-
|
|
15416
|
-
|
|
15417
|
-
|
|
15886
|
+
var cancellation = openObject({
|
|
15887
|
+
status: openEnum(["free", "partial_penalty", "non_refundable", "unknown"]),
|
|
15888
|
+
asOf: instant,
|
|
15889
|
+
freeCancellationEndsAt: instant.nullable(),
|
|
15890
|
+
penalties: external_exports.array(
|
|
15891
|
+
openObject({ startsAt: instant.nullable(), amount: money })
|
|
15892
|
+
),
|
|
15893
|
+
timeZone: external_exports.string().nullable()
|
|
15894
|
+
});
|
|
15895
|
+
var confirmation = openObject({
|
|
15896
|
+
mode: openEnum(["instant", "on_request", "unknown"]),
|
|
15897
|
+
withinMinutes: external_exports.number().int().nullable()
|
|
15898
|
+
});
|
|
15899
|
+
var offer = openObject({
|
|
15900
|
+
offerId,
|
|
15901
|
+
type: openEnum(["hotel"]),
|
|
15902
|
+
expiresAt: instant,
|
|
15903
|
+
hotel: openObject({
|
|
15904
|
+
hotelId,
|
|
15905
|
+
room: openObject({ name: localizedText.nullable(), ...roomFacts }),
|
|
15906
|
+
roomCount: external_exports.number().int(),
|
|
15907
|
+
meals
|
|
15908
|
+
}),
|
|
15909
|
+
price: openObject({
|
|
15910
|
+
total: money,
|
|
15911
|
+
perNightAllRooms: money,
|
|
15912
|
+
approx: openObject({
|
|
15913
|
+
total: money,
|
|
15914
|
+
perNightAllRooms: money,
|
|
15915
|
+
rateDate: localDate
|
|
15916
|
+
}).nullable(),
|
|
15917
|
+
payAtProperty
|
|
15918
|
+
}),
|
|
15919
|
+
cancellation,
|
|
15920
|
+
confirmation
|
|
15921
|
+
});
|
|
15922
|
+
var omitted = openObject({
|
|
15923
|
+
byNationality: external_exports.number().int(),
|
|
15924
|
+
byFilters: external_exports.number().int(),
|
|
15925
|
+
dominated: external_exports.number().int(),
|
|
15926
|
+
unreadable: external_exports.number().int(),
|
|
15927
|
+
byLimit: external_exports.number().int()
|
|
15928
|
+
});
|
|
15929
|
+
var UNAVAILABLE_REASONS = [
|
|
15930
|
+
"sold_out",
|
|
15931
|
+
"no_matching_offers",
|
|
15932
|
+
"nationality_restricted",
|
|
15933
|
+
"offers_unreadable",
|
|
15934
|
+
"not_found",
|
|
15935
|
+
"supplier_unavailable",
|
|
15936
|
+
"stay_in_past"
|
|
15937
|
+
];
|
|
15938
|
+
var hotelOffersResult = openObject({
|
|
15939
|
+
stay: stayEcho,
|
|
15940
|
+
hotels: external_exports.array(
|
|
15941
|
+
openObject({
|
|
15942
|
+
hotel: openObject({ hotelId, name: external_exports.string(), names }),
|
|
15943
|
+
offers: external_exports.array(offer),
|
|
15944
|
+
omitted
|
|
15945
|
+
})
|
|
15946
|
+
),
|
|
15947
|
+
unavailable: external_exports.array(
|
|
15948
|
+
openObject({
|
|
15949
|
+
hotelId,
|
|
15950
|
+
reason: openEnum(UNAVAILABLE_REASONS),
|
|
15951
|
+
omitted: omitted.nullable()
|
|
15952
|
+
})
|
|
15418
15953
|
)
|
|
15419
15954
|
});
|
|
15420
|
-
|
|
15421
|
-
|
|
15955
|
+
|
|
15956
|
+
// ../contract/src/outputs/order.ts
|
|
15957
|
+
var ORDER_STATUSES = [
|
|
15958
|
+
"awaiting_payment",
|
|
15959
|
+
"expired",
|
|
15960
|
+
"confirming",
|
|
15961
|
+
"confirmed",
|
|
15962
|
+
"failed",
|
|
15963
|
+
"cancelling",
|
|
15964
|
+
"cancelled",
|
|
15965
|
+
"needs_attention"
|
|
15966
|
+
];
|
|
15967
|
+
var PAYMENT_STATUSES = [
|
|
15968
|
+
"unpaid",
|
|
15969
|
+
"authorized",
|
|
15970
|
+
"paid",
|
|
15971
|
+
"released",
|
|
15972
|
+
"refund_pending",
|
|
15973
|
+
"refunded"
|
|
15974
|
+
];
|
|
15975
|
+
var orderReference = external_exports.string().regex(/^FT-[2-9A-HJ-NP-Z]{6}$/);
|
|
15976
|
+
var hotelItem = openObject({
|
|
15977
|
+
type: external_exports.literal("hotel"),
|
|
15978
|
+
hotel: openObject({
|
|
15979
|
+
hotelId: hotelId.nullable(),
|
|
15980
|
+
name: external_exports.string(),
|
|
15981
|
+
names,
|
|
15982
|
+
checkIn: localDate,
|
|
15983
|
+
checkOut: localDate,
|
|
15984
|
+
timeZone: external_exports.string().nullable(),
|
|
15985
|
+
rooms: stayEcho.shape.rooms,
|
|
15986
|
+
room: openObject({
|
|
15987
|
+
name: localizedText.nullable(),
|
|
15988
|
+
beds: localizedText.nullable()
|
|
15989
|
+
}),
|
|
15990
|
+
meals,
|
|
15991
|
+
cancellation,
|
|
15992
|
+
payAtProperty,
|
|
15993
|
+
confirmationNumber: external_exports.string().nullable()
|
|
15994
|
+
})
|
|
15995
|
+
});
|
|
15996
|
+
var order = openObject({
|
|
15997
|
+
orderId,
|
|
15998
|
+
reference: orderReference,
|
|
15999
|
+
status: openEnum(ORDER_STATUSES),
|
|
16000
|
+
createdAt: instant,
|
|
16001
|
+
updatedAt: instant,
|
|
16002
|
+
items: external_exports.array(openUnion("type", [hotelItem])).min(1),
|
|
16003
|
+
price: openObject({ total: money }),
|
|
16004
|
+
payment: openObject({
|
|
16005
|
+
status: openEnum(PAYMENT_STATUSES),
|
|
16006
|
+
url: external_exports.string().nullable(),
|
|
16007
|
+
requiresSignIn: external_exports.boolean(),
|
|
16008
|
+
expiresAt: instant.nullable(),
|
|
16009
|
+
chargeCurrency: external_exports.string(),
|
|
16010
|
+
localCurrencyAtCheckout: external_exports.boolean(),
|
|
16011
|
+
localCharge: money.nullable()
|
|
16012
|
+
}),
|
|
16013
|
+
manageUrl: external_exports.string()
|
|
16014
|
+
});
|
|
16015
|
+
var orderList = openObject({
|
|
16016
|
+
data: external_exports.array(order),
|
|
16017
|
+
nextCursor: external_exports.string().nullable()
|
|
15422
16018
|
});
|
|
15423
|
-
|
|
16019
|
+
function change(field, value) {
|
|
16020
|
+
return openObject({
|
|
16021
|
+
field: external_exports.literal(field),
|
|
16022
|
+
previous: value,
|
|
16023
|
+
current: value,
|
|
16024
|
+
worse: external_exports.boolean()
|
|
16025
|
+
});
|
|
16026
|
+
}
|
|
16027
|
+
var offerChange = openUnion("field", [
|
|
16028
|
+
change("price", money),
|
|
16029
|
+
change("meals", meals),
|
|
16030
|
+
change("cancellation", cancellation),
|
|
16031
|
+
change("confirmation", confirmation),
|
|
16032
|
+
change("pay_at_property", payAtProperty)
|
|
16033
|
+
]);
|
|
16034
|
+
|
|
16035
|
+
// ../contract/src/tools.ts
|
|
16036
|
+
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.";
|
|
16037
|
+
var nextStep = external_exports.string().optional().describe(
|
|
16038
|
+
"What to show and ask before giving a payment.url. Present when the answer holds a non-null payment.url, and only then."
|
|
16039
|
+
);
|
|
16040
|
+
var MCP_OUTPUT = {
|
|
16041
|
+
book_hotel: order.extend({ nextStep }),
|
|
16042
|
+
get_order: order.extend({ nextStep }),
|
|
16043
|
+
list_orders: orderList.extend({ nextStep })
|
|
16044
|
+
};
|
|
16045
|
+
var READ = {
|
|
16046
|
+
readOnlyHint: true,
|
|
16047
|
+
idempotentHint: true,
|
|
16048
|
+
openWorldHint: true,
|
|
16049
|
+
destructiveHint: false
|
|
16050
|
+
};
|
|
16051
|
+
var READ_OWN = { ...READ, openWorldHint: false };
|
|
15424
16052
|
var TOOLS = [
|
|
15425
16053
|
{
|
|
15426
16054
|
name: "get_planning_guide",
|
|
15427
16055
|
title: "Get hotel planning guide",
|
|
15428
16056
|
description: `Returns Fortrip's planning guide for a request that is more than one hotel for fixed dates (flexible dates, a budget, split stays, several destinations, "find me the best"): what to ask the user, which results count as evidence, how to rank and how to present options.`,
|
|
15429
|
-
annotations:
|
|
15430
|
-
readOnlyHint: true,
|
|
15431
|
-
idempotentHint: true,
|
|
15432
|
-
openWorldHint: false,
|
|
15433
|
-
destructiveHint: false
|
|
15434
|
-
},
|
|
16057
|
+
annotations: READ_OWN,
|
|
15435
16058
|
input: null,
|
|
15436
16059
|
output: null
|
|
15437
16060
|
},
|
|
15438
16061
|
{
|
|
15439
16062
|
name: "search_hotels",
|
|
15440
16063
|
title: "Search hotels",
|
|
15441
|
-
description:
|
|
15442
|
-
annotations:
|
|
15443
|
-
readOnlyHint: true,
|
|
15444
|
-
idempotentHint: true,
|
|
15445
|
-
openWorldHint: true,
|
|
15446
|
-
destructiveHint: false
|
|
15447
|
-
},
|
|
16064
|
+
description: 'Find hotels by destination and/or text, with an indicative per-room-night price when a stay is given. destination is a place_name with its countryCode, a place or locality by placeId from an earlier answer, a point, or a country. A name several places bear answers destination_ambiguous with candidates: ask the user, then search the chosen placeId. text matches hotel names, brands and areas word for word. Indicative prices are estimates and not bookable: get_hotel_offers returns what the user pays. A search too broad to answer is refused with search_too_broad and its localities: pass one back as { kind: "locality", placeId }, never by name. The answer states the scope, the counts and notes; a next page is the cursor and, optionally, limit.',
|
|
16065
|
+
annotations: READ,
|
|
15448
16066
|
input: searchHotelsInput,
|
|
15449
|
-
output:
|
|
16067
|
+
output: searchResult
|
|
15450
16068
|
},
|
|
15451
16069
|
{
|
|
15452
16070
|
name: "get_hotel",
|
|
15453
16071
|
title: "Get hotel details",
|
|
15454
|
-
description:
|
|
15455
|
-
annotations:
|
|
15456
|
-
readOnlyHint: true,
|
|
15457
|
-
idempotentHint: true,
|
|
15458
|
-
openWorldHint: true,
|
|
15459
|
-
destructiveHint: false
|
|
15460
|
-
},
|
|
16072
|
+
description: `One hotel's catalogue facts: names, address, location, time zone, check-in and check-out times, the full description, every policy and the amenities, and its rooms with include: "rooms". "Not listed" is not "no". No prices or availability: get_hotel_offers has those.`,
|
|
16073
|
+
annotations: READ,
|
|
15461
16074
|
input: getHotelInput,
|
|
15462
|
-
output:
|
|
16075
|
+
output: hotel
|
|
15463
16076
|
},
|
|
15464
16077
|
{
|
|
15465
|
-
name: "
|
|
15466
|
-
title: "Get live hotel
|
|
15467
|
-
description: `Live
|
|
15468
|
-
annotations:
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
openWorldHint: true,
|
|
15472
|
-
destructiveHint: false
|
|
15473
|
-
},
|
|
15474
|
-
input: MCP_RATES_INPUT,
|
|
15475
|
-
output: null
|
|
16078
|
+
name: "get_hotel_offers",
|
|
16079
|
+
title: "Get live hotel offers",
|
|
16080
|
+
description: `Live, bookable offers for up to ${LIMITS.offersHotels} hotels and one stay. Every room in stay.rooms must have the same party: for different parties, call once per party. Each offer has the room, the meals included, price.total in USD for the whole stay and every room (what is charged), fees payable at the property, the cancellation terms with times in UTC and the hotel's time zone, the confirmation mode, and an offerId for book_hotel that expires at expiresAt. Hotels with nothing to offer are listed under unavailable with the reason. Up to limitPerHotel offers per hotel, default ${LIMITS.offersPerHotelDefault}, cheapest first.`,
|
|
16081
|
+
annotations: READ,
|
|
16082
|
+
input: hotelOffersInput,
|
|
16083
|
+
output: hotelOffersResult
|
|
15476
16084
|
},
|
|
15477
16085
|
{
|
|
15478
16086
|
name: "book_hotel",
|
|
15479
|
-
title: "Create hotel
|
|
15480
|
-
description: `
|
|
16087
|
+
title: "Create hotel order and payment link",
|
|
16088
|
+
description: `Order one offer from get_hotel_offers. The answer is an unpaid order with a payment link: status awaiting_payment, the total, the stay and its terms, and payment.url, a Fortrip page where the user pays. Nothing is charged until the user pays there; anyone holding the link can pay. Send the contact and, for each room of the offer, at least one guest. If the price or terms changed since the offer, the answer is offer_changed with every change and a new offer: show the changes and ask the user again before ordering it. Sending the same details again returns the same order. This tool can't change or cancel an order: for changes, cancellations or refunds the user emails ${SUPPORT_EMAIL} with the order's reference.`,
|
|
15481
16089
|
annotations: {
|
|
15482
16090
|
readOnlyHint: false,
|
|
15483
|
-
idempotentHint:
|
|
16091
|
+
idempotentHint: false,
|
|
15484
16092
|
openWorldHint: true,
|
|
15485
16093
|
destructiveHint: false
|
|
15486
16094
|
},
|
|
15487
|
-
input:
|
|
15488
|
-
output:
|
|
16095
|
+
input: createOrderInput,
|
|
16096
|
+
output: MCP_OUTPUT.book_hotel
|
|
16097
|
+
},
|
|
16098
|
+
{
|
|
16099
|
+
name: "get_order",
|
|
16100
|
+
title: "Get order",
|
|
16101
|
+
description: "One of the user's orders by orderId: its status, the payment status and, while it awaits payment, payment.url; the stay and its terms; and the hotel's confirmation number once there is one. Check it after the user pays. An order awaiting payment whose payment.url is null had its link unreadable this time: read it again.",
|
|
16102
|
+
annotations: READ_OWN,
|
|
16103
|
+
input: getOrderInput,
|
|
16104
|
+
output: MCP_OUTPUT.get_order
|
|
16105
|
+
},
|
|
16106
|
+
{
|
|
16107
|
+
name: "list_orders",
|
|
16108
|
+
title: "List orders",
|
|
16109
|
+
description: "The user's hotel orders, newest first, including those made on the Fortrip website. Pass nextCursor back as cursor for the next page.",
|
|
16110
|
+
annotations: READ_OWN,
|
|
16111
|
+
input: listOrdersInput,
|
|
16112
|
+
output: MCP_OUTPUT.list_orders
|
|
15489
16113
|
}
|
|
15490
16114
|
];
|
|
15491
16115
|
|
|
16116
|
+
// ../contract/src/errors.ts
|
|
16117
|
+
var ERROR_CODES = {
|
|
16118
|
+
invalid_request: { status: [400], retryable: false },
|
|
16119
|
+
cursor_invalid: { status: [400], retryable: false },
|
|
16120
|
+
unauthenticated: { status: [401], retryable: false },
|
|
16121
|
+
forbidden: { status: [403], retryable: false },
|
|
16122
|
+
not_found: { status: [404], retryable: false },
|
|
16123
|
+
order_removed: { status: [410], retryable: false },
|
|
16124
|
+
offer_expired: { status: [409], retryable: false },
|
|
16125
|
+
offer_changed: { status: [409], retryable: false },
|
|
16126
|
+
offer_already_ordered: { status: [409], retryable: false },
|
|
16127
|
+
order_exists: { status: [409], retryable: false },
|
|
16128
|
+
order_in_progress: { status: [409], retryable: true },
|
|
16129
|
+
order_attempts_exhausted: { status: [409], retryable: false },
|
|
16130
|
+
destination_ambiguous: { status: [422], retryable: false },
|
|
16131
|
+
destination_not_found: { status: [422], retryable: false },
|
|
16132
|
+
search_too_broad: { status: [422], retryable: false },
|
|
16133
|
+
stay_unsupported: { status: [422], retryable: false },
|
|
16134
|
+
stay_in_past: { status: [422], retryable: false },
|
|
16135
|
+
offer_unavailable: { status: [422], retryable: false },
|
|
16136
|
+
rate_limited: { status: [429], retryable: true },
|
|
16137
|
+
ordering_paused: { status: [503], retryable: true },
|
|
16138
|
+
service_unavailable: { status: [503], retryable: true },
|
|
16139
|
+
supplier_unavailable: { status: [503, 504], retryable: true },
|
|
16140
|
+
internal: { status: [500], retryable: false }
|
|
16141
|
+
};
|
|
16142
|
+
|
|
16143
|
+
// ../contract/src/outputs/error-details.ts
|
|
16144
|
+
var count2 = external_exports.number().int();
|
|
16145
|
+
var ERROR_DETAILS = {
|
|
16146
|
+
invalid_request: openObject({
|
|
16147
|
+
issues: external_exports.array(openObject({ pointer: external_exports.string(), message: external_exports.string() })),
|
|
16148
|
+
filterable: external_exports.array(openEnum(AMENITY_CODES)).optional(),
|
|
16149
|
+
share: openObject({
|
|
16150
|
+
code: openEnum(AMENITY_CODES),
|
|
16151
|
+
listedShare: external_exports.number().min(0).max(1)
|
|
16152
|
+
}).optional()
|
|
16153
|
+
}),
|
|
16154
|
+
cursor_invalid: openObject({
|
|
16155
|
+
reason: openEnum(["expired", "mismatch", "malformed"])
|
|
16156
|
+
}),
|
|
16157
|
+
unauthenticated: null,
|
|
16158
|
+
forbidden: null,
|
|
16159
|
+
not_found: null,
|
|
16160
|
+
order_removed: openObject({ orderId, reference: orderReference }),
|
|
16161
|
+
offer_expired: openObject({ offerId, expiredAt: instant }),
|
|
16162
|
+
offer_changed: openObject({ changes: external_exports.array(offerChange).min(1), offer }),
|
|
16163
|
+
offer_already_ordered: openObject({ orderId }),
|
|
16164
|
+
order_exists: openObject({ orderId, status: openEnum(ORDER_STATUSES) }),
|
|
16165
|
+
order_in_progress: openObject({ orderId }),
|
|
16166
|
+
order_attempts_exhausted: openObject({ orderId }),
|
|
16167
|
+
destination_ambiguous: openObject({
|
|
16168
|
+
name: external_exports.string(),
|
|
16169
|
+
countryCode: external_exports.string(),
|
|
16170
|
+
candidates: external_exports.array(
|
|
16171
|
+
openObject({
|
|
16172
|
+
...place.shape,
|
|
16173
|
+
hotels: openObject({
|
|
16174
|
+
scope: openEnum(["area", "radius"]),
|
|
16175
|
+
radiusMeters: external_exports.number().nullable(),
|
|
16176
|
+
count: count2
|
|
16177
|
+
})
|
|
16178
|
+
})
|
|
16179
|
+
),
|
|
16180
|
+
candidatesTotal: count2,
|
|
16181
|
+
nameAsText: count2.nullable()
|
|
16182
|
+
}),
|
|
16183
|
+
destination_not_found: openUnion("reason", [
|
|
16184
|
+
openObject({
|
|
16185
|
+
reason: external_exports.literal("no_match"),
|
|
16186
|
+
name: external_exports.string(),
|
|
16187
|
+
countryCode: external_exports.string(),
|
|
16188
|
+
nameAsText: count2.nullable()
|
|
16189
|
+
}),
|
|
16190
|
+
openObject({ reason: external_exports.literal("unknown_id"), placeId }),
|
|
16191
|
+
openObject({
|
|
16192
|
+
reason: external_exports.literal("not_a_locality"),
|
|
16193
|
+
placeId,
|
|
16194
|
+
hub: placeRef.nullable(),
|
|
16195
|
+
hubTotal: count2.nullable()
|
|
16196
|
+
})
|
|
16197
|
+
]),
|
|
16198
|
+
search_too_broad: openObject({
|
|
16199
|
+
matched: count2,
|
|
16200
|
+
poolLimit: count2,
|
|
16201
|
+
localities,
|
|
16202
|
+
/** The searched area's center, as a search echoes it; null for text only.
|
|
16203
|
+
* Optional for clients; the server must send it (`SERVER_DETAILS`). */
|
|
16204
|
+
center: center.nullable().optional()
|
|
16205
|
+
}),
|
|
16206
|
+
stay_unsupported: openUnion("reason", [
|
|
16207
|
+
openObject({ reason: external_exports.literal("mixed_occupancy") }),
|
|
16208
|
+
openObject({ reason: external_exports.literal("max_nights"), max: count2 })
|
|
16209
|
+
]),
|
|
16210
|
+
stay_in_past: openObject({
|
|
16211
|
+
checkIn: localDate,
|
|
16212
|
+
timeZone: external_exports.string().nullable()
|
|
16213
|
+
}),
|
|
16214
|
+
offer_unavailable: openObject({
|
|
16215
|
+
offerId,
|
|
16216
|
+
reason: openEnum(["sold_out", "refused"])
|
|
16217
|
+
}),
|
|
16218
|
+
rate_limited: openObject({
|
|
16219
|
+
scope: openEnum(["search", "offers", "hotel", "orders_read", "orders_create"])
|
|
16220
|
+
}),
|
|
16221
|
+
ordering_paused: null,
|
|
16222
|
+
service_unavailable: null,
|
|
16223
|
+
supplier_unavailable: openObject({
|
|
16224
|
+
outcome: openEnum(["not_started", "unknown"])
|
|
16225
|
+
}),
|
|
16226
|
+
internal: null
|
|
16227
|
+
};
|
|
16228
|
+
var SERVER_DETAILS = {
|
|
16229
|
+
search_too_broad: ERROR_DETAILS.search_too_broad.extend({ center: center.nullable() })
|
|
16230
|
+
};
|
|
16231
|
+
function envelopeSchema(detailsRequired) {
|
|
16232
|
+
return openObject({
|
|
16233
|
+
error: openUnion(
|
|
16234
|
+
"code",
|
|
16235
|
+
Object.keys(ERROR_CODES).map((code) => {
|
|
16236
|
+
const details = detailsRequired ? SERVER_DETAILS[code] ?? ERROR_DETAILS[code] : ERROR_DETAILS[code];
|
|
16237
|
+
return openObject({
|
|
16238
|
+
code: external_exports.literal(code),
|
|
16239
|
+
message: external_exports.string(),
|
|
16240
|
+
param: external_exports.string().optional(),
|
|
16241
|
+
retryable: external_exports.boolean(),
|
|
16242
|
+
retryAfterSeconds: external_exports.number().int().optional(),
|
|
16243
|
+
...details ? { details: detailsRequired ? details : details.optional() } : {},
|
|
16244
|
+
requestId: external_exports.string()
|
|
16245
|
+
});
|
|
16246
|
+
})
|
|
16247
|
+
)
|
|
16248
|
+
});
|
|
16249
|
+
}
|
|
16250
|
+
var errorEnvelope = envelopeSchema(false);
|
|
16251
|
+
|
|
16252
|
+
// ../contract/src/render/format.ts
|
|
16253
|
+
function num(n) {
|
|
16254
|
+
return n.toLocaleString("en-US");
|
|
16255
|
+
}
|
|
16256
|
+
function km(meters) {
|
|
16257
|
+
return `${(meters / 1e3).toFixed(1)} km`;
|
|
16258
|
+
}
|
|
16259
|
+
function money2(m) {
|
|
16260
|
+
return `${m.currency} ${m.amount}`;
|
|
16261
|
+
}
|
|
16262
|
+
function plural(n, one, many = `${one}s`) {
|
|
16263
|
+
return `${num(n)} ${n === 1 ? one : many}`;
|
|
16264
|
+
}
|
|
16265
|
+
function named(name, names2) {
|
|
16266
|
+
const other = [names2.en, names2.zh].find((n) => n && n !== name);
|
|
16267
|
+
return other ? `${name} (${other})` : name;
|
|
16268
|
+
}
|
|
16269
|
+
function label(words, value) {
|
|
16270
|
+
return Object.hasOwn(words, value) ? words[value] : value;
|
|
16271
|
+
}
|
|
16272
|
+
function stayLine(checkIn, checkOut) {
|
|
16273
|
+
const nights = countIsoDateDays(checkIn, checkOut);
|
|
16274
|
+
return `${checkIn} \u2192 ${checkOut} (${plural(nights, "night")})`;
|
|
16275
|
+
}
|
|
16276
|
+
function partyOf(room) {
|
|
16277
|
+
const adults = plural(room.adults, "adult");
|
|
16278
|
+
const n = room.childrenAges.length;
|
|
16279
|
+
if (n === 0) return adults;
|
|
16280
|
+
const ages = room.childrenAges.join(", ");
|
|
16281
|
+
return `${adults} and ${plural(n, "child", "children")} (${n === 1 ? "age" : "ages"} ${ages})`;
|
|
16282
|
+
}
|
|
16283
|
+
function party(rooms) {
|
|
16284
|
+
const parties = rooms.map(partyOf);
|
|
16285
|
+
if (parties.length === 1) return `1 room: ${parties[0]}`;
|
|
16286
|
+
if (parties.every((p) => p === parties[0])) {
|
|
16287
|
+
return `${parties.length} rooms, each ${parties[0]}`;
|
|
16288
|
+
}
|
|
16289
|
+
return `${parties.length} rooms: ${parties.map((p, i) => `room ${i + 1} ${p}`).join("; ")}`;
|
|
16290
|
+
}
|
|
16291
|
+
function when(instant2, timeZone2) {
|
|
16292
|
+
const utc = `${instant2.slice(0, 10)} ${instant2.slice(11, 16)} UTC`;
|
|
16293
|
+
if (!timeZone2) return utc;
|
|
16294
|
+
try {
|
|
16295
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
16296
|
+
timeZone: timeZone2,
|
|
16297
|
+
year: "numeric",
|
|
16298
|
+
month: "2-digit",
|
|
16299
|
+
day: "2-digit",
|
|
16300
|
+
hour: "2-digit",
|
|
16301
|
+
minute: "2-digit",
|
|
16302
|
+
hourCycle: "h23"
|
|
16303
|
+
}).formatToParts(new Date(instant2));
|
|
16304
|
+
const part = (type) => parts.find((p) => p.type === type)?.value;
|
|
16305
|
+
return `${utc} (${part("year")}-${part("month")}-${part("day")} ${part("hour")}:${part("minute")} ${timeZone2})`;
|
|
16306
|
+
} catch {
|
|
16307
|
+
return utc;
|
|
16308
|
+
}
|
|
16309
|
+
}
|
|
16310
|
+
function raw(value) {
|
|
16311
|
+
try {
|
|
16312
|
+
return JSON.stringify(value) ?? "no value";
|
|
16313
|
+
} catch {
|
|
16314
|
+
return String(value);
|
|
16315
|
+
}
|
|
16316
|
+
}
|
|
16317
|
+
var WINDOWS = {
|
|
16318
|
+
yes: "window",
|
|
16319
|
+
no: "no window",
|
|
16320
|
+
partial: "window in some rooms"
|
|
16321
|
+
};
|
|
16322
|
+
function roomFacts2(r) {
|
|
16323
|
+
const area = r.areaSqm ? r.areaSqm.min === r.areaSqm.max ? `${r.areaSqm.min} m\xB2` : `${r.areaSqm.min}\u2013${r.areaSqm.max} m\xB2` : "size not stated";
|
|
16324
|
+
return [
|
|
16325
|
+
r.beds ? `beds: ${r.beds.text}` : "beds not listed",
|
|
16326
|
+
area,
|
|
16327
|
+
r.maxOccupancy === null ? "max occupancy not listed" : `max ${plural(r.maxOccupancy, "guest")}`,
|
|
16328
|
+
r.window === null ? "window not listed" : label(WINDOWS, r.window)
|
|
16329
|
+
];
|
|
16330
|
+
}
|
|
16331
|
+
|
|
16332
|
+
// ../contract/src/render/offers.ts
|
|
16333
|
+
var MEALS_UNKNOWN = "meals not stated by the supplier \u2014 not known to be room only, may include breakfast; ask or check at booking";
|
|
16334
|
+
function mealsText(m) {
|
|
16335
|
+
if (!m.known) return MEALS_UNKNOWN;
|
|
16336
|
+
if (m.included === null) return "meals not stated";
|
|
16337
|
+
let base;
|
|
16338
|
+
if (m.allInclusive) {
|
|
16339
|
+
base = "all inclusive";
|
|
16340
|
+
} else {
|
|
16341
|
+
const extra = m.unspecifiedMainMeals;
|
|
16342
|
+
const parts = [...m.included];
|
|
16343
|
+
if (extra !== null && extra > 0) parts.push(`${plural(extra, "more main meal")} (not named)`);
|
|
16344
|
+
if (parts.length === 0 && extra === 0) return "room only";
|
|
16345
|
+
base = parts.length > 0 ? parts.join(" + ") : "no meals named";
|
|
16346
|
+
if (extra === null) base += "; other main meals not stated";
|
|
16347
|
+
}
|
|
16348
|
+
return m.guestsCoveredPerRoom === null ? `${base}, guests covered not stated` : `${base} for ${plural(m.guestsCoveredPerRoom, "guest")} per room`;
|
|
16349
|
+
}
|
|
16350
|
+
function mealsWithLabel(m) {
|
|
16351
|
+
const text2 = mealsText(m);
|
|
16352
|
+
return m.label ? `${text2} (labelled "${m.label.text}")` : text2;
|
|
16353
|
+
}
|
|
16354
|
+
function cancellationText(c) {
|
|
16355
|
+
if (c.status === "unknown") return "cancellation terms unknown";
|
|
16356
|
+
const steps = c.penalties.map(
|
|
16357
|
+
(p) => `${money2(p.amount)} from ${p.startsAt === null ? "booking" : when(p.startsAt, c.timeZone)}`
|
|
16358
|
+
);
|
|
16359
|
+
if (c.status === "free") {
|
|
16360
|
+
const head2 = c.freeCancellationEndsAt === null ? "free cancellation until check-in" : `free cancellation until ${when(c.freeCancellationEndsAt, c.timeZone)}`;
|
|
16361
|
+
return steps.length ? `${head2}, then ${steps.join("; ")}` : head2;
|
|
16362
|
+
}
|
|
16363
|
+
const head = label(
|
|
16364
|
+
{ partial_penalty: "partial penalty now", non_refundable: "non-refundable" },
|
|
16365
|
+
c.status
|
|
16366
|
+
);
|
|
16367
|
+
return steps.length ? `${head}: ${steps.join("; ")}` : head;
|
|
16368
|
+
}
|
|
16369
|
+
var BASES = {
|
|
16370
|
+
per_stay: "per stay",
|
|
16371
|
+
per_night: "per night",
|
|
16372
|
+
per_person: "per person",
|
|
16373
|
+
per_person_per_night: "per person per night",
|
|
16374
|
+
unknown: "basis unknown"
|
|
16375
|
+
};
|
|
16376
|
+
function feesText(p) {
|
|
16377
|
+
const items = p.items.map(
|
|
16378
|
+
(i) => [
|
|
16379
|
+
`${i.label ? i.label.text : "unnamed fee"} ${i.amount ? money2(i.amount) : "amount not stated"}`,
|
|
16380
|
+
label(BASES, i.basis),
|
|
16381
|
+
i.mandatory ? "mandatory" : "optional",
|
|
16382
|
+
i.collectedAt === "property" ? "at the property" : i.collectedAt === "unknown" ? "collection not stated (could be prepaid or at the property)" : `collected: ${i.collectedAt}`
|
|
16383
|
+
].filter(Boolean).join(", ")
|
|
16384
|
+
);
|
|
16385
|
+
const listed = `fees not in the total: ${items.join("; ")}`;
|
|
16386
|
+
if (p.disclosure === "complete") {
|
|
16387
|
+
return items.length ? listed : "nothing to pay at the property";
|
|
16388
|
+
}
|
|
16389
|
+
const caveat = p.disclosure === "incomplete" ? "fees not fully disclosed (amounts or how they are collected may be missing)" : `fees not in the total: disclosure ${p.disclosure}`;
|
|
16390
|
+
return items.length ? `${listed}; ${caveat}` : caveat;
|
|
16391
|
+
}
|
|
16392
|
+
function confirmationText(c) {
|
|
16393
|
+
if (c.mode === "instant") return "instant confirmation";
|
|
16394
|
+
if (c.mode === "on_request") {
|
|
16395
|
+
return c.withinMinutes === null ? "confirmation on request" : `confirmation on request, within ${c.withinMinutes} min`;
|
|
16396
|
+
}
|
|
16397
|
+
if (c.mode === "unknown") return "confirmation mode unknown";
|
|
16398
|
+
return `confirmation ${c.mode}`;
|
|
16399
|
+
}
|
|
16400
|
+
function roomText(r) {
|
|
16401
|
+
return [r.name ? r.name.text : "room name not listed", ...roomFacts2(r)].join(", ");
|
|
16402
|
+
}
|
|
16403
|
+
function offerLine(o) {
|
|
16404
|
+
const p = o.price;
|
|
16405
|
+
let price = `${money2(p.total)} total (${money2(p.perNightAllRooms)} per night, all rooms)`;
|
|
16406
|
+
if (p.approx) {
|
|
16407
|
+
price += ` \u2248 ${money2(p.approx.total)} (${money2(p.approx.perNightAllRooms)} per night), exchange rate of ${p.approx.rateDate}`;
|
|
16408
|
+
}
|
|
16409
|
+
return [
|
|
16410
|
+
`${o.offerId}${o.type === "hotel" ? "" : ` (${o.type})`}`,
|
|
16411
|
+
`${plural(o.hotel.roomCount, "room")}: ${roomText(o.hotel.room)}`,
|
|
16412
|
+
price,
|
|
16413
|
+
mealsWithLabel(o.hotel.meals),
|
|
16414
|
+
cancellationText(o.cancellation),
|
|
16415
|
+
confirmationText(o.confirmation),
|
|
16416
|
+
feesText(p.payAtProperty),
|
|
16417
|
+
`offer expires ${when(o.expiresAt, null)}`
|
|
16418
|
+
].join(" \xB7 ");
|
|
16419
|
+
}
|
|
16420
|
+
var OMITTED = {
|
|
16421
|
+
byNationality: "restricted by nationality",
|
|
16422
|
+
byFilters: "removed by the filters",
|
|
16423
|
+
dominated: "beaten by another offer for the same room",
|
|
16424
|
+
unreadable: "unreadable"
|
|
16425
|
+
};
|
|
16426
|
+
function limitText(o) {
|
|
16427
|
+
const n = o.byLimit;
|
|
16428
|
+
return typeof n === "number" && n > 0 ? `${plural(n, "more offer")} not shown (limitPerHotel); a filtered request (requireBreakfast / requireFreeCancellation) or a higher limitPerHotel may show them \u2014 don't conclude they don't exist` : "";
|
|
16429
|
+
}
|
|
16430
|
+
function omittedText(o) {
|
|
16431
|
+
return Object.entries(OMITTED).filter(([key]) => typeof o[key] === "number" && o[key] > 0).map(([key, words]) => `${o[key]} ${words}`).join(", ");
|
|
16432
|
+
}
|
|
16433
|
+
var UNAVAILABLE = {
|
|
16434
|
+
sold_out: "sold out",
|
|
16435
|
+
no_matching_offers: "no offer matches the filters",
|
|
16436
|
+
nationality_restricted: "restricted by nationality",
|
|
16437
|
+
offers_unreadable: "offers unreadable",
|
|
16438
|
+
not_found: "hotel not found",
|
|
16439
|
+
supplier_unavailable: "offers could not be fetched this time",
|
|
16440
|
+
stay_in_past: "check-in is already past"
|
|
16441
|
+
};
|
|
16442
|
+
function renderOffers(r) {
|
|
16443
|
+
const lines = [
|
|
16444
|
+
`Live offers \xB7 ${stayLine(r.stay.checkIn, r.stay.checkOut)} \xB7 ${party(r.stay.rooms)}`,
|
|
16445
|
+
"total = the whole stay and every room, the amount charged. Fees not in the total are in the property's currency, not converted. Times are UTC, then the hotel's time zone."
|
|
16446
|
+
];
|
|
16447
|
+
if (r.hotels.length === 0) lines.push("", "No hotel has offers for this stay.");
|
|
16448
|
+
for (const h of r.hotels) {
|
|
16449
|
+
lines.push("", `${named(h.hotel.name, h.hotel.names)} \xB7 ${h.hotel.hotelId} \xB7 ${plural(h.offers.length, "offer")}`);
|
|
16450
|
+
h.offers.forEach((o, i) => lines.push(`${i + 1}. ${offerLine(o)}`));
|
|
16451
|
+
const omitted2 = omittedText(h.omitted);
|
|
16452
|
+
if (omitted2) lines.push(`Not shown: ${omitted2}.`);
|
|
16453
|
+
const limited = limitText(h.omitted);
|
|
16454
|
+
if (limited) lines.push(`${limited}.`);
|
|
16455
|
+
}
|
|
16456
|
+
if (r.unavailable.length > 0) {
|
|
16457
|
+
lines.push("", "Unavailable:");
|
|
16458
|
+
for (const u of r.unavailable) {
|
|
16459
|
+
const omitted2 = u.omitted ? [omittedText(u.omitted), limitText(u.omitted)].filter(Boolean).join("; ") : "";
|
|
16460
|
+
lines.push(`- ${u.hotelId}: ${label(UNAVAILABLE, u.reason)}${omitted2 ? ` (${omitted2})` : ""}`);
|
|
16461
|
+
}
|
|
16462
|
+
}
|
|
16463
|
+
return lines.join("\n");
|
|
16464
|
+
}
|
|
16465
|
+
|
|
16466
|
+
// ../contract/src/render/order.ts
|
|
16467
|
+
var ORDER_STATUS_WORDS = {
|
|
16468
|
+
awaiting_payment: "awaiting payment",
|
|
16469
|
+
expired: "expired (not paid in time)",
|
|
16470
|
+
confirming: "confirming with the hotel",
|
|
16471
|
+
confirmed: "confirmed",
|
|
16472
|
+
failed: "failed",
|
|
16473
|
+
cancelling: "cancelling",
|
|
16474
|
+
cancelled: "cancelled",
|
|
16475
|
+
needs_attention: "needs attention from Fortrip"
|
|
16476
|
+
};
|
|
16477
|
+
var PAYMENT_WORDS = {
|
|
16478
|
+
unpaid: "unpaid",
|
|
16479
|
+
authorized: "authorized (held, not yet charged)",
|
|
16480
|
+
paid: "paid",
|
|
16481
|
+
released: "released (the hold was let go)",
|
|
16482
|
+
refund_pending: "refund pending",
|
|
16483
|
+
refunded: "refunded"
|
|
16484
|
+
};
|
|
16485
|
+
function itemLines(item) {
|
|
16486
|
+
if (item.type !== "hotel") {
|
|
16487
|
+
return [`Item of type ${item.type}: not shown in this text. ${raw(item)}`];
|
|
16488
|
+
}
|
|
16489
|
+
const h = item.hotel;
|
|
16490
|
+
return [
|
|
16491
|
+
[
|
|
16492
|
+
`Hotel: ${named(h.name, h.names)}`,
|
|
16493
|
+
h.hotelId ?? "not in the hotel catalogue",
|
|
16494
|
+
stayLine(h.checkIn, h.checkOut),
|
|
16495
|
+
party(h.rooms),
|
|
16496
|
+
`time zone ${h.timeZone ?? "not listed"}`
|
|
16497
|
+
].join(" \xB7 "),
|
|
16498
|
+
`Room: ${h.room.name ? h.room.name.text : "room name not listed"} \xB7 ${h.room.beds ? `beds: ${h.room.beds.text}` : "beds not listed"}`,
|
|
16499
|
+
`Terms: ${mealsWithLabel(h.meals)} \xB7 ${cancellationText(h.cancellation)} \xB7 ${feesText(h.payAtProperty)}`,
|
|
16500
|
+
`Hotel confirmation number: ${h.confirmationNumber ?? "none yet"}`
|
|
16501
|
+
];
|
|
16502
|
+
}
|
|
16503
|
+
function paymentLines(o) {
|
|
16504
|
+
const p = o.payment;
|
|
16505
|
+
const lines = [
|
|
16506
|
+
[
|
|
16507
|
+
`Total: ${money2(o.price.total)}`,
|
|
16508
|
+
`payment ${label(PAYMENT_WORDS, p.status)}`,
|
|
16509
|
+
`charged in ${p.chargeCurrency}${p.localCurrencyAtCheckout ? " (the user's card may pay in its local currency at checkout)" : ""}`,
|
|
16510
|
+
p.localCharge ? `card charged ${money2(p.localCharge)}` : ""
|
|
16511
|
+
].filter(Boolean).join(" \xB7 ")
|
|
16512
|
+
];
|
|
16513
|
+
if (p.url !== null) {
|
|
16514
|
+
const about = [
|
|
16515
|
+
p.expiresAt === null ? "" : `expires ${when(p.expiresAt, null)}`,
|
|
16516
|
+
p.requiresSignIn ? "the user signs in to Fortrip to pay" : "no sign-in needed"
|
|
16517
|
+
].filter(Boolean);
|
|
16518
|
+
lines.push(`Payment link (${about.join("; ")}): ${p.url}`);
|
|
16519
|
+
} else if (o.status === "awaiting_payment") {
|
|
16520
|
+
lines.push("Payment link: could not be read this time; read this order again for it.");
|
|
16521
|
+
}
|
|
16522
|
+
lines.push(`Manage: ${o.manageUrl}`);
|
|
16523
|
+
return lines;
|
|
16524
|
+
}
|
|
16525
|
+
function orderLines(o) {
|
|
16526
|
+
return [
|
|
16527
|
+
`Order ${o.orderId} \xB7 reference ${o.reference} \xB7 ${label(ORDER_STATUS_WORDS, o.status)} \xB7 created ${when(o.createdAt, null)} \xB7 updated ${when(o.updatedAt, null)}`,
|
|
16528
|
+
...o.items.flatMap(itemLines),
|
|
16529
|
+
...paymentLines(o)
|
|
16530
|
+
];
|
|
16531
|
+
}
|
|
16532
|
+
function renderOrder(o) {
|
|
16533
|
+
return orderLines(o).join("\n");
|
|
16534
|
+
}
|
|
16535
|
+
function renderOrderList(list) {
|
|
16536
|
+
const lines = [
|
|
16537
|
+
list.data.length === 0 ? "No orders on this page." : `Orders, newest first: ${list.data.length} on this page.`
|
|
16538
|
+
];
|
|
16539
|
+
for (const o of list.data) lines.push("", ...orderLines(o));
|
|
16540
|
+
lines.push(
|
|
16541
|
+
"",
|
|
16542
|
+
list.nextCursor === null ? "Last page." : `Next page: call again with { cursor: "${list.nextCursor}" }.`
|
|
16543
|
+
);
|
|
16544
|
+
return lines.join("\n");
|
|
16545
|
+
}
|
|
16546
|
+
|
|
16547
|
+
// ../contract/src/render/search.ts
|
|
16548
|
+
var SORTS = {
|
|
16549
|
+
distance: "nearest first",
|
|
16550
|
+
relevance: "best text match first",
|
|
16551
|
+
stars_desc: "most stars first",
|
|
16552
|
+
price_asc: "cheapest first",
|
|
16553
|
+
price_desc: "dearest first"
|
|
16554
|
+
};
|
|
16555
|
+
var CENTER_BASES2 = {
|
|
16556
|
+
seat: "the seat",
|
|
16557
|
+
capital: "the capital",
|
|
16558
|
+
busiest_settlement: "the settlement with the most hotels",
|
|
16559
|
+
hub: "the locality's hub",
|
|
16560
|
+
point: "its location"
|
|
16561
|
+
};
|
|
16562
|
+
function radiusWords(meters, source) {
|
|
16563
|
+
return `within ${km(meters)}${source === "default" ? ", the default radius" : ""}`;
|
|
16564
|
+
}
|
|
16565
|
+
function placeHeader(d, matched) {
|
|
16566
|
+
const p = d.place;
|
|
16567
|
+
const scope = d.scope === "locality" ? "locality" : d.scope === "area" ? d.radiusMeters === null ? p.kind : `${p.kind}, within ${km(d.radiusMeters)} of its center` : d.scope === "radius" ? d.radiusMeters === null ? `${p.kind}, radius not stated` : `${p.kind}, ${radiusWords(d.radiusMeters, d.radiusSource)}` : `${p.kind}, scope ${d.scope}`;
|
|
16568
|
+
const where = [p.region && `${p.region.name} ${p.region.placeId}`, p.countryCode].filter(Boolean).join(", ");
|
|
16569
|
+
const other = [p.names.en, p.names.zh].find((n) => n && n !== p.name);
|
|
16570
|
+
return [
|
|
16571
|
+
`${p.name} (${scope})`,
|
|
16572
|
+
plural(matched, "hotel"),
|
|
16573
|
+
p.placeId,
|
|
16574
|
+
where,
|
|
16575
|
+
other ? `also named ${other}` : ""
|
|
16576
|
+
].filter(Boolean).join(" \xB7 ");
|
|
16577
|
+
}
|
|
16578
|
+
function header(r) {
|
|
16579
|
+
const d = r.destination;
|
|
16580
|
+
const matched = r.coverage.matched;
|
|
16581
|
+
let head;
|
|
16582
|
+
if (d === null) {
|
|
16583
|
+
head = `${r.text ? `Text "${r.text.value}"` : "All hotels"} \xB7 ${plural(matched, "hotel")}`;
|
|
16584
|
+
} else if (d.kind === "place") {
|
|
16585
|
+
head = placeHeader(d, matched);
|
|
16586
|
+
} else if (d.kind === "point") {
|
|
16587
|
+
const p = d;
|
|
16588
|
+
const { lat, lng } = p.center.location;
|
|
16589
|
+
head = `Point ${lat}, ${lng} (${radiusWords(p.radiusMeters, p.radiusSource)}) \xB7 ${plural(matched, "hotel")}`;
|
|
16590
|
+
} else {
|
|
16591
|
+
head = `Destination of kind ${d.kind} \xB7 ${plural(matched, "hotel")}`;
|
|
16592
|
+
}
|
|
16593
|
+
if (d !== null && r.text) head += ` \xB7 text "${r.text.value}" (exact words)`;
|
|
16594
|
+
return head;
|
|
16595
|
+
}
|
|
16596
|
+
function centerLine(d) {
|
|
16597
|
+
if (d === null || d.kind !== "place" && d.kind !== "point") return null;
|
|
16598
|
+
const c = d.center;
|
|
16599
|
+
const from = c.place ? `${c.place.name} ${c.place.placeId}` : `${c.location.lat}, ${c.location.lng}`;
|
|
16600
|
+
return `Distances from ${from} (${label(CENTER_BASES2, c.basis)}).`;
|
|
16601
|
+
}
|
|
16602
|
+
function pricingWords(r) {
|
|
16603
|
+
const { mode, scope, status } = r.pricing;
|
|
16604
|
+
if (mode === "none") return "no prices (no stay given)";
|
|
16605
|
+
if (mode === "indicative" && status === "unavailable") {
|
|
16606
|
+
return "indicative prices unavailable this time";
|
|
16607
|
+
}
|
|
16608
|
+
if (mode === "indicative") {
|
|
16609
|
+
return "indicative prices per room-night: estimates, not bookable (only offers are)";
|
|
16610
|
+
}
|
|
16611
|
+
return `prices ${[mode, scope, status].filter(Boolean).join(", ")}`;
|
|
16612
|
+
}
|
|
16613
|
+
function countsLine(r) {
|
|
16614
|
+
const c = r.coverage;
|
|
16615
|
+
const country = r.destination?.kind === "place" ? r.destination.place.countryCode : "the country";
|
|
16616
|
+
const parts = [`${num(c.matched)} matched`];
|
|
16617
|
+
if (c.priced !== null) parts.push(`${num(c.priced)} priced`);
|
|
16618
|
+
if (c.withPrice !== null) parts.push(`${num(c.withPrice)} with a price`);
|
|
16619
|
+
else if (r.pricing.mode === "indicative") parts.push("with a price: unknown");
|
|
16620
|
+
if (c.excludedNoPrice !== null) {
|
|
16621
|
+
parts.push(`${num(c.excludedNoPrice)} left out for having no indicative price`);
|
|
16622
|
+
}
|
|
16623
|
+
if (c.excludedNoFacilityData !== null) {
|
|
16624
|
+
parts.push(`${num(c.excludedNoFacilityData)} left out for listing no facilities`);
|
|
16625
|
+
}
|
|
16626
|
+
if (c.withinMaxRadius !== null) parts.push(`${num(c.withinMaxRadius)} within 80 km`);
|
|
16627
|
+
if (c.textAnywhere !== null) {
|
|
16628
|
+
parts.push(`${num(c.textAnywhere)} match the text anywhere in ${country}`);
|
|
16629
|
+
}
|
|
16630
|
+
if (c.nameAsText !== null) parts.push(`${num(c.nameAsText)} have the name as text`);
|
|
16631
|
+
return `Counts: ${parts.join(", ")}.`;
|
|
16632
|
+
}
|
|
16633
|
+
function localitiesLines(loc, areaName) {
|
|
16634
|
+
const lines = [
|
|
16635
|
+
'Localities: pass one back as destination { kind: "locality", placeId }, never the name. The number is its matching hotels; "incl." lists places inside it, with all their hotels regardless of filters ("hotels overall").'
|
|
16636
|
+
];
|
|
16637
|
+
for (const item of loc.items) {
|
|
16638
|
+
let line = `- ${item.place.name} ${item.place.placeId} ${num(item.count)}`;
|
|
16639
|
+
if (item.total !== item.count) line += ` here, ${num(item.total)} as a locality`;
|
|
16640
|
+
const sameName = areaName !== null && [item.place.name, item.place.names.en, item.place.names.zh].some(
|
|
16641
|
+
(n) => n?.toLowerCase() === areaName.toLowerCase()
|
|
16642
|
+
);
|
|
16643
|
+
if (sameName) line += ` \u2014 the locality, not the whole of ${areaName}`;
|
|
16644
|
+
if (item.includes.length > 0) {
|
|
16645
|
+
const inc = item.includes.map(
|
|
16646
|
+
(i) => `${i.place.name} ${i.place.placeId}, ${num(i.members)} hotels overall`
|
|
16647
|
+
);
|
|
16648
|
+
line += ` (incl. ${inc.join("; ")})`;
|
|
16649
|
+
}
|
|
16650
|
+
lines.push(line);
|
|
16651
|
+
}
|
|
16652
|
+
if (loc.unassigned > 0) {
|
|
16653
|
+
lines.push(`${plural(loc.unassigned, "hotel")} in this search belong to no locality.`);
|
|
16654
|
+
}
|
|
16655
|
+
if (loc.more > 0) {
|
|
16656
|
+
lines.push(`${num(loc.more)} more localities; name one with place_name.`);
|
|
16657
|
+
}
|
|
16658
|
+
return lines;
|
|
16659
|
+
}
|
|
16660
|
+
function priceWords(h, status) {
|
|
16661
|
+
const p = h.price;
|
|
16662
|
+
if (p === null) {
|
|
16663
|
+
return status === "ok" ? "no indicative price" : "price unavailable this time";
|
|
16664
|
+
}
|
|
16665
|
+
let text2 = `from ${money2(p.fromPerRoomNight)}/room-night (${money2(p.fromPerRoomStay)} per room for the stay)`;
|
|
16666
|
+
if (p.approx) {
|
|
16667
|
+
text2 += ` \u2248 ${money2(p.approx.fromPerRoomNight)}/room-night (${money2(p.approx.fromPerRoomStay)}), exchange rate of ${p.approx.rateDate}`;
|
|
16668
|
+
}
|
|
16669
|
+
if (p.kind !== "indicative" || p.basis !== "lowest_unit") {
|
|
16670
|
+
text2 += ` [${p.kind}, ${p.basis}]`;
|
|
16671
|
+
}
|
|
16672
|
+
return text2;
|
|
16673
|
+
}
|
|
16674
|
+
function hotelLine(h, i, r) {
|
|
16675
|
+
const where = [h.address.region?.name, h.address.countryCode].filter(Boolean).join(", ");
|
|
16676
|
+
return [
|
|
16677
|
+
`${i + 1}. ${named(h.name, h.names)}`,
|
|
16678
|
+
h.stars === null ? "stars not listed" : `${h.stars}\u2605`,
|
|
16679
|
+
r.destination === null ? "" : h.distanceMeters === null ? "distance unknown" : km(h.distanceMeters),
|
|
16680
|
+
r.pricing.mode === "none" ? "" : priceWords(h, r.pricing.status),
|
|
16681
|
+
h.address.line ? h.address.line.text : "address not listed",
|
|
16682
|
+
where,
|
|
16683
|
+
h.hotelId
|
|
16684
|
+
].filter(Boolean).join(" \xB7 ");
|
|
16685
|
+
}
|
|
16686
|
+
function renderSearch(r) {
|
|
16687
|
+
const lines = [header(r)];
|
|
16688
|
+
lines.push(
|
|
16689
|
+
[
|
|
16690
|
+
r.stay ? `Stay ${stayLine(r.stay.checkIn, r.stay.checkOut)}, ${party(r.stay.rooms)}` : "No stay",
|
|
16691
|
+
pricingWords(r),
|
|
16692
|
+
`sort: ${label(SORTS, r.sort)}`
|
|
16693
|
+
].join(" \xB7 ")
|
|
16694
|
+
);
|
|
16695
|
+
const center2 = centerLine(r.destination);
|
|
16696
|
+
if (center2) lines.push(center2);
|
|
16697
|
+
lines.push(countsLine(r));
|
|
16698
|
+
const pool = r.coverage.poolRadiusMeters;
|
|
16699
|
+
if (pool !== null) {
|
|
16700
|
+
const d = r.destination;
|
|
16701
|
+
const known = d !== null && (d.kind === "place" || d.kind === "point") ? d : null;
|
|
16702
|
+
const around = known?.center.place?.name ?? (known?.kind === "place" ? known.place.name : "the center");
|
|
16703
|
+
const priced = r.coverage.priced === null ? "" : ` ${num(r.coverage.priced)}`;
|
|
16704
|
+
lines.push(
|
|
16705
|
+
`Only the nearest${priced} matches were priced: "cheapest" here means cheapest within ${km(pool)} of ${around}.`
|
|
16706
|
+
);
|
|
16707
|
+
}
|
|
16708
|
+
if (r.coverage.localities) {
|
|
16709
|
+
const areaName = r.destination?.kind === "place" ? r.destination.place.name : null;
|
|
16710
|
+
lines.push("", ...localitiesLines(r.coverage.localities, areaName));
|
|
16711
|
+
}
|
|
16712
|
+
lines.push("");
|
|
16713
|
+
if (r.data.length === 0) lines.push("No hotels on this page.");
|
|
16714
|
+
r.data.forEach((h, i) => lines.push(hotelLine(h, i, r)));
|
|
16715
|
+
if (r.notes.length > 0) {
|
|
16716
|
+
lines.push("", "Notes:", ...r.notes.map((n) => `- ${n.code}: ${n.message}`));
|
|
16717
|
+
}
|
|
16718
|
+
lines.push(
|
|
16719
|
+
"",
|
|
16720
|
+
r.nextCursor === null ? "Last page." : `Next page: call again with only the cursor and, optionally, limit: { cursor: "${r.nextCursor}" }.`
|
|
16721
|
+
);
|
|
16722
|
+
return lines.join("\n");
|
|
16723
|
+
}
|
|
16724
|
+
|
|
16725
|
+
// ../contract/src/render/errors.ts
|
|
16726
|
+
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16727
|
+
function changeLine(c) {
|
|
16728
|
+
const worse = c.worse === true ? " (worse)" : c.worse === false ? " (not worse)" : "";
|
|
16729
|
+
const pair = (show) => `${show(c.previous)} \u2192 ${show(c.current)}`;
|
|
16730
|
+
switch (c.field) {
|
|
16731
|
+
case "price":
|
|
16732
|
+
return `- price: ${pair(money2)}${worse}`;
|
|
16733
|
+
case "meals":
|
|
16734
|
+
return `- meals: ${pair(mealsText)}${worse}`;
|
|
16735
|
+
case "cancellation":
|
|
16736
|
+
return `- cancellation: ${pair(cancellationText)}${worse}`;
|
|
16737
|
+
case "confirmation":
|
|
16738
|
+
return `- confirmation: ${pair(confirmationText)}${worse}`;
|
|
16739
|
+
case "pay_at_property":
|
|
16740
|
+
return `- fees not in the total: ${pair(feesText)}${worse}`;
|
|
16741
|
+
default:
|
|
16742
|
+
return `- ${c.field}: ${raw(c.previous)} \u2192 ${raw(c.current)}${worse}`;
|
|
16743
|
+
}
|
|
16744
|
+
}
|
|
16745
|
+
function tooBroad(d) {
|
|
16746
|
+
const lines = [
|
|
16747
|
+
'Next: show the user these localities and ask which they prefer, unless their request already names one of them; then search again with destination { kind: "locality", placeId: "<id>" } and the same text, filters, stay and sort.',
|
|
16748
|
+
...localitiesLines(d.localities, null)
|
|
16749
|
+
];
|
|
16750
|
+
const top = d.localities.items[0];
|
|
16751
|
+
if (top && top.total > d.poolLimit && d.center?.place?.placeId === top.place.placeId) {
|
|
16752
|
+
lines.push(
|
|
16753
|
+
`Or: ${top.place.name}, the area's center, alone has ${num(top.total)} matching hotels: the same destination with radiusMeters (up to ${LIMITS.radiusMetersMax}) prices nearly the same hotels (the nearest ${num(d.poolLimit)} around ${top.place.name}).`
|
|
16754
|
+
);
|
|
16755
|
+
}
|
|
16756
|
+
return lines;
|
|
16757
|
+
}
|
|
16758
|
+
function ambiguous(d) {
|
|
16759
|
+
const shown = d.candidatesTotal > d.candidates.length ? `${d.candidates.length} of ${d.candidatesTotal} shown` : `${d.candidatesTotal}`;
|
|
16760
|
+
const lines = [
|
|
16761
|
+
`Places named "${d.name}" in ${d.countryCode} (${shown}). Ask the user which one, then search with destination { kind: "place", placeId: "<id>" }:`
|
|
16762
|
+
];
|
|
16763
|
+
for (const c of d.candidates) {
|
|
16764
|
+
const other = [c.names.en, c.names.zh].find((n) => n && n !== c.name);
|
|
16765
|
+
const where = [c.region && `${c.region.name} ${c.region.placeId}`, c.countryCode].filter(Boolean).join(", ");
|
|
16766
|
+
const hotels = c.hotels.scope === "area" ? `${plural(c.hotels.count, "hotel")} in the area` : c.hotels.radiusMeters === null ? plural(c.hotels.count, "hotel") : `${plural(c.hotels.count, "hotel")} within ${km(c.hotels.radiusMeters)}`;
|
|
16767
|
+
lines.push(
|
|
16768
|
+
[
|
|
16769
|
+
`- ${c.name}${other ? ` (${other})` : ""} ${c.placeId}`,
|
|
16770
|
+
`${c.kind} in ${where}`,
|
|
16771
|
+
c.population === null ? "population not recorded" : `population ${num(c.population)}`,
|
|
16772
|
+
hotels
|
|
16773
|
+
].join(" \xB7 ")
|
|
16774
|
+
);
|
|
16775
|
+
}
|
|
16776
|
+
if (d.nameAsText !== null) {
|
|
16777
|
+
lines.push(`${plural(d.nameAsText, "hotel")} have "${d.name}" in their name or address: it can also be searched as text.`);
|
|
16778
|
+
}
|
|
16779
|
+
return lines;
|
|
16780
|
+
}
|
|
16781
|
+
function notFound(d) {
|
|
16782
|
+
switch (d.reason) {
|
|
16783
|
+
case "no_match": {
|
|
16784
|
+
const m = d;
|
|
16785
|
+
const text2 = m.nameAsText === null ? "" : ` ${plural(m.nameAsText, "hotel")} have "${m.name}" in their name or address: it can be searched as text.`;
|
|
16786
|
+
return [`No place named "${m.name}" in ${m.countryCode}.${text2}`];
|
|
16787
|
+
}
|
|
16788
|
+
case "unknown_id":
|
|
16789
|
+
return [`Unknown placeId ${d.placeId}.`];
|
|
16790
|
+
case "not_a_locality": {
|
|
16791
|
+
const n = d;
|
|
16792
|
+
const town = `{ kind: "place", placeId: "${n.placeId}" }`;
|
|
16793
|
+
if (n.hub === null) {
|
|
16794
|
+
return [`${n.placeId} belongs to no locality: search ${town} for an 8 km circle around it.`];
|
|
16795
|
+
}
|
|
16796
|
+
const total = n.hubTotal === null ? "" : ` (${plural(n.hubTotal, "matching hotel")})`;
|
|
16797
|
+
return [
|
|
16798
|
+
`${n.placeId} is not a locality. Its hotels belong to the locality ${n.hub.name} ${n.hub.placeId}${total}.`,
|
|
16799
|
+
`Search { kind: "locality", placeId: "${n.hub.placeId}" } for those, or ${town} for an 8 km circle around the town itself.`
|
|
16800
|
+
];
|
|
16801
|
+
}
|
|
16802
|
+
default:
|
|
16803
|
+
return [`Details: ${raw(d)}`];
|
|
16804
|
+
}
|
|
16805
|
+
}
|
|
16806
|
+
function detailLines(e) {
|
|
16807
|
+
if (!("details" in e) || e.details === void 0) return [];
|
|
16808
|
+
switch (e.code) {
|
|
16809
|
+
case "invalid_request": {
|
|
16810
|
+
const d = e.details;
|
|
16811
|
+
const lines = d.issues.map((i) => `- ${i.pointer}: ${i.message}`);
|
|
16812
|
+
if (d.filterable) lines.push(`Filterable amenities: ${d.filterable.join(", ")}.`);
|
|
16813
|
+
if (d.share) {
|
|
16814
|
+
lines.push(
|
|
16815
|
+
`${d.share.code} is listed by ${(d.share.listedShare * 100).toFixed(1)} % of hotels that list any facility.`
|
|
16816
|
+
);
|
|
16817
|
+
}
|
|
16818
|
+
return lines;
|
|
16819
|
+
}
|
|
16820
|
+
case "cursor_invalid":
|
|
16821
|
+
return [`The cursor is ${e.details.reason}: search again without a cursor.`];
|
|
16822
|
+
case "order_removed":
|
|
16823
|
+
return [`Order ${e.details.orderId}, reference ${e.details.reference}.`];
|
|
16824
|
+
case "offer_expired":
|
|
16825
|
+
return [`Offer ${e.details.offerId} expired at ${when(e.details.expiredAt, null)}: get fresh offers.`];
|
|
16826
|
+
case "offer_changed":
|
|
16827
|
+
return [
|
|
16828
|
+
"Changes:",
|
|
16829
|
+
...e.details.changes.map(changeLine),
|
|
16830
|
+
"New offer, held for ordering:",
|
|
16831
|
+
offerLine(e.details.offer),
|
|
16832
|
+
"Show the user these changes and ask again before ordering the new offer."
|
|
16833
|
+
];
|
|
16834
|
+
case "offer_already_ordered":
|
|
16835
|
+
case "order_in_progress":
|
|
16836
|
+
case "order_attempts_exhausted":
|
|
16837
|
+
return [`Order ${e.details.orderId}.`];
|
|
16838
|
+
case "order_exists":
|
|
16839
|
+
return [`Order ${e.details.orderId}, ${label(ORDER_STATUS_WORDS, e.details.status)}.`];
|
|
16840
|
+
case "destination_ambiguous":
|
|
16841
|
+
return ambiguous(e.details);
|
|
16842
|
+
case "destination_not_found":
|
|
16843
|
+
return notFound(e.details);
|
|
16844
|
+
case "search_too_broad":
|
|
16845
|
+
return tooBroad(e.details);
|
|
16846
|
+
case "stay_unsupported":
|
|
16847
|
+
return e.details.reason === "mixed_occupancy" ? ["Every room must have the same party: ask once per party."] : e.details.reason === "max_nights" ? [`At most ${plural(e.details.max, "night")} per stay.`] : [`Details: ${raw(e.details)}`];
|
|
16848
|
+
case "stay_in_past":
|
|
16849
|
+
return [`Check-in ${e.details.checkIn} is already past${e.details.timeZone ? ` in ${e.details.timeZone}` : ""}.`];
|
|
16850
|
+
case "offer_unavailable":
|
|
16851
|
+
return [
|
|
16852
|
+
`Offer ${e.details.offerId}: ${label({ sold_out: "sold out", refused: "can no longer be booked" }, e.details.reason)}. Get fresh offers.`
|
|
16853
|
+
];
|
|
16854
|
+
case "rate_limited":
|
|
16855
|
+
return [`Limit reached for: ${e.details.scope}.`];
|
|
16856
|
+
case "supplier_unavailable":
|
|
16857
|
+
return [
|
|
16858
|
+
label(
|
|
16859
|
+
{
|
|
16860
|
+
not_started: "Nothing was sent to the hotel's supplier.",
|
|
16861
|
+
unknown: "The request was sent and timed out: its outcome is unknown."
|
|
16862
|
+
},
|
|
16863
|
+
e.details.outcome
|
|
16864
|
+
)
|
|
16865
|
+
];
|
|
16866
|
+
default:
|
|
16867
|
+
return [`Details: ${raw(e.details)}`];
|
|
16868
|
+
}
|
|
16869
|
+
}
|
|
16870
|
+
function retryLine(retryable, after) {
|
|
16871
|
+
if (retryable === true) {
|
|
16872
|
+
return typeof after === "number" ? `Retryable: retry after ${after} s.` : "Retryable.";
|
|
16873
|
+
}
|
|
16874
|
+
return retryable === false ? "Not retryable." : "Retryable: unknown.";
|
|
16875
|
+
}
|
|
16876
|
+
function renderError(envelope, status) {
|
|
16877
|
+
const http = status === void 0 ? "" : ` (HTTP ${status})`;
|
|
16878
|
+
let known = null;
|
|
16879
|
+
let body = null;
|
|
16880
|
+
try {
|
|
16881
|
+
const parsed = parseOpen(errorEnvelope, envelope).error;
|
|
16882
|
+
body = parsed;
|
|
16883
|
+
if (Object.hasOwn(ERROR_CODES, parsed.code)) known = parsed;
|
|
16884
|
+
} catch {
|
|
16885
|
+
if (isObject2(envelope) && isObject2(envelope.error)) body = envelope.error;
|
|
16886
|
+
}
|
|
16887
|
+
if (body === null) {
|
|
16888
|
+
return `Error${http}: the answer carried no readable error.
|
|
16889
|
+
Retryable: unknown.`;
|
|
16890
|
+
}
|
|
16891
|
+
const code = typeof body.code === "string" ? body.code : "unknown";
|
|
16892
|
+
const lines = [
|
|
16893
|
+
`Error ${code}${http}${typeof body.message === "string" ? `: ${body.message}` : ""}`,
|
|
16894
|
+
retryLine(body.retryable, body.retryAfterSeconds)
|
|
16895
|
+
];
|
|
16896
|
+
if (typeof body.param === "string") lines.push(`Field: ${body.param}`);
|
|
16897
|
+
if (known) lines.push(...detailLines(known));
|
|
16898
|
+
else if (body.details !== void 0) lines.push(`Details: ${raw(body.details)}`);
|
|
16899
|
+
if (typeof body.requestId === "string") lines.push(`Request id: ${body.requestId}`);
|
|
16900
|
+
return lines.join("\n");
|
|
16901
|
+
}
|
|
16902
|
+
|
|
16903
|
+
// ../contract/src/render/hotel.ts
|
|
16904
|
+
function times(what, t) {
|
|
16905
|
+
if (t.from === null && t.until === null) return `${what}: not listed`;
|
|
16906
|
+
return `${what} from ${t.from ?? "not listed"}, until ${t.until ?? "not listed"}`;
|
|
16907
|
+
}
|
|
16908
|
+
function deposit(d) {
|
|
16909
|
+
if (d.required === null) return "Deposit: not stated";
|
|
16910
|
+
if (!d.required) return "Deposit: not required";
|
|
16911
|
+
return `Deposit: required, ${d.amount ? money2(d.amount) : "amount not stated"}`;
|
|
16912
|
+
}
|
|
16913
|
+
function roomLine(r) {
|
|
16914
|
+
return [`- ${r.name.text}`, ...roomFacts2(r)].join(" \xB7 ");
|
|
16915
|
+
}
|
|
16916
|
+
function renderHotel(h) {
|
|
16917
|
+
const region = h.address.region;
|
|
16918
|
+
const lines = [
|
|
16919
|
+
[
|
|
16920
|
+
named(h.name, h.names),
|
|
16921
|
+
h.stars === null ? "stars not listed" : `${h.stars}\u2605`,
|
|
16922
|
+
h.hotelId,
|
|
16923
|
+
h.bookable ? "" : "no longer bookable (it has left the catalogue)"
|
|
16924
|
+
].filter(Boolean).join(" \xB7 "),
|
|
16925
|
+
[
|
|
16926
|
+
`Address: ${h.address.line ? h.address.line.text : "not listed"}`,
|
|
16927
|
+
region ? `${region.name} ${region.placeId}` : "",
|
|
16928
|
+
h.address.countryCode ?? "",
|
|
16929
|
+
h.location ? `${h.location.lat}, ${h.location.lng}` : "location not listed",
|
|
16930
|
+
`time zone ${h.timeZone ?? "not listed"}`
|
|
16931
|
+
].filter(Boolean).join(" \xB7 "),
|
|
16932
|
+
`${times("Check-in", h.checkInTimes)} \xB7 ${times("Check-out", h.checkOutTimes)} (local time)`,
|
|
16933
|
+
`Breakfast served: ${h.breakfastServed === null ? "not stated" : h.breakfastServed ? "yes" : "no"}`,
|
|
16934
|
+
deposit(h.deposit),
|
|
16935
|
+
`Phone: ${h.contact.phone ?? "not listed"} \xB7 Website: ${h.contact.website ?? "not listed"}`
|
|
16936
|
+
];
|
|
16937
|
+
if (!h.amenities.listed) {
|
|
16938
|
+
lines.push('Amenities: not listed (unknown, not "none")');
|
|
16939
|
+
} else if (h.amenities.items.length === 0) {
|
|
16940
|
+
lines.push("Amenities: none listed");
|
|
16941
|
+
} else {
|
|
16942
|
+
const items = h.amenities.items.map((a) => `${a.name.text} (${a.code})`);
|
|
16943
|
+
lines.push(`Amenities listed (others may exist): ${items.join(", ")}`);
|
|
16944
|
+
}
|
|
16945
|
+
if (!h.policies.listed) {
|
|
16946
|
+
lines.push("Policies: not listed");
|
|
16947
|
+
} else if (h.policies.items.length === 0) {
|
|
16948
|
+
lines.push("Policies: none listed");
|
|
16949
|
+
} else {
|
|
16950
|
+
lines.push("Policies:", ...h.policies.items.map((p) => `- ${p.topic}: ${p.text}`));
|
|
16951
|
+
}
|
|
16952
|
+
if (h.rooms) {
|
|
16953
|
+
if (!h.rooms.listed) lines.push("Rooms: not listed");
|
|
16954
|
+
else if (h.rooms.items.length === 0) lines.push("Rooms: none listed");
|
|
16955
|
+
else lines.push("Rooms:", ...h.rooms.items.map(roomLine));
|
|
16956
|
+
}
|
|
16957
|
+
if (h.description === null) {
|
|
16958
|
+
lines.push("Description: not listed");
|
|
16959
|
+
} else {
|
|
16960
|
+
lines.push(`Description (${h.description.locale}):`, h.description.text);
|
|
16961
|
+
}
|
|
16962
|
+
return lines.join("\n");
|
|
16963
|
+
}
|
|
16964
|
+
|
|
16965
|
+
// ../contract/src/render/index.ts
|
|
16966
|
+
function renderToolResult(tool, data) {
|
|
16967
|
+
const schema = TOOLS.find((t) => t.name === tool)?.output;
|
|
16968
|
+
let parsed;
|
|
16969
|
+
try {
|
|
16970
|
+
if (!schema) throw new Error(`no output schema for ${tool}`);
|
|
16971
|
+
parsed = parseOpen(schema, data);
|
|
16972
|
+
} catch {
|
|
16973
|
+
const text2 = `This ${tool} result is not in the expected shape; as JSON:
|
|
16974
|
+
${raw(data)}`;
|
|
16975
|
+
return ORDER_TOOLS.has(tool) && holdsPaymentUrl(data) ? `${text2}
|
|
16976
|
+
|
|
16977
|
+
${ORDER_NEXT_STEP}` : text2;
|
|
16978
|
+
}
|
|
16979
|
+
switch (tool) {
|
|
16980
|
+
case "search_hotels":
|
|
16981
|
+
return renderSearch(parsed);
|
|
16982
|
+
case "get_hotel":
|
|
16983
|
+
return renderHotel(parsed);
|
|
16984
|
+
case "get_hotel_offers":
|
|
16985
|
+
return renderOffers(parsed);
|
|
16986
|
+
case "book_hotel":
|
|
16987
|
+
case "get_order": {
|
|
16988
|
+
const order2 = parsed;
|
|
16989
|
+
return withConsent(renderOrder(order2), [order2]);
|
|
16990
|
+
}
|
|
16991
|
+
case "list_orders": {
|
|
16992
|
+
const list = parsed;
|
|
16993
|
+
return withConsent(renderOrderList(list), list.data);
|
|
16994
|
+
}
|
|
16995
|
+
}
|
|
16996
|
+
}
|
|
16997
|
+
var ORDER_TOOLS = /* @__PURE__ */ new Set(["book_hotel", "get_order", "list_orders"]);
|
|
16998
|
+
function holdsPaymentUrl(value, seen = /* @__PURE__ */ new Set()) {
|
|
16999
|
+
if (typeof value !== "object" || value === null || seen.has(value)) return false;
|
|
17000
|
+
seen.add(value);
|
|
17001
|
+
const payment = value.payment;
|
|
17002
|
+
if (typeof payment === "object" && payment !== null && typeof payment.url === "string" && payment.url !== "") {
|
|
17003
|
+
return true;
|
|
17004
|
+
}
|
|
17005
|
+
return Object.values(value).some((v) => holdsPaymentUrl(v, seen));
|
|
17006
|
+
}
|
|
17007
|
+
function withConsent(text2, orders) {
|
|
17008
|
+
return orders.some((o) => o.payment.url !== null) ? `${text2}
|
|
17009
|
+
|
|
17010
|
+
${ORDER_NEXT_STEP}` : text2;
|
|
17011
|
+
}
|
|
17012
|
+
|
|
15492
17013
|
// ../contract/package.json
|
|
15493
17014
|
var package_default = {
|
|
15494
17015
|
name: "@fortrip/agent-contract",
|
|
15495
|
-
version: "
|
|
17016
|
+
version: "3.0.0",
|
|
15496
17017
|
license: "Apache-2.0",
|
|
15497
|
-
description: "The contract of Fortrip's agent API (
|
|
17018
|
+
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.",
|
|
15498
17019
|
type: "module",
|
|
15499
17020
|
repository: {
|
|
15500
17021
|
type: "git",
|
|
@@ -15541,7 +17062,46 @@ var package_default = {
|
|
|
15541
17062
|
// ../contract/src/version.ts
|
|
15542
17063
|
var CONTRACT_VERSION = package_default.version;
|
|
15543
17064
|
|
|
17065
|
+
// ../contract/src/outputs/account.ts
|
|
17066
|
+
var meResult = openObject({
|
|
17067
|
+
userId: external_exports.string(),
|
|
17068
|
+
email: external_exports.string().nullable(),
|
|
17069
|
+
name: external_exports.string().nullable()
|
|
17070
|
+
});
|
|
17071
|
+
var authConfig = openObject({
|
|
17072
|
+
issuer: external_exports.string(),
|
|
17073
|
+
clientId: external_exports.string(),
|
|
17074
|
+
scopes: external_exports.string(),
|
|
17075
|
+
deviceAuthorizationEndpoint: external_exports.string(),
|
|
17076
|
+
tokenEndpoint: external_exports.string(),
|
|
17077
|
+
revocationEndpoint: external_exports.string()
|
|
17078
|
+
});
|
|
17079
|
+
|
|
17080
|
+
// ../contract/src/paths.ts
|
|
17081
|
+
var PUBLIC_ORIGINS = {
|
|
17082
|
+
api: "https://api.fortrip.ai",
|
|
17083
|
+
mcp: "https://mcp.fortrip.ai"
|
|
17084
|
+
};
|
|
17085
|
+
var API = {
|
|
17086
|
+
me: "/v1/me",
|
|
17087
|
+
authConfig: "/v1/auth/config",
|
|
17088
|
+
hotelSearch: "/v1/hotels/search",
|
|
17089
|
+
hotelOffers: "/v1/hotels/offers",
|
|
17090
|
+
orders: "/v1/orders",
|
|
17091
|
+
hotel(id, opts) {
|
|
17092
|
+
const query = [];
|
|
17093
|
+
if (opts?.rooms) query.push("include=rooms");
|
|
17094
|
+
if (opts?.locale) query.push(`locale=${opts.locale}`);
|
|
17095
|
+
return `/v1/hotels/${encodeURIComponent(id)}${query.length ? `?${query.join("&")}` : ""}`;
|
|
17096
|
+
},
|
|
17097
|
+
order(id, opts) {
|
|
17098
|
+
return `/v1/orders/${encodeURIComponent(id)}${opts?.locale ? `?locale=${opts.locale}` : ""}`;
|
|
17099
|
+
}
|
|
17100
|
+
};
|
|
17101
|
+
var MCP_PATH = "/";
|
|
17102
|
+
|
|
15544
17103
|
// src/errors.ts
|
|
17104
|
+
import { randomBytes } from "crypto";
|
|
15545
17105
|
var EXIT = {
|
|
15546
17106
|
ok: 0,
|
|
15547
17107
|
error: 1,
|
|
@@ -15550,18 +17110,72 @@ var EXIT = {
|
|
|
15550
17110
|
rateLimited: 4
|
|
15551
17111
|
};
|
|
15552
17112
|
var CliError = class extends Error {
|
|
15553
|
-
constructor(message, exitCode, details = {}) {
|
|
17113
|
+
constructor(message, exitCode, details = {}, envelope) {
|
|
15554
17114
|
super(message);
|
|
15555
17115
|
this.exitCode = exitCode;
|
|
15556
17116
|
this.details = details;
|
|
17117
|
+
this.envelope = envelope;
|
|
15557
17118
|
}
|
|
15558
17119
|
exitCode;
|
|
15559
17120
|
details;
|
|
17121
|
+
envelope;
|
|
15560
17122
|
};
|
|
17123
|
+
function localRequestId() {
|
|
17124
|
+
return `local_${randomBytes(10).toString("hex")}`;
|
|
17125
|
+
}
|
|
15561
17126
|
var notices = {};
|
|
15562
17127
|
function usageError(message) {
|
|
15563
17128
|
return new CliError(message, EXIT.usage);
|
|
15564
17129
|
}
|
|
17130
|
+
function fieldLines(fields) {
|
|
17131
|
+
return Object.entries(fields).filter(([, value]) => value !== void 0).map(([name, value]) => `${name}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
17132
|
+
}
|
|
17133
|
+
var ERROR_FIELDS = /* @__PURE__ */ new Set(["retryable", "retryAfterSeconds", "details"]);
|
|
17134
|
+
function localCode(failure) {
|
|
17135
|
+
switch (failure.exitCode) {
|
|
17136
|
+
case EXIT.usage:
|
|
17137
|
+
return "invalid_request";
|
|
17138
|
+
case EXIT.unauthenticated:
|
|
17139
|
+
return "unauthenticated";
|
|
17140
|
+
case EXIT.rateLimited:
|
|
17141
|
+
return "rate_limited";
|
|
17142
|
+
default:
|
|
17143
|
+
return failure.details.retryable === true ? "service_unavailable" : "cli_error";
|
|
17144
|
+
}
|
|
17145
|
+
}
|
|
17146
|
+
function issueLines(details) {
|
|
17147
|
+
const issues = details?.issues;
|
|
17148
|
+
if (!Array.isArray(issues)) return null;
|
|
17149
|
+
return issues.map((issue2) => `- ${issue2.pointer}: ${issue2.message}`);
|
|
17150
|
+
}
|
|
17151
|
+
function formatError2(failure, json2) {
|
|
17152
|
+
const fromApi = failure.envelope?.error;
|
|
17153
|
+
const inError = (name) => fromApi ? Object.hasOwn(fromApi, name) : ERROR_FIELDS.has(name);
|
|
17154
|
+
const own = Object.entries(failure.details);
|
|
17155
|
+
if (json2) {
|
|
17156
|
+
const error51 = fromApi ?? {
|
|
17157
|
+
code: localCode(failure),
|
|
17158
|
+
message: failure.message,
|
|
17159
|
+
retryable: failure.details.retryable === true,
|
|
17160
|
+
requestId: localRequestId(),
|
|
17161
|
+
...Object.fromEntries(own.filter(([n]) => inError(n) && n !== "retryable"))
|
|
17162
|
+
};
|
|
17163
|
+
return JSON.stringify({ error: error51, ...Object.fromEntries(own.filter(([n]) => !inError(n))), ...notices });
|
|
17164
|
+
}
|
|
17165
|
+
const lines = [];
|
|
17166
|
+
if (fromApi) {
|
|
17167
|
+
const status = failure.details.status;
|
|
17168
|
+
const known = Object.hasOwn(ERROR_CODES, fromApi.code);
|
|
17169
|
+
lines.push(renderError(failure.envelope, known || typeof status !== "number" ? void 0 : status));
|
|
17170
|
+
if (failure.message !== fromApi.message) lines.push(failure.message);
|
|
17171
|
+
lines.push(...fieldLines(Object.fromEntries(own.filter(([n]) => !inError(n) && n !== "status"))));
|
|
17172
|
+
} else {
|
|
17173
|
+
const { details, ...facts } = failure.details;
|
|
17174
|
+
lines.push(`Error: ${failure.message}`, ...issueLines(details) ?? fieldLines({ details }), ...fieldLines(facts));
|
|
17175
|
+
}
|
|
17176
|
+
lines.push(...fieldLines(notices));
|
|
17177
|
+
return lines.join("\n");
|
|
17178
|
+
}
|
|
15565
17179
|
|
|
15566
17180
|
// src/version.ts
|
|
15567
17181
|
import { readFileSync } from "fs";
|
|
@@ -15603,16 +17217,11 @@ async function request(apiUrl, path, {
|
|
|
15603
17217
|
} catch (error51) {
|
|
15604
17218
|
throw transportError(apiUrl, error51, response.status);
|
|
15605
17219
|
}
|
|
15606
|
-
let parsed = null;
|
|
15607
17220
|
try {
|
|
15608
|
-
|
|
17221
|
+
return { status: response.status, body: JSON.parse(text2), headers: response.headers };
|
|
15609
17222
|
} catch {
|
|
15610
|
-
|
|
17223
|
+
return { status: response.status, body: null, headers: response.headers, raw: text2.slice(0, 200) };
|
|
15611
17224
|
}
|
|
15612
|
-
const isObject3 = parsed !== null && typeof parsed === "object";
|
|
15613
|
-
const result = isObject3 ? parsed : { error: `Unexpected ${response.status} response`, raw: text2.slice(0, 200) };
|
|
15614
|
-
if (response.status >= 500 && !(isObject3 && text2)) result.retryable = true;
|
|
15615
|
-
return { status: response.status, body: result, headers: response.headers };
|
|
15616
17225
|
}
|
|
15617
17226
|
async function postForm(url2, form, timeoutMs = OAUTH_TIMEOUT_MS) {
|
|
15618
17227
|
const { origin } = new URL(url2);
|
|
@@ -15653,34 +17262,48 @@ function transportError(apiUrl, error51, status, timeoutMs = REQUEST_TIMEOUT_MS)
|
|
|
15653
17262
|
retryable: true
|
|
15654
17263
|
});
|
|
15655
17264
|
}
|
|
15656
|
-
function
|
|
15657
|
-
const
|
|
15658
|
-
|
|
15659
|
-
|
|
15660
|
-
|
|
15661
|
-
|
|
15662
|
-
|
|
15663
|
-
status,
|
|
15664
|
-
...rest,
|
|
15665
|
-
...Number.isFinite(retryAfter) && retryAfter > 0 ? { retryAfterSeconds: retryAfter } : {}
|
|
15666
|
-
};
|
|
15667
|
-
if (status === 401) {
|
|
15668
|
-
throw new CliError(`${text2}. ${LOGIN_HINT}`, EXIT.unauthenticated, details);
|
|
17265
|
+
function isEnvelope(body) {
|
|
17266
|
+
const error51 = body?.error;
|
|
17267
|
+
return typeof error51 === "object" && error51 !== null && typeof error51.code === "string";
|
|
17268
|
+
}
|
|
17269
|
+
function exitCodeFor(code, status) {
|
|
17270
|
+
if (code !== void 0 && Object.hasOwn(ERROR_CODES, code)) {
|
|
17271
|
+
return code === "invalid_request" ? EXIT.usage : code === "unauthenticated" ? EXIT.unauthenticated : code === "rate_limited" ? EXIT.rateLimited : EXIT.error;
|
|
15669
17272
|
}
|
|
15670
|
-
|
|
15671
|
-
|
|
17273
|
+
return status === 400 ? EXIT.usage : status === 401 ? EXIT.unauthenticated : status === 429 ? EXIT.rateLimited : EXIT.error;
|
|
17274
|
+
}
|
|
17275
|
+
function unwrap(response) {
|
|
17276
|
+
const { status, body, headers, raw: raw2 } = response;
|
|
17277
|
+
if (status >= 200 && status < 300 && body !== null) return body;
|
|
15672
17278
|
if (status >= 300 && status < 400) {
|
|
15673
17279
|
throw new CliError(
|
|
15674
17280
|
`Unexpected redirect to ${headers.get("location") ?? "?"}; check FORTRIP_API_URL`,
|
|
15675
17281
|
EXIT.error,
|
|
15676
|
-
|
|
17282
|
+
{ status }
|
|
17283
|
+
);
|
|
17284
|
+
}
|
|
17285
|
+
if (isEnvelope(body)) {
|
|
17286
|
+
const { code, message, retryable } = body.error;
|
|
17287
|
+
const text2 = typeof message === "string" ? message : `Request failed with status ${status}`;
|
|
17288
|
+
const exitCode = exitCodeFor(code, status);
|
|
17289
|
+
throw new CliError(
|
|
17290
|
+
exitCode === EXIT.unauthenticated ? LOGIN_HINT : text2,
|
|
17291
|
+
exitCode,
|
|
17292
|
+
{ status, ...retryable === true ? { retryable } : {} },
|
|
17293
|
+
body
|
|
15677
17294
|
);
|
|
15678
17295
|
}
|
|
15679
|
-
|
|
17296
|
+
const retryAfter = Number(headers.get("retry-after"));
|
|
17297
|
+
throw new CliError(`Unexpected ${status} answer from the API`, exitCodeFor(void 0, status), {
|
|
17298
|
+
status,
|
|
17299
|
+
...status >= 500 ? { retryable: true } : {},
|
|
17300
|
+
...Number.isFinite(retryAfter) && retryAfter > 0 ? { retryAfterSeconds: retryAfter } : {},
|
|
17301
|
+
...raw2 ? { raw: raw2 } : {}
|
|
17302
|
+
});
|
|
15680
17303
|
}
|
|
15681
17304
|
|
|
15682
17305
|
// src/credentials.ts
|
|
15683
|
-
import { randomBytes } from "crypto";
|
|
17306
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
15684
17307
|
import { readlinkSync } from "fs";
|
|
15685
17308
|
import {
|
|
15686
17309
|
chmod,
|
|
@@ -15695,7 +17318,8 @@ import {
|
|
|
15695
17318
|
import { homedir, hostname as hostname3 } from "os";
|
|
15696
17319
|
import { basename, dirname, join } from "path";
|
|
15697
17320
|
import { setTimeout as sleep } from "timers/promises";
|
|
15698
|
-
var DEFAULT_API_URL =
|
|
17321
|
+
var DEFAULT_API_URL = PUBLIC_ORIGINS.api;
|
|
17322
|
+
var DEFAULT_MCP_URL = `${PUBLIC_ORIGINS.mcp}${MCP_PATH}`;
|
|
15699
17323
|
var CREDENTIALS_LOCK_STALE_MS = 1e4;
|
|
15700
17324
|
var LOGIN_LOCK_STALE_MS = REQUEST_TIMEOUT_MS + 3e4;
|
|
15701
17325
|
var TAKEOVER_STALE_MS = 2e3;
|
|
@@ -15705,45 +17329,64 @@ function credentialsPath(env = process.env) {
|
|
|
15705
17329
|
const base = env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
15706
17330
|
return join(base, "fortrip", "credentials.json");
|
|
15707
17331
|
}
|
|
15708
|
-
function
|
|
15709
|
-
const raw = env.FORTRIP_API_URL?.trim() || DEFAULT_API_URL;
|
|
17332
|
+
function vettedUrl(name, raw2) {
|
|
15710
17333
|
let url2;
|
|
15711
17334
|
try {
|
|
15712
|
-
url2 = new URL(
|
|
17335
|
+
url2 = new URL(raw2);
|
|
15713
17336
|
} catch {
|
|
15714
|
-
throw usageError(
|
|
17337
|
+
throw usageError(`${name} is not a valid URL: ${raw2}`);
|
|
15715
17338
|
}
|
|
15716
17339
|
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname);
|
|
15717
17340
|
if (url2.protocol !== "https:" && !(url2.protocol === "http:" && isLocal)) {
|
|
15718
|
-
throw usageError(
|
|
17341
|
+
throw usageError(`${name} must use https (http only for localhost)`);
|
|
15719
17342
|
}
|
|
15720
|
-
return url2
|
|
17343
|
+
return url2;
|
|
17344
|
+
}
|
|
17345
|
+
function resolveApiUrl(env = process.env) {
|
|
17346
|
+
const url2 = vettedUrl("FORTRIP_API_URL", env.FORTRIP_API_URL?.trim() || DEFAULT_API_URL);
|
|
17347
|
+
return `${url2.origin}${url2.pathname.replace(/\/+$/, "")}`;
|
|
17348
|
+
}
|
|
17349
|
+
function resolveMcpUrl(apiUrl, env = process.env) {
|
|
17350
|
+
const given = env.FORTRIP_MCP_URL?.trim();
|
|
17351
|
+
const url2 = vettedUrl("FORTRIP_MCP_URL", given || DEFAULT_MCP_URL);
|
|
17352
|
+
const mcpUrl = `${url2.origin}${url2.pathname}`;
|
|
17353
|
+
if (apiUrl === DEFAULT_API_URL) {
|
|
17354
|
+
if (mcpUrl === DEFAULT_MCP_URL) return mcpUrl;
|
|
17355
|
+
throw usageError(
|
|
17356
|
+
`FORTRIP_MCP_URL must be ${DEFAULT_MCP_URL} while FORTRIP_API_URL is ${DEFAULT_API_URL}: the sign-in with ${DEFAULT_API_URL} is sent to no other server`
|
|
17357
|
+
);
|
|
17358
|
+
}
|
|
17359
|
+
const { origin } = new URL(apiUrl);
|
|
17360
|
+
if (url2.origin === origin) return mcpUrl;
|
|
17361
|
+
throw usageError(
|
|
17362
|
+
given ? `FORTRIP_MCP_URL must be on ${origin}, the origin of FORTRIP_API_URL: the sign-in with ${apiUrl} is sent to no other server` : `FORTRIP_API_URL is set, so set FORTRIP_MCP_URL too, to the MCP server on ${origin}: the sign-in with ${apiUrl} is sent to no other server`
|
|
17363
|
+
);
|
|
15721
17364
|
}
|
|
15722
|
-
function
|
|
17365
|
+
function isObject3(value) {
|
|
15723
17366
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15724
17367
|
}
|
|
15725
17368
|
function isLogin(value) {
|
|
15726
|
-
return
|
|
17369
|
+
return isObject3(value) && typeof value.deviceCode === "string" && typeof value.userCode === "string" && typeof value.loginUrl === "string" && typeof value.expiresAt === "string";
|
|
15727
17370
|
}
|
|
15728
17371
|
function isSession(value) {
|
|
15729
|
-
return
|
|
17372
|
+
return isObject3(value) && typeof value.accessToken === "string" && typeof value.refreshToken === "string" && typeof value.expiresAt === "number" && typeof value.clientId === "string";
|
|
15730
17373
|
}
|
|
15731
17374
|
function parse3(text2) {
|
|
15732
|
-
let
|
|
17375
|
+
let raw2;
|
|
15733
17376
|
try {
|
|
15734
|
-
|
|
17377
|
+
raw2 = JSON.parse(text2);
|
|
15735
17378
|
} catch {
|
|
15736
17379
|
return null;
|
|
15737
17380
|
}
|
|
15738
|
-
if (!
|
|
15739
|
-
const { version: version2, sessions, pendingLogins, ...other } =
|
|
17381
|
+
if (!isObject3(raw2)) return null;
|
|
17382
|
+
const { version: version2, sessions, pendingLogins, ...other } = raw2;
|
|
15740
17383
|
if (version2 === void 0) return emptyCredentials();
|
|
15741
17384
|
if (version2 !== 2) return null;
|
|
15742
17385
|
const credentials = { sessions: {}, logins: {}, other };
|
|
15743
|
-
for (const [apiUrl, session] of Object.entries(
|
|
17386
|
+
for (const [apiUrl, session] of Object.entries(isObject3(sessions) ? sessions : {})) {
|
|
15744
17387
|
if (isSession(session)) credentials.sessions[apiUrl] = session;
|
|
15745
17388
|
}
|
|
15746
|
-
for (const [apiUrl, list] of Object.entries(
|
|
17389
|
+
for (const [apiUrl, list] of Object.entries(isObject3(pendingLogins) ? pendingLogins : {})) {
|
|
15747
17390
|
credentials.logins[apiUrl] = (Array.isArray(list) ? list : []).filter(isLogin);
|
|
15748
17391
|
}
|
|
15749
17392
|
return credentials;
|
|
@@ -15782,11 +17425,11 @@ async function readCredentials(path) {
|
|
|
15782
17425
|
function emptyCredentials() {
|
|
15783
17426
|
return { sessions: {}, logins: {}, other: {} };
|
|
15784
17427
|
}
|
|
15785
|
-
async function updateCredentials(env,
|
|
17428
|
+
async function updateCredentials(env, change2) {
|
|
15786
17429
|
const path = credentialsPath(env);
|
|
15787
17430
|
return withLock(path, async () => {
|
|
15788
17431
|
const { text: text2, credentials, recovered } = await readCredentials(path);
|
|
15789
|
-
const result =
|
|
17432
|
+
const result = change2(credentials);
|
|
15790
17433
|
const next = serialize(credentials);
|
|
15791
17434
|
if (next !== text2) await replace(path, next);
|
|
15792
17435
|
for (const file2 of recovered) await rm(file2, { force: true });
|
|
@@ -15804,9 +17447,9 @@ function recoveryPrefix(path) {
|
|
|
15804
17447
|
}
|
|
15805
17448
|
async function readRecovered(path) {
|
|
15806
17449
|
const prefix = recoveryPrefix(path);
|
|
15807
|
-
const
|
|
17450
|
+
const names2 = (await readdir(dirname(path)).catch(() => [])).filter((name) => name.startsWith(prefix) && name.endsWith(".json")).sort();
|
|
15808
17451
|
const recovered = [];
|
|
15809
|
-
for (const name of
|
|
17452
|
+
for (const name of names2) {
|
|
15810
17453
|
const file2 = join(dirname(path), name);
|
|
15811
17454
|
const parsed = parse3(await readText(file2).catch(() => null) ?? "");
|
|
15812
17455
|
if (parsed) recovered.push({ file: file2, sessions: parsed.sessions });
|
|
@@ -15819,7 +17462,7 @@ async function loadRecoveredSessions(apiUrl, env = process.env) {
|
|
|
15819
17462
|
);
|
|
15820
17463
|
}
|
|
15821
17464
|
async function replace(path, text2) {
|
|
15822
|
-
const temp = `${path}.${process.pid}.${
|
|
17465
|
+
const temp = `${path}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
15823
17466
|
try {
|
|
15824
17467
|
await writeFile(temp, text2, { mode: 384, flag: "wx" });
|
|
15825
17468
|
await chmod(temp, 384);
|
|
@@ -15907,7 +17550,7 @@ function newOwner() {
|
|
|
15907
17550
|
pid: process.pid,
|
|
15908
17551
|
host: hostname3(),
|
|
15909
17552
|
pidns: PID_NAMESPACE,
|
|
15910
|
-
id:
|
|
17553
|
+
id: randomBytes2(8).toString("hex")
|
|
15911
17554
|
});
|
|
15912
17555
|
}
|
|
15913
17556
|
async function tryLock(path, staleMs) {
|
|
@@ -15977,7 +17620,7 @@ function isAbandoned(path, { content, mtimeMs }, staleMs) {
|
|
|
15977
17620
|
function parseOwner(content) {
|
|
15978
17621
|
try {
|
|
15979
17622
|
const owner = JSON.parse(content);
|
|
15980
|
-
return
|
|
17623
|
+
return isObject3(owner) && Number.isSafeInteger(owner.pid) && owner.pid > 0 && typeof owner.host === "string" && typeof owner.pidns === "string" ? owner : null;
|
|
15981
17624
|
} catch {
|
|
15982
17625
|
return null;
|
|
15983
17626
|
}
|
|
@@ -16010,7 +17653,7 @@ async function saveSession(apiUrl, session, env = process.env, replacing) {
|
|
|
16010
17653
|
} catch (error51) {
|
|
16011
17654
|
const kept = join(
|
|
16012
17655
|
dirname(path),
|
|
16013
|
-
`${recoveryPrefix(path)}${Date.now()}-${
|
|
17656
|
+
`${recoveryPrefix(path)}${Date.now()}-${randomBytes2(3).toString("hex")}.json`
|
|
16014
17657
|
);
|
|
16015
17658
|
const content = `${JSON.stringify({ version: 2, sessions: { [apiUrl]: session } }, null, 2)}
|
|
16016
17659
|
`;
|
|
@@ -16277,7 +17920,8 @@ async function authorizedRequest(apiUrl, path, init = {}) {
|
|
|
16277
17920
|
throw new CliError(
|
|
16278
17921
|
`${apiUrl} didn't accept FORTRIP_TOKEN. If this keeps happening, the user should replace it, or unset it to sign in through the browser.`,
|
|
16279
17922
|
EXIT.unauthenticated,
|
|
16280
|
-
{ status: 401, tokenSource: "FORTRIP_TOKEN" }
|
|
17923
|
+
{ status: 401, tokenSource: "FORTRIP_TOKEN" },
|
|
17924
|
+
isEnvelope(response.body) ? response.body : void 0
|
|
16281
17925
|
);
|
|
16282
17926
|
}
|
|
16283
17927
|
if (!auth.fresh) {
|
|
@@ -16461,6 +18105,7 @@ async function whoami(apiUrl, token) {
|
|
|
16461
18105
|
}
|
|
16462
18106
|
|
|
16463
18107
|
export {
|
|
18108
|
+
LIMITS,
|
|
16464
18109
|
COMMAND_GROUPS,
|
|
16465
18110
|
COMMANDS,
|
|
16466
18111
|
commandInvocation,
|
|
@@ -16468,14 +18113,23 @@ export {
|
|
|
16468
18113
|
renderGuide,
|
|
16469
18114
|
API,
|
|
16470
18115
|
TOOLS,
|
|
18116
|
+
renderError,
|
|
18117
|
+
renderToolResult,
|
|
16471
18118
|
CONTRACT_VERSION,
|
|
16472
18119
|
EXIT,
|
|
16473
18120
|
CliError,
|
|
18121
|
+
localRequestId,
|
|
16474
18122
|
notices,
|
|
16475
18123
|
usageError,
|
|
18124
|
+
fieldLines,
|
|
18125
|
+
formatError2 as formatError,
|
|
16476
18126
|
VERSION,
|
|
18127
|
+
isEnvelope,
|
|
16477
18128
|
unwrap,
|
|
18129
|
+
DEFAULT_API_URL,
|
|
18130
|
+
DEFAULT_MCP_URL,
|
|
16478
18131
|
resolveApiUrl,
|
|
18132
|
+
resolveMcpUrl,
|
|
16479
18133
|
loadSession,
|
|
16480
18134
|
requireToken,
|
|
16481
18135
|
refreshSession,
|