@tks/wayfinder 0.4.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
@@ -94,10 +100,16 @@ Search hotels:
94
100
  wayfinder hotels --where "New York, NY" --check-in 2026-04-10 --check-out 2026-04-12
95
101
  ```
96
102
 
97
- Search hotels with filters:
103
+ Search hotels with price and rating filters:
104
+
105
+ ```bash
106
+ wayfinder hotels --where "Tokyo" --check-in 2026-04-10 --check-out 2026-04-13 --adults 2 --min-price 120 --max-price 300 --rating 4
107
+ ```
108
+
109
+ Search family-friendly hotels with cancellation and class filters:
98
110
 
99
111
  ```bash
100
- wayfinder hotels --where "Tokyo" --check-in 2026-04-10 --check-out 2026-04-13 --adults 2 --max-price 300 --rating 4
112
+ wayfinder hotels --where "Tokyo" --check-in 2026-04-10 --check-out 2026-04-13 --adults 2 --children 2 --children-ages 4,7 --free-cancellation --hotel-class 4,5
101
113
  ```
102
114
 
103
115
  Structured hotel output for scripting:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tks/wayfinder",
3
- "version": "0.4.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
@@ -22,7 +22,7 @@ interface RunOptions {
22
22
  promptImpl?: (prompt: string) => Promise<string>;
23
23
  }
24
24
 
25
- const HELP_TEXT = `wayfinder v0.4.0 travel search
25
+ const HELP_TEXT = `wayfinder travel search
26
26
 
27
27
  Usage:
28
28
  wayfinder setup [--reset]
@@ -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
@@ -63,7 +64,12 @@ Hotels required:
63
64
 
64
65
  Hotels optional filters:
65
66
  --adults <N> Number of adults (default 2)
66
- --max-price <USD> Max nightly rate in USD
67
+ --children <N> Number of children
68
+ --children-ages <A,B> Child ages, comma-separated, example 4,7
69
+ --free-cancellation Only show hotels with free cancellation
70
+ --hotel-class <A,B> Hotel star class, comma-separated: 2,3,4,5
71
+ --min-price <USD> Lower price bound in USD
72
+ --max-price <USD> Upper price bound in USD
67
73
  --rating <3.5|4|4.5|5> Minimum guest rating
68
74
 
69
75
  Places required:
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,7 +2,9 @@ import { CliError } from "./errors";
2
2
  import {
3
3
  ExitCode,
4
4
  FlightBookingQuery,
5
+ CabinClass,
5
6
  FlightsQuery,
7
+ HotelClass,
6
8
  HotelQuery,
7
9
  ParsedArgs,
8
10
  PlaceQuery,
@@ -15,6 +17,7 @@ interface FlightRawOptions {
15
17
  to?: string;
16
18
  dates: string[];
17
19
  airline?: string;
20
+ cabin?: string;
18
21
  maxStops?: string;
19
22
  maxPrice?: string;
20
23
  departAfter?: string;
@@ -29,6 +32,11 @@ interface HotelRawOptions {
29
32
  checkIn?: string;
30
33
  checkOut?: string;
31
34
  adults?: string;
35
+ children?: string;
36
+ childrenAges?: string;
37
+ freeCancellation: boolean;
38
+ hotelClass?: string;
39
+ minPrice?: string;
32
40
  maxPrice?: string;
33
41
  rating?: string;
34
42
  outputJson: boolean;
@@ -134,6 +142,9 @@ function parseFlightsArgs(args: string[]): ParsedArgs {
134
142
  case "--airline":
135
143
  raw.airline = value;
136
144
  break;
145
+ case "--cabin":
146
+ raw.cabin = value;
147
+ break;
137
148
  case "--max-stops":
138
149
  raw.maxStops = value;
139
150
  break;
@@ -170,6 +181,7 @@ function parseFlightsArgs(args: string[]): ParsedArgs {
170
181
 
171
182
  function parseHotelsArgs(args: string[]): ParsedArgs {
172
183
  const raw: HotelRawOptions = {
184
+ freeCancellation: false,
173
185
  outputJson: false,
174
186
  help: false,
175
187
  };
@@ -187,6 +199,11 @@ function parseHotelsArgs(args: string[]): ParsedArgs {
187
199
  continue;
188
200
  }
189
201
 
202
+ if (token === "--free-cancellation") {
203
+ raw.freeCancellation = true;
204
+ continue;
205
+ }
206
+
190
207
  if (!token.startsWith("--")) {
191
208
  throw new CliError(`Unexpected argument: ${token}`, ExitCode.InvalidInput);
192
209
  }
@@ -209,6 +226,18 @@ function parseHotelsArgs(args: string[]): ParsedArgs {
209
226
  case "--adults":
210
227
  raw.adults = value;
211
228
  break;
229
+ case "--children":
230
+ raw.children = value;
231
+ break;
232
+ case "--children-ages":
233
+ raw.childrenAges = value;
234
+ break;
235
+ case "--hotel-class":
236
+ raw.hotelClass = value;
237
+ break;
238
+ case "--min-price":
239
+ raw.minPrice = value;
240
+ break;
212
241
  case "--max-price":
213
242
  raw.maxPrice = value;
214
243
  break;
@@ -404,8 +433,13 @@ function buildFlightQuery(raw: FlightRawOptions): FlightsQuery {
404
433
  }
405
434
 
406
435
  const airlineCode = raw.airline ? normalizeAirlineCode(raw.airline) : undefined;
436
+ const cabin = raw.cabin ? normalizeCabin(raw.cabin) : undefined;
407
437
  const maxStops = raw.maxStops ? normalizeMaxStops(raw.maxStops) : undefined;
408
- const maxPrice = raw.maxPrice ? normalizeMaxPrice(raw.maxPrice) : undefined;
438
+ const maxPrice = raw.maxPrice ? normalizePrice(raw.maxPrice, "--max-price") : undefined;
439
+
440
+ if (raw.excludeBasic && cabin && cabin !== "economy") {
441
+ throw new CliError("--exclude-basic can only be used with --cabin economy", ExitCode.InvalidInput);
442
+ }
409
443
 
410
444
  const hasDepartAfter = typeof raw.departAfter === "string";
411
445
  const hasDepartBefore = typeof raw.departBefore === "string";
@@ -436,6 +470,7 @@ function buildFlightQuery(raw: FlightRawOptions): FlightsQuery {
436
470
  origin,
437
471
  destination,
438
472
  airlineCode,
473
+ cabin,
439
474
  maxStops,
440
475
  maxPrice,
441
476
  departureAfterMinutes,
@@ -468,7 +503,12 @@ function buildHotelQuery(raw: HotelRawOptions): HotelQuery {
468
503
  const checkInDate = normalizeDate(raw.checkIn, "check-in");
469
504
  const checkOutDate = normalizeDate(raw.checkOut, "check-out");
470
505
  const adults = raw.adults ? normalizeAdults(raw.adults) : 2;
471
- const maxPrice = raw.maxPrice ? normalizeMaxPrice(raw.maxPrice) : undefined;
506
+ const children = raw.children ? normalizeChildren(raw.children) : 0;
507
+ const childrenAges = raw.childrenAges ? normalizeChildrenAges(raw.childrenAges) : undefined;
508
+ const freeCancellation = raw.freeCancellation || undefined;
509
+ const hotelClasses = raw.hotelClass ? normalizeHotelClasses(raw.hotelClass) : undefined;
510
+ const minPrice = raw.minPrice ? normalizePrice(raw.minPrice, "--min-price") : undefined;
511
+ const maxPrice = raw.maxPrice ? normalizePrice(raw.maxPrice, "--max-price") : undefined;
472
512
  const minRating = raw.rating ? normalizeMinRating(raw.rating) : undefined;
473
513
 
474
514
  const checkIn = parseDateOnly(checkInDate);
@@ -478,11 +518,31 @@ function buildHotelQuery(raw: HotelRawOptions): HotelQuery {
478
518
  throw new CliError("Check-out date must be after check-in date", ExitCode.InvalidInput);
479
519
  }
480
520
 
521
+ if (childrenAges && children === 0) {
522
+ throw new CliError("--children-ages requires --children", ExitCode.InvalidInput);
523
+ }
524
+
525
+ if (childrenAges && childrenAges.length !== children) {
526
+ throw new CliError(
527
+ "--children-ages count must match --children",
528
+ ExitCode.InvalidInput,
529
+ );
530
+ }
531
+
532
+ if (typeof minPrice === "number" && typeof maxPrice === "number" && minPrice > maxPrice) {
533
+ throw new CliError("--min-price cannot be greater than --max-price", ExitCode.InvalidInput);
534
+ }
535
+
481
536
  return {
482
537
  location,
483
538
  checkInDate,
484
539
  checkOutDate,
485
540
  adults,
541
+ children,
542
+ childrenAges,
543
+ freeCancellation,
544
+ hotelClasses,
545
+ minPrice,
486
546
  maxPrice,
487
547
  minRating,
488
548
  };
@@ -590,6 +650,27 @@ function normalizeAirlineCode(value: string): string {
590
650
  return upper;
591
651
  }
592
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
+
593
674
  function normalizeLocation(value: string): string {
594
675
  const trimmed = value.trim();
595
676
  if (trimmed.length === 0) {
@@ -640,11 +721,17 @@ function normalizeMaxStops(value: string): number {
640
721
  return numeric;
641
722
  }
642
723
 
643
- function normalizeMaxPrice(value: string): number {
644
- const numeric = Number.parseInt(value, 10);
724
+ function normalizePrice(value: string, flagName: "--min-price" | "--max-price"): number {
725
+ const trimmed = value.trim();
726
+ if (!/^\d+$/.test(trimmed)) {
727
+ throw new CliError(`${flagName} must be a positive integer`, ExitCode.InvalidInput);
728
+ }
729
+
730
+ const numeric = Number.parseInt(trimmed, 10);
645
731
  if (!Number.isInteger(numeric) || numeric <= 0) {
646
- throw new CliError("--max-price must be a positive integer", ExitCode.InvalidInput);
732
+ throw new CliError(`${flagName} must be a positive integer`, ExitCode.InvalidInput);
647
733
  }
734
+
648
735
  return numeric;
649
736
  }
650
737
 
@@ -666,6 +753,62 @@ function normalizeAdults(value: string): number {
666
753
  return numeric;
667
754
  }
668
755
 
756
+ function normalizeChildren(value: string): number {
757
+ if (!/^\d+$/.test(value.trim())) {
758
+ throw new CliError("--children must be a positive integer", ExitCode.InvalidInput);
759
+ }
760
+
761
+ const numeric = Number.parseInt(value, 10);
762
+ if (!Number.isInteger(numeric) || numeric <= 0) {
763
+ throw new CliError("--children must be a positive integer", ExitCode.InvalidInput);
764
+ }
765
+
766
+ return numeric;
767
+ }
768
+
769
+ function normalizeChildrenAges(value: string): number[] {
770
+ const parts = value.split(",");
771
+ if (parts.length === 0) {
772
+ throw new CliError(
773
+ "--children-ages must be a comma-separated list of ages 1 through 17",
774
+ ExitCode.InvalidInput,
775
+ );
776
+ }
777
+
778
+ return parts.map((part) => {
779
+ const trimmed = part.trim();
780
+ if (!/^\d+$/.test(trimmed)) {
781
+ throw new CliError(
782
+ "--children-ages must be a comma-separated list of ages 1 through 17",
783
+ ExitCode.InvalidInput,
784
+ );
785
+ }
786
+
787
+ const age = Number.parseInt(trimmed, 10);
788
+ if (age < 1 || age > 17) {
789
+ throw new CliError("--children-ages ages must be between 1 and 17", ExitCode.InvalidInput);
790
+ }
791
+
792
+ return age;
793
+ });
794
+ }
795
+
796
+ function normalizeHotelClasses(value: string): HotelClass[] {
797
+ const hotelClasses = value.split(",").map((part) => {
798
+ const trimmed = part.trim();
799
+ if (trimmed !== "2" && trimmed !== "3" && trimmed !== "4" && trimmed !== "5") {
800
+ throw new CliError(
801
+ "--hotel-class must be a comma-separated list of: 2, 3, 4, 5",
802
+ ExitCode.InvalidInput,
803
+ );
804
+ }
805
+
806
+ return Number.parseInt(trimmed, 10) as HotelClass;
807
+ });
808
+
809
+ return [...new Set(hotelClasses)].sort((a, b) => a - b);
810
+ }
811
+
669
812
  function normalizeMinRating(value: string): 3.5 | 4 | 4.5 | 5 {
670
813
  const numeric = Number.parseFloat(value);
671
814
  if (numeric !== 3.5 && numeric !== 4 && numeric !== 4.5 && numeric !== 5) {
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
 
@@ -405,9 +423,26 @@ function buildHotelRequestUrl(query: HotelQuery, apiKey: string): string {
405
423
  url.searchParams.set("check_in_date", query.checkInDate);
406
424
  url.searchParams.set("check_out_date", query.checkOutDate);
407
425
  url.searchParams.set("adults", String(query.adults));
426
+ url.searchParams.set("children", String(query.children));
408
427
  url.searchParams.set("currency", "USD");
409
428
  url.searchParams.set("api_key", apiKey);
410
429
 
430
+ if (query.childrenAges && query.childrenAges.length > 0) {
431
+ url.searchParams.set("children_ages", query.childrenAges.join(","));
432
+ }
433
+
434
+ if (query.freeCancellation) {
435
+ url.searchParams.set("free_cancellation", "true");
436
+ }
437
+
438
+ if (query.hotelClasses && query.hotelClasses.length > 0) {
439
+ url.searchParams.set("hotel_class", query.hotelClasses.join(","));
440
+ }
441
+
442
+ if (typeof query.minPrice === "number") {
443
+ url.searchParams.set("min_price", String(query.minPrice));
444
+ }
445
+
411
446
  if (typeof query.maxPrice === "number") {
412
447
  url.searchParams.set("max_price", String(query.maxPrice));
413
448
  }
@@ -462,6 +497,22 @@ function toSerpApiStopsFilter(maxStops: number): string {
462
497
  return "3";
463
498
  }
464
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
+
465
516
  function toSerpApiRating(rating: 3.5 | 4 | 4.5 | 5): string {
466
517
  if (rating === 3.5) {
467
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;
@@ -35,11 +39,18 @@ export interface FlightMultiDateQuery {
35
39
 
36
40
  export type FlightsQuery = FlightQuery | FlightMultiDateQuery;
37
41
 
42
+ export type HotelClass = 2 | 3 | 4 | 5;
43
+
38
44
  export interface HotelQuery {
39
45
  location: string;
40
46
  checkInDate: string;
41
47
  checkOutDate: string;
42
48
  adults: number;
49
+ children: number;
50
+ childrenAges?: number[];
51
+ freeCancellation?: boolean;
52
+ hotelClasses?: HotelClass[];
53
+ minPrice?: number;
43
54
  maxPrice?: number;
44
55
  minRating?: 3.5 | 4 | 4.5 | 5;
45
56
  }
@@ -116,6 +127,7 @@ export interface FlightOption {
116
127
  arrivalTime: string;
117
128
  durationMinutes: number;
118
129
  stops: number;
130
+ cabin?: string;
119
131
  bookingToken?: string;
120
132
  }
121
133