@tks/wayfinder 0.2.3 → 0.3.1

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
@@ -99,3 +99,33 @@ Structured hotel output for scripting:
99
99
  ```bash
100
100
  wayfinder hotels --where "Paris" --check-in 2026-04-10 --check-out 2026-04-12 --json | jq '.results[] | {name,nightlyPrice,rating}'
101
101
  ```
102
+
103
+ Search nearby restaurants from a location:
104
+
105
+ ```bash
106
+ wayfinder places --near "Shinjuku, Tokyo"
107
+ ```
108
+
109
+ Use a specific location name for better relevance:
110
+
111
+ ```bash
112
+ wayfinder places --near "Domino Park, Brooklyn, NY"
113
+ ```
114
+
115
+ Search nearby coffee spots:
116
+
117
+ ```bash
118
+ wayfinder places --near "Shinjuku, Tokyo" --type coffee --limit 5
119
+ ```
120
+
121
+ Narrow results to walking-distance intent:
122
+
123
+ ```bash
124
+ wayfinder places --near "Domino Park, Brooklyn, NY" --range walk
125
+ ```
126
+
127
+ Structured places output for scripting:
128
+
129
+ ```bash
130
+ wayfinder places --near "Shinjuku, Tokyo" --type coffee --json | jq '.results[] | {name,rating,reviews,googleMapsUrl}'
131
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tks/wayfinder",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "Travel search for your terminal and your AI agents",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { getConfigPath, readConfigApiKey, resolveApiKey, writeConfigApiKey } from "./config";
2
2
  import { CliError } from "./errors";
3
- import { renderFlightTable, renderHotelTable } from "./format";
3
+ import { renderFlightTable, renderHotelTable, renderPlaceTable } from "./format";
4
4
  import { parseCliArgs } from "./parse";
5
- import { searchFlightBookingOptions, searchFlights, searchHotels } from "./serpapi";
5
+ import { searchFlightBookingOptions, searchFlights, searchHotels, searchPlaces } from "./serpapi";
6
6
  import { ExitCode } from "./types";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
@@ -22,7 +22,7 @@ interface RunOptions {
22
22
  promptImpl?: (prompt: string) => Promise<string>;
23
23
  }
24
24
 
25
- const HELP_TEXT = `wayfinder v0.2.1 travel search
25
+ const HELP_TEXT = `wayfinder v0.3.1 travel search
26
26
 
27
27
  Usage:
28
28
  wayfinder setup [--reset]
@@ -30,6 +30,7 @@ Usage:
30
30
  wayfinder flights one-way --from SFO --to JFK --date 2026-03-21 [filters]
31
31
  wayfinder flights booking --from SFO --to JFK --date 2026-03-21 --token <BOOKING_TOKEN> [--token <BOOKING_TOKEN>] [--json]
32
32
  wayfinder hotels --where "New York, NY" --check-in 2026-03-21 --check-out 2026-03-23 [filters]
33
+ wayfinder places --near "Shinjuku, Tokyo" [--type restaurant|coffee] [--range walk] [--limit N] [--json]
33
34
 
34
35
  Setup:
35
36
  Runs interactive key setup and stores your SerpApi key in local config.
@@ -65,6 +66,15 @@ Hotels optional filters:
65
66
  --max-price <USD> Max nightly rate in USD
66
67
  --rating <3.5|4|4.5|5> Minimum guest rating
67
68
 
69
+ Places required:
70
+ --near <QUERY> Specific location query, example "Domino Park, Brooklyn, NY"
71
+ Broad names can return mixed-city results
72
+
73
+ Places optional filters:
74
+ --type <restaurant|coffee> Place type (default restaurant)
75
+ --range <walk> Heuristic nearby scope; "walk" narrows to walking-distance intent
76
+ --limit <N> Maximum number of results (default 10)
77
+
68
78
  Output:
69
79
  --json Print structured JSON output`;
70
80
 
@@ -181,7 +191,7 @@ export async function runWayfinder(
181
191
  } else {
182
192
  output.stdout(renderFlightBookingText(flightLinks));
183
193
  }
184
- } else {
194
+ } else if (parsed.mode === "hotels") {
185
195
  const hotels = await searchHotels(parsed.query, apiKey, options.fetchImpl ?? fetch);
186
196
 
187
197
  if (hotels.length === 0) {
@@ -202,6 +212,27 @@ export async function runWayfinder(
202
212
  } else {
203
213
  output.stdout(renderHotelTable(hotels));
204
214
  }
215
+ } else {
216
+ const places = await searchPlaces(parsed.query, apiKey, options.fetchImpl ?? fetch);
217
+
218
+ if (places.length === 0) {
219
+ throw new CliError("No places found for the selected query", ExitCode.NoResults);
220
+ }
221
+
222
+ if (parsed.outputJson) {
223
+ output.stdout(
224
+ JSON.stringify(
225
+ {
226
+ query: parsed.query,
227
+ results: places,
228
+ },
229
+ null,
230
+ 2,
231
+ ),
232
+ );
233
+ } else {
234
+ output.stdout(renderPlaceTable(places));
235
+ }
205
236
  }
206
237
 
207
238
  return ExitCode.Success;
package/src/format.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FlightOption, HotelOption } from "./types";
1
+ import { FlightOption, HotelOption, PlaceOption } from "./types";
2
2
 
3
3
  const currencyFormatter = new Intl.NumberFormat("en-US", {
4
4
  style: "currency",
@@ -142,6 +142,74 @@ export function renderHotelTable(options: HotelOption[]): string {
142
142
  return lines.join("\n");
143
143
  }
144
144
 
145
+ export function renderPlaceTable(options: PlaceOption[]): string {
146
+ const rows = options.map((option) => ({
147
+ name: option.name,
148
+ type: option.category,
149
+ rating: typeof option.rating === "number" ? option.rating.toFixed(1) : "n/a",
150
+ reviews: typeof option.reviews === "number" ? String(option.reviews) : "n/a",
151
+ distance: formatDistance(option.distanceMeters),
152
+ address: option.address ?? "n/a",
153
+ }));
154
+
155
+ const headers = {
156
+ name: "NAME",
157
+ type: "TYPE",
158
+ rating: "RATING",
159
+ reviews: "REVIEWS",
160
+ distance: "DISTANCE",
161
+ address: "ADDRESS",
162
+ };
163
+
164
+ const widths = {
165
+ name: maxWidth(rows, "name", headers.name),
166
+ type: maxWidth(rows, "type", headers.type),
167
+ rating: maxWidth(rows, "rating", headers.rating),
168
+ reviews: maxWidth(rows, "reviews", headers.reviews),
169
+ distance: maxWidth(rows, "distance", headers.distance),
170
+ address: maxWidth(rows, "address", headers.address),
171
+ };
172
+
173
+ const lines: string[] = [];
174
+
175
+ lines.push(
176
+ [
177
+ headers.name.padEnd(widths.name),
178
+ headers.type.padEnd(widths.type),
179
+ headers.rating.padEnd(widths.rating),
180
+ headers.reviews.padEnd(widths.reviews),
181
+ headers.distance.padEnd(widths.distance),
182
+ headers.address.padEnd(widths.address),
183
+ ].join(" "),
184
+ );
185
+
186
+ lines.push(
187
+ [
188
+ "-".repeat(widths.name),
189
+ "-".repeat(widths.type),
190
+ "-".repeat(widths.rating),
191
+ "-".repeat(widths.reviews),
192
+ "-".repeat(widths.distance),
193
+ "-".repeat(widths.address),
194
+ ].join(" "),
195
+ );
196
+
197
+ for (const row of rows) {
198
+ lines.push(
199
+ [
200
+ row.name.padEnd(widths.name),
201
+ row.type.padEnd(widths.type),
202
+ row.rating.padEnd(widths.rating),
203
+ row.reviews.padEnd(widths.reviews),
204
+ row.distance.padEnd(widths.distance),
205
+ row.address.padEnd(widths.address),
206
+ ].join(" "),
207
+ );
208
+ }
209
+
210
+ return lines.join("\n");
211
+ }
212
+
145
213
  function maxWidth(rows: Array<Record<string, string>>, key: string, header: string): number {
146
214
  return rows.reduce((width, row) => Math.max(width, row[key].length), header.length);
147
215
  }
@@ -164,3 +232,16 @@ function formatDuration(totalMinutes: number): string {
164
232
 
165
233
  return `${hours}h ${minutes}m`;
166
234
  }
235
+
236
+ function formatDistance(distanceMeters?: number): string {
237
+ if (!Number.isFinite(distanceMeters)) {
238
+ return "n/a";
239
+ }
240
+
241
+ if ((distanceMeters as number) < 1000) {
242
+ return `${Math.round(distanceMeters as number)}m`;
243
+ }
244
+
245
+ const km = (distanceMeters as number) / 1000;
246
+ return `${km.toFixed(1)}km`;
247
+ }
package/src/parse.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import { CliError } from "./errors";
2
- import { ExitCode, FlightBookingQuery, FlightQuery, HotelQuery, ParsedArgs } from "./types";
2
+ import {
3
+ ExitCode,
4
+ FlightBookingQuery,
5
+ FlightQuery,
6
+ HotelQuery,
7
+ ParsedArgs,
8
+ PlaceQuery,
9
+ PlaceRange,
10
+ PlaceType,
11
+ } from "./types";
3
12
 
4
13
  interface FlightRawOptions {
5
14
  from?: string;
@@ -35,7 +44,16 @@ interface FlightBookingRawOptions {
35
44
  help: boolean;
36
45
  }
37
46
 
38
- type SearchMode = "flights" | "hotels" | "flight-booking" | "setup";
47
+ interface PlaceRawOptions {
48
+ near?: string;
49
+ type?: string;
50
+ limit?: string;
51
+ range?: string;
52
+ outputJson: boolean;
53
+ help: boolean;
54
+ }
55
+
56
+ type SearchMode = "flights" | "hotels" | "places" | "flight-booking" | "setup";
39
57
 
40
58
  const HELP_FLAGS = new Set(["-h", "--help"]);
41
59
 
@@ -57,6 +75,10 @@ export function parseCliArgs(argv: string[]): ParsedArgs {
57
75
  return parseFlightBookingArgs(args);
58
76
  }
59
77
 
78
+ if (mode === "places") {
79
+ return parsePlacesArgs(args);
80
+ }
81
+
60
82
  if (mode === "setup") {
61
83
  return parseSetupArgs(args);
62
84
  }
@@ -281,6 +303,69 @@ function parseFlightBookingArgs(args: string[]): ParsedArgs {
281
303
  };
282
304
  }
283
305
 
306
+ function parsePlacesArgs(args: string[]): ParsedArgs {
307
+ const raw: PlaceRawOptions = {
308
+ outputJson: false,
309
+ help: false,
310
+ };
311
+
312
+ for (let i = 0; i < args.length; i += 1) {
313
+ const token = args[i];
314
+
315
+ if (HELP_FLAGS.has(token)) {
316
+ raw.help = true;
317
+ continue;
318
+ }
319
+
320
+ if (token === "--json") {
321
+ raw.outputJson = true;
322
+ continue;
323
+ }
324
+
325
+ if (!token.startsWith("--")) {
326
+ throw new CliError(`Unexpected argument: ${token}`, ExitCode.InvalidInput);
327
+ }
328
+
329
+ const value = args[i + 1];
330
+ if (!value || value.startsWith("--")) {
331
+ throw new CliError(`Missing value for ${token}`, ExitCode.InvalidInput);
332
+ }
333
+
334
+ switch (token) {
335
+ case "--near":
336
+ raw.near = value;
337
+ break;
338
+ case "--type":
339
+ raw.type = value;
340
+ break;
341
+ case "--limit":
342
+ raw.limit = value;
343
+ break;
344
+ case "--range":
345
+ raw.range = value;
346
+ break;
347
+ default:
348
+ throw new CliError(`Unknown flag: ${token}`, ExitCode.InvalidInput);
349
+ }
350
+
351
+ i += 1;
352
+ }
353
+
354
+ if (raw.help) {
355
+ return {
356
+ help: true,
357
+ outputJson: raw.outputJson,
358
+ };
359
+ }
360
+
361
+ return {
362
+ help: false,
363
+ mode: "places",
364
+ outputJson: raw.outputJson,
365
+ query: buildPlaceQuery(raw),
366
+ };
367
+ }
368
+
284
369
  function parseSetupArgs(args: string[]): ParsedArgs {
285
370
  const outputJson = args.includes("--json");
286
371
  const reset = args.includes("--reset");
@@ -409,6 +494,24 @@ function buildFlightBookingQuery(raw: FlightBookingRawOptions): FlightBookingQue
409
494
  };
410
495
  }
411
496
 
497
+ function buildPlaceQuery(raw: PlaceRawOptions): PlaceQuery {
498
+ if (!raw.near) {
499
+ throw new CliError("Missing required flag: --near", ExitCode.InvalidInput);
500
+ }
501
+
502
+ const near = normalizeLocation(raw.near);
503
+ const type = raw.type ? normalizePlaceType(raw.type) : "restaurant";
504
+ const limit = raw.limit ? normalizeLimit(raw.limit, "--limit") : 10;
505
+ const range = raw.range ? normalizePlaceRange(raw.range) : undefined;
506
+
507
+ return {
508
+ near,
509
+ type,
510
+ limit,
511
+ range,
512
+ };
513
+ }
514
+
412
515
  function stripSubcommands(argv: string[]): { mode: SearchMode; args: string[] } {
413
516
  const args = [...argv];
414
517
  if (args[0] === "hotels") {
@@ -435,8 +538,13 @@ function stripSubcommands(argv: string[]): { mode: SearchMode; args: string[] }
435
538
  return { mode: "setup", args };
436
539
  }
437
540
 
541
+ if (args[0] === "places") {
542
+ args.shift();
543
+ return { mode: "places", args };
544
+ }
545
+
438
546
  throw new CliError(
439
- "Missing subcommand: use `setup`, `flights`, or `hotels`",
547
+ "Missing subcommand: use `setup`, `flights`, `hotels`, or `places`",
440
548
  ExitCode.InvalidInput,
441
549
  );
442
550
  }
@@ -521,6 +629,15 @@ function normalizeMaxPrice(value: string): number {
521
629
  return numeric;
522
630
  }
523
631
 
632
+ function normalizeLimit(value: string, flagName: string): number {
633
+ const numeric = Number.parseInt(value, 10);
634
+ if (!Number.isInteger(numeric) || numeric <= 0) {
635
+ throw new CliError(`${flagName} must be a positive integer`, ExitCode.InvalidInput);
636
+ }
637
+
638
+ return numeric;
639
+ }
640
+
524
641
  function normalizeAdults(value: string): number {
525
642
  const numeric = Number.parseInt(value, 10);
526
643
  if (!Number.isInteger(numeric) || numeric <= 0) {
@@ -539,6 +656,24 @@ function normalizeMinRating(value: string): 3.5 | 4 | 4.5 | 5 {
539
656
  return numeric;
540
657
  }
541
658
 
659
+ function normalizePlaceType(value: string): PlaceType {
660
+ const normalized = value.trim().toLowerCase();
661
+ if (normalized !== "restaurant" && normalized !== "coffee") {
662
+ throw new CliError("--type must be one of: restaurant, coffee", ExitCode.InvalidInput);
663
+ }
664
+
665
+ return normalized;
666
+ }
667
+
668
+ function normalizePlaceRange(value: string): PlaceRange {
669
+ const normalized = value.trim().toLowerCase();
670
+ if (normalized !== "walk") {
671
+ throw new CliError("--range must be: walk", ExitCode.InvalidInput);
672
+ }
673
+
674
+ return normalized;
675
+ }
676
+
542
677
  function normalizeTime(value: string, flagName: string): number {
543
678
  const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
544
679
  if (!match) {
package/src/serpapi.ts CHANGED
@@ -8,6 +8,9 @@ import {
8
8
  FlightSearchResult,
9
9
  HotelOption,
10
10
  HotelQuery,
11
+ PlaceOption,
12
+ PlaceQuery,
13
+ PlaceType,
11
14
  } from "./types";
12
15
 
13
16
  interface SerpApiAirport {
@@ -80,6 +83,23 @@ interface SerpApiHotelsResponse {
80
83
  properties?: SerpApiHotelProperty[];
81
84
  }
82
85
 
86
+ interface SerpApiLocalResult {
87
+ title?: string;
88
+ rating?: number;
89
+ reviews?: number;
90
+ address?: string;
91
+ distance?: string;
92
+ open_state?: string;
93
+ type?: string;
94
+ place_id_search?: string;
95
+ data_id?: string;
96
+ }
97
+
98
+ interface SerpApiPlacesResponse {
99
+ error?: string;
100
+ local_results?: SerpApiLocalResult[];
101
+ }
102
+
83
103
  export async function searchFlights(
84
104
  query: FlightQuery,
85
105
  apiKey: string,
@@ -158,6 +178,25 @@ export async function searchHotels(
158
178
  return hotels;
159
179
  }
160
180
 
181
+ export async function searchPlaces(
182
+ query: PlaceQuery,
183
+ apiKey: string,
184
+ fetchImpl: typeof fetch = fetch,
185
+ ): Promise<PlaceOption[]> {
186
+ const payload = await fetchSerpApiJson<SerpApiPlacesResponse>(
187
+ buildPlaceRequestUrl(query, apiKey),
188
+ fetchImpl,
189
+ );
190
+
191
+ if (typeof payload.error === "string" && payload.error.trim() !== "") {
192
+ throw new CliError(`SerpApi error: ${payload.error}`, ExitCode.ApiFailure);
193
+ }
194
+
195
+ const places = shapePlaceSerpApiResponse(payload, query.type);
196
+ places.sort((a, b) => b.score - a.score);
197
+ return places.slice(0, query.limit);
198
+ }
199
+
161
200
  export function shapeSerpApiResponse(payload: SerpApiFlightsResponse): FlightOption[] {
162
201
  const merged = [...(payload.best_flights ?? []), ...(payload.other_flights ?? [])];
163
202
 
@@ -172,6 +211,15 @@ export function shapeHotelSerpApiResponse(payload: SerpApiHotelsResponse): Hotel
172
211
  .filter((option): option is HotelOption => option !== null);
173
212
  }
174
213
 
214
+ export function shapePlaceSerpApiResponse(
215
+ payload: SerpApiPlacesResponse,
216
+ category: PlaceType,
217
+ ): PlaceOption[] {
218
+ return (payload.local_results ?? [])
219
+ .map((result) => shapeLocalResult(result, category))
220
+ .filter((option): option is PlaceOption => option !== null);
221
+ }
222
+
175
223
  export function filterByDepartureWindow(
176
224
  options: FlightOption[],
177
225
  minMinutes: number,
@@ -336,6 +384,18 @@ function buildHotelRequestUrl(query: HotelQuery, apiKey: string): string {
336
384
  return url.toString();
337
385
  }
338
386
 
387
+ function buildPlaceRequestUrl(query: PlaceQuery, apiKey: string): string {
388
+ const url = new URL("https://serpapi.com/search.json");
389
+ const searchTerm = query.type === "coffee" ? "coffee" : "restaurants";
390
+ const rangePrefix = query.range === "walk" ? "within walking distance " : "";
391
+
392
+ url.searchParams.set("engine", "google_maps");
393
+ url.searchParams.set("type", "search");
394
+ url.searchParams.set("q", `${rangePrefix}${searchTerm} near ${query.near}`);
395
+ url.searchParams.set("api_key", apiKey);
396
+ return url.toString();
397
+ }
398
+
339
399
  function buildFlightBookingOptionsRequestUrl(
340
400
  query: FlightBookingQuery,
341
401
  token: string,
@@ -383,6 +443,101 @@ function toSerpApiRating(rating: 3.5 | 4 | 4.5 | 5): string {
383
443
  return "10";
384
444
  }
385
445
 
446
+ function shapeLocalResult(result: SerpApiLocalResult, category: PlaceType): PlaceOption | null {
447
+ if (typeof result.title !== "string" || result.title.trim() === "") {
448
+ return null;
449
+ }
450
+
451
+ const rating = Number.isFinite(result.rating) ? (result.rating as number) : undefined;
452
+ const reviews = Number.isFinite(result.reviews) ? (result.reviews as number) : undefined;
453
+ const distanceMeters = parseDistanceMeters(result.distance);
454
+ const link = buildGoogleMapsPlaceLink(result);
455
+ const googleMapsUrl = buildDirectGoogleMapsUrl(result);
456
+
457
+ return {
458
+ name: result.title.trim(),
459
+ category,
460
+ rating,
461
+ reviews,
462
+ address: typeof result.address === "string" ? result.address : undefined,
463
+ distanceMeters,
464
+ openState: typeof result.open_state === "string" ? result.open_state : undefined,
465
+ link,
466
+ googleMapsUrl,
467
+ score: computePlaceScore(rating, reviews, distanceMeters),
468
+ };
469
+ }
470
+
471
+ function computePlaceScore(
472
+ rating: number | undefined,
473
+ reviews: number | undefined,
474
+ distanceMeters: number | undefined,
475
+ ): number {
476
+ const ratingComponent = typeof rating === "number" ? rating / 5 : 0;
477
+ const reviewComponent =
478
+ typeof reviews === "number" && reviews > 0 ? Math.log10(reviews + 1) / 4 : 0;
479
+ const distanceComponent =
480
+ typeof distanceMeters === "number" && distanceMeters >= 0
481
+ ? Math.max(0, 1 - distanceMeters / 10_000)
482
+ : 0;
483
+
484
+ return ratingComponent * 0.6 + reviewComponent * 0.25 + distanceComponent * 0.15;
485
+ }
486
+
487
+ function parseDistanceMeters(value?: string): number | undefined {
488
+ if (typeof value !== "string" || value.trim() === "") {
489
+ return undefined;
490
+ }
491
+
492
+ const normalized = value.trim().toLowerCase().replace(/,/g, "");
493
+ const match = /^(\d+(?:\.\d+)?)\s*(m|meter|meters|km|mi)$/.exec(normalized);
494
+ if (!match) {
495
+ return undefined;
496
+ }
497
+
498
+ const amount = Number(match[1]);
499
+ const unit = match[2];
500
+ if (!Number.isFinite(amount)) {
501
+ return undefined;
502
+ }
503
+
504
+ if (unit === "m" || unit === "meter" || unit === "meters") {
505
+ return amount;
506
+ }
507
+
508
+ if (unit === "km") {
509
+ return amount * 1000;
510
+ }
511
+
512
+ return amount * 1609.34;
513
+ }
514
+
515
+ function buildGoogleMapsPlaceLink(result: SerpApiLocalResult): string | undefined {
516
+ if (typeof result.place_id_search === "string" && result.place_id_search.trim() !== "") {
517
+ return result.place_id_search;
518
+ }
519
+
520
+ return undefined;
521
+ }
522
+
523
+ function buildDirectGoogleMapsUrl(result: SerpApiLocalResult): string | undefined {
524
+ if (typeof result.place_id_search !== "string" || result.place_id_search.trim() === "") {
525
+ return undefined;
526
+ }
527
+
528
+ try {
529
+ const parsed = new URL(result.place_id_search);
530
+ const placeId = parsed.searchParams.get("place_id");
531
+ if (!placeId || placeId.trim() === "") {
532
+ return undefined;
533
+ }
534
+
535
+ return `https://www.google.com/maps/place/?q=place_id:${encodeURIComponent(placeId)}`;
536
+ } catch {
537
+ return undefined;
538
+ }
539
+ }
540
+
386
541
  async function fetchSerpApiJson<T>(
387
542
  requestUrl: string,
388
543
  fetchImpl: typeof fetch,
package/src/types.ts CHANGED
@@ -30,6 +30,16 @@ export interface HotelQuery {
30
30
  minRating?: 3.5 | 4 | 4.5 | 5;
31
31
  }
32
32
 
33
+ export type PlaceType = "restaurant" | "coffee";
34
+ export type PlaceRange = "walk";
35
+
36
+ export interface PlaceQuery {
37
+ near: string;
38
+ type: PlaceType;
39
+ limit: number;
40
+ range?: PlaceRange;
41
+ }
42
+
33
43
  export interface ParsedArgsHelp {
34
44
  help: true;
35
45
  outputJson: boolean;
@@ -70,10 +80,18 @@ export interface ParsedArgsSetup {
70
80
  reset: boolean;
71
81
  }
72
82
 
83
+ export interface ParsedArgsPlaces {
84
+ help: false;
85
+ mode: "places";
86
+ outputJson: boolean;
87
+ query: PlaceQuery;
88
+ }
89
+
73
90
  export type ParsedArgs =
74
91
  | ParsedArgsHelp
75
92
  | ParsedArgsFlights
76
93
  | ParsedArgsHotels
94
+ | ParsedArgsPlaces
77
95
  | ParsedArgsFlightBooking
78
96
  | ParsedArgsSetup;
79
97
 
@@ -97,6 +115,19 @@ export interface HotelOption {
97
115
  link?: string;
98
116
  }
99
117
 
118
+ export interface PlaceOption {
119
+ name: string;
120
+ category: PlaceType;
121
+ rating?: number;
122
+ reviews?: number;
123
+ address?: string;
124
+ distanceMeters?: number;
125
+ openState?: string;
126
+ link?: string;
127
+ googleMapsUrl?: string;
128
+ score: number;
129
+ }
130
+
100
131
  export interface FlightSearchResult {
101
132
  options: FlightOption[];
102
133
  googleFlightsUrl?: string;