@tks/wayfinder 0.3.1 → 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 +14 -2
- package/package.json +1 -1
- package/src/cli.ts +56 -23
- package/src/format.ts +14 -1
- package/src/parse.ts +143 -13
- package/src/serpapi.ts +82 -0
- package/src/types.ts +32 -1
package/README.md
CHANGED
|
@@ -58,6 +58,12 @@ Search one way flights:
|
|
|
58
58
|
wayfinder flights --from SFO --to JFK --date 2026-04-10
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
Search up to 3 departure dates in one command:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
wayfinder flights --from SFO --to JFK --date 2026-04-10 --date 2026-04-11 --date 2026-04-13
|
|
65
|
+
```
|
|
66
|
+
|
|
61
67
|
Search with filters:
|
|
62
68
|
|
|
63
69
|
```bash
|
|
@@ -88,10 +94,16 @@ Search hotels:
|
|
|
88
94
|
wayfinder hotels --where "New York, NY" --check-in 2026-04-10 --check-out 2026-04-12
|
|
89
95
|
```
|
|
90
96
|
|
|
91
|
-
Search hotels with filters:
|
|
97
|
+
Search hotels with price and rating filters:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
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:
|
|
92
104
|
|
|
93
105
|
```bash
|
|
94
|
-
wayfinder hotels --where "Tokyo" --check-in 2026-04-10 --check-out 2026-04-13 --adults 2 --
|
|
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
|
|
95
107
|
```
|
|
96
108
|
|
|
97
109
|
Structured hotel output for scripting:
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { getConfigPath, readConfigApiKey, resolveApiKey, writeConfigApiKey } from "./config";
|
|
2
2
|
import { CliError } from "./errors";
|
|
3
|
-
import { renderFlightTable, renderHotelTable, renderPlaceTable } from "./format";
|
|
3
|
+
import { renderFlightTable, renderFlightTablesByDate, renderHotelTable, renderPlaceTable } from "./format";
|
|
4
4
|
import { parseCliArgs } from "./parse";
|
|
5
|
-
import { searchFlightBookingOptions, searchFlights, searchHotels, searchPlaces } from "./serpapi";
|
|
6
|
-
import { ExitCode } from "./types";
|
|
5
|
+
import { searchFlightBookingOptions, searchFlights, searchFlightsMultiDate, searchHotels, searchPlaces } from "./serpapi";
|
|
6
|
+
import { ExitCode, FlightMultiDateQuery, FlightsQuery } from "./types";
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
8
|
import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
|
|
9
9
|
import { existsSync, unlinkSync } from "node:fs";
|
|
@@ -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]
|
|
@@ -39,7 +39,7 @@ Setup:
|
|
|
39
39
|
Flights required:
|
|
40
40
|
--from <IATA> Origin airport code
|
|
41
41
|
--to <IATA> Destination airport code
|
|
42
|
-
--date <YYYY-MM-DD> Departure date
|
|
42
|
+
--date <YYYY-MM-DD> Departure date (repeat up to 3 unique dates)
|
|
43
43
|
|
|
44
44
|
Flights optional filters:
|
|
45
45
|
--airline <IATA> Airline code, example UA
|
|
@@ -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:
|
|
@@ -135,26 +140,50 @@ export async function runWayfinder(
|
|
|
135
140
|
|
|
136
141
|
const apiKey = resolveApiKey(env, homeDir);
|
|
137
142
|
if (parsed.mode === "flights") {
|
|
138
|
-
|
|
143
|
+
if (isMultiDateFlightQuery(parsed.query)) {
|
|
144
|
+
const multiDateFlights = await searchFlightsMultiDate(parsed.query, apiKey, options.fetchImpl ?? fetch);
|
|
145
|
+
const nonEmptyDateResults = multiDateFlights.resultsByDate.filter((entry) => entry.results.length > 0);
|
|
139
146
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
if (nonEmptyDateResults.length === 0) {
|
|
148
|
+
throw new CliError("No flights found for the selected query", ExitCode.NoResults);
|
|
149
|
+
}
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
155
|
-
|
|
151
|
+
if (parsed.outputJson) {
|
|
152
|
+
output.stdout(
|
|
153
|
+
JSON.stringify(
|
|
154
|
+
{
|
|
155
|
+
query: parsed.query,
|
|
156
|
+
resultsByDate: nonEmptyDateResults,
|
|
157
|
+
},
|
|
158
|
+
null,
|
|
159
|
+
2,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
output.stdout(renderFlightTablesByDate(nonEmptyDateResults));
|
|
164
|
+
}
|
|
156
165
|
} else {
|
|
157
|
-
|
|
166
|
+
const flights = await searchFlights(parsed.query, apiKey, options.fetchImpl ?? fetch);
|
|
167
|
+
|
|
168
|
+
if (flights.options.length === 0) {
|
|
169
|
+
throw new CliError("No flights found for the selected query", ExitCode.NoResults);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (parsed.outputJson) {
|
|
173
|
+
output.stdout(
|
|
174
|
+
JSON.stringify(
|
|
175
|
+
{
|
|
176
|
+
query: parsed.query,
|
|
177
|
+
googleFlightsUrl: flights.googleFlightsUrl,
|
|
178
|
+
results: flights.options,
|
|
179
|
+
},
|
|
180
|
+
null,
|
|
181
|
+
2,
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
output.stdout(renderFlightTable(flights.options));
|
|
186
|
+
}
|
|
158
187
|
}
|
|
159
188
|
} else if (parsed.mode === "flight-booking") {
|
|
160
189
|
const bookingResults = await searchFlightBookingOptions(
|
|
@@ -340,3 +369,7 @@ async function promptWithFallback(
|
|
|
340
369
|
rl.close();
|
|
341
370
|
}
|
|
342
371
|
}
|
|
372
|
+
|
|
373
|
+
function isMultiDateFlightQuery(query: FlightsQuery): query is FlightMultiDateQuery {
|
|
374
|
+
return "departureDates" in query;
|
|
375
|
+
}
|
package/src/format.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FlightOption, HotelOption, PlaceOption } from "./types";
|
|
1
|
+
import { FlightDateResult, FlightOption, HotelOption, PlaceOption } from "./types";
|
|
2
2
|
|
|
3
3
|
const currencyFormatter = new Intl.NumberFormat("en-US", {
|
|
4
4
|
style: "currency",
|
|
@@ -74,6 +74,19 @@ export function renderFlightTable(options: FlightOption[]): string {
|
|
|
74
74
|
return lines.join("\n");
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export function renderFlightTablesByDate(resultsByDate: FlightDateResult[]): string {
|
|
78
|
+
const sections = resultsByDate.map((result) => {
|
|
79
|
+
const lines = [`DATE: ${result.date}`];
|
|
80
|
+
if (typeof result.googleFlightsUrl === "string") {
|
|
81
|
+
lines.push(`GOOGLE FLIGHTS: ${result.googleFlightsUrl}`);
|
|
82
|
+
}
|
|
83
|
+
lines.push(renderFlightTable(result.results));
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return sections.join("\n\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
export function renderHotelTable(options: HotelOption[]): string {
|
|
78
91
|
const rows = options.map((option) => ({
|
|
79
92
|
nightly: currencyFormatter.format(option.nightlyPrice),
|
package/src/parse.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { CliError } from "./errors";
|
|
|
2
2
|
import {
|
|
3
3
|
ExitCode,
|
|
4
4
|
FlightBookingQuery,
|
|
5
|
-
|
|
5
|
+
FlightsQuery,
|
|
6
|
+
HotelClass,
|
|
6
7
|
HotelQuery,
|
|
7
8
|
ParsedArgs,
|
|
8
9
|
PlaceQuery,
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
interface FlightRawOptions {
|
|
14
15
|
from?: string;
|
|
15
16
|
to?: string;
|
|
16
|
-
|
|
17
|
+
dates: string[];
|
|
17
18
|
airline?: string;
|
|
18
19
|
maxStops?: string;
|
|
19
20
|
maxPrice?: string;
|
|
@@ -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;
|
|
@@ -88,6 +94,7 @@ export function parseCliArgs(argv: string[]): ParsedArgs {
|
|
|
88
94
|
|
|
89
95
|
function parseFlightsArgs(args: string[]): ParsedArgs {
|
|
90
96
|
const raw: FlightRawOptions = {
|
|
97
|
+
dates: [],
|
|
91
98
|
excludeBasic: false,
|
|
92
99
|
outputJson: false,
|
|
93
100
|
help: false,
|
|
@@ -128,7 +135,7 @@ function parseFlightsArgs(args: string[]): ParsedArgs {
|
|
|
128
135
|
raw.to = value;
|
|
129
136
|
break;
|
|
130
137
|
case "--date":
|
|
131
|
-
raw.
|
|
138
|
+
raw.dates.push(value);
|
|
132
139
|
break;
|
|
133
140
|
case "--airline":
|
|
134
141
|
raw.airline = value;
|
|
@@ -169,6 +176,7 @@ function parseFlightsArgs(args: string[]): ParsedArgs {
|
|
|
169
176
|
|
|
170
177
|
function parseHotelsArgs(args: string[]): ParsedArgs {
|
|
171
178
|
const raw: HotelRawOptions = {
|
|
179
|
+
freeCancellation: false,
|
|
172
180
|
outputJson: false,
|
|
173
181
|
help: false,
|
|
174
182
|
};
|
|
@@ -186,6 +194,11 @@ function parseHotelsArgs(args: string[]): ParsedArgs {
|
|
|
186
194
|
continue;
|
|
187
195
|
}
|
|
188
196
|
|
|
197
|
+
if (token === "--free-cancellation") {
|
|
198
|
+
raw.freeCancellation = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
189
202
|
if (!token.startsWith("--")) {
|
|
190
203
|
throw new CliError(`Unexpected argument: ${token}`, ExitCode.InvalidInput);
|
|
191
204
|
}
|
|
@@ -208,6 +221,18 @@ function parseHotelsArgs(args: string[]): ParsedArgs {
|
|
|
208
221
|
case "--adults":
|
|
209
222
|
raw.adults = value;
|
|
210
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;
|
|
211
236
|
case "--max-price":
|
|
212
237
|
raw.maxPrice = value;
|
|
213
238
|
break;
|
|
@@ -382,8 +407,8 @@ function parseSetupArgs(args: string[]): ParsedArgs {
|
|
|
382
407
|
};
|
|
383
408
|
}
|
|
384
409
|
|
|
385
|
-
function buildFlightQuery(raw: FlightRawOptions):
|
|
386
|
-
if (!raw.from || !raw.to ||
|
|
410
|
+
function buildFlightQuery(raw: FlightRawOptions): FlightsQuery {
|
|
411
|
+
if (!raw.from || !raw.to || raw.dates.length === 0) {
|
|
387
412
|
throw new CliError("Missing required flags: --from, --to, --date", ExitCode.InvalidInput);
|
|
388
413
|
}
|
|
389
414
|
|
|
@@ -394,10 +419,17 @@ function buildFlightQuery(raw: FlightRawOptions): FlightQuery {
|
|
|
394
419
|
throw new CliError("Origin and destination must be different airports", ExitCode.InvalidInput);
|
|
395
420
|
}
|
|
396
421
|
|
|
397
|
-
const
|
|
422
|
+
const departureDates = [...new Set(raw.dates.map((date) => normalizeDate(date, "departure")))];
|
|
423
|
+
if (departureDates.length > 3) {
|
|
424
|
+
throw new CliError(
|
|
425
|
+
"Too many dates: maximum 3 unique --date values per search",
|
|
426
|
+
ExitCode.InvalidInput,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
398
430
|
const airlineCode = raw.airline ? normalizeAirlineCode(raw.airline) : undefined;
|
|
399
431
|
const maxStops = raw.maxStops ? normalizeMaxStops(raw.maxStops) : undefined;
|
|
400
|
-
const maxPrice = raw.maxPrice ?
|
|
432
|
+
const maxPrice = raw.maxPrice ? normalizePrice(raw.maxPrice, "--max-price") : undefined;
|
|
401
433
|
|
|
402
434
|
const hasDepartAfter = typeof raw.departAfter === "string";
|
|
403
435
|
const hasDepartBefore = typeof raw.departBefore === "string";
|
|
@@ -424,10 +456,9 @@ function buildFlightQuery(raw: FlightRawOptions): FlightQuery {
|
|
|
424
456
|
}
|
|
425
457
|
}
|
|
426
458
|
|
|
427
|
-
|
|
459
|
+
const shared = {
|
|
428
460
|
origin,
|
|
429
461
|
destination,
|
|
430
|
-
departureDate,
|
|
431
462
|
airlineCode,
|
|
432
463
|
maxStops,
|
|
433
464
|
maxPrice,
|
|
@@ -435,6 +466,18 @@ function buildFlightQuery(raw: FlightRawOptions): FlightQuery {
|
|
|
435
466
|
departureBeforeMinutes,
|
|
436
467
|
excludeBasic: raw.excludeBasic || undefined,
|
|
437
468
|
};
|
|
469
|
+
|
|
470
|
+
if (departureDates.length === 1) {
|
|
471
|
+
return {
|
|
472
|
+
...shared,
|
|
473
|
+
departureDate: departureDates[0] as string,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
...shared,
|
|
479
|
+
departureDates,
|
|
480
|
+
};
|
|
438
481
|
}
|
|
439
482
|
|
|
440
483
|
function buildHotelQuery(raw: HotelRawOptions): HotelQuery {
|
|
@@ -449,7 +492,12 @@ function buildHotelQuery(raw: HotelRawOptions): HotelQuery {
|
|
|
449
492
|
const checkInDate = normalizeDate(raw.checkIn, "check-in");
|
|
450
493
|
const checkOutDate = normalizeDate(raw.checkOut, "check-out");
|
|
451
494
|
const adults = raw.adults ? normalizeAdults(raw.adults) : 2;
|
|
452
|
-
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;
|
|
453
501
|
const minRating = raw.rating ? normalizeMinRating(raw.rating) : undefined;
|
|
454
502
|
|
|
455
503
|
const checkIn = parseDateOnly(checkInDate);
|
|
@@ -459,11 +507,31 @@ function buildHotelQuery(raw: HotelRawOptions): HotelQuery {
|
|
|
459
507
|
throw new CliError("Check-out date must be after check-in date", ExitCode.InvalidInput);
|
|
460
508
|
}
|
|
461
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
|
+
|
|
462
525
|
return {
|
|
463
526
|
location,
|
|
464
527
|
checkInDate,
|
|
465
528
|
checkOutDate,
|
|
466
529
|
adults,
|
|
530
|
+
children,
|
|
531
|
+
childrenAges,
|
|
532
|
+
freeCancellation,
|
|
533
|
+
hotelClasses,
|
|
534
|
+
minPrice,
|
|
467
535
|
maxPrice,
|
|
468
536
|
minRating,
|
|
469
537
|
};
|
|
@@ -621,11 +689,17 @@ function normalizeMaxStops(value: string): number {
|
|
|
621
689
|
return numeric;
|
|
622
690
|
}
|
|
623
691
|
|
|
624
|
-
function
|
|
625
|
-
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);
|
|
626
699
|
if (!Number.isInteger(numeric) || numeric <= 0) {
|
|
627
|
-
throw new CliError(
|
|
700
|
+
throw new CliError(`${flagName} must be a positive integer`, ExitCode.InvalidInput);
|
|
628
701
|
}
|
|
702
|
+
|
|
629
703
|
return numeric;
|
|
630
704
|
}
|
|
631
705
|
|
|
@@ -647,6 +721,62 @@ function normalizeAdults(value: string): number {
|
|
|
647
721
|
return numeric;
|
|
648
722
|
}
|
|
649
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
|
+
|
|
650
780
|
function normalizeMinRating(value: string): 3.5 | 4 | 4.5 | 5 {
|
|
651
781
|
const numeric = Number.parseFloat(value);
|
|
652
782
|
if (numeric !== 3.5 && numeric !== 4 && numeric !== 4.5 && numeric !== 5) {
|
package/src/serpapi.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { CliError } from "./errors";
|
|
2
2
|
import {
|
|
3
3
|
ExitCode,
|
|
4
|
+
FlightDateResult,
|
|
4
5
|
FlightBookingQuery,
|
|
5
6
|
FlightBookingResult,
|
|
7
|
+
FlightMultiDateQuery,
|
|
8
|
+
FlightMultiDateSearchResult,
|
|
6
9
|
FlightOption,
|
|
7
10
|
FlightQuery,
|
|
8
11
|
FlightSearchResult,
|
|
@@ -134,6 +137,38 @@ export async function searchFlights(
|
|
|
134
137
|
};
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
export async function searchFlightsMultiDate(
|
|
141
|
+
query: FlightMultiDateQuery,
|
|
142
|
+
apiKey: string,
|
|
143
|
+
fetchImpl: typeof fetch = fetch,
|
|
144
|
+
): Promise<FlightMultiDateSearchResult> {
|
|
145
|
+
const resultsByDate = await mapWithConcurrency(query.departureDates, 2, async (date) => {
|
|
146
|
+
const singleDateQuery: FlightQuery = {
|
|
147
|
+
origin: query.origin,
|
|
148
|
+
destination: query.destination,
|
|
149
|
+
departureDate: date,
|
|
150
|
+
airlineCode: query.airlineCode,
|
|
151
|
+
maxStops: query.maxStops,
|
|
152
|
+
maxPrice: query.maxPrice,
|
|
153
|
+
departureAfterMinutes: query.departureAfterMinutes,
|
|
154
|
+
departureBeforeMinutes: query.departureBeforeMinutes,
|
|
155
|
+
excludeBasic: query.excludeBasic,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const result = await searchFlights(singleDateQuery, apiKey, fetchImpl);
|
|
159
|
+
const grouped: FlightDateResult = {
|
|
160
|
+
date,
|
|
161
|
+
results: result.options,
|
|
162
|
+
googleFlightsUrl: result.googleFlightsUrl,
|
|
163
|
+
};
|
|
164
|
+
return grouped;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
resultsByDate,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
137
172
|
export async function searchFlightBookingOptions(
|
|
138
173
|
query: FlightBookingQuery,
|
|
139
174
|
apiKey: string,
|
|
@@ -370,9 +405,26 @@ function buildHotelRequestUrl(query: HotelQuery, apiKey: string): string {
|
|
|
370
405
|
url.searchParams.set("check_in_date", query.checkInDate);
|
|
371
406
|
url.searchParams.set("check_out_date", query.checkOutDate);
|
|
372
407
|
url.searchParams.set("adults", String(query.adults));
|
|
408
|
+
url.searchParams.set("children", String(query.children));
|
|
373
409
|
url.searchParams.set("currency", "USD");
|
|
374
410
|
url.searchParams.set("api_key", apiKey);
|
|
375
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
|
+
|
|
376
428
|
if (typeof query.maxPrice === "number") {
|
|
377
429
|
url.searchParams.set("max_price", String(query.maxPrice));
|
|
378
430
|
}
|
|
@@ -685,3 +737,33 @@ function collectBookingLinks(
|
|
|
685
737
|
collectBookingLinks(child, links);
|
|
686
738
|
}
|
|
687
739
|
}
|
|
740
|
+
|
|
741
|
+
async function mapWithConcurrency<TInput, TOutput>(
|
|
742
|
+
items: TInput[],
|
|
743
|
+
concurrency: number,
|
|
744
|
+
worker: (item: TInput, index: number) => Promise<TOutput>,
|
|
745
|
+
): Promise<TOutput[]> {
|
|
746
|
+
const output: TOutput[] = new Array(items.length);
|
|
747
|
+
let cursor = 0;
|
|
748
|
+
|
|
749
|
+
async function runWorker(): Promise<void> {
|
|
750
|
+
while (true) {
|
|
751
|
+
const index = cursor;
|
|
752
|
+
cursor += 1;
|
|
753
|
+
|
|
754
|
+
if (index >= items.length) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
output[index] = await worker(items[index] as TInput, index);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const workers = Array.from(
|
|
763
|
+
{ length: Math.min(Math.max(concurrency, 1), items.length) },
|
|
764
|
+
() => runWorker(),
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
await Promise.all(workers);
|
|
768
|
+
return output;
|
|
769
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -21,11 +21,32 @@ export interface FlightQuery {
|
|
|
21
21
|
excludeBasic?: boolean;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface FlightMultiDateQuery {
|
|
25
|
+
origin: string;
|
|
26
|
+
destination: string;
|
|
27
|
+
departureDates: string[];
|
|
28
|
+
airlineCode?: string;
|
|
29
|
+
maxStops?: number;
|
|
30
|
+
maxPrice?: number;
|
|
31
|
+
departureAfterMinutes?: number;
|
|
32
|
+
departureBeforeMinutes?: number;
|
|
33
|
+
excludeBasic?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type FlightsQuery = FlightQuery | FlightMultiDateQuery;
|
|
37
|
+
|
|
38
|
+
export type HotelClass = 2 | 3 | 4 | 5;
|
|
39
|
+
|
|
24
40
|
export interface HotelQuery {
|
|
25
41
|
location: string;
|
|
26
42
|
checkInDate: string;
|
|
27
43
|
checkOutDate: string;
|
|
28
44
|
adults: number;
|
|
45
|
+
children: number;
|
|
46
|
+
childrenAges?: number[];
|
|
47
|
+
freeCancellation?: boolean;
|
|
48
|
+
hotelClasses?: HotelClass[];
|
|
49
|
+
minPrice?: number;
|
|
29
50
|
maxPrice?: number;
|
|
30
51
|
minRating?: 3.5 | 4 | 4.5 | 5;
|
|
31
52
|
}
|
|
@@ -49,7 +70,7 @@ export interface ParsedArgsFlights {
|
|
|
49
70
|
help: false;
|
|
50
71
|
mode: "flights";
|
|
51
72
|
outputJson: boolean;
|
|
52
|
-
query:
|
|
73
|
+
query: FlightsQuery;
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
export interface ParsedArgsHotels {
|
|
@@ -133,6 +154,16 @@ export interface FlightSearchResult {
|
|
|
133
154
|
googleFlightsUrl?: string;
|
|
134
155
|
}
|
|
135
156
|
|
|
157
|
+
export interface FlightDateResult {
|
|
158
|
+
date: string;
|
|
159
|
+
results: FlightOption[];
|
|
160
|
+
googleFlightsUrl?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface FlightMultiDateSearchResult {
|
|
164
|
+
resultsByDate: FlightDateResult[];
|
|
165
|
+
}
|
|
166
|
+
|
|
136
167
|
export interface FlightBookingLink {
|
|
137
168
|
url: string;
|
|
138
169
|
source?: string;
|