@nitrostack/cli 1.0.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/README.md +131 -0
- package/dist/commands/build.d.ts +6 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +185 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +365 -0
- package/dist/commands/generate-types.d.ts +8 -0
- package/dist/commands/generate-types.d.ts.map +1 -0
- package/dist/commands/generate-types.js +219 -0
- package/dist/commands/generate.d.ts +12 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +375 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +324 -0
- package/dist/commands/install.d.ts +10 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +80 -0
- package/dist/commands/start.d.ts +6 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +70 -0
- package/dist/commands/upgrade.d.ts +10 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +214 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/mcp-dev-wrapper.d.ts +15 -0
- package/dist/mcp-dev-wrapper.d.ts.map +1 -0
- package/dist/mcp-dev-wrapper.js +187 -0
- package/dist/ui/branding.d.ts +31 -0
- package/dist/ui/branding.d.ts.map +1 -0
- package/dist/ui/branding.js +136 -0
- package/package.json +69 -0
- package/templates/typescript-oauth/.env.example +27 -0
- package/templates/typescript-oauth/OAUTH_SETUP.md +592 -0
- package/templates/typescript-oauth/README.md +263 -0
- package/templates/typescript-oauth/package.json +29 -0
- package/templates/typescript-oauth/src/app.module.ts +92 -0
- package/templates/typescript-oauth/src/guards/oauth.guard.ts +126 -0
- package/templates/typescript-oauth/src/health/system.health.ts +55 -0
- package/templates/typescript-oauth/src/index.ts +63 -0
- package/templates/typescript-oauth/src/modules/flights/booking.tools.ts +323 -0
- package/templates/typescript-oauth/src/modules/flights/flights.module.ts +14 -0
- package/templates/typescript-oauth/src/modules/flights/flights.prompts.ts +228 -0
- package/templates/typescript-oauth/src/modules/flights/flights.resources.ts +215 -0
- package/templates/typescript-oauth/src/modules/flights/flights.tools.ts +457 -0
- package/templates/typescript-oauth/src/services/duffel.service.ts +285 -0
- package/templates/typescript-oauth/src/widgets/app/airport-search/page.tsx +270 -0
- package/templates/typescript-oauth/src/widgets/app/flight-details/page.tsx +261 -0
- package/templates/typescript-oauth/src/widgets/app/flight-search-results/page.tsx +378 -0
- package/templates/typescript-oauth/src/widgets/app/globals.css +167 -0
- package/templates/typescript-oauth/src/widgets/app/layout.tsx +18 -0
- package/templates/typescript-oauth/src/widgets/app/order-cancellation/page.tsx +207 -0
- package/templates/typescript-oauth/src/widgets/app/order-summary/page.tsx +245 -0
- package/templates/typescript-oauth/src/widgets/app/payment-confirmation/page.tsx +152 -0
- package/templates/typescript-oauth/src/widgets/app/seat-selection/page.tsx +486 -0
- package/templates/typescript-oauth/src/widgets/next-env.d.ts +5 -0
- package/templates/typescript-oauth/src/widgets/next.config.js +45 -0
- package/templates/typescript-oauth/src/widgets/package-lock.json +4493 -0
- package/templates/typescript-oauth/src/widgets/package.json +24 -0
- package/templates/typescript-oauth/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-oauth/src/widgets/widget-manifest.json +395 -0
- package/templates/typescript-oauth/tsconfig.json +23 -0
- package/templates/typescript-pizzaz/README.md +252 -0
- package/templates/typescript-pizzaz/package.json +34 -0
- package/templates/typescript-pizzaz/src/app.module.ts +28 -0
- package/templates/typescript-pizzaz/src/index.ts +30 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.data.ts +106 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.module.ts +11 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.service.ts +60 -0
- package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.tools.ts +197 -0
- package/templates/typescript-pizzaz/src/widgets/app/layout.tsx +18 -0
- package/templates/typescript-pizzaz/src/widgets/app/pizza-list/page.tsx +272 -0
- package/templates/typescript-pizzaz/src/widgets/app/pizza-map/page.tsx +216 -0
- package/templates/typescript-pizzaz/src/widgets/app/pizza-shop/page.tsx +374 -0
- package/templates/typescript-pizzaz/src/widgets/components/CompactShopCard.tsx +144 -0
- package/templates/typescript-pizzaz/src/widgets/components/PizzaCard.tsx +191 -0
- package/templates/typescript-pizzaz/src/widgets/next.config.js +45 -0
- package/templates/typescript-pizzaz/src/widgets/package.json +30 -0
- package/templates/typescript-pizzaz/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-pizzaz/src/widgets/widget-manifest.json +253 -0
- package/templates/typescript-pizzaz/tsconfig.json +30 -0
- package/templates/typescript-starter/README.md +320 -0
- package/templates/typescript-starter/package.json +25 -0
- package/templates/typescript-starter/src/app.module.ts +34 -0
- package/templates/typescript-starter/src/health/system.health.ts +55 -0
- package/templates/typescript-starter/src/index.ts +29 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.module.ts +12 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.prompts.ts +73 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.resources.ts +59 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +166 -0
- package/templates/typescript-starter/src/widgets/app/calculator-result/page.tsx +180 -0
- package/templates/typescript-starter/src/widgets/app/layout.tsx +18 -0
- package/templates/typescript-starter/src/widgets/next.config.js +45 -0
- package/templates/typescript-starter/src/widgets/package.json +24 -0
- package/templates/typescript-starter/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-starter/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-starter/tsconfig.json +23 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { ToolDecorator as Tool, Widget, ExecutionContext, z, UseGuards, Injectable } from 'nitrostack';
|
|
2
|
+
import { OAuthGuard } from '../../guards/oauth.guard.js';
|
|
3
|
+
import { DuffelService } from '../../services/duffel.service.js';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class BookingTools {
|
|
7
|
+
constructor(private duffelService: DuffelService) { }
|
|
8
|
+
|
|
9
|
+
@Tool({
|
|
10
|
+
name: 'create_order',
|
|
11
|
+
description: 'Create a flight order with hold (no payment required). IMPORTANT: Before calling this tool, you MUST collect passenger information from the user. Ask for: full name (first and last), title (Mr/Ms/Mrs/Miss/Dr), gender (M/F), date of birth (YYYY-MM-DD), email, and phone number with country code. The order will be held for later payment.',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
offerId: z.string().describe('The offer ID to book'),
|
|
14
|
+
passengers: z.string().describe('JSON string containing array of passenger objects. Each passenger must have: title (mr/ms/mrs/miss/dr), givenName (first name), familyName (last name), gender (M/F), bornOn (YYYY-MM-DD), email, phoneNumber. Example: \'[{"title":"mr","givenName":"John","familyName":"Doe","gender":"M","bornOn":"1990-01-15","email":"john@example.com","phoneNumber":"+1234567890"}]\'')
|
|
15
|
+
}),
|
|
16
|
+
examples: {
|
|
17
|
+
request: {
|
|
18
|
+
offerId: 'off_123456',
|
|
19
|
+
passengers: '[{"title":"mr","givenName":"John","familyName":"Doe","gender":"M","bornOn":"1990-01-15","email":"john.doe@example.com","phoneNumber":"+1234567890"}]'
|
|
20
|
+
},
|
|
21
|
+
response: {
|
|
22
|
+
orderId: 'ord_123456',
|
|
23
|
+
status: 'held',
|
|
24
|
+
totalAmount: '450.00',
|
|
25
|
+
totalCurrency: 'USD',
|
|
26
|
+
expiresAt: '2024-03-01T12:00:00Z',
|
|
27
|
+
passengers: [],
|
|
28
|
+
slices: [],
|
|
29
|
+
message: 'Order created and held successfully.'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
@UseGuards(OAuthGuard)
|
|
34
|
+
@Widget('order-summary')
|
|
35
|
+
async createOrder(input: any, ctx: ExecutionContext) {
|
|
36
|
+
ctx.logger.info('Creating flight order (hold)', {
|
|
37
|
+
user: ctx.auth?.subject,
|
|
38
|
+
offerId: input.offerId
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Validate and parse passengers
|
|
42
|
+
let passengersArray;
|
|
43
|
+
try {
|
|
44
|
+
if (typeof input.passengers === 'string') {
|
|
45
|
+
// Try to parse the JSON string
|
|
46
|
+
// Handle both regular JSON and double-encoded JSON
|
|
47
|
+
let passengerStr = input.passengers;
|
|
48
|
+
|
|
49
|
+
// If the string starts with escaped quotes, it might be double-encoded
|
|
50
|
+
if (passengerStr.startsWith('\\"') || passengerStr.includes('\\"')) {
|
|
51
|
+
// Remove escape characters
|
|
52
|
+
passengerStr = passengerStr.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
passengersArray = JSON.parse(passengerStr);
|
|
56
|
+
} else if (Array.isArray(input.passengers)) {
|
|
57
|
+
passengersArray = input.passengers;
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error('Passengers must be a JSON string or array');
|
|
60
|
+
}
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
ctx.logger.error('Failed to parse passengers', {
|
|
63
|
+
input: input.passengers,
|
|
64
|
+
error: error.message
|
|
65
|
+
});
|
|
66
|
+
throw new Error(`Invalid passengers format: ${error.message}. Expected JSON string like '[{"title":"mr","givenName":"John","familyName":"Doe","gender":"M","bornOn":"1990-01-15","email":"john@example.com","phoneNumber":"+1234567890"}]'`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!passengersArray || !Array.isArray(passengersArray) || passengersArray.length === 0) {
|
|
70
|
+
throw new Error('At least one passenger is required to create an order');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Transform passengers to Duffel format
|
|
74
|
+
// Pass inline passenger data - Duffel will create passenger records automatically
|
|
75
|
+
const passengers = passengersArray.map((pax: any) => ({
|
|
76
|
+
title: pax.title,
|
|
77
|
+
given_name: pax.givenName,
|
|
78
|
+
family_name: pax.familyName,
|
|
79
|
+
gender: pax.gender,
|
|
80
|
+
born_on: pax.bornOn,
|
|
81
|
+
email: pax.email,
|
|
82
|
+
phone_number: pax.phoneNumber
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
const orderParams: any = {
|
|
86
|
+
selectedOffers: [input.offerId],
|
|
87
|
+
passengers,
|
|
88
|
+
type: 'hold' // Always create hold orders
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const order = await this.duffelService.createOrder(orderParams);
|
|
92
|
+
|
|
93
|
+
ctx.logger.info('Order created successfully', {
|
|
94
|
+
user: ctx.auth?.subject,
|
|
95
|
+
orderId: order.id,
|
|
96
|
+
status: 'held'
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
orderId: order.id,
|
|
101
|
+
status: 'held',
|
|
102
|
+
totalAmount: order.total_amount,
|
|
103
|
+
totalCurrency: order.total_currency,
|
|
104
|
+
expiresAt: (order as any).expires_at,
|
|
105
|
+
bookingReference: order.booking_reference,
|
|
106
|
+
passengers: order.passengers.map((pax: any) => ({
|
|
107
|
+
id: pax.id,
|
|
108
|
+
name: `${pax.given_name} ${pax.family_name}`,
|
|
109
|
+
type: pax.type
|
|
110
|
+
})),
|
|
111
|
+
slices: order.slices.map((slice: any) => ({
|
|
112
|
+
origin: slice.origin.iata_code,
|
|
113
|
+
destination: slice.destination.iata_code,
|
|
114
|
+
departureTime: slice.segments[0].departing_at,
|
|
115
|
+
arrivalTime: slice.segments[slice.segments.length - 1].arriving_at
|
|
116
|
+
})),
|
|
117
|
+
message: 'Order created and held successfully.'
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@Tool({
|
|
124
|
+
name: 'get_order_details',
|
|
125
|
+
description: 'Get detailed information about an order',
|
|
126
|
+
inputSchema: z.object({
|
|
127
|
+
orderId: z.string().describe('The order ID')
|
|
128
|
+
}),
|
|
129
|
+
examples: {
|
|
130
|
+
request: {
|
|
131
|
+
orderId: 'ord_123456'
|
|
132
|
+
},
|
|
133
|
+
response: {
|
|
134
|
+
orderId: 'ord_123456',
|
|
135
|
+
status: 'confirmed',
|
|
136
|
+
bookingReference: 'ABC123',
|
|
137
|
+
totalAmount: '450.00',
|
|
138
|
+
totalCurrency: 'USD',
|
|
139
|
+
passengers: [],
|
|
140
|
+
slices: []
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
@UseGuards(OAuthGuard)
|
|
145
|
+
@Widget('order-summary')
|
|
146
|
+
async getOrderDetails(input: any, ctx: ExecutionContext) {
|
|
147
|
+
ctx.logger.info('Getting order details', {
|
|
148
|
+
user: ctx.auth?.subject,
|
|
149
|
+
orderId: input.orderId
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const order = await this.duffelService.getOrder(input.orderId);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
orderId: order.id,
|
|
156
|
+
status: (order as any).status || 'confirmed',
|
|
157
|
+
bookingReference: order.booking_reference,
|
|
158
|
+
totalAmount: order.total_amount,
|
|
159
|
+
totalCurrency: order.total_currency,
|
|
160
|
+
createdAt: order.created_at,
|
|
161
|
+
expiresAt: (order as any).expires_at,
|
|
162
|
+
passengers: order.passengers.map((pax: any) => ({
|
|
163
|
+
id: pax.id,
|
|
164
|
+
name: `${pax.given_name} ${pax.family_name}`,
|
|
165
|
+
type: pax.type,
|
|
166
|
+
email: pax.email,
|
|
167
|
+
phoneNumber: pax.phone_number
|
|
168
|
+
})),
|
|
169
|
+
slices: order.slices.map((slice: any) => ({
|
|
170
|
+
id: slice.id,
|
|
171
|
+
origin: {
|
|
172
|
+
code: slice.origin.iata_code,
|
|
173
|
+
name: slice.origin.name,
|
|
174
|
+
city: slice.origin.city_name
|
|
175
|
+
},
|
|
176
|
+
destination: {
|
|
177
|
+
code: slice.destination.iata_code,
|
|
178
|
+
name: slice.destination.name,
|
|
179
|
+
city: slice.destination.city_name
|
|
180
|
+
},
|
|
181
|
+
duration: slice.duration,
|
|
182
|
+
segments: slice.segments.map((seg: any) => ({
|
|
183
|
+
id: seg.id,
|
|
184
|
+
origin: seg.origin.iata_code,
|
|
185
|
+
destination: seg.destination.iata_code,
|
|
186
|
+
departingAt: seg.departing_at,
|
|
187
|
+
arrivingAt: seg.arriving_at,
|
|
188
|
+
airline: seg.marketing_carrier.name,
|
|
189
|
+
flightNumber: seg.marketing_carrier_flight_number,
|
|
190
|
+
aircraft: seg.aircraft?.name
|
|
191
|
+
}))
|
|
192
|
+
}))
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Tool({
|
|
197
|
+
name: 'get_seat_map',
|
|
198
|
+
description: 'Get available seats for a flight offer to allow seat selection',
|
|
199
|
+
inputSchema: z.object({
|
|
200
|
+
offerId: z.string().describe('The offer ID to get seats for')
|
|
201
|
+
}),
|
|
202
|
+
examples: {
|
|
203
|
+
request: {
|
|
204
|
+
offerId: 'off_123456'
|
|
205
|
+
},
|
|
206
|
+
response: {
|
|
207
|
+
offerId: 'off_123456',
|
|
208
|
+
cabins: [
|
|
209
|
+
{
|
|
210
|
+
cabinClass: 'economy',
|
|
211
|
+
rows: [
|
|
212
|
+
{
|
|
213
|
+
rowNumber: 10,
|
|
214
|
+
seats: [
|
|
215
|
+
{
|
|
216
|
+
id: 'seat_10a',
|
|
217
|
+
column: 'A',
|
|
218
|
+
available: true,
|
|
219
|
+
price: '25.00',
|
|
220
|
+
currency: 'USD',
|
|
221
|
+
type: 'window'
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'seat_10b',
|
|
225
|
+
column: 'B',
|
|
226
|
+
available: true,
|
|
227
|
+
price: '0',
|
|
228
|
+
currency: 'USD',
|
|
229
|
+
type: 'middle'
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'seat_10c',
|
|
233
|
+
column: 'C',
|
|
234
|
+
available: true,
|
|
235
|
+
price: '15.00',
|
|
236
|
+
currency: 'USD',
|
|
237
|
+
type: 'aisle'
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
],
|
|
244
|
+
message: 'Select your preferred seats from the available options'
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
@UseGuards(OAuthGuard)
|
|
249
|
+
@Widget('seat-selection')
|
|
250
|
+
async getSeatMap(input: any, ctx: ExecutionContext) {
|
|
251
|
+
ctx.logger.info('Getting seat map', {
|
|
252
|
+
user: ctx.auth?.subject,
|
|
253
|
+
offerId: input.offerId
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const seatMaps = await this.duffelService.getSeatsForOffer(input.offerId);
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
offerId: input.offerId,
|
|
260
|
+
cabins: seatMaps.map((cabin: any) => ({
|
|
261
|
+
cabinClass: cabin.cabin_class,
|
|
262
|
+
rows: cabin.rows.map((row: any) => ({
|
|
263
|
+
rowNumber: row.row_number,
|
|
264
|
+
seats: row.sections.flatMap((section: any) =>
|
|
265
|
+
section.elements.filter((el: any) => el.type === 'seat').map((seat: any) => ({
|
|
266
|
+
id: seat.id,
|
|
267
|
+
column: seat.designator,
|
|
268
|
+
available: seat.available_services?.length > 0,
|
|
269
|
+
price: seat.available_services?.[0]?.total_amount,
|
|
270
|
+
currency: seat.available_services?.[0]?.total_currency,
|
|
271
|
+
type: seat.disclosures?.join(', ') || 'standard'
|
|
272
|
+
}))
|
|
273
|
+
)
|
|
274
|
+
}))
|
|
275
|
+
})),
|
|
276
|
+
message: 'Select your preferred seats from the available options'
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@Tool({
|
|
281
|
+
name: 'cancel_order',
|
|
282
|
+
description: 'Cancel a flight order and request refund if applicable',
|
|
283
|
+
inputSchema: z.object({
|
|
284
|
+
orderId: z.string().describe('The order ID to cancel')
|
|
285
|
+
}),
|
|
286
|
+
examples: {
|
|
287
|
+
request: {
|
|
288
|
+
orderId: 'ord_123456'
|
|
289
|
+
},
|
|
290
|
+
response: {
|
|
291
|
+
orderId: 'ord_123456',
|
|
292
|
+
cancellationId: 'ocr_123456',
|
|
293
|
+
status: 'cancelled',
|
|
294
|
+
refundAmount: '450.00',
|
|
295
|
+
refundCurrency: 'USD',
|
|
296
|
+
confirmedAt: '2024-03-01T12:00:00Z',
|
|
297
|
+
message: 'Order cancelled. Refund of USD 450.00 will be processed.'
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
@UseGuards(OAuthGuard)
|
|
302
|
+
@Widget('order-cancellation')
|
|
303
|
+
async cancelOrder(input: any, ctx: ExecutionContext) {
|
|
304
|
+
ctx.logger.info('Cancelling order', {
|
|
305
|
+
user: ctx.auth?.subject,
|
|
306
|
+
orderId: input.orderId
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const cancellation = await this.duffelService.cancelOrder(input.orderId);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
orderId: input.orderId,
|
|
313
|
+
cancellationId: cancellation.id,
|
|
314
|
+
status: 'cancelled',
|
|
315
|
+
refundAmount: cancellation.refund_amount,
|
|
316
|
+
refundCurrency: cancellation.refund_currency,
|
|
317
|
+
confirmedAt: cancellation.confirmed_at,
|
|
318
|
+
message: cancellation.refund_amount
|
|
319
|
+
? `Order cancelled. Refund of ${cancellation.refund_currency} ${cancellation.refund_amount} will be processed.`
|
|
320
|
+
: 'Order cancelled. No refund available for this booking.'
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Module } from 'nitrostack';
|
|
2
|
+
import { FlightTools } from './flights.tools.js';
|
|
3
|
+
import { BookingTools } from './booking.tools.js';
|
|
4
|
+
import { FlightPrompts } from './flights.prompts.js';
|
|
5
|
+
import { FlightResources } from './flights.resources.js';
|
|
6
|
+
import { DuffelService } from '../../services/duffel.service.js';
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
name: 'flights',
|
|
10
|
+
description: 'Professional flight search and booking system powered by Duffel API',
|
|
11
|
+
controllers: [FlightTools, BookingTools, FlightPrompts, FlightResources],
|
|
12
|
+
providers: [DuffelService]
|
|
13
|
+
})
|
|
14
|
+
export class FlightsModule { }
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { PromptDecorator as Prompt, ExecutionContext, Injectable } from 'nitrostack';
|
|
2
|
+
import { DuffelService } from '../../services/duffel.service.js';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class FlightPrompts {
|
|
6
|
+
constructor(private duffelService: DuffelService) { }
|
|
7
|
+
|
|
8
|
+
@Prompt({
|
|
9
|
+
name: 'flight_search_assistant',
|
|
10
|
+
description: 'An AI assistant specialized in helping users search for flights, understand flight options, and make booking decisions.',
|
|
11
|
+
arguments: [
|
|
12
|
+
{
|
|
13
|
+
name: 'userQuery',
|
|
14
|
+
description: 'The user\'s flight search query or question',
|
|
15
|
+
required: true
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'context',
|
|
19
|
+
description: 'Optional context including previous searches and selected offers',
|
|
20
|
+
required: false
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
})
|
|
24
|
+
async flightSearchAssistant(input: any, ctx: ExecutionContext) {
|
|
25
|
+
const systemPrompt = `You are a professional flight booking assistant with expertise in helping travelers find flight information.
|
|
26
|
+
|
|
27
|
+
⚠️ CRITICAL: Only do what the user specifically asks. Do NOT assume additional steps.
|
|
28
|
+
|
|
29
|
+
Your capabilities:
|
|
30
|
+
- Search for airports using search_airports tool
|
|
31
|
+
- Search for flights using the search_flights tool
|
|
32
|
+
- Get detailed flight information using get_flight_details tool
|
|
33
|
+
- Help book flights when explicitly requested
|
|
34
|
+
|
|
35
|
+
**IMPORTANT RULES:**
|
|
36
|
+
1. If user asks about airports, ONLY search airports - do NOT search for flights
|
|
37
|
+
2. If user asks about flights, ONLY search flights - do NOT automatically book
|
|
38
|
+
3. If user asks to book, ONLY then proceed with booking workflow
|
|
39
|
+
4. NEVER chain operations unless user explicitly requests it
|
|
40
|
+
|
|
41
|
+
**EXAMPLES:**
|
|
42
|
+
- "show me airports in London" → search_airports("London") → show results → STOP
|
|
43
|
+
- "find flights from NYC to LAX" → search_flights → show results → STOP
|
|
44
|
+
- "book this flight" → THEN start booking workflow
|
|
45
|
+
|
|
46
|
+
BOOKING WORKFLOW (only when user explicitly wants to book):
|
|
47
|
+
1. FIRST, collect ALL passenger information (name, title, gender, date of birth, email, phone)
|
|
48
|
+
2. THEN, call create_order tool with complete passenger details
|
|
49
|
+
⚠️ NEVER call create_order without collecting passenger information first!
|
|
50
|
+
⚠️ All bookings are automatically held - no payment is required at booking time
|
|
51
|
+
|
|
52
|
+
Current user query: ${input.userQuery}
|
|
53
|
+
|
|
54
|
+
${input.context?.previousSearches?.length ? `Previous searches in this conversation:\n${JSON.stringify(input.context.previousSearches, null, 2)}` : ''}
|
|
55
|
+
|
|
56
|
+
Respond to EXACTLY what the user asked - nothing more.`;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
role: 'assistant',
|
|
60
|
+
content: systemPrompt
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Prompt({
|
|
65
|
+
name: 'flight_comparison',
|
|
66
|
+
description: 'Compare multiple flight offers and provide recommendations based on various factors.',
|
|
67
|
+
arguments: [
|
|
68
|
+
{
|
|
69
|
+
name: 'offerIds',
|
|
70
|
+
description: 'Flight offer IDs to compare (2-5 offers)',
|
|
71
|
+
required: true
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'priorities',
|
|
75
|
+
description: 'User priorities for comparison (price, duration, stops, airline, departure_time, flexibility)',
|
|
76
|
+
required: false
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
})
|
|
80
|
+
async flightComparison(input: any, ctx: ExecutionContext) {
|
|
81
|
+
const offers = await Promise.all(
|
|
82
|
+
input.offerIds.map((id: string) => this.duffelService.getOffer(id))
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const comparisonData = offers.map((offer: any) => {
|
|
86
|
+
const outbound = offer.slices[0];
|
|
87
|
+
return {
|
|
88
|
+
offerId: offer.id,
|
|
89
|
+
price: `${offer.total_amount} ${offer.total_currency}`,
|
|
90
|
+
airline: outbound.segments[0].marketing_carrier.name,
|
|
91
|
+
duration: outbound.duration,
|
|
92
|
+
stops: outbound.segments.length - 1,
|
|
93
|
+
departureTime: outbound.segments[0].departing_at,
|
|
94
|
+
arrivalTime: outbound.segments[outbound.segments.length - 1].arriving_at,
|
|
95
|
+
refundable: offer.conditions?.refund_before_departure?.allowed || false,
|
|
96
|
+
changeable: offer.conditions?.change_before_departure?.allowed || false
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const priorities = input.priorities || ['price', 'duration', 'stops'];
|
|
101
|
+
|
|
102
|
+
const prompt = `Compare these flight options and provide a recommendation:
|
|
103
|
+
|
|
104
|
+
${JSON.stringify(comparisonData, null, 2)}
|
|
105
|
+
|
|
106
|
+
User priorities: ${priorities.join(', ')}
|
|
107
|
+
|
|
108
|
+
Provide:
|
|
109
|
+
1. A clear comparison of the key differences
|
|
110
|
+
2. Pros and cons of each option
|
|
111
|
+
3. Your recommendation based on the user's priorities
|
|
112
|
+
4. Any important considerations (layover times, airline reputation, flexibility, etc.)`;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
role: 'assistant',
|
|
116
|
+
content: prompt
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Prompt({
|
|
121
|
+
name: 'travel_tips',
|
|
122
|
+
description: 'Provide travel tips and advice for a specific route and travel dates.',
|
|
123
|
+
arguments: [
|
|
124
|
+
{
|
|
125
|
+
name: 'origin',
|
|
126
|
+
description: 'Origin airport code',
|
|
127
|
+
required: true
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'destination',
|
|
131
|
+
description: 'Destination airport code',
|
|
132
|
+
required: true
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'departureDate',
|
|
136
|
+
description: 'Departure date',
|
|
137
|
+
required: true
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'tripType',
|
|
141
|
+
description: 'Type of trip: business, leisure, or family',
|
|
142
|
+
required: false
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
})
|
|
146
|
+
async travelTips(input: any, ctx: ExecutionContext) {
|
|
147
|
+
const prompt = `Provide helpful travel tips for a trip from ${input.origin} to ${input.destination} departing on ${input.departureDate}.
|
|
148
|
+
|
|
149
|
+
Include advice on:
|
|
150
|
+
1. Best time to book for this route
|
|
151
|
+
2. Typical weather at destination during this time
|
|
152
|
+
3. Airport tips (check-in, security, lounges)
|
|
153
|
+
4. Baggage recommendations
|
|
154
|
+
5. Connection considerations if applicable
|
|
155
|
+
6. Time zone differences and jet lag tips
|
|
156
|
+
${input.tripType ? `7. Specific tips for ${input.tripType} travel` : ''}
|
|
157
|
+
|
|
158
|
+
Be concise but informative.`;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
role: 'assistant',
|
|
162
|
+
content: prompt
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@Prompt({
|
|
167
|
+
name: 'booking_assistant',
|
|
168
|
+
description: 'Guide users through the flight booking process, collecting all necessary passenger information before creating an order.',
|
|
169
|
+
arguments: [
|
|
170
|
+
{
|
|
171
|
+
name: 'offerId',
|
|
172
|
+
description: 'The flight offer ID the user wants to book',
|
|
173
|
+
required: true
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'passengerCount',
|
|
177
|
+
description: 'Number of passengers (default: 1)',
|
|
178
|
+
required: false
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
})
|
|
182
|
+
async bookingAssistant(input: any, ctx: ExecutionContext) {
|
|
183
|
+
const passengerCount = input.passengerCount || 1;
|
|
184
|
+
|
|
185
|
+
const prompt = `You are helping the user book flight offer: ${input.offerId}
|
|
186
|
+
|
|
187
|
+
IMPORTANT BOOKING WORKFLOW:
|
|
188
|
+
Before you can create the order, you MUST collect the following information for ${passengerCount} passenger(s):
|
|
189
|
+
|
|
190
|
+
For EACH passenger, ask for:
|
|
191
|
+
1. **Title**: Mr, Ms, Mrs, Miss, or Dr
|
|
192
|
+
2. **Full Name**: First name and last name (as it appears on their passport/ID)
|
|
193
|
+
3. **Gender**: Male (M) or Female (F)
|
|
194
|
+
4. **Date of Birth**: In YYYY-MM-DD format (e.g., 1990-01-15)
|
|
195
|
+
5. **Email Address**: For booking confirmation
|
|
196
|
+
6. **Phone Number**: With country code (e.g., +1234567890)
|
|
197
|
+
|
|
198
|
+
COLLECTION STRATEGY:
|
|
199
|
+
- Ask for all information in a friendly, conversational way
|
|
200
|
+
- You can ask for multiple fields at once to make it efficient
|
|
201
|
+
- Validate the format (especially date of birth and email)
|
|
202
|
+
- Confirm all details with the user before proceeding
|
|
203
|
+
|
|
204
|
+
EXAMPLE QUESTIONS:
|
|
205
|
+
"Great! To complete your booking, I'll need some passenger details. Could you please provide:
|
|
206
|
+
- Full name (first and last)
|
|
207
|
+
- Title (Mr/Ms/Mrs/Miss/Dr)
|
|
208
|
+
- Date of birth (YYYY-MM-DD)
|
|
209
|
+
- Gender (M/F)
|
|
210
|
+
- Email address
|
|
211
|
+
- Phone number with country code"
|
|
212
|
+
|
|
213
|
+
BOOKING PROCESS:
|
|
214
|
+
Once you have ALL passenger information:
|
|
215
|
+
- Call create_order with the offer ID and passenger details
|
|
216
|
+
- The booking will be automatically held (no payment required)
|
|
217
|
+
- The user will receive booking confirmation with expiration details
|
|
218
|
+
- Payment can be completed later before the hold expires
|
|
219
|
+
|
|
220
|
+
DO NOT ask for payment details - all bookings are held by default.
|
|
221
|
+
ALWAYS inform the user that their booking is held and they have time to complete payment.`;
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
role: 'assistant',
|
|
225
|
+
content: prompt
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|