imprint-mcp 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -189
- package/examples/discoverandgo/README.md +1 -1
- package/examples/echo/README.md +1 -1
- package/examples/google-flights/README.md +28 -0
- package/examples/google-flights/_shared/batchexecute.ts +63 -0
- package/examples/google-flights/_shared/flights_request.ts +95 -0
- package/examples/google-flights/_shared/package.json +9 -0
- package/examples/google-flights/get_flight_booking_details/index.ts +159 -0
- package/examples/google-flights/get_flight_booking_details/package.json +9 -0
- package/examples/google-flights/get_flight_booking_details/parser.ts +182 -0
- package/examples/google-flights/get_flight_booking_details/playbook.yaml +138 -0
- package/examples/google-flights/get_flight_booking_details/request-transform.ts +86 -0
- package/examples/google-flights/get_flight_booking_details/workflow.json +98 -0
- package/examples/google-flights/get_flight_calendar_prices/index.ts +131 -0
- package/examples/google-flights/get_flight_calendar_prices/package.json +9 -0
- package/examples/google-flights/get_flight_calendar_prices/parser.ts +86 -0
- package/examples/google-flights/get_flight_calendar_prices/playbook.yaml +97 -0
- package/examples/google-flights/get_flight_calendar_prices/request-transform.ts +31 -0
- package/examples/google-flights/get_flight_calendar_prices/workflow.json +78 -0
- package/examples/google-flights/lookup_airport/index.ts +101 -0
- package/examples/google-flights/lookup_airport/package.json +9 -0
- package/examples/google-flights/lookup_airport/parser.ts +66 -0
- package/examples/google-flights/lookup_airport/playbook.yaml +47 -0
- package/examples/google-flights/lookup_airport/request-transform.ts +20 -0
- package/examples/google-flights/lookup_airport/workflow.json +57 -0
- package/examples/google-flights/search_flights/index.ts +219 -0
- package/examples/google-flights/search_flights/package.json +9 -0
- package/examples/google-flights/search_flights/parser.ts +169 -0
- package/examples/google-flights/search_flights/playbook.yaml +184 -0
- package/examples/google-flights/search_flights/request-transform.ts +119 -0
- package/examples/google-flights/search_flights/workflow.json +143 -0
- package/examples/google-hotels/README.md +29 -0
- package/examples/google-hotels/_shared/batchexecute.ts +73 -0
- package/examples/google-hotels/_shared/freq.ts +158 -0
- package/examples/google-hotels/_shared/package.json +9 -0
- package/examples/google-hotels/autocomplete_hotel_location/index.ts +80 -0
- package/examples/google-hotels/autocomplete_hotel_location/package.json +9 -0
- package/examples/google-hotels/autocomplete_hotel_location/parser.ts +71 -0
- package/examples/google-hotels/autocomplete_hotel_location/playbook.yaml +36 -0
- package/examples/google-hotels/autocomplete_hotel_location/request-transform.ts +37 -0
- package/examples/google-hotels/autocomplete_hotel_location/workflow.json +36 -0
- package/examples/google-hotels/get_hotel_booking_options/index.ts +143 -0
- package/examples/google-hotels/get_hotel_booking_options/package.json +9 -0
- package/examples/google-hotels/get_hotel_booking_options/parser.ts +271 -0
- package/examples/google-hotels/get_hotel_booking_options/playbook.yaml +154 -0
- package/examples/google-hotels/get_hotel_booking_options/request-transform.ts +154 -0
- package/examples/google-hotels/get_hotel_booking_options/workflow.json +84 -0
- package/examples/google-hotels/get_hotel_reviews/index.ts +81 -0
- package/examples/google-hotels/get_hotel_reviews/package.json +9 -0
- package/examples/google-hotels/get_hotel_reviews/parser.ts +128 -0
- package/examples/google-hotels/get_hotel_reviews/playbook.yaml +64 -0
- package/examples/google-hotels/get_hotel_reviews/request-transform.ts +42 -0
- package/examples/google-hotels/get_hotel_reviews/workflow.json +37 -0
- package/examples/google-hotels/search_hotels/index.ts +207 -0
- package/examples/google-hotels/search_hotels/package.json +9 -0
- package/examples/google-hotels/search_hotels/parser.ts +260 -0
- package/examples/google-hotels/search_hotels/playbook.yaml +87 -0
- package/examples/google-hotels/search_hotels/request-transform.ts +197 -0
- package/examples/google-hotels/search_hotels/workflow.json +127 -0
- package/examples/southwest/README.md +3 -2
- package/examples/southwest/search_southwest_flights/index.ts +18 -1
- package/examples/southwest/search_southwest_flights/workflow.json +18 -1
- package/package.json +3 -2
- package/prompts/audit-agent.md +71 -0
- package/prompts/build-planning.md +74 -0
- package/prompts/compile-agent.md +131 -27
- package/prompts/prereq-builder.md +64 -0
- package/prompts/prereq-planner.md +34 -0
- package/prompts/tool-planning.md +39 -0
- package/src/cli.ts +116 -3
- package/src/imprint/agent.ts +5 -0
- package/src/imprint/audit.ts +996 -0
- package/src/imprint/backend-ladder.ts +1214 -184
- package/src/imprint/build-plan.ts +1051 -0
- package/src/imprint/cdp-browser-fetch.ts +592 -0
- package/src/imprint/cdp-jar-cache.ts +320 -0
- package/src/imprint/chromium.ts +414 -8
- package/src/imprint/claude-cli-compile.ts +125 -25
- package/src/imprint/codex-cli-compile.ts +26 -23
- package/src/imprint/compile-agent-types.ts +38 -0
- package/src/imprint/compile-agent.ts +63 -25
- package/src/imprint/compile-tools.ts +1666 -66
- package/src/imprint/compile.ts +13 -1
- package/src/imprint/concurrency.ts +87 -0
- package/src/imprint/cron.ts +4 -0
- package/src/imprint/doctor.ts +48 -3
- package/src/imprint/freeform-redact.ts +5 -4
- package/src/imprint/install.ts +79 -4
- package/src/imprint/integrations.ts +3 -3
- package/src/imprint/llm.ts +56 -8
- package/src/imprint/mcp-compile-server.ts +43 -10
- package/src/imprint/mcp-maintenance.ts +18 -102
- package/src/imprint/mcp-server.ts +73 -7
- package/src/imprint/multi-progress.ts +7 -2
- package/src/imprint/param-grounding.ts +367 -0
- package/src/imprint/paths.ts +29 -0
- package/src/imprint/playbook-runner.ts +101 -40
- package/src/imprint/prereq-builder.ts +651 -0
- package/src/imprint/probe-backends.ts +6 -3
- package/src/imprint/record.ts +10 -1
- package/src/imprint/redact.ts +30 -2
- package/src/imprint/replay-capture.ts +19 -18
- package/src/imprint/runtime.ts +19 -10
- package/src/imprint/session-diff.ts +79 -2
- package/src/imprint/session-merge.ts +9 -5
- package/src/imprint/stealth-chromium.ts +79 -0
- package/src/imprint/stealth-fetch.ts +309 -29
- package/src/imprint/stealth-token-cache.ts +88 -0
- package/src/imprint/teach-plan.ts +251 -0
- package/src/imprint/teach-state.ts +10 -0
- package/src/imprint/teach.ts +456 -142
- package/src/imprint/tool-candidates.ts +72 -14
- package/src/imprint/tool-plan.ts +313 -0
- package/src/imprint/tracing.ts +135 -6
- package/src/imprint/types.ts +61 -3
- package/examples/google-flights/search_google_flights/index.ts +0 -101
- package/examples/google-flights/search_google_flights/parser.test.ts +0 -140
- package/examples/google-flights/search_google_flights/parser.ts +0 -189
- package/examples/google-flights/search_google_flights/playbook.yaml +0 -130
- package/examples/google-flights/search_google_flights/workflow.json +0 -48
- package/examples/google-hotels/search_google_hotels/index.ts +0 -194
- package/examples/google-hotels/search_google_hotels/parser.test.ts +0 -168
- package/examples/google-hotels/search_google_hotels/parser.ts +0 -330
- package/examples/google-hotels/search_google_hotels/playbook.yaml +0 -125
- package/examples/google-hotels/search_google_hotels/workflow.json +0 -111
- package/examples/namecheap-domains/search_namecheap_domains/index.ts +0 -144
- package/examples/namecheap-domains/search_namecheap_domains/parser.ts +0 -380
- package/examples/namecheap-domains/search_namecheap_domains/playbook.yaml +0 -50
- package/examples/namecheap-domains/search_namecheap_domains/request-transform.ts +0 -136
- package/examples/namecheap-domains/search_namecheap_domains/workflow.json +0 -97
|
@@ -0,0 +1,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,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,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
|
+
}
|