imprint-mcp 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +193 -189
  2. package/examples/discoverandgo/README.md +1 -1
  3. package/examples/echo/README.md +1 -1
  4. package/examples/google-flights/README.md +28 -0
  5. package/examples/google-flights/_shared/batchexecute.ts +63 -0
  6. package/examples/google-flights/_shared/flights_request.ts +95 -0
  7. package/examples/google-flights/_shared/package.json +9 -0
  8. package/examples/google-flights/get_flight_booking_details/index.ts +159 -0
  9. package/examples/google-flights/get_flight_booking_details/package.json +9 -0
  10. package/examples/google-flights/get_flight_booking_details/parser.ts +182 -0
  11. package/examples/google-flights/get_flight_booking_details/playbook.yaml +138 -0
  12. package/examples/google-flights/get_flight_booking_details/request-transform.ts +86 -0
  13. package/examples/google-flights/get_flight_booking_details/workflow.json +98 -0
  14. package/examples/google-flights/get_flight_calendar_prices/index.ts +131 -0
  15. package/examples/google-flights/get_flight_calendar_prices/package.json +9 -0
  16. package/examples/google-flights/get_flight_calendar_prices/parser.ts +86 -0
  17. package/examples/google-flights/get_flight_calendar_prices/playbook.yaml +97 -0
  18. package/examples/google-flights/get_flight_calendar_prices/request-transform.ts +31 -0
  19. package/examples/google-flights/get_flight_calendar_prices/workflow.json +78 -0
  20. package/examples/google-flights/lookup_airport/index.ts +101 -0
  21. package/examples/google-flights/lookup_airport/package.json +9 -0
  22. package/examples/google-flights/lookup_airport/parser.ts +66 -0
  23. package/examples/google-flights/lookup_airport/playbook.yaml +47 -0
  24. package/examples/google-flights/lookup_airport/request-transform.ts +20 -0
  25. package/examples/google-flights/lookup_airport/workflow.json +57 -0
  26. package/examples/google-flights/search_flights/index.ts +219 -0
  27. package/examples/google-flights/search_flights/package.json +9 -0
  28. package/examples/google-flights/search_flights/parser.ts +169 -0
  29. package/examples/google-flights/search_flights/playbook.yaml +184 -0
  30. package/examples/google-flights/search_flights/request-transform.ts +119 -0
  31. package/examples/google-flights/search_flights/workflow.json +143 -0
  32. package/examples/google-hotels/README.md +29 -0
  33. package/examples/google-hotels/_shared/batchexecute.ts +73 -0
  34. package/examples/google-hotels/_shared/freq.ts +158 -0
  35. package/examples/google-hotels/_shared/package.json +9 -0
  36. package/examples/google-hotels/autocomplete_hotel_location/index.ts +80 -0
  37. package/examples/google-hotels/autocomplete_hotel_location/package.json +9 -0
  38. package/examples/google-hotels/autocomplete_hotel_location/parser.ts +71 -0
  39. package/examples/google-hotels/autocomplete_hotel_location/playbook.yaml +36 -0
  40. package/examples/google-hotels/autocomplete_hotel_location/request-transform.ts +37 -0
  41. package/examples/google-hotels/autocomplete_hotel_location/workflow.json +36 -0
  42. package/examples/google-hotels/get_hotel_booking_options/index.ts +143 -0
  43. package/examples/google-hotels/get_hotel_booking_options/package.json +9 -0
  44. package/examples/google-hotels/get_hotel_booking_options/parser.ts +271 -0
  45. package/examples/google-hotels/get_hotel_booking_options/playbook.yaml +154 -0
  46. package/examples/google-hotels/get_hotel_booking_options/request-transform.ts +154 -0
  47. package/examples/google-hotels/get_hotel_booking_options/workflow.json +84 -0
  48. package/examples/google-hotels/get_hotel_reviews/index.ts +81 -0
  49. package/examples/google-hotels/get_hotel_reviews/package.json +9 -0
  50. package/examples/google-hotels/get_hotel_reviews/parser.ts +128 -0
  51. package/examples/google-hotels/get_hotel_reviews/playbook.yaml +64 -0
  52. package/examples/google-hotels/get_hotel_reviews/request-transform.ts +42 -0
  53. package/examples/google-hotels/get_hotel_reviews/workflow.json +37 -0
  54. package/examples/google-hotels/search_hotels/index.ts +207 -0
  55. package/examples/google-hotels/search_hotels/package.json +9 -0
  56. package/examples/google-hotels/search_hotels/parser.ts +260 -0
  57. package/examples/google-hotels/search_hotels/playbook.yaml +87 -0
  58. package/examples/google-hotels/search_hotels/request-transform.ts +197 -0
  59. package/examples/google-hotels/search_hotels/workflow.json +127 -0
  60. package/examples/southwest/README.md +3 -2
  61. package/examples/southwest/search_southwest_flights/index.ts +18 -1
  62. package/examples/southwest/search_southwest_flights/workflow.json +18 -1
  63. package/package.json +3 -2
  64. package/prompts/audit-agent.md +71 -0
  65. package/prompts/build-planning.md +74 -0
  66. package/prompts/compile-agent.md +131 -27
  67. package/prompts/prereq-builder.md +64 -0
  68. package/prompts/prereq-planner.md +34 -0
  69. package/prompts/tool-planning.md +39 -0
  70. package/src/cli.ts +116 -3
  71. package/src/imprint/agent.ts +5 -0
  72. package/src/imprint/audit.ts +996 -0
  73. package/src/imprint/backend-ladder.ts +1214 -184
  74. package/src/imprint/build-plan.ts +1051 -0
  75. package/src/imprint/cdp-browser-fetch.ts +592 -0
  76. package/src/imprint/cdp-jar-cache.ts +320 -0
  77. package/src/imprint/chromium.ts +414 -8
  78. package/src/imprint/claude-cli-compile.ts +125 -25
  79. package/src/imprint/codex-cli-compile.ts +26 -23
  80. package/src/imprint/compile-agent-types.ts +38 -0
  81. package/src/imprint/compile-agent.ts +63 -25
  82. package/src/imprint/compile-tools.ts +1666 -66
  83. package/src/imprint/compile.ts +13 -1
  84. package/src/imprint/concurrency.ts +87 -0
  85. package/src/imprint/cron.ts +4 -0
  86. package/src/imprint/doctor.ts +48 -3
  87. package/src/imprint/freeform-redact.ts +5 -4
  88. package/src/imprint/install.ts +79 -4
  89. package/src/imprint/integrations.ts +3 -3
  90. package/src/imprint/llm.ts +56 -8
  91. package/src/imprint/mcp-compile-server.ts +43 -10
  92. package/src/imprint/mcp-maintenance.ts +18 -102
  93. package/src/imprint/mcp-server.ts +73 -7
  94. package/src/imprint/multi-progress.ts +7 -2
  95. package/src/imprint/param-grounding.ts +367 -0
  96. package/src/imprint/paths.ts +29 -0
  97. package/src/imprint/playbook-runner.ts +101 -40
  98. package/src/imprint/prereq-builder.ts +651 -0
  99. package/src/imprint/probe-backends.ts +6 -3
  100. package/src/imprint/record.ts +10 -1
  101. package/src/imprint/redact.ts +30 -2
  102. package/src/imprint/replay-capture.ts +19 -18
  103. package/src/imprint/runtime.ts +19 -10
  104. package/src/imprint/session-diff.ts +79 -2
  105. package/src/imprint/session-merge.ts +9 -5
  106. package/src/imprint/stealth-chromium.ts +79 -0
  107. package/src/imprint/stealth-fetch.ts +309 -29
  108. package/src/imprint/stealth-token-cache.ts +88 -0
  109. package/src/imprint/teach-plan.ts +251 -0
  110. package/src/imprint/teach-state.ts +10 -0
  111. package/src/imprint/teach.ts +456 -142
  112. package/src/imprint/tool-candidates.ts +72 -14
  113. package/src/imprint/tool-plan.ts +313 -0
  114. package/src/imprint/tracing.ts +135 -6
  115. package/src/imprint/types.ts +61 -3
  116. package/examples/google-flights/search_google_flights/index.ts +0 -101
  117. package/examples/google-flights/search_google_flights/parser.test.ts +0 -140
  118. package/examples/google-flights/search_google_flights/parser.ts +0 -189
  119. package/examples/google-flights/search_google_flights/playbook.yaml +0 -130
  120. package/examples/google-flights/search_google_flights/workflow.json +0 -48
  121. package/examples/google-hotels/search_google_hotels/index.ts +0 -194
  122. package/examples/google-hotels/search_google_hotels/parser.test.ts +0 -168
  123. package/examples/google-hotels/search_google_hotels/parser.ts +0 -330
  124. package/examples/google-hotels/search_google_hotels/playbook.yaml +0 -125
  125. package/examples/google-hotels/search_google_hotels/workflow.json +0 -111
  126. package/examples/namecheap-domains/search_namecheap_domains/index.ts +0 -144
  127. package/examples/namecheap-domains/search_namecheap_domains/parser.ts +0 -380
  128. package/examples/namecheap-domains/search_namecheap_domains/playbook.yaml +0 -50
  129. package/examples/namecheap-domains/search_namecheap_domains/request-transform.ts +0 -136
  130. package/examples/namecheap-domains/search_namecheap_domains/workflow.json +0 -97
@@ -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) → stealth-fetch (bot-defense state + API replay) playbook
303
- * (full DOM walk). 'auto' only inserts fetch-bootstrap for declared or
304
- * satisfiable browser-minted state. */
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.