letsfg 2026.5.53 → 2026.5.55

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/dist/index.d.ts CHANGED
@@ -1,19 +1,286 @@
1
+ type TripPurpose = 'honeymoon' | 'special_occasion' | 'business' | 'ski' | 'beach' | 'city_break' | 'family_holiday' | 'graduation' | 'concert_festival' | 'sports_event' | 'spring_break';
2
+ declare const TRIP_PURPOSES: readonly TripPurpose[];
3
+ interface TripPurposeOptions {
4
+ tripPurpose?: TripPurpose | null;
5
+ tripPurposes?: ReadonlyArray<TripPurpose | null | undefined> | null;
6
+ }
7
+ declare function normalizeTripPurposes({ tripPurpose, tripPurposes }: TripPurposeOptions): TripPurpose[];
8
+ declare function getPrimaryTripPurpose(options: TripPurposeOptions): TripPurpose | undefined;
9
+
10
+ /**
11
+ * LetsFG Personalized Flight Ranking Engine
12
+ *
13
+ * Open-source implementation of the scoring algorithm that powers letsfg.co.
14
+ * Scores each offer across 9 dimensions with personalization weights that shift
15
+ * based on trip context and purpose. Pure TypeScript — no external dependencies,
16
+ * safe to run in both Node and browser.
17
+ *
18
+ * Usage:
19
+ * import { rankOffers, type RankingContext } from 'letsfg/ranking'
20
+ * const ranked = rankOffers(offers, { tripContext: 'family', requireBag: true })
21
+ * // ranked[0].offer is the best pick, ranked[0].heroFacts explains why
22
+ *
23
+ * Companion modules in the same package:
24
+ * - trip-purpose.ts — TripPurpose type + normalization helpers
25
+ * - offer-details.ts — extractOfferDetailSignals, getOfferDetailBadges, etc.
26
+ */
27
+
28
+ interface RankOfferSegment {
29
+ layover_minutes?: number;
30
+ airline?: string;
31
+ flight_number?: string;
32
+ aircraft?: string;
33
+ origin?: string;
34
+ destination?: string;
35
+ }
36
+ interface RankOffer {
37
+ id: string;
38
+ price: number;
39
+ /** Display total in the user's chosen currency (ticket + fee + ancillaries).
40
+ * When provided, this is used for all price scoring and penalty calculations
41
+ * instead of `price`. Callers should populate this so the ranking reflects
42
+ * exactly what the user will pay. */
43
+ displayPrice?: number;
44
+ google_flights_price?: number;
45
+ currency: string;
46
+ airline: string;
47
+ origin?: string;
48
+ destination?: string;
49
+ departure_time: string;
50
+ arrival_time: string;
51
+ duration_minutes: number;
52
+ stops: number;
53
+ segments?: RankOfferSegment[];
54
+ inbound?: {
55
+ departure_time?: string;
56
+ arrival_time?: string;
57
+ origin?: string;
58
+ destination?: string;
59
+ stops?: number;
60
+ /** Inbound-leg duration in minutes. Required for round-trip scoring to
61
+ * see the return leg in `scoreDuration`. When absent the inbound is
62
+ * treated as zero-duration (best case for the offer). */
63
+ duration_minutes?: number;
64
+ segments?: RankOfferSegment[];
65
+ };
66
+ ancillaries?: {
67
+ cabin_bag?: {
68
+ included?: boolean;
69
+ price?: number;
70
+ currency?: string;
71
+ description?: string;
72
+ };
73
+ checked_bag?: {
74
+ included?: boolean;
75
+ price?: number;
76
+ currency?: string;
77
+ description?: string;
78
+ };
79
+ seat_selection?: {
80
+ included?: boolean;
81
+ price?: number;
82
+ currency?: string;
83
+ description?: string;
84
+ };
85
+ };
86
+ conditions?: {
87
+ refund_before_departure?: 'allowed' | 'not_allowed' | 'allowed_with_fee' | 'unknown';
88
+ change_before_departure?: 'allowed' | 'not_allowed' | 'allowed_with_fee' | 'unknown';
89
+ [key: string]: string | undefined;
90
+ };
91
+ /** Quality tag set by upstream validateOfferBatch when the offer's intrinsic
92
+ * data is untrustworthy (date drift from search criteria, asymmetric leg
93
+ * durations, etc.). The ranker uses this as the highest-priority hero gate:
94
+ * a suspect offer can never be hero unless every offer in the pool is suspect.
95
+ * Rank-not-filter — suspect offers still appear as runner-ups. */
96
+ quality?: 'suspect' | string;
97
+ }
98
+ interface RankingContext {
99
+ tripContext?: 'solo' | 'couple' | 'family' | 'group' | 'business_traveler';
100
+ tripPurpose?: TripPurpose;
101
+ tripPurposes?: ReadonlyArray<TripPurpose>;
102
+ travelerCount?: number;
103
+ depTimePref?: 'early_morning' | 'morning' | 'afternoon' | 'evening' | 'night' | 'red_eye';
104
+ retTimePref?: 'early_morning' | 'morning' | 'afternoon' | 'evening' | 'night' | 'red_eye';
105
+ arrivalTimePref?: 'morning' | 'afternoon' | 'evening';
106
+ requireBag?: boolean;
107
+ requireSeat?: boolean;
108
+ requireMeals?: boolean;
109
+ requireCancellation?: boolean;
110
+ preferredAirline?: string;
111
+ preferQuickFlight?: boolean;
112
+ /** User said "direct" or "nonstop" — strongly boost stops weight but don't filter.
113
+ * If no directs exist, 1-stop will naturally float to the top over 3-stop. */
114
+ preferDirect?: boolean;
115
+ /** User explicitly asked for cheapest / lowest price — price dominates all other factors. */
116
+ preferCheapest?: boolean;
117
+ /** User asked for "1 stop" / "max 2 stops" / "at most 1 connection" — a soft cap
118
+ * on the number of stops. Offers exceeding the cap are gated out of the HERO slot
119
+ * (rank-not-filter: they still appear as runner-ups, just below within-cap offers).
120
+ * When no offer satisfies the cap it is relaxed like the other gates. preferDirect
121
+ * (an implicit cap of 0) is handled by the dedicated 'direct' path; this covers
122
+ * explicit caps >= 1, where the user tolerates a connection but not a 3-leg odyssey. */
123
+ maxStops?: number;
124
+ /** User explicitly wants — or is happy with — a LONG layover (a stopover to break
125
+ * the journey or see a city). Inverts the default short-layover preference in
126
+ * scoreLayover so longer connections score higher, and boosts the layover weight.
127
+ * Default (undefined/false) leaves the normal short-layover scoring intact. */
128
+ preferLongLayover?: boolean;
129
+ /** Hard lower bound on departure time, in minutes from midnight.
130
+ * Flights departing before this time receive a heavy score penalty (effectively filtered out). */
131
+ departAfterMins?: number;
132
+ /** Hard upper bound on departure time, in minutes from midnight. */
133
+ departBeforeMins?: number;
134
+ }
135
+ interface ScoreBreakdown {
136
+ price: number;
137
+ stops: number;
138
+ duration: number;
139
+ depTime: number;
140
+ arrivalTime: number;
141
+ baggage: number;
142
+ savings: number;
143
+ comfortHours: number;
144
+ layover: number;
145
+ }
146
+ interface RankedOffer<T extends RankOffer = RankOffer> {
147
+ offer: T;
148
+ score: number;
149
+ rank: number;
150
+ breakdown: ScoreBreakdown;
151
+ heroFacts: string[];
152
+ tradeoffs: string[];
153
+ /** Hero only: names of user-stated criteria gates that had to be relaxed to
154
+ * find a hero (no offer satisfied them all). Surfaced so the UI can banner
155
+ * the mismatch — e.g. "No direct flights on this route" or "No refundable
156
+ * fare available". Empty/undefined means the hero satisfied every stated
157
+ * criterion. Relax order: refund -> bag -> time -> direct. */
158
+ relaxedGates?: string[];
159
+ }
160
+ /** Remove near-duplicate offers: when multiple connectors return the same physical
161
+ * flight (e.g. Ryanair FR1234 from both the direct connector and Kiwi/Skyscanner),
162
+ * keep only the cheapest. Two offers are considered identical only when they share
163
+ * the same calendar date, route, airline, outbound timing buckets, inbound timing
164
+ * buckets (for round-trips), stop counts, and core fare conditions. Including the
165
+ * inbound leg prevents collapsing distinct return options for the same outbound. */
166
+ declare function deduplicateOffers<T extends RankOffer>(offers: T[]): T[];
167
+ /**
168
+ * Rank an array of flight offers by personalized score.
169
+ * Returns a new array sorted best-first. The original array is not mutated.
170
+ *
171
+ * @param offers Array of flight offers (any type extending RankOffer)
172
+ * @param ctx User intent context from the NL query parser
173
+ */
174
+ declare function rankOffers<T extends RankOffer>(offers: T[], ctx: RankingContext, options?: {
175
+ skipDedup?: boolean;
176
+ }): RankedOffer<T>[];
177
+ /**
178
+ * From an already-ranked list, picks the top N offers that are genuinely
179
+ * different from each other — so runner-ups are real propositions, not just
180
+ * the same flight at +$3 from a different booking source.
181
+ *
182
+ * Two offers are considered "the same" for this purpose if both their
183
+ * departure time slot (3-hour window) AND their stop count are identical.
184
+ * Diversity requires differing in at least one of those dimensions.
185
+ *
186
+ * Falls back to next-best-ranked when the pool lacks enough diverse options.
187
+ */
188
+ declare function selectDiverseTop<T extends RankOffer>(ranked: RankedOffer<T>[], n: number): RankedOffer<T>[];
189
+ /**
190
+ * Returns a short human-readable label describing the ranking profile
191
+ * that was applied (e.g. "City break", "Family holiday"). Returns null
192
+ * if the default generic profile is used.
193
+ */
194
+ declare function getProfileLabel(ctx: RankingContext): string | null;
195
+
196
+ type OfferConditionState = 'allowed' | 'not_allowed' | 'allowed_with_fee' | 'unknown';
197
+ type OfferAmenityState = 'included' | 'available' | 'unavailable' | 'unknown';
198
+ type OfferAmenityConfidence = 'verified' | 'inferred' | 'unknown';
199
+ type OfferAmenitySource = 'ancillary' | 'condition' | 'unknown';
200
+ interface OfferDetailAncillary {
201
+ included?: boolean;
202
+ price?: number;
203
+ currency?: string;
204
+ description?: string;
205
+ }
206
+ interface OfferDetailConditions {
207
+ refund_before_departure?: OfferConditionState;
208
+ change_before_departure?: OfferConditionState;
209
+ [key: string]: string | undefined;
210
+ }
211
+ interface OfferDetailSegmentLike {
212
+ airline?: string;
213
+ flight_number?: string;
214
+ aircraft?: string;
215
+ }
216
+ interface OfferDetailLegLike {
217
+ segments?: OfferDetailSegmentLike[];
218
+ }
219
+ interface OfferDetailLike {
220
+ airline?: string;
221
+ flight_number?: string;
222
+ segments?: OfferDetailSegmentLike[];
223
+ inbound?: OfferDetailLegLike;
224
+ ancillaries?: {
225
+ cabin_bag?: OfferDetailAncillary;
226
+ checked_bag?: OfferDetailAncillary;
227
+ seat_selection?: OfferDetailAncillary;
228
+ };
229
+ conditions?: OfferDetailConditions;
230
+ }
231
+ type OfferServiceSignal = 'included' | 'available' | null;
232
+ interface OfferAmenityAssessment {
233
+ state: OfferAmenityState;
234
+ confidence: OfferAmenityConfidence;
235
+ source: OfferAmenitySource;
236
+ evidence?: string;
237
+ }
238
+ interface OfferDetailSignals {
239
+ refundability: OfferConditionState | null;
240
+ changeability: OfferConditionState | null;
241
+ meals: OfferServiceSignal;
242
+ refreshments: OfferServiceSignal;
243
+ insurance: OfferServiceSignal;
244
+ lounge: OfferServiceSignal;
245
+ wifi: OfferServiceSignal;
246
+ power: OfferServiceSignal;
247
+ entertainment: OfferServiceSignal;
248
+ amenities: {
249
+ meals: OfferAmenityAssessment;
250
+ refreshments: OfferAmenityAssessment;
251
+ insurance: OfferAmenityAssessment;
252
+ lounge: OfferAmenityAssessment;
253
+ wifi: OfferAmenityAssessment;
254
+ power: OfferAmenityAssessment;
255
+ entertainment: OfferAmenityAssessment;
256
+ };
257
+ }
258
+ interface OfferDetailBadge {
259
+ key: string;
260
+ label: string;
261
+ tone: 'positive' | 'neutral' | 'negative';
262
+ }
263
+ declare function extractOfferDetailSignals(offer: OfferDetailLike): OfferDetailSignals;
264
+ declare function getOfferDetailBadges(offer: OfferDetailLike): OfferDetailBadge[];
265
+ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
266
+
1
267
  /**
2
268
  * LetsFG — Agent-native flight search & booking SDK for Node.js/TypeScript.
3
269
  *
4
- * 75 airline connectors run locally via Python + backend API for enterprise GDS/NDC sources.
5
- * Zero external JS dependencies. Uses native fetch (Node 18+).
270
+ * Server-side engine covers hundreds of airlines. Free search via PFS Bearer token
271
+ * or prepaid Developer API. Zero external JS dependencies. Uses native fetch (Node 18+).
6
272
  *
7
273
  * @example
8
274
  * ```ts
9
- * import { LetsFG, searchLocal } from 'letsfg';
10
- *
11
- * // Local search — FREE, no API key
12
- * const local = await searchLocal('SHA', 'CTU', '2026-03-20');
275
+ * import { LetsFG } from 'letsfg';
13
276
  *
14
- * // Full API search + unlock + book
15
- * const bt = new LetsFG({ apiKey: 'trav_...' });
277
+ * // PFS (free, Bearer token from `letsfg auth`)
278
+ * const bt = new LetsFG({ bearerToken: process.env.LETSFG_BEARER_TOKEN });
16
279
  * const flights = await bt.search('GDN', 'BER', '2026-03-03');
280
+ *
281
+ * // Developer API (prepaid credits)
282
+ * const bt2 = new LetsFG({ apiKey: 'trav_...' });
283
+ * const flights2 = await bt2.search('LHR', 'JFK', '2026-04-15');
17
284
  * ```
18
285
  */
19
286
  interface FlightSegment {
@@ -112,28 +379,11 @@ interface SearchOptions {
112
379
  currency?: string;
113
380
  limit?: number;
114
381
  sort?: 'price' | 'duration';
115
- /** Max concurrent browser instances (1-32). Omit for auto-detect based on system RAM. */
116
- maxBrowsers?: number;
117
- /** Search mode. Omit for full search (all 200+ connectors). 'fast' = OTAs + key direct airlines (~25 connectors, 20-40s). */
118
- mode?: 'fast';
119
- }
120
- interface CheckoutProgress {
121
- status: string;
122
- step: string;
123
- step_index: number;
124
- airline: string;
125
- source: string;
126
- offer_id: string;
127
- total_price: number;
128
- currency: string;
129
- booking_url: string;
130
- screenshot_b64: string;
131
- message: string;
132
- can_complete_manually: boolean;
133
- elapsed_seconds: number;
134
- details: Record<string, unknown>;
135
382
  }
136
383
  interface LetsFGConfig {
384
+ /** PFS Bearer token from `letsfg auth`. Enables free search via POST /api/search polling. */
385
+ bearerToken?: string;
386
+ /** Developer API key (prepaid credits, no per-booking fee). */
137
387
  apiKey?: string;
138
388
  baseUrl?: string;
139
389
  timeout?: number;
@@ -189,28 +439,22 @@ declare class ValidationError extends LetsFGError {
189
439
  declare function offerSummary(offer: FlightOffer): string;
190
440
  /** Get cheapest offer from search results */
191
441
  declare function cheapestOffer(result: FlightSearchResult): FlightOffer | null;
192
- /**
193
- * Search flights using 73 local airline connectors — FREE, no API key needed.
194
- *
195
- * Requires: pip install letsfg && playwright install chromium
196
- *
197
- * @param origin - IATA code (e.g., "SHA")
198
- * @param destination - IATA code (e.g., "CTU")
199
- * @param dateFrom - Departure date "YYYY-MM-DD"
200
- * @param options - Optional: currency, adults, limit, etc.
201
- */
202
- declare function searchLocal(origin: string, destination: string, dateFrom: string, options?: Partial<SearchOptions>): Promise<FlightSearchResult>;
203
442
  declare class LetsFG {
443
+ private bearerToken;
204
444
  private apiKey;
205
445
  private baseUrl;
206
446
  private timeout;
207
447
  constructor(config?: LetsFGConfig);
448
+ private requireAuth;
208
449
  private requireApiKey;
450
+ /** True when using PFS Bearer token (free search path) */
451
+ private get usingPFS();
209
452
  /**
210
- * Search for flights — FREE, unlimited, runs locally on your machine.
453
+ * Search for flights — FREE.
211
454
  *
212
- * Uses 200 airline connectors via Python subprocess. No backend call.
213
- * Requires: pip install letsfg && playwright install chromium
455
+ * Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
456
+ * PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
457
+ * Developer API: synchronous 60-90s call.
214
458
  *
215
459
  * @param origin - IATA code (e.g., "GDN", "LON")
216
460
  * @param destination - IATA code (e.g., "BER", "BCN")
@@ -218,73 +462,45 @@ declare class LetsFG {
218
462
  * @param options - Optional search parameters
219
463
  */
220
464
  search(origin: string, destination: string, dateFrom: string, options?: SearchOptions): Promise<FlightSearchResult>;
465
+ /** PFS path: POST /api/search -> poll /api/results/<id> */
466
+ private searchPFS;
221
467
  /**
222
- * Resolve a city/airport name to IATA codes — runs locally, no backend call.
468
+ * Resolve a city/airport name to IATA codes.
223
469
  */
224
470
  resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
225
471
  /**
226
- * Unlock a flight offer — FREE with GitHub star.
227
- * Confirms price, reserves for 30 minutes.
472
+ * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
473
+ * Cost: 1% of ticket price, min $3. Free with Developer API.
228
474
  */
229
475
  unlock(offerId: string): Promise<UnlockResult>;
230
476
  /**
231
- * Book a flight — charges ticket price via Stripe.
232
- * Creates a real airline reservation with PNR.
233
- *
477
+ * Book a flight via Developer API — charges ticket price via Stripe, creates real PNR.
234
478
  * Always provide idempotencyKey to prevent double-bookings on retry.
235
479
  */
236
480
  book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string): Promise<BookingResult>;
237
481
  /**
238
- * Set up payment method (required before booking).
482
+ * Set up payment method (required before unlock/booking).
239
483
  */
240
484
  setupPayment(token?: string): Promise<Record<string, unknown>>;
241
- /**
242
- * Start automated checkout — drives to payment page, NEVER submits payment.
243
- *
244
- * Requires unlock first. Returns progress with screenshot and
245
- * booking URL for manual completion.
246
- *
247
- * @param offerId - Offer ID from search results
248
- * @param passengers - Passenger details (use test data for safety)
249
- * @param checkoutToken - Token from unlock() response
250
- */
251
- startCheckout(offerId: string, passengers: Passenger[], checkoutToken: string): Promise<CheckoutProgress>;
252
- /**
253
- * Start checkout locally via Python (runs on your machine).
254
- * Requires: pip install letsfg && playwright install chromium
255
- *
256
- * @param offer - Full FlightOffer object from search results
257
- * @param passengers - Passenger details
258
- * @param checkoutToken - Token from unlock()
259
- */
260
- startCheckoutLocal(offer: FlightOffer, passengers: Passenger[], checkoutToken: string): Promise<CheckoutProgress>;
261
- /**
262
- * Link GitHub account for FREE unlimited access.
263
- *
264
- * Star https://github.com/LetsFG/LetsFG, then call this with your username.
265
- * Once verified, access is permanent.
266
- */
267
- linkGithub(githubUsername: string): Promise<Record<string, unknown>>;
268
485
  /**
269
486
  * Get current agent profile and usage stats.
270
487
  */
271
488
  me(): Promise<Record<string, unknown>>;
272
489
  /**
273
- * Register a new agent — no API key needed.
490
+ * Register a new Developer API agent — no auth needed.
274
491
  */
275
492
  static register(agentName: string, email: string, baseUrl?: string, ownerName?: string, description?: string): Promise<Record<string, unknown>>;
493
+ private postWithBearer;
494
+ private getNoAuth;
495
+ private getWithAuth;
496
+ private postWithAuth;
276
497
  private post;
277
498
  private get;
278
- private request;
499
+ private requestWithHeaders;
279
500
  }
280
- /**
281
- * Get system resource profile and recommended concurrency settings.
282
- * Calls the Python backend's system-info detection.
283
- */
284
- declare function systemInfo(): Promise<Record<string, unknown>>;
285
501
 
286
502
  declare const BoostedTravel: typeof LetsFG;
287
503
  declare const BoostedTravelError: typeof LetsFGError;
288
504
  type BoostedTravelConfig = LetsFGConfig;
289
505
 
290
- export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, type CheckoutProgress, ErrorCategory, type ErrorCategoryType, ErrorCode, type ErrorCodeType, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, LetsFG, type LetsFGConfig, LetsFGError, OfferExpiredError, type Passenger, PaymentRequiredError, type SearchOptions, type UnlockResult, ValidationError, cheapestOffer, LetsFG as default, systemInfo as getSystemInfo, searchLocal as localSearch, offerSummary, searchLocal, systemInfo };
506
+ 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 };