boostedtravel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,520 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/index.ts
5
+ var BoostedTravelError = class extends Error {
6
+ statusCode;
7
+ response;
8
+ constructor(message, statusCode = 0, response = {}) {
9
+ super(message);
10
+ this.name = "BoostedTravelError";
11
+ this.statusCode = statusCode;
12
+ this.response = response;
13
+ }
14
+ };
15
+ var AuthenticationError = class extends BoostedTravelError {
16
+ constructor(message, response = {}) {
17
+ super(message, 401, response);
18
+ this.name = "AuthenticationError";
19
+ }
20
+ };
21
+ var PaymentRequiredError = class extends BoostedTravelError {
22
+ constructor(message, response = {}) {
23
+ super(message, 402, response);
24
+ this.name = "PaymentRequiredError";
25
+ }
26
+ };
27
+ function routeStr(route) {
28
+ if (!route.segments.length) return "";
29
+ const codes = [route.segments[0].origin, ...route.segments.map((s) => s.destination)];
30
+ return codes.join(" \u2192 ");
31
+ }
32
+ function durationHuman(seconds) {
33
+ const h = Math.floor(seconds / 3600);
34
+ const m = Math.floor(seconds % 3600 / 60);
35
+ return `${h}h${m.toString().padStart(2, "0")}m`;
36
+ }
37
+ function offerSummary(offer) {
38
+ const route = routeStr(offer.outbound);
39
+ const dur = durationHuman(offer.outbound.total_duration_seconds);
40
+ const airline = offer.owner_airline || offer.airlines[0] || "?";
41
+ return `${offer.currency} ${offer.price.toFixed(2)} | ${airline} | ${route} | ${dur} | ${offer.outbound.stopovers} stop(s)`;
42
+ }
43
+ var DEFAULT_BASE_URL = "https://api.boostedchat.com";
44
+ var BoostedTravel = class {
45
+ apiKey;
46
+ baseUrl;
47
+ timeout;
48
+ constructor(config = {}) {
49
+ this.apiKey = config.apiKey || process.env.BOOSTEDTRAVEL_API_KEY || "";
50
+ this.baseUrl = (config.baseUrl || process.env.BOOSTEDTRAVEL_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
51
+ this.timeout = config.timeout || 3e4;
52
+ if (!this.apiKey) {
53
+ throw new AuthenticationError(
54
+ "API key required. Set apiKey in config or BOOSTEDTRAVEL_API_KEY env var. Get one: POST /api/v1/agents/register"
55
+ );
56
+ }
57
+ }
58
+ // ── Core methods ─────────────────────────────────────────────────────
59
+ /**
60
+ * Search for flights — FREE, unlimited.
61
+ *
62
+ * @param origin - IATA code (e.g., "GDN", "LON")
63
+ * @param destination - IATA code (e.g., "BER", "BCN")
64
+ * @param dateFrom - Departure date "YYYY-MM-DD"
65
+ * @param options - Optional search parameters
66
+ */
67
+ async search(origin, destination, dateFrom, options = {}) {
68
+ const body = {
69
+ origin: origin.toUpperCase(),
70
+ destination: destination.toUpperCase(),
71
+ date_from: dateFrom,
72
+ adults: options.adults ?? 1,
73
+ children: options.children ?? 0,
74
+ infants: options.infants ?? 0,
75
+ max_stopovers: options.maxStopovers ?? 2,
76
+ currency: options.currency ?? "EUR",
77
+ limit: options.limit ?? 20,
78
+ sort: options.sort ?? "price"
79
+ };
80
+ if (options.returnDate) body.return_from = options.returnDate;
81
+ if (options.cabinClass) body.cabin_class = options.cabinClass;
82
+ return this.post("/api/v1/flights/search", body);
83
+ }
84
+ /**
85
+ * Resolve a city/airport name to IATA codes.
86
+ */
87
+ async resolveLocation(query) {
88
+ const data = await this.get(`/api/v1/flights/locations/${encodeURIComponent(query)}`);
89
+ if (Array.isArray(data)) return data;
90
+ if (data && Array.isArray(data.locations)) return data.locations;
91
+ return data ? [data] : [];
92
+ }
93
+ /**
94
+ * Unlock a flight offer — $1 fee.
95
+ * Confirms price, reserves for 30 minutes.
96
+ */
97
+ async unlock(offerId) {
98
+ return this.post("/api/v1/bookings/unlock", { offer_id: offerId });
99
+ }
100
+ /**
101
+ * Book a flight — 2.5% service fee.
102
+ * Creates a real airline reservation with PNR.
103
+ */
104
+ async book(offerId, passengers, contactEmail, contactPhone = "") {
105
+ return this.post("/api/v1/bookings/book", {
106
+ offer_id: offerId,
107
+ booking_type: "flight",
108
+ passengers,
109
+ contact_email: contactEmail,
110
+ contact_phone: contactPhone
111
+ });
112
+ }
113
+ /**
114
+ * Set up payment method (payment token).
115
+ */
116
+ async setupPayment(token = "tok_visa") {
117
+ return this.post("/api/v1/agents/setup-payment", { token });
118
+ }
119
+ /**
120
+ * Get current agent profile and usage stats.
121
+ */
122
+ async me() {
123
+ return this.get("/api/v1/agents/me");
124
+ }
125
+ // ── Static methods ───────────────────────────────────────────────────
126
+ /**
127
+ * Register a new agent — no API key needed.
128
+ */
129
+ static async register(agentName, email, baseUrl, ownerName = "", description = "") {
130
+ const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
131
+ const resp = await fetch(`${url}/api/v1/agents/register`, {
132
+ method: "POST",
133
+ headers: { "Content-Type": "application/json" },
134
+ body: JSON.stringify({
135
+ agent_name: agentName,
136
+ email,
137
+ owner_name: ownerName,
138
+ description
139
+ })
140
+ });
141
+ const data = await resp.json();
142
+ if (!resp.ok) {
143
+ throw new BoostedTravelError(
144
+ data.detail || `Registration failed (${resp.status})`,
145
+ resp.status,
146
+ data
147
+ );
148
+ }
149
+ return data;
150
+ }
151
+ // ── Internal ────────────────────────────────────────────────────────
152
+ async post(path, body) {
153
+ return this.request(path, {
154
+ method: "POST",
155
+ body: JSON.stringify(body)
156
+ });
157
+ }
158
+ async get(path) {
159
+ return this.request(path, { method: "GET" });
160
+ }
161
+ async request(path, init) {
162
+ const controller = new AbortController();
163
+ const timer = setTimeout(() => controller.abort(), this.timeout);
164
+ try {
165
+ const resp = await fetch(`${this.baseUrl}${path}`, {
166
+ ...init,
167
+ headers: {
168
+ "Content-Type": "application/json",
169
+ "X-API-Key": this.apiKey,
170
+ "User-Agent": "boostedtravel-js/0.1.0",
171
+ ...init.headers || {}
172
+ },
173
+ signal: controller.signal
174
+ });
175
+ const data = await resp.json();
176
+ if (!resp.ok) {
177
+ const detail = data.detail || `API error (${resp.status})`;
178
+ if (resp.status === 401) throw new AuthenticationError(detail, data);
179
+ if (resp.status === 402) throw new PaymentRequiredError(detail, data);
180
+ throw new BoostedTravelError(detail, resp.status, data);
181
+ }
182
+ return data;
183
+ } finally {
184
+ clearTimeout(timer);
185
+ }
186
+ }
187
+ };
188
+
189
+ // src/cli.ts
190
+ function getFlag(args, flag, alias) {
191
+ for (let i = 0; i < args.length; i++) {
192
+ if (args[i] === flag || alias && args[i] === alias) {
193
+ const val = args[i + 1];
194
+ args.splice(i, 2);
195
+ return val;
196
+ }
197
+ if (args[i].startsWith(`${flag}=`)) {
198
+ const val = args[i].split("=").slice(1).join("=");
199
+ args.splice(i, 1);
200
+ return val;
201
+ }
202
+ }
203
+ return void 0;
204
+ }
205
+ function hasFlag(args, flag) {
206
+ const idx = args.indexOf(flag);
207
+ if (idx >= 0) {
208
+ args.splice(idx, 1);
209
+ return true;
210
+ }
211
+ return false;
212
+ }
213
+ function getAllFlags(args, flag, alias) {
214
+ const results = [];
215
+ let i = 0;
216
+ while (i < args.length) {
217
+ if (args[i] === flag || alias && args[i] === alias) {
218
+ results.push(args[i + 1]);
219
+ args.splice(i, 2);
220
+ } else {
221
+ i++;
222
+ }
223
+ }
224
+ return results;
225
+ }
226
+ async function cmdSearch(args) {
227
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
228
+ const apiKey = getFlag(args, "--api-key", "-k");
229
+ const baseUrl = getFlag(args, "--base-url");
230
+ const returnDate = getFlag(args, "--return", "-r");
231
+ const adults = parseInt(getFlag(args, "--adults", "-a") || "1");
232
+ const cabin = getFlag(args, "--cabin", "-c");
233
+ const stops = parseInt(getFlag(args, "--max-stops", "-s") || "2");
234
+ const currency = getFlag(args, "--currency") || "EUR";
235
+ const limit = parseInt(getFlag(args, "--limit", "-l") || "20");
236
+ const sort = getFlag(args, "--sort") || "price";
237
+ const [origin, destination, date] = args;
238
+ if (!origin || !destination || !date) {
239
+ console.error("Usage: boostedtravel search <origin> <destination> <date> [options]");
240
+ process.exit(1);
241
+ }
242
+ const bt = new BoostedTravel({ apiKey, baseUrl });
243
+ const result = await bt.search(origin, destination, date, {
244
+ returnDate,
245
+ adults,
246
+ cabinClass: cabin,
247
+ maxStopovers: stops,
248
+ currency,
249
+ limit,
250
+ sort
251
+ });
252
+ if (jsonOut) {
253
+ console.log(JSON.stringify({
254
+ passenger_ids: result.passenger_ids,
255
+ total_results: result.total_results,
256
+ offers: result.offers.map((o) => ({
257
+ id: o.id,
258
+ price: o.price,
259
+ currency: o.currency,
260
+ airlines: o.airlines,
261
+ owner_airline: o.owner_airline,
262
+ route: [o.outbound.segments[0]?.origin, ...o.outbound.segments.map((s) => s.destination)].join(" \u2192 "),
263
+ duration_seconds: o.outbound.total_duration_seconds,
264
+ stopovers: o.outbound.stopovers,
265
+ conditions: o.conditions,
266
+ is_locked: o.is_locked
267
+ }))
268
+ }, null, 2));
269
+ return;
270
+ }
271
+ if (!result.offers.length) {
272
+ console.log(`No flights found for ${origin} \u2192 ${destination} on ${date}`);
273
+ return;
274
+ }
275
+ console.log(`
276
+ ${result.total_results} offers | ${origin} \u2192 ${destination} | ${date}`);
277
+ console.log(` Passenger IDs: ${JSON.stringify(result.passenger_ids)}
278
+ `);
279
+ result.offers.forEach((o, i) => {
280
+ console.log(` ${(i + 1).toString().padStart(3)}. ${offerSummary(o)}`);
281
+ console.log(` ID: ${o.id}`);
282
+ });
283
+ console.log(`
284
+ To unlock: boostedtravel unlock <offer_id>`);
285
+ console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
286
+ `);
287
+ }
288
+ async function cmdUnlock(args) {
289
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
290
+ const apiKey = getFlag(args, "--api-key", "-k");
291
+ const baseUrl = getFlag(args, "--base-url");
292
+ const offerId = args[0];
293
+ if (!offerId) {
294
+ console.error("Usage: boostedtravel unlock <offer_id>");
295
+ process.exit(1);
296
+ }
297
+ const bt = new BoostedTravel({ apiKey, baseUrl });
298
+ const result = await bt.unlock(offerId);
299
+ if (jsonOut) {
300
+ console.log(JSON.stringify(result, null, 2));
301
+ return;
302
+ }
303
+ if (result.unlock_status === "unlocked") {
304
+ console.log(`
305
+ \u2713 Offer unlocked!`);
306
+ console.log(` Confirmed price: ${result.confirmed_currency} ${result.confirmed_price?.toFixed(2)}`);
307
+ console.log(` Expires at: ${result.offer_expires_at}`);
308
+ console.log(` $1 unlock fee charged`);
309
+ console.log(`
310
+ Next: boostedtravel book ${offerId} --passenger '{...}' --email you@example.com
311
+ `);
312
+ } else {
313
+ console.error(` \u2717 Unlock failed: ${result.message}`);
314
+ process.exit(1);
315
+ }
316
+ }
317
+ async function cmdBook(args) {
318
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
319
+ const apiKey = getFlag(args, "--api-key", "-k");
320
+ const baseUrl = getFlag(args, "--base-url");
321
+ const email = getFlag(args, "--email", "-e") || "";
322
+ const phone = getFlag(args, "--phone") || "";
323
+ const passengerStrs = getAllFlags(args, "--passenger", "-p");
324
+ const offerId = args[0];
325
+ if (!offerId || !passengerStrs.length || !email) {
326
+ console.error(`Usage: boostedtravel book <offer_id> --passenger '{"id":"pas_xxx",...}' --email you@example.com`);
327
+ process.exit(1);
328
+ }
329
+ const passengers = passengerStrs.map((s) => JSON.parse(s));
330
+ const bt = new BoostedTravel({ apiKey, baseUrl });
331
+ const result = await bt.book(offerId, passengers, email, phone);
332
+ if (jsonOut) {
333
+ console.log(JSON.stringify(result, null, 2));
334
+ return;
335
+ }
336
+ if (result.status === "confirmed") {
337
+ console.log(`
338
+ \u2713 Booking confirmed!`);
339
+ console.log(` PNR: ${result.booking_reference}`);
340
+ console.log(` Flight: ${result.currency} ${result.flight_price.toFixed(2)}`);
341
+ console.log(` Fee: ${result.currency} ${result.service_fee.toFixed(2)} (${result.service_fee_percentage}%)`);
342
+ console.log(` Total: ${result.currency} ${result.total_charged.toFixed(2)}`);
343
+ console.log(` Order: ${result.order_id}
344
+ `);
345
+ } else {
346
+ console.error(` \u2717 Booking failed`);
347
+ console.error(JSON.stringify(result.details, null, 2));
348
+ process.exit(1);
349
+ }
350
+ }
351
+ async function cmdLocations(args) {
352
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
353
+ const apiKey = getFlag(args, "--api-key", "-k");
354
+ const baseUrl = getFlag(args, "--base-url");
355
+ const query = args[0];
356
+ if (!query) {
357
+ console.error("Usage: boostedtravel locations <city-or-airport-name>");
358
+ process.exit(1);
359
+ }
360
+ const bt = new BoostedTravel({ apiKey, baseUrl });
361
+ const result = await bt.resolveLocation(query);
362
+ if (jsonOut) {
363
+ console.log(JSON.stringify(result, null, 2));
364
+ return;
365
+ }
366
+ if (!result.length) {
367
+ console.log(`No locations found for '${query}'`);
368
+ return;
369
+ }
370
+ for (const loc of result) {
371
+ const iata = (loc.iata_code || "???").padEnd(5);
372
+ const name = loc.name || "";
373
+ const type = loc.type || "";
374
+ const city = loc.city_name || "";
375
+ const country = loc.country || "";
376
+ console.log(` ${iata} ${name} (${type}) \u2014 ${city}, ${country}`);
377
+ }
378
+ }
379
+ async function cmdRegister(args) {
380
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
381
+ const baseUrl = getFlag(args, "--base-url");
382
+ const name = getFlag(args, "--name", "-n");
383
+ const email = getFlag(args, "--email", "-e");
384
+ const owner = getFlag(args, "--owner") || "";
385
+ const desc = getFlag(args, "--desc") || "";
386
+ if (!name || !email) {
387
+ console.error("Usage: boostedtravel register --name my-agent --email agent@example.com");
388
+ process.exit(1);
389
+ }
390
+ const result = await BoostedTravel.register(name, email, baseUrl, owner, desc);
391
+ if (jsonOut) {
392
+ console.log(JSON.stringify(result, null, 2));
393
+ return;
394
+ }
395
+ console.log(`
396
+ \u2713 Agent registered!`);
397
+ console.log(` Agent ID: ${result.agent_id}`);
398
+ console.log(` API Key: ${result.api_key}`);
399
+ console.log(`
400
+ Save your API key:`);
401
+ console.log(` export BOOSTEDTRAVEL_API_KEY=${result.api_key}`);
402
+ console.log(`
403
+ Next: boostedtravel setup-payment --token tok_visa
404
+ `);
405
+ }
406
+ async function cmdSetupPayment(args) {
407
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
408
+ const apiKey = getFlag(args, "--api-key", "-k");
409
+ const baseUrl = getFlag(args, "--base-url");
410
+ const token = getFlag(args, "--token", "-t") || "tok_visa";
411
+ const bt = new BoostedTravel({ apiKey, baseUrl });
412
+ const result = await bt.setupPayment(token);
413
+ if (jsonOut) {
414
+ console.log(JSON.stringify(result, null, 2));
415
+ return;
416
+ }
417
+ if (result.status === "ready") {
418
+ console.log(`
419
+ \u2713 Payment ready! You can now unlock offers and book flights.
420
+ `);
421
+ } else {
422
+ console.error(` \u2717 Payment setup failed: ${result.message || result.status}`);
423
+ process.exit(1);
424
+ }
425
+ }
426
+ async function cmdMe(args) {
427
+ const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
428
+ const apiKey = getFlag(args, "--api-key", "-k");
429
+ const baseUrl = getFlag(args, "--base-url");
430
+ const bt = new BoostedTravel({ apiKey, baseUrl });
431
+ const profile = await bt.me();
432
+ if (jsonOut) {
433
+ console.log(JSON.stringify(profile, null, 2));
434
+ return;
435
+ }
436
+ const p = profile;
437
+ const u = p.usage || {};
438
+ console.log(`
439
+ Agent: ${p.agent_name} (${p.agent_id})`);
440
+ console.log(` Email: ${p.email}`);
441
+ console.log(` Tier: ${p.tier}`);
442
+ console.log(` Payment: ${p.payment_ready ? "\u2713 Ready" : "\u2717 Not set up"}`);
443
+ console.log(` Searches: ${u.total_searches || 0}`);
444
+ console.log(` Unlocks: ${u.total_unlocks || 0}`);
445
+ console.log(` Bookings: ${u.total_bookings || 0}`);
446
+ console.log(` Total spent: $${((u.total_spent_cents || 0) / 100).toFixed(2)}
447
+ `);
448
+ }
449
+ var HELP = `
450
+ BoostedTravel \u2014 Agent-native flight search & booking.
451
+
452
+ Search 300+ airlines at prices $10-30 cheaper than OTAs.
453
+ Search is FREE. Unlock: $1. Book: 2.5% fee.
454
+
455
+ Commands:
456
+ search <origin> <dest> <date> Search for flights (FREE)
457
+ locations <query> Resolve city name to IATA codes
458
+ unlock <offer_id> Unlock offer ($1)
459
+ book <offer_id> --passenger ... Book flight (2.5% fee)
460
+ register --name ... --email ... Register new agent
461
+ setup-payment Set up payment card
462
+ me Show agent profile
463
+
464
+ Options:
465
+ --json, -j Output raw JSON
466
+ --api-key, -k API key (or set BOOSTEDTRAVEL_API_KEY)
467
+ --base-url API URL (default: https://api.boostedchat.com)
468
+
469
+ Examples:
470
+ boostedtravel search GDN BER 2026-03-03 --sort price
471
+ boostedtravel search LON BCN 2026-04-01 --return 2026-04-08 --json
472
+ boostedtravel unlock off_xxx
473
+ boostedtravel book off_xxx -p '{"id":"pas_xxx","given_name":"John","family_name":"Doe","born_on":"1990-01-15"}' -e john@ex.com
474
+ `;
475
+ async function main() {
476
+ const args = process.argv.slice(2);
477
+ const command = args.shift();
478
+ try {
479
+ switch (command) {
480
+ case "search":
481
+ await cmdSearch(args);
482
+ break;
483
+ case "unlock":
484
+ await cmdUnlock(args);
485
+ break;
486
+ case "book":
487
+ await cmdBook(args);
488
+ break;
489
+ case "locations":
490
+ await cmdLocations(args);
491
+ break;
492
+ case "register":
493
+ await cmdRegister(args);
494
+ break;
495
+ case "setup-payment":
496
+ await cmdSetupPayment(args);
497
+ break;
498
+ case "me":
499
+ await cmdMe(args);
500
+ break;
501
+ case "--help":
502
+ case "-h":
503
+ case "help":
504
+ case void 0:
505
+ console.log(HELP);
506
+ break;
507
+ default:
508
+ console.error(`Unknown command: ${command}`);
509
+ console.log(HELP);
510
+ process.exit(1);
511
+ }
512
+ } catch (e) {
513
+ if (e instanceof BoostedTravelError) {
514
+ console.error(`Error: ${e.message}`);
515
+ process.exit(1);
516
+ }
517
+ throw e;
518
+ }
519
+ }
520
+ main();