imprint-mcp 0.4.11 → 0.5.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 +6 -4
- package/examples/discoverandgo/README.md +1 -1
- package/examples/discoverandgo/book_discoverandgo_museum_pass/workflow.json +7 -0
- package/examples/google-flights/README.md +41 -14
- package/examples/google-flights/_shared/google_batchexecute_parser.ts +165 -0
- package/examples/google-flights/_shared/google_flights_transport.ts +70 -0
- package/examples/google-flights/_shared/google_flights_types.ts +56 -0
- package/examples/google-flights/get_flight_booking_options/backends.json +43 -0
- package/examples/google-flights/get_flight_booking_options/index.ts +129 -0
- package/examples/google-flights/get_flight_booking_options/parser.ts +209 -0
- package/examples/google-flights/get_flight_booking_options/playbook.yaml +46 -0
- package/examples/google-flights/get_flight_booking_options/request-transform.ts +210 -0
- package/examples/google-flights/get_flight_booking_options/workflow.json +85 -0
- package/examples/google-flights/get_flight_calendar_prices/backends.json +27 -0
- package/examples/google-flights/get_flight_calendar_prices/index.ts +44 -29
- package/examples/google-flights/get_flight_calendar_prices/parser.ts +76 -69
- package/examples/google-flights/get_flight_calendar_prices/playbook.yaml +67 -40
- package/examples/google-flights/get_flight_calendar_prices/workflow.json +29 -17
- package/examples/google-flights/get_flight_location_details/backends.json +27 -0
- package/examples/google-flights/{lookup_airport → get_flight_location_details}/index.ts +30 -20
- package/examples/google-flights/get_flight_location_details/parser.ts +103 -0
- package/examples/google-flights/get_flight_location_details/playbook.yaml +59 -0
- package/examples/google-flights/{lookup_airport → get_flight_location_details}/workflow.json +19 -12
- package/examples/google-flights/lookup_flight_locations/backends.json +27 -0
- package/examples/google-flights/lookup_flight_locations/index.ts +111 -0
- package/examples/google-flights/lookup_flight_locations/package.json +9 -0
- package/examples/google-flights/lookup_flight_locations/parser.ts +135 -0
- package/examples/google-flights/lookup_flight_locations/playbook.yaml +47 -0
- package/examples/google-flights/lookup_flight_locations/request-transform.ts +53 -0
- package/examples/google-flights/lookup_flight_locations/workflow.json +64 -0
- package/examples/google-flights/search_flights/backends.json +42 -0
- package/examples/google-flights/search_flights/index.ts +105 -69
- package/examples/google-flights/search_flights/parser.ts +186 -183
- package/examples/google-flights/search_flights/playbook.yaml +295 -88
- package/examples/google-flights/search_flights/request-transform.ts +247 -92
- package/examples/google-flights/search_flights/workflow.json +66 -40
- package/examples/google-flights/validate_flight_itinerary/backends.json +27 -0
- package/examples/google-flights/validate_flight_itinerary/index.ts +103 -0
- package/examples/google-flights/validate_flight_itinerary/package.json +9 -0
- package/examples/google-flights/validate_flight_itinerary/parser.ts +101 -0
- package/examples/google-flights/validate_flight_itinerary/playbook.yaml +84 -0
- package/examples/google-flights/validate_flight_itinerary/request-transform.ts +92 -0
- package/examples/google-flights/validate_flight_itinerary/workflow.json +59 -0
- package/examples/southwest/README.md +2 -2
- package/examples/southwest/search_southwest_flights/index.ts +2 -1
- package/examples/southwest/search_southwest_flights/workflow.json +2 -1
- package/package.json +1 -1
- package/prompts/audit-agent.md +1 -1
- package/prompts/auth-compile-agent.md +165 -0
- package/prompts/build-planning.md +32 -4
- package/prompts/compile-agent.md +66 -55
- package/prompts/prereq-builder.md +3 -1
- package/prompts/tool-candidate-detection.md +58 -18
- package/src/cli.ts +61 -6
- package/src/imprint/audit.ts +304 -32
- package/src/imprint/auth-bootstrap.ts +178 -0
- package/src/imprint/auth-compile-agent.ts +538 -0
- package/src/imprint/auth-compile-tools.ts +209 -0
- package/src/imprint/auth-verifier.ts +234 -0
- package/src/imprint/backend-ladder.ts +229 -40
- package/src/imprint/bot-defense.ts +12 -0
- package/src/imprint/build-plan.ts +980 -15
- package/src/imprint/cdp-browser-fetch.ts +339 -55
- package/src/imprint/cdp-jar-cache.ts +6 -5
- package/src/imprint/chromium.ts +34 -0
- package/src/imprint/claude-cli-compile.ts +165 -58
- package/src/imprint/codex-cli-compile.ts +50 -19
- package/src/imprint/compile-agent-types.ts +42 -2
- package/src/imprint/compile-agent.ts +5 -5
- package/src/imprint/compile-tools.ts +711 -20
- package/src/imprint/compile.ts +42 -3
- package/src/imprint/credential-extract.ts +87 -0
- package/src/imprint/credential-store.ts +24 -0
- package/src/imprint/cron.ts +1 -6
- package/src/imprint/endpoint-key.ts +23 -0
- package/src/imprint/login.ts +83 -75
- package/src/imprint/mcp-compile-server.ts +231 -20
- package/src/imprint/mcp-server.ts +45 -13
- package/src/imprint/param-grounding.ts +61 -52
- package/src/imprint/playbook-runner.ts +188 -26
- package/src/imprint/redact.ts +64 -19
- package/src/imprint/request-capture.ts +93 -0
- package/src/imprint/runtime.ts +393 -35
- package/src/imprint/sensitive-keys.ts +18 -0
- package/src/imprint/stealth-fetch.ts +3 -19
- package/src/imprint/teach-plan.ts +16 -3
- package/src/imprint/teach-state.ts +229 -0
- package/src/imprint/teach.ts +634 -157
- package/src/imprint/tool-candidates.ts +21 -4
- package/src/imprint/types.ts +100 -1
- package/examples/google-flights/_shared/batchexecute.ts +0 -63
- package/examples/google-flights/_shared/flights_request.ts +0 -97
- package/examples/google-flights/get_flight_booking_details/index.ts +0 -159
- package/examples/google-flights/get_flight_booking_details/parser.ts +0 -182
- package/examples/google-flights/get_flight_booking_details/playbook.yaml +0 -138
- package/examples/google-flights/get_flight_booking_details/request-transform.ts +0 -86
- package/examples/google-flights/get_flight_booking_details/workflow.json +0 -98
- package/examples/google-flights/get_flight_calendar_prices/request-transform.ts +0 -31
- package/examples/google-flights/lookup_airport/parser.ts +0 -66
- package/examples/google-flights/lookup_airport/playbook.yaml +0 -47
- package/examples/google-flights/lookup_airport/request-transform.ts +0 -20
- /package/examples/google-flights/{get_flight_booking_details → get_flight_booking_options}/package.json +0 -0
- /package/examples/google-flights/{lookup_airport → get_flight_location_details}/package.json +0 -0
package/README.md
CHANGED
|
@@ -51,9 +51,9 @@ When `HERMES_HOME` is set, Imprint writes Hermes MCP entries to `$HERMES_HOME/co
|
|
|
51
51
|
|
|
52
52
|
## See It in Action
|
|
53
53
|
|
|
54
|
-
**Teach once.** `imprint teach google-flights` records
|
|
54
|
+
**Teach once.** `imprint teach google-flights` records real browser flows and compiles a **6-tool** MCP server — the compile agent reverse-engineers Google's `batchexecute` wire format itself and wires the staged search→booking token chain, with no hand-written request code. Here is the actual run behind the example snapshot:
|
|
55
55
|
|
|
56
|
-

|
|
57
57
|
|
|
58
58
|
**Then your agent calls those tools** like any other — real-time results through a live trusted-Chrome (`cdp-replay`) backend:
|
|
59
59
|
|
|
@@ -65,7 +65,9 @@ $ claude "cheapest nonstop SJC→SAN the first week of July, with a carry-on"
|
|
|
65
65
|
Delta DL2901 SJC→SAN 7:10a→8:44a nonstop $169
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
The
|
|
68
|
+
The current example snapshot includes six live-verified tools, including staged
|
|
69
|
+
multi-city search and booking. *(The terminal above is a faithful replay —
|
|
70
|
+
regenerate/record it with `bun scripts/demo-teach.ts`.)*
|
|
69
71
|
|
|
70
72
|
---
|
|
71
73
|
|
|
@@ -210,7 +212,7 @@ Every example below was **one-shot compiled from a single real browser-session r
|
|
|
210
212
|
|
|
211
213
|
| Example | Tools | Audit | What it shows |
|
|
212
214
|
|:--|:--|:--|:--|
|
|
213
|
-
| [**google-flights**](examples/google-flights) |
|
|
215
|
+
| [**google-flights**](examples/google-flights) | 6 | live-verified | `batchexecute` wire-format decode + staged multi-city search→booking token chain, live `cdp-replay` |
|
|
214
216
|
| [**google-hotels**](examples/google-hotels) | 4 | 91.7% | autocomplete → search → reviews/booking producer-token chaining |
|
|
215
217
|
|
|
216
218
|
Other examples:
|
|
@@ -49,7 +49,7 @@ imprint cron discoverandgo --once
|
|
|
49
49
|
## Notes
|
|
50
50
|
|
|
51
51
|
- Discover & Go's auth model is patron-ID + session cookies. The session cookie expires; re-run `imprint login` if you start seeing AUTH_EXPIRED.
|
|
52
|
-
- `imprint login`
|
|
52
|
+
- `imprint login` resolves the workflow's `authConfig.sessionCapture` declarations against the recorded Login response — `patron_id ← patronID`, `session_id ← session`, `patron_email ← patronEmail` — and stores each in the credential store. The resolver is fully generic; the per-site field mapping lives in `workflow.json`, not in imprint's code. The booking `workflow.json` then references `${credential.patron_id}` — no Login call is replayed at runtime.
|
|
53
53
|
|
|
54
54
|
## Not in this demo
|
|
55
55
|
|
|
@@ -35,5 +35,12 @@
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
],
|
|
38
|
+
"authConfig": {
|
|
39
|
+
"sessionCapture": [
|
|
40
|
+
{ "name": "patron_id", "source": "json", "path": "patronID" },
|
|
41
|
+
{ "name": "session_id", "source": "json", "path": "session" },
|
|
42
|
+
{ "name": "patron_email", "source": "json", "path": "patronEmail" }
|
|
43
|
+
]
|
|
44
|
+
},
|
|
38
45
|
"site": "discoverandgo"
|
|
39
46
|
}
|
|
@@ -1,23 +1,49 @@
|
|
|
1
|
-
# Google Flights
|
|
1
|
+
# Google Flights - `imprint-google-flights`
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Generated Google Flights MCP example. This snapshot was refreshed from the
|
|
4
|
+
> audited `~/.imprint/google-flights` tools after live verification of one-way,
|
|
5
|
+
> round-trip, and staged multi-city booking flows.
|
|
4
6
|
|
|
5
|
-
A
|
|
7
|
+
A 6-tool MCP server for Google Flights, compiled from recorded browser sessions
|
|
8
|
+
and checked in as an example of a generated Imprint integration.
|
|
6
9
|
|
|
7
10
|
## Tools
|
|
8
11
|
|
|
9
12
|
| Tool | What it does | Notes |
|
|
10
13
|
|---|---|---|
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
14
|
+
| `lookup_flight_locations` | Resolve a city, airport, or region query to Google Flights location entities | Use before search when a user gives ambiguous locations. |
|
|
15
|
+
| `get_flight_location_details` | Return structured details for a Google Flights location token | Helpful for validating selected origins and destinations. |
|
|
16
|
+
| `search_flights` | Search one-way, round-trip, and multi-city itineraries with filters | Supports staged selection via `selection_token` and `selected_flights`. |
|
|
17
|
+
| `get_flight_booking_options` | Return fares and booking providers for selected flights | Pass the complete ordered `selected_flights` array for round-trip and multi-city bookings. |
|
|
18
|
+
| `get_flight_calendar_prices` | Return calendar fare data for a route/date window | Uses Google Flights calendar RPC data. |
|
|
19
|
+
| `validate_flight_itinerary` | Validate itinerary details against Google Flights | Useful for checking complete selected itineraries before booking. |
|
|
20
|
+
|
|
21
|
+
## Multi-City Selection
|
|
22
|
+
|
|
23
|
+
For round-trip and multi-city booking, Google Flights mints booking state from
|
|
24
|
+
the route selections made so far. Agents should use the staged contract:
|
|
25
|
+
|
|
26
|
+
1. Call `search_flights` with `trip_type="multi_city"` and an ordered
|
|
27
|
+
`itinerary`, for example `BOM,SFO,2026-09-07;BOS,BOM,2026-10-12`.
|
|
28
|
+
2. Pick a first-leg result and call `search_flights` again with that result's
|
|
29
|
+
`selection_token` and `selected_flights` to fetch the next leg options.
|
|
30
|
+
3. Pass the complete ordered `selected_flights` array from the staged result to
|
|
31
|
+
`get_flight_booking_options`.
|
|
32
|
+
|
|
33
|
+
Passing only the first selected leg to `get_flight_booking_options` is treated
|
|
34
|
+
as a one-way booking. For open-jaw trips, keep the trip as `multi_city`; do not
|
|
35
|
+
rewrite it as a round trip.
|
|
36
|
+
|
|
37
|
+
## How It Was Compiled
|
|
38
|
+
|
|
39
|
+
- **Protocol**: Google Flights uses the `/_/FlightsFrontendUi` `batchexecute`
|
|
40
|
+
endpoint. Shared parser and transport helpers live in `_shared`, with
|
|
41
|
+
per-tool request transforms and parsers in each tool directory.
|
|
42
|
+
- **Anti-bot**: current backend preferences are committed in each
|
|
43
|
+
`backends.json`; booking and search use `cdp-replay` where Google state must
|
|
44
|
+
be minted inside a live browser context.
|
|
45
|
+
- **Artifacts per tool**: `workflow.json` (API replay), `playbook.yaml`
|
|
46
|
+
(DOM fallback), `index.ts` (MCP tool), and parser/transform code where needed.
|
|
21
47
|
|
|
22
48
|
## Install
|
|
23
49
|
|
|
@@ -25,4 +51,5 @@ A 4-tool MCP server for Google Flights, compiled from a recording of a normal fl
|
|
|
25
51
|
imprint install google-flights --source examples --platform claude-desktop
|
|
26
52
|
```
|
|
27
53
|
|
|
28
|
-
|
|
54
|
+
Recording-derived defaults such as dates age out, so pass explicit values. See
|
|
55
|
+
the repo [README](../../README.md) and [architecture docs](../../docs/architecture.md).
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export type WrbRecord = { rpcid: string | null; payload: unknown; rawPayload: string | null; envelope: unknown[] };
|
|
2
|
+
|
|
3
|
+
const XSSI_PREFIX = ")]}'";
|
|
4
|
+
|
|
5
|
+
export function stripXssiPrefix(body: string): string {
|
|
6
|
+
const withoutPrefix = body.startsWith(XSSI_PREFIX) ? body.slice(XSSI_PREFIX.length) : body;
|
|
7
|
+
return withoutPrefix.replace(/^\s+/, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function utf8EndOffset(text: string, start: number, byteLength: number): number | null {
|
|
11
|
+
let offset = start;
|
|
12
|
+
let bytes = 0;
|
|
13
|
+
|
|
14
|
+
while (offset < text.length && bytes < byteLength) {
|
|
15
|
+
const codePoint = text.codePointAt(offset);
|
|
16
|
+
if (codePoint === undefined) return null;
|
|
17
|
+
|
|
18
|
+
if (codePoint <= 0x7f) bytes += 1;
|
|
19
|
+
else if (codePoint <= 0x7ff) bytes += 2;
|
|
20
|
+
else if (codePoint <= 0xffff) bytes += 3;
|
|
21
|
+
else bytes += 4;
|
|
22
|
+
|
|
23
|
+
offset += codePoint > 0xffff ? 2 : 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return bytes === byteLength ? offset : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findBalancedJsonEnd(text: string, start: number): number | null {
|
|
30
|
+
let offset = start;
|
|
31
|
+
while (offset < text.length && /\s/.test(text.charAt(offset))) offset += 1;
|
|
32
|
+
|
|
33
|
+
const first = text.charAt(offset);
|
|
34
|
+
if (first !== "[" && first !== "{") return null;
|
|
35
|
+
|
|
36
|
+
const stack: string[] = [];
|
|
37
|
+
let inString = false;
|
|
38
|
+
let escaped = false;
|
|
39
|
+
|
|
40
|
+
for (let i = offset; i < text.length; i += 1) {
|
|
41
|
+
const ch = text.charAt(i);
|
|
42
|
+
|
|
43
|
+
if (inString) {
|
|
44
|
+
if (escaped) {
|
|
45
|
+
escaped = false;
|
|
46
|
+
} else if (ch === "\\") {
|
|
47
|
+
escaped = true;
|
|
48
|
+
} else if (ch === "\"") {
|
|
49
|
+
inString = false;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (ch === "\"") {
|
|
55
|
+
inString = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (ch === "[" || ch === "{") {
|
|
60
|
+
stack.push(ch === "[" ? "]" : "}");
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (ch === "]" || ch === "}") {
|
|
65
|
+
const expected = stack.pop();
|
|
66
|
+
if (expected !== ch) return null;
|
|
67
|
+
if (stack.length === 0) return i + 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function tryParseFrame(text: string, start: number, frameLength: number): { parsed: unknown; end: number } | null {
|
|
75
|
+
const charEnd = start + frameLength;
|
|
76
|
+
if (charEnd <= text.length) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed: unknown = JSON.parse(text.slice(start, charEnd));
|
|
79
|
+
return { parsed, end: charEnd };
|
|
80
|
+
} catch {
|
|
81
|
+
// Google frame lengths are byte counts in some recordings; retry with UTF-8 offsets.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const byteEnd = utf8EndOffset(text, start, frameLength);
|
|
86
|
+
if (byteEnd !== null && byteEnd <= text.length) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed: unknown = JSON.parse(text.slice(start, byteEnd));
|
|
89
|
+
return { parsed, end: byteEnd };
|
|
90
|
+
} catch {
|
|
91
|
+
// Fall through to balanced parsing for recordings whose stored text length differs.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const balancedEnd = findBalancedJsonEnd(text, start);
|
|
96
|
+
if (balancedEnd === null) return null;
|
|
97
|
+
const parsed: unknown = JSON.parse(text.slice(start, balancedEnd));
|
|
98
|
+
return { parsed, end: balancedEnd };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function parseLengthFramedJson(body: string): unknown[] {
|
|
102
|
+
const text = stripXssiPrefix(body);
|
|
103
|
+
const frames: unknown[] = [];
|
|
104
|
+
let offset = 0;
|
|
105
|
+
|
|
106
|
+
while (offset < text.length) {
|
|
107
|
+
while (offset < text.length && /\s/.test(text.charAt(offset))) offset += 1;
|
|
108
|
+
if (offset >= text.length) break;
|
|
109
|
+
|
|
110
|
+
const match = /^(\d+)/.exec(text.slice(offset));
|
|
111
|
+
if (!match?.[1]) break;
|
|
112
|
+
|
|
113
|
+
const frameLength = Number(match[1]);
|
|
114
|
+
offset += match[1].length;
|
|
115
|
+
|
|
116
|
+
if (text.charAt(offset) === "\r") offset += 1;
|
|
117
|
+
if (text.charAt(offset) !== "\n") break;
|
|
118
|
+
offset += 1;
|
|
119
|
+
|
|
120
|
+
const frame = tryParseFrame(text, offset, frameLength);
|
|
121
|
+
if (frame === null) break;
|
|
122
|
+
|
|
123
|
+
frames.push(frame.parsed);
|
|
124
|
+
offset = frame.end;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return frames;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function parseNestedPayload(value: unknown): unknown {
|
|
131
|
+
if (typeof value !== "string") return value;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const parsed: unknown = JSON.parse(value);
|
|
135
|
+
return parsed;
|
|
136
|
+
} catch {
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function extractWrbRecords(body: string): WrbRecord[] {
|
|
142
|
+
const records: WrbRecord[] = [];
|
|
143
|
+
|
|
144
|
+
for (const frame of parseLengthFramedJson(body)) {
|
|
145
|
+
if (!Array.isArray(frame)) continue;
|
|
146
|
+
|
|
147
|
+
for (const maybeRecord of frame) {
|
|
148
|
+
if (!Array.isArray(maybeRecord)) continue;
|
|
149
|
+
if (maybeRecord[0] !== "wrb.fr") continue;
|
|
150
|
+
|
|
151
|
+
const rpcidValue = maybeRecord[1];
|
|
152
|
+
const payloadValue = maybeRecord[2];
|
|
153
|
+
const envelope = maybeRecord as unknown[];
|
|
154
|
+
|
|
155
|
+
records.push({
|
|
156
|
+
rpcid: typeof rpcidValue === "string" ? rpcidValue : null,
|
|
157
|
+
payload: parseNestedPayload(payloadValue),
|
|
158
|
+
rawPayload: typeof payloadValue === "string" ? payloadValue : null,
|
|
159
|
+
envelope,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return records;
|
|
165
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type GoogleFlightsTransformParams = { fReq: unknown; rpcid?: string; sourcePath?: string; referer?: string; headers?: Record<string, string>; state?: Record<string, string>; params?: Record<string, unknown> };
|
|
2
|
+
|
|
3
|
+
const DEFAULT_REFERER = "https://www.google.com/travel/flights";
|
|
4
|
+
const CONTENT_TYPE = "application/x-www-form-urlencoded;charset=UTF-8";
|
|
5
|
+
|
|
6
|
+
function getStringValue(primary: unknown, secondary: unknown, fallback: string | null): string | undefined {
|
|
7
|
+
if (typeof primary === "string" && primary.length > 0) return primary;
|
|
8
|
+
if (typeof secondary === "string" && secondary.length > 0) return secondary;
|
|
9
|
+
return fallback ?? undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function nextReqId(): string {
|
|
13
|
+
const randomPart = Math.floor(Math.random() * 1_000_000).toString().padStart(6, "0");
|
|
14
|
+
return `${Date.now()}${randomPart}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function encodeFReq(payload: unknown): string {
|
|
18
|
+
const json = JSON.stringify(payload);
|
|
19
|
+
if (json === undefined) {
|
|
20
|
+
throw new Error("f.req payload is not JSON-serializable");
|
|
21
|
+
}
|
|
22
|
+
return encodeURIComponent(json);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function transform(method: string, url: string, responses: unknown[], params?: GoogleFlightsTransformParams): { url: string; body: string; headers: Record<string, string> } {
|
|
26
|
+
void responses;
|
|
27
|
+
const urlObj = new URL(url);
|
|
28
|
+
const existing = urlObj.searchParams;
|
|
29
|
+
const isBatchExecute = urlObj.pathname.includes("/batchexecute");
|
|
30
|
+
|
|
31
|
+
const fSid = getStringValue(params?.state?.["f.sid"], params?.params?.["f.sid"], existing.get("f.sid"));
|
|
32
|
+
const bl = getStringValue(params?.state?.bl, params?.params?.bl, existing.get("bl"));
|
|
33
|
+
|
|
34
|
+
if (!fSid) throw new Error("Google Flights transform requires f.sid in state, params, or URL");
|
|
35
|
+
if (!bl) throw new Error("Google Flights transform requires bl in state, params, or URL");
|
|
36
|
+
|
|
37
|
+
const nextParams = new URLSearchParams();
|
|
38
|
+
if (isBatchExecute) {
|
|
39
|
+
const rpcid = params?.rpcid ?? existing.get("rpcids");
|
|
40
|
+
if (!rpcid) throw new Error("Google Flights batchexecute transform requires rpcid or existing rpcids");
|
|
41
|
+
nextParams.set("rpcids", rpcid);
|
|
42
|
+
nextParams.set("source-path", params?.sourcePath ?? existing.get("source-path") ?? "/travel/flights");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
nextParams.set("f.sid", fSid);
|
|
46
|
+
nextParams.set("bl", bl);
|
|
47
|
+
nextParams.set("hl", "en-US");
|
|
48
|
+
nextParams.set("soc-app", "162");
|
|
49
|
+
nextParams.set("soc-platform", "1");
|
|
50
|
+
nextParams.set("soc-device", "1");
|
|
51
|
+
nextParams.set("_reqid", nextReqId());
|
|
52
|
+
nextParams.set("rt", "c");
|
|
53
|
+
urlObj.search = nextParams.toString();
|
|
54
|
+
|
|
55
|
+
const headers: Record<string, string> = {
|
|
56
|
+
"X-Same-Domain": "1",
|
|
57
|
+
"Content-Type": CONTENT_TYPE,
|
|
58
|
+
Referer: params?.referer ?? params?.headers?.Referer ?? DEFAULT_REFERER,
|
|
59
|
+
...(params?.headers ?? {}),
|
|
60
|
+
};
|
|
61
|
+
headers["X-Same-Domain"] = "1";
|
|
62
|
+
headers["Content-Type"] = CONTENT_TYPE;
|
|
63
|
+
headers.Referer = params?.referer ?? headers.Referer ?? DEFAULT_REFERER;
|
|
64
|
+
|
|
65
|
+
// Some verifier passes only the recorded URL to check browser-minted _reqid regeneration.
|
|
66
|
+
const body = params ? `f.req=${encodeFReq(params.fReq)}&` : "f.req=&";
|
|
67
|
+
void method;
|
|
68
|
+
|
|
69
|
+
return { url: urlObj.toString(), body, headers };
|
|
70
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface FlightLocation {
|
|
2
|
+
id: string;
|
|
3
|
+
type: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
city?: string;
|
|
6
|
+
region?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
airportCode?: string;
|
|
9
|
+
placeId?: string;
|
|
10
|
+
coordinates?: { lat: number; lng: number };
|
|
11
|
+
imageUrls?: string[];
|
|
12
|
+
nestedAirports?: FlightLocation[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FlightLeg {
|
|
16
|
+
origin: string;
|
|
17
|
+
destination: string;
|
|
18
|
+
departureDate?: string;
|
|
19
|
+
departureTime?: string;
|
|
20
|
+
arrivalDate?: string;
|
|
21
|
+
arrivalTime?: string;
|
|
22
|
+
airline?: string;
|
|
23
|
+
carrierCode?: string;
|
|
24
|
+
flightNumber?: string;
|
|
25
|
+
durationMinutes?: number;
|
|
26
|
+
stops?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FlightItinerary {
|
|
30
|
+
price?: number;
|
|
31
|
+
currency?: string;
|
|
32
|
+
legs: FlightLeg[];
|
|
33
|
+
selection_token?: string;
|
|
34
|
+
selected_flights?: string;
|
|
35
|
+
emissions?: unknown;
|
|
36
|
+
group?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CalendarPrice {
|
|
40
|
+
departureDate: string;
|
|
41
|
+
returnDate?: string;
|
|
42
|
+
price?: number;
|
|
43
|
+
currency?: string;
|
|
44
|
+
tripLength?: string;
|
|
45
|
+
selectionToken?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BookingOption {
|
|
49
|
+
provider?: string;
|
|
50
|
+
price?: number;
|
|
51
|
+
currency?: string;
|
|
52
|
+
bookingUrl?: string;
|
|
53
|
+
fareNotes?: string[];
|
|
54
|
+
restrictions?: string[];
|
|
55
|
+
legs: FlightLeg[];
|
|
56
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"probedAt": "2026-07-01T02:13:17.022Z",
|
|
3
|
+
"imprintVersion": "0.5.0",
|
|
4
|
+
"schemaVersion": 2,
|
|
5
|
+
"workflowHash": "14325a4a20e8d5a760cd76bf967f4425da4358bf202a0da2b8e3c4efe26c4111",
|
|
6
|
+
"capabilityHash": "5fc16811cd65aff7afee349ddca05c806a65c9c2c96948192ea1bafba9284c38",
|
|
7
|
+
"preferredOrder": [
|
|
8
|
+
"cdp-replay"
|
|
9
|
+
],
|
|
10
|
+
"results": {
|
|
11
|
+
"fetch": {
|
|
12
|
+
"outcome": "failed",
|
|
13
|
+
"durationMs": 57,
|
|
14
|
+
"error": "STATE_MISSING",
|
|
15
|
+
"detail": "Workflow placeholder ${state.bl} but state \"bl\" has not been captured yet"
|
|
16
|
+
},
|
|
17
|
+
"fetch-bootstrap": {
|
|
18
|
+
"outcome": "forbidden",
|
|
19
|
+
"durationMs": 30797,
|
|
20
|
+
"detail": "fetch-bootstrap: cdp-minted jar did not validate; cdp-replay (in-page) required."
|
|
21
|
+
},
|
|
22
|
+
"cdp-replay": {
|
|
23
|
+
"outcome": "ok",
|
|
24
|
+
"durationMs": 32258,
|
|
25
|
+
"coldDurationMs": 32258,
|
|
26
|
+
"warmDurationMs": 3103,
|
|
27
|
+
"rankingDurationMs": 3103,
|
|
28
|
+
"detail": "warm cdp-replay succeeded in 3103ms"
|
|
29
|
+
},
|
|
30
|
+
"stealth-fetch": {
|
|
31
|
+
"outcome": "failed",
|
|
32
|
+
"durationMs": 5396,
|
|
33
|
+
"error": "BAD_RESPONSE",
|
|
34
|
+
"detail": "Request 0 (POST https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults?f.sid=-2694610512443079423&bl=boq_travel-frontend-flights-ui_20260629."
|
|
35
|
+
},
|
|
36
|
+
"playbook": {
|
|
37
|
+
"outcome": "failed",
|
|
38
|
+
"durationMs": 14,
|
|
39
|
+
"error": "UNKNOWN",
|
|
40
|
+
"detail": "Missing required parameter: selection_token\n→ pass --param selection_token=<value> on the CLI, or set it in cron.json."
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by `imprint emit` — DO NOT EDIT BY HAND.
|
|
3
|
+
*
|
|
4
|
+
* Tool: get_flight_booking_options
|
|
5
|
+
* Site: google-flights
|
|
6
|
+
* Intent: Get booking and fare details for a selected Google Flights itinerary.
|
|
7
|
+
*
|
|
8
|
+
* To regenerate: imprint emit ~/.imprint/google-flights/get_flight_booking_options/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_options",
|
|
21
|
+
"intent": {
|
|
22
|
+
"description": "Get booking and fare details for a selected Google Flights itinerary.",
|
|
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; clicked the option for the first leg; clicked the option for the second leg and came to the booking options page"
|
|
24
|
+
},
|
|
25
|
+
"parameters": [
|
|
26
|
+
{
|
|
27
|
+
"name": "selected_flights",
|
|
28
|
+
"type": "string",
|
|
29
|
+
"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.",
|
|
30
|
+
"verified": true,
|
|
31
|
+
"sourcedFrom": {
|
|
32
|
+
"tool": "search_flights",
|
|
33
|
+
"field": "selected_flights"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"requests": [
|
|
38
|
+
{
|
|
39
|
+
"method": "POST",
|
|
40
|
+
"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",
|
|
41
|
+
"headers": {
|
|
42
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
43
|
+
"X-Same-Domain": "1",
|
|
44
|
+
"sec-ch-ua-full-version": "\"148.0.7778.96\"",
|
|
45
|
+
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
|
|
46
|
+
},
|
|
47
|
+
"body": "{\"selected_flights\":\"${param.selected_flights}\"}",
|
|
48
|
+
"effect": "safe"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"method": "POST",
|
|
52
|
+
"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",
|
|
53
|
+
"headers": {
|
|
54
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
55
|
+
"X-Same-Domain": "1",
|
|
56
|
+
"sec-ch-ua-full-version": "\"148.0.7778.96\"",
|
|
57
|
+
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
|
|
58
|
+
},
|
|
59
|
+
"body": "{\"selected_flights\":\"${param.selected_flights}\"}",
|
|
60
|
+
"effect": "safe"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"method": "POST",
|
|
64
|
+
"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",
|
|
65
|
+
"headers": {
|
|
66
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
67
|
+
"X-Same-Domain": "1",
|
|
68
|
+
"sec-ch-ua-full-version": "\"148.0.7778.96\"",
|
|
69
|
+
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
|
|
70
|
+
},
|
|
71
|
+
"body": "{\"selected_flights\":\"${param.selected_flights}\"}",
|
|
72
|
+
"effect": "safe"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"site": "google-flights",
|
|
76
|
+
"bootstrap": {
|
|
77
|
+
"url": "https://www.google.com/travel/flights/search?tfs=CBwQAhoeEgoyMDI2LTA2LTA4agcIARIDU0pDcgcIARIDU0FOGh4SCjIwMjYtMDYtMTFqBwgBEgNTQU5yBwgBEgNTSkNAAUgBcAGCAQsI____________AZgBAQ",
|
|
78
|
+
"waitUntil": "domcontentloaded",
|
|
79
|
+
"waitMs": 500,
|
|
80
|
+
"timeoutMs": 30000,
|
|
81
|
+
"captures": [
|
|
82
|
+
{
|
|
83
|
+
"name": "f_sid",
|
|
84
|
+
"required": true,
|
|
85
|
+
"capability": "browser_bootstrap",
|
|
86
|
+
"source": "html_regex",
|
|
87
|
+
"pattern": "(?:FdrFJe|f\\.sid)[^0-9-]{0,80}(-?[0-9]{10,})",
|
|
88
|
+
"group": 1
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "bl",
|
|
92
|
+
"required": true,
|
|
93
|
+
"capability": "browser_bootstrap",
|
|
94
|
+
"source": "html_regex",
|
|
95
|
+
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
96
|
+
"group": 1
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"parserModule": "./parser.ts",
|
|
101
|
+
"requestTransformModule": "./request-transform.ts",
|
|
102
|
+
"liveVerified": true
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export interface GetFlightBookingOptionsInput {
|
|
106
|
+
/** 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. */
|
|
107
|
+
selected_flights: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function getFlightBookingOptions(
|
|
111
|
+
input: GetFlightBookingOptionsInput,
|
|
112
|
+
opts: { credentials?: CredentialStore; fetchImpl?: typeof fetch; initialState?: Record<string, unknown> } = {},
|
|
113
|
+
): Promise<ToolResult> {
|
|
114
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
115
|
+
const params: Record<string, string | number | boolean> = {
|
|
116
|
+
selected_flights: input.selected_flights,
|
|
117
|
+
|
|
118
|
+
};
|
|
119
|
+
return executeWorkflow({
|
|
120
|
+
workflow: WORKFLOW,
|
|
121
|
+
params,
|
|
122
|
+
credentials: opts.credentials,
|
|
123
|
+
fetchImpl: opts.fetchImpl,
|
|
124
|
+
initialState: opts.initialState,
|
|
125
|
+
workflowPath: join(__dirname, 'workflow.json'),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { WORKFLOW };
|