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.
Files changed (130) hide show
  1. package/README.md +193 -189
  2. package/examples/discoverandgo/README.md +1 -1
  3. package/examples/echo/README.md +1 -1
  4. package/examples/google-flights/README.md +28 -0
  5. package/examples/google-flights/_shared/batchexecute.ts +63 -0
  6. package/examples/google-flights/_shared/flights_request.ts +95 -0
  7. package/examples/google-flights/_shared/package.json +9 -0
  8. package/examples/google-flights/get_flight_booking_details/index.ts +159 -0
  9. package/examples/google-flights/get_flight_booking_details/package.json +9 -0
  10. package/examples/google-flights/get_flight_booking_details/parser.ts +182 -0
  11. package/examples/google-flights/get_flight_booking_details/playbook.yaml +138 -0
  12. package/examples/google-flights/get_flight_booking_details/request-transform.ts +86 -0
  13. package/examples/google-flights/get_flight_booking_details/workflow.json +98 -0
  14. package/examples/google-flights/get_flight_calendar_prices/index.ts +131 -0
  15. package/examples/google-flights/get_flight_calendar_prices/package.json +9 -0
  16. package/examples/google-flights/get_flight_calendar_prices/parser.ts +86 -0
  17. package/examples/google-flights/get_flight_calendar_prices/playbook.yaml +97 -0
  18. package/examples/google-flights/get_flight_calendar_prices/request-transform.ts +31 -0
  19. package/examples/google-flights/get_flight_calendar_prices/workflow.json +78 -0
  20. package/examples/google-flights/lookup_airport/index.ts +101 -0
  21. package/examples/google-flights/lookup_airport/package.json +9 -0
  22. package/examples/google-flights/lookup_airport/parser.ts +66 -0
  23. package/examples/google-flights/lookup_airport/playbook.yaml +47 -0
  24. package/examples/google-flights/lookup_airport/request-transform.ts +20 -0
  25. package/examples/google-flights/lookup_airport/workflow.json +57 -0
  26. package/examples/google-flights/search_flights/index.ts +219 -0
  27. package/examples/google-flights/search_flights/package.json +9 -0
  28. package/examples/google-flights/search_flights/parser.ts +169 -0
  29. package/examples/google-flights/search_flights/playbook.yaml +184 -0
  30. package/examples/google-flights/search_flights/request-transform.ts +119 -0
  31. package/examples/google-flights/search_flights/workflow.json +143 -0
  32. package/examples/google-hotels/README.md +29 -0
  33. package/examples/google-hotels/_shared/batchexecute.ts +73 -0
  34. package/examples/google-hotels/_shared/freq.ts +158 -0
  35. package/examples/google-hotels/_shared/package.json +9 -0
  36. package/examples/google-hotels/autocomplete_hotel_location/index.ts +80 -0
  37. package/examples/google-hotels/autocomplete_hotel_location/package.json +9 -0
  38. package/examples/google-hotels/autocomplete_hotel_location/parser.ts +71 -0
  39. package/examples/google-hotels/autocomplete_hotel_location/playbook.yaml +36 -0
  40. package/examples/google-hotels/autocomplete_hotel_location/request-transform.ts +37 -0
  41. package/examples/google-hotels/autocomplete_hotel_location/workflow.json +36 -0
  42. package/examples/google-hotels/get_hotel_booking_options/index.ts +143 -0
  43. package/examples/google-hotels/get_hotel_booking_options/package.json +9 -0
  44. package/examples/google-hotels/get_hotel_booking_options/parser.ts +271 -0
  45. package/examples/google-hotels/get_hotel_booking_options/playbook.yaml +154 -0
  46. package/examples/google-hotels/get_hotel_booking_options/request-transform.ts +154 -0
  47. package/examples/google-hotels/get_hotel_booking_options/workflow.json +84 -0
  48. package/examples/google-hotels/get_hotel_reviews/index.ts +81 -0
  49. package/examples/google-hotels/get_hotel_reviews/package.json +9 -0
  50. package/examples/google-hotels/get_hotel_reviews/parser.ts +128 -0
  51. package/examples/google-hotels/get_hotel_reviews/playbook.yaml +64 -0
  52. package/examples/google-hotels/get_hotel_reviews/request-transform.ts +42 -0
  53. package/examples/google-hotels/get_hotel_reviews/workflow.json +37 -0
  54. package/examples/google-hotels/search_hotels/index.ts +207 -0
  55. package/examples/google-hotels/search_hotels/package.json +9 -0
  56. package/examples/google-hotels/search_hotels/parser.ts +260 -0
  57. package/examples/google-hotels/search_hotels/playbook.yaml +87 -0
  58. package/examples/google-hotels/search_hotels/request-transform.ts +197 -0
  59. package/examples/google-hotels/search_hotels/workflow.json +127 -0
  60. package/examples/southwest/README.md +3 -2
  61. package/examples/southwest/search_southwest_flights/index.ts +18 -1
  62. package/examples/southwest/search_southwest_flights/workflow.json +18 -1
  63. package/package.json +3 -2
  64. package/prompts/audit-agent.md +71 -0
  65. package/prompts/build-planning.md +74 -0
  66. package/prompts/compile-agent.md +131 -27
  67. package/prompts/prereq-builder.md +64 -0
  68. package/prompts/prereq-planner.md +34 -0
  69. package/prompts/tool-planning.md +39 -0
  70. package/src/cli.ts +116 -3
  71. package/src/imprint/agent.ts +5 -0
  72. package/src/imprint/audit.ts +996 -0
  73. package/src/imprint/backend-ladder.ts +1214 -184
  74. package/src/imprint/build-plan.ts +1051 -0
  75. package/src/imprint/cdp-browser-fetch.ts +592 -0
  76. package/src/imprint/cdp-jar-cache.ts +320 -0
  77. package/src/imprint/chromium.ts +414 -8
  78. package/src/imprint/claude-cli-compile.ts +125 -25
  79. package/src/imprint/codex-cli-compile.ts +26 -23
  80. package/src/imprint/compile-agent-types.ts +38 -0
  81. package/src/imprint/compile-agent.ts +63 -25
  82. package/src/imprint/compile-tools.ts +1666 -66
  83. package/src/imprint/compile.ts +13 -1
  84. package/src/imprint/concurrency.ts +87 -0
  85. package/src/imprint/cron.ts +4 -0
  86. package/src/imprint/doctor.ts +48 -3
  87. package/src/imprint/freeform-redact.ts +5 -4
  88. package/src/imprint/install.ts +79 -4
  89. package/src/imprint/integrations.ts +3 -3
  90. package/src/imprint/llm.ts +56 -8
  91. package/src/imprint/mcp-compile-server.ts +43 -10
  92. package/src/imprint/mcp-maintenance.ts +18 -102
  93. package/src/imprint/mcp-server.ts +73 -7
  94. package/src/imprint/multi-progress.ts +7 -2
  95. package/src/imprint/param-grounding.ts +367 -0
  96. package/src/imprint/paths.ts +29 -0
  97. package/src/imprint/playbook-runner.ts +101 -40
  98. package/src/imprint/prereq-builder.ts +651 -0
  99. package/src/imprint/probe-backends.ts +6 -3
  100. package/src/imprint/record.ts +10 -1
  101. package/src/imprint/redact.ts +30 -2
  102. package/src/imprint/replay-capture.ts +19 -18
  103. package/src/imprint/runtime.ts +19 -10
  104. package/src/imprint/session-diff.ts +79 -2
  105. package/src/imprint/session-merge.ts +9 -5
  106. package/src/imprint/stealth-chromium.ts +79 -0
  107. package/src/imprint/stealth-fetch.ts +309 -29
  108. package/src/imprint/stealth-token-cache.ts +88 -0
  109. package/src/imprint/teach-plan.ts +251 -0
  110. package/src/imprint/teach-state.ts +10 -0
  111. package/src/imprint/teach.ts +456 -142
  112. package/src/imprint/tool-candidates.ts +72 -14
  113. package/src/imprint/tool-plan.ts +313 -0
  114. package/src/imprint/tracing.ts +135 -6
  115. package/src/imprint/types.ts +61 -3
  116. package/examples/google-flights/search_google_flights/index.ts +0 -101
  117. package/examples/google-flights/search_google_flights/parser.test.ts +0 -140
  118. package/examples/google-flights/search_google_flights/parser.ts +0 -189
  119. package/examples/google-flights/search_google_flights/playbook.yaml +0 -130
  120. package/examples/google-flights/search_google_flights/workflow.json +0 -48
  121. package/examples/google-hotels/search_google_hotels/index.ts +0 -194
  122. package/examples/google-hotels/search_google_hotels/parser.test.ts +0 -168
  123. package/examples/google-hotels/search_google_hotels/parser.ts +0 -330
  124. package/examples/google-hotels/search_google_hotels/playbook.yaml +0 -125
  125. package/examples/google-hotels/search_google_hotels/workflow.json +0 -111
  126. package/examples/namecheap-domains/search_namecheap_domains/index.ts +0 -144
  127. package/examples/namecheap-domains/search_namecheap_domains/parser.ts +0 -380
  128. package/examples/namecheap-domains/search_namecheap_domains/playbook.yaml +0 -50
  129. package/examples/namecheap-domains/search_namecheap_domains/request-transform.ts +0 -136
  130. 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
+ }