letsfg 2026.5.66 → 2026.5.68

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
@@ -86,6 +86,19 @@ and `idempotencyKey`).
86
86
  - `offerSummary(offer)` — One-line string summary
87
87
  - `cheapestOffer(result)` — Get cheapest offer from search
88
88
 
89
+ ## Starlink Wi-Fi
90
+
91
+ Offers may carry `starlink`: `confirmed_all` / `confirmed_some` mean the carrier
92
+ has **fully** fitted that aircraft type; `likely_all` / `likely_some` mean the
93
+ rollout on that type is underway but incomplete. Segments carry `confirmed` or
94
+ `likely`.
95
+
96
+ Only `confirmed_*` is safe to state as fact — `likely_*` is a signal, not a
97
+ promise. Anything ending `_some` has at least one leg without it. An **absent**
98
+ field means no information, **not** an absence of Wi-Fi.
99
+
100
+ Full semantics: [docs/api-search.md](https://github.com/LetsFG/LetsFG/blob/main/docs/api-search.md#starlink-wi-fi).
101
+
89
102
  ## Zero Dependencies
90
103
 
91
104
  Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
@@ -150,6 +163,17 @@ else; after it, the hotel's own cancellation ladder applies and can reach 100%.
150
163
  That ladder ships in the booking's `terms`, so you can always see the cost before
151
164
  you cancel.
152
165
 
166
+ ### What search costs
167
+
168
+ Search is metered separately from booking, on **either** auth path (free PFS
169
+ Bearer token or Developer API key — both count against the same agent):
170
+ **the first 1,000 `search_hotels` calls since your last hotel booking are
171
+ free.** Past that, searches are billed in blocks of 1,000 for **$5**
172
+ (~$0.005/search) from your prepaid balance — refused with a 402 if the
173
+ balance can't cover the next block, never silently allowed. Book a hotel
174
+ and the count resets to zero. Resolving a city name (`hotel_destinations`)
175
+ is not metered, only the search call itself.
176
+
153
177
  ### Things worth knowing before you build
154
178
 
155
179
  - **A card on file is required for every hotel call, including search.** That is
@@ -87,6 +87,23 @@ function collectFlightNumbers(offer) {
87
87
  offer.flight_number
88
88
  ]);
89
89
  }
90
+ function buildStarlinkNote(offer) {
91
+ const segments = collectOfferSegments(offer);
92
+ const label = (s) => s.origin && s.destination ? `${s.origin}\u2192${s.destination}` : (s.flight_number || "").trim();
93
+ const confirmed = segments.filter((s) => s.starlink === "confirmed");
94
+ const likely = segments.filter((s) => s.starlink === "likely");
95
+ if (confirmed.length === 0 && likely.length === 0) return void 0;
96
+ const parts = [];
97
+ if (confirmed.length > 0) {
98
+ const where = confirmed.length === segments.length ? "every leg" : confirmed.map(label).filter(Boolean).join(", ");
99
+ parts.push(`Starlink Wi-Fi on ${where}`);
100
+ }
101
+ if (likely.length > 0) {
102
+ const where = likely.map(label).filter(Boolean).join(", ");
103
+ parts.push(`Starlink being installed on this aircraft type${where ? ` (${where})` : ""}, not guaranteed`);
104
+ }
105
+ return parts.join("; ");
106
+ }
90
107
  function collectAircraftTypes(offer) {
91
108
  return collectUniqueDetailValues(
92
109
  collectOfferSegments(offer).map((segment) => segment.aircraft?.replace(/\s*\([^)]*\)/g, "").trim())
@@ -428,6 +445,10 @@ function getOfferDetailPromptNotes(offer) {
428
445
  if (aircraftTypes.length > 0) {
429
446
  pushUniqueNote(notes, `Aircraft shown: ${aircraftTypes.join(", ")}`);
430
447
  }
448
+ const starlinkNote = buildStarlinkNote(offer);
449
+ if (starlinkNote) {
450
+ pushUniqueNote(notes, starlinkNote);
451
+ }
431
452
  const operatingCarriers = collectOperatingCarriers(offer);
432
453
  const normalizedOfferAirline = typeof offer.airline === "string" ? offer.airline.trim().toLowerCase() : "";
433
454
  if (operatingCarriers.length === 1 && operatingCarriers[0].toLowerCase() !== normalizedOfferAirline) {
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  LetsFG,
4
4
  LetsFGError,
5
5
  offerSummary
6
- } from "./chunk-2SNO3AGS.mjs";
6
+ } from "./chunk-QLCIHPPX.mjs";
7
7
  import {
8
8
  BearerTokenError,
9
9
  getBearerToken,
package/dist/index.d.mts CHANGED
@@ -212,6 +212,10 @@ interface OfferDetailSegmentLike {
212
212
  airline?: string;
213
213
  flight_number?: string;
214
214
  aircraft?: string;
215
+ origin?: string;
216
+ destination?: string;
217
+ /** Per-leg Starlink verdict; see StarlinkSegmentVerdict in index.ts. */
218
+ starlink?: 'confirmed' | 'likely';
215
219
  }
216
220
  interface OfferDetailLegLike {
217
221
  segments?: OfferDetailSegmentLike[];
@@ -296,7 +300,17 @@ interface FlightSegment {
296
300
  duration_seconds: number;
297
301
  cabin_class: string;
298
302
  aircraft: string;
303
+ /** Starlink Wi-Fi on this leg. 'confirmed' = the carrier has fitted every
304
+ * aircraft of this type. 'likely' = installation on this type is underway
305
+ * but incomplete, so this airframe may not have it. Undefined means no
306
+ * information — NOT an absence of Wi-Fi. */
307
+ starlink?: StarlinkSegmentVerdict;
299
308
  }
309
+ type StarlinkSegmentVerdict = 'confirmed' | 'likely';
310
+ /** Itinerary-level Starlink verdict. '*_some' means at least one leg has none;
311
+ * 'likely_*' means the rollout on that subfleet is underway but incomplete.
312
+ * Only 'confirmed_*' should be shown to an end user as a fact. */
313
+ type StarlinkOfferVerdict = 'confirmed_all' | 'confirmed_some' | 'likely_all' | 'likely_some';
300
314
  interface FlightRoute {
301
315
  segments: FlightSegment[];
302
316
  total_duration_seconds: number;
@@ -312,6 +326,9 @@ interface FlightOffer {
312
326
  airlines: string[];
313
327
  owner_airline: string;
314
328
  bags_price: Record<string, number>;
329
+ /** Starlink Wi-Fi across the whole itinerary; per-leg detail is on each
330
+ * segment's `starlink`. Undefined when no leg has any information. */
331
+ starlink?: StarlinkOfferVerdict;
315
332
  availability_seats: number | null;
316
333
  conditions: Record<string, string>;
317
334
  is_locked: boolean;
@@ -628,4 +645,4 @@ declare const BoostedTravel: typeof LetsFG;
628
645
  declare const BoostedTravelError: typeof LetsFGError;
629
646
  type BoostedTravelConfig = LetsFGConfig;
630
647
 
631
- export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, ErrorCategory, type ErrorCategoryType, ErrorCode, type ErrorCodeType, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, LetsFG, type LetsFGConfig, LetsFGError, type OfferDetailSignals, OfferExpiredError, type Passenger, PaymentRequiredError, type RankOffer, type RankedOffer, type RankingContext, type ScoreBreakdown, type SearchOptions, TRIP_PURPOSES, type TripPurpose, type TripPurposeOptions, type UnlockResult, ValidationError, cheapestOffer, deduplicateOffers, LetsFG as default, extractOfferDetailSignals, getOfferDetailBadges, getOfferDetailPromptNotes, getPrimaryTripPurpose, getProfileLabel, normalizeTripPurposes, offerSummary, rankOffers, selectDiverseTop };
648
+ export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, ErrorCategory, type ErrorCategoryType, ErrorCode, type ErrorCodeType, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, LetsFG, type LetsFGConfig, LetsFGError, type OfferDetailSignals, OfferExpiredError, type Passenger, PaymentRequiredError, type RankOffer, type RankedOffer, type RankingContext, type ScoreBreakdown, type SearchOptions, type StarlinkOfferVerdict, type StarlinkSegmentVerdict, TRIP_PURPOSES, type TripPurpose, type TripPurposeOptions, type UnlockResult, ValidationError, cheapestOffer, deduplicateOffers, LetsFG as default, extractOfferDetailSignals, getOfferDetailBadges, getOfferDetailPromptNotes, getPrimaryTripPurpose, getProfileLabel, normalizeTripPurposes, offerSummary, rankOffers, selectDiverseTop };
package/dist/index.d.ts CHANGED
@@ -212,6 +212,10 @@ interface OfferDetailSegmentLike {
212
212
  airline?: string;
213
213
  flight_number?: string;
214
214
  aircraft?: string;
215
+ origin?: string;
216
+ destination?: string;
217
+ /** Per-leg Starlink verdict; see StarlinkSegmentVerdict in index.ts. */
218
+ starlink?: 'confirmed' | 'likely';
215
219
  }
216
220
  interface OfferDetailLegLike {
217
221
  segments?: OfferDetailSegmentLike[];
@@ -296,7 +300,17 @@ interface FlightSegment {
296
300
  duration_seconds: number;
297
301
  cabin_class: string;
298
302
  aircraft: string;
303
+ /** Starlink Wi-Fi on this leg. 'confirmed' = the carrier has fitted every
304
+ * aircraft of this type. 'likely' = installation on this type is underway
305
+ * but incomplete, so this airframe may not have it. Undefined means no
306
+ * information — NOT an absence of Wi-Fi. */
307
+ starlink?: StarlinkSegmentVerdict;
299
308
  }
309
+ type StarlinkSegmentVerdict = 'confirmed' | 'likely';
310
+ /** Itinerary-level Starlink verdict. '*_some' means at least one leg has none;
311
+ * 'likely_*' means the rollout on that subfleet is underway but incomplete.
312
+ * Only 'confirmed_*' should be shown to an end user as a fact. */
313
+ type StarlinkOfferVerdict = 'confirmed_all' | 'confirmed_some' | 'likely_all' | 'likely_some';
300
314
  interface FlightRoute {
301
315
  segments: FlightSegment[];
302
316
  total_duration_seconds: number;
@@ -312,6 +326,9 @@ interface FlightOffer {
312
326
  airlines: string[];
313
327
  owner_airline: string;
314
328
  bags_price: Record<string, number>;
329
+ /** Starlink Wi-Fi across the whole itinerary; per-leg detail is on each
330
+ * segment's `starlink`. Undefined when no leg has any information. */
331
+ starlink?: StarlinkOfferVerdict;
315
332
  availability_seats: number | null;
316
333
  conditions: Record<string, string>;
317
334
  is_locked: boolean;
@@ -628,4 +645,4 @@ declare const BoostedTravel: typeof LetsFG;
628
645
  declare const BoostedTravelError: typeof LetsFGError;
629
646
  type BoostedTravelConfig = LetsFGConfig;
630
647
 
631
- export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, ErrorCategory, type ErrorCategoryType, ErrorCode, type ErrorCodeType, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, LetsFG, type LetsFGConfig, LetsFGError, type OfferDetailSignals, OfferExpiredError, type Passenger, PaymentRequiredError, type RankOffer, type RankedOffer, type RankingContext, type ScoreBreakdown, type SearchOptions, TRIP_PURPOSES, type TripPurpose, type TripPurposeOptions, type UnlockResult, ValidationError, cheapestOffer, deduplicateOffers, LetsFG as default, extractOfferDetailSignals, getOfferDetailBadges, getOfferDetailPromptNotes, getPrimaryTripPurpose, getProfileLabel, normalizeTripPurposes, offerSummary, rankOffers, selectDiverseTop };
648
+ export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, ErrorCategory, type ErrorCategoryType, ErrorCode, type ErrorCodeType, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, LetsFG, type LetsFGConfig, LetsFGError, type OfferDetailSignals, OfferExpiredError, type Passenger, PaymentRequiredError, type RankOffer, type RankedOffer, type RankingContext, type ScoreBreakdown, type SearchOptions, type StarlinkOfferVerdict, type StarlinkSegmentVerdict, TRIP_PURPOSES, type TripPurpose, type TripPurposeOptions, type UnlockResult, ValidationError, cheapestOffer, deduplicateOffers, LetsFG as default, extractOfferDetailSignals, getOfferDetailBadges, getOfferDetailPromptNotes, getPrimaryTripPurpose, getProfileLabel, normalizeTripPurposes, offerSummary, rankOffers, selectDiverseTop };
package/dist/index.js CHANGED
@@ -135,6 +135,23 @@ function collectFlightNumbers(offer) {
135
135
  offer.flight_number
136
136
  ]);
137
137
  }
138
+ function buildStarlinkNote(offer) {
139
+ const segments = collectOfferSegments(offer);
140
+ const label = (s) => s.origin && s.destination ? `${s.origin}\u2192${s.destination}` : (s.flight_number || "").trim();
141
+ const confirmed = segments.filter((s) => s.starlink === "confirmed");
142
+ const likely = segments.filter((s) => s.starlink === "likely");
143
+ if (confirmed.length === 0 && likely.length === 0) return void 0;
144
+ const parts = [];
145
+ if (confirmed.length > 0) {
146
+ const where = confirmed.length === segments.length ? "every leg" : confirmed.map(label).filter(Boolean).join(", ");
147
+ parts.push(`Starlink Wi-Fi on ${where}`);
148
+ }
149
+ if (likely.length > 0) {
150
+ const where = likely.map(label).filter(Boolean).join(", ");
151
+ parts.push(`Starlink being installed on this aircraft type${where ? ` (${where})` : ""}, not guaranteed`);
152
+ }
153
+ return parts.join("; ");
154
+ }
138
155
  function collectAircraftTypes(offer) {
139
156
  return collectUniqueDetailValues(
140
157
  collectOfferSegments(offer).map((segment) => segment.aircraft?.replace(/\s*\([^)]*\)/g, "").trim())
@@ -476,6 +493,10 @@ function getOfferDetailPromptNotes(offer) {
476
493
  if (aircraftTypes.length > 0) {
477
494
  pushUniqueNote(notes, `Aircraft shown: ${aircraftTypes.join(", ")}`);
478
495
  }
496
+ const starlinkNote = buildStarlinkNote(offer);
497
+ if (starlinkNote) {
498
+ pushUniqueNote(notes, starlinkNote);
499
+ }
479
500
  const operatingCarriers = collectOperatingCarriers(offer);
480
501
  const normalizedOfferAirline = typeof offer.airline === "string" ? offer.airline.trim().toLowerCase() : "";
481
502
  if (operatingCarriers.length === 1 && operatingCarriers[0].toLowerCase() !== normalizedOfferAirline) {
package/dist/index.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  offerSummary,
23
23
  rankOffers,
24
24
  selectDiverseTop
25
- } from "./chunk-2SNO3AGS.mjs";
25
+ } from "./chunk-QLCIHPPX.mjs";
26
26
  export {
27
27
  AuthenticationError,
28
28
  BoostedTravel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letsfg",
3
- "version": "2026.5.66",
3
+ "version": "2026.5.68",
4
4
  "description": "Flights and hotels for AI agents. Server-side engine covers hundreds of airlines; hotels are real bookable inventory with free cancellation and pay-later terms. Includes open-source ranking engine.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",