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/dist/cli.js ADDED
@@ -0,0 +1,708 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var ErrorCode = {
28
+ // Transient (safe to retry after short delay)
29
+ SUPPLIER_TIMEOUT: "SUPPLIER_TIMEOUT",
30
+ RATE_LIMITED: "RATE_LIMITED",
31
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
32
+ NETWORK_ERROR: "NETWORK_ERROR",
33
+ // Validation (fix input, then retry)
34
+ INVALID_IATA: "INVALID_IATA",
35
+ INVALID_DATE: "INVALID_DATE",
36
+ INVALID_PASSENGERS: "INVALID_PASSENGERS",
37
+ UNSUPPORTED_ROUTE: "UNSUPPORTED_ROUTE",
38
+ MISSING_PARAMETER: "MISSING_PARAMETER",
39
+ INVALID_PARAMETER: "INVALID_PARAMETER",
40
+ // Business (requires human decision)
41
+ AUTH_INVALID: "AUTH_INVALID",
42
+ PAYMENT_REQUIRED: "PAYMENT_REQUIRED",
43
+ PAYMENT_DECLINED: "PAYMENT_DECLINED",
44
+ OFFER_EXPIRED: "OFFER_EXPIRED",
45
+ OFFER_NOT_UNLOCKED: "OFFER_NOT_UNLOCKED",
46
+ FARE_CHANGED: "FARE_CHANGED",
47
+ ALREADY_BOOKED: "ALREADY_BOOKED",
48
+ BOOKING_FAILED: "BOOKING_FAILED"
49
+ };
50
+ var ErrorCategory = {
51
+ TRANSIENT: "transient",
52
+ VALIDATION: "validation",
53
+ BUSINESS: "business"
54
+ };
55
+ var CODE_TO_CATEGORY = {
56
+ [ErrorCode.SUPPLIER_TIMEOUT]: ErrorCategory.TRANSIENT,
57
+ [ErrorCode.RATE_LIMITED]: ErrorCategory.TRANSIENT,
58
+ [ErrorCode.SERVICE_UNAVAILABLE]: ErrorCategory.TRANSIENT,
59
+ [ErrorCode.NETWORK_ERROR]: ErrorCategory.TRANSIENT,
60
+ [ErrorCode.INVALID_IATA]: ErrorCategory.VALIDATION,
61
+ [ErrorCode.INVALID_DATE]: ErrorCategory.VALIDATION,
62
+ [ErrorCode.INVALID_PASSENGERS]: ErrorCategory.VALIDATION,
63
+ [ErrorCode.UNSUPPORTED_ROUTE]: ErrorCategory.VALIDATION,
64
+ [ErrorCode.MISSING_PARAMETER]: ErrorCategory.VALIDATION,
65
+ [ErrorCode.INVALID_PARAMETER]: ErrorCategory.VALIDATION,
66
+ [ErrorCode.AUTH_INVALID]: ErrorCategory.BUSINESS,
67
+ [ErrorCode.PAYMENT_REQUIRED]: ErrorCategory.BUSINESS,
68
+ [ErrorCode.PAYMENT_DECLINED]: ErrorCategory.BUSINESS,
69
+ [ErrorCode.OFFER_EXPIRED]: ErrorCategory.BUSINESS,
70
+ [ErrorCode.OFFER_NOT_UNLOCKED]: ErrorCategory.BUSINESS,
71
+ [ErrorCode.FARE_CHANGED]: ErrorCategory.BUSINESS,
72
+ [ErrorCode.ALREADY_BOOKED]: ErrorCategory.BUSINESS,
73
+ [ErrorCode.BOOKING_FAILED]: ErrorCategory.BUSINESS
74
+ };
75
+ function inferErrorCode(statusCode, detail) {
76
+ const d = detail.toLowerCase();
77
+ if (statusCode === 401) return ErrorCode.AUTH_INVALID;
78
+ if (statusCode === 402) return d.includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
79
+ if (statusCode === 410) return ErrorCode.OFFER_EXPIRED;
80
+ if (statusCode === 422) {
81
+ if (d.includes("iata") || d.includes("airport")) return ErrorCode.INVALID_IATA;
82
+ if (d.includes("date")) return ErrorCode.INVALID_DATE;
83
+ if (d.includes("passenger")) return ErrorCode.INVALID_PASSENGERS;
84
+ if (d.includes("route")) return ErrorCode.UNSUPPORTED_ROUTE;
85
+ return ErrorCode.INVALID_PARAMETER;
86
+ }
87
+ if (statusCode === 429) return ErrorCode.RATE_LIMITED;
88
+ if (statusCode === 503) return ErrorCode.SERVICE_UNAVAILABLE;
89
+ if (statusCode === 504) return ErrorCode.SUPPLIER_TIMEOUT;
90
+ if (statusCode === 409) return ErrorCode.ALREADY_BOOKED;
91
+ return statusCode >= 500 ? ErrorCode.BOOKING_FAILED : ErrorCode.INVALID_PARAMETER;
92
+ }
93
+ var LetsFGError = class extends Error {
94
+ statusCode;
95
+ response;
96
+ errorCode;
97
+ errorCategory;
98
+ isRetryable;
99
+ constructor(message, statusCode = 0, response = {}, errorCode = "") {
100
+ super(message);
101
+ this.name = "LetsFGError";
102
+ this.statusCode = statusCode;
103
+ this.response = response;
104
+ this.errorCode = errorCode || response.error_code || "";
105
+ this.errorCategory = CODE_TO_CATEGORY[this.errorCode] || ErrorCategory.BUSINESS;
106
+ this.isRetryable = this.errorCategory === ErrorCategory.TRANSIENT;
107
+ }
108
+ };
109
+ var AuthenticationError = class extends LetsFGError {
110
+ constructor(message, response = {}) {
111
+ super(message, 401, response, ErrorCode.AUTH_INVALID);
112
+ this.name = "AuthenticationError";
113
+ }
114
+ };
115
+ var PaymentRequiredError = class extends LetsFGError {
116
+ constructor(message, response = {}) {
117
+ const code = message.toLowerCase().includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
118
+ super(message, 402, response, code);
119
+ this.name = "PaymentRequiredError";
120
+ }
121
+ };
122
+ var OfferExpiredError = class extends LetsFGError {
123
+ constructor(message, response = {}) {
124
+ super(message, 410, response, ErrorCode.OFFER_EXPIRED);
125
+ this.name = "OfferExpiredError";
126
+ }
127
+ };
128
+ var ValidationError = class extends LetsFGError {
129
+ constructor(message, statusCode = 422, response = {}, errorCode = "") {
130
+ super(message, statusCode, response, errorCode || ErrorCode.INVALID_PARAMETER);
131
+ this.name = "ValidationError";
132
+ }
133
+ };
134
+ function routeStr(route) {
135
+ if (!route.segments.length) return "";
136
+ const codes = [route.segments[0].origin, ...route.segments.map((s) => s.destination)];
137
+ return codes.join(" \u2192 ");
138
+ }
139
+ function durationHuman(seconds) {
140
+ const h = Math.floor(seconds / 3600);
141
+ const m = Math.floor(seconds % 3600 / 60);
142
+ return `${h}h${m.toString().padStart(2, "0")}m`;
143
+ }
144
+ function offerSummary(offer) {
145
+ const route = routeStr(offer.outbound);
146
+ const dur = durationHuman(offer.outbound.total_duration_seconds);
147
+ const airline = offer.owner_airline || offer.airlines[0] || "?";
148
+ return `${offer.currency} ${offer.price.toFixed(2)} | ${airline} | ${route} | ${dur} | ${offer.outbound.stopovers} stop(s)`;
149
+ }
150
+ var DEFAULT_BASE_URL = "https://api.letsfg.co";
151
+ var LetsFG = class {
152
+ apiKey;
153
+ baseUrl;
154
+ timeout;
155
+ constructor(config = {}) {
156
+ this.apiKey = config.apiKey || process.env.LETSFG_API_KEY || "";
157
+ this.baseUrl = (config.baseUrl || process.env.LETSFG_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
158
+ this.timeout = config.timeout || 3e4;
159
+ }
160
+ requireApiKey() {
161
+ if (!this.apiKey) {
162
+ throw new AuthenticationError(
163
+ "API key required for this operation. Set apiKey in config or LETSFG_API_KEY env var.\nNote: searchLocal() works without an API key."
164
+ );
165
+ }
166
+ }
167
+ // ── Core methods ─────────────────────────────────────────────────────
168
+ /**
169
+ * Search for flights — FREE, unlimited.
170
+ *
171
+ * @param origin - IATA code (e.g., "GDN", "LON")
172
+ * @param destination - IATA code (e.g., "BER", "BCN")
173
+ * @param dateFrom - Departure date "YYYY-MM-DD"
174
+ * @param options - Optional search parameters
175
+ */
176
+ async search(origin, destination, dateFrom, options = {}) {
177
+ this.requireApiKey();
178
+ const body = {
179
+ origin: origin.toUpperCase(),
180
+ destination: destination.toUpperCase(),
181
+ date_from: dateFrom,
182
+ adults: options.adults ?? 1,
183
+ children: options.children ?? 0,
184
+ infants: options.infants ?? 0,
185
+ max_stopovers: options.maxStopovers ?? 2,
186
+ currency: options.currency ?? "EUR",
187
+ limit: options.limit ?? 20,
188
+ sort: options.sort ?? "price"
189
+ };
190
+ if (options.returnDate) body.return_from = options.returnDate;
191
+ if (options.cabinClass) body.cabin_class = options.cabinClass;
192
+ return this.post("/api/v1/flights/search", body);
193
+ }
194
+ /**
195
+ * Resolve a city/airport name to IATA codes.
196
+ */
197
+ async resolveLocation(query) {
198
+ this.requireApiKey();
199
+ const data = await this.get(`/api/v1/flights/locations/${encodeURIComponent(query)}`);
200
+ if (Array.isArray(data)) return data;
201
+ if (data && Array.isArray(data.locations)) return data.locations;
202
+ return data ? [data] : [];
203
+ }
204
+ /**
205
+ * Unlock a flight offer — $1 fee.
206
+ * Confirms price, reserves for 30 minutes.
207
+ */
208
+ async unlock(offerId) {
209
+ this.requireApiKey();
210
+ return this.post("/api/v1/bookings/unlock", { offer_id: offerId });
211
+ }
212
+ /**
213
+ * Book a flight — FREE after unlock.
214
+ * Creates a real airline reservation with PNR.
215
+ *
216
+ * Always provide idempotencyKey to prevent double-bookings on retry.
217
+ */
218
+ async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
219
+ this.requireApiKey();
220
+ const body = {
221
+ offer_id: offerId,
222
+ booking_type: "flight",
223
+ passengers,
224
+ contact_email: contactEmail,
225
+ contact_phone: contactPhone
226
+ };
227
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
228
+ return this.post("/api/v1/bookings/book", body);
229
+ }
230
+ /**
231
+ * Set up payment method (payment token).
232
+ */
233
+ async setupPayment(token = "tok_visa") {
234
+ this.requireApiKey();
235
+ return this.post("/api/v1/agents/setup-payment", { token });
236
+ }
237
+ /**
238
+ * Start automated checkout — drives to payment page, NEVER submits payment.
239
+ *
240
+ * Requires unlock first ($1 fee). Returns progress with screenshot and
241
+ * booking URL for manual completion.
242
+ *
243
+ * @param offerId - Offer ID from search results
244
+ * @param passengers - Passenger details (use test data for safety)
245
+ * @param checkoutToken - Token from unlock() response
246
+ */
247
+ async startCheckout(offerId, passengers, checkoutToken) {
248
+ this.requireApiKey();
249
+ return this.post("/api/v1/bookings/start-checkout", {
250
+ offer_id: offerId,
251
+ passengers,
252
+ checkout_token: checkoutToken
253
+ });
254
+ }
255
+ /**
256
+ * Start checkout locally via Python (runs on your machine).
257
+ * Requires: pip install letsfg && playwright install chromium
258
+ *
259
+ * @param offer - Full FlightOffer object from search results
260
+ * @param passengers - Passenger details
261
+ * @param checkoutToken - Token from unlock()
262
+ */
263
+ async startCheckoutLocal(offer, passengers, checkoutToken) {
264
+ const { spawn } = await import("child_process");
265
+ const input = JSON.stringify({
266
+ __checkout: true,
267
+ offer,
268
+ passengers,
269
+ checkout_token: checkoutToken,
270
+ api_key: this.apiKey,
271
+ base_url: this.baseUrl
272
+ });
273
+ return new Promise((resolve, reject) => {
274
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
275
+ const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
276
+ stdio: ["pipe", "pipe", "pipe"],
277
+ timeout: 18e4
278
+ });
279
+ let stdout = "";
280
+ let stderr = "";
281
+ child.stdout.on("data", (d) => {
282
+ stdout += d.toString();
283
+ });
284
+ child.stderr.on("data", (d) => {
285
+ stderr += d.toString();
286
+ });
287
+ child.on("close", (code) => {
288
+ try {
289
+ const data = JSON.parse(stdout);
290
+ if (data.error) reject(new LetsFGError(data.error));
291
+ else resolve(data);
292
+ } catch {
293
+ reject(new LetsFGError(`Checkout failed (code ${code}): ${stdout || stderr}`));
294
+ }
295
+ });
296
+ child.on("error", (err) => {
297
+ reject(new LetsFGError(`Cannot start Python: ${err.message}`));
298
+ });
299
+ child.stdin.write(input);
300
+ child.stdin.end();
301
+ });
302
+ }
303
+ /**
304
+ * Get current agent profile and usage stats.
305
+ */
306
+ async me() {
307
+ this.requireApiKey();
308
+ return this.get("/api/v1/agents/me");
309
+ }
310
+ // ── Static methods ───────────────────────────────────────────────────
311
+ /**
312
+ * Register a new agent — no API key needed.
313
+ */
314
+ static async register(agentName, email, baseUrl, ownerName = "", description = "") {
315
+ const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
316
+ const resp = await fetch(`${url}/api/v1/agents/register`, {
317
+ method: "POST",
318
+ headers: { "Content-Type": "application/json" },
319
+ body: JSON.stringify({
320
+ agent_name: agentName,
321
+ email,
322
+ owner_name: ownerName,
323
+ description
324
+ })
325
+ });
326
+ const data = await resp.json();
327
+ if (!resp.ok) {
328
+ throw new LetsFGError(
329
+ data.detail || `Registration failed (${resp.status})`,
330
+ resp.status,
331
+ data
332
+ );
333
+ }
334
+ return data;
335
+ }
336
+ // ── Internal ────────────────────────────────────────────────────────
337
+ async post(path, body) {
338
+ return this.request(path, {
339
+ method: "POST",
340
+ body: JSON.stringify(body)
341
+ });
342
+ }
343
+ async get(path) {
344
+ return this.request(path, { method: "GET" });
345
+ }
346
+ async request(path, init) {
347
+ const controller = new AbortController();
348
+ const timer = setTimeout(() => controller.abort(), this.timeout);
349
+ try {
350
+ const resp = await fetch(`${this.baseUrl}${path}`, {
351
+ ...init,
352
+ headers: {
353
+ "Content-Type": "application/json",
354
+ "X-API-Key": this.apiKey,
355
+ "User-Agent": "LetsFG-js/0.1.0",
356
+ ...init.headers || {}
357
+ },
358
+ signal: controller.signal
359
+ });
360
+ const data = await resp.json();
361
+ if (!resp.ok) {
362
+ const detail = data.detail || `API error (${resp.status})`;
363
+ const code = data.error_code || inferErrorCode(resp.status, detail);
364
+ if (resp.status === 401) throw new AuthenticationError(detail, data);
365
+ if (resp.status === 402) throw new PaymentRequiredError(detail, data);
366
+ if (resp.status === 410) throw new OfferExpiredError(detail, data);
367
+ if (resp.status === 422) throw new ValidationError(detail, resp.status, data, code);
368
+ throw new LetsFGError(detail, resp.status, data, code);
369
+ }
370
+ return data;
371
+ } finally {
372
+ clearTimeout(timer);
373
+ }
374
+ }
375
+ };
376
+
377
+ // src/cli.ts
378
+ function getFlag(args, flag, alias) {
379
+ for (let i = 0; i < args.length; i++) {
380
+ if (args[i] === flag || alias && args[i] === alias) {
381
+ const val = args[i + 1];
382
+ args.splice(i, 2);
383
+ return val;
384
+ }
385
+ if (args[i].startsWith(`${flag}=`)) {
386
+ const val = args[i].split("=").slice(1).join("=");
387
+ args.splice(i, 1);
388
+ return val;
389
+ }
390
+ }
391
+ return void 0;
392
+ }
393
+ function hasFlag(args, flag) {
394
+ const idx = args.indexOf(flag);
395
+ if (idx >= 0) {
396
+ args.splice(idx, 1);
397
+ return true;
398
+ }
399
+ return false;
400
+ }
401
+ function getAllFlags(args, flag, alias) {
402
+ const results = [];
403
+ let i = 0;
404
+ while (i < args.length) {
405
+ if (args[i] === flag || alias && args[i] === alias) {
406
+ results.push(args[i + 1]);
407
+ args.splice(i, 2);
408
+ } else {
409
+ i++;
410
+ }
411
+ }
412
+ return results;
413
+ }
414
+ async function cmdSearch(args) {
415
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
416
+ const apiKey = getFlag(args, "--api-key", "-k");
417
+ const baseUrl = getFlag(args, "--base-url");
418
+ const returnDate = getFlag(args, "--return", "-r");
419
+ const adults = parseInt(getFlag(args, "--adults", "-a") || "1");
420
+ const cabin = getFlag(args, "--cabin", "-c");
421
+ const stops = parseInt(getFlag(args, "--max-stops", "-s") || "2");
422
+ const currency = getFlag(args, "--currency") || "EUR";
423
+ const limit = parseInt(getFlag(args, "--limit", "-l") || "20");
424
+ const sort = getFlag(args, "--sort") || "price";
425
+ const [origin, destination, date] = args;
426
+ if (!origin || !destination || !date) {
427
+ console.error("Usage: letsfg search <origin> <destination> <date> [options]");
428
+ process.exit(1);
429
+ }
430
+ const bt = new LetsFG({ apiKey, baseUrl });
431
+ const result = await bt.search(origin, destination, date, {
432
+ returnDate,
433
+ adults,
434
+ cabinClass: cabin,
435
+ maxStopovers: stops,
436
+ currency,
437
+ limit,
438
+ sort
439
+ });
440
+ if (jsonOut) {
441
+ console.log(JSON.stringify({
442
+ passenger_ids: result.passenger_ids,
443
+ total_results: result.total_results,
444
+ offers: result.offers.map((o) => ({
445
+ id: o.id,
446
+ price: o.price,
447
+ currency: o.currency,
448
+ airlines: o.airlines,
449
+ owner_airline: o.owner_airline,
450
+ route: [o.outbound.segments[0]?.origin, ...o.outbound.segments.map((s) => s.destination)].join(" \u2192 "),
451
+ duration_seconds: o.outbound.total_duration_seconds,
452
+ stopovers: o.outbound.stopovers,
453
+ conditions: o.conditions,
454
+ is_locked: o.is_locked
455
+ }))
456
+ }, null, 2));
457
+ return;
458
+ }
459
+ if (!result.offers.length) {
460
+ console.log(`No flights found for ${origin} \u2192 ${destination} on ${date}`);
461
+ return;
462
+ }
463
+ console.log(`
464
+ ${result.total_results} offers | ${origin} \u2192 ${destination} | ${date}`);
465
+ console.log(` Passenger IDs: ${JSON.stringify(result.passenger_ids)}
466
+ `);
467
+ result.offers.forEach((o, i) => {
468
+ console.log(` ${(i + 1).toString().padStart(3)}. ${offerSummary(o)}`);
469
+ console.log(` ID: ${o.id}`);
470
+ });
471
+ console.log(`
472
+ To unlock: letsfg unlock <offer_id>`);
473
+ console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
474
+ `);
475
+ }
476
+ async function cmdUnlock(args) {
477
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
478
+ const apiKey = getFlag(args, "--api-key", "-k");
479
+ const baseUrl = getFlag(args, "--base-url");
480
+ const offerId = args[0];
481
+ if (!offerId) {
482
+ console.error("Usage: letsfg unlock <offer_id>");
483
+ process.exit(1);
484
+ }
485
+ const bt = new LetsFG({ apiKey, baseUrl });
486
+ const result = await bt.unlock(offerId);
487
+ if (jsonOut) {
488
+ console.log(JSON.stringify(result, null, 2));
489
+ return;
490
+ }
491
+ if (result.unlock_status === "unlocked") {
492
+ console.log(`
493
+ \u2713 Offer unlocked!`);
494
+ console.log(` Confirmed price: ${result.confirmed_currency} ${result.confirmed_price?.toFixed(2)}`);
495
+ console.log(` Expires at: ${result.offer_expires_at}`);
496
+ console.log(` $1 unlock fee charged`);
497
+ console.log(`
498
+ Next: letsfg book ${offerId} --passenger '{...}' --email you@example.com
499
+ `);
500
+ } else {
501
+ console.error(` \u2717 Unlock failed: ${result.message}`);
502
+ process.exit(1);
503
+ }
504
+ }
505
+ async function cmdBook(args) {
506
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
507
+ const apiKey = getFlag(args, "--api-key", "-k");
508
+ const baseUrl = getFlag(args, "--base-url");
509
+ const email = getFlag(args, "--email", "-e") || "";
510
+ const phone = getFlag(args, "--phone") || "";
511
+ const passengerStrs = getAllFlags(args, "--passenger", "-p");
512
+ const offerId = args[0];
513
+ if (!offerId || !passengerStrs.length || !email) {
514
+ console.error(`Usage: letsfg book <offer_id> --passenger '{"id":"pas_xxx",...}' --email you@example.com`);
515
+ process.exit(1);
516
+ }
517
+ const passengers = passengerStrs.map((s) => JSON.parse(s));
518
+ const bt = new LetsFG({ apiKey, baseUrl });
519
+ const result = await bt.book(offerId, passengers, email, phone);
520
+ if (jsonOut) {
521
+ console.log(JSON.stringify(result, null, 2));
522
+ return;
523
+ }
524
+ if (result.status === "confirmed") {
525
+ console.log(`
526
+ \u2713 Booking confirmed!`);
527
+ console.log(` PNR: ${result.booking_reference}`);
528
+ console.log(` Flight: ${result.currency} ${result.flight_price.toFixed(2)}`);
529
+ console.log(` Fee: ${result.currency} ${result.service_fee.toFixed(2)} (${result.service_fee_percentage}%)`);
530
+ console.log(` Total: ${result.currency} ${result.total_charged.toFixed(2)}`);
531
+ console.log(` Order: ${result.order_id}
532
+ `);
533
+ } else {
534
+ console.error(` \u2717 Booking failed`);
535
+ console.error(JSON.stringify(result.details, null, 2));
536
+ process.exit(1);
537
+ }
538
+ }
539
+ async function cmdLocations(args) {
540
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
541
+ const apiKey = getFlag(args, "--api-key", "-k");
542
+ const baseUrl = getFlag(args, "--base-url");
543
+ const query = args[0];
544
+ if (!query) {
545
+ console.error("Usage: letsfg locations <city-or-airport-name>");
546
+ process.exit(1);
547
+ }
548
+ const bt = new LetsFG({ apiKey, baseUrl });
549
+ const result = await bt.resolveLocation(query);
550
+ if (jsonOut) {
551
+ console.log(JSON.stringify(result, null, 2));
552
+ return;
553
+ }
554
+ if (!result.length) {
555
+ console.log(`No locations found for '${query}'`);
556
+ return;
557
+ }
558
+ for (const loc of result) {
559
+ const iata = (loc.iata_code || "???").padEnd(5);
560
+ const name = loc.name || "";
561
+ const type = loc.type || "";
562
+ const city = loc.city_name || "";
563
+ const country = loc.country || "";
564
+ console.log(` ${iata} ${name} (${type}) \u2014 ${city}, ${country}`);
565
+ }
566
+ }
567
+ async function cmdRegister(args) {
568
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
569
+ const baseUrl = getFlag(args, "--base-url");
570
+ const name = getFlag(args, "--name", "-n");
571
+ const email = getFlag(args, "--email", "-e");
572
+ const owner = getFlag(args, "--owner") || "";
573
+ const desc = getFlag(args, "--desc") || "";
574
+ if (!name || !email) {
575
+ console.error("Usage: letsfg register --name my-agent --email agent@example.com");
576
+ process.exit(1);
577
+ }
578
+ const result = await LetsFG.register(name, email, baseUrl, owner, desc);
579
+ if (jsonOut) {
580
+ console.log(JSON.stringify(result, null, 2));
581
+ return;
582
+ }
583
+ console.log(`
584
+ \u2713 Agent registered!`);
585
+ console.log(` Agent ID: ${result.agent_id}`);
586
+ console.log(` API Key: ${result.api_key}`);
587
+ console.log(`
588
+ Save your API key:`);
589
+ console.log(` export LETSFG_API_KEY=${result.api_key}`);
590
+ console.log(`
591
+ Next: LetsFG setup-payment --token tok_visa
592
+ `);
593
+ }
594
+ async function cmdSetupPayment(args) {
595
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
596
+ const apiKey = getFlag(args, "--api-key", "-k");
597
+ const baseUrl = getFlag(args, "--base-url");
598
+ const token = getFlag(args, "--token", "-t") || "tok_visa";
599
+ const bt = new LetsFG({ apiKey, baseUrl });
600
+ const result = await bt.setupPayment(token);
601
+ if (jsonOut) {
602
+ console.log(JSON.stringify(result, null, 2));
603
+ return;
604
+ }
605
+ if (result.status === "ready") {
606
+ console.log(`
607
+ \u2713 Payment ready! You can now unlock offers and book flights.
608
+ `);
609
+ } else {
610
+ console.error(` \u2717 Payment setup failed: ${result.message || result.status}`);
611
+ process.exit(1);
612
+ }
613
+ }
614
+ async function cmdMe(args) {
615
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
616
+ const apiKey = getFlag(args, "--api-key", "-k");
617
+ const baseUrl = getFlag(args, "--base-url");
618
+ const bt = new LetsFG({ apiKey, baseUrl });
619
+ const profile = await bt.me();
620
+ if (jsonOut) {
621
+ console.log(JSON.stringify(profile, null, 2));
622
+ return;
623
+ }
624
+ const p = profile;
625
+ const u = p.usage || {};
626
+ console.log(`
627
+ Agent: ${p.agent_name} (${p.agent_id})`);
628
+ console.log(` Email: ${p.email}`);
629
+ console.log(` Tier: ${p.tier}`);
630
+ console.log(` Payment: ${p.payment_ready ? "\u2713 Ready" : "\u2717 Not set up"}`);
631
+ console.log(` Searches: ${u.total_searches || 0}`);
632
+ console.log(` Unlocks: ${u.total_unlocks || 0}`);
633
+ console.log(` Bookings: ${u.total_bookings || 0}`);
634
+ console.log(` Total spent: $${((u.total_spent_cents || 0) / 100).toFixed(2)}
635
+ `);
636
+ }
637
+ var HELP = `
638
+ LetsFG \u2014 Agent-native flight search & booking.
639
+
640
+ Search 400+ airlines at raw airline prices \u2014 $20-50 cheaper than OTAs.
641
+ Search is FREE. Unlock: $1. Book: FREE after unlock.
642
+
643
+ Commands:
644
+ search <origin> <dest> <date> Search for flights (FREE)
645
+ locations <query> Resolve city name to IATA codes
646
+ unlock <offer_id> Unlock offer ($1)
647
+ book <offer_id> --passenger ... Book flight (FREE after unlock)
648
+ register --name ... --email ... Register new agent
649
+ setup-payment Set up payment card
650
+ me Show agent profile
651
+
652
+ Options:
653
+ --json, -j Output raw JSON
654
+ --api-key, -k API key (or set LETSFG_API_KEY)
655
+ --base-url API URL (default: https://api.letsfg.co)
656
+
657
+ Examples:
658
+ letsfg search GDN BER 2026-03-03 --sort price
659
+ letsfg search LON BCN 2026-04-01 --return 2026-04-08 --json
660
+ letsfg unlock off_xxx
661
+ letsfg book off_xxx -p '{"id":"pas_xxx","given_name":"John","family_name":"Doe","born_on":"1990-01-15"}' -e john@ex.com
662
+ `;
663
+ async function main() {
664
+ const args = process.argv.slice(2);
665
+ const command = args.shift();
666
+ try {
667
+ switch (command) {
668
+ case "search":
669
+ await cmdSearch(args);
670
+ break;
671
+ case "unlock":
672
+ await cmdUnlock(args);
673
+ break;
674
+ case "book":
675
+ await cmdBook(args);
676
+ break;
677
+ case "locations":
678
+ await cmdLocations(args);
679
+ break;
680
+ case "register":
681
+ await cmdRegister(args);
682
+ break;
683
+ case "setup-payment":
684
+ await cmdSetupPayment(args);
685
+ break;
686
+ case "me":
687
+ await cmdMe(args);
688
+ break;
689
+ case "--help":
690
+ case "-h":
691
+ case "help":
692
+ case void 0:
693
+ console.log(HELP);
694
+ break;
695
+ default:
696
+ console.error(`Unknown command: ${command}`);
697
+ console.log(HELP);
698
+ process.exit(1);
699
+ }
700
+ } catch (e) {
701
+ if (e instanceof LetsFGError) {
702
+ console.error(`Error: ${e.message}`);
703
+ process.exit(1);
704
+ }
705
+ throw e;
706
+ }
707
+ }
708
+ main();