imprint-mcp 0.5.0 → 0.6.0

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 (202) hide show
  1. package/README.md +6 -4
  2. package/examples/discoverandgo/README.md +1 -1
  3. package/examples/discoverandgo/book_discoverandgo_museum_pass/workflow.json +7 -0
  4. package/examples/google-flights/README.md +41 -14
  5. package/examples/google-flights/_shared/google_batchexecute_parser.ts +165 -0
  6. package/examples/google-flights/_shared/google_flights_transport.ts +70 -0
  7. package/examples/google-flights/_shared/google_flights_types.ts +56 -0
  8. package/examples/google-flights/get_flight_booking_options/backends.json +43 -0
  9. package/examples/google-flights/get_flight_booking_options/index.ts +129 -0
  10. package/examples/google-flights/get_flight_booking_options/parser.ts +209 -0
  11. package/examples/google-flights/get_flight_booking_options/playbook.yaml +46 -0
  12. package/examples/google-flights/get_flight_booking_options/request-transform.ts +210 -0
  13. package/examples/google-flights/get_flight_booking_options/workflow.json +85 -0
  14. package/examples/google-flights/get_flight_calendar_prices/backends.json +27 -0
  15. package/examples/google-flights/get_flight_calendar_prices/index.ts +44 -29
  16. package/examples/google-flights/get_flight_calendar_prices/parser.ts +76 -69
  17. package/examples/google-flights/get_flight_calendar_prices/playbook.yaml +67 -40
  18. package/examples/google-flights/get_flight_calendar_prices/workflow.json +29 -17
  19. package/examples/google-flights/get_flight_location_details/backends.json +27 -0
  20. package/examples/google-flights/{lookup_airport → get_flight_location_details}/index.ts +30 -20
  21. package/examples/google-flights/get_flight_location_details/parser.ts +103 -0
  22. package/examples/google-flights/get_flight_location_details/playbook.yaml +59 -0
  23. package/examples/google-flights/{lookup_airport → get_flight_location_details}/workflow.json +19 -12
  24. package/examples/google-flights/lookup_flight_locations/backends.json +27 -0
  25. package/examples/google-flights/lookup_flight_locations/index.ts +111 -0
  26. package/examples/google-flights/lookup_flight_locations/package.json +9 -0
  27. package/examples/google-flights/lookup_flight_locations/parser.ts +135 -0
  28. package/examples/google-flights/lookup_flight_locations/playbook.yaml +47 -0
  29. package/examples/google-flights/lookup_flight_locations/request-transform.ts +53 -0
  30. package/examples/google-flights/lookup_flight_locations/workflow.json +64 -0
  31. package/examples/google-flights/search_flights/backends.json +42 -0
  32. package/examples/google-flights/search_flights/index.ts +105 -69
  33. package/examples/google-flights/search_flights/parser.ts +186 -183
  34. package/examples/google-flights/search_flights/playbook.yaml +295 -88
  35. package/examples/google-flights/search_flights/request-transform.ts +247 -92
  36. package/examples/google-flights/search_flights/workflow.json +66 -40
  37. package/examples/google-flights/validate_flight_itinerary/backends.json +27 -0
  38. package/examples/google-flights/validate_flight_itinerary/index.ts +103 -0
  39. package/examples/google-flights/validate_flight_itinerary/package.json +9 -0
  40. package/examples/google-flights/validate_flight_itinerary/parser.ts +101 -0
  41. package/examples/google-flights/validate_flight_itinerary/playbook.yaml +84 -0
  42. package/examples/google-flights/validate_flight_itinerary/request-transform.ts +92 -0
  43. package/examples/google-flights/validate_flight_itinerary/workflow.json +59 -0
  44. package/examples/google-hotels/README.md +40 -15
  45. package/examples/google-hotels/get_hotel_details/backends.json +16 -0
  46. package/examples/google-hotels/get_hotel_details/index.ts +122 -0
  47. package/examples/google-hotels/get_hotel_details/parser.ts +275 -0
  48. package/examples/google-hotels/get_hotel_details/playbook.yaml +40 -0
  49. package/examples/google-hotels/get_hotel_details/request-transform.ts +67 -0
  50. package/examples/google-hotels/get_hotel_details/workflow.json +67 -0
  51. package/examples/google-hotels/get_hotel_photos/backends.json +16 -0
  52. package/examples/google-hotels/get_hotel_photos/index.ts +90 -0
  53. package/examples/google-hotels/get_hotel_photos/parser.ts +75 -0
  54. package/examples/google-hotels/get_hotel_photos/playbook.yaml +23 -0
  55. package/examples/google-hotels/get_hotel_photos/request-transform.ts +27 -0
  56. package/examples/google-hotels/get_hotel_photos/workflow.json +44 -0
  57. package/examples/google-hotels/get_hotel_reviews/backends.json +16 -0
  58. package/examples/google-hotels/get_hotel_reviews/index.ts +14 -17
  59. package/examples/google-hotels/get_hotel_reviews/parser.ts +177 -105
  60. package/examples/google-hotels/get_hotel_reviews/playbook.yaml +9 -54
  61. package/examples/google-hotels/get_hotel_reviews/workflow.json +10 -13
  62. package/examples/google-hotels/get_travel_area_boundaries/backends.json +16 -0
  63. package/examples/google-hotels/get_travel_area_boundaries/index.ts +104 -0
  64. package/examples/google-hotels/get_travel_area_boundaries/package.json +9 -0
  65. package/examples/google-hotels/get_travel_area_boundaries/parser.ts +127 -0
  66. package/examples/google-hotels/get_travel_area_boundaries/playbook.yaml +38 -0
  67. package/examples/google-hotels/get_travel_area_boundaries/request-transform.ts +52 -0
  68. package/examples/google-hotels/get_travel_area_boundaries/workflow.json +54 -0
  69. package/examples/google-hotels/list_hotel_booking_options/backends.json +16 -0
  70. package/examples/google-hotels/list_hotel_booking_options/index.ts +123 -0
  71. package/examples/google-hotels/list_hotel_booking_options/package.json +9 -0
  72. package/examples/google-hotels/list_hotel_booking_options/parser.ts +308 -0
  73. package/examples/google-hotels/list_hotel_booking_options/playbook.yaml +95 -0
  74. package/examples/google-hotels/list_hotel_booking_options/request-transform.ts +77 -0
  75. package/examples/google-hotels/list_hotel_booking_options/workflow.json +67 -0
  76. package/examples/google-hotels/search_hotel_web_links/backends.json +16 -0
  77. package/examples/google-hotels/search_hotel_web_links/index.ts +90 -0
  78. package/examples/google-hotels/search_hotel_web_links/package.json +9 -0
  79. package/examples/google-hotels/search_hotel_web_links/parser.ts +96 -0
  80. package/examples/google-hotels/search_hotel_web_links/playbook.yaml +53 -0
  81. package/examples/google-hotels/search_hotel_web_links/request-transform.ts +17 -0
  82. package/examples/google-hotels/search_hotel_web_links/workflow.json +43 -0
  83. package/examples/google-hotels/search_hotels/backends.json +16 -0
  84. package/examples/google-hotels/search_hotels/index.ts +32 -102
  85. package/examples/google-hotels/search_hotels/parser.ts +237 -216
  86. package/examples/google-hotels/search_hotels/playbook.yaml +177 -42
  87. package/examples/google-hotels/search_hotels/request-transform.ts +101 -170
  88. package/examples/google-hotels/search_hotels/workflow.json +24 -73
  89. package/examples/google-hotels/suggest_hotel_destinations/backends.json +16 -0
  90. package/examples/google-hotels/{autocomplete_hotel_location → suggest_hotel_destinations}/index.ts +30 -19
  91. package/examples/google-hotels/suggest_hotel_destinations/package.json +9 -0
  92. package/examples/google-hotels/suggest_hotel_destinations/parser.ts +138 -0
  93. package/examples/google-hotels/suggest_hotel_destinations/playbook.yaml +47 -0
  94. package/examples/google-hotels/suggest_hotel_destinations/request-transform.ts +33 -0
  95. package/examples/google-hotels/suggest_hotel_destinations/workflow.json +44 -0
  96. package/examples/southwest/README.md +31 -70
  97. package/examples/southwest/get_account_details/backends.json +22 -0
  98. package/examples/southwest/get_account_details/credentials.manifest.json +17 -0
  99. package/examples/southwest/get_account_details/index.ts +138 -0
  100. package/examples/{google-hotels/_shared → southwest/get_account_details}/package.json +2 -2
  101. package/examples/southwest/get_account_details/parser.ts +136 -0
  102. package/examples/southwest/get_account_details/playbook.yaml +19 -0
  103. package/examples/southwest/get_account_details/workflow.json +93 -0
  104. package/examples/southwest/get_flight_status/backends.json +21 -0
  105. package/examples/southwest/get_flight_status/index.ts +130 -0
  106. package/examples/southwest/get_flight_status/package.json +9 -0
  107. package/examples/southwest/get_flight_status/parser.ts +138 -0
  108. package/examples/southwest/get_flight_status/playbook.yaml +58 -0
  109. package/examples/southwest/get_flight_status/workflow.json +77 -0
  110. package/examples/southwest/get_low_fare_calendar/backends.json +21 -0
  111. package/examples/southwest/get_low_fare_calendar/index.ts +134 -0
  112. package/examples/southwest/get_low_fare_calendar/package.json +9 -0
  113. package/examples/southwest/get_low_fare_calendar/parser.ts +107 -0
  114. package/examples/southwest/get_low_fare_calendar/playbook.yaml +41 -0
  115. package/examples/southwest/get_low_fare_calendar/workflow.json +81 -0
  116. package/examples/southwest/list_upcoming_trips/backends.json +22 -0
  117. package/examples/southwest/list_upcoming_trips/credentials.manifest.json +17 -0
  118. package/examples/southwest/list_upcoming_trips/index.ts +123 -0
  119. package/examples/southwest/list_upcoming_trips/package.json +9 -0
  120. package/examples/southwest/list_upcoming_trips/parser.ts +160 -0
  121. package/examples/southwest/list_upcoming_trips/playbook.yaml +24 -0
  122. package/examples/southwest/list_upcoming_trips/workflow.json +78 -0
  123. package/examples/southwest/search_flights/backends.json +21 -0
  124. package/examples/southwest/search_flights/index.ts +150 -0
  125. package/examples/southwest/search_flights/package.json +9 -0
  126. package/examples/southwest/search_flights/parser.ts +214 -0
  127. package/examples/southwest/search_flights/playbook.yaml +52 -0
  128. package/examples/southwest/search_flights/workflow.json +88 -0
  129. package/package.json +1 -1
  130. package/prompts/audit-agent.md +2 -1
  131. package/prompts/build-planning.md +22 -1
  132. package/prompts/compile-agent.md +66 -53
  133. package/prompts/prereq-builder.md +3 -1
  134. package/prompts/tool-planning.md +2 -0
  135. package/src/cli.ts +8 -3
  136. package/src/imprint/audit.ts +337 -44
  137. package/src/imprint/backend-cache.ts +114 -0
  138. package/src/imprint/backend-ladder.ts +74 -24
  139. package/src/imprint/bot-defense.ts +12 -0
  140. package/src/imprint/build-plan.ts +346 -53
  141. package/src/imprint/cdp-browser-fetch.ts +96 -43
  142. package/src/imprint/cdp-jar-cache.ts +6 -5
  143. package/src/imprint/chromium.ts +34 -0
  144. package/src/imprint/codex-cli-compile.ts +13 -2
  145. package/src/imprint/compile-agent.ts +3 -1
  146. package/src/imprint/compile-tools.ts +715 -138
  147. package/src/imprint/compile.ts +41 -9
  148. package/src/imprint/concurrency.ts +26 -3
  149. package/src/imprint/cron.ts +0 -3
  150. package/src/imprint/endpoint-key.ts +23 -0
  151. package/src/imprint/llm-url.ts +69 -0
  152. package/src/imprint/llm.ts +366 -47
  153. package/src/imprint/login.ts +83 -75
  154. package/src/imprint/mcp-compile-server.ts +4 -1
  155. package/src/imprint/mcp-server.ts +51 -16
  156. package/src/imprint/param-grounding.ts +61 -52
  157. package/src/imprint/probe-backends.ts +21 -115
  158. package/src/imprint/redact.ts +17 -15
  159. package/src/imprint/request-capture.ts +94 -0
  160. package/src/imprint/runtime.ts +1 -78
  161. package/src/imprint/session-diff.ts +14 -1
  162. package/src/imprint/stealth-fetch.ts +30 -20
  163. package/src/imprint/stealth-token-cache.ts +2 -2
  164. package/src/imprint/teach-plan.ts +2 -0
  165. package/src/imprint/teach-state.ts +14 -9
  166. package/src/imprint/teach.ts +41 -7
  167. package/src/imprint/telemetry.ts +3 -0
  168. package/src/imprint/tool-candidates.ts +8 -7
  169. package/src/imprint/tool-plan.ts +29 -3
  170. package/src/imprint/tracing.ts +18 -2
  171. package/examples/google-flights/_shared/batchexecute.ts +0 -63
  172. package/examples/google-flights/_shared/flights_request.ts +0 -97
  173. package/examples/google-flights/get_flight_booking_details/index.ts +0 -159
  174. package/examples/google-flights/get_flight_booking_details/parser.ts +0 -182
  175. package/examples/google-flights/get_flight_booking_details/playbook.yaml +0 -138
  176. package/examples/google-flights/get_flight_booking_details/request-transform.ts +0 -86
  177. package/examples/google-flights/get_flight_booking_details/workflow.json +0 -98
  178. package/examples/google-flights/get_flight_calendar_prices/request-transform.ts +0 -31
  179. package/examples/google-flights/lookup_airport/parser.ts +0 -66
  180. package/examples/google-flights/lookup_airport/playbook.yaml +0 -47
  181. package/examples/google-flights/lookup_airport/request-transform.ts +0 -20
  182. package/examples/google-hotels/_shared/batchexecute.ts +0 -73
  183. package/examples/google-hotels/_shared/freq.ts +0 -158
  184. package/examples/google-hotels/autocomplete_hotel_location/parser.ts +0 -71
  185. package/examples/google-hotels/autocomplete_hotel_location/playbook.yaml +0 -36
  186. package/examples/google-hotels/autocomplete_hotel_location/request-transform.ts +0 -37
  187. package/examples/google-hotels/autocomplete_hotel_location/workflow.json +0 -36
  188. package/examples/google-hotels/get_hotel_booking_options/index.ts +0 -143
  189. package/examples/google-hotels/get_hotel_booking_options/parser.ts +0 -271
  190. package/examples/google-hotels/get_hotel_booking_options/playbook.yaml +0 -154
  191. package/examples/google-hotels/get_hotel_booking_options/request-transform.ts +0 -154
  192. package/examples/google-hotels/get_hotel_booking_options/workflow.json +0 -84
  193. package/examples/google-hotels/get_hotel_reviews/request-transform.ts +0 -42
  194. package/examples/southwest/search_southwest_flights/backends.json +0 -23
  195. package/examples/southwest/search_southwest_flights/cron.json +0 -19
  196. package/examples/southwest/search_southwest_flights/index.ts +0 -127
  197. package/examples/southwest/search_southwest_flights/playbook.yaml +0 -46
  198. package/examples/southwest/search_southwest_flights/workflow.json +0 -71
  199. /package/examples/google-flights/{get_flight_booking_details → get_flight_booking_options}/package.json +0 -0
  200. /package/examples/google-flights/{lookup_airport → get_flight_location_details}/package.json +0 -0
  201. /package/examples/google-hotels/{autocomplete_hotel_location → get_hotel_details}/package.json +0 -0
  202. /package/examples/google-hotels/{get_hotel_booking_options → get_hotel_photos}/package.json +0 -0
@@ -0,0 +1,209 @@
1
+ import { extractWrbRecords, parseNestedPayload } from '../_shared/google_batchexecute_parser.ts';
2
+ import type { BookingOption, FlightLeg } from '../_shared/google_flights_types.ts';
3
+
4
+ type JsonArray = unknown[];
5
+
6
+ type Output = {
7
+ selectedItinerary: {
8
+ legs: FlightLeg[];
9
+ carriers: string[];
10
+ };
11
+ bookingOptions: BookingOption[];
12
+ warnings?: string[];
13
+ };
14
+
15
+ function isArray(value: unknown): value is JsonArray {
16
+ return Array.isArray(value);
17
+ }
18
+
19
+ function asString(value: unknown): string | undefined {
20
+ return typeof value === 'string' && value.trim() ? value : undefined;
21
+ }
22
+
23
+ function asNumber(value: unknown): number | undefined {
24
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
25
+ }
26
+
27
+ function formatDate(value: unknown): string | undefined {
28
+ if (!isArray(value) || value.length < 3) return undefined;
29
+ const [year, month, day] = value.map(Number);
30
+ if (!year || !month || !day) return undefined;
31
+ return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
32
+ }
33
+
34
+ function formatTime(value: unknown): string | undefined {
35
+ if (!isArray(value) || value.length === 0) return undefined;
36
+ const hour = Number(value[0]);
37
+ const minute = Number(value[1] ?? 0);
38
+ if (!Number.isFinite(hour) || !Number.isFinite(minute)) return undefined;
39
+ return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
40
+ }
41
+
42
+ function walk(value: unknown, visit: (node: JsonArray) => void): void {
43
+ if (!isArray(value)) return;
44
+ visit(value);
45
+ for (const child of value) walk(child, visit);
46
+ }
47
+
48
+ function looksLikeCarrierGroup(node: JsonArray): boolean {
49
+ return typeof node[0] === 'string' && isArray(node[1]) && isArray(node[2]) && node[2].some((segment) => isArray(segment) && typeof segment[3] === 'string' && typeof segment[6] === 'string');
50
+ }
51
+
52
+ function legFromSegment(segment: JsonArray, fallbackAirline?: string): FlightLeg | null {
53
+ const origin = asString(segment[3]);
54
+ const destination = asString(segment[6]);
55
+ if (!origin || !destination) return null;
56
+ const marketing = isArray(segment[22]) ? segment[22] : [];
57
+ const operating = isArray(segment[15]) && isArray(segment[15][0]) ? segment[15][0] as JsonArray : [];
58
+ return {
59
+ origin,
60
+ destination,
61
+ departureDate: formatDate(segment[20]),
62
+ departureTime: formatTime(segment[8]),
63
+ arrivalDate: formatDate(segment[21]),
64
+ arrivalTime: formatTime(segment[10]),
65
+ airline: asString(marketing[3]) ?? fallbackAirline,
66
+ carrierCode: asString(marketing[0]) ?? asString(operating[0]),
67
+ flightNumber: asString(marketing[1]) ?? asString(operating[1]),
68
+ durationMinutes: asNumber(segment[11]),
69
+ };
70
+ }
71
+
72
+ function collectLegs(payloads: unknown[]): FlightLeg[] {
73
+ const seen = new Set<string>();
74
+ const legs: FlightLeg[] = [];
75
+ for (const payload of payloads) {
76
+ walk(payload, (node) => {
77
+ if (!looksLikeCarrierGroup(node)) return;
78
+ const airline = isArray(node[1]) ? asString(node[1][0]) : undefined;
79
+ for (const segment of node[2] as JsonArray[]) {
80
+ if (!isArray(segment)) continue;
81
+ const leg = legFromSegment(segment, airline);
82
+ if (!leg) continue;
83
+ const key = [leg.origin, leg.destination, leg.departureDate, leg.carrierCode, leg.flightNumber].join('|');
84
+ if (seen.has(key)) continue;
85
+ seen.add(key);
86
+ legs.push(leg);
87
+ }
88
+ });
89
+ }
90
+ return legs;
91
+ }
92
+
93
+ function providerFromOption(option: JsonArray): string | undefined {
94
+ const providerGroup = isArray(option[1]) && isArray(option[1][0]) ? option[1][0] as JsonArray : undefined;
95
+ if (!providerGroup) return undefined;
96
+ return asString(providerGroup[1]) ?? asString(providerGroup[0]);
97
+ }
98
+
99
+ function codeFromProvider(option: JsonArray): string | undefined {
100
+ const providerGroup = isArray(option[1]) && isArray(option[1][0]) ? option[1][0] as JsonArray : undefined;
101
+ return providerGroup ? asString(providerGroup[0]) : undefined;
102
+ }
103
+
104
+ function priceFromOption(option: JsonArray): number | undefined {
105
+ const explicit = isArray(option[7]) && isArray(option[7][0]) ? asNumber((option[7][0] as JsonArray)[1]) : undefined;
106
+ if (explicit !== undefined) return explicit;
107
+ let best: number | undefined;
108
+ walk(option, (node) => {
109
+ if (node.length === 2 && node[0] === null && typeof node[1] === 'number' && node[1] > 0 && node[1] < 10000) {
110
+ best = best === undefined ? node[1] as number : Math.min(best, node[1] as number);
111
+ }
112
+ });
113
+ return best;
114
+ }
115
+
116
+ function bookingUrlFromOption(option: JsonArray): string | undefined {
117
+ let found: string | undefined;
118
+ walk(option, (node) => {
119
+ if (found) return;
120
+ if (node[0] === 'https://www.google.com/travel/clk/f') found = node[0];
121
+ for (const value of node) {
122
+ if (typeof value === 'string' && /^https?:\/\//.test(value) && !value.includes('accessible-services')) {
123
+ found = value;
124
+ return;
125
+ }
126
+ }
127
+ });
128
+ return found;
129
+ }
130
+
131
+ function collectStrings(value: unknown): string[] {
132
+ const strings = new Set<string>();
133
+ walk(value, (node) => {
134
+ for (const item of node) {
135
+ if (typeof item !== 'string') continue;
136
+ const text = item.trim();
137
+ if (!text) continue;
138
+ if (text.length > 80) continue;
139
+ if (/^https?:\/\//.test(text) || text.includes('/travel/flights')) continue;
140
+ if (/^[A-Za-z0-9_-]{24,}$/.test(text)) continue;
141
+ strings.add(text);
142
+ }
143
+ });
144
+ return [...strings];
145
+ }
146
+
147
+ function looksLikeBookingOption(node: JsonArray): boolean {
148
+ if (node[0] !== 0) return false;
149
+ if (!providerFromOption(node)) return false;
150
+ if (!isArray(node[3]) || !node[3].some((flight) => isArray(flight) && typeof flight[0] === 'string' && String(flight[1] ?? '').length > 0)) return false;
151
+ return isArray(node[5]) || priceFromOption(node) !== undefined;
152
+ }
153
+
154
+ function collectBookingOptions(payloads: unknown[], legs: FlightLeg[]): BookingOption[] {
155
+ const options: BookingOption[] = [];
156
+ const seen = new Set<string>();
157
+ for (const payload of payloads) {
158
+ walk(payload, (node) => {
159
+ if (!looksLikeBookingOption(node)) return;
160
+ const provider = providerFromOption(node);
161
+ const price = priceFromOption(node);
162
+ const bookingUrl = bookingUrlFromOption(node);
163
+ const code = codeFromProvider(node);
164
+ const strings = collectStrings(node);
165
+ const fareNotes = strings.filter((text) => {
166
+ if (text === provider || text === code) return false;
167
+ if (/^[A-Z0-9]{2,6}$/.test(text)) return false;
168
+ if (/^www\./.test(text)) return false;
169
+ return ['Basic', 'Choice', 'Choice Preferred', 'Choice Extra', 'Saver', 'Main', 'First'].includes(text) || /bag|seat|basic|main|first|choice|saver/i.test(text);
170
+ });
171
+ const optionLegs = legs.length ? legs : [];
172
+ if (!provider && price === undefined && !bookingUrl) return;
173
+ const key = `${provider ?? ''}|${price ?? ''}|${bookingUrl ?? ''}|${fareNotes.join(',')}`;
174
+ if (seen.has(key)) return;
175
+ seen.add(key);
176
+ options.push({
177
+ provider,
178
+ price,
179
+ currency: price !== undefined ? 'USD' : undefined,
180
+ bookingUrl,
181
+ fareNotes,
182
+ restrictions: strings.filter((text) => /restriction|refund|change|carry|bag/i.test(text) && !fareNotes.includes(text)),
183
+ legs: optionLegs,
184
+ });
185
+ });
186
+ }
187
+ return options;
188
+ }
189
+
190
+ function parsePayloads(rawResponse: unknown): unknown[] {
191
+ if (typeof rawResponse !== 'string') return [rawResponse];
192
+ const records = extractWrbRecords(rawResponse);
193
+ return records.map((record) => parseNestedPayload(record.payload)).filter(Boolean);
194
+ }
195
+
196
+ export function extract(rawResponse: unknown): Output {
197
+ const payloads = parsePayloads(rawResponse);
198
+ const legs = collectLegs(payloads);
199
+ const bookingOptions = collectBookingOptions(payloads, legs);
200
+ const carriers = [...new Set(legs.map((leg) => leg.airline ?? leg.carrierCode).filter((value): value is string => Boolean(value)))];
201
+ return {
202
+ selectedItinerary: {
203
+ legs,
204
+ carriers,
205
+ },
206
+ bookingOptions,
207
+ warnings: bookingOptions.length === 0 ? ['No booking options were returned for this selected itinerary.'] : undefined,
208
+ };
209
+ }
@@ -0,0 +1,46 @@
1
+ toolName: get_flight_booking_options
2
+ summary: Get booking and fare details for a selected Google Flights itinerary.
3
+ parameters:
4
+ - name: selection_token
5
+ type: string
6
+ description: Encoded flight selection token returned by search results for the chosen itinerary or leg.
7
+ - name: trip_type
8
+ type: string
9
+ description: Ticket type for the selected itinerary, such as one_way, round_trip, or multi_city.
10
+ - name: selected_flights
11
+ type: string
12
+ description: Comma-separated selected leg identifiers such as carrier, flight number, date, origin, and destination when required.
13
+ steps:
14
+ - action: navigate
15
+ url: "https://www.google.com/travel/flights"
16
+ wait_for: networkidle
17
+ - action: click
18
+ locators:
19
+ - by: role
20
+ value: button
21
+ name: "${trip_type}"
22
+ - by: aria_label
23
+ value_pattern: "${trip_type}"
24
+ - by: text
25
+ value_pattern: "${trip_type}"
26
+ - by: css
27
+ value: "div.TQYpgc.gInvKb"
28
+ wait_for:
29
+ sleep_ms: 300
30
+ - action: click
31
+ locators:
32
+ - by: text
33
+ value_pattern: "${selected_flights}"
34
+ - by: aria_label
35
+ value_pattern: "${selected_flights}"
36
+ - by: css
37
+ value: "div.mxvQLc.ceis6c div.zxVSec.YMlIz"
38
+ wait_for:
39
+ xhr: "GetBookingResults"
40
+ method: POST
41
+ result:
42
+ source: xhr
43
+ url_pattern: "FlightsFrontendService/GetBookingResults"
44
+ extract: "[]"
45
+ return_as: booking_options
46
+ notes: "Run after a Google Flights search has rendered matching results. For round_trip or multi_city selections, selected_flights should identify the complete visible itinerary or the next required leg option; Google may issue GetShoppingResults for intermediate leg selection before the final GetBookingResults response."
@@ -0,0 +1,210 @@
1
+ import { transform as sharedTransform } from '../_shared/google_flights_transport.ts';
2
+ import { extract as extractSearchResults } from '../search_flights/parser.ts';
3
+
4
+ type SegmentTuple = [string, string, string, null, string, string];
5
+ type SegmentGroup = SegmentTuple[];
6
+
7
+ const BOOKING_ENDPOINT = 'https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetBookingResults';
8
+ const SHOPPING_ENDPOINT = 'https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults';
9
+ const ROUND_TRIP_TYPE = 1;
10
+ const ONE_WAY_TRIP_TYPE = 2;
11
+ const MULTI_CITY_TRIP_TYPE = 3;
12
+
13
+ function normalizeSegment(value: unknown): SegmentTuple | null {
14
+ if (Array.isArray(value)) {
15
+ const [origin, date, destination, ignored, carrier, flightNumber] = value;
16
+ if (typeof origin === 'string' && typeof date === 'string' && typeof destination === 'string' && typeof carrier === 'string' && String(flightNumber)) {
17
+ return [origin, date, destination, ignored === null ? null : null, carrier, String(flightNumber)];
18
+ }
19
+ }
20
+ if (value && typeof value === 'object') {
21
+ const obj = value as Record<string, unknown>;
22
+ const origin = obj.origin;
23
+ const date = obj.date ?? obj.departureDate;
24
+ const destination = obj.destination;
25
+ const carrier = obj.carrier ?? obj.carrierCode;
26
+ const flightNumber = obj.flightNumber ?? obj.flight_number;
27
+ if (typeof origin === 'string' && typeof date === 'string' && typeof destination === 'string' && typeof carrier === 'string' && flightNumber != null) {
28
+ return [origin, date, destination, null, carrier, String(flightNumber)];
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function parseSelectedFlights(raw: unknown): SegmentTuple[] {
35
+ if (Array.isArray(raw)) return raw.map(normalizeSegment).filter((item): item is SegmentTuple => Boolean(item));
36
+ if (typeof raw !== 'string') return [];
37
+ const trimmed = raw.trim();
38
+ if (!trimmed) return [];
39
+ try {
40
+ return parseSelectedFlights(JSON.parse(trimmed));
41
+ } catch {
42
+ const parts = trimmed.split(',').map((part) => part.trim()).filter(Boolean);
43
+ if (parts.length % 5 !== 0) return [];
44
+ const out: SegmentTuple[] = [];
45
+ for (let i = 0; i < parts.length; i += 5) {
46
+ const carrier = parts[i];
47
+ const flightNumber = parts[i + 1];
48
+ const date = parts[i + 2];
49
+ const origin = parts[i + 3];
50
+ const destination = parts[i + 4];
51
+ if (!carrier || !flightNumber || !date || !origin || !destination) continue;
52
+ out.push([origin, date, destination, null, carrier, flightNumber]);
53
+ }
54
+ return out;
55
+ }
56
+ }
57
+
58
+ function sameCode(left: string, right: string): boolean {
59
+ return left.trim().toUpperCase() === right.trim().toUpperCase();
60
+ }
61
+
62
+ function dateDeltaDays(left: string, right: string): number | null {
63
+ const leftTime = Date.parse(`${left}T00:00:00Z`);
64
+ const rightTime = Date.parse(`${right}T00:00:00Z`);
65
+ if (!Number.isFinite(leftTime) || !Number.isFinite(rightTime)) return null;
66
+ return Math.round((rightTime - leftTime) / 86_400_000);
67
+ }
68
+
69
+ function groupSelectedSegments(segments: SegmentTuple[]): SegmentGroup[] {
70
+ const groups: SegmentGroup[] = [];
71
+ for (const segment of segments) {
72
+ const current = groups[groups.length - 1];
73
+ const previous = current?.[current.length - 1];
74
+ const startsReturnToGroupOrigin = current ? sameCode(segment[2], current[0]![0]) : false;
75
+ const dateDelta = previous ? dateDeltaDays(previous[1], segment[1]) : null;
76
+ const continuesConnection = previous
77
+ && sameCode(segment[0], previous[2])
78
+ && (dateDelta === null || (dateDelta >= 0 && dateDelta <= 1))
79
+ && !startsReturnToGroupOrigin;
80
+ if (current && continuesConnection) {
81
+ current.push(segment);
82
+ } else {
83
+ groups.push([segment]);
84
+ }
85
+ }
86
+ return groups;
87
+ }
88
+
89
+ function airportRef(code: string) {
90
+ const type = code.startsWith('/m/') ? 5 : 0;
91
+ return [[[code, type]]];
92
+ }
93
+
94
+ function routeLegEndpoints(group: SegmentGroup): { origin: string; date: string; destination: string } {
95
+ const first = group[0]!;
96
+ const last = group[group.length - 1]!;
97
+ return { origin: first[0], date: first[1], destination: last[2] };
98
+ }
99
+
100
+ function searchLeg(group: SegmentGroup, selected = false) {
101
+ const { origin, date, destination } = routeLegEndpoints(group);
102
+ return [airportRef(origin), airportRef(destination), null, 0, null, null, date, null, selected ? group : null, null, null, null, null, null, 1];
103
+ }
104
+
105
+ function tripTypeForGroups(groups: SegmentGroup[]): number {
106
+ if (groups.length <= 1) return ONE_WAY_TRIP_TYPE;
107
+ const first = routeLegEndpoints(groups[0]!);
108
+ const second = routeLegEndpoints(groups[1]!);
109
+ if (
110
+ groups.length === 2
111
+ && sameCode(first.origin, second.destination)
112
+ && sameCode(first.destination, second.origin)
113
+ ) {
114
+ return ROUND_TRIP_TYPE;
115
+ }
116
+ return MULTI_CITY_TRIP_TYPE;
117
+ }
118
+
119
+ function legCriteria(group: SegmentGroup, index: number, tripType: number) {
120
+ const { origin, date, destination } = routeLegEndpoints(group);
121
+ const legKind = tripType === ONE_WAY_TRIP_TYPE || (tripType === ROUND_TRIP_TYPE && index === 0) ? 3 : 1;
122
+ return [airportRef(origin), airportRef(destination), null, 0, null, null, date, null, group, null, null, null, null, null, legKind];
123
+ }
124
+
125
+ function tokenFromSearchResponse(rawResponse: unknown, params: Record<string, unknown>, targetIndex: number): string {
126
+ const requestedGroups = groupSelectedSegments(parseSelectedFlights(params.selected_flights));
127
+ if (requestedGroups.length === 0) throw new Error('selected_flights must be a JSON array of selected flight segment tuples');
128
+ const target = requestedGroups[Math.min(Math.max(targetIndex, 0), requestedGroups.length - 1)]!;
129
+
130
+ const searchData = extractSearchResults(rawResponse, { params: {}, responses: [] }) as {
131
+ itineraries?: Array<{ selected_flights?: unknown; selection_token?: unknown }>;
132
+ };
133
+ const itineraries = Array.isArray(searchData?.itineraries) ? searchData.itineraries : [];
134
+
135
+ for (const itinerary of itineraries) {
136
+ const candidateGroups = groupSelectedSegments(parseSelectedFlights(itinerary.selected_flights));
137
+ const candidate = candidateGroups.length === 1 ? candidateGroups[0]! : [];
138
+ if (
139
+ sameSelectedFlights(target, candidate)
140
+ && typeof itinerary.selection_token === 'string'
141
+ && itinerary.selection_token
142
+ ) {
143
+ return itinerary.selection_token;
144
+ }
145
+ }
146
+
147
+ if (requestedGroups.length === 1) {
148
+ const fallback = itineraries.find((itinerary) => typeof itinerary.selection_token === 'string' && itinerary.selection_token)?.selection_token;
149
+ if (typeof fallback === 'string' && fallback) return fallback;
150
+ }
151
+ throw new Error(`Unable to mint a fresh booking selection token for selected_flights segment ${targetIndex + 1}`);
152
+ }
153
+
154
+ function buildShoppingInner(params: Record<string, unknown>, responses: unknown[]) {
155
+ const groups = groupSelectedSegments(parseSelectedFlights(params.selected_flights));
156
+ if (groups.length === 0) throw new Error('selected_flights must be a JSON array of selected flight segment tuples');
157
+ const tripType = tripTypeForGroups(groups);
158
+ const selectedPrefixCount = Math.min(responses.length, Math.max(0, groups.length - 1));
159
+ const searchLegs = groups.map((group, index) => searchLeg(group, index < selectedPrefixCount));
160
+ if (tripType === ONE_WAY_TRIP_TYPE) {
161
+ searchLegs[0]![14] = 3;
162
+ }
163
+ const filters = [null, null, tripType, null, [], 1, [1, 0, 0, 0], null, null, null, null, null, null, searchLegs, null, null, null, 1];
164
+ if (selectedPrefixCount === 0) return [[], filters, 0, 0, 0, 1];
165
+ const selectionToken = tokenFromSearchResponse(responses[responses.length - 1], params, selectedPrefixCount - 1);
166
+ return [[null, selectionToken], filters, 0, 0, 0, 1];
167
+ }
168
+
169
+ function sameSegment(left: SegmentTuple, right: SegmentTuple): boolean {
170
+ return left[0] === right[0] && left[1] === right[1] && left[2] === right[2] && left[4] === right[4] && left[5] === right[5];
171
+ }
172
+
173
+ function sameSelectedFlights(requested: SegmentTuple[], candidate: SegmentTuple[], allowPrefix = false): boolean {
174
+ if (requested.length === 0 || candidate.length === 0) return false;
175
+ if (allowPrefix) {
176
+ if (candidate.length > requested.length) return false;
177
+ return candidate.every((segment, index) => sameSegment(segment, requested[index]!));
178
+ }
179
+ if (requested.length !== candidate.length) return false;
180
+ return requested.every((segment, index) => sameSegment(segment, candidate[index]!));
181
+ }
182
+
183
+ function buildBookingInner(params: Record<string, unknown>, responses: unknown[]) {
184
+ const groups = groupSelectedSegments(parseSelectedFlights(params.selected_flights));
185
+ if (groups.length === 0) throw new Error('selected_flights must be a JSON array of selected flight segment tuples');
186
+ const tokenSource = responses[responses.length - 1];
187
+ const selectionToken = tokenFromSearchResponse(tokenSource, params, groups.length - 1);
188
+ const tripType = tripTypeForGroups(groups);
189
+ const criteria = [null, null, tripType, null, [], 1, [1, 0, 0, 0], null, null, null, null, null, null, groups.map((group, index) => legCriteria(group, index, tripType)), null, null, null, 1];
190
+ const selectedOptionIndex = Number(params.selected_option_index ?? 0);
191
+ return [[null, selectionToken], criteria, null, Number.isFinite(selectedOptionIndex) ? selectedOptionIndex : 0];
192
+ }
193
+
194
+ export function transform(method: string, url: string, responses: unknown[], params?: Record<string, unknown>): { url: string; body: string; headers: Record<string, string> } {
195
+ const requestParams = params ?? {};
196
+ const isShoppingRequest = (url || '').includes('/GetShoppingResults');
197
+ const inner = isShoppingRequest ? buildShoppingInner(requestParams, responses) : buildBookingInner(requestParams, responses);
198
+ const endpoint = isShoppingRequest ? SHOPPING_ENDPOINT : BOOKING_ENDPOINT;
199
+ return sharedTransform(method, url || endpoint, responses, {
200
+ fReq: [null, JSON.stringify(inner)],
201
+ rpcid: isShoppingRequest ? 'GetShoppingResults' : 'GetBookingResults',
202
+ sourcePath: isShoppingRequest ? '/travel/flights' : '/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetBookingResults',
203
+ referer: isShoppingRequest ? 'https://www.google.com/travel/flights/search' : 'https://www.google.com/travel/flights/booking',
204
+ headers: {
205
+ 'sec-ch-ua-full-version': '"148.0.7778.96"',
206
+ 'x-goog-ext-259736195-jspb': '["en-US","US","USD",2,null,[420],null,null,7,[]]',
207
+ },
208
+ params: requestParams,
209
+ });
210
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "toolName": "get_flight_booking_options",
3
+ "intent": {
4
+ "description": "Get booking and fare details for a selected Google Flights itinerary.",
5
+ "userSaid": "clicked one of the one-way fares to find more details and booking details; saw the options for another flight; chose round trip fares; kept exploring round trip fare details; clicked the option for the first leg; clicked the option for the second leg and came to the booking options page"
6
+ },
7
+ "site": "google-flights",
8
+ "parameters": [
9
+ {
10
+ "name": "selected_flights",
11
+ "type": "string",
12
+ "description": "JSON array of selected segment tuples [origin, departureDate, destination, null, carrierCode, flightNumber]. For round-trip and multi-city booking, pass the complete ordered array of selected legs; a single first-leg value is treated as a one-way booking.",
13
+ "verified": true,
14
+ "sourcedFrom": {
15
+ "tool": "search_flights",
16
+ "field": "selected_flights"
17
+ }
18
+ }
19
+ ],
20
+ "bootstrap": {
21
+ "url": "https://www.google.com/travel/flights/search?tfs=CBwQAhoeEgoyMDI2LTA2LTA4agcIARIDU0pDcgcIARIDU0FOGh4SCjIwMjYtMDYtMTFqBwgBEgNTQU5yBwgBEgNTSkNAAUgBcAGCAQsI____________AZgBAQ",
22
+ "waitUntil": "domcontentloaded",
23
+ "waitMs": 500,
24
+ "timeoutMs": 30000,
25
+ "captures": [
26
+ {
27
+ "source": "html_regex",
28
+ "name": "f_sid",
29
+ "pattern": "(?:FdrFJe|f\\.sid)[^0-9-]{0,80}(-?[0-9]{10,})",
30
+ "group": 1,
31
+ "required": true,
32
+ "capability": "browser_bootstrap"
33
+ },
34
+ {
35
+ "source": "html_regex",
36
+ "name": "bl",
37
+ "pattern": "\"cfb2h\":\"([^\"]+)\"",
38
+ "group": 1,
39
+ "required": true,
40
+ "capability": "browser_bootstrap"
41
+ }
42
+ ]
43
+ },
44
+ "requests": [
45
+ {
46
+ "method": "POST",
47
+ "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=${generated.epoch_ms}&rt=c",
48
+ "headers": {
49
+ "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
50
+ "X-Same-Domain": "1",
51
+ "sec-ch-ua-full-version": "\"148.0.7778.96\"",
52
+ "x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
53
+ },
54
+ "body": "{\"selected_flights\":\"${param.selected_flights}\"}",
55
+ "effect": "safe"
56
+ },
57
+ {
58
+ "method": "POST",
59
+ "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=${generated.epoch_ms}&rt=c",
60
+ "headers": {
61
+ "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
62
+ "X-Same-Domain": "1",
63
+ "sec-ch-ua-full-version": "\"148.0.7778.96\"",
64
+ "x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
65
+ },
66
+ "body": "{\"selected_flights\":\"${param.selected_flights}\"}",
67
+ "effect": "safe"
68
+ },
69
+ {
70
+ "method": "POST",
71
+ "url": "https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetBookingResults?f.sid=${state.f_sid}&bl=${state.bl}&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=${generated.epoch_ms}&rt=c",
72
+ "headers": {
73
+ "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
74
+ "X-Same-Domain": "1",
75
+ "sec-ch-ua-full-version": "\"148.0.7778.96\"",
76
+ "x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
77
+ },
78
+ "body": "{\"selected_flights\":\"${param.selected_flights}\"}",
79
+ "effect": "safe"
80
+ }
81
+ ],
82
+ "requestTransformModule": "./request-transform.ts",
83
+ "parserModule": "./parser.ts",
84
+ "liveVerified": true
85
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "probedAt": "2026-07-01T01:13:54.138Z",
3
+ "imprintVersion": "0.5.0",
4
+ "schemaVersion": 2,
5
+ "workflowHash": "48de2030ea75e30e2c8f30826bda3cad0cec686621316268a5b8898a08752006",
6
+ "capabilityHash": "5fc16811cd65aff7afee349ddca05c806a65c9c2c96948192ea1bafba9284c38",
7
+ "preferredOrder": [
8
+ "cdp-replay"
9
+ ],
10
+ "results": {
11
+ "fetch": {
12
+ "outcome": "failed",
13
+ "durationMs": 17,
14
+ "error": "STATE_MISSING",
15
+ "detail": "STATE_MISSING: Workflow placeholder ${state.bl} but state \"bl\" has not been captured yet"
16
+ },
17
+ "fetch-bootstrap": {
18
+ "outcome": "forbidden",
19
+ "durationMs": 29952,
20
+ "detail": "FORBIDDEN: fetch-bootstrap: cdp-minted jar did not validate; cdp-replay (in-page) required."
21
+ },
22
+ "cdp-replay": {
23
+ "outcome": "ok",
24
+ "durationMs": 550
25
+ }
26
+ }
27
+ }