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,260 @@
|
|
|
1
|
+
// Parser for google-hotels search_hotels (AtySUc batchexecute response).
|
|
2
|
+
// Decodes the anti-XSSI envelope via the shared helper, then walks the inner
|
|
3
|
+
// JSON collecting hotel entries (keyed "397419284"), the result-count/area row
|
|
4
|
+
// (keyed "416343588"), and recursively extracting per-hotel fields whose exact
|
|
5
|
+
// array positions drift between entries.
|
|
6
|
+
import { parseBatchExecute } from '../_shared/batchexecute.ts';
|
|
7
|
+
|
|
8
|
+
type Ctx = {
|
|
9
|
+
params?: Record<string, string | number | boolean>;
|
|
10
|
+
responses?: unknown[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function isArr(v: unknown): v is unknown[] {
|
|
14
|
+
return Array.isArray(v);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Depth-first: collect every object value stored under `key` anywhere in the tree.
|
|
18
|
+
function collectByKey(node: unknown, key: string, out: unknown[]): void {
|
|
19
|
+
if (isArr(node)) {
|
|
20
|
+
for (const child of node) collectByKey(child, key, out);
|
|
21
|
+
} else if (node && typeof node === 'object') {
|
|
22
|
+
const obj = node as Record<string, unknown>;
|
|
23
|
+
for (const k of Object.keys(obj)) {
|
|
24
|
+
if (k === key) out.push(obj[k]);
|
|
25
|
+
collectByKey(obj[k], key, out);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Recursively find the first node satisfying `pred`.
|
|
31
|
+
function findFirst(node: unknown, pred: (n: unknown) => boolean): unknown {
|
|
32
|
+
if (pred(node)) return node;
|
|
33
|
+
if (isArr(node)) {
|
|
34
|
+
for (const child of node) {
|
|
35
|
+
const hit = findFirst(child, pred);
|
|
36
|
+
if (hit !== undefined) return hit;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findHotelId(entry: unknown): string | null {
|
|
43
|
+
const hit = findFirst(
|
|
44
|
+
entry,
|
|
45
|
+
(n) => typeof n === 'string' && /^Ch[a-zA-Z]I[A-Za-z0-9_-]{8,}$/.test(n),
|
|
46
|
+
);
|
|
47
|
+
return typeof hit === 'string' ? hit : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findLatLng(entry: unknown): { lat: number; lng: number } | null {
|
|
51
|
+
const hit = findFirst(
|
|
52
|
+
entry,
|
|
53
|
+
(n) =>
|
|
54
|
+
isArr(n) &&
|
|
55
|
+
n.length === 2 &&
|
|
56
|
+
typeof n[0] === 'number' &&
|
|
57
|
+
typeof n[1] === 'number' &&
|
|
58
|
+
Math.abs(n[0]) <= 90 &&
|
|
59
|
+
Math.abs(n[1]) <= 180 &&
|
|
60
|
+
// exclude small integer pairs like [12,18]; coords have decimals
|
|
61
|
+
(!Number.isInteger(n[0]) || !Number.isInteger(n[1])),
|
|
62
|
+
) as number[] | undefined;
|
|
63
|
+
return hit ? { lat: hit[0]!, lng: hit[1]! } : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Overall rating block looks like [[4,"4.6"],[1,"4.8"],...] (4 = overall).
|
|
67
|
+
function findRating(entry: unknown): { overall: number | null; categories: Record<string, number> } {
|
|
68
|
+
const block = findFirst(entry, (n) => {
|
|
69
|
+
if (!isArr(n) || n.length === 0) return false;
|
|
70
|
+
return n.every(
|
|
71
|
+
(p) =>
|
|
72
|
+
isArr(p) &&
|
|
73
|
+
p.length === 2 &&
|
|
74
|
+
typeof p[0] === 'number' &&
|
|
75
|
+
typeof p[1] === 'string' &&
|
|
76
|
+
/^\d+(\.\d+)?$/.test(p[1]),
|
|
77
|
+
);
|
|
78
|
+
}) as Array<[number, string]> | undefined;
|
|
79
|
+
const categories: Record<string, number> = {};
|
|
80
|
+
let overall: number | null = null;
|
|
81
|
+
if (block) {
|
|
82
|
+
const labels: Record<number, string> = {
|
|
83
|
+
4: 'overall',
|
|
84
|
+
1: 'location',
|
|
85
|
+
5: 'rooms',
|
|
86
|
+
2: 'service',
|
|
87
|
+
3: 'value',
|
|
88
|
+
};
|
|
89
|
+
for (const [code, val] of block) {
|
|
90
|
+
const f = Number(val);
|
|
91
|
+
if (code === 4) overall = f;
|
|
92
|
+
categories[labels[code] ?? String(code)] = f;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { overall, categories };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findCheckInOut(entry: unknown): { checkIn: string | null; checkOut: string | null } {
|
|
99
|
+
const hit = findFirst(
|
|
100
|
+
entry,
|
|
101
|
+
(n) =>
|
|
102
|
+
isArr(n) &&
|
|
103
|
+
n.length === 2 &&
|
|
104
|
+
typeof n[0] === 'string' &&
|
|
105
|
+
typeof n[1] === 'string' &&
|
|
106
|
+
/\d{1,2}:\d{2}\s?(AM|PM)/i.test(n[0]) &&
|
|
107
|
+
/\d{1,2}:\d{2}\s?(AM|PM)/i.test(n[1]),
|
|
108
|
+
) as string[] | undefined;
|
|
109
|
+
return hit ? { checkIn: hit[0]!, checkOut: hit[1]! } : { checkIn: null, checkOut: null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Price block: ["$98","$115",98,null,98] -> display + numeric.
|
|
113
|
+
function findPrice(entry: unknown): { display: string | null; amount: number | null } {
|
|
114
|
+
const hit = findFirst(
|
|
115
|
+
entry,
|
|
116
|
+
(n) =>
|
|
117
|
+
isArr(n) &&
|
|
118
|
+
n.length >= 3 &&
|
|
119
|
+
typeof n[0] === 'string' &&
|
|
120
|
+
n[0].startsWith('$') &&
|
|
121
|
+
typeof n[2] === 'number',
|
|
122
|
+
) as unknown[] | undefined;
|
|
123
|
+
return hit
|
|
124
|
+
? { display: hit[0] as string, amount: hit[2] as number }
|
|
125
|
+
: { display: null, amount: null };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findStarClass(entry: unknown): string | null {
|
|
129
|
+
const hit = findFirst(
|
|
130
|
+
entry,
|
|
131
|
+
(n) =>
|
|
132
|
+
isArr(n) &&
|
|
133
|
+
n.length === 2 &&
|
|
134
|
+
typeof n[0] === 'string' &&
|
|
135
|
+
/-star (hotel|property)|star hotel|hotel$/i.test(n[0]) &&
|
|
136
|
+
typeof n[1] === 'number',
|
|
137
|
+
) as unknown[] | undefined;
|
|
138
|
+
return hit ? (hit[0] as string) : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function findAreaMid(entry: unknown): string | null {
|
|
142
|
+
const hit = findFirst(
|
|
143
|
+
entry,
|
|
144
|
+
(n) => typeof n === 'string' && /^\/m\/[a-z0-9_]+$/i.test(n),
|
|
145
|
+
);
|
|
146
|
+
return typeof hit === 'string' ? hit : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Nearby POIs: [[1,[1,null,[["Grant Park",null,[[2,"6 min"]]]]]],...]
|
|
150
|
+
function findPois(entry: unknown): string[] {
|
|
151
|
+
const names: string[] = [];
|
|
152
|
+
function walk(n: unknown): void {
|
|
153
|
+
if (isArr(n)) {
|
|
154
|
+
// a POI tuple ["Name", null, [[code,"X min"]], ...]
|
|
155
|
+
if (
|
|
156
|
+
typeof n[0] === 'string' &&
|
|
157
|
+
n[0].length > 1 &&
|
|
158
|
+
isArr(n[2]) &&
|
|
159
|
+
n[2].some(
|
|
160
|
+
(d) => isArr(d) && typeof d[1] === 'string' && /\bmin\b|\bhr\b/.test(d[1]),
|
|
161
|
+
)
|
|
162
|
+
) {
|
|
163
|
+
if (!names.includes(n[0])) names.push(n[0]);
|
|
164
|
+
}
|
|
165
|
+
for (const c of n) walk(c);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
walk(entry);
|
|
169
|
+
return names.slice(0, 8);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function mapHotel(rawEntry: unknown, areaMid: string | null, areaName: string): Record<string, unknown> | null {
|
|
173
|
+
// The "397419284" value wraps the hotel entry: [ [null,"Name",[...]] ].
|
|
174
|
+
let entry: unknown = rawEntry;
|
|
175
|
+
if (
|
|
176
|
+
isArr(entry) &&
|
|
177
|
+
typeof entry[1] !== 'string' &&
|
|
178
|
+
isArr(entry[0]) &&
|
|
179
|
+
typeof (entry[0] as unknown[])[1] === 'string'
|
|
180
|
+
) {
|
|
181
|
+
entry = entry[0];
|
|
182
|
+
}
|
|
183
|
+
const name = isArr(entry) && typeof entry[1] === 'string' ? entry[1] : null;
|
|
184
|
+
const hotelId = findHotelId(entry);
|
|
185
|
+
if (!name && !hotelId) return null; // content-less sentinel
|
|
186
|
+
const latlng = findLatLng(entry);
|
|
187
|
+
const rating = findRating(entry);
|
|
188
|
+
const times = findCheckInOut(entry);
|
|
189
|
+
const price = findPrice(entry);
|
|
190
|
+
const ownMid = findAreaMid(entry) ?? areaMid;
|
|
191
|
+
return {
|
|
192
|
+
name,
|
|
193
|
+
hotel_id: hotelId, // PRODUCER token (ChcI… ftid)
|
|
194
|
+
location_context: `${ownMid ?? ''}|${areaName}`, // PRODUCER token "<mid>|<displayName>"
|
|
195
|
+
latitude: latlng?.lat ?? null,
|
|
196
|
+
longitude: latlng?.lng ?? null,
|
|
197
|
+
rating: rating.overall,
|
|
198
|
+
rating_categories: rating.categories,
|
|
199
|
+
star_class: findStarClass(entry),
|
|
200
|
+
check_in_time: times.checkIn,
|
|
201
|
+
check_out_time: times.checkOut,
|
|
202
|
+
price: price.display,
|
|
203
|
+
price_amount: price.amount,
|
|
204
|
+
nearby: findPois(entry),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function extract(rawResponse: unknown, context?: Ctx): unknown {
|
|
209
|
+
const raw =
|
|
210
|
+
typeof rawResponse === 'string' ? rawResponse : JSON.stringify(rawResponse);
|
|
211
|
+
let inner: unknown;
|
|
212
|
+
try {
|
|
213
|
+
inner = parseBatchExecute(raw, 'AtySUc');
|
|
214
|
+
} catch {
|
|
215
|
+
inner = null;
|
|
216
|
+
}
|
|
217
|
+
if (inner == null) {
|
|
218
|
+
return { area: null, result_count: 0, hotels: [] };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// result-count / area row: [922,false,"Chicago Loop",true,2]
|
|
222
|
+
const countRows: unknown[] = [];
|
|
223
|
+
collectByKey(inner, '416343588', countRows);
|
|
224
|
+
let resultCount = 0;
|
|
225
|
+
let areaName = '';
|
|
226
|
+
for (const row of countRows) {
|
|
227
|
+
if (isArr(row) && typeof row[0] === 'number') {
|
|
228
|
+
resultCount = row[0];
|
|
229
|
+
if (typeof row[2] === 'string') areaName = row[2];
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!areaName && context?.params?.location) {
|
|
234
|
+
areaName = String(context.params.location);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// area mid from anywhere in the tree
|
|
238
|
+
const areaMid = findAreaMid(inner);
|
|
239
|
+
|
|
240
|
+
// hotel entries
|
|
241
|
+
const hotelVals: unknown[] = [];
|
|
242
|
+
collectByKey(inner, '397419284', hotelVals);
|
|
243
|
+
const seen = new Set<string>();
|
|
244
|
+
const hotels: Record<string, unknown>[] = [];
|
|
245
|
+
for (const v of hotelVals) {
|
|
246
|
+
const mapped = mapHotel(v, areaMid, areaName);
|
|
247
|
+
if (!mapped) continue;
|
|
248
|
+
const dedup = `${mapped.name}|${mapped.hotel_id}`;
|
|
249
|
+
if (seen.has(dedup)) continue;
|
|
250
|
+
seen.add(dedup);
|
|
251
|
+
hotels.push(mapped);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
area: areaName || null,
|
|
256
|
+
area_mid: areaMid,
|
|
257
|
+
result_count: resultCount || hotels.length,
|
|
258
|
+
hotels,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
toolName: search_hotels
|
|
2
|
+
summary: Search Google Hotels for a location over a check-in/check-out date range and return the hotel result list from the AtySUc search XHR.
|
|
3
|
+
parameters:
|
|
4
|
+
- name: location
|
|
5
|
+
type: string
|
|
6
|
+
description: "Destination query typed into the hotel search box, e.g. 'chicago loop', 'tahoe city', 'denver downtown'."
|
|
7
|
+
- name: check_in_date
|
|
8
|
+
type: string
|
|
9
|
+
description: "Check-in day as it appears in the date-picker cell aria-label, e.g. 'Friday, July 3, 2026'."
|
|
10
|
+
- name: check_out_date
|
|
11
|
+
type: string
|
|
12
|
+
description: "Check-out day as it appears in the date-picker cell aria-label, e.g. 'Monday, July 6, 2026'."
|
|
13
|
+
steps:
|
|
14
|
+
- action: navigate
|
|
15
|
+
url: https://www.google.com/travel/search
|
|
16
|
+
wait_for: networkidle
|
|
17
|
+
- action: click
|
|
18
|
+
locators:
|
|
19
|
+
- by: aria_label
|
|
20
|
+
value: Search for places, hotels and more
|
|
21
|
+
- by: css
|
|
22
|
+
value: input.II2One.j0Ppje
|
|
23
|
+
wait_for:
|
|
24
|
+
sleep_ms: 300
|
|
25
|
+
- action: type
|
|
26
|
+
locators:
|
|
27
|
+
- by: aria_label
|
|
28
|
+
value: Search for places, hotels and more
|
|
29
|
+
- by: css
|
|
30
|
+
value: input.II2One.j0Ppje
|
|
31
|
+
value: ${location}
|
|
32
|
+
wait_for:
|
|
33
|
+
sleep_ms: 600
|
|
34
|
+
- action: click
|
|
35
|
+
locators:
|
|
36
|
+
- by: text
|
|
37
|
+
value_pattern: ${location}
|
|
38
|
+
- by: css
|
|
39
|
+
value: ul.F3AVKd > li.Q1RWxd
|
|
40
|
+
wait_for:
|
|
41
|
+
xhr: rpcids=AtySUc
|
|
42
|
+
- action: click
|
|
43
|
+
locators:
|
|
44
|
+
- by: aria_label
|
|
45
|
+
value: Check-in
|
|
46
|
+
- by: css
|
|
47
|
+
value: input.TP4Lpb.eoY5cb
|
|
48
|
+
wait_for:
|
|
49
|
+
sleep_ms: 400
|
|
50
|
+
- action: click
|
|
51
|
+
locators:
|
|
52
|
+
- by: aria_label
|
|
53
|
+
value_pattern: ${check_in_date}
|
|
54
|
+
wait_for:
|
|
55
|
+
sleep_ms: 300
|
|
56
|
+
- action: click
|
|
57
|
+
locators:
|
|
58
|
+
- by: aria_label
|
|
59
|
+
value_pattern: ${check_out_date}
|
|
60
|
+
wait_for:
|
|
61
|
+
sleep_ms: 300
|
|
62
|
+
- action: click
|
|
63
|
+
locators:
|
|
64
|
+
- by: text
|
|
65
|
+
value: Done
|
|
66
|
+
- by: css
|
|
67
|
+
value: div.XfpsVe.J9fJmf button.VfPpkd-LgbsSe-OWXEXe-k8QpJ
|
|
68
|
+
wait_for:
|
|
69
|
+
xhr: rpcids=AtySUc
|
|
70
|
+
result:
|
|
71
|
+
source: xhr
|
|
72
|
+
url_pattern: rpcids=AtySUc
|
|
73
|
+
extract: wrb.fr.AtySUc
|
|
74
|
+
return_as: hotels
|
|
75
|
+
notes: >-
|
|
76
|
+
The AtySUc response is a batchexecute anti-XSSI envelope: strip the ")]}'" prefix and chunk-length
|
|
77
|
+
lines, then JSON.parse the inner escaped string inside [["wrb.fr","AtySUc","<inner-json>"]]. The inner
|
|
78
|
+
payload is positional nested arrays (no named keys), so the result-bearing hotel list and the
|
|
79
|
+
"$low/typical/high" price-index block must be walked by index after parsing — the same shared helper
|
|
80
|
+
used by the API workflow does this. Each typed location triggers an mejVKc autocomplete lookup plus an
|
|
81
|
+
AtySUc search; clicking the first suggestion that contains ${location} re-runs AtySUc with the resolved
|
|
82
|
+
Knowledge-Graph mid. Date cells are targeted by their full aria-label (e.g. "Friday, July 3, 2026"), so
|
|
83
|
+
check_in_date/check_out_date must be passed in that exact format; if the target month is not visible,
|
|
84
|
+
the picker may need next-month navigation first. Filter refinements seen in the recording (min_rating,
|
|
85
|
+
min_price/max_price, amenities, hotel_class, brands, sort_by, property_type) all re-issue the same
|
|
86
|
+
AtySUc endpoint with integer filter codes and are better applied via the API workflow than via static
|
|
87
|
+
DOM steps; this playbook covers the core location + date search that grounds every variation.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Per-tool request transform for google-hotels search_hotels.
|
|
2
|
+
// Builds the AtySUc batchexecute inner payload from the tool params, then wraps
|
|
3
|
+
// it with the SHARED freq.ts helpers (buildFreqBody / buildBatchExecuteUrl).
|
|
4
|
+
// The structure was reverse-engineered from recorded requests:
|
|
5
|
+
// seq 229 (location only) -> proves a bare text query resolves
|
|
6
|
+
// seq 300 (location + dates) -> [1][2] location/dates block
|
|
7
|
+
// seq 691 (4+ rating) -> [1][4][3]/[1][4][4]
|
|
8
|
+
// seq 745 (price max 338) -> [1][4][3][1] = [min,max]
|
|
9
|
+
// seq 1098 (19 amenities + 4/5 star) -> [1][4][0][0] amenities, [1][4][0][1] stars
|
|
10
|
+
// seq 1745 (brands) -> [1][4][0][7] brands
|
|
11
|
+
// seq 1842 (sort lowest price) -> [1][4][0][4] sort code
|
|
12
|
+
// seq 2082 (property type / vacation) -> [1][0] = 1 hotels / 2 vacation rentals
|
|
13
|
+
import {
|
|
14
|
+
buildFreqBody,
|
|
15
|
+
buildBatchExecuteUrl,
|
|
16
|
+
encodeDate,
|
|
17
|
+
} from '../_shared/freq.ts';
|
|
18
|
+
|
|
19
|
+
type Params = Record<string, string | number | boolean | undefined>;
|
|
20
|
+
|
|
21
|
+
function num(v: unknown, dflt = 0): number {
|
|
22
|
+
if (v === undefined || v === null || v === '') return dflt;
|
|
23
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
24
|
+
return Number.isFinite(n) ? n : dflt;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function str(v: unknown): string {
|
|
28
|
+
return v === undefined || v === null ? '' : String(v).trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Amenity name aliases -> Google integer codes (best-effort; numeric codes pass
|
|
32
|
+
// through unchanged). Codes observed in the recording's 19-amenity list.
|
|
33
|
+
const AMENITY_ALIASES: Record<string, number> = {
|
|
34
|
+
'free breakfast': 4,
|
|
35
|
+
breakfast: 4,
|
|
36
|
+
'free wi-fi': 6,
|
|
37
|
+
'free wifi': 6,
|
|
38
|
+
wifi: 6,
|
|
39
|
+
'wi-fi': 6,
|
|
40
|
+
bar: 2,
|
|
41
|
+
pool: 9,
|
|
42
|
+
spa: 15,
|
|
43
|
+
'air conditioning': 8,
|
|
44
|
+
'pet-friendly': 12,
|
|
45
|
+
'pet friendly': 12,
|
|
46
|
+
'fitness center': 19,
|
|
47
|
+
gym: 19,
|
|
48
|
+
restaurant: 1,
|
|
49
|
+
'room service': 3,
|
|
50
|
+
'airport shuttle': 22,
|
|
51
|
+
'electric vehicle charging station': 61,
|
|
52
|
+
parking: 7,
|
|
53
|
+
'free parking': 10,
|
|
54
|
+
'hot tub': 40,
|
|
55
|
+
'accessible': 5,
|
|
56
|
+
'kid-friendly': 53,
|
|
57
|
+
'all-inclusive available': 11,
|
|
58
|
+
'beach access': 52,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function parseCodeList(raw: string, aliases?: Record<string, number>): number[] {
|
|
62
|
+
if (!raw) return [];
|
|
63
|
+
const out: number[] = [];
|
|
64
|
+
for (const part of raw.split(',')) {
|
|
65
|
+
const t = part.trim().toLowerCase();
|
|
66
|
+
if (!t) continue;
|
|
67
|
+
if (aliases && t in aliases) {
|
|
68
|
+
out.push(aliases[t]!);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const n = Number(t);
|
|
72
|
+
if (Number.isFinite(n)) out.push(n);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Brand filter: each entry encodes as [parentCode,[childCode]]. Accept "parent:child"
|
|
78
|
+
// or a bare numeric code (wrapped as [code,[code]]).
|
|
79
|
+
function parseBrands(raw: string): Array<[number, number[]]> {
|
|
80
|
+
if (!raw) return [];
|
|
81
|
+
const out: Array<[number, number[]]> = [];
|
|
82
|
+
for (const part of raw.split(',')) {
|
|
83
|
+
const t = part.trim();
|
|
84
|
+
if (!t) continue;
|
|
85
|
+
if (t.includes(':')) {
|
|
86
|
+
const [a, b] = t.split(':');
|
|
87
|
+
const pa = Number(a);
|
|
88
|
+
const pb = Number(b);
|
|
89
|
+
if (Number.isFinite(pa) && Number.isFinite(pb)) out.push([pa, [pb]]);
|
|
90
|
+
} else {
|
|
91
|
+
const n = Number(t);
|
|
92
|
+
if (Number.isFinite(n)) out.push([n, [n]]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const SORT_CODES: Record<string, number | null> = {
|
|
99
|
+
relevance: null,
|
|
100
|
+
lowest_price: 3,
|
|
101
|
+
highest_rating: 8,
|
|
102
|
+
most_reviewed: 13,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function nightsBetween(ci: string, co: string): number {
|
|
106
|
+
const a = Date.parse(ci + 'T00:00:00Z');
|
|
107
|
+
const b = Date.parse(co + 'T00:00:00Z');
|
|
108
|
+
if (!Number.isFinite(a) || !Number.isFinite(b) || b <= a) return 1;
|
|
109
|
+
return Math.round((b - a) / 86_400_000);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildInnerPayload(params: Params): unknown {
|
|
113
|
+
const location = str(params.location);
|
|
114
|
+
const checkIn = str(params.check_in_date);
|
|
115
|
+
const checkOut = str(params.check_out_date);
|
|
116
|
+
const adults = Math.max(1, num(params.adults, 2));
|
|
117
|
+
const children = Math.max(0, num(params.children, 0));
|
|
118
|
+
const minRating = num(params.min_rating, 0);
|
|
119
|
+
const minPrice = num(params.min_price, 0);
|
|
120
|
+
const maxPrice = num(params.max_price, 0);
|
|
121
|
+
const amenities = parseCodeList(str(params.amenities), AMENITY_ALIASES);
|
|
122
|
+
const stars = parseCodeList(str(params.hotel_class));
|
|
123
|
+
const brands = parseBrands(str(params.brands));
|
|
124
|
+
const sortKey = str(params.sort_by).toLowerCase() || 'relevance';
|
|
125
|
+
const sortCode = sortKey in SORT_CODES ? SORT_CODES[sortKey] : null;
|
|
126
|
+
const propertyType =
|
|
127
|
+
str(params.property_type).toLowerCase() === 'vacation_rentals' ? 2 : 1;
|
|
128
|
+
|
|
129
|
+
// [1][1] occupancy: array of adults (each [3]) + children scalar
|
|
130
|
+
const occupancy: unknown = [Array.from({ length: adults }, () => [3]), children];
|
|
131
|
+
|
|
132
|
+
// [1][2] location/dates: empty mid block + dates block
|
|
133
|
+
let locDates: unknown;
|
|
134
|
+
if (checkIn && checkOut) {
|
|
135
|
+
locDates = [
|
|
136
|
+
[],
|
|
137
|
+
[
|
|
138
|
+
null,
|
|
139
|
+
[encodeDate(checkIn), encodeDate(checkOut), nightsBetween(checkIn, checkOut)],
|
|
140
|
+
null,
|
|
141
|
+
null,
|
|
142
|
+
null,
|
|
143
|
+
[1],
|
|
144
|
+
],
|
|
145
|
+
];
|
|
146
|
+
} else {
|
|
147
|
+
locDates = null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// [1][4] filters
|
|
151
|
+
const filterObj: unknown[] = [
|
|
152
|
+
amenities.length ? amenities : null, // [0] amenities
|
|
153
|
+
stars.length ? stars : null, // [1] hotel class (stars)
|
|
154
|
+
null, // [2]
|
|
155
|
+
null, // [3]
|
|
156
|
+
sortCode ?? null, // [4] sort
|
|
157
|
+
null, // [5]
|
|
158
|
+
'USD', // [6] currency
|
|
159
|
+
];
|
|
160
|
+
if (brands.length) filterObj[7] = brands; // [7] brands
|
|
161
|
+
|
|
162
|
+
const f14: unknown[] = [filterObj, null, []];
|
|
163
|
+
|
|
164
|
+
const ratingCode = minRating > 0 ? Math.round(minRating * 2) : 0;
|
|
165
|
+
const priceSet = minPrice > 0 || maxPrice > 0;
|
|
166
|
+
if (priceSet || ratingCode) {
|
|
167
|
+
const priceTuple = priceSet ? [minPrice > 0 ? minPrice : null, maxPrice > 0 ? maxPrice : null] : null;
|
|
168
|
+
f14[3] = [null, priceTuple, 1];
|
|
169
|
+
if (ratingCode) f14[4] = ratingCode;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const block1: unknown = [propertyType, occupancy, locDates, null, f14];
|
|
173
|
+
|
|
174
|
+
// [2] fresh-search block (matches seq 229 / a brand-new query)
|
|
175
|
+
const block2: unknown = [0, null, null, 0, 0, null, null, null, 0];
|
|
176
|
+
|
|
177
|
+
return [location, block1, block2];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function transform(
|
|
181
|
+
method: string,
|
|
182
|
+
url: string,
|
|
183
|
+
responses: Record<string, any>,
|
|
184
|
+
params?: Params,
|
|
185
|
+
): { url: string; body: string } {
|
|
186
|
+
void method;
|
|
187
|
+
void responses;
|
|
188
|
+
const u = new URL(url);
|
|
189
|
+
const rpcid = u.searchParams.get('rpcids') || 'AtySUc';
|
|
190
|
+
const fSid = u.searchParams.get('f.sid') ?? '';
|
|
191
|
+
const bl = u.searchParams.get('bl') ?? '';
|
|
192
|
+
const inner = buildInnerPayload(params ?? {});
|
|
193
|
+
return {
|
|
194
|
+
url: buildBatchExecuteUrl(rpcid, { f_sid: fSid, bl }),
|
|
195
|
+
body: buildFreqBody(rpcid, inner, '1'),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"toolName": "search_hotels",
|
|
3
|
+
"intent": {
|
|
4
|
+
"description": "Search Google Hotels for a location and date range with optional filters (price, rating, amenities, star class, brands, sort, property type).",
|
|
5
|
+
"userSaid": "searched for hotels at chicago loop from july 3-6; filtered for > 4.0 rating; filtered min and max price; added amenities; added 2/4/5 star; added brand filters; sorted by lowest price, highest rating, most reviewed; changed property type to vacation rentals; changed location to tahoe city and denver downtown"
|
|
6
|
+
},
|
|
7
|
+
"parameters": [
|
|
8
|
+
{
|
|
9
|
+
"name": "location",
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Destination query, e.g. 'chicago loop', 'tahoe city', 'denver downtown'",
|
|
12
|
+
"default": "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": false,
|
|
21
|
+
"verifyNote": "annotated"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "check_out_date",
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Check-out date (YYYY-MM-DD)",
|
|
27
|
+
"default": "2026-07-06",
|
|
28
|
+
"verified": false,
|
|
29
|
+
"verifyNote": "annotated"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "adults",
|
|
33
|
+
"type": "number",
|
|
34
|
+
"description": "Number of adult travelers",
|
|
35
|
+
"default": 2,
|
|
36
|
+
"verified": false,
|
|
37
|
+
"verifyNote": "annotated"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "children",
|
|
41
|
+
"type": "number",
|
|
42
|
+
"description": "Number of child travelers",
|
|
43
|
+
"default": 0,
|
|
44
|
+
"verified": false,
|
|
45
|
+
"verifyNote": "annotated"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "min_rating",
|
|
49
|
+
"type": "number",
|
|
50
|
+
"description": "Minimum guest rating filter, e.g. 4.0 (0 = no filter)",
|
|
51
|
+
"default": 0,
|
|
52
|
+
"verified": false,
|
|
53
|
+
"verifyNote": "annotated"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "min_price",
|
|
57
|
+
"type": "number",
|
|
58
|
+
"description": "Minimum nightly price filter (0 = no filter)",
|
|
59
|
+
"default": 0,
|
|
60
|
+
"verified": false,
|
|
61
|
+
"verifyNote": "annotated"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "max_price",
|
|
65
|
+
"type": "number",
|
|
66
|
+
"description": "Maximum nightly price filter, e.g. 338 (0 = no filter)",
|
|
67
|
+
"default": 0,
|
|
68
|
+
"verified": true
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "amenities",
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Comma-separated amenity names or codes (e.g. 'free wi-fi,pool' or '6,9'); empty = no filter",
|
|
74
|
+
"default": "",
|
|
75
|
+
"verified": false,
|
|
76
|
+
"verifyNote": "annotated"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "hotel_class",
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Comma-separated star classes, e.g. '2,3' or '4,5'; empty = no filter",
|
|
82
|
+
"default": "",
|
|
83
|
+
"verified": true
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "brands",
|
|
87
|
+
"type": "string",
|
|
88
|
+
"description": "Comma-separated brand codes ('parent:child' or numeric); empty = no filter",
|
|
89
|
+
"default": "",
|
|
90
|
+
"verified": false,
|
|
91
|
+
"verifyNote": "annotated"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "sort_by",
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "Sort order: relevance | lowest_price | highest_rating | most_reviewed",
|
|
97
|
+
"default": "relevance",
|
|
98
|
+
"verified": true
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "property_type",
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "hotels | vacation_rentals",
|
|
104
|
+
"default": "hotels",
|
|
105
|
+
"verified": true
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
"requests": [
|
|
109
|
+
{
|
|
110
|
+
"method": "POST",
|
|
111
|
+
"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",
|
|
112
|
+
"headers": {
|
|
113
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
114
|
+
"X-Same-Domain": "1",
|
|
115
|
+
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]",
|
|
116
|
+
"x-goog-ext-190139975-jspb": "[\"US\",\"ZZ\",\"x6c25Q==\"]",
|
|
117
|
+
"Referer": "https://www.google.com/travel/search"
|
|
118
|
+
},
|
|
119
|
+
"body": "f.req={\"location\":\"${param.location}\",\"check_in_date\":\"${param.check_in_date}\",\"check_out_date\":\"${param.check_out_date}\",\"adults\":\"${param.adults}\",\"children\":\"${param.children}\",\"min_rating\":\"${param.min_rating}\",\"min_price\":\"${param.min_price}\",\"max_price\":\"${param.max_price}\",\"amenities\":\"${param.amenities}\",\"hotel_class\":\"${param.hotel_class}\",\"brands\":\"${param.brands}\",\"sort_by\":\"${param.sort_by}\",\"property_type\":\"${param.property_type}\"}",
|
|
120
|
+
"effect": "safe"
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"site": "google-hotels",
|
|
124
|
+
"requestTransformModule": "./request-transform.ts",
|
|
125
|
+
"parserModule": "./parser.ts",
|
|
126
|
+
"liveVerified": true
|
|
127
|
+
}
|
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
- **`probe-backends` skipping the futile rung.** The cached `backends.json` orders the ladder `stealth-fetch → playbook` so cron doesn't burn 200ms on a fetch attempt every tick.
|
|
9
9
|
- **`notifyWhen: price_below`** pushing only on real drops, with the `pricePath` extracting from real Southwest response shape.
|
|
10
10
|
- **The fresh-UUID header trick** — Southwest rejects stale `X-User-Experience-ID`; stealth-fetch regenerates per call.
|
|
11
|
+
- **Public bootstrap config capture.** The workflow fetches Southwest's public bootstrap `data.js` and captures the current `api-keys.prod` value, so installed examples do not require a `SOUTHWEST_API_KEY` environment variable.
|
|
11
12
|
- **Multi-path `pricePath`** — `cron.json`'s notifyWhen lists both the raw API shape (when stealth-fetch wins) and the playbook's reshaped output, so the push fires regardless of which backend produced the result.
|
|
12
13
|
|
|
13
14
|
## Run it
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
|
-
# One-time setup
|
|
17
|
-
|
|
17
|
+
# One-time setup: registers the example MCP and installs Playwright Chromium if missing
|
|
18
|
+
imprint install southwest --source examples --platform claude-desktop
|
|
18
19
|
|
|
19
20
|
# Run a single tick (verifies everything still works)
|
|
20
21
|
imprint cron southwest --once
|