letsfg 2026.5.61 → 2026.5.63
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 +197 -104
- package/dist/{chunk-I2JLOQPR.mjs → chunk-ZX2EIEN7.mjs} +162 -7
- package/dist/cli.js +162 -7
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +106 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +162 -7
- package/dist/index.mjs +1 -1
- package/package.json +52 -52
package/README.md
CHANGED
|
@@ -1,104 +1,197 @@
|
|
|
1
|
-
# LetsFG — Your AI agent just learned to book flights. (Node.js)
|
|
2
|
-
|
|
3
|
-
**Server-side search engine. Real prices. One function call.** Search hundreds of airlines at raw airline prices — **$20–$50 cheaper** than Booking.com, Kayak, and other OTAs. Zero dependencies. Built for AI agents.
|
|
4
|
-
|
|
5
|
-
[](https://github.com/LetsFG/LetsFG)
|
|
6
|
-
[](https://www.npmjs.com/package/letsfg)
|
|
7
|
-
|
|
8
|
-
## Two ways to use LetsFG
|
|
9
|
-
|
|
10
|
-
| | **CLI / SDK** (this package) | **Developer API** |
|
|
11
|
-
|---|---|---|
|
|
12
|
-
| **Search cost** | Free (Twitter/X Bearer token via `letsfg auth`) | Prepaid credits |
|
|
13
|
-
| **Booking** | `POST /api/agent-book` | Direct airline URL |
|
|
14
|
-
| **Speed** | 60–90 s | 2–5 s (discover) · 60–90 s (full) |
|
|
15
|
-
| **Setup** | `npm install letsfg` then `letsfg auth` | [letsfg.co/developers](https://letsfg.co/developers) |
|
|
16
|
-
|
|
17
|
-
> **Want direct airline URLs without any per-booking fee?** Use the [Developer API](https://letsfg.co/developers) — prepaid credits, results in seconds, no checkout step.
|
|
18
|
-
|
|
19
|
-
## Install
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install letsfg
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Quick Start (SDK)
|
|
26
|
-
|
|
27
|
-
```typescript
|
|
28
|
-
import { LetsFG, cheapestOffer, offerSummary } from 'letsfg';
|
|
29
|
-
|
|
30
|
-
// Register (one-time)
|
|
31
|
-
const creds = await LetsFG.register('my-agent', 'agent@example.com');
|
|
32
|
-
console.log(creds.api_key); // Save this
|
|
33
|
-
|
|
34
|
-
// Use
|
|
35
|
-
const bt = new LetsFG({ apiKey: 'trav_...' });
|
|
36
|
-
|
|
37
|
-
// Search — FREE
|
|
38
|
-
const flights = await bt.search('GDN', 'BER', '2026-03-03');
|
|
39
|
-
const best = cheapestOffer(flights);
|
|
40
|
-
console.log(offerSummary(best));
|
|
41
|
-
|
|
42
|
-
// Unlock
|
|
43
|
-
const unlock = await bt.unlock(best.id);
|
|
44
|
-
|
|
45
|
-
// Book
|
|
46
|
-
const booking = await bt.book(
|
|
47
|
-
best.id,
|
|
48
|
-
[{
|
|
49
|
-
id: flights.passenger_ids[0],
|
|
50
|
-
given_name: 'John',
|
|
51
|
-
family_name: 'Doe',
|
|
52
|
-
born_on: '1990-01-15',
|
|
53
|
-
gender: 'm',
|
|
54
|
-
title: 'mr',
|
|
55
|
-
email: 'john@example.com',
|
|
56
|
-
}],
|
|
57
|
-
'john@example.com'
|
|
58
|
-
);
|
|
59
|
-
console.log(`PNR: ${booking.booking_reference}`);
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Quick Start (CLI)
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
export LETSFG_BEARER_TOKEN=<your-bearer-token>
|
|
66
|
-
|
|
67
|
-
letsfg search GDN BER 2026-03-03 --sort price
|
|
68
|
-
letsfg search LON BCN 2026-04-01 --json # Machine-readable
|
|
69
|
-
letsfg unlock off_xxx
|
|
70
|
-
letsfg book off_xxx -p '{"id":"pas_xxx","given_name":"John",...}' -e john@example.com
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## API
|
|
74
|
-
|
|
75
|
-
### `new LetsFG({ apiKey, baseUrl?, timeout? })`
|
|
76
|
-
|
|
77
|
-
### `bt.search(origin, destination, dateFrom, options?)`
|
|
78
|
-
### `bt.resolveLocation(query)`
|
|
79
|
-
### `bt.unlock(offerId)`
|
|
80
|
-
### `bt.book(offerId, passengers, contactEmail, contactPhone?)`
|
|
81
|
-
### `bt.setupPayment(token?)`
|
|
82
|
-
### `bt.me()`
|
|
83
|
-
### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)`
|
|
84
|
-
|
|
85
|
-
### Helpers
|
|
86
|
-
- `offerSummary(offer)` — One-line string summary
|
|
87
|
-
- `cheapestOffer(result)` — Get cheapest offer from search
|
|
88
|
-
|
|
89
|
-
## Zero Dependencies
|
|
90
|
-
|
|
91
|
-
Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
|
|
92
|
-
|
|
93
|
-
## Also Available As
|
|
94
|
-
|
|
95
|
-
- **MCP Server**: `npx letsfg-mcp` — [npm](https://www.npmjs.com/package/letsfg-mcp)
|
|
96
|
-
- **Python SDK + CLI**: `pip install letsfg` — [PyPI](https://pypi.org/project/letsfg/)
|
|
97
|
-
- **Try without installing**: [letsfg.co](https://letsfg.co) — search instantly in your browser
|
|
98
|
-
- **GitHub**: [LetsFG/LetsFG](https://github.com/LetsFG/LetsFG)
|
|
99
|
-
|
|
100
|
-
> ⭐ **[Star the repo](https://github.com/LetsFG/LetsFG)** — we appreciate the support.
|
|
101
|
-
|
|
102
|
-
## License
|
|
103
|
-
|
|
104
|
-
MIT
|
|
1
|
+
# LetsFG — Your AI agent just learned to book flights. (Node.js)
|
|
2
|
+
|
|
3
|
+
**Server-side search engine. Real prices. One function call.** Search hundreds of airlines at raw airline prices — **$20–$50 cheaper** than Booking.com, Kayak, and other OTAs. Zero dependencies. Built for AI agents.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/LetsFG/LetsFG)
|
|
6
|
+
[](https://www.npmjs.com/package/letsfg)
|
|
7
|
+
|
|
8
|
+
## Two ways to use LetsFG
|
|
9
|
+
|
|
10
|
+
| | **CLI / SDK** (this package) | **Developer API** |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **Search cost** | Free (Twitter/X Bearer token via `letsfg auth`) | Prepaid credits |
|
|
13
|
+
| **Booking** | `POST /api/agent-book` | Direct airline URL |
|
|
14
|
+
| **Speed** | 60–90 s | 2–5 s (discover) · 60–90 s (full) |
|
|
15
|
+
| **Setup** | `npm install letsfg` then `letsfg auth` | [letsfg.co/developers](https://letsfg.co/developers) |
|
|
16
|
+
|
|
17
|
+
> **Want direct airline URLs without any per-booking fee?** Use the [Developer API](https://letsfg.co/developers) — prepaid credits, results in seconds, no checkout step.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install letsfg
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start (SDK)
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { LetsFG, cheapestOffer, offerSummary } from 'letsfg';
|
|
29
|
+
|
|
30
|
+
// Register (one-time)
|
|
31
|
+
const creds = await LetsFG.register('my-agent', 'agent@example.com');
|
|
32
|
+
console.log(creds.api_key); // Save this
|
|
33
|
+
|
|
34
|
+
// Use
|
|
35
|
+
const bt = new LetsFG({ apiKey: 'trav_...' });
|
|
36
|
+
|
|
37
|
+
// Search — FREE
|
|
38
|
+
const flights = await bt.search('GDN', 'BER', '2026-03-03');
|
|
39
|
+
const best = cheapestOffer(flights);
|
|
40
|
+
console.log(offerSummary(best));
|
|
41
|
+
|
|
42
|
+
// Unlock
|
|
43
|
+
const unlock = await bt.unlock(best.id);
|
|
44
|
+
|
|
45
|
+
// Book
|
|
46
|
+
const booking = await bt.book(
|
|
47
|
+
best.id,
|
|
48
|
+
[{
|
|
49
|
+
id: flights.passenger_ids[0],
|
|
50
|
+
given_name: 'John',
|
|
51
|
+
family_name: 'Doe',
|
|
52
|
+
born_on: '1990-01-15',
|
|
53
|
+
gender: 'm',
|
|
54
|
+
title: 'mr',
|
|
55
|
+
email: 'john@example.com',
|
|
56
|
+
}],
|
|
57
|
+
'john@example.com'
|
|
58
|
+
);
|
|
59
|
+
console.log(`PNR: ${booking.booking_reference}`);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start (CLI)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export LETSFG_BEARER_TOKEN=<your-bearer-token>
|
|
66
|
+
|
|
67
|
+
letsfg search GDN BER 2026-03-03 --sort price
|
|
68
|
+
letsfg search LON BCN 2026-04-01 --json # Machine-readable
|
|
69
|
+
letsfg unlock off_xxx
|
|
70
|
+
letsfg book off_xxx -p '{"id":"pas_xxx","given_name":"John",...}' -e john@example.com
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API
|
|
74
|
+
|
|
75
|
+
### `new LetsFG({ apiKey, baseUrl?, timeout? })`
|
|
76
|
+
|
|
77
|
+
### `bt.search(origin, destination, dateFrom, options?)`
|
|
78
|
+
### `bt.resolveLocation(query)`
|
|
79
|
+
### `bt.unlock(offerId)`
|
|
80
|
+
### `bt.book(offerId, passengers, contactEmail, contactPhone?)`
|
|
81
|
+
### `bt.setupPayment(token?)`
|
|
82
|
+
### `bt.me()`
|
|
83
|
+
### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)`
|
|
84
|
+
|
|
85
|
+
### Helpers
|
|
86
|
+
- `offerSummary(offer)` — One-line string summary
|
|
87
|
+
- `cheapestOffer(result)` — Get cheapest offer from search
|
|
88
|
+
|
|
89
|
+
## Zero Dependencies
|
|
90
|
+
|
|
91
|
+
Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
|
|
92
|
+
|
|
93
|
+
## Also Available As
|
|
94
|
+
|
|
95
|
+
- **MCP Server**: `npx letsfg-mcp` — [npm](https://www.npmjs.com/package/letsfg-mcp)
|
|
96
|
+
- **Python SDK + CLI**: `pip install letsfg` — [PyPI](https://pypi.org/project/letsfg/)
|
|
97
|
+
- **Try without installing**: [letsfg.co](https://letsfg.co) — search instantly in your browser
|
|
98
|
+
- **GitHub**: [LetsFG/LetsFG](https://github.com/LetsFG/LetsFG)
|
|
99
|
+
|
|
100
|
+
> ⭐ **[Star the repo](https://github.com/LetsFG/LetsFG)** — we appreciate the support.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
105
|
+
|
|
106
|
+
## 🏨 Hotels — new, and live
|
|
107
|
+
|
|
108
|
+
Your agent can now book hotels, not just flights. Same API key, same card on file.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from letsfg import LetsFG
|
|
112
|
+
lfg = LetsFG()
|
|
113
|
+
|
|
114
|
+
city = lfg.hotel_destinations("Warsaw")[0]
|
|
115
|
+
stays = lfg.search_hotels(
|
|
116
|
+
city_id=city["Id"], city_name=city["Name"],
|
|
117
|
+
check_in="2026-11-10", check_out="2026-11-12", adults=2,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
hotel = stays["hotels"][0]
|
|
121
|
+
offer = hotel["offers"][0]
|
|
122
|
+
print(hotel["name"], offer["price"], stays["currency"])
|
|
123
|
+
# Hotel Gromada Warszawa Centrum 669.86 PLN
|
|
124
|
+
|
|
125
|
+
booking = lfg.book_hotel_and_wait(
|
|
126
|
+
session_id=stays["session_id"],
|
|
127
|
+
hotel_code=hotel["hotel_code"],
|
|
128
|
+
combination_id_v2=offer["combination_id_v2"],
|
|
129
|
+
expected_price=offer["price"],
|
|
130
|
+
expected_balance=offer["balance_to_supplier"],
|
|
131
|
+
city_id=city["Id"], city_name=city["Name"],
|
|
132
|
+
check_in="2026-11-10", check_out="2026-11-12",
|
|
133
|
+
guests=[{"title": "Mr", "first_name": "Jan", "last_name": "Kowalski"}],
|
|
134
|
+
email="guest@example.com", phone="512345678",
|
|
135
|
+
)
|
|
136
|
+
print(booking["confirmation"], booking["pay_link"])
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### How you pay
|
|
140
|
+
|
|
141
|
+
**10% now, the rest to the hotel later.** At booking we charge 10% of the price
|
|
142
|
+
to your card as a reservation fee. The remaining balance is paid **directly to
|
|
143
|
+
the supplier** through a `pay_link` we return — we never hold it.
|
|
144
|
+
|
|
145
|
+
`balance_due_by` is the supplier's own auto-cancellation date, not a date we
|
|
146
|
+
invent. Miss it and the room is released.
|
|
147
|
+
|
|
148
|
+
The 10% is **non-refundable**. Cancelling before `balance_due_by` costs nothing
|
|
149
|
+
else; after it, the hotel's own cancellation ladder applies and can reach 100%.
|
|
150
|
+
That ladder ships in the booking's `terms`, so you can always see the cost before
|
|
151
|
+
you cancel.
|
|
152
|
+
|
|
153
|
+
### Things worth knowing before you build
|
|
154
|
+
|
|
155
|
+
- **A card on file is required for every hotel call, including search.** That is
|
|
156
|
+
unusual and it is deliberate: a hotel search opens a real session at the
|
|
157
|
+
supplier, and booking blocks a real rate. We would rather refuse up front than
|
|
158
|
+
let you reach the point of commitment and discover you cannot pay. The same
|
|
159
|
+
card that authorises flight booking authorises hotels — there is no separate
|
|
160
|
+
hotel signup.
|
|
161
|
+
- **Only free-cancellation, pay-later rates are sold.** Those are the rates where
|
|
162
|
+
the balance can safely be settled with the supplier after booking, which is
|
|
163
|
+
what makes 10%-now/rest-later work at all. You will see fewer results than a
|
|
164
|
+
metasearch shows you. Every one of them can actually be booked.
|
|
165
|
+
- **Booking is asynchronous.** `book_hotel` returns a `booking_job_id`, not a
|
|
166
|
+
booking — the real thing takes minutes. Poll `hotel_booking(job_id)` until
|
|
167
|
+
`status` is `succeeded` or `failed`, or call `book_hotel_and_wait` and let the
|
|
168
|
+
SDK do it. This is not ceremony: it is what makes it impossible to charge a
|
|
169
|
+
card and then lose the confirmation to a timeout.
|
|
170
|
+
- **The fee is charged before the room is committed.** A declined card therefore
|
|
171
|
+
costs nothing to unwind — no reservation exists and nothing is charged.
|
|
172
|
+
- **Do not retry a booking blindly.** Calling `book_hotel` twice for the same
|
|
173
|
+
rate books the room twice and charges two reservation fees.
|
|
174
|
+
- `price` is what the guest pays. There is no wholesale figure in the response to
|
|
175
|
+
quote by mistake.
|
|
176
|
+
|
|
177
|
+
### JavaScript
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
import { LetsFG } from 'letsfg';
|
|
181
|
+
const lfg = new LetsFG({ apiKey: process.env.LETSFG_API_KEY });
|
|
182
|
+
|
|
183
|
+
const [city] = await lfg.hotelDestinations('Warsaw');
|
|
184
|
+
const stays = await lfg.searchHotels({
|
|
185
|
+
cityId: city.Id, cityName: city.Name,
|
|
186
|
+
checkIn: '2026-11-10', checkOut: '2026-11-12', adults: 2,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const booking = await lfg.bookHotelAndWait({ /* ...offer + guest details... */ });
|
|
190
|
+
console.log(booking.confirmation, booking.pay_link);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### MCP
|
|
194
|
+
|
|
195
|
+
Five new tools, in the order you call them: `resolve_hotel_city` →
|
|
196
|
+
`search_hotels` → `book_hotel` → `get_hotel_booking` → `cancel_hotel_booking`.
|
|
197
|
+
|
|
@@ -1652,6 +1652,161 @@ var LetsFG = class {
|
|
|
1652
1652
|
if (idempotencyKey) body.idempotency_key = idempotencyKey;
|
|
1653
1653
|
return this.post("/developers/api/v1/bookings/book", body);
|
|
1654
1654
|
}
|
|
1655
|
+
// ── Hotels ──────────────────────────────────────────────────────────
|
|
1656
|
+
//
|
|
1657
|
+
// A card on file is required for EVERY hotel call, search included. That is
|
|
1658
|
+
// deliberate: a hotel search opens a real session at the supplier and booking
|
|
1659
|
+
// blocks a real rate, so a caller is never allowed to reach the point of
|
|
1660
|
+
// commitment only to discover it cannot pay. The same card that authorises
|
|
1661
|
+
// flight booking authorises hotels — there is no separate hotel enrolment.
|
|
1662
|
+
//
|
|
1663
|
+
// Only free-cancellation, pay-later rates are sold. Those are the rates where
|
|
1664
|
+
// the guest's balance can safely be settled with the supplier after booking,
|
|
1665
|
+
// which is what makes 10%-now/rest-later work. The result set is smaller than
|
|
1666
|
+
// a metasearch's, and every row in it can actually be booked.
|
|
1667
|
+
/**
|
|
1668
|
+
* Resolve a place name to the city id that searchHotels() needs.
|
|
1669
|
+
*
|
|
1670
|
+
* Use `Id` from the first result as `cityId` and `Name` as `cityName`.
|
|
1671
|
+
*/
|
|
1672
|
+
async hotelDestinations(text) {
|
|
1673
|
+
this.requireApiKey();
|
|
1674
|
+
const data = await this.post(
|
|
1675
|
+
"/developers/api/v1/hotels/destinations",
|
|
1676
|
+
{ text },
|
|
1677
|
+
6e4
|
|
1678
|
+
);
|
|
1679
|
+
return data.results ?? [];
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Search real, bookable hotel inventory.
|
|
1683
|
+
*
|
|
1684
|
+
* Slow by nature — the supplier streams a whole city and every rate is priced
|
|
1685
|
+
* — so this gets its own generous timeout rather than the client default.
|
|
1686
|
+
*
|
|
1687
|
+
* Each offer carries `price` (what the guest pays), `reservation_fee_now`
|
|
1688
|
+
* (the 10% taken at booking), `balance_to_supplier`, `balance_due_by` and
|
|
1689
|
+
* `free_cancellation_until`. There is no wholesale figure to quote by mistake.
|
|
1690
|
+
*
|
|
1691
|
+
* Keep `session_id` and the chosen offer's `combination_id_v2`: together they
|
|
1692
|
+
* identify the exact rate, and booking needs both.
|
|
1693
|
+
*/
|
|
1694
|
+
async searchHotels(params) {
|
|
1695
|
+
this.requireApiKey();
|
|
1696
|
+
const body = {
|
|
1697
|
+
city_id: params.cityId,
|
|
1698
|
+
city_name: params.cityName,
|
|
1699
|
+
check_in: params.checkIn,
|
|
1700
|
+
check_out: params.checkOut,
|
|
1701
|
+
adults: params.adults ?? 2,
|
|
1702
|
+
children: params.children ?? 0,
|
|
1703
|
+
nationality: params.nationality ?? "PL",
|
|
1704
|
+
limit: params.limit ?? 40,
|
|
1705
|
+
with_images: params.withImages ?? true
|
|
1706
|
+
};
|
|
1707
|
+
if (params.childAges?.length) body.child_ages = params.childAges;
|
|
1708
|
+
return this.post("/developers/api/v1/hotels/search", body, 24e4);
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Start a booking. Returns a job immediately — it does NOT book inline.
|
|
1712
|
+
*
|
|
1713
|
+
* A booking takes minutes: the rate is re-blocked at the supplier, every
|
|
1714
|
+
* price and date rail is checked, the 10% reservation fee is charged to your
|
|
1715
|
+
* card, and only then is the room committed. No proxy holds a connection that
|
|
1716
|
+
* long, so this returns at once and you poll hotelBooking() for the outcome.
|
|
1717
|
+
* Use bookHotelAndWait() if you would rather block.
|
|
1718
|
+
*
|
|
1719
|
+
* Because the fee is taken BEFORE the commit, a declined card costs nothing
|
|
1720
|
+
* to unwind: no reservation exists and nothing is charged.
|
|
1721
|
+
*
|
|
1722
|
+
* Send `expectedPrice` and `expectedBalance` back exactly as search returned
|
|
1723
|
+
* them — the booking is refused if the supplier has moved beyond tolerance,
|
|
1724
|
+
* so a guest is never charged a price they did not agree to.
|
|
1725
|
+
*
|
|
1726
|
+
* Do NOT call this again for the same rate while a job is running: that books
|
|
1727
|
+
* the room twice and charges two reservation fees.
|
|
1728
|
+
*/
|
|
1729
|
+
async bookHotel(params) {
|
|
1730
|
+
this.requireApiKey();
|
|
1731
|
+
const body = {
|
|
1732
|
+
session_id: params.sessionId,
|
|
1733
|
+
hotel_code: params.hotelCode,
|
|
1734
|
+
combination_id_v2: params.combinationIdV2,
|
|
1735
|
+
expected_price: params.expectedPrice,
|
|
1736
|
+
expected_balance: params.expectedBalance,
|
|
1737
|
+
city_id: params.cityId,
|
|
1738
|
+
city_name: params.cityName,
|
|
1739
|
+
check_in: params.checkIn,
|
|
1740
|
+
check_out: params.checkOut,
|
|
1741
|
+
adults: params.adults ?? 2,
|
|
1742
|
+
guests: params.guests,
|
|
1743
|
+
email: params.email,
|
|
1744
|
+
phone: params.phone,
|
|
1745
|
+
phone_country_code: params.phoneCountryCode ?? "48",
|
|
1746
|
+
special_requests: params.specialRequests ?? []
|
|
1747
|
+
};
|
|
1748
|
+
if (params.combinationId != null) body.combination_id = params.combinationId;
|
|
1749
|
+
if (params.hotelName) body.hotel_name = params.hotelName;
|
|
1750
|
+
return this.post("/developers/api/v1/hotels/book", body, 9e4);
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Collect the result of a booking started with bookHotel().
|
|
1754
|
+
*
|
|
1755
|
+
* `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
|
|
1756
|
+
* `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
|
|
1757
|
+
* `balance_due_by` and `terms` (including the full cancellation ladder).
|
|
1758
|
+
*/
|
|
1759
|
+
async hotelBooking(bookingJobId) {
|
|
1760
|
+
this.requireApiKey();
|
|
1761
|
+
return this.get(
|
|
1762
|
+
`/developers/api/v1/hotels/booking/${encodeURIComponent(bookingJobId)}`,
|
|
1763
|
+
6e4
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* bookHotel(), then poll until the booking settles. Convenience only.
|
|
1768
|
+
*
|
|
1769
|
+
* Giving up after `maxWaitMs` does NOT cancel anything — the booking may
|
|
1770
|
+
* still complete. The returned object carries `booking_job_id` so you can
|
|
1771
|
+
* keep polling, and the confirmation is emailed to the guest regardless.
|
|
1772
|
+
*/
|
|
1773
|
+
async bookHotelAndWait(params) {
|
|
1774
|
+
const pollIntervalMs = params.pollIntervalMs ?? 2e4;
|
|
1775
|
+
const maxWaitMs = params.maxWaitMs ?? 6e5;
|
|
1776
|
+
const job = await this.bookHotel(params);
|
|
1777
|
+
const jobId = job.booking_job_id;
|
|
1778
|
+
if (!jobId) return job;
|
|
1779
|
+
let waited = 0;
|
|
1780
|
+
let result = job;
|
|
1781
|
+
while (waited < maxWaitMs) {
|
|
1782
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1783
|
+
waited += pollIntervalMs;
|
|
1784
|
+
result = await this.hotelBooking(jobId);
|
|
1785
|
+
const st = result.status;
|
|
1786
|
+
if (st === "succeeded" || st === "failed") return result;
|
|
1787
|
+
}
|
|
1788
|
+
if (result.booking_job_id == null) result.booking_job_id = jobId;
|
|
1789
|
+
return result;
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Release a reservation at the supplier.
|
|
1793
|
+
*
|
|
1794
|
+
* Free until `balance_due_by`; after that the hotel's own cancellation ladder
|
|
1795
|
+
* applies and can reach 100%. The ladder ships in the booking's `terms`, so
|
|
1796
|
+
* you can see the cost before calling this. The 10% reservation fee is NOT
|
|
1797
|
+
* refunded.
|
|
1798
|
+
*
|
|
1799
|
+
* This drives a browser at the supplier and takes over a minute. If it times
|
|
1800
|
+
* out, do not assume it failed — re-check before retrying.
|
|
1801
|
+
*/
|
|
1802
|
+
async cancelHotel(confirmation) {
|
|
1803
|
+
this.requireApiKey();
|
|
1804
|
+
return this.post(
|
|
1805
|
+
"/developers/api/v1/hotels/cancel",
|
|
1806
|
+
{ confirmation },
|
|
1807
|
+
3e5
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1655
1810
|
/**
|
|
1656
1811
|
* [Developer API only] Attach a card to a PAID prepaid Developer API account.
|
|
1657
1812
|
*
|
|
@@ -1681,7 +1836,7 @@ var LetsFG = class {
|
|
|
1681
1836
|
const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1682
1837
|
const resp = await fetch(`${url}/developers/api/v1/agents/register`, {
|
|
1683
1838
|
method: "POST",
|
|
1684
|
-
headers: { "Content-Type": "application/json" },
|
|
1839
|
+
headers: { "Content-Type": "application/json", "User-Agent": "LetsFG-js/0.1.0" },
|
|
1685
1840
|
body: JSON.stringify({ agent_name: agentName, email, owner_name: ownerName, description })
|
|
1686
1841
|
});
|
|
1687
1842
|
const data = await resp.json();
|
|
@@ -1709,15 +1864,15 @@ var LetsFG = class {
|
|
|
1709
1864
|
const headers = this.usingPFS ? { "Authorization": `Bearer ${this.bearerToken}` } : { "X-API-Key": this.apiKey };
|
|
1710
1865
|
return this.requestWithHeaders(path, "POST", headers, body);
|
|
1711
1866
|
}
|
|
1712
|
-
async post(path, body) {
|
|
1713
|
-
return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body);
|
|
1867
|
+
async post(path, body, timeoutMs) {
|
|
1868
|
+
return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body, timeoutMs);
|
|
1714
1869
|
}
|
|
1715
|
-
async get(path) {
|
|
1716
|
-
return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey });
|
|
1870
|
+
async get(path, timeoutMs) {
|
|
1871
|
+
return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey }, void 0, timeoutMs);
|
|
1717
1872
|
}
|
|
1718
|
-
async requestWithHeaders(path, method, extraHeaders, body) {
|
|
1873
|
+
async requestWithHeaders(path, method, extraHeaders, body, timeoutMs) {
|
|
1719
1874
|
const controller = new AbortController();
|
|
1720
|
-
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
1875
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs ?? this.timeout);
|
|
1721
1876
|
try {
|
|
1722
1877
|
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
1723
1878
|
method,
|
package/dist/cli.js
CHANGED
|
@@ -407,6 +407,161 @@ var LetsFG = class {
|
|
|
407
407
|
if (idempotencyKey) body.idempotency_key = idempotencyKey;
|
|
408
408
|
return this.post("/developers/api/v1/bookings/book", body);
|
|
409
409
|
}
|
|
410
|
+
// ── Hotels ──────────────────────────────────────────────────────────
|
|
411
|
+
//
|
|
412
|
+
// A card on file is required for EVERY hotel call, search included. That is
|
|
413
|
+
// deliberate: a hotel search opens a real session at the supplier and booking
|
|
414
|
+
// blocks a real rate, so a caller is never allowed to reach the point of
|
|
415
|
+
// commitment only to discover it cannot pay. The same card that authorises
|
|
416
|
+
// flight booking authorises hotels — there is no separate hotel enrolment.
|
|
417
|
+
//
|
|
418
|
+
// Only free-cancellation, pay-later rates are sold. Those are the rates where
|
|
419
|
+
// the guest's balance can safely be settled with the supplier after booking,
|
|
420
|
+
// which is what makes 10%-now/rest-later work. The result set is smaller than
|
|
421
|
+
// a metasearch's, and every row in it can actually be booked.
|
|
422
|
+
/**
|
|
423
|
+
* Resolve a place name to the city id that searchHotels() needs.
|
|
424
|
+
*
|
|
425
|
+
* Use `Id` from the first result as `cityId` and `Name` as `cityName`.
|
|
426
|
+
*/
|
|
427
|
+
async hotelDestinations(text) {
|
|
428
|
+
this.requireApiKey();
|
|
429
|
+
const data = await this.post(
|
|
430
|
+
"/developers/api/v1/hotels/destinations",
|
|
431
|
+
{ text },
|
|
432
|
+
6e4
|
|
433
|
+
);
|
|
434
|
+
return data.results ?? [];
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Search real, bookable hotel inventory.
|
|
438
|
+
*
|
|
439
|
+
* Slow by nature — the supplier streams a whole city and every rate is priced
|
|
440
|
+
* — so this gets its own generous timeout rather than the client default.
|
|
441
|
+
*
|
|
442
|
+
* Each offer carries `price` (what the guest pays), `reservation_fee_now`
|
|
443
|
+
* (the 10% taken at booking), `balance_to_supplier`, `balance_due_by` and
|
|
444
|
+
* `free_cancellation_until`. There is no wholesale figure to quote by mistake.
|
|
445
|
+
*
|
|
446
|
+
* Keep `session_id` and the chosen offer's `combination_id_v2`: together they
|
|
447
|
+
* identify the exact rate, and booking needs both.
|
|
448
|
+
*/
|
|
449
|
+
async searchHotels(params) {
|
|
450
|
+
this.requireApiKey();
|
|
451
|
+
const body = {
|
|
452
|
+
city_id: params.cityId,
|
|
453
|
+
city_name: params.cityName,
|
|
454
|
+
check_in: params.checkIn,
|
|
455
|
+
check_out: params.checkOut,
|
|
456
|
+
adults: params.adults ?? 2,
|
|
457
|
+
children: params.children ?? 0,
|
|
458
|
+
nationality: params.nationality ?? "PL",
|
|
459
|
+
limit: params.limit ?? 40,
|
|
460
|
+
with_images: params.withImages ?? true
|
|
461
|
+
};
|
|
462
|
+
if (params.childAges?.length) body.child_ages = params.childAges;
|
|
463
|
+
return this.post("/developers/api/v1/hotels/search", body, 24e4);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Start a booking. Returns a job immediately — it does NOT book inline.
|
|
467
|
+
*
|
|
468
|
+
* A booking takes minutes: the rate is re-blocked at the supplier, every
|
|
469
|
+
* price and date rail is checked, the 10% reservation fee is charged to your
|
|
470
|
+
* card, and only then is the room committed. No proxy holds a connection that
|
|
471
|
+
* long, so this returns at once and you poll hotelBooking() for the outcome.
|
|
472
|
+
* Use bookHotelAndWait() if you would rather block.
|
|
473
|
+
*
|
|
474
|
+
* Because the fee is taken BEFORE the commit, a declined card costs nothing
|
|
475
|
+
* to unwind: no reservation exists and nothing is charged.
|
|
476
|
+
*
|
|
477
|
+
* Send `expectedPrice` and `expectedBalance` back exactly as search returned
|
|
478
|
+
* them — the booking is refused if the supplier has moved beyond tolerance,
|
|
479
|
+
* so a guest is never charged a price they did not agree to.
|
|
480
|
+
*
|
|
481
|
+
* Do NOT call this again for the same rate while a job is running: that books
|
|
482
|
+
* the room twice and charges two reservation fees.
|
|
483
|
+
*/
|
|
484
|
+
async bookHotel(params) {
|
|
485
|
+
this.requireApiKey();
|
|
486
|
+
const body = {
|
|
487
|
+
session_id: params.sessionId,
|
|
488
|
+
hotel_code: params.hotelCode,
|
|
489
|
+
combination_id_v2: params.combinationIdV2,
|
|
490
|
+
expected_price: params.expectedPrice,
|
|
491
|
+
expected_balance: params.expectedBalance,
|
|
492
|
+
city_id: params.cityId,
|
|
493
|
+
city_name: params.cityName,
|
|
494
|
+
check_in: params.checkIn,
|
|
495
|
+
check_out: params.checkOut,
|
|
496
|
+
adults: params.adults ?? 2,
|
|
497
|
+
guests: params.guests,
|
|
498
|
+
email: params.email,
|
|
499
|
+
phone: params.phone,
|
|
500
|
+
phone_country_code: params.phoneCountryCode ?? "48",
|
|
501
|
+
special_requests: params.specialRequests ?? []
|
|
502
|
+
};
|
|
503
|
+
if (params.combinationId != null) body.combination_id = params.combinationId;
|
|
504
|
+
if (params.hotelName) body.hotel_name = params.hotelName;
|
|
505
|
+
return this.post("/developers/api/v1/hotels/book", body, 9e4);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Collect the result of a booking started with bookHotel().
|
|
509
|
+
*
|
|
510
|
+
* `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
|
|
511
|
+
* `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
|
|
512
|
+
* `balance_due_by` and `terms` (including the full cancellation ladder).
|
|
513
|
+
*/
|
|
514
|
+
async hotelBooking(bookingJobId) {
|
|
515
|
+
this.requireApiKey();
|
|
516
|
+
return this.get(
|
|
517
|
+
`/developers/api/v1/hotels/booking/${encodeURIComponent(bookingJobId)}`,
|
|
518
|
+
6e4
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* bookHotel(), then poll until the booking settles. Convenience only.
|
|
523
|
+
*
|
|
524
|
+
* Giving up after `maxWaitMs` does NOT cancel anything — the booking may
|
|
525
|
+
* still complete. The returned object carries `booking_job_id` so you can
|
|
526
|
+
* keep polling, and the confirmation is emailed to the guest regardless.
|
|
527
|
+
*/
|
|
528
|
+
async bookHotelAndWait(params) {
|
|
529
|
+
const pollIntervalMs = params.pollIntervalMs ?? 2e4;
|
|
530
|
+
const maxWaitMs = params.maxWaitMs ?? 6e5;
|
|
531
|
+
const job = await this.bookHotel(params);
|
|
532
|
+
const jobId = job.booking_job_id;
|
|
533
|
+
if (!jobId) return job;
|
|
534
|
+
let waited = 0;
|
|
535
|
+
let result = job;
|
|
536
|
+
while (waited < maxWaitMs) {
|
|
537
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
538
|
+
waited += pollIntervalMs;
|
|
539
|
+
result = await this.hotelBooking(jobId);
|
|
540
|
+
const st = result.status;
|
|
541
|
+
if (st === "succeeded" || st === "failed") return result;
|
|
542
|
+
}
|
|
543
|
+
if (result.booking_job_id == null) result.booking_job_id = jobId;
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Release a reservation at the supplier.
|
|
548
|
+
*
|
|
549
|
+
* Free until `balance_due_by`; after that the hotel's own cancellation ladder
|
|
550
|
+
* applies and can reach 100%. The ladder ships in the booking's `terms`, so
|
|
551
|
+
* you can see the cost before calling this. The 10% reservation fee is NOT
|
|
552
|
+
* refunded.
|
|
553
|
+
*
|
|
554
|
+
* This drives a browser at the supplier and takes over a minute. If it times
|
|
555
|
+
* out, do not assume it failed — re-check before retrying.
|
|
556
|
+
*/
|
|
557
|
+
async cancelHotel(confirmation) {
|
|
558
|
+
this.requireApiKey();
|
|
559
|
+
return this.post(
|
|
560
|
+
"/developers/api/v1/hotels/cancel",
|
|
561
|
+
{ confirmation },
|
|
562
|
+
3e5
|
|
563
|
+
);
|
|
564
|
+
}
|
|
410
565
|
/**
|
|
411
566
|
* [Developer API only] Attach a card to a PAID prepaid Developer API account.
|
|
412
567
|
*
|
|
@@ -436,7 +591,7 @@ var LetsFG = class {
|
|
|
436
591
|
const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
437
592
|
const resp = await fetch(`${url}/developers/api/v1/agents/register`, {
|
|
438
593
|
method: "POST",
|
|
439
|
-
headers: { "Content-Type": "application/json" },
|
|
594
|
+
headers: { "Content-Type": "application/json", "User-Agent": "LetsFG-js/0.1.0" },
|
|
440
595
|
body: JSON.stringify({ agent_name: agentName, email, owner_name: ownerName, description })
|
|
441
596
|
});
|
|
442
597
|
const data = await resp.json();
|
|
@@ -464,15 +619,15 @@ var LetsFG = class {
|
|
|
464
619
|
const headers = this.usingPFS ? { "Authorization": `Bearer ${this.bearerToken}` } : { "X-API-Key": this.apiKey };
|
|
465
620
|
return this.requestWithHeaders(path, "POST", headers, body);
|
|
466
621
|
}
|
|
467
|
-
async post(path, body) {
|
|
468
|
-
return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body);
|
|
622
|
+
async post(path, body, timeoutMs) {
|
|
623
|
+
return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body, timeoutMs);
|
|
469
624
|
}
|
|
470
|
-
async get(path) {
|
|
471
|
-
return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey });
|
|
625
|
+
async get(path, timeoutMs) {
|
|
626
|
+
return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey }, void 0, timeoutMs);
|
|
472
627
|
}
|
|
473
|
-
async requestWithHeaders(path, method, extraHeaders, body) {
|
|
628
|
+
async requestWithHeaders(path, method, extraHeaders, body, timeoutMs) {
|
|
474
629
|
const controller = new AbortController();
|
|
475
|
-
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
630
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs ?? this.timeout);
|
|
476
631
|
try {
|
|
477
632
|
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
478
633
|
method,
|
package/dist/cli.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -480,6 +480,112 @@ declare class LetsFG {
|
|
|
480
480
|
* Always provide idempotencyKey to prevent double-bookings on retry.
|
|
481
481
|
*/
|
|
482
482
|
book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string): Promise<BookingResult>;
|
|
483
|
+
/**
|
|
484
|
+
* Resolve a place name to the city id that searchHotels() needs.
|
|
485
|
+
*
|
|
486
|
+
* Use `Id` from the first result as `cityId` and `Name` as `cityName`.
|
|
487
|
+
*/
|
|
488
|
+
hotelDestinations(text: string): Promise<Array<Record<string, unknown>>>;
|
|
489
|
+
/**
|
|
490
|
+
* Search real, bookable hotel inventory.
|
|
491
|
+
*
|
|
492
|
+
* Slow by nature — the supplier streams a whole city and every rate is priced
|
|
493
|
+
* — so this gets its own generous timeout rather than the client default.
|
|
494
|
+
*
|
|
495
|
+
* Each offer carries `price` (what the guest pays), `reservation_fee_now`
|
|
496
|
+
* (the 10% taken at booking), `balance_to_supplier`, `balance_due_by` and
|
|
497
|
+
* `free_cancellation_until`. There is no wholesale figure to quote by mistake.
|
|
498
|
+
*
|
|
499
|
+
* Keep `session_id` and the chosen offer's `combination_id_v2`: together they
|
|
500
|
+
* identify the exact rate, and booking needs both.
|
|
501
|
+
*/
|
|
502
|
+
searchHotels(params: {
|
|
503
|
+
cityId: number;
|
|
504
|
+
cityName: string;
|
|
505
|
+
checkIn: string;
|
|
506
|
+
checkOut: string;
|
|
507
|
+
adults?: number;
|
|
508
|
+
children?: number;
|
|
509
|
+
childAges?: number[];
|
|
510
|
+
/** Two-letter code. Rates and taxes genuinely differ by nationality. */
|
|
511
|
+
nationality?: string;
|
|
512
|
+
limit?: number;
|
|
513
|
+
withImages?: boolean;
|
|
514
|
+
}): Promise<Record<string, unknown>>;
|
|
515
|
+
/**
|
|
516
|
+
* Start a booking. Returns a job immediately — it does NOT book inline.
|
|
517
|
+
*
|
|
518
|
+
* A booking takes minutes: the rate is re-blocked at the supplier, every
|
|
519
|
+
* price and date rail is checked, the 10% reservation fee is charged to your
|
|
520
|
+
* card, and only then is the room committed. No proxy holds a connection that
|
|
521
|
+
* long, so this returns at once and you poll hotelBooking() for the outcome.
|
|
522
|
+
* Use bookHotelAndWait() if you would rather block.
|
|
523
|
+
*
|
|
524
|
+
* Because the fee is taken BEFORE the commit, a declined card costs nothing
|
|
525
|
+
* to unwind: no reservation exists and nothing is charged.
|
|
526
|
+
*
|
|
527
|
+
* Send `expectedPrice` and `expectedBalance` back exactly as search returned
|
|
528
|
+
* them — the booking is refused if the supplier has moved beyond tolerance,
|
|
529
|
+
* so a guest is never charged a price they did not agree to.
|
|
530
|
+
*
|
|
531
|
+
* Do NOT call this again for the same rate while a job is running: that books
|
|
532
|
+
* the room twice and charges two reservation fees.
|
|
533
|
+
*/
|
|
534
|
+
bookHotel(params: {
|
|
535
|
+
sessionId: string;
|
|
536
|
+
hotelCode: number;
|
|
537
|
+
combinationIdV2: string;
|
|
538
|
+
expectedPrice: number;
|
|
539
|
+
expectedBalance: number;
|
|
540
|
+
cityId: number;
|
|
541
|
+
cityName: string;
|
|
542
|
+
checkIn: string;
|
|
543
|
+
checkOut: string;
|
|
544
|
+
guests: Array<{
|
|
545
|
+
title: string;
|
|
546
|
+
first_name: string;
|
|
547
|
+
last_name: string;
|
|
548
|
+
}>;
|
|
549
|
+
/** The voucher and the pay link go here. A typo loses the booking. */
|
|
550
|
+
email: string;
|
|
551
|
+
phone: string;
|
|
552
|
+
adults?: number;
|
|
553
|
+
combinationId?: number;
|
|
554
|
+
hotelName?: string;
|
|
555
|
+
phoneCountryCode?: string;
|
|
556
|
+
specialRequests?: string[];
|
|
557
|
+
}): Promise<Record<string, unknown>>;
|
|
558
|
+
/**
|
|
559
|
+
* Collect the result of a booking started with bookHotel().
|
|
560
|
+
*
|
|
561
|
+
* `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
|
|
562
|
+
* `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
|
|
563
|
+
* `balance_due_by` and `terms` (including the full cancellation ladder).
|
|
564
|
+
*/
|
|
565
|
+
hotelBooking(bookingJobId: string): Promise<Record<string, unknown>>;
|
|
566
|
+
/**
|
|
567
|
+
* bookHotel(), then poll until the booking settles. Convenience only.
|
|
568
|
+
*
|
|
569
|
+
* Giving up after `maxWaitMs` does NOT cancel anything — the booking may
|
|
570
|
+
* still complete. The returned object carries `booking_job_id` so you can
|
|
571
|
+
* keep polling, and the confirmation is emailed to the guest regardless.
|
|
572
|
+
*/
|
|
573
|
+
bookHotelAndWait(params: Parameters<LetsFG['bookHotel']>[0] & {
|
|
574
|
+
pollIntervalMs?: number;
|
|
575
|
+
maxWaitMs?: number;
|
|
576
|
+
}): Promise<Record<string, unknown>>;
|
|
577
|
+
/**
|
|
578
|
+
* Release a reservation at the supplier.
|
|
579
|
+
*
|
|
580
|
+
* Free until `balance_due_by`; after that the hotel's own cancellation ladder
|
|
581
|
+
* applies and can reach 100%. The ladder ships in the booking's `terms`, so
|
|
582
|
+
* you can see the cost before calling this. The 10% reservation fee is NOT
|
|
583
|
+
* refunded.
|
|
584
|
+
*
|
|
585
|
+
* This drives a browser at the supplier and takes over a minute. If it times
|
|
586
|
+
* out, do not assume it failed — re-check before retrying.
|
|
587
|
+
*/
|
|
588
|
+
cancelHotel(confirmation: string): Promise<Record<string, unknown>>;
|
|
483
589
|
/**
|
|
484
590
|
* [Developer API only] Attach a card to a PAID prepaid Developer API account.
|
|
485
591
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -480,6 +480,112 @@ declare class LetsFG {
|
|
|
480
480
|
* Always provide idempotencyKey to prevent double-bookings on retry.
|
|
481
481
|
*/
|
|
482
482
|
book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string): Promise<BookingResult>;
|
|
483
|
+
/**
|
|
484
|
+
* Resolve a place name to the city id that searchHotels() needs.
|
|
485
|
+
*
|
|
486
|
+
* Use `Id` from the first result as `cityId` and `Name` as `cityName`.
|
|
487
|
+
*/
|
|
488
|
+
hotelDestinations(text: string): Promise<Array<Record<string, unknown>>>;
|
|
489
|
+
/**
|
|
490
|
+
* Search real, bookable hotel inventory.
|
|
491
|
+
*
|
|
492
|
+
* Slow by nature — the supplier streams a whole city and every rate is priced
|
|
493
|
+
* — so this gets its own generous timeout rather than the client default.
|
|
494
|
+
*
|
|
495
|
+
* Each offer carries `price` (what the guest pays), `reservation_fee_now`
|
|
496
|
+
* (the 10% taken at booking), `balance_to_supplier`, `balance_due_by` and
|
|
497
|
+
* `free_cancellation_until`. There is no wholesale figure to quote by mistake.
|
|
498
|
+
*
|
|
499
|
+
* Keep `session_id` and the chosen offer's `combination_id_v2`: together they
|
|
500
|
+
* identify the exact rate, and booking needs both.
|
|
501
|
+
*/
|
|
502
|
+
searchHotels(params: {
|
|
503
|
+
cityId: number;
|
|
504
|
+
cityName: string;
|
|
505
|
+
checkIn: string;
|
|
506
|
+
checkOut: string;
|
|
507
|
+
adults?: number;
|
|
508
|
+
children?: number;
|
|
509
|
+
childAges?: number[];
|
|
510
|
+
/** Two-letter code. Rates and taxes genuinely differ by nationality. */
|
|
511
|
+
nationality?: string;
|
|
512
|
+
limit?: number;
|
|
513
|
+
withImages?: boolean;
|
|
514
|
+
}): Promise<Record<string, unknown>>;
|
|
515
|
+
/**
|
|
516
|
+
* Start a booking. Returns a job immediately — it does NOT book inline.
|
|
517
|
+
*
|
|
518
|
+
* A booking takes minutes: the rate is re-blocked at the supplier, every
|
|
519
|
+
* price and date rail is checked, the 10% reservation fee is charged to your
|
|
520
|
+
* card, and only then is the room committed. No proxy holds a connection that
|
|
521
|
+
* long, so this returns at once and you poll hotelBooking() for the outcome.
|
|
522
|
+
* Use bookHotelAndWait() if you would rather block.
|
|
523
|
+
*
|
|
524
|
+
* Because the fee is taken BEFORE the commit, a declined card costs nothing
|
|
525
|
+
* to unwind: no reservation exists and nothing is charged.
|
|
526
|
+
*
|
|
527
|
+
* Send `expectedPrice` and `expectedBalance` back exactly as search returned
|
|
528
|
+
* them — the booking is refused if the supplier has moved beyond tolerance,
|
|
529
|
+
* so a guest is never charged a price they did not agree to.
|
|
530
|
+
*
|
|
531
|
+
* Do NOT call this again for the same rate while a job is running: that books
|
|
532
|
+
* the room twice and charges two reservation fees.
|
|
533
|
+
*/
|
|
534
|
+
bookHotel(params: {
|
|
535
|
+
sessionId: string;
|
|
536
|
+
hotelCode: number;
|
|
537
|
+
combinationIdV2: string;
|
|
538
|
+
expectedPrice: number;
|
|
539
|
+
expectedBalance: number;
|
|
540
|
+
cityId: number;
|
|
541
|
+
cityName: string;
|
|
542
|
+
checkIn: string;
|
|
543
|
+
checkOut: string;
|
|
544
|
+
guests: Array<{
|
|
545
|
+
title: string;
|
|
546
|
+
first_name: string;
|
|
547
|
+
last_name: string;
|
|
548
|
+
}>;
|
|
549
|
+
/** The voucher and the pay link go here. A typo loses the booking. */
|
|
550
|
+
email: string;
|
|
551
|
+
phone: string;
|
|
552
|
+
adults?: number;
|
|
553
|
+
combinationId?: number;
|
|
554
|
+
hotelName?: string;
|
|
555
|
+
phoneCountryCode?: string;
|
|
556
|
+
specialRequests?: string[];
|
|
557
|
+
}): Promise<Record<string, unknown>>;
|
|
558
|
+
/**
|
|
559
|
+
* Collect the result of a booking started with bookHotel().
|
|
560
|
+
*
|
|
561
|
+
* `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
|
|
562
|
+
* `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
|
|
563
|
+
* `balance_due_by` and `terms` (including the full cancellation ladder).
|
|
564
|
+
*/
|
|
565
|
+
hotelBooking(bookingJobId: string): Promise<Record<string, unknown>>;
|
|
566
|
+
/**
|
|
567
|
+
* bookHotel(), then poll until the booking settles. Convenience only.
|
|
568
|
+
*
|
|
569
|
+
* Giving up after `maxWaitMs` does NOT cancel anything — the booking may
|
|
570
|
+
* still complete. The returned object carries `booking_job_id` so you can
|
|
571
|
+
* keep polling, and the confirmation is emailed to the guest regardless.
|
|
572
|
+
*/
|
|
573
|
+
bookHotelAndWait(params: Parameters<LetsFG['bookHotel']>[0] & {
|
|
574
|
+
pollIntervalMs?: number;
|
|
575
|
+
maxWaitMs?: number;
|
|
576
|
+
}): Promise<Record<string, unknown>>;
|
|
577
|
+
/**
|
|
578
|
+
* Release a reservation at the supplier.
|
|
579
|
+
*
|
|
580
|
+
* Free until `balance_due_by`; after that the hotel's own cancellation ladder
|
|
581
|
+
* applies and can reach 100%. The ladder ships in the booking's `terms`, so
|
|
582
|
+
* you can see the cost before calling this. The 10% reservation fee is NOT
|
|
583
|
+
* refunded.
|
|
584
|
+
*
|
|
585
|
+
* This drives a browser at the supplier and takes over a minute. If it times
|
|
586
|
+
* out, do not assume it failed — re-check before retrying.
|
|
587
|
+
*/
|
|
588
|
+
cancelHotel(confirmation: string): Promise<Record<string, unknown>>;
|
|
483
589
|
/**
|
|
484
590
|
* [Developer API only] Attach a card to a PAID prepaid Developer API account.
|
|
485
591
|
*
|
package/dist/index.js
CHANGED
|
@@ -1700,6 +1700,161 @@ var LetsFG = class {
|
|
|
1700
1700
|
if (idempotencyKey) body.idempotency_key = idempotencyKey;
|
|
1701
1701
|
return this.post("/developers/api/v1/bookings/book", body);
|
|
1702
1702
|
}
|
|
1703
|
+
// ── Hotels ──────────────────────────────────────────────────────────
|
|
1704
|
+
//
|
|
1705
|
+
// A card on file is required for EVERY hotel call, search included. That is
|
|
1706
|
+
// deliberate: a hotel search opens a real session at the supplier and booking
|
|
1707
|
+
// blocks a real rate, so a caller is never allowed to reach the point of
|
|
1708
|
+
// commitment only to discover it cannot pay. The same card that authorises
|
|
1709
|
+
// flight booking authorises hotels — there is no separate hotel enrolment.
|
|
1710
|
+
//
|
|
1711
|
+
// Only free-cancellation, pay-later rates are sold. Those are the rates where
|
|
1712
|
+
// the guest's balance can safely be settled with the supplier after booking,
|
|
1713
|
+
// which is what makes 10%-now/rest-later work. The result set is smaller than
|
|
1714
|
+
// a metasearch's, and every row in it can actually be booked.
|
|
1715
|
+
/**
|
|
1716
|
+
* Resolve a place name to the city id that searchHotels() needs.
|
|
1717
|
+
*
|
|
1718
|
+
* Use `Id` from the first result as `cityId` and `Name` as `cityName`.
|
|
1719
|
+
*/
|
|
1720
|
+
async hotelDestinations(text) {
|
|
1721
|
+
this.requireApiKey();
|
|
1722
|
+
const data = await this.post(
|
|
1723
|
+
"/developers/api/v1/hotels/destinations",
|
|
1724
|
+
{ text },
|
|
1725
|
+
6e4
|
|
1726
|
+
);
|
|
1727
|
+
return data.results ?? [];
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Search real, bookable hotel inventory.
|
|
1731
|
+
*
|
|
1732
|
+
* Slow by nature — the supplier streams a whole city and every rate is priced
|
|
1733
|
+
* — so this gets its own generous timeout rather than the client default.
|
|
1734
|
+
*
|
|
1735
|
+
* Each offer carries `price` (what the guest pays), `reservation_fee_now`
|
|
1736
|
+
* (the 10% taken at booking), `balance_to_supplier`, `balance_due_by` and
|
|
1737
|
+
* `free_cancellation_until`. There is no wholesale figure to quote by mistake.
|
|
1738
|
+
*
|
|
1739
|
+
* Keep `session_id` and the chosen offer's `combination_id_v2`: together they
|
|
1740
|
+
* identify the exact rate, and booking needs both.
|
|
1741
|
+
*/
|
|
1742
|
+
async searchHotels(params) {
|
|
1743
|
+
this.requireApiKey();
|
|
1744
|
+
const body = {
|
|
1745
|
+
city_id: params.cityId,
|
|
1746
|
+
city_name: params.cityName,
|
|
1747
|
+
check_in: params.checkIn,
|
|
1748
|
+
check_out: params.checkOut,
|
|
1749
|
+
adults: params.adults ?? 2,
|
|
1750
|
+
children: params.children ?? 0,
|
|
1751
|
+
nationality: params.nationality ?? "PL",
|
|
1752
|
+
limit: params.limit ?? 40,
|
|
1753
|
+
with_images: params.withImages ?? true
|
|
1754
|
+
};
|
|
1755
|
+
if (params.childAges?.length) body.child_ages = params.childAges;
|
|
1756
|
+
return this.post("/developers/api/v1/hotels/search", body, 24e4);
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Start a booking. Returns a job immediately — it does NOT book inline.
|
|
1760
|
+
*
|
|
1761
|
+
* A booking takes minutes: the rate is re-blocked at the supplier, every
|
|
1762
|
+
* price and date rail is checked, the 10% reservation fee is charged to your
|
|
1763
|
+
* card, and only then is the room committed. No proxy holds a connection that
|
|
1764
|
+
* long, so this returns at once and you poll hotelBooking() for the outcome.
|
|
1765
|
+
* Use bookHotelAndWait() if you would rather block.
|
|
1766
|
+
*
|
|
1767
|
+
* Because the fee is taken BEFORE the commit, a declined card costs nothing
|
|
1768
|
+
* to unwind: no reservation exists and nothing is charged.
|
|
1769
|
+
*
|
|
1770
|
+
* Send `expectedPrice` and `expectedBalance` back exactly as search returned
|
|
1771
|
+
* them — the booking is refused if the supplier has moved beyond tolerance,
|
|
1772
|
+
* so a guest is never charged a price they did not agree to.
|
|
1773
|
+
*
|
|
1774
|
+
* Do NOT call this again for the same rate while a job is running: that books
|
|
1775
|
+
* the room twice and charges two reservation fees.
|
|
1776
|
+
*/
|
|
1777
|
+
async bookHotel(params) {
|
|
1778
|
+
this.requireApiKey();
|
|
1779
|
+
const body = {
|
|
1780
|
+
session_id: params.sessionId,
|
|
1781
|
+
hotel_code: params.hotelCode,
|
|
1782
|
+
combination_id_v2: params.combinationIdV2,
|
|
1783
|
+
expected_price: params.expectedPrice,
|
|
1784
|
+
expected_balance: params.expectedBalance,
|
|
1785
|
+
city_id: params.cityId,
|
|
1786
|
+
city_name: params.cityName,
|
|
1787
|
+
check_in: params.checkIn,
|
|
1788
|
+
check_out: params.checkOut,
|
|
1789
|
+
adults: params.adults ?? 2,
|
|
1790
|
+
guests: params.guests,
|
|
1791
|
+
email: params.email,
|
|
1792
|
+
phone: params.phone,
|
|
1793
|
+
phone_country_code: params.phoneCountryCode ?? "48",
|
|
1794
|
+
special_requests: params.specialRequests ?? []
|
|
1795
|
+
};
|
|
1796
|
+
if (params.combinationId != null) body.combination_id = params.combinationId;
|
|
1797
|
+
if (params.hotelName) body.hotel_name = params.hotelName;
|
|
1798
|
+
return this.post("/developers/api/v1/hotels/book", body, 9e4);
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Collect the result of a booking started with bookHotel().
|
|
1802
|
+
*
|
|
1803
|
+
* `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
|
|
1804
|
+
* `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
|
|
1805
|
+
* `balance_due_by` and `terms` (including the full cancellation ladder).
|
|
1806
|
+
*/
|
|
1807
|
+
async hotelBooking(bookingJobId) {
|
|
1808
|
+
this.requireApiKey();
|
|
1809
|
+
return this.get(
|
|
1810
|
+
`/developers/api/v1/hotels/booking/${encodeURIComponent(bookingJobId)}`,
|
|
1811
|
+
6e4
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* bookHotel(), then poll until the booking settles. Convenience only.
|
|
1816
|
+
*
|
|
1817
|
+
* Giving up after `maxWaitMs` does NOT cancel anything — the booking may
|
|
1818
|
+
* still complete. The returned object carries `booking_job_id` so you can
|
|
1819
|
+
* keep polling, and the confirmation is emailed to the guest regardless.
|
|
1820
|
+
*/
|
|
1821
|
+
async bookHotelAndWait(params) {
|
|
1822
|
+
const pollIntervalMs = params.pollIntervalMs ?? 2e4;
|
|
1823
|
+
const maxWaitMs = params.maxWaitMs ?? 6e5;
|
|
1824
|
+
const job = await this.bookHotel(params);
|
|
1825
|
+
const jobId = job.booking_job_id;
|
|
1826
|
+
if (!jobId) return job;
|
|
1827
|
+
let waited = 0;
|
|
1828
|
+
let result = job;
|
|
1829
|
+
while (waited < maxWaitMs) {
|
|
1830
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1831
|
+
waited += pollIntervalMs;
|
|
1832
|
+
result = await this.hotelBooking(jobId);
|
|
1833
|
+
const st = result.status;
|
|
1834
|
+
if (st === "succeeded" || st === "failed") return result;
|
|
1835
|
+
}
|
|
1836
|
+
if (result.booking_job_id == null) result.booking_job_id = jobId;
|
|
1837
|
+
return result;
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Release a reservation at the supplier.
|
|
1841
|
+
*
|
|
1842
|
+
* Free until `balance_due_by`; after that the hotel's own cancellation ladder
|
|
1843
|
+
* applies and can reach 100%. The ladder ships in the booking's `terms`, so
|
|
1844
|
+
* you can see the cost before calling this. The 10% reservation fee is NOT
|
|
1845
|
+
* refunded.
|
|
1846
|
+
*
|
|
1847
|
+
* This drives a browser at the supplier and takes over a minute. If it times
|
|
1848
|
+
* out, do not assume it failed — re-check before retrying.
|
|
1849
|
+
*/
|
|
1850
|
+
async cancelHotel(confirmation) {
|
|
1851
|
+
this.requireApiKey();
|
|
1852
|
+
return this.post(
|
|
1853
|
+
"/developers/api/v1/hotels/cancel",
|
|
1854
|
+
{ confirmation },
|
|
1855
|
+
3e5
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1703
1858
|
/**
|
|
1704
1859
|
* [Developer API only] Attach a card to a PAID prepaid Developer API account.
|
|
1705
1860
|
*
|
|
@@ -1729,7 +1884,7 @@ var LetsFG = class {
|
|
|
1729
1884
|
const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1730
1885
|
const resp = await fetch(`${url}/developers/api/v1/agents/register`, {
|
|
1731
1886
|
method: "POST",
|
|
1732
|
-
headers: { "Content-Type": "application/json" },
|
|
1887
|
+
headers: { "Content-Type": "application/json", "User-Agent": "LetsFG-js/0.1.0" },
|
|
1733
1888
|
body: JSON.stringify({ agent_name: agentName, email, owner_name: ownerName, description })
|
|
1734
1889
|
});
|
|
1735
1890
|
const data = await resp.json();
|
|
@@ -1757,15 +1912,15 @@ var LetsFG = class {
|
|
|
1757
1912
|
const headers = this.usingPFS ? { "Authorization": `Bearer ${this.bearerToken}` } : { "X-API-Key": this.apiKey };
|
|
1758
1913
|
return this.requestWithHeaders(path, "POST", headers, body);
|
|
1759
1914
|
}
|
|
1760
|
-
async post(path, body) {
|
|
1761
|
-
return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body);
|
|
1915
|
+
async post(path, body, timeoutMs) {
|
|
1916
|
+
return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body, timeoutMs);
|
|
1762
1917
|
}
|
|
1763
|
-
async get(path) {
|
|
1764
|
-
return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey });
|
|
1918
|
+
async get(path, timeoutMs) {
|
|
1919
|
+
return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey }, void 0, timeoutMs);
|
|
1765
1920
|
}
|
|
1766
|
-
async requestWithHeaders(path, method, extraHeaders, body) {
|
|
1921
|
+
async requestWithHeaders(path, method, extraHeaders, body, timeoutMs) {
|
|
1767
1922
|
const controller = new AbortController();
|
|
1768
|
-
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
1923
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs ?? this.timeout);
|
|
1769
1924
|
try {
|
|
1770
1925
|
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
1771
1926
|
method,
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "letsfg",
|
|
3
|
-
"version": "2026.5.
|
|
4
|
-
"description": "Flight search & booking for AI agents. Server-side engine covers hundreds of airlines. Free search via Bearer token or prepaid Developer API. 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",
|
|
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
|
-
],
|
|
36
|
-
"author": "LetsFG",
|
|
37
|
-
"license": "MIT",
|
|
38
|
-
"repository": {
|
|
39
|
-
"type": "git",
|
|
40
|
-
"url": "https://github.com/LetsFG/LetsFG.git"
|
|
41
|
-
},
|
|
42
|
-
"homepage": "https://letsfg.co",
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"@types/node": "^25.3.3",
|
|
45
|
-
"tsup": "^8.0.0",
|
|
46
|
-
"tsx": "^4.19.0",
|
|
47
|
-
"typescript": "^5.3.0"
|
|
48
|
-
},
|
|
49
|
-
"engines": {
|
|
50
|
-
"node": ">=18.0.0"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "letsfg",
|
|
3
|
+
"version": "2026.5.63",
|
|
4
|
+
"description": "Flight search & booking for AI agents. Server-side engine covers hundreds of airlines. Free search via Bearer token or prepaid Developer API. 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",
|
|
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
|
+
],
|
|
36
|
+
"author": "LetsFG",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/LetsFG/LetsFG.git"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://letsfg.co",
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^25.3.3",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"tsx": "^4.19.0",
|
|
47
|
+
"typescript": "^5.3.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|