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 +24 -0
- package/dist/{chunk-2SNO3AGS.mjs → chunk-QLCIHPPX.mjs} +21 -0
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +21 -0
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
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
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letsfg",
|
|
3
|
-
"version": "2026.5.
|
|
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",
|