@tks/wayfinder 0.4.0 → 0.5.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
@@ -94,10 +94,16 @@ Search hotels:
94
94
  wayfinder hotels --where "New York, NY" --check-in 2026-04-10 --check-out 2026-04-12
95
95
  ```
96
96
 
97
- Search hotels with filters:
97
+ Search hotels with price and rating filters:
98
98
 
99
99
  ```bash
100
- wayfinder hotels --where "Tokyo" --check-in 2026-04-10 --check-out 2026-04-13 --adults 2 --max-price 300 --rating 4
100
+ wayfinder hotels --where "Tokyo" --check-in 2026-04-10 --check-out 2026-04-13 --adults 2 --min-price 120 --max-price 300 --rating 4
101
+ ```
102
+
103
+ Search family-friendly hotels with cancellation and class filters:
104
+
105
+ ```bash
106
+ 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
107
  ```
102
108
 
103
109
  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.5.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]
@@ -63,7 +63,12 @@ Hotels required:
63
63
 
64
64
  Hotels optional filters:
65
65
  --adults <N> Number of adults (default 2)
66
- --max-price <USD> Max nightly rate in USD
66
+ --children <N> Number of children
67
+ --children-ages <A,B> Child ages, comma-separated, example 4,7
68
+ --free-cancellation Only show hotels with free cancellation
69
+ --hotel-class <A,B> Hotel star class, comma-separated: 2,3,4,5
70
+ --min-price <USD> Lower price bound in USD
71
+ --max-price <USD> Upper price bound in USD
67
72
  --rating <3.5|4|4.5|5> Minimum guest rating
68
73
 
69
74
  Places required:
package/src/parse.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  ExitCode,
4
4
  FlightBookingQuery,
5
5
  FlightsQuery,
6
+ HotelClass,
6
7
  HotelQuery,
7
8
  ParsedArgs,
8
9
  PlaceQuery,
@@ -29,6 +30,11 @@ interface HotelRawOptions {
29
30
  checkIn?: string;
30
31
  checkOut?: string;
31
32
  adults?: string;
33
+ children?: string;
34
+ childrenAges?: string;
35
+ freeCancellation: boolean;
36
+ hotelClass?: string;
37
+ minPrice?: string;
32
38
  maxPrice?: string;
33
39
  rating?: string;
34
40
  outputJson: boolean;
@@ -170,6 +176,7 @@ function parseFlightsArgs(args: string[]): ParsedArgs {
170
176
 
171
177
  function parseHotelsArgs(args: string[]): ParsedArgs {
172
178
  const raw: HotelRawOptions = {
179
+ freeCancellation: false,
173
180
  outputJson: false,
174
181
  help: false,
175
182
  };
@@ -187,6 +194,11 @@ function parseHotelsArgs(args: string[]): ParsedArgs {
187
194
  continue;
188
195
  }
189
196
 
197
+ if (token === "--free-cancellation") {
198
+ raw.freeCancellation = true;
199
+ continue;
200
+ }
201
+
190
202
  if (!token.startsWith("--")) {
191
203
  throw new CliError(`Unexpected argument: ${token}`, ExitCode.InvalidInput);
192
204
  }
@@ -209,6 +221,18 @@ function parseHotelsArgs(args: string[]): ParsedArgs {
209
221
  case "--adults":
210
222
  raw.adults = value;
211
223
  break;
224
+ case "--children":
225
+ raw.children = value;
226
+ break;
227
+ case "--children-ages":
228
+ raw.childrenAges = value;
229
+ break;
230
+ case "--hotel-class":
231
+ raw.hotelClass = value;
232
+ break;
233
+ case "--min-price":
234
+ raw.minPrice = value;
235
+ break;
212
236
  case "--max-price":
213
237
  raw.maxPrice = value;
214
238
  break;
@@ -405,7 +429,7 @@ function buildFlightQuery(raw: FlightRawOptions): FlightsQuery {
405
429
 
406
430
  const airlineCode = raw.airline ? normalizeAirlineCode(raw.airline) : undefined;
407
431
  const maxStops = raw.maxStops ? normalizeMaxStops(raw.maxStops) : undefined;
408
- const maxPrice = raw.maxPrice ? normalizeMaxPrice(raw.maxPrice) : undefined;
432
+ const maxPrice = raw.maxPrice ? normalizePrice(raw.maxPrice, "--max-price") : undefined;
409
433
 
410
434
  const hasDepartAfter = typeof raw.departAfter === "string";
411
435
  const hasDepartBefore = typeof raw.departBefore === "string";
@@ -468,7 +492,12 @@ function buildHotelQuery(raw: HotelRawOptions): HotelQuery {
468
492
  const checkInDate = normalizeDate(raw.checkIn, "check-in");
469
493
  const checkOutDate = normalizeDate(raw.checkOut, "check-out");
470
494
  const adults = raw.adults ? normalizeAdults(raw.adults) : 2;
471
- const maxPrice = raw.maxPrice ? normalizeMaxPrice(raw.maxPrice) : undefined;
495
+ const children = raw.children ? normalizeChildren(raw.children) : 0;
496
+ const childrenAges = raw.childrenAges ? normalizeChildrenAges(raw.childrenAges) : undefined;
497
+ const freeCancellation = raw.freeCancellation || undefined;
498
+ const hotelClasses = raw.hotelClass ? normalizeHotelClasses(raw.hotelClass) : undefined;
499
+ const minPrice = raw.minPrice ? normalizePrice(raw.minPrice, "--min-price") : undefined;
500
+ const maxPrice = raw.maxPrice ? normalizePrice(raw.maxPrice, "--max-price") : undefined;
472
501
  const minRating = raw.rating ? normalizeMinRating(raw.rating) : undefined;
473
502
 
474
503
  const checkIn = parseDateOnly(checkInDate);
@@ -478,11 +507,31 @@ function buildHotelQuery(raw: HotelRawOptions): HotelQuery {
478
507
  throw new CliError("Check-out date must be after check-in date", ExitCode.InvalidInput);
479
508
  }
480
509
 
510
+ if (childrenAges && children === 0) {
511
+ throw new CliError("--children-ages requires --children", ExitCode.InvalidInput);
512
+ }
513
+
514
+ if (childrenAges && childrenAges.length !== children) {
515
+ throw new CliError(
516
+ "--children-ages count must match --children",
517
+ ExitCode.InvalidInput,
518
+ );
519
+ }
520
+
521
+ if (typeof minPrice === "number" && typeof maxPrice === "number" && minPrice > maxPrice) {
522
+ throw new CliError("--min-price cannot be greater than --max-price", ExitCode.InvalidInput);
523
+ }
524
+
481
525
  return {
482
526
  location,
483
527
  checkInDate,
484
528
  checkOutDate,
485
529
  adults,
530
+ children,
531
+ childrenAges,
532
+ freeCancellation,
533
+ hotelClasses,
534
+ minPrice,
486
535
  maxPrice,
487
536
  minRating,
488
537
  };
@@ -640,11 +689,17 @@ function normalizeMaxStops(value: string): number {
640
689
  return numeric;
641
690
  }
642
691
 
643
- function normalizeMaxPrice(value: string): number {
644
- const numeric = Number.parseInt(value, 10);
692
+ function normalizePrice(value: string, flagName: "--min-price" | "--max-price"): number {
693
+ const trimmed = value.trim();
694
+ if (!/^\d+$/.test(trimmed)) {
695
+ throw new CliError(`${flagName} must be a positive integer`, ExitCode.InvalidInput);
696
+ }
697
+
698
+ const numeric = Number.parseInt(trimmed, 10);
645
699
  if (!Number.isInteger(numeric) || numeric <= 0) {
646
- throw new CliError("--max-price must be a positive integer", ExitCode.InvalidInput);
700
+ throw new CliError(`${flagName} must be a positive integer`, ExitCode.InvalidInput);
647
701
  }
702
+
648
703
  return numeric;
649
704
  }
650
705
 
@@ -666,6 +721,62 @@ function normalizeAdults(value: string): number {
666
721
  return numeric;
667
722
  }
668
723
 
724
+ function normalizeChildren(value: string): number {
725
+ if (!/^\d+$/.test(value.trim())) {
726
+ throw new CliError("--children must be a positive integer", ExitCode.InvalidInput);
727
+ }
728
+
729
+ const numeric = Number.parseInt(value, 10);
730
+ if (!Number.isInteger(numeric) || numeric <= 0) {
731
+ throw new CliError("--children must be a positive integer", ExitCode.InvalidInput);
732
+ }
733
+
734
+ return numeric;
735
+ }
736
+
737
+ function normalizeChildrenAges(value: string): number[] {
738
+ const parts = value.split(",");
739
+ if (parts.length === 0) {
740
+ throw new CliError(
741
+ "--children-ages must be a comma-separated list of ages 1 through 17",
742
+ ExitCode.InvalidInput,
743
+ );
744
+ }
745
+
746
+ return parts.map((part) => {
747
+ const trimmed = part.trim();
748
+ if (!/^\d+$/.test(trimmed)) {
749
+ throw new CliError(
750
+ "--children-ages must be a comma-separated list of ages 1 through 17",
751
+ ExitCode.InvalidInput,
752
+ );
753
+ }
754
+
755
+ const age = Number.parseInt(trimmed, 10);
756
+ if (age < 1 || age > 17) {
757
+ throw new CliError("--children-ages ages must be between 1 and 17", ExitCode.InvalidInput);
758
+ }
759
+
760
+ return age;
761
+ });
762
+ }
763
+
764
+ function normalizeHotelClasses(value: string): HotelClass[] {
765
+ const hotelClasses = value.split(",").map((part) => {
766
+ const trimmed = part.trim();
767
+ if (trimmed !== "2" && trimmed !== "3" && trimmed !== "4" && trimmed !== "5") {
768
+ throw new CliError(
769
+ "--hotel-class must be a comma-separated list of: 2, 3, 4, 5",
770
+ ExitCode.InvalidInput,
771
+ );
772
+ }
773
+
774
+ return Number.parseInt(trimmed, 10) as HotelClass;
775
+ });
776
+
777
+ return [...new Set(hotelClasses)].sort((a, b) => a - b);
778
+ }
779
+
669
780
  function normalizeMinRating(value: string): 3.5 | 4 | 4.5 | 5 {
670
781
  const numeric = Number.parseFloat(value);
671
782
  if (numeric !== 3.5 && numeric !== 4 && numeric !== 4.5 && numeric !== 5) {
package/src/serpapi.ts CHANGED
@@ -405,9 +405,26 @@ function buildHotelRequestUrl(query: HotelQuery, apiKey: string): string {
405
405
  url.searchParams.set("check_in_date", query.checkInDate);
406
406
  url.searchParams.set("check_out_date", query.checkOutDate);
407
407
  url.searchParams.set("adults", String(query.adults));
408
+ url.searchParams.set("children", String(query.children));
408
409
  url.searchParams.set("currency", "USD");
409
410
  url.searchParams.set("api_key", apiKey);
410
411
 
412
+ if (query.childrenAges && query.childrenAges.length > 0) {
413
+ url.searchParams.set("children_ages", query.childrenAges.join(","));
414
+ }
415
+
416
+ if (query.freeCancellation) {
417
+ url.searchParams.set("free_cancellation", "true");
418
+ }
419
+
420
+ if (query.hotelClasses && query.hotelClasses.length > 0) {
421
+ url.searchParams.set("hotel_class", query.hotelClasses.join(","));
422
+ }
423
+
424
+ if (typeof query.minPrice === "number") {
425
+ url.searchParams.set("min_price", String(query.minPrice));
426
+ }
427
+
411
428
  if (typeof query.maxPrice === "number") {
412
429
  url.searchParams.set("max_price", String(query.maxPrice));
413
430
  }
package/src/types.ts CHANGED
@@ -35,11 +35,18 @@ export interface FlightMultiDateQuery {
35
35
 
36
36
  export type FlightsQuery = FlightQuery | FlightMultiDateQuery;
37
37
 
38
+ export type HotelClass = 2 | 3 | 4 | 5;
39
+
38
40
  export interface HotelQuery {
39
41
  location: string;
40
42
  checkInDate: string;
41
43
  checkOutDate: string;
42
44
  adults: number;
45
+ children: number;
46
+ childrenAges?: number[];
47
+ freeCancellation?: boolean;
48
+ hotelClasses?: HotelClass[];
49
+ minPrice?: number;
43
50
  maxPrice?: number;
44
51
  minRating?: 3.5 | 4 | 4.5 | 5;
45
52
  }