@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 +8 -2
- package/package.json +1 -1
- package/src/cli.ts +7 -2
- package/src/parse.ts +116 -5
- package/src/serpapi.ts +17 -0
- package/src/types.ts +7 -0
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
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
|
|
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
|
-
--
|
|
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 ?
|
|
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
|
|
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
|
|
644
|
-
const
|
|
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(
|
|
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
|
}
|