imprint-mcp 0.2.1 → 0.3.1
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 +193 -189
- package/examples/discoverandgo/README.md +1 -1
- package/examples/echo/README.md +1 -1
- package/examples/google-flights/README.md +28 -0
- package/examples/google-flights/_shared/batchexecute.ts +63 -0
- package/examples/google-flights/_shared/flights_request.ts +95 -0
- package/examples/google-flights/_shared/package.json +9 -0
- package/examples/google-flights/get_flight_booking_details/index.ts +159 -0
- package/examples/google-flights/get_flight_booking_details/package.json +9 -0
- package/examples/google-flights/get_flight_booking_details/parser.ts +182 -0
- package/examples/google-flights/get_flight_booking_details/playbook.yaml +138 -0
- package/examples/google-flights/get_flight_booking_details/request-transform.ts +86 -0
- package/examples/google-flights/get_flight_booking_details/workflow.json +98 -0
- package/examples/google-flights/get_flight_calendar_prices/index.ts +131 -0
- package/examples/google-flights/get_flight_calendar_prices/package.json +9 -0
- package/examples/google-flights/get_flight_calendar_prices/parser.ts +86 -0
- package/examples/google-flights/get_flight_calendar_prices/playbook.yaml +97 -0
- package/examples/google-flights/get_flight_calendar_prices/request-transform.ts +31 -0
- package/examples/google-flights/get_flight_calendar_prices/workflow.json +78 -0
- package/examples/google-flights/lookup_airport/index.ts +101 -0
- package/examples/google-flights/lookup_airport/package.json +9 -0
- package/examples/google-flights/lookup_airport/parser.ts +66 -0
- package/examples/google-flights/lookup_airport/playbook.yaml +47 -0
- package/examples/google-flights/lookup_airport/request-transform.ts +20 -0
- package/examples/google-flights/lookup_airport/workflow.json +57 -0
- package/examples/google-flights/search_flights/index.ts +219 -0
- package/examples/google-flights/search_flights/package.json +9 -0
- package/examples/google-flights/search_flights/parser.ts +169 -0
- package/examples/google-flights/search_flights/playbook.yaml +184 -0
- package/examples/google-flights/search_flights/request-transform.ts +119 -0
- package/examples/google-flights/search_flights/workflow.json +143 -0
- package/examples/google-hotels/README.md +29 -0
- package/examples/google-hotels/_shared/batchexecute.ts +73 -0
- package/examples/google-hotels/_shared/freq.ts +158 -0
- package/examples/google-hotels/_shared/package.json +9 -0
- package/examples/google-hotels/autocomplete_hotel_location/index.ts +80 -0
- package/examples/google-hotels/autocomplete_hotel_location/package.json +9 -0
- package/examples/google-hotels/autocomplete_hotel_location/parser.ts +71 -0
- package/examples/google-hotels/autocomplete_hotel_location/playbook.yaml +36 -0
- package/examples/google-hotels/autocomplete_hotel_location/request-transform.ts +37 -0
- package/examples/google-hotels/autocomplete_hotel_location/workflow.json +36 -0
- package/examples/google-hotels/get_hotel_booking_options/index.ts +143 -0
- package/examples/google-hotels/get_hotel_booking_options/package.json +9 -0
- package/examples/google-hotels/get_hotel_booking_options/parser.ts +271 -0
- package/examples/google-hotels/get_hotel_booking_options/playbook.yaml +154 -0
- package/examples/google-hotels/get_hotel_booking_options/request-transform.ts +154 -0
- package/examples/google-hotels/get_hotel_booking_options/workflow.json +84 -0
- package/examples/google-hotels/get_hotel_reviews/index.ts +81 -0
- package/examples/google-hotels/get_hotel_reviews/package.json +9 -0
- package/examples/google-hotels/get_hotel_reviews/parser.ts +128 -0
- package/examples/google-hotels/get_hotel_reviews/playbook.yaml +64 -0
- package/examples/google-hotels/get_hotel_reviews/request-transform.ts +42 -0
- package/examples/google-hotels/get_hotel_reviews/workflow.json +37 -0
- package/examples/google-hotels/search_hotels/index.ts +207 -0
- package/examples/google-hotels/search_hotels/package.json +9 -0
- package/examples/google-hotels/search_hotels/parser.ts +260 -0
- package/examples/google-hotels/search_hotels/playbook.yaml +87 -0
- package/examples/google-hotels/search_hotels/request-transform.ts +197 -0
- package/examples/google-hotels/search_hotels/workflow.json +127 -0
- package/examples/southwest/README.md +3 -2
- package/examples/southwest/search_southwest_flights/index.ts +18 -1
- package/examples/southwest/search_southwest_flights/workflow.json +18 -1
- package/package.json +3 -2
- package/prompts/audit-agent.md +71 -0
- package/prompts/build-planning.md +74 -0
- package/prompts/compile-agent.md +131 -27
- package/prompts/prereq-builder.md +64 -0
- package/prompts/prereq-planner.md +34 -0
- package/prompts/tool-planning.md +39 -0
- package/src/cli.ts +116 -3
- package/src/imprint/agent.ts +5 -0
- package/src/imprint/audit.ts +996 -0
- package/src/imprint/backend-ladder.ts +1214 -184
- package/src/imprint/build-plan.ts +1051 -0
- package/src/imprint/cdp-browser-fetch.ts +592 -0
- package/src/imprint/cdp-jar-cache.ts +320 -0
- package/src/imprint/chromium.ts +414 -8
- package/src/imprint/claude-cli-compile.ts +125 -25
- package/src/imprint/codex-cli-compile.ts +26 -23
- package/src/imprint/compile-agent-types.ts +38 -0
- package/src/imprint/compile-agent.ts +63 -25
- package/src/imprint/compile-tools.ts +1666 -66
- package/src/imprint/compile.ts +13 -1
- package/src/imprint/concurrency.ts +87 -0
- package/src/imprint/cron.ts +4 -0
- package/src/imprint/doctor.ts +48 -3
- package/src/imprint/freeform-redact.ts +5 -4
- package/src/imprint/install.ts +79 -4
- package/src/imprint/integrations.ts +3 -3
- package/src/imprint/llm.ts +56 -8
- package/src/imprint/mcp-compile-server.ts +43 -10
- package/src/imprint/mcp-maintenance.ts +18 -102
- package/src/imprint/mcp-server.ts +73 -7
- package/src/imprint/multi-progress.ts +7 -2
- package/src/imprint/param-grounding.ts +367 -0
- package/src/imprint/paths.ts +29 -0
- package/src/imprint/playbook-runner.ts +101 -40
- package/src/imprint/prereq-builder.ts +651 -0
- package/src/imprint/probe-backends.ts +6 -3
- package/src/imprint/record.ts +10 -1
- package/src/imprint/redact.ts +30 -2
- package/src/imprint/replay-capture.ts +19 -18
- package/src/imprint/runtime.ts +19 -10
- package/src/imprint/session-diff.ts +79 -2
- package/src/imprint/session-merge.ts +9 -5
- package/src/imprint/stealth-chromium.ts +79 -0
- package/src/imprint/stealth-fetch.ts +309 -29
- package/src/imprint/stealth-token-cache.ts +88 -0
- package/src/imprint/teach-plan.ts +251 -0
- package/src/imprint/teach-state.ts +10 -0
- package/src/imprint/teach.ts +456 -142
- package/src/imprint/tool-candidates.ts +72 -14
- package/src/imprint/tool-plan.ts +313 -0
- package/src/imprint/tracing.ts +135 -6
- package/src/imprint/types.ts +61 -3
- package/examples/google-flights/search_google_flights/index.ts +0 -101
- package/examples/google-flights/search_google_flights/parser.test.ts +0 -140
- package/examples/google-flights/search_google_flights/parser.ts +0 -189
- package/examples/google-flights/search_google_flights/playbook.yaml +0 -130
- package/examples/google-flights/search_google_flights/workflow.json +0 -48
- package/examples/google-hotels/search_google_hotels/index.ts +0 -194
- package/examples/google-hotels/search_google_hotels/parser.test.ts +0 -168
- package/examples/google-hotels/search_google_hotels/parser.ts +0 -330
- package/examples/google-hotels/search_google_hotels/playbook.yaml +0 -125
- package/examples/google-hotels/search_google_hotels/workflow.json +0 -111
- package/examples/namecheap-domains/search_namecheap_domains/index.ts +0 -144
- package/examples/namecheap-domains/search_namecheap_domains/parser.ts +0 -380
- package/examples/namecheap-domains/search_namecheap_domains/playbook.yaml +0 -50
- package/examples/namecheap-domains/search_namecheap_domains/request-transform.ts +0 -136
- package/examples/namecheap-domains/search_namecheap_domains/workflow.json +0 -97
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Parser for google-hotels get_hotel_booking_options (M0CRd pricing response).
|
|
2
|
+
// Decodes the batchexecute anti-XSSI envelope with the SHARED helper, then
|
|
3
|
+
// normalizes BOTH M0CRd response shapes into one `hotels[]` output:
|
|
4
|
+
//
|
|
5
|
+
// * single-hotel mode (live booking-options call, [2][1] is a price tuple):
|
|
6
|
+
// one hotel entry carrying its per-provider booking `offers[]`.
|
|
7
|
+
// * area-list mode (recorded seq 286/2083, hotels at <node>[9]):
|
|
8
|
+
// many hotel entries, each with its summary nightly + stay-total price.
|
|
9
|
+
//
|
|
10
|
+
// Each hotel: name, class, rating, price_nightly, price_total, ftid, offers[].
|
|
11
|
+
import { parseBatchExecute } from '../_shared/batchexecute.ts';
|
|
12
|
+
|
|
13
|
+
type PriceTuple = {
|
|
14
|
+
display: string | null; // base label, e.g. "$254"
|
|
15
|
+
display_with_fees: string | null; // incl. taxes/fees, e.g. "$301"
|
|
16
|
+
amount: number | null; // numeric base
|
|
17
|
+
amount_with_fees: number | null; // numeric incl. taxes/fees
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Offer = {
|
|
21
|
+
provider: string;
|
|
22
|
+
booking_url: string | null;
|
|
23
|
+
price_nightly: PriceTuple | null;
|
|
24
|
+
price_total: PriceTuple | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type Hotel = {
|
|
28
|
+
name: string;
|
|
29
|
+
hotel_class: string | null;
|
|
30
|
+
star_rating: number | null;
|
|
31
|
+
rating: number | null;
|
|
32
|
+
reviews: number | null;
|
|
33
|
+
price_nightly: PriceTuple | null;
|
|
34
|
+
price_total: PriceTuple | null;
|
|
35
|
+
ftid: string | null;
|
|
36
|
+
description: string | null;
|
|
37
|
+
coordinates: { lat: number; lng: number } | null;
|
|
38
|
+
offers: Offer[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function priceFromTuple(t: unknown): PriceTuple | null {
|
|
42
|
+
if (!Array.isArray(t)) return null;
|
|
43
|
+
if (typeof t[0] !== 'string' && typeof t[2] !== 'number') return null;
|
|
44
|
+
return {
|
|
45
|
+
display: typeof t[0] === 'string' ? t[0] : null,
|
|
46
|
+
display_with_fees: typeof t[1] === 'string' ? t[1] : null,
|
|
47
|
+
amount: typeof t[2] === 'number' ? t[2] : null,
|
|
48
|
+
amount_with_fees: typeof t[3] === 'number' ? t[3] : null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function absoluteUrl(u: string): string {
|
|
53
|
+
if (u.startsWith('http')) return u;
|
|
54
|
+
if (u.startsWith('/')) return 'https://www.google.com' + u;
|
|
55
|
+
return u;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Booking-offer entry:
|
|
59
|
+
// [ [providerName, partnerId, bookingUrl, [logo], …], // e[0]
|
|
60
|
+
// null × 11,
|
|
61
|
+
// [null,null,null,null, nightlyTuple, stayTotalTuple, …], // e[12]
|
|
62
|
+
// … ]
|
|
63
|
+
function looksLikeOffer(e: any): boolean {
|
|
64
|
+
return (
|
|
65
|
+
Array.isArray(e) &&
|
|
66
|
+
Array.isArray(e[0]) &&
|
|
67
|
+
typeof e[0][0] === 'string' &&
|
|
68
|
+
e[0][0].length > 0 &&
|
|
69
|
+
typeof e[0][2] === 'string' &&
|
|
70
|
+
Array.isArray(e[12]) &&
|
|
71
|
+
Array.isArray(e[12][4]) &&
|
|
72
|
+
(typeof e[12][4][0] === 'string' || typeof e[12][4][2] === 'number')
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function collectOffers(node: any, out: Offer[], seen: Set<unknown>): void {
|
|
77
|
+
if (!node || typeof node !== 'object') return;
|
|
78
|
+
if (seen.has(node)) return;
|
|
79
|
+
seen.add(node);
|
|
80
|
+
if (Array.isArray(node)) {
|
|
81
|
+
if (looksLikeOffer(node)) {
|
|
82
|
+
const url = typeof node[0][2] === 'string' ? node[0][2] : null;
|
|
83
|
+
out.push({
|
|
84
|
+
provider: node[0][0],
|
|
85
|
+
booking_url: url ? absoluteUrl(url) : null,
|
|
86
|
+
price_nightly: priceFromTuple(node[12][4]),
|
|
87
|
+
price_total: priceFromTuple(node[12][5]),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
for (const child of node) collectOffers(child, out, seen);
|
|
91
|
+
} else {
|
|
92
|
+
for (const k of Object.keys(node)) collectOffers(node[k], out, seen);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function dedupeOffers(offers: Offer[]): Offer[] {
|
|
97
|
+
const byProvider = new Map<string, Offer>();
|
|
98
|
+
for (const o of offers) {
|
|
99
|
+
const prev = byProvider.get(o.provider);
|
|
100
|
+
if (!prev) {
|
|
101
|
+
byProvider.set(o.provider, o);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const a = o.price_nightly?.amount;
|
|
105
|
+
const b = prev.price_nightly?.amount;
|
|
106
|
+
if (typeof a === 'number' && (typeof b !== 'number' || a < b)) {
|
|
107
|
+
byProvider.set(o.provider, o);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return Array.from(byProvider.values());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Area-list mode: hotels live in a nested array whose [9] slot holds entries
|
|
114
|
+
// shaped [n, {"<key>": [hotelData]}].
|
|
115
|
+
function findHotelsArray(inner: any[]): any[] {
|
|
116
|
+
for (const el of inner) {
|
|
117
|
+
if (Array.isArray(el) && Array.isArray(el[9])) {
|
|
118
|
+
const cand = el[9];
|
|
119
|
+
if (
|
|
120
|
+
cand.length &&
|
|
121
|
+
Array.isArray(cand[0]) &&
|
|
122
|
+
cand[0].length >= 2 &&
|
|
123
|
+
cand[0][1] &&
|
|
124
|
+
typeof cand[0][1] === 'object'
|
|
125
|
+
) {
|
|
126
|
+
return cand;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function toInner(rawResponse: unknown): any {
|
|
134
|
+
if (typeof rawResponse === 'string') return parseBatchExecute(rawResponse, 'M0CRd');
|
|
135
|
+
return rawResponse;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function extract(
|
|
139
|
+
rawResponse: unknown,
|
|
140
|
+
context?: {
|
|
141
|
+
params: Record<string, string | number | boolean>;
|
|
142
|
+
responses: unknown[];
|
|
143
|
+
},
|
|
144
|
+
): unknown {
|
|
145
|
+
const inner = toInner(rawResponse);
|
|
146
|
+
|
|
147
|
+
const priceMode =
|
|
148
|
+
context?.params && typeof context.params.price_mode === 'string'
|
|
149
|
+
? context.params.price_mode
|
|
150
|
+
: 'nightly';
|
|
151
|
+
|
|
152
|
+
const out = {
|
|
153
|
+
location: { mid: null as string | null, name: null as string | null },
|
|
154
|
+
currency: null as string | null,
|
|
155
|
+
check_in: null as unknown,
|
|
156
|
+
check_out: null as unknown,
|
|
157
|
+
nights: null as number | null,
|
|
158
|
+
adults: null as number | null,
|
|
159
|
+
children: null as number | null,
|
|
160
|
+
price_mode: priceMode,
|
|
161
|
+
mode: 'list' as 'list' | 'single',
|
|
162
|
+
hotels: [] as Hotel[],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (!Array.isArray(inner)) return out;
|
|
166
|
+
|
|
167
|
+
const ctx: any[] = Array.isArray(inner[1]) ? inner[1] : [];
|
|
168
|
+
out.currency = typeof ctx[3] === 'string' ? ctx[3] : null;
|
|
169
|
+
const dates = Array.isArray(ctx[4]) ? ctx[4] : null;
|
|
170
|
+
const occ = Array.isArray(ctx[13]) ? ctx[13] : null;
|
|
171
|
+
const loc = Array.isArray(ctx[18]) ? ctx[18] : [];
|
|
172
|
+
out.location = { mid: loc[0] ?? null, name: loc[1] ?? null };
|
|
173
|
+
out.check_in = dates ? dates[0] : null;
|
|
174
|
+
out.check_out = dates ? dates[1] : null;
|
|
175
|
+
out.nights = dates && typeof dates[2] === 'number' ? dates[2] : null;
|
|
176
|
+
out.adults = occ && typeof occ[0] === 'number' ? occ[0] : null;
|
|
177
|
+
out.children = occ && typeof occ[2] === 'number' ? occ[2] : null;
|
|
178
|
+
|
|
179
|
+
const offersBlock = inner[2];
|
|
180
|
+
const singleHeadline = Array.isArray(offersBlock)
|
|
181
|
+
? priceFromTuple(offersBlock[1])
|
|
182
|
+
: null;
|
|
183
|
+
|
|
184
|
+
if (singleHeadline) {
|
|
185
|
+
// SINGLE-hotel booking-options mode.
|
|
186
|
+
out.mode = 'single';
|
|
187
|
+
let hotelName: string | null = null; // eslint-disable-line prefer-const
|
|
188
|
+
const featured = offersBlock[2];
|
|
189
|
+
if (
|
|
190
|
+
Array.isArray(featured) &&
|
|
191
|
+
Array.isArray(featured[0]) &&
|
|
192
|
+
Array.isArray(featured[0][0]) &&
|
|
193
|
+
typeof featured[0][0][0] === 'string'
|
|
194
|
+
) {
|
|
195
|
+
hotelName = featured[0][0][0];
|
|
196
|
+
}
|
|
197
|
+
const collected: Offer[] = [];
|
|
198
|
+
collectOffers(offersBlock, collected, new Set());
|
|
199
|
+
const offers = dedupeOffers(collected);
|
|
200
|
+
// Prefer a stay-total taken from any offer carrying it.
|
|
201
|
+
const totalFrom = offers.find((o) => o.price_total)?.price_total ?? null;
|
|
202
|
+
// Fallback: if the featured-offer name was not where we expected, use the
|
|
203
|
+
// first collected offer's provider (often the hotel's own official site).
|
|
204
|
+
if (!hotelName && offers.length) hotelName = offers[0]!.provider;
|
|
205
|
+
out.hotels.push({
|
|
206
|
+
name: hotelName ?? '',
|
|
207
|
+
hotel_class: null,
|
|
208
|
+
star_rating: null,
|
|
209
|
+
rating: null,
|
|
210
|
+
reviews: null,
|
|
211
|
+
price_nightly: singleHeadline,
|
|
212
|
+
price_total: totalFrom,
|
|
213
|
+
ftid: null,
|
|
214
|
+
description: null,
|
|
215
|
+
coordinates: null,
|
|
216
|
+
offers,
|
|
217
|
+
});
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// AREA-LIST mode.
|
|
222
|
+
const hotelsRaw = findHotelsArray(inner);
|
|
223
|
+
for (const entry of hotelsRaw) {
|
|
224
|
+
if (!Array.isArray(entry)) continue;
|
|
225
|
+
const obj = entry[1];
|
|
226
|
+
if (!obj || typeof obj !== 'object') continue;
|
|
227
|
+
const keys = Object.keys(obj as Record<string, unknown>);
|
|
228
|
+
if (!keys.length) continue;
|
|
229
|
+
const h = (obj as Record<string, unknown>)[keys[0]!];
|
|
230
|
+
if (!Array.isArray(h)) continue;
|
|
231
|
+
const name = h[1];
|
|
232
|
+
if (typeof name !== 'string' || name.length === 0) continue; // sentinel filter
|
|
233
|
+
|
|
234
|
+
const classInfo = Array.isArray(h[3]) ? h[3] : null;
|
|
235
|
+
let nightly: PriceTuple | null = null;
|
|
236
|
+
let total: PriceTuple | null = null;
|
|
237
|
+
const pricing = h[6];
|
|
238
|
+
if (Array.isArray(pricing) && Array.isArray(pricing[2])) {
|
|
239
|
+
nightly = priceFromTuple(pricing[2][1]);
|
|
240
|
+
total = priceFromTuple(pricing[2][9]);
|
|
241
|
+
}
|
|
242
|
+
const ratingArr =
|
|
243
|
+
Array.isArray(h[7]) && Array.isArray(h[7][0]) ? h[7][0] : null;
|
|
244
|
+
const coordArr =
|
|
245
|
+
Array.isArray(h[2]) && Array.isArray(h[2][0]) ? h[2][0] : null;
|
|
246
|
+
|
|
247
|
+
out.hotels.push({
|
|
248
|
+
name,
|
|
249
|
+
hotel_class:
|
|
250
|
+
classInfo && typeof classInfo[0] === 'string' ? classInfo[0] : null,
|
|
251
|
+
star_rating:
|
|
252
|
+
classInfo && typeof classInfo[1] === 'number' ? classInfo[1] : null,
|
|
253
|
+
rating: ratingArr && typeof ratingArr[0] === 'number' ? ratingArr[0] : null,
|
|
254
|
+
reviews: ratingArr && typeof ratingArr[1] === 'number' ? ratingArr[1] : null,
|
|
255
|
+
price_nightly: nightly,
|
|
256
|
+
price_total: total,
|
|
257
|
+
ftid: typeof h[9] === 'string' ? h[9] : null,
|
|
258
|
+
description:
|
|
259
|
+
Array.isArray(h[11]) && typeof h[11][0] === 'string' ? h[11][0] : null,
|
|
260
|
+
coordinates:
|
|
261
|
+
coordArr &&
|
|
262
|
+
typeof coordArr[0] === 'number' &&
|
|
263
|
+
typeof coordArr[1] === 'number'
|
|
264
|
+
? { lat: coordArr[0], lng: coordArr[1] }
|
|
265
|
+
: null,
|
|
266
|
+
offers: [],
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
toolName: get_hotel_booking_options
|
|
2
|
+
summary: "Open a hotel's booking options on Google Hotels for a location/date window and read per-provider pricing, with optional stay-total vs nightly pricing."
|
|
3
|
+
parameters:
|
|
4
|
+
- name: location
|
|
5
|
+
type: string
|
|
6
|
+
description: "Place to search (resolved by the autocomplete), e.g. Chicago Loop"
|
|
7
|
+
- name: check_in_date
|
|
8
|
+
type: string
|
|
9
|
+
description: "Check-in date (YYYY-MM-DD)"
|
|
10
|
+
- name: check_out_date
|
|
11
|
+
type: string
|
|
12
|
+
description: "Check-out date (YYYY-MM-DD)"
|
|
13
|
+
- name: adults
|
|
14
|
+
type: number
|
|
15
|
+
description: "Number of adults for pricing"
|
|
16
|
+
default: 2
|
|
17
|
+
- name: price_mode
|
|
18
|
+
type: string
|
|
19
|
+
description: "nightly | stay_total — controls whether totals reflect the full stay"
|
|
20
|
+
default: nightly
|
|
21
|
+
steps:
|
|
22
|
+
- action: navigate
|
|
23
|
+
url: https://www.google.com/travel/search
|
|
24
|
+
wait_for: networkidle
|
|
25
|
+
- action: click
|
|
26
|
+
locators:
|
|
27
|
+
- by: aria_label
|
|
28
|
+
value: Search for places, hotels and more
|
|
29
|
+
- by: css
|
|
30
|
+
value: input.II2One.j0Ppje
|
|
31
|
+
wait_for:
|
|
32
|
+
sleep_ms: 300
|
|
33
|
+
- action: type
|
|
34
|
+
locators:
|
|
35
|
+
- by: aria_label
|
|
36
|
+
value: Search for places, hotels and more
|
|
37
|
+
- by: css
|
|
38
|
+
value: input.II2One.j0Ppje
|
|
39
|
+
value: ${location}
|
|
40
|
+
wait_for:
|
|
41
|
+
xhr: rpcids=mejVKc
|
|
42
|
+
- action: click
|
|
43
|
+
locators:
|
|
44
|
+
- by: text
|
|
45
|
+
value_pattern: ${location}
|
|
46
|
+
- by: css
|
|
47
|
+
value: ul.F3AVKd > li.Q1RWxd span.wA6vgd
|
|
48
|
+
wait_for:
|
|
49
|
+
xhr: rpcids=AtySUc
|
|
50
|
+
- action: click
|
|
51
|
+
locators:
|
|
52
|
+
- by: aria_label
|
|
53
|
+
value: Check-in
|
|
54
|
+
- by: css
|
|
55
|
+
value: div.NA5Egc.ESCxub > input.TP4Lpb.eoY5cb
|
|
56
|
+
wait_for:
|
|
57
|
+
sleep_ms: 400
|
|
58
|
+
- action: type
|
|
59
|
+
locators:
|
|
60
|
+
- by: aria_label
|
|
61
|
+
value: Check-in
|
|
62
|
+
- by: css
|
|
63
|
+
value: div.NA5Egc.ESCxub > input.TP4Lpb.eoY5cb
|
|
64
|
+
value: ${check_in_date}
|
|
65
|
+
wait_for:
|
|
66
|
+
sleep_ms: 300
|
|
67
|
+
- action: type
|
|
68
|
+
locators:
|
|
69
|
+
- by: aria_label
|
|
70
|
+
value: Check-out
|
|
71
|
+
- by: css
|
|
72
|
+
value: div.GYgkab.YICvqf input.TP4Lpb.eoY5cb
|
|
73
|
+
value: ${check_out_date}
|
|
74
|
+
wait_for:
|
|
75
|
+
sleep_ms: 300
|
|
76
|
+
- action: click
|
|
77
|
+
locators:
|
|
78
|
+
- by: role
|
|
79
|
+
value: button
|
|
80
|
+
name: Done
|
|
81
|
+
- by: text
|
|
82
|
+
value: Done
|
|
83
|
+
wait_for:
|
|
84
|
+
xhr: rpcids=AtySUc
|
|
85
|
+
- action: click
|
|
86
|
+
locators:
|
|
87
|
+
- by: aria_label
|
|
88
|
+
value: "Number of travelers."
|
|
89
|
+
- by: css
|
|
90
|
+
value: div.rb1Kdf.CpGuFd
|
|
91
|
+
wait_for:
|
|
92
|
+
sleep_ms: 300
|
|
93
|
+
- action: click
|
|
94
|
+
locators:
|
|
95
|
+
- by: role
|
|
96
|
+
value: button
|
|
97
|
+
name: Done
|
|
98
|
+
- by: text
|
|
99
|
+
value: Done
|
|
100
|
+
wait_for:
|
|
101
|
+
xhr: rpcids=AtySUc
|
|
102
|
+
- action: click
|
|
103
|
+
locators:
|
|
104
|
+
- by: role
|
|
105
|
+
value: link
|
|
106
|
+
name: ${location}
|
|
107
|
+
- by: css
|
|
108
|
+
value: c-wiz.K1smNd a.PVOOXe
|
|
109
|
+
wait_for:
|
|
110
|
+
xhr: rpcids=M0CRd
|
|
111
|
+
- action: click
|
|
112
|
+
locators:
|
|
113
|
+
- by: text
|
|
114
|
+
value: Nightly price with fees
|
|
115
|
+
- by: css
|
|
116
|
+
value: span.BoOe8c button.VfPpkd-LgbsSe
|
|
117
|
+
wait_for:
|
|
118
|
+
sleep_ms: 300
|
|
119
|
+
- action: click
|
|
120
|
+
locators:
|
|
121
|
+
- by: text
|
|
122
|
+
value: Stay total
|
|
123
|
+
- by: css
|
|
124
|
+
value: label.LLYsl div.GgTqfe
|
|
125
|
+
wait_for:
|
|
126
|
+
sleep_ms: 300
|
|
127
|
+
- action: click
|
|
128
|
+
locators:
|
|
129
|
+
- by: role
|
|
130
|
+
value: button
|
|
131
|
+
name: Done
|
|
132
|
+
- by: text
|
|
133
|
+
value: Done
|
|
134
|
+
wait_for:
|
|
135
|
+
xhr: rpcids=M0CRd
|
|
136
|
+
result:
|
|
137
|
+
source: xhr
|
|
138
|
+
url_pattern: rpcids=M0CRd
|
|
139
|
+
extract: "2.2[]"
|
|
140
|
+
return_as: booking_options
|
|
141
|
+
notes: >-
|
|
142
|
+
The M0CRd response is a batchexecute anti-XSSI envelope: strip the ")]}'" prefix and chunk-length
|
|
143
|
+
lines, JSON.parse the outer array, then JSON.parse the inner escaped string (element [2] of the
|
|
144
|
+
["wrb.fr","M0CRd", ...] tuple) — use the shared batchexecute helper. The parsed inner JSON is
|
|
145
|
+
positional (no keys). Pricing context is at index [1] (currency, [[checkInY,M-1,D],[checkOutY,M-1,D],
|
|
146
|
+
nights/occupancy], location [mid,name]). The offer block is at index [2]: [2][1] is the summary
|
|
147
|
+
["$199" nightly, "$237" nightly-with-fees, ...] and [2][2] is the per-provider offer list, where
|
|
148
|
+
each entry's [0][0]=provider name, [0][1]=numeric price, [0][2]=booking click-through URL. When
|
|
149
|
+
price_mode=stay_total, the toggle steps make totals reflect the full stay (summary/totals fields
|
|
150
|
+
change accordingly); for price_mode=nightly, skip the "Nightly price with fees"/"Stay total"/"Done"
|
|
151
|
+
steps. Location is resolved to a Google KG mid via the mejVKc/AtySUc autocomplete flow before
|
|
152
|
+
pricing is available. adults is set through the travelers popover (use +/- to reach the target);
|
|
153
|
+
the date inputs accept typed YYYY-MM-DD, falling back to calendar-cell clicks if typing is rejected.
|
|
154
|
+
f.sid/bl/X-Goog-BatchExecute-Bgr are session-bound and supplied by the live browser context.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Per-tool request transform for google-hotels get_hotel_booking_options.
|
|
2
|
+
//
|
|
3
|
+
// This tool returns the BOOKING OPTIONS (per-provider offers) for the top hotel
|
|
4
|
+
// in an area, matching the narration "clicked one of the offerings, saw booking
|
|
5
|
+
// options". The M0CRd pricing RPC requires a per-hotel entry token ([3]) that is
|
|
6
|
+
// minted by the AtySUc hotel-search RPC, so the workflow is a 2-request chain:
|
|
7
|
+
//
|
|
8
|
+
// request[0] AtySUc — search the area (by the display name carried in the
|
|
9
|
+
// location_context token) to mint hotel entry tokens.
|
|
10
|
+
// request[1] M0CRd — pricing/booking options, using the first hotel token
|
|
11
|
+
// from request[0] as [3], plus the area mid + dates +
|
|
12
|
+
// occupancy. (Empirically M0CRd needs NO bounds.)
|
|
13
|
+
//
|
|
14
|
+
// Proven by the recording + live probes:
|
|
15
|
+
// seq 525 (nightly) / seq 536 (stay total) -> [2][2] price-mode flag (1 vs 2)
|
|
16
|
+
// seq 2083 -> [1][13] = [adults,null,children]
|
|
17
|
+
// live probe -> M0CRd 403s/empties when [3]=null;
|
|
18
|
+
// a fresh AtySUc hotel token fixes it.
|
|
19
|
+
//
|
|
20
|
+
// location_context is the opaque token emitted by the sibling search_hotels tool
|
|
21
|
+
// in the shape "<mid>|<displayName>" (e.g. "/m/0gz469|Chicago Loop").
|
|
22
|
+
import {
|
|
23
|
+
buildFreqBody,
|
|
24
|
+
buildBatchExecuteUrl,
|
|
25
|
+
encodeDate,
|
|
26
|
+
} from '../_shared/freq.ts';
|
|
27
|
+
import { parseBatchExecute } from '../_shared/batchexecute.ts';
|
|
28
|
+
import { buildInnerPayload as buildSearchInner } from '../search_hotels/request-transform.ts';
|
|
29
|
+
|
|
30
|
+
type Params = Record<string, string | number | boolean | undefined>;
|
|
31
|
+
|
|
32
|
+
function str(v: unknown): string {
|
|
33
|
+
return v === undefined || v === null ? '' : String(v).trim();
|
|
34
|
+
}
|
|
35
|
+
function num(v: unknown, dflt = 0): number {
|
|
36
|
+
if (v === undefined || v === null || v === '') return dflt;
|
|
37
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
38
|
+
return Number.isFinite(n) ? n : dflt;
|
|
39
|
+
}
|
|
40
|
+
function nightsBetween(ci: string, co: string): number {
|
|
41
|
+
const a = Date.parse(ci + 'T00:00:00Z');
|
|
42
|
+
const b = Date.parse(co + 'T00:00:00Z');
|
|
43
|
+
if (!Number.isFinite(a) || !Number.isFinite(b) || b <= a) return 1;
|
|
44
|
+
return Math.round((b - a) / 86_400_000);
|
|
45
|
+
}
|
|
46
|
+
function parseLocationContext(raw: string): { mid: string; name: string } {
|
|
47
|
+
const i = raw.indexOf('|');
|
|
48
|
+
if (i === -1) return { mid: raw, name: '' };
|
|
49
|
+
return { mid: raw.slice(0, i), name: raw.slice(i + 1) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Find the raw AtySUc response body among whatever shape the runtime passes.
|
|
53
|
+
function firstResponseString(responses: unknown): string | null {
|
|
54
|
+
if (!responses) return null;
|
|
55
|
+
const vals = Array.isArray(responses)
|
|
56
|
+
? responses
|
|
57
|
+
: typeof responses === 'object'
|
|
58
|
+
? Object.values(responses as Record<string, unknown>)
|
|
59
|
+
: [];
|
|
60
|
+
for (const v of vals) {
|
|
61
|
+
if (typeof v === 'string' && v.includes('AtySUc')) return v;
|
|
62
|
+
}
|
|
63
|
+
for (const v of vals) {
|
|
64
|
+
if (typeof v === 'string') return v;
|
|
65
|
+
if (v && typeof v === 'object') {
|
|
66
|
+
const b = (v as { body?: unknown }).body;
|
|
67
|
+
if (typeof b === 'string') return b;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Pull the first hotel entry token ("Ch…") from an AtySUc response.
|
|
74
|
+
export function extractHotelToken(atysRaw: string): string | null {
|
|
75
|
+
let inner: unknown = null;
|
|
76
|
+
try {
|
|
77
|
+
inner = parseBatchExecute(atysRaw, 'AtySUc');
|
|
78
|
+
} catch {
|
|
79
|
+
inner = null;
|
|
80
|
+
}
|
|
81
|
+
if (!inner) return null;
|
|
82
|
+
const m = JSON.stringify(inner).match(/"(Ch[A-Za-z0-9_-]{30,})"/);
|
|
83
|
+
return m ? m[1]! : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildSearchPayload(params: Params): unknown {
|
|
87
|
+
const { name } = parseLocationContext(str(params.location_context));
|
|
88
|
+
return buildSearchInner({
|
|
89
|
+
location: name || str(params.location_context),
|
|
90
|
+
check_in_date: str(params.check_in_date),
|
|
91
|
+
check_out_date: str(params.check_out_date),
|
|
92
|
+
adults: num(params.adults, 2),
|
|
93
|
+
children: num(params.children, 0),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildPricingPayload(params: Params, token: string | null): unknown {
|
|
98
|
+
const { mid, name } = parseLocationContext(str(params.location_context));
|
|
99
|
+
const checkIn = str(params.check_in_date) || '2026-07-03';
|
|
100
|
+
const checkOut = str(params.check_out_date) || '2026-07-06';
|
|
101
|
+
const adults = Math.max(1, num(params.adults, 2));
|
|
102
|
+
const children = Math.max(0, num(params.children, 0));
|
|
103
|
+
const priceMode =
|
|
104
|
+
str(params.price_mode).toLowerCase() === 'stay_total' ? 2 : 1;
|
|
105
|
+
const nights = nightsBetween(checkIn, checkOut);
|
|
106
|
+
|
|
107
|
+
const context: unknown[] = new Array(19).fill(null);
|
|
108
|
+
context[0] = [200, 0];
|
|
109
|
+
context[3] = 'USD';
|
|
110
|
+
context[4] = [encodeDate(checkIn), encodeDate(checkOut), nights, null, 0];
|
|
111
|
+
context[13] = [adults, null, children];
|
|
112
|
+
context[18] = [mid, name];
|
|
113
|
+
|
|
114
|
+
return [null, context, [1, null, priceMode, 1], token ?? null, []];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function transform(
|
|
118
|
+
method: string,
|
|
119
|
+
url: string,
|
|
120
|
+
responses: Record<string, any>,
|
|
121
|
+
params?: Params,
|
|
122
|
+
): { url: string; body: string } {
|
|
123
|
+
void method;
|
|
124
|
+
const u = new URL(url);
|
|
125
|
+
const rpcid = u.searchParams.get('rpcids') || 'M0CRd';
|
|
126
|
+
const fSid = u.searchParams.get('f.sid') ?? '';
|
|
127
|
+
const bl = u.searchParams.get('bl') ?? '';
|
|
128
|
+
const p = params ?? {};
|
|
129
|
+
|
|
130
|
+
if (rpcid === 'AtySUc') {
|
|
131
|
+
return {
|
|
132
|
+
url: buildBatchExecuteUrl('AtySUc', { f_sid: fSid, bl }),
|
|
133
|
+
body: buildFreqBody('AtySUc', buildSearchPayload(p), '1'),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// M0CRd: needs the hotel entry token from the AtySUc response.
|
|
138
|
+
const atysRaw = firstResponseString(responses);
|
|
139
|
+
const token = atysRaw ? extractHotelToken(atysRaw) : null;
|
|
140
|
+
if (process.env.GHO_DEBUG) {
|
|
141
|
+
console.error(
|
|
142
|
+
'[gho transform] responses type=',
|
|
143
|
+
Array.isArray(responses) ? 'array' : typeof responses,
|
|
144
|
+
'keys=',
|
|
145
|
+
responses && typeof responses === 'object' ? Object.keys(responses) : null,
|
|
146
|
+
'token=',
|
|
147
|
+
token,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
url: buildBatchExecuteUrl('M0CRd', { f_sid: fSid, bl }),
|
|
152
|
+
body: buildFreqBody('M0CRd', buildPricingPayload(p, token), 'generic'),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"toolName": "get_hotel_booking_options",
|
|
3
|
+
"intent": {
|
|
4
|
+
"description": "Get booking options (per-provider nightly and stay-total prices with click-through URLs) for the top hotel in a location/date window.",
|
|
5
|
+
"userSaid": "clicked one of the offerings, saw booking options; changed to stay total pricing; looked at more booking options; changed number of adults"
|
|
6
|
+
},
|
|
7
|
+
"parameters": [
|
|
8
|
+
{
|
|
9
|
+
"name": "location_context",
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Opaque area token from search_hotels (shape '<mid>|<displayName>', e.g. '/m/0gz469|Chicago Loop').",
|
|
12
|
+
"default": "/m/0gz469|Chicago Loop",
|
|
13
|
+
"verified": true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "check_in_date",
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Check-in date (YYYY-MM-DD)",
|
|
19
|
+
"default": "2026-07-03",
|
|
20
|
+
"verified": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "check_out_date",
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Check-out date (YYYY-MM-DD)",
|
|
26
|
+
"default": "2026-07-06",
|
|
27
|
+
"verified": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "adults",
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "Number of adults for pricing",
|
|
33
|
+
"default": 2,
|
|
34
|
+
"verified": true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "children",
|
|
38
|
+
"type": "number",
|
|
39
|
+
"description": "Number of children for pricing (0 = none)",
|
|
40
|
+
"default": 0,
|
|
41
|
+
"verified": false,
|
|
42
|
+
"verifyNote": "annotated"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "price_mode",
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "nightly | stay_total. Each offer carries both; the flag selects the UI default.",
|
|
48
|
+
"default": "nightly",
|
|
49
|
+
"verified": true
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"requests": [
|
|
53
|
+
{
|
|
54
|
+
"method": "POST",
|
|
55
|
+
"url": "https://www.google.com/_/TravelFrontendUi/data/batchexecute?rpcids=AtySUc&source-path=%2Ftravel%2Fsearch&f.sid=7513562915459271421&bl=boq_travel-frontend-ui_20260527.01_p0&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=2252256&rt=c",
|
|
56
|
+
"headers": {
|
|
57
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
58
|
+
"X-Same-Domain": "1",
|
|
59
|
+
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]",
|
|
60
|
+
"x-goog-ext-190139975-jspb": "[\"US\",\"ZZ\",\"x6c25Q==\"]",
|
|
61
|
+
"Referer": "https://www.google.com/travel/search"
|
|
62
|
+
},
|
|
63
|
+
"body": "f.req={\"location_context\":\"${param.location_context}\",\"check_in_date\":\"${param.check_in_date}\",\"check_out_date\":\"${param.check_out_date}\",\"adults\":\"${param.adults}\",\"children\":\"${param.children}\"}",
|
|
64
|
+
"effect": "safe"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"method": "POST",
|
|
68
|
+
"url": "https://www.google.com/_/TravelFrontendUi/data/batchexecute?rpcids=M0CRd&source-path=%2Ftravel%2Fsearch&f.sid=7513562915459271421&bl=boq_travel-frontend-ui_20260527.01_p0&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=2552256&rt=c",
|
|
69
|
+
"headers": {
|
|
70
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
71
|
+
"X-Same-Domain": "1",
|
|
72
|
+
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]",
|
|
73
|
+
"x-goog-ext-190139975-jspb": "[\"US\",\"ZZ\",\"x6c25Q==\"]",
|
|
74
|
+
"Referer": "https://www.google.com/travel/search"
|
|
75
|
+
},
|
|
76
|
+
"body": "f.req={\"location_context\":\"${param.location_context}\",\"check_in_date\":\"${param.check_in_date}\",\"check_out_date\":\"${param.check_out_date}\",\"adults\":\"${param.adults}\",\"children\":\"${param.children}\",\"price_mode\":\"${param.price_mode}\"}",
|
|
77
|
+
"effect": "safe"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"site": "google-hotels",
|
|
81
|
+
"requestTransformModule": "./request-transform.ts",
|
|
82
|
+
"parserModule": "./parser.ts",
|
|
83
|
+
"liveVerified": true
|
|
84
|
+
}
|