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/index.mjs
CHANGED
|
@@ -8,13 +8,21 @@ import {
|
|
|
8
8
|
LetsFGError,
|
|
9
9
|
OfferExpiredError,
|
|
10
10
|
PaymentRequiredError,
|
|
11
|
+
TRIP_PURPOSES,
|
|
11
12
|
ValidationError,
|
|
12
13
|
cheapestOffer,
|
|
14
|
+
deduplicateOffers,
|
|
15
|
+
extractOfferDetailSignals,
|
|
16
|
+
getOfferDetailBadges,
|
|
17
|
+
getOfferDetailPromptNotes,
|
|
18
|
+
getPrimaryTripPurpose,
|
|
19
|
+
getProfileLabel,
|
|
13
20
|
index_default,
|
|
21
|
+
normalizeTripPurposes,
|
|
14
22
|
offerSummary,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "./chunk-
|
|
23
|
+
rankOffers,
|
|
24
|
+
selectDiverseTop
|
|
25
|
+
} from "./chunk-7C3EKQ37.mjs";
|
|
18
26
|
export {
|
|
19
27
|
AuthenticationError,
|
|
20
28
|
BoostedTravel,
|
|
@@ -25,12 +33,18 @@ export {
|
|
|
25
33
|
LetsFGError,
|
|
26
34
|
OfferExpiredError,
|
|
27
35
|
PaymentRequiredError,
|
|
36
|
+
TRIP_PURPOSES,
|
|
28
37
|
ValidationError,
|
|
29
38
|
cheapestOffer,
|
|
39
|
+
deduplicateOffers,
|
|
30
40
|
index_default as default,
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
extractOfferDetailSignals,
|
|
42
|
+
getOfferDetailBadges,
|
|
43
|
+
getOfferDetailPromptNotes,
|
|
44
|
+
getPrimaryTripPurpose,
|
|
45
|
+
getProfileLabel,
|
|
46
|
+
normalizeTripPurposes,
|
|
33
47
|
offerSummary,
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
rankOffers,
|
|
49
|
+
selectDiverseTop
|
|
36
50
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letsfg",
|
|
3
|
-
"version": "2026.5.
|
|
4
|
-
"description": "Flight search & booking for AI agents.
|
|
3
|
+
"version": "2026.5.55",
|
|
4
|
+
"description": "Flight search & booking for AI agents. Server-side engine covers hundreds of airlines. Free search via Bearer token or prepaid Developer API. Includes open-source ranking engine.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean",
|
|
18
|
+
"test": "tsx --test src/index.test.ts",
|
|
18
19
|
"prepublishOnly": "npm run build"
|
|
19
20
|
},
|
|
20
21
|
"keywords": [
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@types/node": "^25.3.3",
|
|
44
45
|
"tsup": "^8.0.0",
|
|
46
|
+
"tsx": "^4.19.0",
|
|
45
47
|
"typescript": "^5.3.0"
|
|
46
48
|
},
|
|
47
49
|
"engines": {
|
package/dist/chunk-LBHJ4GE4.mjs
DELETED
|
@@ -1,487 +0,0 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
var ErrorCode = {
|
|
3
|
-
// Transient (safe to retry after short delay)
|
|
4
|
-
SUPPLIER_TIMEOUT: "SUPPLIER_TIMEOUT",
|
|
5
|
-
RATE_LIMITED: "RATE_LIMITED",
|
|
6
|
-
SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
|
|
7
|
-
NETWORK_ERROR: "NETWORK_ERROR",
|
|
8
|
-
// Validation (fix input, then retry)
|
|
9
|
-
INVALID_IATA: "INVALID_IATA",
|
|
10
|
-
INVALID_DATE: "INVALID_DATE",
|
|
11
|
-
INVALID_PASSENGERS: "INVALID_PASSENGERS",
|
|
12
|
-
UNSUPPORTED_ROUTE: "UNSUPPORTED_ROUTE",
|
|
13
|
-
MISSING_PARAMETER: "MISSING_PARAMETER",
|
|
14
|
-
INVALID_PARAMETER: "INVALID_PARAMETER",
|
|
15
|
-
// Business (requires human decision)
|
|
16
|
-
AUTH_INVALID: "AUTH_INVALID",
|
|
17
|
-
PAYMENT_REQUIRED: "PAYMENT_REQUIRED",
|
|
18
|
-
PAYMENT_DECLINED: "PAYMENT_DECLINED",
|
|
19
|
-
OFFER_EXPIRED: "OFFER_EXPIRED",
|
|
20
|
-
OFFER_NOT_UNLOCKED: "OFFER_NOT_UNLOCKED",
|
|
21
|
-
FARE_CHANGED: "FARE_CHANGED",
|
|
22
|
-
ALREADY_BOOKED: "ALREADY_BOOKED",
|
|
23
|
-
BOOKING_FAILED: "BOOKING_FAILED"
|
|
24
|
-
};
|
|
25
|
-
var ErrorCategory = {
|
|
26
|
-
TRANSIENT: "transient",
|
|
27
|
-
VALIDATION: "validation",
|
|
28
|
-
BUSINESS: "business"
|
|
29
|
-
};
|
|
30
|
-
var CODE_TO_CATEGORY = {
|
|
31
|
-
[ErrorCode.SUPPLIER_TIMEOUT]: ErrorCategory.TRANSIENT,
|
|
32
|
-
[ErrorCode.RATE_LIMITED]: ErrorCategory.TRANSIENT,
|
|
33
|
-
[ErrorCode.SERVICE_UNAVAILABLE]: ErrorCategory.TRANSIENT,
|
|
34
|
-
[ErrorCode.NETWORK_ERROR]: ErrorCategory.TRANSIENT,
|
|
35
|
-
[ErrorCode.INVALID_IATA]: ErrorCategory.VALIDATION,
|
|
36
|
-
[ErrorCode.INVALID_DATE]: ErrorCategory.VALIDATION,
|
|
37
|
-
[ErrorCode.INVALID_PASSENGERS]: ErrorCategory.VALIDATION,
|
|
38
|
-
[ErrorCode.UNSUPPORTED_ROUTE]: ErrorCategory.VALIDATION,
|
|
39
|
-
[ErrorCode.MISSING_PARAMETER]: ErrorCategory.VALIDATION,
|
|
40
|
-
[ErrorCode.INVALID_PARAMETER]: ErrorCategory.VALIDATION,
|
|
41
|
-
[ErrorCode.AUTH_INVALID]: ErrorCategory.BUSINESS,
|
|
42
|
-
[ErrorCode.PAYMENT_REQUIRED]: ErrorCategory.BUSINESS,
|
|
43
|
-
[ErrorCode.PAYMENT_DECLINED]: ErrorCategory.BUSINESS,
|
|
44
|
-
[ErrorCode.OFFER_EXPIRED]: ErrorCategory.BUSINESS,
|
|
45
|
-
[ErrorCode.OFFER_NOT_UNLOCKED]: ErrorCategory.BUSINESS,
|
|
46
|
-
[ErrorCode.FARE_CHANGED]: ErrorCategory.BUSINESS,
|
|
47
|
-
[ErrorCode.ALREADY_BOOKED]: ErrorCategory.BUSINESS,
|
|
48
|
-
[ErrorCode.BOOKING_FAILED]: ErrorCategory.BUSINESS
|
|
49
|
-
};
|
|
50
|
-
function inferErrorCode(statusCode, detail) {
|
|
51
|
-
const d = detail.toLowerCase();
|
|
52
|
-
if (statusCode === 401) return ErrorCode.AUTH_INVALID;
|
|
53
|
-
if (statusCode === 402) return d.includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
|
|
54
|
-
if (statusCode === 410) return ErrorCode.OFFER_EXPIRED;
|
|
55
|
-
if (statusCode === 422) {
|
|
56
|
-
if (d.includes("iata") || d.includes("airport")) return ErrorCode.INVALID_IATA;
|
|
57
|
-
if (d.includes("date")) return ErrorCode.INVALID_DATE;
|
|
58
|
-
if (d.includes("passenger")) return ErrorCode.INVALID_PASSENGERS;
|
|
59
|
-
if (d.includes("route")) return ErrorCode.UNSUPPORTED_ROUTE;
|
|
60
|
-
return ErrorCode.INVALID_PARAMETER;
|
|
61
|
-
}
|
|
62
|
-
if (statusCode === 429) return ErrorCode.RATE_LIMITED;
|
|
63
|
-
if (statusCode === 503) return ErrorCode.SERVICE_UNAVAILABLE;
|
|
64
|
-
if (statusCode === 504) return ErrorCode.SUPPLIER_TIMEOUT;
|
|
65
|
-
if (statusCode === 409) return ErrorCode.ALREADY_BOOKED;
|
|
66
|
-
return statusCode >= 500 ? ErrorCode.BOOKING_FAILED : ErrorCode.INVALID_PARAMETER;
|
|
67
|
-
}
|
|
68
|
-
var LetsFGError = class extends Error {
|
|
69
|
-
statusCode;
|
|
70
|
-
response;
|
|
71
|
-
errorCode;
|
|
72
|
-
errorCategory;
|
|
73
|
-
isRetryable;
|
|
74
|
-
constructor(message, statusCode = 0, response = {}, errorCode = "") {
|
|
75
|
-
super(message);
|
|
76
|
-
this.name = "LetsFGError";
|
|
77
|
-
this.statusCode = statusCode;
|
|
78
|
-
this.response = response;
|
|
79
|
-
this.errorCode = errorCode || response.error_code || "";
|
|
80
|
-
this.errorCategory = CODE_TO_CATEGORY[this.errorCode] || ErrorCategory.BUSINESS;
|
|
81
|
-
this.isRetryable = this.errorCategory === ErrorCategory.TRANSIENT;
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
var AuthenticationError = class extends LetsFGError {
|
|
85
|
-
constructor(message, response = {}) {
|
|
86
|
-
super(message, 401, response, ErrorCode.AUTH_INVALID);
|
|
87
|
-
this.name = "AuthenticationError";
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
var PaymentRequiredError = class extends LetsFGError {
|
|
91
|
-
constructor(message, response = {}) {
|
|
92
|
-
const code = message.toLowerCase().includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
|
|
93
|
-
super(message, 402, response, code);
|
|
94
|
-
this.name = "PaymentRequiredError";
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
var OfferExpiredError = class extends LetsFGError {
|
|
98
|
-
constructor(message, response = {}) {
|
|
99
|
-
super(message, 410, response, ErrorCode.OFFER_EXPIRED);
|
|
100
|
-
this.name = "OfferExpiredError";
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
var ValidationError = class extends LetsFGError {
|
|
104
|
-
constructor(message, statusCode = 422, response = {}, errorCode = "") {
|
|
105
|
-
super(message, statusCode, response, errorCode || ErrorCode.INVALID_PARAMETER);
|
|
106
|
-
this.name = "ValidationError";
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
function routeStr(route) {
|
|
110
|
-
if (!route.segments.length) return "";
|
|
111
|
-
const codes = [route.segments[0].origin, ...route.segments.map((s) => s.destination)];
|
|
112
|
-
return codes.join(" \u2192 ");
|
|
113
|
-
}
|
|
114
|
-
function durationHuman(seconds) {
|
|
115
|
-
const h = Math.floor(seconds / 3600);
|
|
116
|
-
const m = Math.floor(seconds % 3600 / 60);
|
|
117
|
-
return `${h}h${m.toString().padStart(2, "0")}m`;
|
|
118
|
-
}
|
|
119
|
-
function offerSummary(offer) {
|
|
120
|
-
const route = routeStr(offer.outbound);
|
|
121
|
-
const dur = durationHuman(offer.outbound.total_duration_seconds);
|
|
122
|
-
const airline = offer.owner_airline || offer.airlines[0] || "?";
|
|
123
|
-
return `${offer.currency} ${offer.price.toFixed(2)} | ${airline} | ${route} | ${dur} | ${offer.outbound.stopovers} stop(s)`;
|
|
124
|
-
}
|
|
125
|
-
function cheapestOffer(result) {
|
|
126
|
-
if (!result.offers.length) return null;
|
|
127
|
-
return result.offers.reduce((min, o) => o.price < min.price ? o : min, result.offers[0]);
|
|
128
|
-
}
|
|
129
|
-
async function searchLocal(origin, destination, dateFrom, options = {}) {
|
|
130
|
-
const { spawn } = await import("child_process");
|
|
131
|
-
const params = JSON.stringify({
|
|
132
|
-
origin: origin.toUpperCase(),
|
|
133
|
-
destination: destination.toUpperCase(),
|
|
134
|
-
date_from: dateFrom,
|
|
135
|
-
adults: options.adults ?? 1,
|
|
136
|
-
children: options.children ?? 0,
|
|
137
|
-
currency: options.currency ?? "EUR",
|
|
138
|
-
limit: options.limit ?? 50,
|
|
139
|
-
return_date: options.returnDate,
|
|
140
|
-
cabin_class: options.cabinClass,
|
|
141
|
-
...options.maxBrowsers != null && { max_browsers: options.maxBrowsers },
|
|
142
|
-
...options.mode != null && { mode: options.mode }
|
|
143
|
-
});
|
|
144
|
-
return new Promise((resolve, reject) => {
|
|
145
|
-
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
146
|
-
const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
|
|
147
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
148
|
-
});
|
|
149
|
-
let stdout = "";
|
|
150
|
-
let stderr = "";
|
|
151
|
-
child.stdout.on("data", (d) => {
|
|
152
|
-
stdout += d.toString();
|
|
153
|
-
});
|
|
154
|
-
child.stderr.on("data", (d) => {
|
|
155
|
-
stderr += d.toString();
|
|
156
|
-
});
|
|
157
|
-
child.on("close", (code) => {
|
|
158
|
-
try {
|
|
159
|
-
const data = JSON.parse(stdout);
|
|
160
|
-
if (data.error) reject(new LetsFGError(data.error));
|
|
161
|
-
else resolve(data);
|
|
162
|
-
} catch {
|
|
163
|
-
reject(new LetsFGError(
|
|
164
|
-
`Python search failed (code ${code}): ${stdout || stderr}
|
|
165
|
-
Make sure LetsFG is installed: pip install letsfg && playwright install chromium`
|
|
166
|
-
));
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
child.on("error", (err) => {
|
|
170
|
-
reject(new LetsFGError(
|
|
171
|
-
`Cannot start Python: ${err.message}
|
|
172
|
-
Install: pip install letsfg && playwright install chromium`
|
|
173
|
-
));
|
|
174
|
-
});
|
|
175
|
-
child.stdin.write(params);
|
|
176
|
-
child.stdin.end();
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
var DEFAULT_BASE_URL = "https://letsfg.co/developers";
|
|
180
|
-
var LetsFG = class {
|
|
181
|
-
apiKey;
|
|
182
|
-
baseUrl;
|
|
183
|
-
timeout;
|
|
184
|
-
constructor(config = {}) {
|
|
185
|
-
this.apiKey = config.apiKey || process.env.LETSFG_API_KEY || "";
|
|
186
|
-
this.baseUrl = (config.baseUrl || process.env.LETSFG_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
187
|
-
this.timeout = config.timeout || 3e4;
|
|
188
|
-
}
|
|
189
|
-
requireApiKey() {
|
|
190
|
-
if (!this.apiKey) {
|
|
191
|
-
throw new AuthenticationError(
|
|
192
|
-
"API key required for this operation. Set apiKey in config or LETSFG_API_KEY env var.\nNote: searchLocal() works without an API key."
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// ── Core methods ─────────────────────────────────────────────────────
|
|
197
|
-
/**
|
|
198
|
-
* Search for flights — FREE, unlimited, runs locally on your machine.
|
|
199
|
-
*
|
|
200
|
-
* Uses 200 airline connectors via Python subprocess. No backend call.
|
|
201
|
-
* Requires: pip install letsfg && playwright install chromium
|
|
202
|
-
*
|
|
203
|
-
* @param origin - IATA code (e.g., "GDN", "LON")
|
|
204
|
-
* @param destination - IATA code (e.g., "BER", "BCN")
|
|
205
|
-
* @param dateFrom - Departure date "YYYY-MM-DD"
|
|
206
|
-
* @param options - Optional search parameters
|
|
207
|
-
*/
|
|
208
|
-
async search(origin, destination, dateFrom, options = {}) {
|
|
209
|
-
return searchLocal(origin, destination, dateFrom, options);
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Resolve a city/airport name to IATA codes — runs locally, no backend call.
|
|
213
|
-
*/
|
|
214
|
-
async resolveLocation(query) {
|
|
215
|
-
const { spawn } = await import("child_process");
|
|
216
|
-
const params = JSON.stringify({ __resolve_location: true, query });
|
|
217
|
-
return new Promise((resolve, reject) => {
|
|
218
|
-
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
219
|
-
const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
|
|
220
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
221
|
-
});
|
|
222
|
-
let stdout = "";
|
|
223
|
-
let stderr = "";
|
|
224
|
-
child.stdout.on("data", (d) => {
|
|
225
|
-
stdout += d.toString();
|
|
226
|
-
});
|
|
227
|
-
child.stderr.on("data", (d) => {
|
|
228
|
-
stderr += d.toString();
|
|
229
|
-
});
|
|
230
|
-
child.on("close", (code) => {
|
|
231
|
-
try {
|
|
232
|
-
const data = JSON.parse(stdout);
|
|
233
|
-
if (data.error) reject(new LetsFGError(data.error));
|
|
234
|
-
else resolve(Array.isArray(data) ? data : data.locations || [data]);
|
|
235
|
-
} catch {
|
|
236
|
-
reject(new LetsFGError(
|
|
237
|
-
`Location resolution failed (code ${code}): ${stdout || stderr}`
|
|
238
|
-
));
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
child.on("error", (err) => {
|
|
242
|
-
reject(new LetsFGError(`Cannot start Python: ${err.message}`));
|
|
243
|
-
});
|
|
244
|
-
child.stdin.write(params);
|
|
245
|
-
child.stdin.end();
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Unlock a flight offer — FREE with GitHub star.
|
|
250
|
-
* Confirms price, reserves for 30 minutes.
|
|
251
|
-
*/
|
|
252
|
-
async unlock(offerId) {
|
|
253
|
-
this.requireApiKey();
|
|
254
|
-
return this.post("/api/v1/bookings/unlock", { offer_id: offerId });
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Book a flight — charges ticket price via Stripe.
|
|
258
|
-
* Creates a real airline reservation with PNR.
|
|
259
|
-
*
|
|
260
|
-
* Always provide idempotencyKey to prevent double-bookings on retry.
|
|
261
|
-
*/
|
|
262
|
-
async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
|
|
263
|
-
this.requireApiKey();
|
|
264
|
-
const body = {
|
|
265
|
-
offer_id: offerId,
|
|
266
|
-
booking_type: "flight",
|
|
267
|
-
passengers,
|
|
268
|
-
contact_email: contactEmail,
|
|
269
|
-
contact_phone: contactPhone
|
|
270
|
-
};
|
|
271
|
-
if (idempotencyKey) body.idempotency_key = idempotencyKey;
|
|
272
|
-
return this.post("/api/v1/bookings/book", body);
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Set up payment method (required before booking).
|
|
276
|
-
*/
|
|
277
|
-
async setupPayment(token = "tok_visa") {
|
|
278
|
-
this.requireApiKey();
|
|
279
|
-
return this.post("/api/v1/agents/setup-payment", { token });
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Start automated checkout — drives to payment page, NEVER submits payment.
|
|
283
|
-
*
|
|
284
|
-
* Requires unlock first. Returns progress with screenshot and
|
|
285
|
-
* booking URL for manual completion.
|
|
286
|
-
*
|
|
287
|
-
* @param offerId - Offer ID from search results
|
|
288
|
-
* @param passengers - Passenger details (use test data for safety)
|
|
289
|
-
* @param checkoutToken - Token from unlock() response
|
|
290
|
-
*/
|
|
291
|
-
async startCheckout(offerId, passengers, checkoutToken) {
|
|
292
|
-
this.requireApiKey();
|
|
293
|
-
return this.post("/api/v1/bookings/start-checkout", {
|
|
294
|
-
offer_id: offerId,
|
|
295
|
-
passengers,
|
|
296
|
-
checkout_token: checkoutToken
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Start checkout locally via Python (runs on your machine).
|
|
301
|
-
* Requires: pip install letsfg && playwright install chromium
|
|
302
|
-
*
|
|
303
|
-
* @param offer - Full FlightOffer object from search results
|
|
304
|
-
* @param passengers - Passenger details
|
|
305
|
-
* @param checkoutToken - Token from unlock()
|
|
306
|
-
*/
|
|
307
|
-
async startCheckoutLocal(offer, passengers, checkoutToken) {
|
|
308
|
-
const { spawn } = await import("child_process");
|
|
309
|
-
const input = JSON.stringify({
|
|
310
|
-
__checkout: true,
|
|
311
|
-
offer,
|
|
312
|
-
passengers,
|
|
313
|
-
checkout_token: checkoutToken,
|
|
314
|
-
api_key: this.apiKey,
|
|
315
|
-
base_url: this.baseUrl
|
|
316
|
-
});
|
|
317
|
-
return new Promise((resolve, reject) => {
|
|
318
|
-
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
319
|
-
const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
|
|
320
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
321
|
-
timeout: 18e4
|
|
322
|
-
});
|
|
323
|
-
let stdout = "";
|
|
324
|
-
let stderr = "";
|
|
325
|
-
child.stdout.on("data", (d) => {
|
|
326
|
-
stdout += d.toString();
|
|
327
|
-
});
|
|
328
|
-
child.stderr.on("data", (d) => {
|
|
329
|
-
stderr += d.toString();
|
|
330
|
-
});
|
|
331
|
-
child.on("close", (code) => {
|
|
332
|
-
try {
|
|
333
|
-
const data = JSON.parse(stdout);
|
|
334
|
-
if (data.error) reject(new LetsFGError(data.error));
|
|
335
|
-
else resolve(data);
|
|
336
|
-
} catch {
|
|
337
|
-
reject(new LetsFGError(`Checkout failed (code ${code}): ${stdout || stderr}`));
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
child.on("error", (err) => {
|
|
341
|
-
reject(new LetsFGError(`Cannot start Python: ${err.message}`));
|
|
342
|
-
});
|
|
343
|
-
child.stdin.write(input);
|
|
344
|
-
child.stdin.end();
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Link GitHub account for FREE unlimited access.
|
|
349
|
-
*
|
|
350
|
-
* Star https://github.com/LetsFG/LetsFG, then call this with your username.
|
|
351
|
-
* Once verified, access is permanent.
|
|
352
|
-
*/
|
|
353
|
-
async linkGithub(githubUsername) {
|
|
354
|
-
this.requireApiKey();
|
|
355
|
-
return this.post("/api/v1/agents/link-github", { github_username: githubUsername });
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Get current agent profile and usage stats.
|
|
359
|
-
*/
|
|
360
|
-
async me() {
|
|
361
|
-
this.requireApiKey();
|
|
362
|
-
return this.get("/api/v1/agents/me");
|
|
363
|
-
}
|
|
364
|
-
// ── Static methods ───────────────────────────────────────────────────
|
|
365
|
-
/**
|
|
366
|
-
* Register a new agent — no API key needed.
|
|
367
|
-
*/
|
|
368
|
-
static async register(agentName, email, baseUrl, ownerName = "", description = "") {
|
|
369
|
-
const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
370
|
-
const resp = await fetch(`${url}/api/v1/agents/register`, {
|
|
371
|
-
method: "POST",
|
|
372
|
-
headers: { "Content-Type": "application/json" },
|
|
373
|
-
body: JSON.stringify({
|
|
374
|
-
agent_name: agentName,
|
|
375
|
-
email,
|
|
376
|
-
owner_name: ownerName,
|
|
377
|
-
description
|
|
378
|
-
})
|
|
379
|
-
});
|
|
380
|
-
const data = await resp.json();
|
|
381
|
-
if (!resp.ok) {
|
|
382
|
-
throw new LetsFGError(
|
|
383
|
-
data.detail || `Registration failed (${resp.status})`,
|
|
384
|
-
resp.status,
|
|
385
|
-
data
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
return data;
|
|
389
|
-
}
|
|
390
|
-
// ── Internal ────────────────────────────────────────────────────────
|
|
391
|
-
async post(path, body) {
|
|
392
|
-
return this.request(path, {
|
|
393
|
-
method: "POST",
|
|
394
|
-
body: JSON.stringify(body)
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
async get(path) {
|
|
398
|
-
return this.request(path, { method: "GET" });
|
|
399
|
-
}
|
|
400
|
-
async request(path, init) {
|
|
401
|
-
const controller = new AbortController();
|
|
402
|
-
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
403
|
-
try {
|
|
404
|
-
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
405
|
-
...init,
|
|
406
|
-
headers: {
|
|
407
|
-
"Content-Type": "application/json",
|
|
408
|
-
"X-API-Key": this.apiKey,
|
|
409
|
-
"User-Agent": "LetsFG-js/0.1.0",
|
|
410
|
-
"X-Client-Type": "js-sdk",
|
|
411
|
-
...init.headers || {}
|
|
412
|
-
},
|
|
413
|
-
signal: controller.signal
|
|
414
|
-
});
|
|
415
|
-
const data = await resp.json();
|
|
416
|
-
if (!resp.ok) {
|
|
417
|
-
const detail = data.detail || `API error (${resp.status})`;
|
|
418
|
-
const code = data.error_code || inferErrorCode(resp.status, detail);
|
|
419
|
-
if (resp.status === 401) throw new AuthenticationError(detail, data);
|
|
420
|
-
if (resp.status === 402) throw new PaymentRequiredError(detail, data);
|
|
421
|
-
if (resp.status === 410) throw new OfferExpiredError(detail, data);
|
|
422
|
-
if (resp.status === 422) throw new ValidationError(detail, resp.status, data, code);
|
|
423
|
-
throw new LetsFGError(detail, resp.status, data, code);
|
|
424
|
-
}
|
|
425
|
-
return data;
|
|
426
|
-
} finally {
|
|
427
|
-
clearTimeout(timer);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
async function systemInfo() {
|
|
432
|
-
const { spawn } = await import("child_process");
|
|
433
|
-
return new Promise((resolve, reject) => {
|
|
434
|
-
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
435
|
-
const child = spawn(pythonCmd, ["-m", "letsfg.local"], {
|
|
436
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
437
|
-
});
|
|
438
|
-
let stdout = "";
|
|
439
|
-
let stderr = "";
|
|
440
|
-
child.stdout.on("data", (d) => {
|
|
441
|
-
stdout += d.toString();
|
|
442
|
-
});
|
|
443
|
-
child.stderr.on("data", (d) => {
|
|
444
|
-
stderr += d.toString();
|
|
445
|
-
});
|
|
446
|
-
child.on("close", (code) => {
|
|
447
|
-
try {
|
|
448
|
-
const data = JSON.parse(stdout);
|
|
449
|
-
if (data.error) reject(new LetsFGError(data.error));
|
|
450
|
-
else resolve(data);
|
|
451
|
-
} catch {
|
|
452
|
-
reject(new LetsFGError(
|
|
453
|
-
`Python system-info failed (code ${code}): ${stdout || stderr}`
|
|
454
|
-
));
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
child.on("error", (err) => {
|
|
458
|
-
reject(new LetsFGError(
|
|
459
|
-
`Cannot start Python: ${err.message}
|
|
460
|
-
Install: pip install letsfg`
|
|
461
|
-
));
|
|
462
|
-
});
|
|
463
|
-
child.stdin.write(JSON.stringify({ __system_info: true }));
|
|
464
|
-
child.stdin.end();
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
var index_default = LetsFG;
|
|
468
|
-
var BoostedTravel = LetsFG;
|
|
469
|
-
var BoostedTravelError = LetsFGError;
|
|
470
|
-
|
|
471
|
-
export {
|
|
472
|
-
ErrorCode,
|
|
473
|
-
ErrorCategory,
|
|
474
|
-
LetsFGError,
|
|
475
|
-
AuthenticationError,
|
|
476
|
-
PaymentRequiredError,
|
|
477
|
-
OfferExpiredError,
|
|
478
|
-
ValidationError,
|
|
479
|
-
offerSummary,
|
|
480
|
-
cheapestOffer,
|
|
481
|
-
searchLocal,
|
|
482
|
-
LetsFG,
|
|
483
|
-
systemInfo,
|
|
484
|
-
index_default,
|
|
485
|
-
BoostedTravel,
|
|
486
|
-
BoostedTravelError
|
|
487
|
-
};
|