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
package/src/imprint/types.ts
CHANGED
|
@@ -113,6 +113,27 @@ const WorkflowParameterSchema = z.object({
|
|
|
113
113
|
description: z.string(),
|
|
114
114
|
/** Optional with this default if set. */
|
|
115
115
|
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
116
|
+
/** Whether a `param:<name>` integration test verified this parameter's effect
|
|
117
|
+
* against live data at compile time. `false` means it ships unverified (the
|
|
118
|
+
* live differential was waived by anti-bot, or explicitly annotated
|
|
119
|
+
* exposed-but-not-verified) and is exercised at runtime via the backend
|
|
120
|
+
* ladder. Undefined on tools compiled before this gate (treated as verified
|
|
121
|
+
* for back-compat). Not surfaced in the user-facing MCP schema. */
|
|
122
|
+
verified: z.boolean().optional(),
|
|
123
|
+
/** Why the parameter is unverified (e.g. `waived-bot`, `waived-infra`,
|
|
124
|
+
* `annotated`, `waived-chain`). Undefined when `verified` is true/undefined. */
|
|
125
|
+
verifyNote: z.string().optional(),
|
|
126
|
+
/** Set when this parameter is an opaque token/id minted by a sibling tool — the
|
|
127
|
+
* consumer takes a value produced by `tool`'s `field` output. Surfaced in the
|
|
128
|
+
* MCP param description so the orchestrating LLM calls `tool` first and reuses
|
|
129
|
+
* the value; used by the compile gate to require a chained verification test and
|
|
130
|
+
* by `imprint audit` to chain producer→consumer instead of fabricating a token. */
|
|
131
|
+
sourcedFrom: z
|
|
132
|
+
.object({
|
|
133
|
+
tool: z.string(),
|
|
134
|
+
field: z.string(),
|
|
135
|
+
})
|
|
136
|
+
.optional(),
|
|
116
137
|
});
|
|
117
138
|
export type WorkflowParameter = z.infer<typeof WorkflowParameterSchema>;
|
|
118
139
|
|
|
@@ -188,6 +209,18 @@ const BootstrapCaptureSchema = z.discriminatedUnion('source', [
|
|
|
188
209
|
selector: z.string(),
|
|
189
210
|
timeoutMs: z.number().int().positive().optional(),
|
|
190
211
|
}),
|
|
212
|
+
/** Read the value of a header from the bootstrap GET's own HTTP response.
|
|
213
|
+
* Use this when the token (CSRF, anti-replay, page nonce, etc.) is
|
|
214
|
+
* returned in a response header — not embedded in the HTML body — which
|
|
215
|
+
* no `html_regex` or `dom_*` capture can ever match. Mirrors the shape
|
|
216
|
+
* of `RequestCaptureSchema.source = 'response_header'` so the agent
|
|
217
|
+
* documents one consistent rule across request- and bootstrap-scoped
|
|
218
|
+
* captures. */
|
|
219
|
+
CaptureCommonSchema.extend({
|
|
220
|
+
source: z.literal('response_header'),
|
|
221
|
+
header: z.string(),
|
|
222
|
+
mode: z.enum(['first', 'last', 'all']).optional().default('last'),
|
|
223
|
+
}),
|
|
191
224
|
]);
|
|
192
225
|
export type BootstrapCapture = z.infer<typeof BootstrapCaptureSchema>;
|
|
193
226
|
|
|
@@ -241,6 +274,27 @@ export const WorkflowSchema = z.object({
|
|
|
241
274
|
* The optional 4th arg `params` carries the resolved workflow parameters
|
|
242
275
|
* so the transform can construct request bodies programmatically. */
|
|
243
276
|
requestTransformModule: z.string().optional(),
|
|
277
|
+
/** Did this tool's integration test produce live data at compile time?
|
|
278
|
+
*
|
|
279
|
+
* - `liveVerified: true` (default when present) — the integration test
|
|
280
|
+
* passed at one of the API/stealth-fetch rungs of the ladder.
|
|
281
|
+
* - `liveVerified: false` — the test failed and was waived (anti-bot
|
|
282
|
+
* block or transient infra), so the tool shipped without a passing
|
|
283
|
+
* live call. Downstream consumers (audit gate, teach summary) treat
|
|
284
|
+
* this as a flying-blind signal — the runtime playbook fallback is
|
|
285
|
+
* the only remaining path, and it is a last-ditch one. `liveVerified`
|
|
286
|
+
* is absent on tools predating this field; absent is treated as
|
|
287
|
+
* "unknown" by readers, which is more honest than defaulting true. */
|
|
288
|
+
liveVerified: z.boolean().optional(),
|
|
289
|
+
/** Structured reason a waiver was applied. Only present when
|
|
290
|
+
* `liveVerified === false`. */
|
|
291
|
+
liveVerifiedWaiver: z
|
|
292
|
+
.object({
|
|
293
|
+
kind: z.enum(['waived-bot', 'waived-infra']),
|
|
294
|
+
firstError: z.string(),
|
|
295
|
+
exhaustedBackends: z.array(z.string()),
|
|
296
|
+
})
|
|
297
|
+
.optional(),
|
|
244
298
|
});
|
|
245
299
|
export type Workflow = z.infer<typeof WorkflowSchema>;
|
|
246
300
|
|
|
@@ -299,12 +353,16 @@ const NotifyWhenSchema = z.discriminatedUnion('type', [
|
|
|
299
353
|
export type NotifyWhen = z.infer<typeof NotifyWhenSchema>;
|
|
300
354
|
|
|
301
355
|
/** fetch (plain API replay) → gated fetch-bootstrap (browser state init +
|
|
302
|
-
* API replay) →
|
|
303
|
-
*
|
|
304
|
-
*
|
|
356
|
+
* API replay) → cdp-replay (API requests run IN a live trusted Chrome page so
|
|
357
|
+
* a protected POST's invalidated _abck is auto-re-validated by the page's bmak
|
|
358
|
+
* sensor between calls — the only way to sustain multiple sensitive .act POSTs)
|
|
359
|
+
* → stealth-fetch (bot-defense state + API replay) → playbook (full DOM walk).
|
|
360
|
+
* 'auto' only inserts fetch-bootstrap / cdp-replay for declared or satisfiable
|
|
361
|
+
* browser-minted state. */
|
|
305
362
|
const ReplayBackendSchema = z.enum([
|
|
306
363
|
'fetch',
|
|
307
364
|
'fetch-bootstrap',
|
|
365
|
+
'cdp-replay',
|
|
308
366
|
'stealth-fetch',
|
|
309
367
|
'playbook',
|
|
310
368
|
'auto',
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GENERATED by `imprint emit` — DO NOT EDIT BY HAND.
|
|
3
|
-
*
|
|
4
|
-
* Tool: search_google_flights
|
|
5
|
-
* Site: google-flights
|
|
6
|
-
* Intent: Search Google Flights for round-trip flights between two airports on specific dates.
|
|
7
|
-
*
|
|
8
|
-
* To regenerate: imprint emit ~/.imprint/google-flights/search_google_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_google_flights",
|
|
21
|
-
"intent": {
|
|
22
|
-
"description": "Search Google Flights for round-trip flights between two airports on specific dates.",
|
|
23
|
-
"userSaid": "i searched for a sample flight the default mode is economy (include basic) now i changed to economy (exclude basic) now i searched for premium economy now i searched for business now i searched for first class now im going to start messing with all the filters available i played with all the filters now i clicked one of the departure flights, and now it's showing me a list of the return flights i selected the return flight, and now it's showing me the booking options"
|
|
24
|
-
},
|
|
25
|
-
"parameters": [
|
|
26
|
-
{
|
|
27
|
-
"name": "origin_airport",
|
|
28
|
-
"type": "string",
|
|
29
|
-
"description": "Origin airport IATA code.",
|
|
30
|
-
"default": "SJC"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"name": "destination_airport",
|
|
34
|
-
"type": "string",
|
|
35
|
-
"description": "Destination airport IATA code.",
|
|
36
|
-
"default": "SAN"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"name": "depart_date",
|
|
40
|
-
"type": "string",
|
|
41
|
-
"description": "Outbound departure date in YYYY-MM-DD format.",
|
|
42
|
-
"default": "2026-07-10"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"name": "return_date",
|
|
46
|
-
"type": "string",
|
|
47
|
-
"description": "Return departure date in YYYY-MM-DD format.",
|
|
48
|
-
"default": "2026-07-16"
|
|
49
|
-
}
|
|
50
|
-
],
|
|
51
|
-
"requests": [
|
|
52
|
-
{
|
|
53
|
-
"method": "POST",
|
|
54
|
-
"url": "https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults?f.sid=-1290970187393889182&bl=boq_travel-frontend-flights-ui_20260506.02_p0&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=4008662&rt=c",
|
|
55
|
-
"headers": {
|
|
56
|
-
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
57
|
-
"Referer": "https://www.google.com/travel/flights",
|
|
58
|
-
"X-Same-Domain": "1",
|
|
59
|
-
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
|
|
60
|
-
},
|
|
61
|
-
"body": "f.req=%5Bnull%2C%22%5B%5B%5D%2C%5Bnull%2Cnull%2C1%2Cnull%2C%5B%5D%2C1%2C%5B1%2C0%2C0%2C0%5D%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2C%5B%5B%5B%5B%5B%5C%22${param.origin_airport}%5C%22%2C0%5D%5D%5D%2C%5B%5B%5B%5C%22${param.destination_airport}%5C%22%2C0%5D%5D%5D%2Cnull%2C0%2Cnull%2Cnull%2C%5C%22${param.depart_date}%5C%22%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2C690%2Cnull%2C3%5D%2C%5B%5B%5B%5B%5C%22${param.destination_airport}%5C%22%2C0%5D%5D%5D%2C%5B%5B%5B%5C%22${param.origin_airport}%5C%22%2C0%5D%5D%5D%2Cnull%2C0%2Cnull%2Cnull%2C%5C%22${param.return_date}%5C%22%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2C690%2Cnull%2C3%5D%5D%2Cnull%2Cnull%2Cnull%2C1%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2Cnull%2C0%5D%2C0%2C0%2C0%2C1%5D%22%5D&"
|
|
62
|
-
}
|
|
63
|
-
],
|
|
64
|
-
"site": "google-flights",
|
|
65
|
-
"parserModule": "./parser.ts"
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export interface SearchGoogleFlightsInput {
|
|
69
|
-
/** Origin airport IATA code. */
|
|
70
|
-
origin_airport?: string;
|
|
71
|
-
/** Destination airport IATA code. */
|
|
72
|
-
destination_airport?: string;
|
|
73
|
-
/** Outbound departure date in YYYY-MM-DD format. */
|
|
74
|
-
depart_date?: string;
|
|
75
|
-
/** Return departure date in YYYY-MM-DD format. */
|
|
76
|
-
return_date?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function searchGoogleFlights(
|
|
80
|
-
input: SearchGoogleFlightsInput,
|
|
81
|
-
opts: { credentials?: CredentialStore; fetchImpl?: typeof fetch; initialState?: Record<string, unknown> } = {},
|
|
82
|
-
): Promise<ToolResult> {
|
|
83
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
84
|
-
const params: Record<string, string | number | boolean> = {
|
|
85
|
-
origin_airport: input.origin_airport ?? "SJC",
|
|
86
|
-
destination_airport: input.destination_airport ?? "SAN",
|
|
87
|
-
depart_date: input.depart_date ?? "2026-07-10",
|
|
88
|
-
return_date: input.return_date ?? "2026-07-16",
|
|
89
|
-
|
|
90
|
-
};
|
|
91
|
-
return executeWorkflow({
|
|
92
|
-
workflow: WORKFLOW,
|
|
93
|
-
params,
|
|
94
|
-
credentials: opts.credentials,
|
|
95
|
-
fetchImpl: opts.fetchImpl,
|
|
96
|
-
initialState: opts.initialState,
|
|
97
|
-
workflowPath: join(__dirname, 'workflow.json'),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export { WORKFLOW };
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe } from 'bun:test';
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { extract } from './parser';
|
|
5
|
-
|
|
6
|
-
const fixtureResponse = readFileSync(
|
|
7
|
-
join(
|
|
8
|
-
import.meta.dir,
|
|
9
|
-
'../../../test/fixtures/examples/google-flights/search_google_flights/fixture_response.txt',
|
|
10
|
-
),
|
|
11
|
-
'utf-8'
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
describe('Google Flights parser', () => {
|
|
15
|
-
test('extract returns an object with flights array', () => {
|
|
16
|
-
const result = extract(fixtureResponse) as any;
|
|
17
|
-
expect(result).toBeDefined();
|
|
18
|
-
expect(result.flights).toBeDefined();
|
|
19
|
-
expect(Array.isArray(result.flights)).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('returns multiple flights (SFO to Tokyo search)', () => {
|
|
23
|
-
const result = extract(fixtureResponse) as any;
|
|
24
|
-
expect(result.flights.length).toBeGreaterThan(0);
|
|
25
|
-
expect(result.totalCount).toBeGreaterThan(0);
|
|
26
|
-
expect(result.flights.length).toBe(result.totalCount);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('flights originate from SFO (San Francisco)', () => {
|
|
30
|
-
const result = extract(fixtureResponse) as any;
|
|
31
|
-
const sfFlights = result.flights.filter((f: any) => f.origin === 'SFO');
|
|
32
|
-
expect(sfFlights.length).toBeGreaterThan(0);
|
|
33
|
-
// All flights should be from SFO
|
|
34
|
-
expect(result.flights.every((f: any) => f.origin === 'SFO')).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('flights go to Tokyo airports (NRT or HND)', () => {
|
|
38
|
-
const result = extract(fixtureResponse) as any;
|
|
39
|
-
const tokyoFlights = result.flights.filter(
|
|
40
|
-
(f: any) => f.destination === 'NRT' || f.destination === 'HND'
|
|
41
|
-
);
|
|
42
|
-
expect(tokyoFlights.length).toBeGreaterThan(0);
|
|
43
|
-
// All flights should go to Tokyo area airports
|
|
44
|
-
expect(result.flights.every((f: any) => f.destination === 'NRT' || f.destination === 'HND')).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('flights have valid prices in USD', () => {
|
|
48
|
-
const result = extract(fixtureResponse) as any;
|
|
49
|
-
const flightsWithPrice = result.flights.filter((f: any) => f.priceUsd !== null);
|
|
50
|
-
expect(flightsWithPrice.length).toBeGreaterThan(0);
|
|
51
|
-
// Prices should be reasonable for SFO-TYO (between $500 and $10000)
|
|
52
|
-
for (const flight of flightsWithPrice) {
|
|
53
|
-
expect(flight.priceUsd).toBeGreaterThan(500);
|
|
54
|
-
expect(flight.priceUsd).toBeLessThan(10000);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('cheapest flight is ZIPAIR Tokyo at $1245', () => {
|
|
59
|
-
const result = extract(fixtureResponse) as any;
|
|
60
|
-
// Flights are sorted by price ascending
|
|
61
|
-
const cheapest = result.flights[0];
|
|
62
|
-
expect(cheapest.priceUsd).toBe(1245);
|
|
63
|
-
expect(cheapest.airlineCode).toBe('ZG');
|
|
64
|
-
expect(cheapest.airlineName).toBe('ZIPAIR Tokyo');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('flights have valid departure dates in 2026', () => {
|
|
68
|
-
const result = extract(fixtureResponse) as any;
|
|
69
|
-
for (const flight of result.flights) {
|
|
70
|
-
expect(flight.departureDate).toMatch(/^2026-/);
|
|
71
|
-
expect(flight.departureDate).toBeTruthy();
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('flights have valid departure and arrival times', () => {
|
|
76
|
-
const result = extract(fixtureResponse) as any;
|
|
77
|
-
for (const flight of result.flights) {
|
|
78
|
-
expect(flight.departureTime).toMatch(/^\d{2}:\d{2}$/);
|
|
79
|
-
expect(flight.arrivalTime).toMatch(/^\d{2}:\d{2}$/);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('flights have positive duration in minutes', () => {
|
|
84
|
-
const result = extract(fixtureResponse) as any;
|
|
85
|
-
for (const flight of result.flights) {
|
|
86
|
-
// SFO to Tokyo is roughly 10-28 hours
|
|
87
|
-
expect(flight.durationMinutes).toBeGreaterThan(600);
|
|
88
|
-
expect(flight.durationMinutes).toBeLessThan(2000);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('flights have segment details', () => {
|
|
93
|
-
const result = extract(fixtureResponse) as any;
|
|
94
|
-
for (const flight of result.flights) {
|
|
95
|
-
expect(Array.isArray(flight.segments)).toBe(true);
|
|
96
|
-
expect(flight.segments.length).toBeGreaterThan(0);
|
|
97
|
-
const seg = flight.segments[0];
|
|
98
|
-
expect(seg.origin).toBe('SFO');
|
|
99
|
-
expect(seg.flightNumber).toBeTruthy();
|
|
100
|
-
expect(seg.aircraft).toBeTruthy();
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('includes known airlines: United, ANA, JAL', () => {
|
|
105
|
-
const result = extract(fixtureResponse) as any;
|
|
106
|
-
const airlineCodes = result.flights.map((f: any) => f.airlineCode);
|
|
107
|
-
expect(airlineCodes).toContain('UA'); // United
|
|
108
|
-
expect(airlineCodes).toContain('NH'); // ANA
|
|
109
|
-
expect(airlineCodes).toContain('JL'); // JAL
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('United flight to NRT costs $1809', () => {
|
|
113
|
-
const result = extract(fixtureResponse) as any;
|
|
114
|
-
const unitedNRT = result.flights.find(
|
|
115
|
-
(f: any) => f.airlineCode === 'UA' && f.destination === 'NRT'
|
|
116
|
-
);
|
|
117
|
-
expect(unitedNRT).toBeDefined();
|
|
118
|
-
expect(unitedNRT.priceUsd).toBe(1809);
|
|
119
|
-
expect(unitedNRT.durationMinutes).toBe(635);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('flights have booking tokens', () => {
|
|
123
|
-
const result = extract(fixtureResponse) as any;
|
|
124
|
-
const flightsWithToken = result.flights.filter((f: any) => f.bookingToken !== null);
|
|
125
|
-
expect(flightsWithToken.length).toBeGreaterThan(0);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('stops field is correctly parsed (0 for nonstop, 1+ for connecting)', () => {
|
|
129
|
-
const result = extract(fixtureResponse) as any;
|
|
130
|
-
// All flights in this response have at least 1 segment
|
|
131
|
-
for (const flight of result.flights) {
|
|
132
|
-
expect(flight.stopCount).toBeGreaterThanOrEqual(0);
|
|
133
|
-
}
|
|
134
|
-
// Asiana Airlines goes via ICN (1 stop)
|
|
135
|
-
const asiana = result.flights.find((f: any) => f.airlineCode === 'OZ');
|
|
136
|
-
if (asiana) {
|
|
137
|
-
expect(asiana.stopCount).toBeGreaterThan(0);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
});
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
interface Segment {
|
|
2
|
-
origin: string;
|
|
3
|
-
originName: string;
|
|
4
|
-
destination: string;
|
|
5
|
-
destinationName: string;
|
|
6
|
-
departureDate: string;
|
|
7
|
-
departureTime: string;
|
|
8
|
-
arrivalDate: string;
|
|
9
|
-
arrivalTime: string;
|
|
10
|
-
durationMinutes: number;
|
|
11
|
-
airlineCode: string;
|
|
12
|
-
airlineName: string;
|
|
13
|
-
flightNumber: string;
|
|
14
|
-
aircraft: string;
|
|
15
|
-
cabinClass: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface Flight {
|
|
19
|
-
airlineCode: string;
|
|
20
|
-
airlineName: string;
|
|
21
|
-
origin: string;
|
|
22
|
-
destination: string;
|
|
23
|
-
departureDate: string;
|
|
24
|
-
departureTime: string;
|
|
25
|
-
arrivalDate: string;
|
|
26
|
-
arrivalTime: string;
|
|
27
|
-
durationMinutes: number;
|
|
28
|
-
stopCount: number;
|
|
29
|
-
priceUsd: number | null;
|
|
30
|
-
bookingToken: string | null;
|
|
31
|
-
flightNumbers: string[];
|
|
32
|
-
segments: Segment[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function formatDate(parts: unknown): string {
|
|
36
|
-
if (!Array.isArray(parts) || parts.length < 3) return '';
|
|
37
|
-
const [year, month, day] = parts as number[];
|
|
38
|
-
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function formatTime(parts: unknown): string {
|
|
42
|
-
if (!Array.isArray(parts) || parts.length === 0) return '';
|
|
43
|
-
const [hour = 0, minute = 0] = parts as number[];
|
|
44
|
-
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function cabinClassLabel(code: unknown): string {
|
|
48
|
-
switch (code) {
|
|
49
|
-
case 1:
|
|
50
|
-
return 'economy';
|
|
51
|
-
case 2:
|
|
52
|
-
return 'premium_economy';
|
|
53
|
-
case 3:
|
|
54
|
-
return 'business';
|
|
55
|
-
case 4:
|
|
56
|
-
return 'first';
|
|
57
|
-
default:
|
|
58
|
-
return 'unknown';
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function parseEnvelope(rawResponse: string): unknown[] | null {
|
|
63
|
-
const jsonLine = rawResponse
|
|
64
|
-
.split('\n')
|
|
65
|
-
.find((line) => line.startsWith('[[') || line.startsWith('[["'));
|
|
66
|
-
|
|
67
|
-
if (!jsonLine) return null;
|
|
68
|
-
|
|
69
|
-
let outer: unknown[];
|
|
70
|
-
try {
|
|
71
|
-
outer = JSON.parse(jsonLine) as unknown[];
|
|
72
|
-
} catch {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const innerString = (outer as unknown[][])?.[0]?.[2];
|
|
77
|
-
if (typeof innerString !== 'string') return null;
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
return JSON.parse(innerString) as unknown[];
|
|
81
|
-
} catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function parseSegment(rawSegment: unknown): Segment | null {
|
|
87
|
-
if (!Array.isArray(rawSegment)) return null;
|
|
88
|
-
|
|
89
|
-
const flightInfo = rawSegment[22];
|
|
90
|
-
return {
|
|
91
|
-
origin: typeof rawSegment[3] === 'string' ? rawSegment[3] : '',
|
|
92
|
-
originName: typeof rawSegment[4] === 'string' ? rawSegment[4] : '',
|
|
93
|
-
destination: typeof rawSegment[6] === 'string' ? rawSegment[6] : '',
|
|
94
|
-
destinationName: typeof rawSegment[5] === 'string' ? rawSegment[5] : '',
|
|
95
|
-
departureDate: formatDate(rawSegment[20]),
|
|
96
|
-
departureTime: formatTime(rawSegment[8]),
|
|
97
|
-
arrivalDate: formatDate(rawSegment[21]),
|
|
98
|
-
arrivalTime: formatTime(rawSegment[10]),
|
|
99
|
-
durationMinutes: typeof rawSegment[11] === 'number' ? rawSegment[11] : 0,
|
|
100
|
-
airlineCode:
|
|
101
|
-
Array.isArray(flightInfo) && typeof flightInfo[0] === 'string' ? flightInfo[0] : '',
|
|
102
|
-
airlineName:
|
|
103
|
-
Array.isArray(flightInfo) && typeof flightInfo[3] === 'string' ? flightInfo[3] : '',
|
|
104
|
-
flightNumber:
|
|
105
|
-
Array.isArray(flightInfo) &&
|
|
106
|
-
typeof flightInfo[0] === 'string' &&
|
|
107
|
-
typeof flightInfo[1] === 'string'
|
|
108
|
-
? `${flightInfo[0]}${flightInfo[1]}`
|
|
109
|
-
: '',
|
|
110
|
-
aircraft: typeof rawSegment[17] === 'string' ? rawSegment[17] : '',
|
|
111
|
-
cabinClass: cabinClassLabel(rawSegment[13]),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function parseFlight(rawOption: unknown): Flight | null {
|
|
116
|
-
if (!Array.isArray(rawOption) || rawOption.length < 2) return null;
|
|
117
|
-
|
|
118
|
-
const details = rawOption[0];
|
|
119
|
-
const priceInfo = rawOption[1];
|
|
120
|
-
if (!Array.isArray(details) || !Array.isArray(details[2])) return null;
|
|
121
|
-
|
|
122
|
-
const segments = details[2]
|
|
123
|
-
.map((segment) => parseSegment(segment))
|
|
124
|
-
.filter((segment): segment is Segment => segment !== null);
|
|
125
|
-
|
|
126
|
-
const firstPrice = Array.isArray(priceInfo) && Array.isArray(priceInfo[0]) ? priceInfo[0] : null;
|
|
127
|
-
const priceUsd = firstPrice && typeof firstPrice[1] === 'number' ? firstPrice[1] : null;
|
|
128
|
-
const bookingToken = Array.isArray(priceInfo) && typeof priceInfo[1] === 'string' ? priceInfo[1] : null;
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
airlineCode: typeof details[0] === 'string' ? details[0] : '',
|
|
132
|
-
airlineName:
|
|
133
|
-
Array.isArray(details[1]) && typeof details[1][0] === 'string' ? details[1][0] : '',
|
|
134
|
-
origin: typeof details[3] === 'string' ? details[3] : '',
|
|
135
|
-
destination: typeof details[6] === 'string' ? details[6] : '',
|
|
136
|
-
departureDate: formatDate(details[4]),
|
|
137
|
-
departureTime: formatTime(details[5]),
|
|
138
|
-
arrivalDate: formatDate(details[7]),
|
|
139
|
-
arrivalTime: formatTime(details[8]),
|
|
140
|
-
durationMinutes: typeof details[9] === 'number' ? details[9] : 0,
|
|
141
|
-
stopCount: Math.max(segments.length - 1, 0),
|
|
142
|
-
priceUsd,
|
|
143
|
-
bookingToken,
|
|
144
|
-
flightNumbers: segments.map((segment) => segment.flightNumber).filter(Boolean),
|
|
145
|
-
segments,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function isFlightSection(section: unknown): section is [unknown[], ...unknown[]] {
|
|
150
|
-
return (
|
|
151
|
-
Array.isArray(section) &&
|
|
152
|
-
Array.isArray(section[0]) &&
|
|
153
|
-
section[0].length > 0 &&
|
|
154
|
-
Array.isArray(section[0][0]) &&
|
|
155
|
-
Array.isArray(section[0][0][0])
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function extract(rawResponse: unknown): unknown {
|
|
160
|
-
if (typeof rawResponse !== 'string') {
|
|
161
|
-
return { flights: [], totalCount: 0, error: 'Expected raw response string.' };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const envelope = parseEnvelope(rawResponse);
|
|
165
|
-
if (!envelope) {
|
|
166
|
-
return { flights: [], totalCount: 0, error: 'Unable to parse Google Flights response.' };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const flights: Flight[] = [];
|
|
170
|
-
for (const section of envelope) {
|
|
171
|
-
if (!isFlightSection(section)) continue;
|
|
172
|
-
for (const rawOption of section[0]) {
|
|
173
|
-
const flight = parseFlight(rawOption);
|
|
174
|
-
if (flight) flights.push(flight);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
flights.sort((a, b) => {
|
|
179
|
-
if (a.priceUsd == null && b.priceUsd == null) return 0;
|
|
180
|
-
if (a.priceUsd == null) return 1;
|
|
181
|
-
if (b.priceUsd == null) return -1;
|
|
182
|
-
return a.priceUsd - b.priceUsd;
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
flights,
|
|
187
|
-
totalCount: flights.length,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
toolName: search_google_flights
|
|
2
|
-
summary: Search Google Flights for round-trip flights between two airports on specific dates.
|
|
3
|
-
parameters:
|
|
4
|
-
- name: origin_airport
|
|
5
|
-
type: string
|
|
6
|
-
description: Origin airport or city code, e.g. SJC
|
|
7
|
-
- name: destination_airport
|
|
8
|
-
type: string
|
|
9
|
-
description: Destination airport or city code, e.g. SAN
|
|
10
|
-
- name: depart_date
|
|
11
|
-
type: string
|
|
12
|
-
description: Departure date in YYYY-MM-DD format
|
|
13
|
-
- name: return_date
|
|
14
|
-
type: string
|
|
15
|
-
description: Return date in YYYY-MM-DD format
|
|
16
|
-
steps:
|
|
17
|
-
- action: navigate
|
|
18
|
-
url: https://www.google.com/travel/flights
|
|
19
|
-
wait_for: networkidle
|
|
20
|
-
- action: type
|
|
21
|
-
locators:
|
|
22
|
-
- by: role
|
|
23
|
-
value: textbox
|
|
24
|
-
name: Where from?
|
|
25
|
-
- by: aria_label
|
|
26
|
-
value: Where from?
|
|
27
|
-
- by: css
|
|
28
|
-
value: input[aria-label="Where from?"]
|
|
29
|
-
value: ${origin_airport}
|
|
30
|
-
wait_for:
|
|
31
|
-
sleep_ms: 500
|
|
32
|
-
- action: click
|
|
33
|
-
locators:
|
|
34
|
-
- by: role
|
|
35
|
-
value: option
|
|
36
|
-
name: ${origin_airport}
|
|
37
|
-
- by: aria_label
|
|
38
|
-
value_pattern: ${origin_airport}
|
|
39
|
-
- by: text
|
|
40
|
-
value_pattern: ${origin_airport}
|
|
41
|
-
- by: css
|
|
42
|
-
value: ul li
|
|
43
|
-
wait_for: visible
|
|
44
|
-
- action: type
|
|
45
|
-
locators:
|
|
46
|
-
- by: role
|
|
47
|
-
value: textbox
|
|
48
|
-
name: Where to?
|
|
49
|
-
- by: aria_label
|
|
50
|
-
value: "Where to? "
|
|
51
|
-
- by: css
|
|
52
|
-
value: input[aria-label="Where to? "]
|
|
53
|
-
value: ${destination_airport}
|
|
54
|
-
wait_for:
|
|
55
|
-
sleep_ms: 500
|
|
56
|
-
- action: click
|
|
57
|
-
locators:
|
|
58
|
-
- by: role
|
|
59
|
-
value: option
|
|
60
|
-
name: ${destination_airport}
|
|
61
|
-
- by: aria_label
|
|
62
|
-
value_pattern: ${destination_airport}
|
|
63
|
-
- by: text
|
|
64
|
-
value_pattern: ${destination_airport}
|
|
65
|
-
- by: css
|
|
66
|
-
value: ul li
|
|
67
|
-
wait_for: visible
|
|
68
|
-
- action: type
|
|
69
|
-
locators:
|
|
70
|
-
- by: role
|
|
71
|
-
value: textbox
|
|
72
|
-
name: Departure
|
|
73
|
-
- by: aria_label
|
|
74
|
-
value: Departure
|
|
75
|
-
- by: css
|
|
76
|
-
value: input[aria-label="Departure"]
|
|
77
|
-
value: ${depart_date}
|
|
78
|
-
wait_for:
|
|
79
|
-
sleep_ms: 300
|
|
80
|
-
- action: type
|
|
81
|
-
locators:
|
|
82
|
-
- by: role
|
|
83
|
-
value: textbox
|
|
84
|
-
name: Return
|
|
85
|
-
- by: aria_label
|
|
86
|
-
value: Return
|
|
87
|
-
- by: css
|
|
88
|
-
value: input[aria-label="Return"]
|
|
89
|
-
value: ${return_date}
|
|
90
|
-
wait_for:
|
|
91
|
-
sleep_ms: 300
|
|
92
|
-
- action: click
|
|
93
|
-
locators:
|
|
94
|
-
- by: role
|
|
95
|
-
value: button
|
|
96
|
-
name: Search
|
|
97
|
-
- by: aria_label
|
|
98
|
-
value_pattern: Search
|
|
99
|
-
- by: css
|
|
100
|
-
value: button
|
|
101
|
-
wait_for:
|
|
102
|
-
xhr: /FlightsFrontendService/GetShoppingResults
|
|
103
|
-
- action: click
|
|
104
|
-
locators:
|
|
105
|
-
- by: aria_label
|
|
106
|
-
value: Nonstop flight.
|
|
107
|
-
- by: text
|
|
108
|
-
value: Nonstop
|
|
109
|
-
- by: css
|
|
110
|
-
value: div.OgQvJf.nKlB3b
|
|
111
|
-
wait_for:
|
|
112
|
-
xhr: /FlightsFrontendService/GetShoppingResults
|
|
113
|
-
- action: click
|
|
114
|
-
locators:
|
|
115
|
-
- by: aria_label
|
|
116
|
-
value: Nonstop flight.
|
|
117
|
-
- by: text
|
|
118
|
-
value: Nonstop
|
|
119
|
-
- by: css
|
|
120
|
-
value: div.OgQvJf.nKlB3b
|
|
121
|
-
wait_for:
|
|
122
|
-
xhr: /FlightsFrontendService/GetBookingResults
|
|
123
|
-
result:
|
|
124
|
-
source: dom
|
|
125
|
-
locators:
|
|
126
|
-
- by: css
|
|
127
|
-
value: a[href*="/travel/clk/f"]
|
|
128
|
-
extract: href
|
|
129
|
-
return_as: booking_links
|
|
130
|
-
notes: Assumes Google Flights opens in round-trip Economy (include Basic) by default and selects the first visible outbound and return flight cards, which in the captured session were nonstop Southwest options.
|