@tks/wayfinder 0.5.0 → 0.6.0

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 CHANGED
@@ -70,6 +70,12 @@ Search with filters:
70
70
  wayfinder flights --from LAX --to SEA --date 2026-04-10 --airline AS --max-stops 0 --max-price 250 --depart-after 06:00 --depart-before 12:00
71
71
  ```
72
72
 
73
+ Search by cabin:
74
+
75
+ ```bash
76
+ wayfinder flights --from JFK --to HND --date 2026-06-15 --cabin premium-economy
77
+ ```
78
+
73
79
  Exclude basic economy fares:
74
80
 
75
81
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tks/wayfinder",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Travel search for your terminal and your AI agents",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.ts CHANGED
@@ -43,6 +43,7 @@ Flights required:
43
43
 
44
44
  Flights optional filters:
45
45
  --airline <IATA> Airline code, example UA
46
+ --cabin <CABIN> Cabin: economy, premium-economy, preeco, business, first
46
47
  --max-stops <0|1|2> Maximum number of stops
47
48
  --max-price <USD> Max price in USD
48
49
  --depart-after <HH:MM> Start of departure window
package/src/format.ts CHANGED
@@ -14,6 +14,7 @@ export function renderFlightTable(options: FlightOption[]): string {
14
14
  arrive: option.arrivalTime,
15
15
  duration: formatDuration(option.durationMinutes),
16
16
  stops: String(option.stops),
17
+ cabin: option.cabin ?? "n/a",
17
18
  }));
18
19
 
19
20
  const headers = {
@@ -23,6 +24,7 @@ export function renderFlightTable(options: FlightOption[]): string {
23
24
  arrive: "ARRIVE",
24
25
  duration: "DURATION",
25
26
  stops: "STOPS",
27
+ cabin: "CABIN",
26
28
  };
27
29
 
28
30
  const widths = {
@@ -32,6 +34,7 @@ export function renderFlightTable(options: FlightOption[]): string {
32
34
  arrive: maxWidth(rows, "arrive", headers.arrive),
33
35
  duration: maxWidth(rows, "duration", headers.duration),
34
36
  stops: maxWidth(rows, "stops", headers.stops),
37
+ cabin: maxWidth(rows, "cabin", headers.cabin),
35
38
  };
36
39
 
37
40
  const lines: string[] = [];
@@ -44,6 +47,7 @@ export function renderFlightTable(options: FlightOption[]): string {
44
47
  headers.arrive.padEnd(widths.arrive),
45
48
  headers.duration.padEnd(widths.duration),
46
49
  headers.stops.padEnd(widths.stops),
50
+ headers.cabin.padEnd(widths.cabin),
47
51
  ].join(" "),
48
52
  );
49
53
 
@@ -55,6 +59,7 @@ export function renderFlightTable(options: FlightOption[]): string {
55
59
  "-".repeat(widths.arrive),
56
60
  "-".repeat(widths.duration),
57
61
  "-".repeat(widths.stops),
62
+ "-".repeat(widths.cabin),
58
63
  ].join(" "),
59
64
  );
60
65
 
@@ -67,6 +72,7 @@ export function renderFlightTable(options: FlightOption[]): string {
67
72
  row.arrive.padEnd(widths.arrive),
68
73
  row.duration.padEnd(widths.duration),
69
74
  row.stops.padEnd(widths.stops),
75
+ row.cabin.padEnd(widths.cabin),
70
76
  ].join(" "),
71
77
  );
72
78
  }
package/src/parse.ts CHANGED
@@ -2,6 +2,7 @@ import { CliError } from "./errors";
2
2
  import {
3
3
  ExitCode,
4
4
  FlightBookingQuery,
5
+ CabinClass,
5
6
  FlightsQuery,
6
7
  HotelClass,
7
8
  HotelQuery,
@@ -16,6 +17,7 @@ interface FlightRawOptions {
16
17
  to?: string;
17
18
  dates: string[];
18
19
  airline?: string;
20
+ cabin?: string;
19
21
  maxStops?: string;
20
22
  maxPrice?: string;
21
23
  departAfter?: string;
@@ -140,6 +142,9 @@ function parseFlightsArgs(args: string[]): ParsedArgs {
140
142
  case "--airline":
141
143
  raw.airline = value;
142
144
  break;
145
+ case "--cabin":
146
+ raw.cabin = value;
147
+ break;
143
148
  case "--max-stops":
144
149
  raw.maxStops = value;
145
150
  break;
@@ -428,9 +433,14 @@ function buildFlightQuery(raw: FlightRawOptions): FlightsQuery {
428
433
  }
429
434
 
430
435
  const airlineCode = raw.airline ? normalizeAirlineCode(raw.airline) : undefined;
436
+ const cabin = raw.cabin ? normalizeCabin(raw.cabin) : undefined;
431
437
  const maxStops = raw.maxStops ? normalizeMaxStops(raw.maxStops) : undefined;
432
438
  const maxPrice = raw.maxPrice ? normalizePrice(raw.maxPrice, "--max-price") : undefined;
433
439
 
440
+ if (raw.excludeBasic && cabin && cabin !== "economy") {
441
+ throw new CliError("--exclude-basic can only be used with --cabin economy", ExitCode.InvalidInput);
442
+ }
443
+
434
444
  const hasDepartAfter = typeof raw.departAfter === "string";
435
445
  const hasDepartBefore = typeof raw.departBefore === "string";
436
446
 
@@ -460,6 +470,7 @@ function buildFlightQuery(raw: FlightRawOptions): FlightsQuery {
460
470
  origin,
461
471
  destination,
462
472
  airlineCode,
473
+ cabin,
463
474
  maxStops,
464
475
  maxPrice,
465
476
  departureAfterMinutes,
@@ -639,6 +650,27 @@ function normalizeAirlineCode(value: string): string {
639
650
  return upper;
640
651
  }
641
652
 
653
+ function normalizeCabin(value: string): CabinClass {
654
+ const normalized = value.trim().toLowerCase();
655
+ if (normalized === "preeco") {
656
+ return "premium-economy";
657
+ }
658
+
659
+ if (
660
+ normalized === "economy" ||
661
+ normalized === "premium-economy" ||
662
+ normalized === "business" ||
663
+ normalized === "first"
664
+ ) {
665
+ return normalized;
666
+ }
667
+
668
+ throw new CliError(
669
+ "--cabin must be one of: economy, premium-economy, preeco, business, first",
670
+ ExitCode.InvalidInput,
671
+ );
672
+ }
673
+
642
674
  function normalizeLocation(value: string): string {
643
675
  const trimmed = value.trim();
644
676
  if (trimmed.length === 0) {
package/src/serpapi.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CliError } from "./errors";
2
2
  import {
3
+ CabinClass,
3
4
  ExitCode,
4
5
  FlightDateResult,
5
6
  FlightBookingQuery,
@@ -22,6 +23,7 @@ interface SerpApiAirport {
22
23
 
23
24
  interface SerpApiSegment {
24
25
  airline?: string;
26
+ travel_class?: string;
25
27
  duration?: number;
26
28
  departure_airport?: SerpApiAirport;
27
29
  arrival_airport?: SerpApiAirport;
@@ -148,6 +150,7 @@ export async function searchFlightsMultiDate(
148
150
  destination: query.destination,
149
151
  departureDate: date,
150
152
  airlineCode: query.airlineCode,
153
+ cabin: query.cabin,
151
154
  maxStops: query.maxStops,
152
155
  maxPrice: query.maxPrice,
153
156
  departureAfterMinutes: query.departureAfterMinutes,
@@ -291,6 +294,9 @@ function shapeItinerary(itinerary: SerpApiItinerary): FlightOption | null {
291
294
  }
292
295
 
293
296
  const uniqueAirlines = [...new Set(segments.map((segment) => segment.airline).filter(Boolean))];
297
+ const cabinClasses = [
298
+ ...new Set(segments.map((segment) => segment.travel_class).filter(isNonEmptyString)),
299
+ ];
294
300
 
295
301
  const option: FlightOption = {
296
302
  price: itinerary.price as number,
@@ -301,6 +307,10 @@ function shapeItinerary(itinerary: SerpApiItinerary): FlightOption | null {
301
307
  stops: Math.max(0, segments.length - 1),
302
308
  };
303
309
 
310
+ if (cabinClasses.length > 0) {
311
+ option.cabin = cabinClasses.join(", ");
312
+ }
313
+
304
314
  if (typeof itinerary.booking_token === "string" && itinerary.booking_token.trim() !== "") {
305
315
  option.bookingToken = itinerary.booking_token.trim();
306
316
  }
@@ -351,6 +361,10 @@ function inferDurationMinutes(itinerary: SerpApiItinerary, segments: SerpApiSegm
351
361
  return segmentDuration;
352
362
  }
353
363
 
364
+ function isNonEmptyString(value: unknown): value is string {
365
+ return typeof value === "string" && value.trim() !== "";
366
+ }
367
+
354
368
  function buildFlightRequestUrl(query: FlightQuery, apiKey: string): string {
355
369
  const url = new URL("https://serpapi.com/search.json");
356
370
 
@@ -367,6 +381,10 @@ function buildFlightRequestUrl(query: FlightQuery, apiKey: string): string {
367
381
  url.searchParams.set("include_airlines", query.airlineCode);
368
382
  }
369
383
 
384
+ if (query.cabin) {
385
+ url.searchParams.set("travel_class", toSerpApiTravelClass(query.cabin));
386
+ }
387
+
370
388
  if (typeof query.maxStops === "number") {
371
389
  url.searchParams.set("stops", toSerpApiStopsFilter(query.maxStops));
372
390
  }
@@ -390,7 +408,7 @@ function buildFlightRequestUrl(query: FlightQuery, apiKey: string): string {
390
408
 
391
409
  if (query.excludeBasic) {
392
410
  url.searchParams.set("exclude_basic", "true");
393
- url.searchParams.set("travel_class", "1");
411
+ url.searchParams.set("travel_class", toSerpApiTravelClass("economy"));
394
412
  url.searchParams.set("gl", "us");
395
413
  }
396
414
 
@@ -479,6 +497,22 @@ function toSerpApiStopsFilter(maxStops: number): string {
479
497
  return "3";
480
498
  }
481
499
 
500
+ function toSerpApiTravelClass(cabin: CabinClass): string {
501
+ if (cabin === "economy") {
502
+ return "1";
503
+ }
504
+
505
+ if (cabin === "premium-economy") {
506
+ return "2";
507
+ }
508
+
509
+ if (cabin === "business") {
510
+ return "3";
511
+ }
512
+
513
+ return "4";
514
+ }
515
+
482
516
  function toSerpApiRating(rating: 3.5 | 4 | 4.5 | 5): string {
483
517
  if (rating === 3.5) {
484
518
  return "7";
package/src/types.ts CHANGED
@@ -9,11 +9,14 @@ export const ExitCode = {
9
9
 
10
10
  export type ExitCodeValue = (typeof ExitCode)[keyof typeof ExitCode];
11
11
 
12
+ export type CabinClass = "economy" | "premium-economy" | "business" | "first";
13
+
12
14
  export interface FlightQuery {
13
15
  origin: string;
14
16
  destination: string;
15
17
  departureDate: string;
16
18
  airlineCode?: string;
19
+ cabin?: CabinClass;
17
20
  maxStops?: number;
18
21
  maxPrice?: number;
19
22
  departureAfterMinutes?: number;
@@ -26,6 +29,7 @@ export interface FlightMultiDateQuery {
26
29
  destination: string;
27
30
  departureDates: string[];
28
31
  airlineCode?: string;
32
+ cabin?: CabinClass;
29
33
  maxStops?: number;
30
34
  maxPrice?: number;
31
35
  departureAfterMinutes?: number;
@@ -123,6 +127,7 @@ export interface FlightOption {
123
127
  arrivalTime: string;
124
128
  durationMinutes: number;
125
129
  stops: number;
130
+ cabin?: string;
126
131
  bookingToken?: string;
127
132
  }
128
133