letsfg 2026.5.54 → 2026.5.55

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,60 @@ 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 (this.usingPFS) {
355
+ return this.searchPFS(body);
356
+ }
357
+ return this.post("/developers/api/v1/flights/search", body);
358
+ }
359
+ /** PFS path: POST /api/search -> poll /api/results/<id> */
360
+ async searchPFS(body) {
361
+ const { search_id } = await this.postWithBearer("/api/search", body);
362
+ const deadline = Date.now() + PFS_POLL_TIMEOUT_MS;
363
+ while (Date.now() < deadline) {
364
+ await new Promise((r) => setTimeout(r, PFS_POLL_INTERVAL_MS));
365
+ const result = await this.getNoAuth(
366
+ `/api/results/${search_id}`
367
+ );
368
+ if (result.status !== "pending") {
369
+ return result;
370
+ }
371
+ }
372
+ throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
231
373
  }
232
374
  /**
233
- * Resolve a city/airport name to IATA codes — runs locally, no backend call.
375
+ * Resolve a city/airport name to IATA codes.
234
376
  */
235
377
  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
- });
378
+ this.requireAuth();
379
+ const path = this.usingPFS ? `/api/locations?q=${encodeURIComponent(query)}` : `/developers/api/v1/flights/locations/${encodeURIComponent(query)}`;
380
+ const data = await this.getWithAuth(path);
381
+ return Array.isArray(data) ? data : data.locations || [];
268
382
  }
269
383
  /**
270
- * Unlock a flight offer — FREE with GitHub star.
271
- * Confirms price, reserves for 30 minutes.
384
+ * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
385
+ * Cost: 1% of ticket price, min $3. Free with Developer API.
272
386
  */
273
387
  async unlock(offerId) {
274
- this.requireApiKey();
275
- return this.post("/api/v1/bookings/unlock", { offer_id: offerId });
388
+ this.requireAuth();
389
+ const path = this.usingPFS ? "/api/unlock" : "/developers/api/v1/bookings/unlock";
390
+ return this.postWithAuth(path, { offer_id: offerId });
276
391
  }
277
392
  /**
278
- * Book a flight — charges ticket price via Stripe.
279
- * Creates a real airline reservation with PNR.
280
- *
393
+ * Book a flight via Developer API — charges ticket price via Stripe, creates real PNR.
281
394
  * Always provide idempotencyKey to prevent double-bookings on retry.
282
395
  */
283
396
  async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
@@ -290,113 +403,32 @@ var LetsFG = class {
290
403
  contact_phone: contactPhone
291
404
  };
292
405
  if (idempotencyKey) body.idempotency_key = idempotencyKey;
293
- return this.post("/api/v1/bookings/book", body);
406
+ return this.post("/developers/api/v1/bookings/book", body);
294
407
  }
295
408
  /**
296
- * Set up payment method (required before booking).
409
+ * Set up payment method (required before unlock/booking).
297
410
  */
298
411
  async setupPayment(token = "tok_visa") {
299
412
  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 });
413
+ return this.post("/developers/api/v1/agents/setup-payment", { token });
377
414
  }
378
415
  /**
379
416
  * Get current agent profile and usage stats.
380
417
  */
381
418
  async me() {
382
419
  this.requireApiKey();
383
- return this.get("/api/v1/agents/me");
420
+ return this.get("/developers/api/v1/agents/me");
384
421
  }
385
422
  // ── Static methods ───────────────────────────────────────────────────
386
423
  /**
387
- * Register a new agent — no API key needed.
424
+ * Register a new Developer API agent — no auth needed.
388
425
  */
389
426
  static async register(agentName, email, baseUrl, ownerName = "", description = "") {
390
427
  const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
391
- const resp = await fetch(`${url}/api/v1/agents/register`, {
428
+ const resp = await fetch(`${url}/developers/api/v1/agents/register`, {
392
429
  method: "POST",
393
430
  headers: { "Content-Type": "application/json" },
394
- body: JSON.stringify({
395
- agent_name: agentName,
396
- email,
397
- owner_name: ownerName,
398
- description
399
- })
431
+ body: JSON.stringify({ agent_name: agentName, email, owner_name: ownerName, description })
400
432
  });
401
433
  const data = await resp.json();
402
434
  if (!resp.ok) {
@@ -409,28 +441,39 @@ var LetsFG = class {
409
441
  return data;
410
442
  }
411
443
  // ── Internal ────────────────────────────────────────────────────────
444
+ async postWithBearer(path, body) {
445
+ return this.requestWithHeaders(path, "POST", { "Authorization": `Bearer ${this.bearerToken}` }, body);
446
+ }
447
+ async getNoAuth(path) {
448
+ return this.requestWithHeaders(path, "GET", {});
449
+ }
450
+ async getWithAuth(path) {
451
+ const headers = this.usingPFS ? { "Authorization": `Bearer ${this.bearerToken}` } : { "X-API-Key": this.apiKey };
452
+ return this.requestWithHeaders(path, "GET", headers);
453
+ }
454
+ async postWithAuth(path, body) {
455
+ const headers = this.usingPFS ? { "Authorization": `Bearer ${this.bearerToken}` } : { "X-API-Key": this.apiKey };
456
+ return this.requestWithHeaders(path, "POST", headers, body);
457
+ }
412
458
  async post(path, body) {
413
- return this.request(path, {
414
- method: "POST",
415
- body: JSON.stringify(body)
416
- });
459
+ return this.requestWithHeaders(path, "POST", { "X-API-Key": this.apiKey }, body);
417
460
  }
418
461
  async get(path) {
419
- return this.request(path, { method: "GET" });
462
+ return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey });
420
463
  }
421
- async request(path, init) {
464
+ async requestWithHeaders(path, method, extraHeaders, body) {
422
465
  const controller = new AbortController();
423
466
  const timer = setTimeout(() => controller.abort(), this.timeout);
424
467
  try {
425
468
  const resp = await fetch(`${this.baseUrl}${path}`, {
426
- ...init,
469
+ method,
427
470
  headers: {
428
471
  "Content-Type": "application/json",
429
- "X-API-Key": this.apiKey,
430
472
  "User-Agent": "LetsFG-js/0.1.0",
431
473
  "X-Client-Type": "js-sdk",
432
- ...init.headers || {}
474
+ ...extraHeaders
433
475
  },
476
+ ...body != null ? { body: JSON.stringify(body) } : {},
434
477
  signal: controller.signal
435
478
  });
436
479
  const data = await resp.json();
@@ -663,50 +706,9 @@ async function cmdRegister(args) {
663
706
  Save your API key:`);
664
707
  console.log(` export LETSFG_API_KEY=${result.api_key}`);
665
708
  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>
709
+ Next: letsfg search GDN BCN 2026-07-15
669
710
  `);
670
711
  }
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
712
  async function cmdSetupPayment(args) {
711
713
  const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
712
714
  const apiKey = getFlag(args, "--api-key", "-k");
@@ -743,17 +745,8 @@ async function cmdMe(args) {
743
745
  Agent: ${p.agent_name} (${p.agent_id})`);
744
746
  console.log(` Email: ${p.email}`);
745
747
  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
748
  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"}`);
749
+ console.log(` Access: ${access ? "\u2713 Granted (search, unlock, book)" : "\u2717 Not granted"}`);
757
750
  console.log(` Payment: ${p.payment_ready ? "\u2713 Ready" : "\u2014"}`);
758
751
  console.log(` Searches: ${u.total_searches || 0}`);
759
752
  console.log(` Unlocks: ${u.total_unlocks || 0}`);
@@ -764,17 +757,16 @@ async function cmdMe(args) {
764
757
  var HELP = `
765
758
  LetsFG \u2014 Agent-native flight search & booking.
766
759
 
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.
760
+ Search hundreds of airlines via the LetsFG cloud engine.
761
+ Free search: authenticate once with Twitter/X, then search instantly.
769
762
 
770
763
  Commands:
771
- search <origin> <dest> <date> Search for flights (FREE)
764
+ search <origin> <dest> <date> Search for flights (free)
772
765
  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
766
+ unlock <offer_id> Unlock offer \u2014 1% of ticket (min $3)
767
+ book <offer_id> --passenger ... Book flight \u2014 charges ticket price via Stripe
768
+ register --name ... --email ... Register for a Developer API key
769
+ setup-payment Attach Stripe payment method
778
770
  me Show agent profile
779
771
 
780
772
  Options:
@@ -784,7 +776,6 @@ Options:
784
776
 
785
777
  Examples:
786
778
  letsfg register --name my-agent --email me@example.com
787
- letsfg star --github octocat
788
779
  letsfg search GDN BER 2026-03-03 --sort price
789
780
  letsfg unlock off_xxx
790
781
  letsfg book off_xxx -p '{"id":"pas_xxx",...}' -e john@ex.com
@@ -809,9 +800,6 @@ async function main() {
809
800
  case "register":
810
801
  await cmdRegister(args);
811
802
  break;
812
- case "star":
813
- await cmdStar(args);
814
- break;
815
803
  case "setup-payment":
816
804
  await cmdSetupPayment(args);
817
805
  break;