letsfg 2026.5.53 → 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/README.md +10 -101
- package/dist/chunk-7C3EKQ37.mjs +1768 -0
- package/dist/cli.js +267 -279
- package/dist/cli.mjs +10 -65
- package/dist/index.d.mts +300 -84
- package/dist/index.d.ts +300 -84
- package/dist/index.js +1525 -248
- package/dist/index.mjs +21 -7
- package/package.json +4 -2
- package/dist/chunk-LBHJ4GE4.mjs +0 -487
package/dist/cli.js
CHANGED
|
@@ -1,27 +1,171 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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("
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
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
|
|
328
|
+
* Search for flights — FREE.
|
|
220
329
|
*
|
|
221
|
-
* Uses
|
|
222
|
-
*
|
|
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
|
-
|
|
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
|
|
375
|
+
* Resolve a city/airport name to IATA codes.
|
|
234
376
|
*/
|
|
235
377
|
async resolveLocation(query) {
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
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 —
|
|
271
|
-
*
|
|
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.
|
|
275
|
-
|
|
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
|
|
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.
|
|
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.
|
|
462
|
+
return this.requestWithHeaders(path, "GET", { "X-API-Key": this.apiKey });
|
|
420
463
|
}
|
|
421
|
-
async
|
|
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
|
-
|
|
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
|
-
...
|
|
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:
|
|
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
|
|
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
|
|
768
|
-
|
|
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 (
|
|
764
|
+
search <origin> <dest> <date> Search for flights (free)
|
|
772
765
|
locations <query> Resolve city name to IATA codes
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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;
|