letsfg 2026.5.54 → 2026.5.56

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 CHANGED
@@ -1,27 +1,171 @@
1
1
  #!/usr/bin/env node
2
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;
3
+
4
+ // src/ranking.ts
5
+ var W = {
6
+ // Generic solo / default — price-driven
7
+ default: {
8
+ price: 0.34,
9
+ stops: 0.22,
10
+ duration: 0.12,
11
+ depTime: 0.08,
12
+ arrivalTime: 0.04,
13
+ baggage: 0.02,
14
+ savings: 0.06,
15
+ comfortHours: 0.04,
16
+ layover: 0.08
17
+ },
18
+ // Business — time and directness > price; long layovers are unacceptable
19
+ business_traveler: {
20
+ price: 0.1,
21
+ stops: 0.26,
22
+ duration: 0.2,
23
+ depTime: 0.18,
24
+ arrivalTime: 0.04,
25
+ baggage: 0.06,
26
+ savings: 0,
27
+ comfortHours: 0.06,
28
+ layover: 0.1
29
+ },
30
+ // Family — directness + baggage practicality; kids + 8h layover = nightmare
31
+ family: {
32
+ price: 0.12,
33
+ stops: 0.2,
34
+ duration: 0.16,
35
+ depTime: 0.08,
36
+ arrivalTime: 0.04,
37
+ baggage: 0.2,
38
+ savings: 0.04,
39
+ comfortHours: 0.08,
40
+ layover: 0.08
41
+ },
42
+ // Couple — balance of price, comfort, arrival time, savings
43
+ couple: {
44
+ price: 0.22,
45
+ stops: 0.2,
46
+ duration: 0.12,
47
+ depTime: 0.1,
48
+ arrivalTime: 0.14,
49
+ baggage: 0.02,
50
+ savings: 0.1,
51
+ comfortHours: 0.04,
52
+ layover: 0.06
53
+ },
54
+ // Honeymoon — direct > everything; no one wants a 10h layover on their honeymoon
55
+ honeymoon: {
56
+ price: 0.08,
57
+ stops: 0.28,
58
+ duration: 0.1,
59
+ depTime: 0.14,
60
+ arrivalTime: 0.18,
61
+ baggage: 0.02,
62
+ savings: 0.02,
63
+ comfortHours: 0.08,
64
+ layover: 0.1
65
+ },
66
+ // Special occasion — still prioritize smooth timing/directness, but keep price meaningful.
67
+ special_occasion: {
68
+ price: 0.14,
69
+ stops: 0.24,
70
+ duration: 0.1,
71
+ depTime: 0.12,
72
+ arrivalTime: 0.16,
73
+ baggage: 0.02,
74
+ savings: 0.06,
75
+ comfortHours: 0.08,
76
+ layover: 0.08
77
+ },
78
+ // Ski — bag essential (equipment); early arrival to maximize slopes
79
+ ski: {
80
+ price: 0.12,
81
+ stops: 0.16,
82
+ duration: 0.1,
83
+ depTime: 0.16,
84
+ arrivalTime: 0.08,
85
+ baggage: 0.24,
86
+ savings: 0.04,
87
+ comfortHours: 0.02,
88
+ layover: 0.08
89
+ },
90
+ // Beach — arrive early to enjoy the day; price matters
91
+ beach: {
92
+ price: 0.24,
93
+ stops: 0.16,
94
+ duration: 0.1,
95
+ depTime: 0.1,
96
+ arrivalTime: 0.14,
97
+ baggage: 0.1,
98
+ savings: 0.06,
99
+ comfortHours: 0.04,
100
+ layover: 0.06
101
+ },
102
+ // City break — maximize time on the ground; 2-day trip loses half a day to a 6h layover
103
+ city_break: {
104
+ price: 0.26,
105
+ stops: 0.2,
106
+ duration: 0.08,
107
+ depTime: 0.12,
108
+ arrivalTime: 0.16,
109
+ baggage: 0.02,
110
+ savings: 0.04,
111
+ comfortHours: 0.04,
112
+ layover: 0.08
113
+ },
114
+ // Quick flight — user explicitly wants shortest possible total duration
115
+ quick_flight: {
116
+ price: 0.14,
117
+ stops: 0.2,
118
+ duration: 0.4,
119
+ depTime: 0.06,
120
+ arrivalTime: 0.04,
121
+ baggage: 0.02,
122
+ savings: 0.04,
123
+ comfortHours: 0.04,
124
+ layover: 0.06
125
+ },
126
+ // Cheapest — user explicitly asked for lowest price; price overwhelms all other factors
127
+ cheapest: {
128
+ price: 0.88,
129
+ stops: 0.06,
130
+ duration: 0.03,
131
+ depTime: 0.01,
132
+ arrivalTime: 0.01,
133
+ baggage: 0.01,
134
+ savings: 0,
135
+ comfortHours: 0,
136
+ layover: 0
137
+ },
138
+ // Cheapest direct — user asked for BOTH cheapest AND direct/nonstop.
139
+ // Stops must dominate enough that a direct flight wins even if it costs more;
140
+ // price wins within the same stop tier (all directs sorted by price, all 1-stops sorted
141
+ // by price below them, etc.). The scoreStops delta between 0-stop (1.00) and 1-stop (0.40)
142
+ // is 0.60; with stops weight 0.38 that gap is worth 0.228, which means a 1-stop would
143
+ // need an enormous price advantage to beat a direct — effectively "direct first".
144
+ cheapest_direct: {
145
+ price: 0.52,
146
+ stops: 0.38,
147
+ duration: 0.04,
148
+ depTime: 0.02,
149
+ arrivalTime: 0.01,
150
+ baggage: 0.01,
151
+ savings: 0,
152
+ comfortHours: 0,
153
+ layover: 0.02
154
+ }
155
+ };
156
+ var PURPOSE_WEIGHT_PROFILES = {
157
+ honeymoon: W.honeymoon,
158
+ special_occasion: W.special_occasion,
159
+ business: W.business_traveler,
160
+ ski: W.ski,
161
+ beach: W.beach,
162
+ city_break: W.city_break,
163
+ family_holiday: W.family,
164
+ graduation: W.city_break,
165
+ concert_festival: W.city_break,
166
+ sports_event: W.city_break,
167
+ spring_break: W.beach
16
168
  };
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
169
 
26
170
  // src/index.ts
27
171
  var ErrorCode = {
@@ -134,7 +278,7 @@ var ValidationError = class extends LetsFGError {
134
278
  function routeStr(route) {
135
279
  if (!route.segments.length) return "";
136
280
  const codes = [route.segments[0].origin, ...route.segments.map((s) => s.destination)];
137
- return codes.join(" \u2192 ");
281
+ return codes.join(" -> ");
138
282
  }
139
283
  function durationHuman(seconds) {
140
284
  const h = Math.floor(seconds / 3600);
@@ -147,79 +291,45 @@ function offerSummary(offer) {
147
291
  const airline = offer.owner_airline || offer.airlines[0] || "?";
148
292
  return `${offer.currency} ${offer.price.toFixed(2)} | ${airline} | ${route} | ${dur} | ${offer.outbound.stopovers} stop(s)`;
149
293
  }
150
- async function searchLocal(origin, destination, dateFrom, options = {}) {
151
- const { spawn } = await import("child_process");
152
- const params = JSON.stringify({
153
- origin: origin.toUpperCase(),
154
- destination: destination.toUpperCase(),
155
- date_from: dateFrom,
156
- adults: options.adults ?? 1,
157
- children: options.children ?? 0,
158
- currency: options.currency ?? "EUR",
159
- limit: options.limit ?? 50,
160
- return_date: options.returnDate,
161
- cabin_class: options.cabinClass,
162
- ...options.maxBrowsers != null && { max_browsers: options.maxBrowsers },
163
- ...options.mode != null && { mode: options.mode }
164
- });
165
- return new Promise((resolve, reject) => {
166
- const pythonCmd = process.platform === "win32" ? "python" : "python3";
167
- const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
168
- stdio: ["pipe", "pipe", "pipe"]
169
- });
170
- let stdout = "";
171
- let stderr = "";
172
- child.stdout.on("data", (d) => {
173
- stdout += d.toString();
174
- });
175
- child.stderr.on("data", (d) => {
176
- stderr += d.toString();
177
- });
178
- child.on("close", (code) => {
179
- try {
180
- const data = JSON.parse(stdout);
181
- if (data.error) reject(new LetsFGError(data.error));
182
- else resolve(data);
183
- } catch {
184
- reject(new LetsFGError(
185
- `Python search failed (code ${code}): ${stdout || stderr}
186
- Make sure LetsFG is installed: pip install letsfg && playwright install chromium`
187
- ));
188
- }
189
- });
190
- child.on("error", (err) => {
191
- reject(new LetsFGError(
192
- `Cannot start Python: ${err.message}
193
- Install: pip install letsfg && playwright install chromium`
194
- ));
195
- });
196
- child.stdin.write(params);
197
- child.stdin.end();
198
- });
199
- }
200
- var DEFAULT_BASE_URL = "https://letsfg.co/developers";
294
+ var DEFAULT_BASE_URL = "https://letsfg.co";
295
+ var PFS_POLL_INTERVAL_MS = 1e4;
296
+ var PFS_POLL_TIMEOUT_MS = 12e4;
201
297
  var LetsFG = class {
298
+ bearerToken;
202
299
  apiKey;
203
300
  baseUrl;
204
301
  timeout;
205
302
  constructor(config = {}) {
303
+ this.bearerToken = config.bearerToken || process.env.LETSFG_BEARER_TOKEN || "";
206
304
  this.apiKey = config.apiKey || process.env.LETSFG_API_KEY || "";
207
305
  this.baseUrl = (config.baseUrl || process.env.LETSFG_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
208
306
  this.timeout = config.timeout || 3e4;
209
307
  }
308
+ requireAuth() {
309
+ if (!this.bearerToken && !this.apiKey) {
310
+ throw new AuthenticationError(
311
+ "Authentication required. Set bearerToken (from `letsfg auth`) or apiKey in config, or set LETSFG_BEARER_TOKEN / LETSFG_API_KEY env vars."
312
+ );
313
+ }
314
+ }
210
315
  requireApiKey() {
211
316
  if (!this.apiKey) {
212
317
  throw new AuthenticationError(
213
- "API key required for this operation. Set apiKey in config or LETSFG_API_KEY env var.\nNote: searchLocal() works without an API key."
318
+ "Developer API key required for this operation. Set apiKey in config or LETSFG_API_KEY env var. Register at letsfg.co/developers."
214
319
  );
215
320
  }
216
321
  }
322
+ /** True when using PFS Bearer token (free search path) */
323
+ get usingPFS() {
324
+ return !!this.bearerToken;
325
+ }
217
326
  // ── Core methods ─────────────────────────────────────────────────────
218
327
  /**
219
- * Search for flights — FREE, unlimited, runs locally on your machine.
328
+ * Search for flights — FREE.
220
329
  *
221
- * Uses 200 airline connectors via Python subprocess. No backend call.
222
- * Requires: pip install letsfg && playwright install chromium
330
+ * Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
331
+ * PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
332
+ * Developer API: synchronous 60-90s call.
223
333
  *
224
334
  * @param origin - IATA code (e.g., "GDN", "LON")
225
335
  * @param destination - IATA code (e.g., "BER", "BCN")
@@ -227,57 +337,62 @@ var LetsFG = class {
227
337
  * @param options - Optional search parameters
228
338
  */
229
339
  async search(origin, destination, dateFrom, options = {}) {
230
- return searchLocal(origin, destination, dateFrom, options);
340
+ this.requireAuth();
341
+ const body = {
342
+ origin: origin.toUpperCase(),
343
+ destination: destination.toUpperCase(),
344
+ date_from: dateFrom,
345
+ adults: options.adults ?? 1,
346
+ children: options.children ?? 0,
347
+ currency: options.currency ?? "EUR",
348
+ limit: options.limit ?? 50
349
+ };
350
+ if (options.returnDate) body.return_date = options.returnDate;
351
+ if (options.cabinClass) body.cabin_class = options.cabinClass;
352
+ if (options.maxStopovers != null) body.max_stopovers = options.maxStopovers;
353
+ if (options.sort) body.sort = options.sort;
354
+ if (options.departureTimeFrom) body.departure_time_from = options.departureTimeFrom;
355
+ if (options.departureTimeTo) body.departure_time_to = options.departureTimeTo;
356
+ if (this.usingPFS) {
357
+ return this.searchPFS(body);
358
+ }
359
+ return this.post("/developers/api/v1/flights/search", body);
360
+ }
361
+ /** PFS path: POST /api/search -> poll /api/results/<id> */
362
+ async searchPFS(body) {
363
+ const { search_id } = await this.postWithBearer("/api/search", body);
364
+ const deadline = Date.now() + PFS_POLL_TIMEOUT_MS;
365
+ while (Date.now() < deadline) {
366
+ await new Promise((r) => setTimeout(r, PFS_POLL_INTERVAL_MS));
367
+ const result = await this.getNoAuth(
368
+ `/api/results/${search_id}`
369
+ );
370
+ if (result.status !== "pending") {
371
+ return result;
372
+ }
373
+ }
374
+ throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
231
375
  }
232
376
  /**
233
- * Resolve a city/airport name to IATA codes — runs locally, no backend call.
377
+ * Resolve a city/airport name to IATA codes.
234
378
  */
235
379
  async resolveLocation(query) {
236
- const { spawn } = await import("child_process");
237
- const params = JSON.stringify({ __resolve_location: true, query });
238
- return new Promise((resolve, reject) => {
239
- const pythonCmd = process.platform === "win32" ? "python" : "python3";
240
- const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
241
- stdio: ["pipe", "pipe", "pipe"]
242
- });
243
- let stdout = "";
244
- let stderr = "";
245
- child.stdout.on("data", (d) => {
246
- stdout += d.toString();
247
- });
248
- child.stderr.on("data", (d) => {
249
- stderr += d.toString();
250
- });
251
- child.on("close", (code) => {
252
- try {
253
- const data = JSON.parse(stdout);
254
- if (data.error) reject(new LetsFGError(data.error));
255
- else resolve(Array.isArray(data) ? data : data.locations || [data]);
256
- } catch {
257
- reject(new LetsFGError(
258
- `Location resolution failed (code ${code}): ${stdout || stderr}`
259
- ));
260
- }
261
- });
262
- child.on("error", (err) => {
263
- reject(new LetsFGError(`Cannot start Python: ${err.message}`));
264
- });
265
- child.stdin.write(params);
266
- child.stdin.end();
267
- });
380
+ this.requireAuth();
381
+ const path = this.usingPFS ? `/api/locations?q=${encodeURIComponent(query)}` : `/developers/api/v1/flights/locations/${encodeURIComponent(query)}`;
382
+ const data = await this.getWithAuth(path);
383
+ return Array.isArray(data) ? data : data.locations || [];
268
384
  }
269
385
  /**
270
- * Unlock a flight offer — FREE with GitHub star.
271
- * Confirms price, reserves for 30 minutes.
386
+ * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
387
+ * Cost: 1% of ticket price, min $3. Free with Developer API.
272
388
  */
273
389
  async unlock(offerId) {
274
- this.requireApiKey();
275
- return this.post("/api/v1/bookings/unlock", { offer_id: offerId });
390
+ this.requireAuth();
391
+ const path = this.usingPFS ? "/api/unlock" : "/developers/api/v1/bookings/unlock";
392
+ return this.postWithAuth(path, { offer_id: offerId });
276
393
  }
277
394
  /**
278
- * Book a flight — charges ticket price via Stripe.
279
- * Creates a real airline reservation with PNR.
280
- *
395
+ * Book a flight via Developer API — charges ticket price via Stripe, creates real PNR.
281
396
  * Always provide idempotencyKey to prevent double-bookings on retry.
282
397
  */
283
398
  async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
@@ -290,113 +405,32 @@ var LetsFG = class {
290
405
  contact_phone: contactPhone
291
406
  };
292
407
  if (idempotencyKey) body.idempotency_key = idempotencyKey;
293
- return this.post("/api/v1/bookings/book", body);
408
+ return this.post("/developers/api/v1/bookings/book", body);
294
409
  }
295
410
  /**
296
- * Set up payment method (required before booking).
411
+ * Set up payment method (required before unlock/booking).
297
412
  */
298
413
  async setupPayment(token = "tok_visa") {
299
414
  this.requireApiKey();
300
- return this.post("/api/v1/agents/setup-payment", { token });
301
- }
302
- /**
303
- * Start automated checkout — drives to payment page, NEVER submits payment.
304
- *
305
- * Requires unlock first. Returns progress with screenshot and
306
- * booking URL for manual completion.
307
- *
308
- * @param offerId - Offer ID from search results
309
- * @param passengers - Passenger details (use test data for safety)
310
- * @param checkoutToken - Token from unlock() response
311
- */
312
- async startCheckout(offerId, passengers, checkoutToken) {
313
- this.requireApiKey();
314
- return this.post("/api/v1/bookings/start-checkout", {
315
- offer_id: offerId,
316
- passengers,
317
- checkout_token: checkoutToken
318
- });
319
- }
320
- /**
321
- * Start checkout locally via Python (runs on your machine).
322
- * Requires: pip install letsfg && playwright install chromium
323
- *
324
- * @param offer - Full FlightOffer object from search results
325
- * @param passengers - Passenger details
326
- * @param checkoutToken - Token from unlock()
327
- */
328
- async startCheckoutLocal(offer, passengers, checkoutToken) {
329
- const { spawn } = await import("child_process");
330
- const input = JSON.stringify({
331
- __checkout: true,
332
- offer,
333
- passengers,
334
- checkout_token: checkoutToken,
335
- api_key: this.apiKey,
336
- base_url: this.baseUrl
337
- });
338
- return new Promise((resolve, reject) => {
339
- const pythonCmd = process.platform === "win32" ? "python" : "python3";
340
- const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
341
- stdio: ["pipe", "pipe", "pipe"],
342
- timeout: 18e4
343
- });
344
- let stdout = "";
345
- let stderr = "";
346
- child.stdout.on("data", (d) => {
347
- stdout += d.toString();
348
- });
349
- child.stderr.on("data", (d) => {
350
- stderr += d.toString();
351
- });
352
- child.on("close", (code) => {
353
- try {
354
- const data = JSON.parse(stdout);
355
- if (data.error) reject(new LetsFGError(data.error));
356
- else resolve(data);
357
- } catch {
358
- reject(new LetsFGError(`Checkout failed (code ${code}): ${stdout || stderr}`));
359
- }
360
- });
361
- child.on("error", (err) => {
362
- reject(new LetsFGError(`Cannot start Python: ${err.message}`));
363
- });
364
- child.stdin.write(input);
365
- child.stdin.end();
366
- });
367
- }
368
- /**
369
- * Link GitHub account for FREE unlimited access.
370
- *
371
- * Star https://github.com/LetsFG/LetsFG, then call this with your username.
372
- * Once verified, access is permanent.
373
- */
374
- async linkGithub(githubUsername) {
375
- this.requireApiKey();
376
- return this.post("/api/v1/agents/link-github", { github_username: githubUsername });
415
+ return this.post("/developers/api/v1/agents/setup-payment", { token });
377
416
  }
378
417
  /**
379
418
  * Get current agent profile and usage stats.
380
419
  */
381
420
  async me() {
382
421
  this.requireApiKey();
383
- return this.get("/api/v1/agents/me");
422
+ return this.get("/developers/api/v1/agents/me");
384
423
  }
385
424
  // ── Static methods ───────────────────────────────────────────────────
386
425
  /**
387
- * Register a new agent — no API key needed.
426
+ * Register a new Developer API agent — no auth needed.
388
427
  */
389
428
  static async register(agentName, email, baseUrl, ownerName = "", description = "") {
390
429
  const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
391
- const resp = await fetch(`${url}/api/v1/agents/register`, {
430
+ const resp = await fetch(`${url}/developers/api/v1/agents/register`, {
392
431
  method: "POST",
393
432
  headers: { "Content-Type": "application/json" },
394
- body: JSON.stringify({
395
- agent_name: agentName,
396
- email,
397
- owner_name: ownerName,
398
- description
399
- })
433
+ body: JSON.stringify({ agent_name: agentName, email, owner_name: ownerName, description })
400
434
  });
401
435
  const data = await resp.json();
402
436
  if (!resp.ok) {
@@ -409,28 +443,39 @@ var LetsFG = class {
409
443
  return data;
410
444
  }
411
445
  // ── Internal ────────────────────────────────────────────────────────
446
+ async postWithBearer(path, body) {
447
+ return this.requestWithHeaders(path, "POST", { "Authorization": `Bearer ${this.bearerToken}` }, body);
448
+ }
449
+ async getNoAuth(path) {
450
+ return this.requestWithHeaders(path, "GET", {});
451
+ }
452
+ async getWithAuth(path) {
453
+ const headers = this.usingPFS ? { "Authorization": `Bearer ${this.bearerToken}` } : { "X-API-Key": this.apiKey };
454
+ return this.requestWithHeaders(path, "GET", headers);
455
+ }
456
+ async postWithAuth(path, body) {
457
+ const headers = this.usingPFS ? { "Authorization": `Bearer ${this.bearerToken}` } : { "X-API-Key": this.apiKey };
458
+ return this.requestWithHeaders(path, "POST", headers, body);
459
+ }
412
460
  async post(path, body) {
413
- return this.request(path, {
414
- method: "POST",
415
- body: JSON.stringify(body)
416
- });
461
+ return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body);
417
462
  }
418
463
  async get(path) {
419
- return this.request(path, { method: "GET" });
464
+ return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey });
420
465
  }
421
- async request(path, init) {
466
+ async requestWithHeaders(path, method, extraHeaders, body) {
422
467
  const controller = new AbortController();
423
468
  const timer = setTimeout(() => controller.abort(), this.timeout);
424
469
  try {
425
470
  const resp = await fetch(`${this.baseUrl}${path}`, {
426
- ...init,
471
+ method,
427
472
  headers: {
428
473
  "Content-Type": "application/json",
429
- "X-API-Key": this.apiKey,
430
474
  "User-Agent": "LetsFG-js/0.1.0",
431
475
  "X-Client-Type": "js-sdk",
432
- ...init.headers || {}
476
+ ...extraHeaders
433
477
  },
478
+ ...body != null ? { body: JSON.stringify(body) } : {},
434
479
  signal: controller.signal
435
480
  });
436
481
  const data = await resp.json();
@@ -498,6 +543,8 @@ async function cmdSearch(args) {
498
543
  const currency = getFlag(args, "--currency") || "EUR";
499
544
  const limit = parseInt(getFlag(args, "--limit", "-l") || "20");
500
545
  const sort = getFlag(args, "--sort") || "price";
546
+ const departureFrom = getFlag(args, "--departure-from");
547
+ const departureTo = getFlag(args, "--departure-to");
501
548
  const [origin, destination, date] = args;
502
549
  if (!origin || !destination || !date) {
503
550
  console.error("Usage: letsfg search <origin> <destination> <date> [options]");
@@ -511,7 +558,9 @@ async function cmdSearch(args) {
511
558
  maxStopovers: stops,
512
559
  currency,
513
560
  limit,
514
- sort
561
+ sort,
562
+ departureTimeFrom: departureFrom,
563
+ departureTimeTo: departureTo
515
564
  });
516
565
  if (jsonOut) {
517
566
  console.log(JSON.stringify({
@@ -663,50 +712,9 @@ async function cmdRegister(args) {
663
712
  Save your API key:`);
664
713
  console.log(` export LETSFG_API_KEY=${result.api_key}`);
665
714
  console.log(`
666
- Next: Star the repo and link your GitHub:`);
667
- console.log(` 1. Star https://github.com/LetsFG/LetsFG`);
668
- console.log(` 2. letsfg star --github <your-github-username>
715
+ Next: letsfg search GDN BCN 2026-07-15
669
716
  `);
670
717
  }
671
- async function cmdStar(args) {
672
- const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
673
- const apiKey = getFlag(args, "--api-key", "-k");
674
- const baseUrl = getFlag(args, "--base-url");
675
- const github = getFlag(args, "--github", "-g");
676
- if (!github) {
677
- console.error("Usage: letsfg star --github <your-github-username>");
678
- process.exit(1);
679
- }
680
- const bt = new LetsFG({ apiKey, baseUrl });
681
- const result = await bt.linkGithub(github);
682
- if (jsonOut) {
683
- console.log(JSON.stringify(result, null, 2));
684
- return;
685
- }
686
- const status = result.status;
687
- if (status === "verified") {
688
- console.log(`
689
- \u2713 GitHub star verified! Unlimited access granted.`);
690
- console.log(` Username: ${result.github_username}`);
691
- console.log(`
692
- You're all set \u2014 search, unlock, and book for free.
693
- `);
694
- } else if (status === "already_verified") {
695
- console.log(`
696
- \u2713 Already verified! You have unlimited access.`);
697
- console.log(` Username: ${result.github_username}
698
- `);
699
- } else if (status === "star_required") {
700
- console.log(`
701
- \u2717 Star not found for '${github}'.`);
702
- console.log(` 1. Star the repo: https://github.com/LetsFG/LetsFG`);
703
- console.log(` 2. Run this command again.
704
- `);
705
- } else {
706
- console.error(` \u2717 Unexpected status: ${status}`);
707
- process.exit(1);
708
- }
709
- }
710
718
  async function cmdSetupPayment(args) {
711
719
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
712
720
  const apiKey = getFlag(args, "--api-key", "-k");
@@ -743,17 +751,8 @@ async function cmdMe(args) {
743
751
  Agent: ${p.agent_name} (${p.agent_id})`);
744
752
  console.log(` Email: ${p.email}`);
745
753
  console.log(` Tier: ${p.tier}`);
746
- const gh = p.github_username || "";
747
- const starOk = p.github_star_verified || false;
748
- if (starOk) {
749
- console.log(` GitHub: \u2713 ${gh} (star verified)`);
750
- } else if (gh) {
751
- console.log(` GitHub: ${gh} (star not yet verified \u2014 run: letsfg star --github ${gh})`);
752
- } else {
753
- console.log(` GitHub: Not linked \u2014 run: letsfg star --github <username>`);
754
- }
755
754
  const access = p.access_granted || false;
756
- console.log(` Access: ${access ? "\u2713 Granted (search, unlock, book)" : "\u2717 Not granted \u2014 star the repo to unlock"}`);
755
+ console.log(` Access: ${access ? "\u2713 Granted (search, unlock, book)" : "\u2717 Not granted"}`);
757
756
  console.log(` Payment: ${p.payment_ready ? "\u2713 Ready" : "\u2014"}`);
758
757
  console.log(` Searches: ${u.total_searches || 0}`);
759
758
  console.log(` Unlocks: ${u.total_unlocks || 0}`);
@@ -764,17 +763,16 @@ async function cmdMe(args) {
764
763
  var HELP = `
765
764
  LetsFG \u2014 Agent-native flight search & booking.
766
765
 
767
- Search 400+ airlines at raw airline prices \u2014 $20-50 cheaper than OTAs.
768
- 100% FREE \u2014 just star our GitHub repo for unlimited access.
766
+ Search hundreds of airlines via the LetsFG cloud engine.
767
+ Free search: authenticate once with Twitter/X, then search instantly.
769
768
 
770
769
  Commands:
771
- search <origin> <dest> <date> Search for flights (FREE)
770
+ search <origin> <dest> <date> Search for flights (free)
772
771
  locations <query> Resolve city name to IATA codes
773
- star --github <username> Link GitHub \u2014 star repo for free access
774
- unlock <offer_id> Unlock offer (FREE with GitHub star)
775
- book <offer_id> --passenger ... Book flight (FREE after unlock)
776
- register --name ... --email ... Register new agent
777
- setup-payment Legacy payment setup
772
+ unlock <offer_id> Unlock offer \u2014 1% of ticket (min $3)
773
+ book <offer_id> --passenger ... Book flight \u2014 charges ticket price via Stripe
774
+ register --name ... --email ... Register for a Developer API key
775
+ setup-payment Attach Stripe payment method
778
776
  me Show agent profile
779
777
 
780
778
  Options:
@@ -784,7 +782,6 @@ Options:
784
782
 
785
783
  Examples:
786
784
  letsfg register --name my-agent --email me@example.com
787
- letsfg star --github octocat
788
785
  letsfg search GDN BER 2026-03-03 --sort price
789
786
  letsfg unlock off_xxx
790
787
  letsfg book off_xxx -p '{"id":"pas_xxx",...}' -e john@ex.com
@@ -809,9 +806,6 @@ async function main() {
809
806
  case "register":
810
807
  await cmdRegister(args);
811
808
  break;
812
- case "star":
813
- await cmdStar(args);
814
- break;
815
809
  case "setup-payment":
816
810
  await cmdSetupPayment(args);
817
811
  break;