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,66 @@
|
|
|
1
|
+
import { extractRpcPayload } from '../_shared/batchexecute.ts';
|
|
2
|
+
|
|
3
|
+
// Parse the tDoGIe (airport/place resolution) batchexecute response.
|
|
4
|
+
//
|
|
5
|
+
// Decoded payload shape (seq 69):
|
|
6
|
+
// [ [null,null,0,"<token>"],
|
|
7
|
+
// [ // <- payload[1]: array of matches
|
|
8
|
+
// [ ["SJC",0], "San Jose Mineta International Airport",
|
|
9
|
+
// ["/m/0f04v","San Jose",[[img],[img]]], [37.3627778,-121.92917],
|
|
10
|
+
// "US", false, "United States" ],
|
|
11
|
+
// [ ["/m/0f04v",4], "San Jose", ["/m/0f04v","San Jose",...], [37.33874,-121.8852525], ... ]
|
|
12
|
+
// ] ]
|
|
13
|
+
// Each match item: [0][0]=code (IATA or /m/ entity id), [1]=name,
|
|
14
|
+
// [2][1]=associated city, [3]=[lat,lng], [4]=country code, [6]=country name.
|
|
15
|
+
|
|
16
|
+
interface AirportMatch {
|
|
17
|
+
code: string | null;
|
|
18
|
+
name: string | null;
|
|
19
|
+
city: string | null;
|
|
20
|
+
lat: number | null;
|
|
21
|
+
lng: number | null;
|
|
22
|
+
country: string | null;
|
|
23
|
+
countryName: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseItem(item: unknown): AirportMatch | null {
|
|
27
|
+
if (!Array.isArray(item)) return null;
|
|
28
|
+
const code = Array.isArray(item[0]) ? (item[0][0] ?? null) : null;
|
|
29
|
+
const name = typeof item[1] === 'string' ? item[1] : null;
|
|
30
|
+
const city = Array.isArray(item[2]) && typeof item[2][1] === 'string' ? item[2][1] : null;
|
|
31
|
+
const coords = item[3];
|
|
32
|
+
const lat = Array.isArray(coords) && typeof coords[0] === 'number' ? coords[0] : null;
|
|
33
|
+
const lng = Array.isArray(coords) && typeof coords[1] === 'number' ? coords[1] : null;
|
|
34
|
+
const country = typeof item[4] === 'string' ? item[4] : null;
|
|
35
|
+
const countryName = typeof item[6] === 'string' ? item[6] : null;
|
|
36
|
+
const m: AirportMatch = { code, name, city, lat, lng, country, countryName };
|
|
37
|
+
// Drop content-less placeholder records (API no-match sentinel).
|
|
38
|
+
if (m.code == null && m.name == null) return null;
|
|
39
|
+
return m;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function extract(
|
|
43
|
+
rawResponse: unknown,
|
|
44
|
+
context?: { params: Record<string, string | number | boolean>; responses: unknown[] },
|
|
45
|
+
): unknown {
|
|
46
|
+
const raw = typeof rawResponse === 'string' ? rawResponse : JSON.stringify(rawResponse);
|
|
47
|
+
const payload = extractRpcPayload(raw, 'tDoGIe');
|
|
48
|
+
|
|
49
|
+
const matchesRaw = Array.isArray(payload) && Array.isArray(payload[1]) ? payload[1] : [];
|
|
50
|
+
const matches = matchesRaw
|
|
51
|
+
.map(parseItem)
|
|
52
|
+
.filter((m): m is AirportMatch => m !== null);
|
|
53
|
+
|
|
54
|
+
const primary = matches[0] ?? null;
|
|
55
|
+
return {
|
|
56
|
+
query: context?.params?.query ?? null,
|
|
57
|
+
matchCount: matches.length,
|
|
58
|
+
matches,
|
|
59
|
+
// Convenience: hoist the best (first) match to the top level.
|
|
60
|
+
code: primary?.code ?? null,
|
|
61
|
+
name: primary?.name ?? null,
|
|
62
|
+
city: primary?.city ?? null,
|
|
63
|
+
lat: primary?.lat ?? null,
|
|
64
|
+
lng: primary?.lng ?? null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
toolName: lookup_airport
|
|
2
|
+
summary: Resolve a city or airport query (e.g. "SJC" or "san jose") into structured airport matches via Google Flights' autocomplete RPC.
|
|
3
|
+
parameters:
|
|
4
|
+
- name: query
|
|
5
|
+
type: string
|
|
6
|
+
description: City or airport search text, e.g. "san jose" or "SJC".
|
|
7
|
+
steps:
|
|
8
|
+
- action: navigate
|
|
9
|
+
url: https://www.google.com/travel/flights
|
|
10
|
+
wait_for: networkidle
|
|
11
|
+
- action: click
|
|
12
|
+
locators:
|
|
13
|
+
- by: aria_label
|
|
14
|
+
value: "Where from?"
|
|
15
|
+
- by: css
|
|
16
|
+
value: "div.cQnuXe.k0gFV input.II2One.j0Ppje"
|
|
17
|
+
wait_for:
|
|
18
|
+
sleep_ms: 300
|
|
19
|
+
- action: type
|
|
20
|
+
locators:
|
|
21
|
+
- by: aria_label
|
|
22
|
+
value: "Where else?"
|
|
23
|
+
- by: aria_label
|
|
24
|
+
value: "Where from?"
|
|
25
|
+
- by: css
|
|
26
|
+
value: "input.II2One.j0Ppje"
|
|
27
|
+
value: ${query}
|
|
28
|
+
wait_for:
|
|
29
|
+
xhr: "batchexecute?rpcids=tDoGIe"
|
|
30
|
+
method: POST
|
|
31
|
+
result:
|
|
32
|
+
source: xhr
|
|
33
|
+
url_pattern: "rpcids=tDoGIe"
|
|
34
|
+
extract: "1[]"
|
|
35
|
+
return_as: airports
|
|
36
|
+
notes: >-
|
|
37
|
+
The tDoGIe response is a Google batchexecute payload, not plain JSON. The
|
|
38
|
+
downstream parser must: (1) strip the )]}' prefix and the numeric length-prefixed
|
|
39
|
+
chunk markers, (2) locate the ["wrb.fr","tDoGIe", <payload-string>, ...] envelope,
|
|
40
|
+
(3) JSON.parse the double-encoded <payload-string>. After decoding, the parsed
|
|
41
|
+
array's index [1] is the list of matches (the `extract` path "1[]" above). Each
|
|
42
|
+
match is positional: code = [0][0] (e.g. "SJC"), name = [1] (e.g. "San Jose Mineta
|
|
43
|
+
International Airport"), city = [2][1], lat = [3][0], lng = [3][1], country = [6].
|
|
44
|
+
Return one {code, name, city, lat, lng} object per match. The XHR is authorized by
|
|
45
|
+
the page-bootstrap session id (f.sid) and X-Goog-BatchExecute-Bgr header, which are
|
|
46
|
+
session-bound — they must come from a freshly loaded page (handled by the navigate
|
|
47
|
+
step), not parameterized. No login/auth is required.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Builds the tDoGIe (airport lookup) batchexecute POST body.
|
|
2
|
+
//
|
|
3
|
+
// The body is a single form field `f.req` whose value is a doubly-encoded JSON
|
|
4
|
+
// array: f.req=[[["tDoGIe","[null,[[\"<query>\",0]]]",null,"generic"]]]
|
|
5
|
+
// The inner element is itself a JSON STRING (JSON-in-JSON), so we build it with
|
|
6
|
+
// two JSON.stringify passes, then URL-encode the whole thing. Recorded seq 69
|
|
7
|
+
// used query="SJC". This is the tool's own transform (the shared flights_request
|
|
8
|
+
// module targets the FlightsFrontendService RPCs, not tDoGIe).
|
|
9
|
+
export function transform(
|
|
10
|
+
_method: string,
|
|
11
|
+
url: string,
|
|
12
|
+
_responses: unknown[],
|
|
13
|
+
params?: Record<string, string | number | boolean>,
|
|
14
|
+
): { url: string; body: string } {
|
|
15
|
+
const query = String(params?.query ?? '').trim();
|
|
16
|
+
const inner = JSON.stringify([null, [[query, 0]]]);
|
|
17
|
+
const outer = JSON.stringify([[['tDoGIe', inner, null, 'generic']]]);
|
|
18
|
+
const body = 'f.req=' + encodeURIComponent(outer) + '&';
|
|
19
|
+
return { url, body };
|
|
20
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"toolName": "lookup_airport",
|
|
3
|
+
"intent": {
|
|
4
|
+
"description": "Resolve a city or airport query into airport codes and structured airport details.",
|
|
5
|
+
"userSaid": "multi city from SJC to SAN to LAX to SFO"
|
|
6
|
+
},
|
|
7
|
+
"site": "google-flights",
|
|
8
|
+
"parameters": [
|
|
9
|
+
{
|
|
10
|
+
"name": "query",
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "City or airport search text, e.g. 'san jose' or 'SJC'",
|
|
13
|
+
"verified": true
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"bootstrap": {
|
|
17
|
+
"url": "https://www.google.com/travel/flights",
|
|
18
|
+
"waitUntil": "domcontentloaded",
|
|
19
|
+
"timeoutMs": 30000,
|
|
20
|
+
"captures": [
|
|
21
|
+
{
|
|
22
|
+
"source": "html_regex",
|
|
23
|
+
"name": "f_sid",
|
|
24
|
+
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
25
|
+
"group": 1,
|
|
26
|
+
"required": true,
|
|
27
|
+
"capability": "browser_bootstrap"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"source": "html_regex",
|
|
31
|
+
"name": "bl",
|
|
32
|
+
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
33
|
+
"group": 1,
|
|
34
|
+
"required": true,
|
|
35
|
+
"capability": "browser_bootstrap"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"requests": [
|
|
40
|
+
{
|
|
41
|
+
"method": "POST",
|
|
42
|
+
"url": "https://www.google.com/_/FlightsFrontendUi/data/batchexecute?rpcids=tDoGIe&source-path=%2Ftravel%2Fflights&f.sid=${state.f_sid}&bl=${state.bl}&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=608023&rt=c",
|
|
43
|
+
"headers": {
|
|
44
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
45
|
+
"X-Same-Domain": "1",
|
|
46
|
+
"Referer": "https://www.google.com/travel/flights",
|
|
47
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
48
|
+
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
|
|
49
|
+
},
|
|
50
|
+
"body": "f.req=%5B%5B%5B%22tDoGIe%22%2C%22%5Bnull%2C%5B%5B%5C%22${param.query}%5C%22%2C0%5D%5D%5D%22%2Cnull%2C%22generic%22%5D%5D%5D&",
|
|
51
|
+
"effect": "safe"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"parserModule": "./parser.ts",
|
|
55
|
+
"requestTransformModule": "./request-transform.ts",
|
|
56
|
+
"liveVerified": true
|
|
57
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by `imprint emit` — DO NOT EDIT BY HAND.
|
|
3
|
+
*
|
|
4
|
+
* Tool: search_flights
|
|
5
|
+
* Site: google-flights
|
|
6
|
+
* Intent: Search Google Flights for itineraries between two airports with dates, trip type, and filters (stops, airlines, price, times, duration, bags).
|
|
7
|
+
*
|
|
8
|
+
* To regenerate: imprint emit ~/.imprint/google-flights/search_flights/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": "search_flights",
|
|
21
|
+
"intent": {
|
|
22
|
+
"description": "Search Google Flights for itineraries between two airports with dates, trip type, and filters (stops, airlines, price, times, duration, bags).",
|
|
23
|
+
"userSaid": "searched for a round trip flight; searched for a one way flight; played with the stops and the airlines filters; added bags; played with the price and time filters; played with the duration filter; did a multi city flight query"
|
|
24
|
+
},
|
|
25
|
+
"parameters": [
|
|
26
|
+
{
|
|
27
|
+
"name": "origin",
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Origin airport/city IATA code, e.g. SJC",
|
|
30
|
+
"verified": false,
|
|
31
|
+
"verifyNote": "annotated"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "destination",
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Destination airport/city IATA code, 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 (omit/empty for one way)",
|
|
51
|
+
"default": "",
|
|
52
|
+
"verified": false,
|
|
53
|
+
"verifyNote": "annotated"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "trip_type",
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "round_trip, one_way, or multi_city",
|
|
59
|
+
"default": "round_trip",
|
|
60
|
+
"verified": false,
|
|
61
|
+
"verifyNote": "annotated"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "max_stops",
|
|
65
|
+
"type": "number",
|
|
66
|
+
"description": "Max stops: 0=nonstop, 1=<=1 stop, 2=<=2 stops, 3=any",
|
|
67
|
+
"default": 3,
|
|
68
|
+
"verified": false,
|
|
69
|
+
"verifyNote": "annotated"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "airlines",
|
|
73
|
+
"type": "string",
|
|
74
|
+
"description": "Comma-separated alliance (ONEWORLD/SKYTEAM/STAR_ALLIANCE) or 2-letter carrier codes (e.g. AS,WN). Empty = no filter",
|
|
75
|
+
"default": "",
|
|
76
|
+
"verified": false,
|
|
77
|
+
"verifyNote": "annotated"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "max_price",
|
|
81
|
+
"type": "number",
|
|
82
|
+
"description": "Maximum total price in USD (0 = no filter)",
|
|
83
|
+
"default": 0,
|
|
84
|
+
"verified": false,
|
|
85
|
+
"verifyNote": "annotated"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "outbound_times",
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "Outbound departure window in hours, e.g. '6-23' (empty = no filter)",
|
|
91
|
+
"default": "",
|
|
92
|
+
"verified": false,
|
|
93
|
+
"verifyNote": "annotated"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "return_times",
|
|
97
|
+
"type": "string",
|
|
98
|
+
"description": "Return departure window in hours, e.g. '6-23' (empty = no filter)",
|
|
99
|
+
"default": "",
|
|
100
|
+
"verified": false,
|
|
101
|
+
"verifyNote": "annotated"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "max_duration",
|
|
105
|
+
"type": "number",
|
|
106
|
+
"description": "Maximum total trip duration in minutes, e.g. 540 (0 = no filter)",
|
|
107
|
+
"default": 0,
|
|
108
|
+
"verified": false,
|
|
109
|
+
"verifyNote": "annotated"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "carry_on_bags",
|
|
113
|
+
"type": "number",
|
|
114
|
+
"description": "Number of carry-on bags to filter/price by (0 = no filter)",
|
|
115
|
+
"default": 0,
|
|
116
|
+
"verified": false,
|
|
117
|
+
"verifyNote": "annotated"
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"requests": [
|
|
121
|
+
{
|
|
122
|
+
"method": "POST",
|
|
123
|
+
"url": "https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults?f.sid=${state.f_sid}&bl=${state.bl}&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=1708023&rt=c",
|
|
124
|
+
"headers": {
|
|
125
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
126
|
+
"X-Same-Domain": "1",
|
|
127
|
+
"Referer": "https://www.google.com/travel/flights",
|
|
128
|
+
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
|
|
129
|
+
},
|
|
130
|
+
"body": "f.req=${param.origin}|${param.destination}|${param.departure_date}|${param.return_date}|${param.trip_type}|${param.max_stops}|${param.airlines}|${param.max_price}|${param.outbound_times}|${param.return_times}|${param.max_duration}|${param.carry_on_bags}&",
|
|
131
|
+
"effect": "safe"
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
"site": "google-flights",
|
|
135
|
+
"bootstrap": {
|
|
136
|
+
"url": "https://www.google.com/travel/flights",
|
|
137
|
+
"waitUntil": "domcontentloaded",
|
|
138
|
+
"timeoutMs": 30000,
|
|
139
|
+
"captures": [
|
|
140
|
+
{
|
|
141
|
+
"name": "f_sid",
|
|
142
|
+
"required": false,
|
|
143
|
+
"capability": "browser_bootstrap",
|
|
144
|
+
"source": "html_regex",
|
|
145
|
+
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
146
|
+
"group": 1
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "bl",
|
|
150
|
+
"required": false,
|
|
151
|
+
"capability": "browser_bootstrap",
|
|
152
|
+
"source": "html_regex",
|
|
153
|
+
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
154
|
+
"group": 1
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
"parserModule": "./parser.ts",
|
|
159
|
+
"requestTransformModule": "./request-transform.ts",
|
|
160
|
+
"liveVerified": true
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export interface SearchFlightsInput {
|
|
164
|
+
/** Origin airport/city IATA code, e.g. SJC */
|
|
165
|
+
origin: string;
|
|
166
|
+
/** Destination airport/city IATA code, e.g. SAN */
|
|
167
|
+
destination: string;
|
|
168
|
+
/** Outbound date in YYYY-MM-DD */
|
|
169
|
+
departure_date: string;
|
|
170
|
+
/** Return date in YYYY-MM-DD (omit/empty for one way) */
|
|
171
|
+
return_date?: string;
|
|
172
|
+
/** round_trip, one_way, or multi_city */
|
|
173
|
+
trip_type?: string;
|
|
174
|
+
/** Max stops: 0=nonstop, 1=<=1 stop, 2=<=2 stops, 3=any */
|
|
175
|
+
max_stops?: number;
|
|
176
|
+
/** Comma-separated alliance (ONEWORLD/SKYTEAM/STAR_ALLIANCE) or 2-letter carrier codes (e.g. AS,WN). Empty = no filter */
|
|
177
|
+
airlines?: string;
|
|
178
|
+
/** Maximum total price in USD (0 = no filter) */
|
|
179
|
+
max_price?: number;
|
|
180
|
+
/** Outbound departure window in hours, e.g. '6-23' (empty = no filter) */
|
|
181
|
+
outbound_times?: string;
|
|
182
|
+
/** Return departure window in hours, e.g. '6-23' (empty = no filter) */
|
|
183
|
+
return_times?: string;
|
|
184
|
+
/** Maximum total trip duration in minutes, e.g. 540 (0 = no filter) */
|
|
185
|
+
max_duration?: number;
|
|
186
|
+
/** Number of carry-on bags to filter/price by (0 = no filter) */
|
|
187
|
+
carry_on_bags?: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function searchFlights(
|
|
191
|
+
input: SearchFlightsInput,
|
|
192
|
+
opts: { credentials?: CredentialStore; fetchImpl?: typeof fetch; initialState?: Record<string, unknown> } = {},
|
|
193
|
+
): Promise<ToolResult> {
|
|
194
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
195
|
+
const params: Record<string, string | number | boolean> = {
|
|
196
|
+
return_date: input.return_date ?? "",
|
|
197
|
+
trip_type: input.trip_type ?? "round_trip",
|
|
198
|
+
max_stops: input.max_stops ?? 3,
|
|
199
|
+
airlines: input.airlines ?? "",
|
|
200
|
+
max_price: input.max_price ?? 0,
|
|
201
|
+
outbound_times: input.outbound_times ?? "",
|
|
202
|
+
return_times: input.return_times ?? "",
|
|
203
|
+
max_duration: input.max_duration ?? 0,
|
|
204
|
+
carry_on_bags: input.carry_on_bags ?? 0,
|
|
205
|
+
origin: input.origin,
|
|
206
|
+
destination: input.destination,
|
|
207
|
+
departure_date: input.departure_date,
|
|
208
|
+
};
|
|
209
|
+
return executeWorkflow({
|
|
210
|
+
workflow: WORKFLOW,
|
|
211
|
+
params,
|
|
212
|
+
credentials: opts.credentials,
|
|
213
|
+
fetchImpl: opts.fetchImpl,
|
|
214
|
+
initialState: opts.initialState,
|
|
215
|
+
workflowPath: join(__dirname, 'workflow.json'),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export { WORKFLOW };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Parser for Google Flights GetShoppingResults (batchexecute RPC).
|
|
2
|
+
// Decodes the envelope with the shared helper, then walks the deeply-nested
|
|
3
|
+
// positional payload to find itinerary records and normalize them.
|
|
4
|
+
import { decodeBatchExecute, extractRpcPayload } from '../_shared/batchexecute.ts';
|
|
5
|
+
|
|
6
|
+
interface Itinerary {
|
|
7
|
+
airlines: string[];
|
|
8
|
+
flightNumbers: string[];
|
|
9
|
+
origin: string;
|
|
10
|
+
destination: string;
|
|
11
|
+
departDate: string | null;
|
|
12
|
+
departTime: string | null;
|
|
13
|
+
arriveDate: string | null;
|
|
14
|
+
arriveTime: string | null;
|
|
15
|
+
durationMinutes: number | null;
|
|
16
|
+
stops: number;
|
|
17
|
+
priceUSD: number | null;
|
|
18
|
+
co2Grams: number | null;
|
|
19
|
+
flight_token: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const AIRPORT = /^[A-Z]{3}$/;
|
|
23
|
+
|
|
24
|
+
// A leg is [carrierCode, [carrierNames], [segments], originIATA, [departDate],
|
|
25
|
+
// [departTime], destIATA, [arriveDate], [arriveTime], durationMinutes, ...].
|
|
26
|
+
function isLeg(leg: unknown): leg is unknown[] {
|
|
27
|
+
if (!Array.isArray(leg)) return false;
|
|
28
|
+
return (
|
|
29
|
+
typeof leg[0] === 'string' &&
|
|
30
|
+
Array.isArray(leg[1]) &&
|
|
31
|
+
typeof leg[3] === 'string' &&
|
|
32
|
+
AIRPORT.test(leg[3] as string) &&
|
|
33
|
+
typeof leg[6] === 'string' &&
|
|
34
|
+
AIRPORT.test(leg[6] as string)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// node[0] is either a single leg or an array of legs; node[1] is
|
|
39
|
+
// [[null, <priceUSD>], "<base64 flight token>"].
|
|
40
|
+
function legsOf(node: unknown[]): unknown[][] {
|
|
41
|
+
const head = node[0];
|
|
42
|
+
if (isLeg(head)) return [head as unknown[]];
|
|
43
|
+
if (Array.isArray(head)) return head.filter(isLeg) as unknown[][];
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isItinerary(node: unknown): node is unknown[] {
|
|
48
|
+
if (!Array.isArray(node)) return false;
|
|
49
|
+
if (legsOf(node).length === 0) return false;
|
|
50
|
+
const priceTok = node[1];
|
|
51
|
+
if (!Array.isArray(priceTok)) return false;
|
|
52
|
+
const priceArr = priceTok[0];
|
|
53
|
+
const token = priceTok[1];
|
|
54
|
+
if (!Array.isArray(priceArr)) return false;
|
|
55
|
+
if (typeof token !== 'string' || token.length < 20) return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fmtTime(t: unknown): string | null {
|
|
60
|
+
if (!Array.isArray(t) || t.length === 0) return null;
|
|
61
|
+
const h = typeof t[0] === 'number' ? t[0] : 0;
|
|
62
|
+
const m = typeof t[1] === 'number' ? t[1] : 0;
|
|
63
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function fmtDate(d: unknown): string | null {
|
|
67
|
+
if (!Array.isArray(d) || d.length < 3) return null;
|
|
68
|
+
const [y, mo, day] = d as number[];
|
|
69
|
+
if (typeof y !== 'number') return null;
|
|
70
|
+
return `${y}-${String(mo).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function walk(node: unknown, found: unknown[][]): void {
|
|
74
|
+
if (!Array.isArray(node)) return;
|
|
75
|
+
if (isItinerary(node)) {
|
|
76
|
+
found.push(node);
|
|
77
|
+
return; // don't recurse into a matched itinerary
|
|
78
|
+
}
|
|
79
|
+
for (const child of node) walk(child, found);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalize(it: unknown[]): Itinerary {
|
|
83
|
+
const legs = legsOf(it);
|
|
84
|
+
const priceTok = it[1] as unknown[];
|
|
85
|
+
const priceArr = priceTok[0] as unknown[];
|
|
86
|
+
const token = priceTok[1] as string;
|
|
87
|
+
|
|
88
|
+
const airlines = new Set<string>();
|
|
89
|
+
const flightNumbers: string[] = [];
|
|
90
|
+
let durationMinutes = 0;
|
|
91
|
+
let stops = 0;
|
|
92
|
+
let co2 = 0;
|
|
93
|
+
|
|
94
|
+
for (const leg of legs) {
|
|
95
|
+
const names = leg[1];
|
|
96
|
+
if (Array.isArray(names)) {
|
|
97
|
+
for (const n of names) if (typeof n === 'string') airlines.add(n);
|
|
98
|
+
}
|
|
99
|
+
if (typeof leg[9] === 'number') durationMinutes += leg[9] as number;
|
|
100
|
+
const segs = leg[2];
|
|
101
|
+
if (Array.isArray(segs)) {
|
|
102
|
+
stops += Math.max(0, segs.length - 1);
|
|
103
|
+
for (const seg of segs) {
|
|
104
|
+
if (!Array.isArray(seg)) continue;
|
|
105
|
+
const fn = seg[22];
|
|
106
|
+
if (Array.isArray(fn) && typeof fn[0] === 'string' && fn[1] != null) {
|
|
107
|
+
flightNumbers.push(`${fn[0]}${fn[1]}`);
|
|
108
|
+
if (typeof fn[3] === 'string') airlines.add(fn[3]);
|
|
109
|
+
}
|
|
110
|
+
// best-effort CO2 (grams): large numeric near the end of the segment.
|
|
111
|
+
const cand = seg[seg.length - 2];
|
|
112
|
+
if (typeof cand === 'number' && cand > 1000 && cand < 10_000_000) co2 += cand;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const firstLeg = legs[0] ?? [];
|
|
118
|
+
const lastLeg = legs[legs.length - 1] ?? [];
|
|
119
|
+
const price = priceArr.find((v) => typeof v === 'number') as number | undefined;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
airlines: [...airlines],
|
|
123
|
+
flightNumbers,
|
|
124
|
+
origin: typeof firstLeg[3] === 'string' ? (firstLeg[3] as string) : '',
|
|
125
|
+
destination: typeof lastLeg[6] === 'string' ? (lastLeg[6] as string) : '',
|
|
126
|
+
departDate: fmtDate(firstLeg[4]),
|
|
127
|
+
departTime: fmtTime(firstLeg[5]),
|
|
128
|
+
arriveDate: fmtDate(lastLeg[7]),
|
|
129
|
+
arriveTime: fmtTime(lastLeg[8]),
|
|
130
|
+
durationMinutes: durationMinutes || null,
|
|
131
|
+
stops,
|
|
132
|
+
priceUSD: price ?? null,
|
|
133
|
+
co2Grams: co2 || null,
|
|
134
|
+
flight_token: token,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function extract(
|
|
139
|
+
rawResponse: unknown,
|
|
140
|
+
_context?: { params: Record<string, string | number | boolean>; responses: unknown[] },
|
|
141
|
+
): unknown {
|
|
142
|
+
let payload: unknown;
|
|
143
|
+
if (typeof rawResponse === 'string') {
|
|
144
|
+
payload =
|
|
145
|
+
extractRpcPayload(rawResponse, 'GetShoppingResults') ?? extractRpcPayload(rawResponse);
|
|
146
|
+
if (payload == null) {
|
|
147
|
+
const frames = decodeBatchExecute(rawResponse);
|
|
148
|
+
payload = frames[0]?.payload;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
payload = rawResponse;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const found: unknown[][] = [];
|
|
155
|
+
if (payload != null) walk(payload, found);
|
|
156
|
+
|
|
157
|
+
const byToken = new Map<string, Itinerary>();
|
|
158
|
+
for (const it of found) {
|
|
159
|
+
const norm = normalize(it);
|
|
160
|
+
if (!norm.flight_token) continue;
|
|
161
|
+
if (!byToken.has(norm.flight_token)) byToken.set(norm.flight_token, norm);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const itineraries = [...byToken.values()];
|
|
165
|
+
return {
|
|
166
|
+
count: itineraries.length,
|
|
167
|
+
itineraries,
|
|
168
|
+
};
|
|
169
|
+
}
|