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,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
+ }