letsfg 1.0.0

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 ADDED
@@ -0,0 +1,121 @@
1
+ # BoostedTravel — Agent-Native Flight Search & Booking (Node.js)
2
+
3
+ Search 400+ airlines at raw airline prices — **$20-50 cheaper** than Booking.com, Kayak, and other OTAs. Zero dependencies. Built for autonomous AI agents — works with OpenClaw, Perplexity Computer, Claude, Cursor, Windsurf, and any MCP-compatible client.
4
+
5
+ > 🎥 **[Watch the demo](https://github.com/Boosted-Chat/LetsFG#demo-boostedtravel-vs-default-agent-search)** — side-by-side comparison of default agent search vs BoostedTravel CLI.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install boostedtravel
11
+ ```
12
+
13
+ ## Quick Start (SDK)
14
+
15
+ ```typescript
16
+ import { BoostedTravel, cheapestOffer, offerSummary } from 'boostedtravel';
17
+
18
+ // Register (one-time)
19
+ const creds = await BoostedTravel.register('my-agent', 'agent@example.com');
20
+ console.log(creds.api_key); // Save this
21
+
22
+ // Use
23
+ const bt = new BoostedTravel({ apiKey: 'trav_...' });
24
+
25
+ // Search — FREE
26
+ const flights = await bt.search('GDN', 'BER', '2026-03-03');
27
+ const best = cheapestOffer(flights);
28
+ console.log(offerSummary(best));
29
+
30
+ // Unlock — $1
31
+ const unlock = await bt.unlock(best.id);
32
+
33
+ // Book — FREE after unlock
34
+ const booking = await bt.book(
35
+ best.id,
36
+ [{
37
+ id: flights.passenger_ids[0],
38
+ given_name: 'John',
39
+ family_name: 'Doe',
40
+ born_on: '1990-01-15',
41
+ gender: 'm',
42
+ title: 'mr',
43
+ email: 'john@example.com',
44
+ }],
45
+ 'john@example.com'
46
+ );
47
+ console.log(`PNR: ${booking.booking_reference}`);
48
+ ```
49
+
50
+ ## Quick Start (CLI)
51
+
52
+ ```bash
53
+ export BOOSTEDTRAVEL_API_KEY=trav_...
54
+
55
+ boostedtravel search GDN BER 2026-03-03 --sort price
56
+ boostedtravel search LON BCN 2026-04-01 --json # Machine-readable
57
+ boostedtravel unlock off_xxx
58
+ boostedtravel book off_xxx -p '{"id":"pas_xxx","given_name":"John",...}' -e john@example.com
59
+ ```
60
+
61
+ ## API
62
+
63
+ ### `new BoostedTravel({ apiKey, baseUrl?, timeout? })`
64
+
65
+ ### `bt.search(origin, destination, dateFrom, options?)`
66
+ ### `bt.resolveLocation(query)`
67
+ ### `bt.unlock(offerId)`
68
+ ### `bt.book(offerId, passengers, contactEmail, contactPhone?)`
69
+ ### `bt.setupPayment(token?)`
70
+ ### `bt.me()`
71
+ ### `BoostedTravel.register(agentName, email, baseUrl?, ownerName?, description?)`
72
+
73
+ ### Helpers
74
+ - `offerSummary(offer)` — One-line string summary
75
+ - `cheapestOffer(result)` — Get cheapest offer from search
76
+
77
+ ### `searchLocal(origin, destination, dateFrom, options?)`
78
+
79
+ Search 75 airline connectors locally (no API key needed). Requires Python + `boostedtravel` installed.
80
+
81
+ ```typescript
82
+ import { searchLocal } from 'boostedtravel';
83
+
84
+ const result = await searchLocal('GDN', 'BCN', '2026-06-15');
85
+ console.log(result.total_results);
86
+
87
+ // Limit browser concurrency for constrained environments
88
+ const result2 = await searchLocal('GDN', 'BCN', '2026-06-15', { maxBrowsers: 4 });
89
+ ```
90
+
91
+ ### `systemInfo()`
92
+
93
+ Get system resource profile and recommended concurrency settings.
94
+
95
+ ```typescript
96
+ import { systemInfo } from 'boostedtravel';
97
+
98
+ const info = await systemInfo();
99
+ console.log(info);
100
+ // { platform: 'win32', cpu_cores: 16, ram_total_gb: 31.2, ram_available_gb: 14.7,
101
+ // tier: 'standard', recommended_max_browsers: 8, current_max_browsers: 8 }
102
+ ```
103
+
104
+ ## Zero Dependencies
105
+
106
+ Uses native `fetch` (Node 18+). No `axios`, no `node-fetch`, nothing. Safe for sandboxed environments.
107
+
108
+ ## Performance Tuning
109
+
110
+ Local search auto-scales browser concurrency based on available RAM. Override with `maxBrowsers`:
111
+
112
+ ```typescript
113
+ // Limit to 4 concurrent browsers
114
+ await searchLocal('LHR', 'BCN', '2026-04-15', { maxBrowsers: 4 });
115
+ ```
116
+
117
+ Or set the `BOOSTEDTRAVEL_MAX_BROWSERS` environment variable globally.
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,460 @@
1
+ // src/index.ts
2
+ var ErrorCode = {
3
+ // Transient (safe to retry after short delay)
4
+ SUPPLIER_TIMEOUT: "SUPPLIER_TIMEOUT",
5
+ RATE_LIMITED: "RATE_LIMITED",
6
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
7
+ NETWORK_ERROR: "NETWORK_ERROR",
8
+ // Validation (fix input, then retry)
9
+ INVALID_IATA: "INVALID_IATA",
10
+ INVALID_DATE: "INVALID_DATE",
11
+ INVALID_PASSENGERS: "INVALID_PASSENGERS",
12
+ UNSUPPORTED_ROUTE: "UNSUPPORTED_ROUTE",
13
+ MISSING_PARAMETER: "MISSING_PARAMETER",
14
+ INVALID_PARAMETER: "INVALID_PARAMETER",
15
+ // Business (requires human decision)
16
+ AUTH_INVALID: "AUTH_INVALID",
17
+ PAYMENT_REQUIRED: "PAYMENT_REQUIRED",
18
+ PAYMENT_DECLINED: "PAYMENT_DECLINED",
19
+ OFFER_EXPIRED: "OFFER_EXPIRED",
20
+ OFFER_NOT_UNLOCKED: "OFFER_NOT_UNLOCKED",
21
+ FARE_CHANGED: "FARE_CHANGED",
22
+ ALREADY_BOOKED: "ALREADY_BOOKED",
23
+ BOOKING_FAILED: "BOOKING_FAILED"
24
+ };
25
+ var ErrorCategory = {
26
+ TRANSIENT: "transient",
27
+ VALIDATION: "validation",
28
+ BUSINESS: "business"
29
+ };
30
+ var CODE_TO_CATEGORY = {
31
+ [ErrorCode.SUPPLIER_TIMEOUT]: ErrorCategory.TRANSIENT,
32
+ [ErrorCode.RATE_LIMITED]: ErrorCategory.TRANSIENT,
33
+ [ErrorCode.SERVICE_UNAVAILABLE]: ErrorCategory.TRANSIENT,
34
+ [ErrorCode.NETWORK_ERROR]: ErrorCategory.TRANSIENT,
35
+ [ErrorCode.INVALID_IATA]: ErrorCategory.VALIDATION,
36
+ [ErrorCode.INVALID_DATE]: ErrorCategory.VALIDATION,
37
+ [ErrorCode.INVALID_PASSENGERS]: ErrorCategory.VALIDATION,
38
+ [ErrorCode.UNSUPPORTED_ROUTE]: ErrorCategory.VALIDATION,
39
+ [ErrorCode.MISSING_PARAMETER]: ErrorCategory.VALIDATION,
40
+ [ErrorCode.INVALID_PARAMETER]: ErrorCategory.VALIDATION,
41
+ [ErrorCode.AUTH_INVALID]: ErrorCategory.BUSINESS,
42
+ [ErrorCode.PAYMENT_REQUIRED]: ErrorCategory.BUSINESS,
43
+ [ErrorCode.PAYMENT_DECLINED]: ErrorCategory.BUSINESS,
44
+ [ErrorCode.OFFER_EXPIRED]: ErrorCategory.BUSINESS,
45
+ [ErrorCode.OFFER_NOT_UNLOCKED]: ErrorCategory.BUSINESS,
46
+ [ErrorCode.FARE_CHANGED]: ErrorCategory.BUSINESS,
47
+ [ErrorCode.ALREADY_BOOKED]: ErrorCategory.BUSINESS,
48
+ [ErrorCode.BOOKING_FAILED]: ErrorCategory.BUSINESS
49
+ };
50
+ function inferErrorCode(statusCode, detail) {
51
+ const d = detail.toLowerCase();
52
+ if (statusCode === 401) return ErrorCode.AUTH_INVALID;
53
+ if (statusCode === 402) return d.includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
54
+ if (statusCode === 410) return ErrorCode.OFFER_EXPIRED;
55
+ if (statusCode === 422) {
56
+ if (d.includes("iata") || d.includes("airport")) return ErrorCode.INVALID_IATA;
57
+ if (d.includes("date")) return ErrorCode.INVALID_DATE;
58
+ if (d.includes("passenger")) return ErrorCode.INVALID_PASSENGERS;
59
+ if (d.includes("route")) return ErrorCode.UNSUPPORTED_ROUTE;
60
+ return ErrorCode.INVALID_PARAMETER;
61
+ }
62
+ if (statusCode === 429) return ErrorCode.RATE_LIMITED;
63
+ if (statusCode === 503) return ErrorCode.SERVICE_UNAVAILABLE;
64
+ if (statusCode === 504) return ErrorCode.SUPPLIER_TIMEOUT;
65
+ if (statusCode === 409) return ErrorCode.ALREADY_BOOKED;
66
+ return statusCode >= 500 ? ErrorCode.BOOKING_FAILED : ErrorCode.INVALID_PARAMETER;
67
+ }
68
+ var LetsFGError = class extends Error {
69
+ statusCode;
70
+ response;
71
+ errorCode;
72
+ errorCategory;
73
+ isRetryable;
74
+ constructor(message, statusCode = 0, response = {}, errorCode = "") {
75
+ super(message);
76
+ this.name = "LetsFGError";
77
+ this.statusCode = statusCode;
78
+ this.response = response;
79
+ this.errorCode = errorCode || response.error_code || "";
80
+ this.errorCategory = CODE_TO_CATEGORY[this.errorCode] || ErrorCategory.BUSINESS;
81
+ this.isRetryable = this.errorCategory === ErrorCategory.TRANSIENT;
82
+ }
83
+ };
84
+ var AuthenticationError = class extends LetsFGError {
85
+ constructor(message, response = {}) {
86
+ super(message, 401, response, ErrorCode.AUTH_INVALID);
87
+ this.name = "AuthenticationError";
88
+ }
89
+ };
90
+ var PaymentRequiredError = class extends LetsFGError {
91
+ constructor(message, response = {}) {
92
+ const code = message.toLowerCase().includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
93
+ super(message, 402, response, code);
94
+ this.name = "PaymentRequiredError";
95
+ }
96
+ };
97
+ var OfferExpiredError = class extends LetsFGError {
98
+ constructor(message, response = {}) {
99
+ super(message, 410, response, ErrorCode.OFFER_EXPIRED);
100
+ this.name = "OfferExpiredError";
101
+ }
102
+ };
103
+ var ValidationError = class extends LetsFGError {
104
+ constructor(message, statusCode = 422, response = {}, errorCode = "") {
105
+ super(message, statusCode, response, errorCode || ErrorCode.INVALID_PARAMETER);
106
+ this.name = "ValidationError";
107
+ }
108
+ };
109
+ function routeStr(route) {
110
+ if (!route.segments.length) return "";
111
+ const codes = [route.segments[0].origin, ...route.segments.map((s) => s.destination)];
112
+ return codes.join(" \u2192 ");
113
+ }
114
+ function durationHuman(seconds) {
115
+ const h = Math.floor(seconds / 3600);
116
+ const m = Math.floor(seconds % 3600 / 60);
117
+ return `${h}h${m.toString().padStart(2, "0")}m`;
118
+ }
119
+ function offerSummary(offer) {
120
+ const route = routeStr(offer.outbound);
121
+ const dur = durationHuman(offer.outbound.total_duration_seconds);
122
+ const airline = offer.owner_airline || offer.airlines[0] || "?";
123
+ return `${offer.currency} ${offer.price.toFixed(2)} | ${airline} | ${route} | ${dur} | ${offer.outbound.stopovers} stop(s)`;
124
+ }
125
+ function cheapestOffer(result) {
126
+ if (!result.offers.length) return null;
127
+ return result.offers.reduce((min, o) => o.price < min.price ? o : min, result.offers[0]);
128
+ }
129
+ async function searchLocal(origin, destination, dateFrom, options = {}) {
130
+ const { spawn } = await import("child_process");
131
+ const params = JSON.stringify({
132
+ origin: origin.toUpperCase(),
133
+ destination: destination.toUpperCase(),
134
+ date_from: dateFrom,
135
+ adults: options.adults ?? 1,
136
+ children: options.children ?? 0,
137
+ currency: options.currency ?? "EUR",
138
+ limit: options.limit ?? 50,
139
+ return_date: options.returnDate,
140
+ cabin_class: options.cabinClass,
141
+ ...options.maxBrowsers != null && { max_browsers: options.maxBrowsers }
142
+ });
143
+ return new Promise((resolve, reject) => {
144
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
145
+ const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
146
+ stdio: ["pipe", "pipe", "pipe"]
147
+ });
148
+ let stdout = "";
149
+ let stderr = "";
150
+ child.stdout.on("data", (d) => {
151
+ stdout += d.toString();
152
+ });
153
+ child.stderr.on("data", (d) => {
154
+ stderr += d.toString();
155
+ });
156
+ child.on("close", (code) => {
157
+ try {
158
+ const data = JSON.parse(stdout);
159
+ if (data.error) reject(new LetsFGError(data.error));
160
+ else resolve(data);
161
+ } catch {
162
+ reject(new LetsFGError(
163
+ `Python search failed (code ${code}): ${stdout || stderr}
164
+ Make sure LetsFG is installed: pip install letsfg && playwright install chromium`
165
+ ));
166
+ }
167
+ });
168
+ child.on("error", (err) => {
169
+ reject(new LetsFGError(
170
+ `Cannot start Python: ${err.message}
171
+ Install: pip install letsfg && playwright install chromium`
172
+ ));
173
+ });
174
+ child.stdin.write(params);
175
+ child.stdin.end();
176
+ });
177
+ }
178
+ var DEFAULT_BASE_URL = "https://api.letsfg.co";
179
+ var LetsFG = class {
180
+ apiKey;
181
+ baseUrl;
182
+ timeout;
183
+ constructor(config = {}) {
184
+ this.apiKey = config.apiKey || process.env.LETSFG_API_KEY || "";
185
+ this.baseUrl = (config.baseUrl || process.env.LETSFG_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
186
+ this.timeout = config.timeout || 3e4;
187
+ }
188
+ requireApiKey() {
189
+ if (!this.apiKey) {
190
+ throw new AuthenticationError(
191
+ "API key required for this operation. Set apiKey in config or LETSFG_API_KEY env var.\nNote: searchLocal() works without an API key."
192
+ );
193
+ }
194
+ }
195
+ // ── Core methods ─────────────────────────────────────────────────────
196
+ /**
197
+ * Search for flights — FREE, unlimited.
198
+ *
199
+ * @param origin - IATA code (e.g., "GDN", "LON")
200
+ * @param destination - IATA code (e.g., "BER", "BCN")
201
+ * @param dateFrom - Departure date "YYYY-MM-DD"
202
+ * @param options - Optional search parameters
203
+ */
204
+ async search(origin, destination, dateFrom, options = {}) {
205
+ this.requireApiKey();
206
+ const body = {
207
+ origin: origin.toUpperCase(),
208
+ destination: destination.toUpperCase(),
209
+ date_from: dateFrom,
210
+ adults: options.adults ?? 1,
211
+ children: options.children ?? 0,
212
+ infants: options.infants ?? 0,
213
+ max_stopovers: options.maxStopovers ?? 2,
214
+ currency: options.currency ?? "EUR",
215
+ limit: options.limit ?? 20,
216
+ sort: options.sort ?? "price"
217
+ };
218
+ if (options.returnDate) body.return_from = options.returnDate;
219
+ if (options.cabinClass) body.cabin_class = options.cabinClass;
220
+ return this.post("/api/v1/flights/search", body);
221
+ }
222
+ /**
223
+ * Resolve a city/airport name to IATA codes.
224
+ */
225
+ async resolveLocation(query) {
226
+ this.requireApiKey();
227
+ const data = await this.get(`/api/v1/flights/locations/${encodeURIComponent(query)}`);
228
+ if (Array.isArray(data)) return data;
229
+ if (data && Array.isArray(data.locations)) return data.locations;
230
+ return data ? [data] : [];
231
+ }
232
+ /**
233
+ * Unlock a flight offer — $1 fee.
234
+ * Confirms price, reserves for 30 minutes.
235
+ */
236
+ async unlock(offerId) {
237
+ this.requireApiKey();
238
+ return this.post("/api/v1/bookings/unlock", { offer_id: offerId });
239
+ }
240
+ /**
241
+ * Book a flight — FREE after unlock.
242
+ * Creates a real airline reservation with PNR.
243
+ *
244
+ * Always provide idempotencyKey to prevent double-bookings on retry.
245
+ */
246
+ async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
247
+ this.requireApiKey();
248
+ const body = {
249
+ offer_id: offerId,
250
+ booking_type: "flight",
251
+ passengers,
252
+ contact_email: contactEmail,
253
+ contact_phone: contactPhone
254
+ };
255
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
256
+ return this.post("/api/v1/bookings/book", body);
257
+ }
258
+ /**
259
+ * Set up payment method (payment token).
260
+ */
261
+ async setupPayment(token = "tok_visa") {
262
+ this.requireApiKey();
263
+ return this.post("/api/v1/agents/setup-payment", { token });
264
+ }
265
+ /**
266
+ * Start automated checkout — drives to payment page, NEVER submits payment.
267
+ *
268
+ * Requires unlock first ($1 fee). Returns progress with screenshot and
269
+ * booking URL for manual completion.
270
+ *
271
+ * @param offerId - Offer ID from search results
272
+ * @param passengers - Passenger details (use test data for safety)
273
+ * @param checkoutToken - Token from unlock() response
274
+ */
275
+ async startCheckout(offerId, passengers, checkoutToken) {
276
+ this.requireApiKey();
277
+ return this.post("/api/v1/bookings/start-checkout", {
278
+ offer_id: offerId,
279
+ passengers,
280
+ checkout_token: checkoutToken
281
+ });
282
+ }
283
+ /**
284
+ * Start checkout locally via Python (runs on your machine).
285
+ * Requires: pip install letsfg && playwright install chromium
286
+ *
287
+ * @param offer - Full FlightOffer object from search results
288
+ * @param passengers - Passenger details
289
+ * @param checkoutToken - Token from unlock()
290
+ */
291
+ async startCheckoutLocal(offer, passengers, checkoutToken) {
292
+ const { spawn } = await import("child_process");
293
+ const input = JSON.stringify({
294
+ __checkout: true,
295
+ offer,
296
+ passengers,
297
+ checkout_token: checkoutToken,
298
+ api_key: this.apiKey,
299
+ base_url: this.baseUrl
300
+ });
301
+ return new Promise((resolve, reject) => {
302
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
303
+ const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
304
+ stdio: ["pipe", "pipe", "pipe"],
305
+ timeout: 18e4
306
+ });
307
+ let stdout = "";
308
+ let stderr = "";
309
+ child.stdout.on("data", (d) => {
310
+ stdout += d.toString();
311
+ });
312
+ child.stderr.on("data", (d) => {
313
+ stderr += d.toString();
314
+ });
315
+ child.on("close", (code) => {
316
+ try {
317
+ const data = JSON.parse(stdout);
318
+ if (data.error) reject(new LetsFGError(data.error));
319
+ else resolve(data);
320
+ } catch {
321
+ reject(new LetsFGError(`Checkout failed (code ${code}): ${stdout || stderr}`));
322
+ }
323
+ });
324
+ child.on("error", (err) => {
325
+ reject(new LetsFGError(`Cannot start Python: ${err.message}`));
326
+ });
327
+ child.stdin.write(input);
328
+ child.stdin.end();
329
+ });
330
+ }
331
+ /**
332
+ * Get current agent profile and usage stats.
333
+ */
334
+ async me() {
335
+ this.requireApiKey();
336
+ return this.get("/api/v1/agents/me");
337
+ }
338
+ // ── Static methods ───────────────────────────────────────────────────
339
+ /**
340
+ * Register a new agent — no API key needed.
341
+ */
342
+ static async register(agentName, email, baseUrl, ownerName = "", description = "") {
343
+ const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
344
+ const resp = await fetch(`${url}/api/v1/agents/register`, {
345
+ method: "POST",
346
+ headers: { "Content-Type": "application/json" },
347
+ body: JSON.stringify({
348
+ agent_name: agentName,
349
+ email,
350
+ owner_name: ownerName,
351
+ description
352
+ })
353
+ });
354
+ const data = await resp.json();
355
+ if (!resp.ok) {
356
+ throw new LetsFGError(
357
+ data.detail || `Registration failed (${resp.status})`,
358
+ resp.status,
359
+ data
360
+ );
361
+ }
362
+ return data;
363
+ }
364
+ // ── Internal ────────────────────────────────────────────────────────
365
+ async post(path, body) {
366
+ return this.request(path, {
367
+ method: "POST",
368
+ body: JSON.stringify(body)
369
+ });
370
+ }
371
+ async get(path) {
372
+ return this.request(path, { method: "GET" });
373
+ }
374
+ async request(path, init) {
375
+ const controller = new AbortController();
376
+ const timer = setTimeout(() => controller.abort(), this.timeout);
377
+ try {
378
+ const resp = await fetch(`${this.baseUrl}${path}`, {
379
+ ...init,
380
+ headers: {
381
+ "Content-Type": "application/json",
382
+ "X-API-Key": this.apiKey,
383
+ "User-Agent": "LetsFG-js/0.1.0",
384
+ ...init.headers || {}
385
+ },
386
+ signal: controller.signal
387
+ });
388
+ const data = await resp.json();
389
+ if (!resp.ok) {
390
+ const detail = data.detail || `API error (${resp.status})`;
391
+ const code = data.error_code || inferErrorCode(resp.status, detail);
392
+ if (resp.status === 401) throw new AuthenticationError(detail, data);
393
+ if (resp.status === 402) throw new PaymentRequiredError(detail, data);
394
+ if (resp.status === 410) throw new OfferExpiredError(detail, data);
395
+ if (resp.status === 422) throw new ValidationError(detail, resp.status, data, code);
396
+ throw new LetsFGError(detail, resp.status, data, code);
397
+ }
398
+ return data;
399
+ } finally {
400
+ clearTimeout(timer);
401
+ }
402
+ }
403
+ };
404
+ async function systemInfo() {
405
+ const { spawn } = await import("child_process");
406
+ return new Promise((resolve, reject) => {
407
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
408
+ const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
409
+ stdio: ["pipe", "pipe", "pipe"]
410
+ });
411
+ let stdout = "";
412
+ let stderr = "";
413
+ child.stdout.on("data", (d) => {
414
+ stdout += d.toString();
415
+ });
416
+ child.stderr.on("data", (d) => {
417
+ stderr += d.toString();
418
+ });
419
+ child.on("close", (code) => {
420
+ try {
421
+ const data = JSON.parse(stdout);
422
+ if (data.error) reject(new LetsFGError(data.error));
423
+ else resolve(data);
424
+ } catch {
425
+ reject(new LetsFGError(
426
+ `Python system-info failed (code ${code}): ${stdout || stderr}`
427
+ ));
428
+ }
429
+ });
430
+ child.on("error", (err) => {
431
+ reject(new LetsFGError(
432
+ `Cannot start Python: ${err.message}
433
+ Install: pip install letsfg`
434
+ ));
435
+ });
436
+ child.stdin.write(JSON.stringify({ __system_info: true }));
437
+ child.stdin.end();
438
+ });
439
+ }
440
+ var index_default = LetsFG;
441
+ var BoostedTravel = LetsFG;
442
+ var BoostedTravelError = LetsFGError;
443
+
444
+ export {
445
+ ErrorCode,
446
+ ErrorCategory,
447
+ LetsFGError,
448
+ AuthenticationError,
449
+ PaymentRequiredError,
450
+ OfferExpiredError,
451
+ ValidationError,
452
+ offerSummary,
453
+ cheapestOffer,
454
+ searchLocal,
455
+ LetsFG,
456
+ systemInfo,
457
+ index_default,
458
+ BoostedTravel,
459
+ BoostedTravelError
460
+ };
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node