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,95 @@
1
+ // Builds the double-encoded `f.req` form body for FlightsFrontendService RPCs
2
+ // (GetShoppingResults / GetCalendarPicker / GetBookingResults). Body construction
3
+ // is session-independent; f.sid/bl/_reqid/X-Goog-BatchExecute-Bgr are runtime state
4
+ // and are intentionally left untouched (transform returns the input url verbatim).
5
+ //
6
+ // NOTE (correction to spec): the leg encoding is NOT byte-for-byte identical across
7
+ // all three RPCs. GetCalendarPicker uses 4-slot legs ([ORIGIN,DEST,TIMES|null,STOPS])
8
+ // with the date range living in the outer wrapper; Shopping/Booking use the full
9
+ // 15-slot leg with DATE at [6]. Verified by decoding seq 97 vs seq 111.
10
+
11
+ // Fresh searches emit wrapper `...,0,0,0,1]` and leg[14]=3 (proven seq 111/140).
12
+ // In-page-refined searches use `...,0,1,0,1]` with return-leg[14]=1 (seq 194/425) —
13
+ // a UI freshness flag, not a user param; we always emit the fresh form for shopping.
14
+ // Booking outbound legs use [14]=3, return legs [14]=1 (seq 764/811).
15
+
16
+ function buildLeg(leg: any): any[] {
17
+ const out: any[] = new Array(15).fill(null);
18
+ out[0] = [[[leg?.origin, 0]]];
19
+ out[1] = [[[leg?.dest, 0]]];
20
+ out[2] = leg?.times ?? null;
21
+ out[3] = leg?.stops ?? 0;
22
+ out[4] = leg?.alliances ?? null;
23
+ out[5] = leg?.carriers ?? null;
24
+ out[6] = leg?.date ?? null;
25
+ out[7] = leg?.duration ?? null;
26
+ out[8] = Array.isArray(leg?.selected)
27
+ ? leg.selected.map((s: any) => [s?.origin, s?.date, s?.dest, null, s?.carrier, s?.flightNumber])
28
+ : null;
29
+ out[14] = 3;
30
+ return out;
31
+ }
32
+
33
+ export function buildFlightSearchParams(params: Record<string, any>): any[] {
34
+ const p: Record<string, any> = params ?? {};
35
+ // 18-slot positional search array shared by every body (proven seq 111).
36
+ const sp: any[] = new Array(18).fill(null);
37
+ sp[2] = p.tripType ?? 1; // 1=round, 2=one-way, 3=multi-city
38
+ sp[4] = [];
39
+ sp[5] = 1;
40
+ sp[6] = [p.adults ?? 1, p.children ?? 0, p.infantsSeat ?? 0, p.infantsLap ?? 0];
41
+ sp[7] = p.maxPrice != null ? [null, p.maxPrice] : null;
42
+ sp[10] = p.bags ? [p.bags.carryOn ?? 0, p.bags.checked ?? 0] : null;
43
+ const legs: any[] = Array.isArray(p.legs) ? p.legs : [];
44
+ sp[13] = legs.map((l: any) => buildLeg(l));
45
+ sp[17] = 1;
46
+ return sp;
47
+ }
48
+
49
+ export function encodeFreq(payload: any): string {
50
+ // payload -> inner json string -> embedded in [null, inner] -> x-www-form-urlencoded.
51
+ // Verified byte-for-byte against seq 111: `\"`->%5C%22, `[`->%5B, `,`->%2C, `=`->%3D.
52
+ const inner = JSON.stringify(payload);
53
+ const outer = JSON.stringify([null, inner]);
54
+ return 'f.req=' + encodeURIComponent(outer) + '&';
55
+ }
56
+
57
+ export function transform(
58
+ method: string,
59
+ url: string,
60
+ responses: Record<string, any>,
61
+ params?: Record<string, any>,
62
+ ): { url: string; body: string } {
63
+ // method/responses are part of the contract but unused for body construction
64
+ // (the booking token arrives via params.flight_token). Reference to satisfy strict.
65
+ void method;
66
+ void responses;
67
+
68
+ const p: Record<string, any> = params ?? {};
69
+ const m = /FlightsFrontendService\/(\w+)/.exec(url);
70
+ if (!m || !m[1]) throw new Error(`unrecognized FlightsFrontendService rpc in url: ${url}`);
71
+ const rpc: string = m[1];
72
+
73
+ const sp = buildFlightSearchParams(p);
74
+ let payload: any;
75
+
76
+ if (rpc === 'GetShoppingResults') {
77
+ payload = [[], sp, 0, 0, 0, 1];
78
+ } else if (rpc === 'GetCalendarPicker') {
79
+ const legs = sp[13];
80
+ if (Array.isArray(legs)) sp[13] = legs.map((l: any) => (Array.isArray(l) ? l.slice(0, 4) : l));
81
+ payload = [null, sp, [p.startDate ?? null, p.endDate ?? null], null, [7, 7]];
82
+ } else if (rpc === 'GetBookingResults') {
83
+ const legs = sp[13];
84
+ if (Array.isArray(legs)) {
85
+ legs.forEach((l: any, i: number) => {
86
+ if (i >= 1 && Array.isArray(l)) l[14] = 1; // return leg(s)
87
+ });
88
+ }
89
+ payload = [[null, p.flight_token ?? null], sp, null, p.tripType === 2 ? 0 : 1];
90
+ } else {
91
+ throw new Error(`unsupported FlightsFrontendService rpc: ${rpc}`);
92
+ }
93
+
94
+ return { url, body: encodeFreq(payload) };
95
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "imprint-shared",
3
+ "private": true,
4
+ "devDependencies": {
5
+ "@types/bun": "latest",
6
+ "@types/node": "latest",
7
+ "bun-types": "latest"
8
+ }
9
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * GENERATED by `imprint emit` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Tool: get_flight_booking_details
5
+ * Site: google-flights
6
+ * Intent: Get booking/fare details and bookable options for a specific selected flight itinerary on Google Flights.
7
+ *
8
+ * To regenerate: imprint emit ~/.imprint/google-flights/get_flight_booking_details/workflow.json --force
9
+ */
10
+
11
+ import { fileURLToPath } from 'node:url';
12
+ import { dirname, join } from 'node:path';
13
+ import {
14
+ executeWorkflow,
15
+ type CredentialStore,
16
+ } from 'imprint/runtime';
17
+ import type { ToolResult, Workflow } from 'imprint/types';
18
+
19
+ const WORKFLOW: Workflow = {
20
+ "toolName": "get_flight_booking_details",
21
+ "intent": {
22
+ "description": "Get booking/fare details and bookable options for a specific selected flight itinerary on Google Flights.",
23
+ "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"
24
+ },
25
+ "parameters": [
26
+ {
27
+ "name": "origin",
28
+ "type": "string",
29
+ "description": "Origin airport IATA code for the outbound leg, e.g. SJC",
30
+ "verified": false,
31
+ "verifyNote": "annotated"
32
+ },
33
+ {
34
+ "name": "destination",
35
+ "type": "string",
36
+ "description": "Destination airport IATA code for the outbound leg, e.g. SAN",
37
+ "verified": false,
38
+ "verifyNote": "annotated"
39
+ },
40
+ {
41
+ "name": "departure_date",
42
+ "type": "string",
43
+ "description": "Outbound date in YYYY-MM-DD",
44
+ "verified": false,
45
+ "verifyNote": "annotated"
46
+ },
47
+ {
48
+ "name": "return_date",
49
+ "type": "string",
50
+ "description": "Return date in YYYY-MM-DD. Empty/omit for a one-way booking.",
51
+ "default": "",
52
+ "verified": true
53
+ },
54
+ {
55
+ "name": "outbound_flight",
56
+ "type": "string",
57
+ "description": "Selected outbound flight as 'carrier number', e.g. 'WN 3489'",
58
+ "verified": false,
59
+ "verifyNote": "annotated"
60
+ },
61
+ {
62
+ "name": "return_flight",
63
+ "type": "string",
64
+ "description": "Selected return flight as 'carrier number' (round trip only), e.g. 'WN 3540'. Empty for one-way.",
65
+ "default": "",
66
+ "verified": false,
67
+ "verifyNote": "annotated"
68
+ },
69
+ {
70
+ "name": "flight_token",
71
+ "type": "string",
72
+ "description": "Opaque per-itinerary booking token minted by the search_flights tool's flight_token output for the selected itinerary."
73
+ }
74
+ ],
75
+ "requests": [
76
+ {
77
+ "method": "POST",
78
+ "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=1659189&rt=c",
79
+ "headers": {
80
+ "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
81
+ "X-Same-Domain": "1",
82
+ "Referer": "https://www.google.com/travel/flights",
83
+ "x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
84
+ },
85
+ "body": "f.req=${param.origin}|${param.destination}|${param.departure_date}|${param.return_date}|${param.outbound_flight}|${param.return_flight}|${param.flight_token}&",
86
+ "effect": "safe"
87
+ }
88
+ ],
89
+ "site": "google-flights",
90
+ "bootstrap": {
91
+ "url": "https://www.google.com/travel/flights",
92
+ "waitUntil": "domcontentloaded",
93
+ "timeoutMs": 30000,
94
+ "captures": [
95
+ {
96
+ "name": "f_sid",
97
+ "required": false,
98
+ "capability": "browser_bootstrap",
99
+ "source": "html_regex",
100
+ "pattern": "\"FdrFJe\":\"([^\"]+)\"",
101
+ "group": 1
102
+ },
103
+ {
104
+ "name": "bl",
105
+ "required": false,
106
+ "capability": "browser_bootstrap",
107
+ "source": "html_regex",
108
+ "pattern": "\"cfb2h\":\"([^\"]+)\"",
109
+ "group": 1
110
+ }
111
+ ]
112
+ },
113
+ "parserModule": "./parser.ts",
114
+ "requestTransformModule": "./request-transform.ts",
115
+ "liveVerified": true
116
+ };
117
+
118
+ export interface GetFlightBookingDetailsInput {
119
+ /** Origin airport IATA code for the outbound leg, e.g. SJC */
120
+ origin: string;
121
+ /** Destination airport IATA code for the outbound leg, e.g. SAN */
122
+ destination: string;
123
+ /** Outbound date in YYYY-MM-DD */
124
+ departure_date: string;
125
+ /** Return date in YYYY-MM-DD. Empty/omit for a one-way booking. */
126
+ return_date?: string;
127
+ /** Selected outbound flight as 'carrier number', e.g. 'WN 3489' */
128
+ outbound_flight: string;
129
+ /** Selected return flight as 'carrier number' (round trip only), e.g. 'WN 3540'. Empty for one-way. */
130
+ return_flight?: string;
131
+ /** Opaque per-itinerary booking token minted by the search_flights tool's flight_token output for the selected itinerary. */
132
+ flight_token: string;
133
+ }
134
+
135
+ export async function getFlightBookingDetails(
136
+ input: GetFlightBookingDetailsInput,
137
+ opts: { credentials?: CredentialStore; fetchImpl?: typeof fetch; initialState?: Record<string, unknown> } = {},
138
+ ): Promise<ToolResult> {
139
+ const __dirname = dirname(fileURLToPath(import.meta.url));
140
+ const params: Record<string, string | number | boolean> = {
141
+ return_date: input.return_date ?? "",
142
+ return_flight: input.return_flight ?? "",
143
+ origin: input.origin,
144
+ destination: input.destination,
145
+ departure_date: input.departure_date,
146
+ outbound_flight: input.outbound_flight,
147
+ flight_token: input.flight_token,
148
+ };
149
+ return executeWorkflow({
150
+ workflow: WORKFLOW,
151
+ params,
152
+ credentials: opts.credentials,
153
+ fetchImpl: opts.fetchImpl,
154
+ initialState: opts.initialState,
155
+ workflowPath: join(__dirname, 'workflow.json'),
156
+ });
157
+ }
158
+
159
+ export { WORKFLOW };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "imprint-tool-google-flights",
3
+ "private": true,
4
+ "devDependencies": {
5
+ "@types/bun": "latest",
6
+ "@types/node": "latest",
7
+ "bun-types": "latest"
8
+ }
9
+ }
@@ -0,0 +1,182 @@
1
+ // Parser for Google Flights GetBookingResults (batchexecute RPC).
2
+ // Decodes the streaming envelope with the shared helper, then walks the deeply
3
+ // nested positional payload to extract the itinerary's per-segment detail and
4
+ // the list of bookable fare options (price USD + booking provider).
5
+ import { decodeBatchExecute } from '../_shared/batchexecute.ts';
6
+
7
+ const AIRPORT = /^[A-Z]{3}$/;
8
+
9
+ interface Segment {
10
+ carrier: string;
11
+ carrierName: string | null;
12
+ flightNumber: string;
13
+ origin: string;
14
+ originName: string | null;
15
+ destination: string;
16
+ destinationName: string | null;
17
+ departDate: string | null;
18
+ departTime: string | null;
19
+ arriveDate: string | null;
20
+ arriveTime: string | null;
21
+ durationMinutes: number | null;
22
+ aircraft: string | null;
23
+ operatingCarrier: { carrier: string; flightNumber: string | null; name: string | null } | null;
24
+ }
25
+
26
+ interface FareOption {
27
+ priceUSD: number;
28
+ provider: string;
29
+ bookingUrl: string | null;
30
+ fareClass: string | null;
31
+ }
32
+
33
+ function fmtTime(t: unknown): string | null {
34
+ if (!Array.isArray(t) || t.length === 0) return null;
35
+ const h = typeof t[0] === 'number' ? t[0] : 0;
36
+ const m = typeof t[1] === 'number' ? t[1] : 0;
37
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
38
+ }
39
+
40
+ function fmtDate(d: unknown): string | null {
41
+ if (!Array.isArray(d) || d.length < 3) return null;
42
+ const [y, mo, day] = d as number[];
43
+ if (typeof y !== 'number') return null;
44
+ return `${y}-${String(mo).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
45
+ }
46
+
47
+ // A segment node: airport codes at [3]/[6], aircraft string at [17],
48
+ // and a marketing-flight tuple [carrier, number, _, carrierName] at [22].
49
+ function isSegment(node: unknown): node is unknown[] {
50
+ if (!Array.isArray(node)) return false;
51
+ if (typeof node[3] !== 'string' || !AIRPORT.test(node[3] as string)) return false;
52
+ if (typeof node[6] !== 'string' || !AIRPORT.test(node[6] as string)) return false;
53
+ const fn = node[22];
54
+ return Array.isArray(fn) && typeof fn[0] === 'string' && fn[1] != null;
55
+ }
56
+
57
+ // A booking-option node: provider tuple at [1][0] = [carrier, providerName,...]
58
+ // and price at [7] = [[null, priceUSD], "<fareToken>"].
59
+ function isBookingOption(node: unknown): node is unknown[] {
60
+ if (!Array.isArray(node)) return false;
61
+ const provider = node[1];
62
+ if (!Array.isArray(provider) || !Array.isArray(provider[0])) return false;
63
+ if (typeof provider[0][1] !== 'string') return false;
64
+ const price = node[7];
65
+ if (!Array.isArray(price) || !Array.isArray(price[0])) return false;
66
+ return typeof price[0][1] === 'number';
67
+ }
68
+
69
+ function toSegment(seg: unknown[]): Segment {
70
+ const fn = seg[22] as unknown[];
71
+ let operatingCarrier: Segment['operatingCarrier'] = null;
72
+ const op = seg[15];
73
+ if (Array.isArray(op) && Array.isArray(op[0]) && typeof op[0][0] === 'string') {
74
+ const o = op[0] as unknown[];
75
+ operatingCarrier = {
76
+ carrier: o[0] as string,
77
+ flightNumber: typeof o[1] === 'string' ? (o[1] as string) : null,
78
+ name: typeof o[3] === 'string' ? (o[3] as string) : null,
79
+ };
80
+ }
81
+ return {
82
+ carrier: typeof fn[0] === 'string' ? (fn[0] as string) : '',
83
+ carrierName: typeof fn[3] === 'string' ? (fn[3] as string) : null,
84
+ flightNumber: fn[1] != null ? String(fn[1]) : '',
85
+ origin: seg[3] as string,
86
+ originName: typeof seg[4] === 'string' ? (seg[4] as string) : null,
87
+ destination: seg[6] as string,
88
+ destinationName: typeof seg[5] === 'string' ? (seg[5] as string) : null,
89
+ departDate: fmtDate(seg[20]),
90
+ departTime: fmtTime(seg[8]),
91
+ arriveDate: fmtDate(seg[21]),
92
+ arriveTime: fmtTime(seg[10]),
93
+ durationMinutes: typeof seg[11] === 'number' ? (seg[11] as number) : null,
94
+ aircraft: typeof seg[17] === 'string' ? (seg[17] as string) : null,
95
+ operatingCarrier,
96
+ };
97
+ }
98
+
99
+ function toFareOption(node: unknown[]): FareOption {
100
+ const provider = node[1] as unknown[];
101
+ const price = node[7] as unknown[];
102
+ const link = node[5];
103
+ let bookingUrl: string | null = null;
104
+ if (Array.isArray(link) && typeof link[0] === 'string') bookingUrl = link[0] as string;
105
+
106
+ let fareClass: string | null = null;
107
+ const fc = node[14];
108
+ // node[14] = [[[null, ["WN","BASIC"], 1]]]
109
+ const inner = Array.isArray(fc) ? (fc[0] as unknown[]) : undefined;
110
+ const innerInner = Array.isArray(inner) ? (inner[0] as unknown[]) : undefined;
111
+ if (Array.isArray(innerInner) && Array.isArray(innerInner[1])) {
112
+ const code = (innerInner[1] as unknown[])[1];
113
+ if (typeof code === 'string') fareClass = code;
114
+ }
115
+
116
+ return {
117
+ priceUSD: (price[0] as unknown[])[1] as number,
118
+ provider: (provider[0] as unknown[])[1] as string,
119
+ bookingUrl,
120
+ fareClass,
121
+ };
122
+ }
123
+
124
+ function walk(node: unknown, segs: unknown[][], fares: unknown[][]): void {
125
+ if (!Array.isArray(node)) return;
126
+ if (isBookingOption(node)) {
127
+ fares.push(node);
128
+ // booking options also contain a nested segment listing; keep recursing
129
+ // so those segments are still discovered (dedup handles overlap).
130
+ }
131
+ if (isSegment(node)) {
132
+ segs.push(node);
133
+ return; // a segment is a leaf for our purposes
134
+ }
135
+ for (const child of node) walk(child, segs, fares);
136
+ }
137
+
138
+ export function extract(
139
+ rawResponse: unknown,
140
+ _context?: { params: Record<string, string | number | boolean>; responses: unknown[] },
141
+ ): unknown {
142
+ let frames: Array<{ rpcid: string | null; payload: any }> = [];
143
+ if (typeof rawResponse === 'string') {
144
+ frames = decodeBatchExecute(rawResponse);
145
+ } else if (rawResponse != null) {
146
+ frames = [{ rpcid: null, payload: rawResponse }];
147
+ }
148
+
149
+ const segNodes: unknown[][] = [];
150
+ const fareNodes: unknown[][] = [];
151
+ for (const f of frames) walk(f.payload, segNodes, fareNodes);
152
+
153
+ // Dedup segments by carrier+number+departDate+departTime.
154
+ const segMap = new Map<string, Segment>();
155
+ for (const s of segNodes) {
156
+ const seg = toSegment(s);
157
+ if (!seg.carrier && !seg.flightNumber) continue;
158
+ const key = `${seg.carrier}${seg.flightNumber}|${seg.departDate}|${seg.departTime}|${seg.origin}`;
159
+ if (!segMap.has(key)) segMap.set(key, seg);
160
+ }
161
+
162
+ // Dedup fare options by fareClass + price + provider.
163
+ const fareMap = new Map<string, FareOption>();
164
+ for (const n of fareNodes) {
165
+ const fare = toFareOption(n);
166
+ if (!fare.provider || typeof fare.priceUSD !== 'number') continue;
167
+ const key = `${fare.fareClass}|${fare.priceUSD}|${fare.provider}`;
168
+ if (!fareMap.has(key)) fareMap.set(key, fare);
169
+ }
170
+
171
+ const segments = [...segMap.values()];
172
+ const fareOptions = [...fareMap.values()];
173
+ const prices = fareOptions.map((f) => f.priceUSD);
174
+
175
+ return {
176
+ segments,
177
+ fareOptions,
178
+ segmentCount: segments.length,
179
+ fareOptionCount: fareOptions.length,
180
+ lowestPriceUSD: prices.length ? Math.min(...prices) : null,
181
+ };
182
+ }
@@ -0,0 +1,138 @@
1
+ toolName: get_flight_booking_details
2
+ summary: "On Google Flights, search a route/date, open a selected flight itinerary, and capture its GetBookingResults fare/booking-provider detail."
3
+ parameters:
4
+ - name: origin
5
+ type: string
6
+ description: Origin airport code, e.g. SJC
7
+ - name: destination
8
+ type: string
9
+ description: Destination airport code, e.g. SAN
10
+ - name: departure_date
11
+ type: string
12
+ description: Outbound date, YYYY-MM-DD
13
+ - name: return_date
14
+ type: string
15
+ description: Return date YYYY-MM-DD. Omit for one-way (the recorded primary flow is one-way).
16
+ - name: outbound_flight
17
+ type: string
18
+ description: Selected outbound flight as carrier+number, e.g. "WN 3489". Used to pick the flight card whose booking details are fetched.
19
+ - name: return_flight
20
+ type: string
21
+ description: Selected return flight as carrier+number (round-trip only); omit for one-way.
22
+ steps:
23
+ - action: navigate
24
+ url: https://www.google.com/travel/flights
25
+ wait_for: networkidle
26
+
27
+ - action: type
28
+ locators:
29
+ - by: aria_label
30
+ value: Where from?
31
+ - by: css
32
+ value: div.wUiEcc.SOcuWe input.II2One.j0Ppje
33
+ value: ${origin}
34
+ wait_for:
35
+ sleep_ms: 500
36
+ - action: click
37
+ locators:
38
+ - by: aria_label
39
+ value_pattern: ${origin}
40
+ - by: text
41
+ value_pattern: ${origin}
42
+ wait_for: visible
43
+
44
+ - action: type
45
+ locators:
46
+ - by: aria_label
47
+ value: "Where to?"
48
+ - by: css
49
+ value: div.cQnuXe.k0gFV input.II2One.j0Ppje
50
+ value: ${destination}
51
+ wait_for:
52
+ sleep_ms: 500
53
+ - action: click
54
+ locators:
55
+ - by: aria_label
56
+ value_pattern: ${destination}
57
+ - by: text
58
+ value_pattern: ${destination}
59
+ wait_for: visible
60
+
61
+ # Trip type -> One way (omit these two steps for a round trip)
62
+ - action: click
63
+ locators:
64
+ - by: aria_label
65
+ value: "Select your ticket type."
66
+ - by: css
67
+ value: div.TQYpgc.gInvKb div.VfPpkd-aPP78e
68
+ wait_for:
69
+ sleep_ms: 300
70
+ - action: click
71
+ locators:
72
+ - by: text
73
+ value: One way
74
+ - by: role
75
+ value: option
76
+ name: One way
77
+ wait_for: visible
78
+
79
+ - action: type
80
+ locators:
81
+ - by: aria_label
82
+ value: Departure
83
+ - by: css
84
+ value: div.GYgkab.YICvqf input.TP4Lpb.eoY5cb
85
+ value: ${departure_date}
86
+ wait_for:
87
+ sleep_ms: 400
88
+ - action: click
89
+ locators:
90
+ - by: text
91
+ value: Done
92
+ - by: aria_label
93
+ value: Done
94
+ wait_for:
95
+ sleep_ms: 300
96
+
97
+ - action: click
98
+ locators:
99
+ - by: text
100
+ value: Search
101
+ - by: aria_label
102
+ value: Search
103
+ wait_for:
104
+ xhr: /GetShoppingResults
105
+
106
+ # Open the chosen flight; this is the click that triggers GetBookingResults.
107
+ - action: click
108
+ locators:
109
+ - by: text
110
+ value_pattern: ${outbound_flight}
111
+ - by: css
112
+ value: ul.Rk10dc > li.pIav2d div.yR1fYc
113
+ wait_for:
114
+ xhr: /GetBookingResults
115
+ result:
116
+ source: xhr
117
+ url_pattern: /data/travel.frontend.flights.FlightsFrontendService/GetBookingResults
118
+ extract: "[1][5][0]"
119
+ return_as: booking_details
120
+ notes: >
121
+ Response is a Google batchexecute payload prefixed with )]}' and split into length-prefixed
122
+ chunks; the parser must strip the prefix, split on the numeric length markers, and JSON.parse
123
+ each "wrb.fr" envelope's double-encoded inner JSON. Flight data is positional, not keyed: in the
124
+ first wrb.fr chunk, [1][5][0] holds the selected itinerary slices (each slice = [carrier,[airlineName],
125
+ [[segment...]], origCode,[y,m,d],[h,m], destCode,...], with segment carrier+number at the
126
+ [\"WN\",\"3489\",null,\"Southwest\"] tuple). The bookable fare options with provider and price live in
127
+ the SECOND wrb.fr chunk of the same response at [1][0]: each option is
128
+ [0,[[provider,providerName,...]],null,[[carrier,number]],false,[bookingUrl...],...,priceCents]; price is
129
+ in cents (e.g. 88008 = $880.08) and provider name is option[1][0][1]. Auth is session-bound:
130
+ f.sid (per-page session id) plus the X-Goog-BatchExecute-Bgr header are captured fresh from page
131
+ bootstrap and are NOT parameterized. The per-itinerary base64 flight token required by the underlying
132
+ GetBookingResults RPC is produced by the prior GetShoppingResults and is not user-controllable — the
133
+ DOM flow handles it automatically by clicking the rendered flight card, which is why this playbook
134
+ clicks rather than POSTs. For a round trip, do NOT set trip type to "One way"; supply return_date so
135
+ the search returns round-trip results, then after the outbound-flight click pick the return flight
136
+ (matching return_flight) before GetBookingResults fires. Result cards show airline name + times rather
137
+ than the raw flight number, so the ${outbound_flight} text locator may need the carrier/time; the
138
+ css fallback selects the first result card.
@@ -0,0 +1,86 @@
1
+ // Adapter around the shared FlightsFrontendService body builder for
2
+ // GetBookingResults. The tool exposes flat snake_case params (origin,
3
+ // destination, departure_date, return_date, outbound_flight, return_flight,
4
+ // flight_token); the shared encoder consumes a structured shape
5
+ // ({ tripType, legs:[{origin,dest,date,selected:[{origin,date,dest,carrier,flightNumber}]}], flight_token }).
6
+ // We map between them here and delegate the byte-for-byte positional encoding
7
+ // (legs, trip-type, the [[null,token],sp,null,selIdx] outer wrapper, token
8
+ // injection, encodeFreq) to the shared module — required reuse.
9
+ import { transform as sharedTransform } from '../_shared/flights_request.ts';
10
+
11
+ type Params = Record<string, string | number | boolean | undefined | null>;
12
+
13
+ // "WN 3489" / "WN3489" -> { carrier:"WN", flightNumber:"3489" }
14
+ function parseFlight(v: unknown): { carrier: string; flightNumber: string } | null {
15
+ if (v == null || v === '') return null;
16
+ const s = String(v).trim();
17
+ const m = /^([A-Za-z0-9]{2})\s*([0-9]{1,5})$/.exec(s) ?? /^(\S+)\s+(\S+)$/.exec(s);
18
+ if (!m) return null;
19
+ const carrier = m[1];
20
+ const flightNumber = m[2];
21
+ if (carrier == null || flightNumber == null) return null;
22
+ return { carrier: carrier.toUpperCase(), flightNumber };
23
+ }
24
+
25
+ export function transform(
26
+ method: string,
27
+ url: string,
28
+ responses: Record<string, any>,
29
+ params?: Params,
30
+ ): { url: string; body: string } {
31
+ const p: Params = params ?? {};
32
+ const origin = p.origin != null ? String(p.origin) : '';
33
+ const destination = p.destination != null ? String(p.destination) : '';
34
+ const departureDate = p.departure_date ? String(p.departure_date) : null;
35
+ const returnDate = p.return_date != null ? String(p.return_date).trim() : '';
36
+ const roundTrip = returnDate !== '';
37
+ const tripType = roundTrip ? 1 : 2; // 1=round trip, 2=one way
38
+
39
+ const ob = parseFlight(p.outbound_flight);
40
+ const legs: any[] = [
41
+ {
42
+ origin,
43
+ dest: destination,
44
+ date: departureDate,
45
+ selected: ob
46
+ ? [
47
+ {
48
+ origin,
49
+ date: departureDate,
50
+ dest: destination,
51
+ carrier: ob.carrier,
52
+ flightNumber: ob.flightNumber,
53
+ },
54
+ ]
55
+ : null,
56
+ },
57
+ ];
58
+
59
+ if (roundTrip) {
60
+ const rb = parseFlight(p.return_flight);
61
+ legs.push({
62
+ origin: destination,
63
+ dest: origin,
64
+ date: returnDate,
65
+ selected: rb
66
+ ? [
67
+ {
68
+ origin: destination,
69
+ date: returnDate,
70
+ dest: origin,
71
+ carrier: rb.carrier,
72
+ flightNumber: rb.flightNumber,
73
+ },
74
+ ]
75
+ : null,
76
+ });
77
+ }
78
+
79
+ const mapped: Record<string, any> = {
80
+ tripType,
81
+ legs,
82
+ flight_token: p.flight_token != null ? String(p.flight_token) : null,
83
+ };
84
+
85
+ return sharedTransform(method, url, responses, mapped);
86
+ }