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,184 @@
|
|
|
1
|
+
toolName: search_flights
|
|
2
|
+
summary: "Search Google Flights for itineraries between two airports on given dates and trip type, returning the GetShoppingResults batchexecute payload."
|
|
3
|
+
parameters:
|
|
4
|
+
- name: origin
|
|
5
|
+
type: string
|
|
6
|
+
description: "Origin airport/city code, e.g. SJC"
|
|
7
|
+
- name: destination
|
|
8
|
+
type: string
|
|
9
|
+
description: "Destination airport/city code, e.g. SAN"
|
|
10
|
+
- name: departure_date
|
|
11
|
+
type: string
|
|
12
|
+
description: "Outbound date in YYYY-MM-DD"
|
|
13
|
+
- name: return_date
|
|
14
|
+
type: string
|
|
15
|
+
description: "Return date in YYYY-MM-DD (omit/ignore for one-way)"
|
|
16
|
+
- name: trip_type
|
|
17
|
+
type: string
|
|
18
|
+
description: "Visible ticket-type label to select: 'Round trip', 'One way', or 'Multi-city' (workflow encodes 1/2/3)"
|
|
19
|
+
- name: max_stops
|
|
20
|
+
type: number
|
|
21
|
+
description: "Max stops filter: 0 nonstop, 1, 2, or 3 any (best-effort via filter bar)"
|
|
22
|
+
- name: airlines
|
|
23
|
+
type: string
|
|
24
|
+
description: "Alliance (Oneworld/SkyTeam/Star Alliance) or carrier name to include (best-effort via filter bar)"
|
|
25
|
+
- name: max_price
|
|
26
|
+
type: number
|
|
27
|
+
description: "Maximum total price in USD (best-effort via filter bar)"
|
|
28
|
+
- name: outbound_times
|
|
29
|
+
type: string
|
|
30
|
+
description: "Outbound departure/arrival time window in hours (best-effort via filter bar)"
|
|
31
|
+
- name: return_times
|
|
32
|
+
type: string
|
|
33
|
+
description: "Return departure/arrival time window in hours (best-effort via filter bar)"
|
|
34
|
+
- name: max_duration
|
|
35
|
+
type: number
|
|
36
|
+
description: "Maximum total trip duration in minutes (best-effort via filter bar)"
|
|
37
|
+
- name: carry_on_bags
|
|
38
|
+
type: number
|
|
39
|
+
description: "Number of carry-on bags to filter/price by (best-effort via filter bar)"
|
|
40
|
+
steps:
|
|
41
|
+
- action: navigate
|
|
42
|
+
url: https://www.google.com/travel/flights
|
|
43
|
+
wait_for: networkidle
|
|
44
|
+
- action: click
|
|
45
|
+
locators:
|
|
46
|
+
- by: aria_label
|
|
47
|
+
value: "Where from?"
|
|
48
|
+
- by: css
|
|
49
|
+
value: 'div.cQnuXe.k0gFV input.II2One.j0Ppje'
|
|
50
|
+
wait_for:
|
|
51
|
+
sleep_ms: 300
|
|
52
|
+
- action: type
|
|
53
|
+
locators:
|
|
54
|
+
- by: aria_label
|
|
55
|
+
value_pattern: "Where (from\\?|else\\?)"
|
|
56
|
+
- by: css
|
|
57
|
+
value: 'div.cQnuXe.k0gFV input.II2One.j0Ppje'
|
|
58
|
+
value: ${origin}
|
|
59
|
+
wait_for:
|
|
60
|
+
sleep_ms: 500
|
|
61
|
+
- action: click
|
|
62
|
+
locators:
|
|
63
|
+
- by: aria_label
|
|
64
|
+
value_pattern: ${origin}
|
|
65
|
+
- by: text
|
|
66
|
+
value_pattern: ${origin}
|
|
67
|
+
wait_for: visible
|
|
68
|
+
- action: click
|
|
69
|
+
locators:
|
|
70
|
+
- by: aria_label
|
|
71
|
+
value: "Where to? "
|
|
72
|
+
- by: css
|
|
73
|
+
value: 'div.wUiEcc.SOcuWe input.II2One.j0Ppje'
|
|
74
|
+
wait_for:
|
|
75
|
+
sleep_ms: 300
|
|
76
|
+
- action: type
|
|
77
|
+
locators:
|
|
78
|
+
- by: aria_label
|
|
79
|
+
value_pattern: "Where (to\\? ?|else\\?)"
|
|
80
|
+
- by: css
|
|
81
|
+
value: 'div.wUiEcc.SOcuWe input.II2One.j0Ppje'
|
|
82
|
+
value: ${destination}
|
|
83
|
+
wait_for:
|
|
84
|
+
sleep_ms: 500
|
|
85
|
+
- action: click
|
|
86
|
+
locators:
|
|
87
|
+
- by: aria_label
|
|
88
|
+
value_pattern: ${destination}
|
|
89
|
+
- by: text
|
|
90
|
+
value_pattern: ${destination}
|
|
91
|
+
wait_for: visible
|
|
92
|
+
- action: click
|
|
93
|
+
locators:
|
|
94
|
+
- by: aria_label
|
|
95
|
+
value: "Select your ticket type."
|
|
96
|
+
- by: css
|
|
97
|
+
value: 'div.TQYpgc.gInvKb div.VfPpkd-aPP78e'
|
|
98
|
+
wait_for:
|
|
99
|
+
sleep_ms: 300
|
|
100
|
+
- action: click
|
|
101
|
+
locators:
|
|
102
|
+
- by: role
|
|
103
|
+
value: option
|
|
104
|
+
name: ${trip_type}
|
|
105
|
+
- by: text
|
|
106
|
+
value_pattern: ${trip_type}
|
|
107
|
+
wait_for: visible
|
|
108
|
+
- action: click
|
|
109
|
+
locators:
|
|
110
|
+
- by: aria_label
|
|
111
|
+
value: "Departure"
|
|
112
|
+
- by: css
|
|
113
|
+
value: 'div.GYgkab.YICvqf input.TP4Lpb.eoY5cb'
|
|
114
|
+
wait_for:
|
|
115
|
+
sleep_ms: 300
|
|
116
|
+
- action: type
|
|
117
|
+
locators:
|
|
118
|
+
- by: aria_label
|
|
119
|
+
value: "Departure"
|
|
120
|
+
- by: css
|
|
121
|
+
value: 'div.GYgkab.YICvqf input.TP4Lpb.eoY5cb'
|
|
122
|
+
value: ${departure_date}
|
|
123
|
+
wait_for:
|
|
124
|
+
sleep_ms: 400
|
|
125
|
+
- action: type
|
|
126
|
+
locators:
|
|
127
|
+
- by: aria_label
|
|
128
|
+
value: "Return"
|
|
129
|
+
value: ${return_date}
|
|
130
|
+
wait_for:
|
|
131
|
+
sleep_ms: 400
|
|
132
|
+
- action: click
|
|
133
|
+
locators:
|
|
134
|
+
- by: text
|
|
135
|
+
value: Done
|
|
136
|
+
- by: aria_label
|
|
137
|
+
value_pattern: "Done"
|
|
138
|
+
wait_for:
|
|
139
|
+
sleep_ms: 300
|
|
140
|
+
- action: click
|
|
141
|
+
locators:
|
|
142
|
+
- by: role
|
|
143
|
+
value: button
|
|
144
|
+
name: Search
|
|
145
|
+
- by: text
|
|
146
|
+
value: Search
|
|
147
|
+
- by: aria_label
|
|
148
|
+
value_pattern: "Search"
|
|
149
|
+
wait_for:
|
|
150
|
+
xhr: /FlightsFrontendService/GetShoppingResults
|
|
151
|
+
result:
|
|
152
|
+
source: xhr
|
|
153
|
+
url_pattern: /FlightsFrontendService/GetShoppingResults
|
|
154
|
+
extract: "2"
|
|
155
|
+
return_as: itineraries
|
|
156
|
+
notes: >
|
|
157
|
+
Google Flights is a batchexecute RPC app. The result-bearing XHR is
|
|
158
|
+
GetShoppingResults (POST to /_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults);
|
|
159
|
+
the largest/last GetShoppingResults response after the Search click carries the
|
|
160
|
+
full itinerary set. The response is NOT plain JSON: it is prefixed with )]}' and
|
|
161
|
+
chunked into length-prefixed lines. The parser must strip the )]}' prefix, split
|
|
162
|
+
on the numeric length markers, JSON.parse each line to find the ["wrb.fr",null,"<json-string>"]
|
|
163
|
+
envelope, then JSON.parse the index-2 element (the double-encoded payload). Flight
|
|
164
|
+
itineraries live in deeply nested positional arrays inside that payload — each
|
|
165
|
+
itinerary exposes carrier code+name, flight number, origin/destination codes,
|
|
166
|
+
[hour,minute] depart/arrive, duration minutes, stops, layover info, and price in
|
|
167
|
+
USD (the integer like 88008 = $880.08 scaled, or the [[null,337],...] cents-ish
|
|
168
|
+
marker). The extract path "2" denotes the wrb.fr payload slot and requires the
|
|
169
|
+
custom decoding above — a plain dot-path cannot traverse the stringified inner JSON.
|
|
170
|
+
DOM-locator caveats: this site uses auto-generated class names (e.g. VfPpkd-*,
|
|
171
|
+
II2One) and react-style ids (#c7293, #c115604) that change every deploy and per
|
|
172
|
+
session — prefer the aria_label/text/role locators; the css fallbacks are brittle.
|
|
173
|
+
The origin/destination editable field re-labels itself to "Where else?" while
|
|
174
|
+
focused, hence the regex aria_label patterns.
|
|
175
|
+
trip_type must be passed as the visible label ("Round trip" / "One way" /
|
|
176
|
+
"Multi-city"), not the 1/2/3 encoding used by the API workflow.
|
|
177
|
+
For one-way searches the "Return" type step is a no-op/absent — skip or expect it
|
|
178
|
+
to be ignored. Multi-city requires per-leg origin/destination entry not captured
|
|
179
|
+
as discrete linear steps here.
|
|
180
|
+
Filter parameters (max_stops, airlines, max_price, outbound_times, return_times,
|
|
181
|
+
max_duration, carry_on_bags) are applied via the chip filter bar (Stops/Airlines/
|
|
182
|
+
Bags/Price/Times/Duration buttons) which open popovers with obfuscated controls;
|
|
183
|
+
they are best applied through the API workflow rather than DOM steps. They are
|
|
184
|
+
listed here only to keep params 1:1 with workflow.json for cron/MCP fallback.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Adapter around the shared FlightsFrontendService body builder.
|
|
2
|
+
// The tool exposes flat snake_case params (origin, destination, departure_date,
|
|
3
|
+
// max_stops, …); the shared encoder consumes a structured camelCase shape
|
|
4
|
+
// ({ tripType, legs:[{origin,dest,date,times,stops,alliances,carriers,duration}],
|
|
5
|
+
// maxPrice, bags }). We map between them here and delegate the byte-for-byte
|
|
6
|
+
// positional encoding to the shared module (required reuse).
|
|
7
|
+
import { transform as sharedTransform } from '../_shared/flights_request.ts';
|
|
8
|
+
|
|
9
|
+
type Params = Record<string, string | number | boolean | undefined | null>;
|
|
10
|
+
|
|
11
|
+
const ALLIANCES = new Set(['ONEWORLD', 'SKYTEAM', 'STAR_ALLIANCE']);
|
|
12
|
+
|
|
13
|
+
function mapTripType(v: unknown): number {
|
|
14
|
+
if (v == null || v === '') return 1;
|
|
15
|
+
if (typeof v === 'number') return v;
|
|
16
|
+
const s = String(v).toLowerCase();
|
|
17
|
+
if (s === 'one_way' || s === 'oneway' || s === '2') return 2;
|
|
18
|
+
if (s === 'multi_city' || s === 'multicity' || s === '3') return 3;
|
|
19
|
+
return 1; // round_trip
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// User semantics (per likelyParam): 0=nonstop, 1=≤1 stop, 2=≤2 stops, 3=any.
|
|
23
|
+
// Google wire encoding: 1=nonstop, 2=≤1, 3=≤2, 0=any.
|
|
24
|
+
function mapStops(v: unknown): number {
|
|
25
|
+
switch (Number(v)) {
|
|
26
|
+
case 0:
|
|
27
|
+
return 1;
|
|
28
|
+
case 1:
|
|
29
|
+
return 2;
|
|
30
|
+
case 2:
|
|
31
|
+
return 3;
|
|
32
|
+
default:
|
|
33
|
+
return 0; // any (default / 3)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// "6-23" -> [depMin, depMax, arrMin, arrMax]; arrival defaults to full day.
|
|
38
|
+
function parseTimes(v: unknown): number[] | null {
|
|
39
|
+
if (v == null || v === '') return null;
|
|
40
|
+
const m = /^(\d{1,2})-(\d{1,2})$/.exec(String(v).trim());
|
|
41
|
+
if (!m) return null;
|
|
42
|
+
return [Number(m[1]), Number(m[2]), 0, 23];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseAirlines(v: unknown): { alliances: string[] | null; carriers: string[] | null } {
|
|
46
|
+
if (v == null || v === '') return { alliances: null, carriers: null };
|
|
47
|
+
const parts = String(v)
|
|
48
|
+
.split(',')
|
|
49
|
+
.map((x) => x.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
const alliances = parts.filter((p) => ALLIANCES.has(p.toUpperCase())).map((p) => p.toUpperCase());
|
|
52
|
+
const carriers = parts.filter((p) => !ALLIANCES.has(p.toUpperCase()));
|
|
53
|
+
return {
|
|
54
|
+
alliances: alliances.length ? alliances : null,
|
|
55
|
+
carriers: carriers.length ? carriers : null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function num(v: unknown): number | undefined {
|
|
60
|
+
if (v == null || v === '') return undefined;
|
|
61
|
+
const n = Number(v);
|
|
62
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function transform(
|
|
66
|
+
method: string,
|
|
67
|
+
url: string,
|
|
68
|
+
responses: Record<string, any>,
|
|
69
|
+
params?: Params,
|
|
70
|
+
): { url: string; body: string } {
|
|
71
|
+
const p: Params = params ?? {};
|
|
72
|
+
const tripType = mapTripType(p.trip_type);
|
|
73
|
+
const stops = p.max_stops != null && p.max_stops !== '' ? mapStops(p.max_stops) : 0;
|
|
74
|
+
const { alliances, carriers } = parseAirlines(p.airlines);
|
|
75
|
+
const maxDur = num(p.max_duration);
|
|
76
|
+
const duration = maxDur != null ? [maxDur] : null;
|
|
77
|
+
|
|
78
|
+
const origin = p.origin != null ? String(p.origin) : '';
|
|
79
|
+
const destination = p.destination != null ? String(p.destination) : '';
|
|
80
|
+
|
|
81
|
+
const legs: any[] = [
|
|
82
|
+
{
|
|
83
|
+
origin,
|
|
84
|
+
dest: destination,
|
|
85
|
+
date: p.departure_date ? String(p.departure_date) : null,
|
|
86
|
+
times: parseTimes(p.outbound_times),
|
|
87
|
+
stops,
|
|
88
|
+
alliances,
|
|
89
|
+
carriers,
|
|
90
|
+
duration,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// Append a return leg for round-trip / multi-city when a return date exists.
|
|
95
|
+
if (tripType !== 2 && p.return_date) {
|
|
96
|
+
legs.push({
|
|
97
|
+
origin: destination,
|
|
98
|
+
dest: origin,
|
|
99
|
+
date: String(p.return_date),
|
|
100
|
+
times: parseTimes(p.return_times),
|
|
101
|
+
stops,
|
|
102
|
+
alliances,
|
|
103
|
+
carriers,
|
|
104
|
+
duration,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const carryOn = num(p.carry_on_bags);
|
|
109
|
+
const mapped: Record<string, any> = {
|
|
110
|
+
tripType,
|
|
111
|
+
legs,
|
|
112
|
+
maxPrice: num(p.max_price),
|
|
113
|
+
// CONFIG[10] wire form is [1, <carry-on count>]; shared builder emits
|
|
114
|
+
// [carryOn, checked], so map count -> checked slot, constant 1 -> first.
|
|
115
|
+
bags: carryOn != null ? { carryOn: 1, checked: carryOn } : undefined,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return sharedTransform(method, url, responses, mapped);
|
|
119
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{
|
|
2
|
+
"toolName": "search_flights",
|
|
3
|
+
"site": "google-flights",
|
|
4
|
+
"intent": {
|
|
5
|
+
"description": "Search Google Flights for itineraries between two airports with dates, trip type, and filters (stops, airlines, price, times, duration, bags).",
|
|
6
|
+
"userSaid": "searched for a round trip flight; searched for a one way flight; played with the stops and the airlines filters; added bags; played with the price and time filters; played with the duration filter; did a multi city flight query"
|
|
7
|
+
},
|
|
8
|
+
"parameters": [
|
|
9
|
+
{
|
|
10
|
+
"name": "origin",
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Origin airport/city IATA code, e.g. SJC",
|
|
13
|
+
"verified": false,
|
|
14
|
+
"verifyNote": "annotated"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "destination",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Destination airport/city IATA code, e.g. SAN",
|
|
20
|
+
"verified": false,
|
|
21
|
+
"verifyNote": "annotated"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "departure_date",
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Outbound date in YYYY-MM-DD",
|
|
27
|
+
"verified": false,
|
|
28
|
+
"verifyNote": "annotated"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "return_date",
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Return date in YYYY-MM-DD (omit/empty for one way)",
|
|
34
|
+
"default": "",
|
|
35
|
+
"verified": false,
|
|
36
|
+
"verifyNote": "annotated"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "trip_type",
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "round_trip, one_way, or multi_city",
|
|
42
|
+
"default": "round_trip",
|
|
43
|
+
"verified": false,
|
|
44
|
+
"verifyNote": "annotated"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "max_stops",
|
|
48
|
+
"type": "number",
|
|
49
|
+
"description": "Max stops: 0=nonstop, 1=<=1 stop, 2=<=2 stops, 3=any",
|
|
50
|
+
"default": 3,
|
|
51
|
+
"verified": false,
|
|
52
|
+
"verifyNote": "annotated"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "airlines",
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Comma-separated alliance (ONEWORLD/SKYTEAM/STAR_ALLIANCE) or 2-letter carrier codes (e.g. AS,WN). Empty = no filter",
|
|
58
|
+
"default": "",
|
|
59
|
+
"verified": false,
|
|
60
|
+
"verifyNote": "annotated"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "max_price",
|
|
64
|
+
"type": "number",
|
|
65
|
+
"description": "Maximum total price in USD (0 = no filter)",
|
|
66
|
+
"default": 0,
|
|
67
|
+
"verified": false,
|
|
68
|
+
"verifyNote": "annotated"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "outbound_times",
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Outbound departure window in hours, e.g. '6-23' (empty = no filter)",
|
|
74
|
+
"default": "",
|
|
75
|
+
"verified": false,
|
|
76
|
+
"verifyNote": "annotated"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "return_times",
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Return departure window in hours, e.g. '6-23' (empty = no filter)",
|
|
82
|
+
"default": "",
|
|
83
|
+
"verified": false,
|
|
84
|
+
"verifyNote": "annotated"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "max_duration",
|
|
88
|
+
"type": "number",
|
|
89
|
+
"description": "Maximum total trip duration in minutes, e.g. 540 (0 = no filter)",
|
|
90
|
+
"default": 0,
|
|
91
|
+
"verified": false,
|
|
92
|
+
"verifyNote": "annotated"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "carry_on_bags",
|
|
96
|
+
"type": "number",
|
|
97
|
+
"description": "Number of carry-on bags to filter/price by (0 = no filter)",
|
|
98
|
+
"default": 0,
|
|
99
|
+
"verified": false,
|
|
100
|
+
"verifyNote": "annotated"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"bootstrap": {
|
|
104
|
+
"url": "https://www.google.com/travel/flights",
|
|
105
|
+
"waitUntil": "domcontentloaded",
|
|
106
|
+
"timeoutMs": 30000,
|
|
107
|
+
"captures": [
|
|
108
|
+
{
|
|
109
|
+
"source": "html_regex",
|
|
110
|
+
"name": "f_sid",
|
|
111
|
+
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
112
|
+
"group": 1,
|
|
113
|
+
"required": false,
|
|
114
|
+
"capability": "browser_bootstrap"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"source": "html_regex",
|
|
118
|
+
"name": "bl",
|
|
119
|
+
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
120
|
+
"group": 1,
|
|
121
|
+
"required": false,
|
|
122
|
+
"capability": "browser_bootstrap"
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
"requests": [
|
|
127
|
+
{
|
|
128
|
+
"method": "POST",
|
|
129
|
+
"url": "https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults?f.sid=${state.f_sid}&bl=${state.bl}&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=1708023&rt=c",
|
|
130
|
+
"headers": {
|
|
131
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
132
|
+
"X-Same-Domain": "1",
|
|
133
|
+
"Referer": "https://www.google.com/travel/flights",
|
|
134
|
+
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
|
|
135
|
+
},
|
|
136
|
+
"body": "f.req=${param.origin}|${param.destination}|${param.departure_date}|${param.return_date}|${param.trip_type}|${param.max_stops}|${param.airlines}|${param.max_price}|${param.outbound_times}|${param.return_times}|${param.max_duration}|${param.carry_on_bags}&",
|
|
137
|
+
"effect": "safe"
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
"requestTransformModule": "./request-transform.ts",
|
|
141
|
+
"parserModule": "./parser.ts",
|
|
142
|
+
"liveVerified": true
|
|
143
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Google Hotels — `imprint-google-hotels`
|
|
2
|
+
|
|
3
|
+
> **One-shot compiled, proof of concept.** Every file in this directory was generated by a single `imprint teach google-hotels` run against **one** recorded browser session — no hand-written request code, parsers, or selectors. It is committed here as a proof of concept of what the compiler produces, not as a maintained integration.
|
|
4
|
+
|
|
5
|
+
A 4-tool MCP server for Google Hotels, compiled from a recording of a normal hotel search. Headless-claude differential audit: **91.7%** — every tool `liveVerified=true`.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
| Tool | What it does | Notes |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `autocomplete_hotel_location` | Resolve a location query to a place token | |
|
|
12
|
+
| `search_hotels` | Search hotels (location, dates, guests, rating, price, amenities, class, sort, property type) | the star tool |
|
|
13
|
+
| `get_hotel_reviews` | Reviews for a hotel | **consumes** a `hotel_id` produced by `search_hotels` |
|
|
14
|
+
| `get_hotel_booking_options` | Booking/price options for a hotel | **consumes** a `location_context` produced by `search_hotels` |
|
|
15
|
+
|
|
16
|
+
## How it was compiled
|
|
17
|
+
|
|
18
|
+
- **Protocol**: Google's `batchexecute` endpoint returns a nested-array payload; the compiler reverse-engineered the decoder into `_shared/batchexecute.ts` + per-tool `parser.ts`, and the request shape into per-tool `request-transform.ts`.
|
|
19
|
+
- **Producer → consumer chaining**: `search_hotels` emits opaque ids that `get_hotel_reviews` / `get_hotel_booking_options` consume — the compiler detected the cross-tool token flow and wired it.
|
|
20
|
+
- **Anti-bot**: calls run on the **cdp-replay** rung (inside a live trusted Chrome) with a **stealth-fetch** fallback.
|
|
21
|
+
- **Artifacts per tool**: `workflow.json` (API replay), `playbook.yaml` (DOM fallback), `index.ts` (MCP tool), `parser.ts` + `request-transform.ts` (codecs).
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
imprint install google-hotels --source examples --platform claude-desktop
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
*Recording-derived defaults (dates, the recorded location) age out — pass explicit values. See the repo [README](../../README.md) and [docs](../../docs/architecture.md).*
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Decodes the Google `batchexecute` anti-XSSI envelope and returns the inner
|
|
2
|
+
// JSON payload for a given rpcid. Every TravelFrontendUi response shares this
|
|
3
|
+
// framing:
|
|
4
|
+
//
|
|
5
|
+
// )]}' <- anti-XSSI guard line
|
|
6
|
+
// <- blank line
|
|
7
|
+
// <len> <- chunk length (UTF-8 BYTE count)
|
|
8
|
+
// [["wrb.fr","<rpcid>","<innerJsonString>",null,null,null,"<src>"]]
|
|
9
|
+
// <len>
|
|
10
|
+
// [["di",..],["af.httprm",..]]
|
|
11
|
+
// ...
|
|
12
|
+
//
|
|
13
|
+
// Key invariants proven by the recordings (seq 222, 229, 286, 300, 497, 525, 2429):
|
|
14
|
+
// - Each JSON chunk sits on ONE physical line; every newline inside string data
|
|
15
|
+
// is escaped (\n -> \\n), so splitting on "\n" cleanly separates length
|
|
16
|
+
// markers from JSON chunks.
|
|
17
|
+
// - A single chunk can carry MULTIPLE rows of mixed type (seq 2429 packs the
|
|
18
|
+
// wrb.fr row alongside ["di",..]/["af.httprm",..]), so we must filter rows by
|
|
19
|
+
// row[0] === "wrb.fr", never assume one row per chunk.
|
|
20
|
+
// - row[2] is the payload as a JSON STRING -> a second JSON.parse yields the
|
|
21
|
+
// result array. \u00xx / \u0026 escapes (Priceline URLs, seq 497) are valid
|
|
22
|
+
// JSON escapes resolved natively by JSON.parse -- no manual unescaping.
|
|
23
|
+
//
|
|
24
|
+
// We deliberately ignore the numeric chunk lengths: they are UTF-8 byte counts,
|
|
25
|
+
// while JS string slicing is by UTF-16 code unit, so honoring them would
|
|
26
|
+
// misalign on multibyte data (e.g. "Costa Rican Colón" in seq 229). Splitting on
|
|
27
|
+
// "\n" is safe because chunks never contain a literal newline.
|
|
28
|
+
|
|
29
|
+
// Collect every envelope row across all chunks. Kept as any[] on purpose:
|
|
30
|
+
// JSON.parse returns `any`, and indexed access on `any` does NOT widen to
|
|
31
|
+
// `T | undefined` under noUncheckedIndexedAccess, so r[0]/r[1]/r[2] stay clean.
|
|
32
|
+
function collectRows(rawResponse: string): any[] {
|
|
33
|
+
const rows: any[] = [];
|
|
34
|
+
const lines = rawResponse.split('\n');
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (line === ")]}'" || line === '') continue; // guard line / blank separator
|
|
37
|
+
if (/^\d+$/.test(line)) continue; // numeric chunk-length marker (.test -> boolean, no capture)
|
|
38
|
+
if (!line.startsWith('[')) continue; // anything else is not a JSON chunk
|
|
39
|
+
let parsed: any;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(line);
|
|
42
|
+
} catch {
|
|
43
|
+
continue; // skip an unparseable / truncated chunk rather than throwing
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(parsed)) {
|
|
46
|
+
for (const row of parsed) rows.push(row);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseBatchExecute(rawResponse: string, rpcid: string): any {
|
|
53
|
+
const rows = collectRows(rawResponse);
|
|
54
|
+
const hit = rows.find(
|
|
55
|
+
(r: any) => Array.isArray(r) && r[0] === 'wrb.fr' && r[1] === rpcid,
|
|
56
|
+
);
|
|
57
|
+
// Missing rpcid (or non-string payload) returns null per spec -- do not throw.
|
|
58
|
+
// The typeof guard also narrows hit[2] for the JSON.parse below.
|
|
59
|
+
if (!hit || typeof hit[2] !== 'string') return null;
|
|
60
|
+
return JSON.parse(hit[2]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseAllRpc(rawResponse: string): Record<string, any> {
|
|
64
|
+
const rows = collectRows(rawResponse);
|
|
65
|
+
const out: Record<string, any> = {};
|
|
66
|
+
for (const r of rows) {
|
|
67
|
+
if (Array.isArray(r) && r[0] === 'wrb.fr' && typeof r[2] === 'string') {
|
|
68
|
+
// Last write wins on duplicate rpcid (not observed in recordings).
|
|
69
|
+
out[r[1]] = JSON.parse(r[2]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|