letsfg 2026.5.71 → 2026.5.73
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 +283 -221
- package/dist/{chunk-GO3FXQXC.mjs → chunk-F5BBI6XX.mjs} +134 -20
- package/dist/cli.js +381 -216
- package/dist/cli.mjs +248 -61
- package/dist/index.d.mts +92 -14
- package/dist/index.d.ts +92 -14
- package/dist/index.js +134 -20
- package/dist/index.mjs +1 -1
- package/package.json +56 -56
- package/dist/auth-SYZ5AYZ6.mjs +0 -16
- package/dist/chunk-XNO2W77S.mjs +0 -126
package/dist/index.d.ts
CHANGED
|
@@ -282,7 +282,7 @@ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
|
|
|
282
282
|
* const bt = new LetsFG({ bearerToken: process.env.LETSFG_BEARER_TOKEN });
|
|
283
283
|
* const flights = await bt.search('GDN', 'BER', '2026-03-03');
|
|
284
284
|
*
|
|
285
|
-
* // Developer API (
|
|
285
|
+
* // Developer API (look-to-book search: 200 free after every booking)
|
|
286
286
|
* const bt2 = new LetsFG({ apiKey: 'letsfg_...' });
|
|
287
287
|
* const flights2 = await bt2.search('LHR', 'JFK', '2026-04-15');
|
|
288
288
|
* ```
|
|
@@ -402,7 +402,7 @@ interface SearchOptions {
|
|
|
402
402
|
interface LetsFGConfig {
|
|
403
403
|
/** PFS Bearer token from `letsfg auth`. Enables free search via POST /api/search polling. */
|
|
404
404
|
bearerToken?: string;
|
|
405
|
-
/** Developer API key
|
|
405
|
+
/** Developer API key. Look-to-book search; no booking fee, no transaction fee. */
|
|
406
406
|
apiKey?: string;
|
|
407
407
|
baseUrl?: string;
|
|
408
408
|
timeout?: number;
|
|
@@ -495,11 +495,19 @@ declare class LetsFG {
|
|
|
495
495
|
*/
|
|
496
496
|
resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
|
|
497
497
|
/**
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
498
|
+
* RETIRED 2026-09-08. Throws instead of calling the server.
|
|
499
|
+
*
|
|
500
|
+
* There is no unlock step on any lane. Unlock existed to confirm a live price
|
|
501
|
+
* before charging; booking now HOLDS the fare on the connected payment method
|
|
502
|
+
* and captures only once a real airline PNR exists, so a fare that moved
|
|
503
|
+
* cannot become a charge for a ticket you did not get. If it moves at
|
|
504
|
+
* checkout you get a `price_change` question to accept or decline instead.
|
|
505
|
+
*
|
|
506
|
+
* Kept as a method, and throwing locally rather than making the request, so an
|
|
507
|
+
* older caller gets one clear sentence at the line that is actually wrong —
|
|
508
|
+
* not a 410 body to decode, and not a TypeError somewhere else.
|
|
501
509
|
*/
|
|
502
|
-
unlock(
|
|
510
|
+
unlock(_offerId: string): Promise<UnlockResult>;
|
|
503
511
|
/**
|
|
504
512
|
* Book a flight.
|
|
505
513
|
*
|
|
@@ -509,11 +517,64 @@ declare class LetsFG {
|
|
|
509
517
|
* complete, { ok, booked: false, booking_url } — hand the link to the user,
|
|
510
518
|
* nothing was charged.
|
|
511
519
|
*
|
|
512
|
-
* Developer API (X-API-Key):
|
|
513
|
-
*
|
|
514
|
-
*
|
|
520
|
+
* Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
|
|
521
|
+
* REQUIRED — an offer is bookable only inside the search that produced it.
|
|
522
|
+
* The connected Revolut method is HELD, not charged; a LetsFG booking agent
|
|
523
|
+
* buys the ticket and the hold is captured only against a real airline PNR.
|
|
524
|
+
* Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
|
|
525
|
+
* poll getBooking(bookingId) until `terminal`, or use bookAndWait().
|
|
526
|
+
* Always provide idempotencyKey: a retry with the same key returns the
|
|
527
|
+
* existing booking instead of opening a second hold on the card.
|
|
515
528
|
*/
|
|
516
529
|
book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string, searchId?: string): Promise<BookingResult | Record<string, unknown>>;
|
|
530
|
+
/**
|
|
531
|
+
* Poll a Developer API flight booking.
|
|
532
|
+
*
|
|
533
|
+
* Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
|
|
534
|
+
* knows you are still there, which is what keeps a booking paused on a
|
|
535
|
+
* question alive — so do not back off to minutes.
|
|
536
|
+
*
|
|
537
|
+
* States: authorised, card_issued, booking_in_progress, awaiting_settlement,
|
|
538
|
+
* then completed (with `pnr` and `charged_amount`), failed (hold released,
|
|
539
|
+
* nothing charged) or needs_attention (a human at LetsFG is on it — do not
|
|
540
|
+
* book again).
|
|
541
|
+
*/
|
|
542
|
+
getBooking(bookingId: string): Promise<Record<string, unknown>>;
|
|
543
|
+
/**
|
|
544
|
+
* Answer the open `question` on a booking.
|
|
545
|
+
*
|
|
546
|
+
* Echo the question's `round`. A stale round is refused with 409 rather than
|
|
547
|
+
* guessed at, so an answer to an old question can never be applied to a new
|
|
548
|
+
* one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
|
|
549
|
+
* { confirm: true } or { skip: true } — declining an extra still completes
|
|
550
|
+
* the booking, without it.
|
|
551
|
+
*/
|
|
552
|
+
answerBooking(bookingId: string, round: number, answer?: {
|
|
553
|
+
seats?: Array<Record<string, unknown>>;
|
|
554
|
+
confirm?: boolean;
|
|
555
|
+
skip?: boolean;
|
|
556
|
+
}): Promise<Record<string, unknown>>;
|
|
557
|
+
/**
|
|
558
|
+
* Book and poll to a terminal state. Mirrors bookHotelAndWait().
|
|
559
|
+
*
|
|
560
|
+
* Blocks for as long as the booking takes (4–11 minutes typically), so use
|
|
561
|
+
* book() + getBooking() instead if your caller has a request timeout.
|
|
562
|
+
*
|
|
563
|
+
* `onQuestion` returns the answer for answerBooking(). Without it, a fare
|
|
564
|
+
* increase is ACCEPTED and a paid extra is DECLINED — the conservative
|
|
565
|
+
* reading of "the traveller asked for this flight".
|
|
566
|
+
*/
|
|
567
|
+
bookAndWait(offerId: string, passengers: Passenger[], contactEmail: string, searchId: string, opts?: {
|
|
568
|
+
contactPhone?: string;
|
|
569
|
+
idempotencyKey?: string;
|
|
570
|
+
pollMs?: number;
|
|
571
|
+
timeoutMs?: number;
|
|
572
|
+
onQuestion?: (q: Record<string, unknown>) => {
|
|
573
|
+
seats?: Array<Record<string, unknown>>;
|
|
574
|
+
confirm?: boolean;
|
|
575
|
+
skip?: boolean;
|
|
576
|
+
};
|
|
577
|
+
}): Promise<Record<string, unknown>>;
|
|
517
578
|
/**
|
|
518
579
|
* Resolve a place name to the city id that searchHotels() needs.
|
|
519
580
|
*
|
|
@@ -621,13 +682,30 @@ declare class LetsFG {
|
|
|
621
682
|
*/
|
|
622
683
|
cancelHotel(confirmation: string): Promise<Record<string, unknown>>;
|
|
623
684
|
/**
|
|
624
|
-
* [Developer API
|
|
685
|
+
* [Developer API] Mint a one-time link for connecting a Revolut payment method.
|
|
686
|
+
*
|
|
687
|
+
* This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
|
|
688
|
+
* card details never touch LetsFG: the returned `connect_url` opens a hosted page
|
|
689
|
+
* where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
|
|
690
|
+
* it in a browser — there is no endpoint that takes card details, so do not ask a
|
|
691
|
+
* user for a card number and do not try to automate this step.
|
|
692
|
+
*
|
|
693
|
+
* Most agents should NOT need a Developer API account at all. To authenticate for
|
|
694
|
+
* search and booking, run `letsfg auth`, which creates no billing account.
|
|
695
|
+
*/
|
|
696
|
+
connectPayment(): Promise<Record<string, unknown>>;
|
|
697
|
+
/**
|
|
698
|
+
* RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
|
|
699
|
+
*
|
|
700
|
+
* `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
|
|
701
|
+
* Revolut rail as the rest of the product: call connectPayment() and open the
|
|
702
|
+
* `connect_url` it returns.
|
|
625
703
|
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
704
|
+
* Kept as a method, and throwing locally rather than making the request, for the
|
|
705
|
+
* same reason as unlock() — an older caller gets one clear sentence at the line that
|
|
706
|
+
* is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
|
|
629
707
|
*/
|
|
630
|
-
setupPayment(
|
|
708
|
+
setupPayment(_token?: string): Promise<Record<string, unknown>>;
|
|
631
709
|
/**
|
|
632
710
|
* Get current agent profile and usage stats.
|
|
633
711
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1719,13 +1719,23 @@ var LetsFG = class {
|
|
|
1719
1719
|
return Array.isArray(data) ? data : data.locations || [];
|
|
1720
1720
|
}
|
|
1721
1721
|
/**
|
|
1722
|
-
*
|
|
1723
|
-
*
|
|
1724
|
-
*
|
|
1722
|
+
* RETIRED 2026-09-08. Throws instead of calling the server.
|
|
1723
|
+
*
|
|
1724
|
+
* There is no unlock step on any lane. Unlock existed to confirm a live price
|
|
1725
|
+
* before charging; booking now HOLDS the fare on the connected payment method
|
|
1726
|
+
* and captures only once a real airline PNR exists, so a fare that moved
|
|
1727
|
+
* cannot become a charge for a ticket you did not get. If it moves at
|
|
1728
|
+
* checkout you get a `price_change` question to accept or decline instead.
|
|
1729
|
+
*
|
|
1730
|
+
* Kept as a method, and throwing locally rather than making the request, so an
|
|
1731
|
+
* older caller gets one clear sentence at the line that is actually wrong —
|
|
1732
|
+
* not a 410 body to decode, and not a TypeError somewhere else.
|
|
1725
1733
|
*/
|
|
1726
|
-
async unlock(
|
|
1727
|
-
|
|
1728
|
-
|
|
1734
|
+
async unlock(_offerId) {
|
|
1735
|
+
throw new LetsFGError(
|
|
1736
|
+
"unlock() was retired on 2026-09-08 and the endpoint answers 410 Gone. There is no unlock step: call book() directly. The fare is held on the connected payment method and captured only against a real airline PNR. See https://letsfg.co/developers/api/docs",
|
|
1737
|
+
410
|
|
1738
|
+
);
|
|
1729
1739
|
}
|
|
1730
1740
|
/**
|
|
1731
1741
|
* Book a flight.
|
|
@@ -1736,9 +1746,14 @@ var LetsFG = class {
|
|
|
1736
1746
|
* complete, { ok, booked: false, booking_url } — hand the link to the user,
|
|
1737
1747
|
* nothing was charged.
|
|
1738
1748
|
*
|
|
1739
|
-
* Developer API (X-API-Key):
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1749
|
+
* Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
|
|
1750
|
+
* REQUIRED — an offer is bookable only inside the search that produced it.
|
|
1751
|
+
* The connected Revolut method is HELD, not charged; a LetsFG booking agent
|
|
1752
|
+
* buys the ticket and the hold is captured only against a real airline PNR.
|
|
1753
|
+
* Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
|
|
1754
|
+
* poll getBooking(bookingId) until `terminal`, or use bookAndWait().
|
|
1755
|
+
* Always provide idempotencyKey: a retry with the same key returns the
|
|
1756
|
+
* existing booking instead of opening a second hold on the card.
|
|
1742
1757
|
*/
|
|
1743
1758
|
async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "", searchId = "") {
|
|
1744
1759
|
this.requireAuth();
|
|
@@ -1759,15 +1774,92 @@ var LetsFG = class {
|
|
|
1759
1774
|
});
|
|
1760
1775
|
}
|
|
1761
1776
|
this.requireApiKey();
|
|
1777
|
+
if (!searchId) {
|
|
1778
|
+
throw new LetsFGError(
|
|
1779
|
+
"searchId is required to book on the Developer API \u2014 pass the search_id from search()'s result. An offer can only be booked inside the search that produced it. (Before 2026-09-08 this argument was ignored on this path.)",
|
|
1780
|
+
400
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
const pax = passengers.map((p) => ({ ...p }));
|
|
1784
|
+
if (contactPhone && pax.length && !pax[0].phone_number) pax[0].phone_number = contactPhone;
|
|
1762
1785
|
const body = {
|
|
1786
|
+
search_id: searchId,
|
|
1763
1787
|
offer_id: offerId,
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
contact_email: contactEmail,
|
|
1767
|
-
contact_phone: contactPhone
|
|
1788
|
+
passengers: pax,
|
|
1789
|
+
contact_email: contactEmail
|
|
1768
1790
|
};
|
|
1769
1791
|
if (idempotencyKey) body.idempotency_key = idempotencyKey;
|
|
1770
|
-
return this.post("/developers/api/v1/
|
|
1792
|
+
return this.post("/developers/api/v1/flights/book", body);
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Poll a Developer API flight booking.
|
|
1796
|
+
*
|
|
1797
|
+
* Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
|
|
1798
|
+
* knows you are still there, which is what keeps a booking paused on a
|
|
1799
|
+
* question alive — so do not back off to minutes.
|
|
1800
|
+
*
|
|
1801
|
+
* States: authorised, card_issued, booking_in_progress, awaiting_settlement,
|
|
1802
|
+
* then completed (with `pnr` and `charged_amount`), failed (hold released,
|
|
1803
|
+
* nothing charged) or needs_attention (a human at LetsFG is on it — do not
|
|
1804
|
+
* book again).
|
|
1805
|
+
*/
|
|
1806
|
+
async getBooking(bookingId) {
|
|
1807
|
+
this.requireApiKey();
|
|
1808
|
+
return this.getWithAuth(
|
|
1809
|
+
`/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Answer the open `question` on a booking.
|
|
1814
|
+
*
|
|
1815
|
+
* Echo the question's `round`. A stale round is refused with 409 rather than
|
|
1816
|
+
* guessed at, so an answer to an old question can never be applied to a new
|
|
1817
|
+
* one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
|
|
1818
|
+
* { confirm: true } or { skip: true } — declining an extra still completes
|
|
1819
|
+
* the booking, without it.
|
|
1820
|
+
*/
|
|
1821
|
+
async answerBooking(bookingId, round, answer = {}) {
|
|
1822
|
+
this.requireApiKey();
|
|
1823
|
+
return this.post(
|
|
1824
|
+
`/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}/answer`,
|
|
1825
|
+
{ round, ...answer }
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Book and poll to a terminal state. Mirrors bookHotelAndWait().
|
|
1830
|
+
*
|
|
1831
|
+
* Blocks for as long as the booking takes (4–11 minutes typically), so use
|
|
1832
|
+
* book() + getBooking() instead if your caller has a request timeout.
|
|
1833
|
+
*
|
|
1834
|
+
* `onQuestion` returns the answer for answerBooking(). Without it, a fare
|
|
1835
|
+
* increase is ACCEPTED and a paid extra is DECLINED — the conservative
|
|
1836
|
+
* reading of "the traveller asked for this flight".
|
|
1837
|
+
*/
|
|
1838
|
+
async bookAndWait(offerId, passengers, contactEmail, searchId, opts = {}) {
|
|
1839
|
+
const { contactPhone = "", idempotencyKey = "", pollMs = 5e3, timeoutMs = 9e5, onQuestion } = opts;
|
|
1840
|
+
const started = await this.book(
|
|
1841
|
+
offerId,
|
|
1842
|
+
passengers,
|
|
1843
|
+
contactEmail,
|
|
1844
|
+
contactPhone,
|
|
1845
|
+
idempotencyKey,
|
|
1846
|
+
searchId
|
|
1847
|
+
);
|
|
1848
|
+
if (!started || started.ok !== true) return started;
|
|
1849
|
+
const bookingId = String(started.booking_id);
|
|
1850
|
+
const deadline = Date.now() + timeoutMs;
|
|
1851
|
+
while (Date.now() < deadline) {
|
|
1852
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
1853
|
+
const state = await this.getBooking(bookingId);
|
|
1854
|
+
const question = state.question;
|
|
1855
|
+
if (question) {
|
|
1856
|
+
const answer = onQuestion ? onQuestion(question) : question.kind === "extra" ? { skip: true } : { confirm: true };
|
|
1857
|
+
await this.answerBooking(bookingId, Number(question.round), answer);
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
if (state.terminal) return state;
|
|
1861
|
+
}
|
|
1862
|
+
return this.getBooking(bookingId);
|
|
1771
1863
|
}
|
|
1772
1864
|
// ── Hotels ──────────────────────────────────────────────────────────
|
|
1773
1865
|
//
|
|
@@ -1925,15 +2017,37 @@ var LetsFG = class {
|
|
|
1925
2017
|
);
|
|
1926
2018
|
}
|
|
1927
2019
|
/**
|
|
1928
|
-
* [Developer API
|
|
2020
|
+
* [Developer API] Mint a one-time link for connecting a Revolut payment method.
|
|
2021
|
+
*
|
|
2022
|
+
* This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
|
|
2023
|
+
* card details never touch LetsFG: the returned `connect_url` opens a hosted page
|
|
2024
|
+
* where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
|
|
2025
|
+
* it in a browser — there is no endpoint that takes card details, so do not ask a
|
|
2026
|
+
* user for a card number and do not try to automate this step.
|
|
1929
2027
|
*
|
|
1930
|
-
* Most agents should NOT
|
|
1931
|
-
* search and booking
|
|
1932
|
-
* through a zero-amount setup and creates no billing account.
|
|
2028
|
+
* Most agents should NOT need a Developer API account at all. To authenticate for
|
|
2029
|
+
* search and booking, run `letsfg auth`, which creates no billing account.
|
|
1933
2030
|
*/
|
|
1934
|
-
async
|
|
2031
|
+
async connectPayment() {
|
|
1935
2032
|
this.requireApiKey();
|
|
1936
|
-
return this.post("/developers/api/v1/agents/
|
|
2033
|
+
return this.post("/developers/api/v1/agents/connect-payment", {});
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
|
|
2037
|
+
*
|
|
2038
|
+
* `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
|
|
2039
|
+
* Revolut rail as the rest of the product: call connectPayment() and open the
|
|
2040
|
+
* `connect_url` it returns.
|
|
2041
|
+
*
|
|
2042
|
+
* Kept as a method, and throwing locally rather than making the request, for the
|
|
2043
|
+
* same reason as unlock() — an older caller gets one clear sentence at the line that
|
|
2044
|
+
* is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
|
|
2045
|
+
*/
|
|
2046
|
+
async setupPayment(_token) {
|
|
2047
|
+
throw new LetsFGError(
|
|
2048
|
+
"setupPayment() was retired on 2026-09-08 with Stripe and the endpoint answers 410 Gone. Call connectPayment() instead and open the connect_url it returns; nothing is charged to connect. See https://letsfg.co/developers/api/docs",
|
|
2049
|
+
410
|
|
2050
|
+
);
|
|
1937
2051
|
}
|
|
1938
2052
|
/**
|
|
1939
2053
|
* Get current agent profile and usage stats.
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "letsfg",
|
|
3
|
-
"version": "2026.5.
|
|
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
|
-
"main": "dist/index.js",
|
|
6
|
-
"module": "dist/index.mjs",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"bin": {
|
|
9
|
-
"letsfg": "dist/cli.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"dist/",
|
|
13
|
-
"README.md",
|
|
14
|
-
"LICENSE"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean",
|
|
18
|
-
"test": "tsx --test src/index.test.ts src/auth.test.ts",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
20
|
-
},
|
|
21
|
-
"keywords": [
|
|
22
|
-
"flights",
|
|
23
|
-
"travel",
|
|
24
|
-
"booking",
|
|
25
|
-
"agent",
|
|
26
|
-
"ai",
|
|
27
|
-
"cli",
|
|
28
|
-
"autonomous",
|
|
29
|
-
"mcp",
|
|
30
|
-
"openai",
|
|
31
|
-
"airline",
|
|
32
|
-
"ndc",
|
|
33
|
-
"search",
|
|
34
|
-
"ndc",
|
|
35
|
-
"hotels",
|
|
36
|
-
"hotel-booking",
|
|
37
|
-
"hotel-search",
|
|
38
|
-
"accommodation"
|
|
39
|
-
],
|
|
40
|
-
"author": "LetsFG",
|
|
41
|
-
"license": "MIT",
|
|
42
|
-
"repository": {
|
|
43
|
-
"type": "git",
|
|
44
|
-
"url": "https://github.com/LetsFG/LetsFG.git"
|
|
45
|
-
},
|
|
46
|
-
"homepage": "https://letsfg.co",
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@types/node": "^25.3.3",
|
|
49
|
-
"tsup": "^8.0.0",
|
|
50
|
-
"tsx": "^4.19.0",
|
|
51
|
-
"typescript": "^5.3.0"
|
|
52
|
-
},
|
|
53
|
-
"engines": {
|
|
54
|
-
"node": ">=18.0.0"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "letsfg",
|
|
3
|
+
"version": "2026.5.73",
|
|
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
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"letsfg": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean",
|
|
18
|
+
"test": "tsx --test src/index.test.ts src/auth.test.ts",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"flights",
|
|
23
|
+
"travel",
|
|
24
|
+
"booking",
|
|
25
|
+
"agent",
|
|
26
|
+
"ai",
|
|
27
|
+
"cli",
|
|
28
|
+
"autonomous",
|
|
29
|
+
"mcp",
|
|
30
|
+
"openai",
|
|
31
|
+
"airline",
|
|
32
|
+
"ndc",
|
|
33
|
+
"search",
|
|
34
|
+
"ndc",
|
|
35
|
+
"hotels",
|
|
36
|
+
"hotel-booking",
|
|
37
|
+
"hotel-search",
|
|
38
|
+
"accommodation"
|
|
39
|
+
],
|
|
40
|
+
"author": "LetsFG",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/LetsFG/LetsFG.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://letsfg.co",
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^25.3.3",
|
|
49
|
+
"tsup": "^8.0.0",
|
|
50
|
+
"tsx": "^4.19.0",
|
|
51
|
+
"typescript": "^5.3.0"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/auth-SYZ5AYZ6.mjs
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BearerTokenError,
|
|
3
|
-
getBearerToken,
|
|
4
|
-
paymentAuth,
|
|
5
|
-
requestEnrolment,
|
|
6
|
-
saveToken,
|
|
7
|
-
verifyPaymentMethod
|
|
8
|
-
} from "./chunk-XNO2W77S.mjs";
|
|
9
|
-
export {
|
|
10
|
-
BearerTokenError,
|
|
11
|
-
getBearerToken,
|
|
12
|
-
paymentAuth,
|
|
13
|
-
requestEnrolment,
|
|
14
|
-
saveToken,
|
|
15
|
-
verifyPaymentMethod
|
|
16
|
-
};
|
package/dist/chunk-XNO2W77S.mjs
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// src/auth.ts
|
|
2
|
-
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";
|
|
3
|
-
import { homedir, platform } from "os";
|
|
4
|
-
import { join, dirname } from "path";
|
|
5
|
-
var BASE_URL = process.env.LETSFG_BASE_URL || "https://letsfg.co";
|
|
6
|
-
var TOKEN_TTL_MS = 90 * 24 * 3600 * 1e3;
|
|
7
|
-
var BearerTokenError = class extends Error {
|
|
8
|
-
};
|
|
9
|
-
function configPath() {
|
|
10
|
-
const base = platform() === "win32" ? process.env.APPDATA || homedir() : homedir();
|
|
11
|
-
return join(base, ".letsfg", "config.json");
|
|
12
|
-
}
|
|
13
|
-
function loadConfig() {
|
|
14
|
-
const p = configPath();
|
|
15
|
-
if (!existsSync(p)) return {};
|
|
16
|
-
try {
|
|
17
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
18
|
-
} catch {
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
function saveConfig(cfg) {
|
|
23
|
-
const p = configPath();
|
|
24
|
-
mkdirSync(dirname(p), { recursive: true });
|
|
25
|
-
writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
26
|
-
try {
|
|
27
|
-
chmodSync(p, 384);
|
|
28
|
-
} catch {
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function getBearerToken() {
|
|
32
|
-
const env = process.env.LETSFG_BEARER_TOKEN;
|
|
33
|
-
if (env) return env;
|
|
34
|
-
const auth = loadConfig().pfs_auth;
|
|
35
|
-
if (auth?.token && Date.now() < auth.expires_at - 36e5) {
|
|
36
|
-
return auth.token;
|
|
37
|
-
}
|
|
38
|
-
throw new BearerTokenError(
|
|
39
|
-
"No valid LetsFG Bearer token.\n Run: letsfg auth (adds a payment method \u2014 nothing is charged)\n Or: export LETSFG_BEARER_TOKEN=<token>"
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
function saveToken(token, expiresAt) {
|
|
43
|
-
const cfg = loadConfig();
|
|
44
|
-
cfg.pfs_auth = { token, expires_at: expiresAt ?? Date.now() + TOKEN_TTL_MS };
|
|
45
|
-
saveConfig(cfg);
|
|
46
|
-
}
|
|
47
|
-
async function postJson(path, payload) {
|
|
48
|
-
const resp = await fetch(`${BASE_URL}${path}`, {
|
|
49
|
-
method: "POST",
|
|
50
|
-
headers: { "Content-Type": "application/json", "User-Agent": "LetsFG-js/0.1.0", "X-Client-Type": "js-sdk" },
|
|
51
|
-
body: JSON.stringify(payload)
|
|
52
|
-
});
|
|
53
|
-
const data = await resp.json().catch(() => ({}));
|
|
54
|
-
return { status: resp.status, data };
|
|
55
|
-
}
|
|
56
|
-
function parseExpiry(raw) {
|
|
57
|
-
if (typeof raw === "number") return raw * 1e3;
|
|
58
|
-
if (typeof raw === "string") {
|
|
59
|
-
const ms = Date.parse(raw);
|
|
60
|
-
if (!Number.isNaN(ms)) return ms;
|
|
61
|
-
}
|
|
62
|
-
return Date.now() + TOKEN_TTL_MS;
|
|
63
|
-
}
|
|
64
|
-
async function requestEnrolment() {
|
|
65
|
-
const { status, data } = await postJson("/api/agent-access/request", {});
|
|
66
|
-
if (status !== 200 && status !== 402) {
|
|
67
|
-
throw new BearerTokenError(`Could not start authentication (HTTP ${status}): ${data.error || ""}`);
|
|
68
|
-
}
|
|
69
|
-
return data;
|
|
70
|
-
}
|
|
71
|
-
async function verifyPaymentMethod(opts) {
|
|
72
|
-
const payload = {};
|
|
73
|
-
if (opts.setupSessionId) payload.setup_session_id = opts.setupSessionId;
|
|
74
|
-
else if (opts.paymentMethodId) payload.payment_method_id = opts.paymentMethodId;
|
|
75
|
-
else if (opts.cardToken) payload.card_token = opts.cardToken;
|
|
76
|
-
else throw new BearerTokenError("Provide one of setupSessionId, paymentMethodId, or cardToken.");
|
|
77
|
-
const { status, data } = await postJson("/api/agent-access/verify", payload);
|
|
78
|
-
if (status !== 200 || !data.token) {
|
|
79
|
-
throw new BearerTokenError(`Verification failed (HTTP ${status}). ${data.hint || data.error || ""}`.trim());
|
|
80
|
-
}
|
|
81
|
-
const token = data.token;
|
|
82
|
-
saveToken(token, parseExpiry(data.expires_at));
|
|
83
|
-
return token;
|
|
84
|
-
}
|
|
85
|
-
function openBrowser(url) {
|
|
86
|
-
import("child_process").then(({ exec }) => {
|
|
87
|
-
const cmd = platform() === "win32" ? `start "" "${url}"` : platform() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
88
|
-
exec(cmd, () => {
|
|
89
|
-
});
|
|
90
|
-
}).catch(() => {
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
async function paymentAuth(openBrowserFlag = true) {
|
|
94
|
-
console.log("\n Connecting to LetsFG...");
|
|
95
|
-
const data = await requestEnrolment();
|
|
96
|
-
const setupUrl = data.setup_url;
|
|
97
|
-
const sessionId = data.setup_session_id;
|
|
98
|
-
if (!setupUrl || !sessionId) {
|
|
99
|
-
throw new BearerTokenError("Server did not return a setup URL. Check https://letsfg.co/for-agents");
|
|
100
|
-
}
|
|
101
|
-
console.log("\n LetsFG needs a payment method on file before it can search or book.");
|
|
102
|
-
console.log(" Nothing is charged now \u2014 this is a zero-amount card setup.\n");
|
|
103
|
-
console.log(" Step 1 \u2014 add a card here:\n");
|
|
104
|
-
console.log(` ${setupUrl}
|
|
105
|
-
`);
|
|
106
|
-
if (openBrowserFlag) openBrowser(setupUrl);
|
|
107
|
-
console.log(" Step 2 \u2014 press Enter once you've finished.");
|
|
108
|
-
const readline = await import("readline/promises");
|
|
109
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
110
|
-
await rl.question("");
|
|
111
|
-
rl.close();
|
|
112
|
-
process.stdout.write(" Verifying... ");
|
|
113
|
-
const token = await verifyPaymentMethod({ setupSessionId: sessionId });
|
|
114
|
-
const expiresAt = loadConfig().pfs_auth?.expires_at ?? Date.now() + TOKEN_TTL_MS;
|
|
115
|
-
console.log(`done. Token valid until ${new Date(expiresAt).toISOString().slice(0, 10)}.`);
|
|
116
|
-
return token;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export {
|
|
120
|
-
BearerTokenError,
|
|
121
|
-
getBearerToken,
|
|
122
|
-
saveToken,
|
|
123
|
-
requestEnrolment,
|
|
124
|
-
verifyPaymentMethod,
|
|
125
|
-
paymentAuth
|
|
126
|
-
};
|