@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.
Files changed (100) hide show
  1. package/README.md +131 -0
  2. package/dist/commands/build.d.ts +6 -0
  3. package/dist/commands/build.d.ts.map +1 -0
  4. package/dist/commands/build.js +185 -0
  5. package/dist/commands/dev.d.ts +7 -0
  6. package/dist/commands/dev.d.ts.map +1 -0
  7. package/dist/commands/dev.js +365 -0
  8. package/dist/commands/generate-types.d.ts +8 -0
  9. package/dist/commands/generate-types.d.ts.map +1 -0
  10. package/dist/commands/generate-types.js +219 -0
  11. package/dist/commands/generate.d.ts +12 -0
  12. package/dist/commands/generate.d.ts.map +1 -0
  13. package/dist/commands/generate.js +375 -0
  14. package/dist/commands/init.d.ts +7 -0
  15. package/dist/commands/init.d.ts.map +1 -0
  16. package/dist/commands/init.js +324 -0
  17. package/dist/commands/install.d.ts +10 -0
  18. package/dist/commands/install.d.ts.map +1 -0
  19. package/dist/commands/install.js +80 -0
  20. package/dist/commands/start.d.ts +6 -0
  21. package/dist/commands/start.d.ts.map +1 -0
  22. package/dist/commands/start.js +70 -0
  23. package/dist/commands/upgrade.d.ts +10 -0
  24. package/dist/commands/upgrade.d.ts.map +1 -0
  25. package/dist/commands/upgrade.js +214 -0
  26. package/dist/index.d.ts +11 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +94 -0
  29. package/dist/mcp-dev-wrapper.d.ts +15 -0
  30. package/dist/mcp-dev-wrapper.d.ts.map +1 -0
  31. package/dist/mcp-dev-wrapper.js +187 -0
  32. package/dist/ui/branding.d.ts +31 -0
  33. package/dist/ui/branding.d.ts.map +1 -0
  34. package/dist/ui/branding.js +136 -0
  35. package/package.json +69 -0
  36. package/templates/typescript-oauth/.env.example +27 -0
  37. package/templates/typescript-oauth/OAUTH_SETUP.md +592 -0
  38. package/templates/typescript-oauth/README.md +263 -0
  39. package/templates/typescript-oauth/package.json +29 -0
  40. package/templates/typescript-oauth/src/app.module.ts +92 -0
  41. package/templates/typescript-oauth/src/guards/oauth.guard.ts +126 -0
  42. package/templates/typescript-oauth/src/health/system.health.ts +55 -0
  43. package/templates/typescript-oauth/src/index.ts +63 -0
  44. package/templates/typescript-oauth/src/modules/flights/booking.tools.ts +323 -0
  45. package/templates/typescript-oauth/src/modules/flights/flights.module.ts +14 -0
  46. package/templates/typescript-oauth/src/modules/flights/flights.prompts.ts +228 -0
  47. package/templates/typescript-oauth/src/modules/flights/flights.resources.ts +215 -0
  48. package/templates/typescript-oauth/src/modules/flights/flights.tools.ts +457 -0
  49. package/templates/typescript-oauth/src/services/duffel.service.ts +285 -0
  50. package/templates/typescript-oauth/src/widgets/app/airport-search/page.tsx +270 -0
  51. package/templates/typescript-oauth/src/widgets/app/flight-details/page.tsx +261 -0
  52. package/templates/typescript-oauth/src/widgets/app/flight-search-results/page.tsx +378 -0
  53. package/templates/typescript-oauth/src/widgets/app/globals.css +167 -0
  54. package/templates/typescript-oauth/src/widgets/app/layout.tsx +18 -0
  55. package/templates/typescript-oauth/src/widgets/app/order-cancellation/page.tsx +207 -0
  56. package/templates/typescript-oauth/src/widgets/app/order-summary/page.tsx +245 -0
  57. package/templates/typescript-oauth/src/widgets/app/payment-confirmation/page.tsx +152 -0
  58. package/templates/typescript-oauth/src/widgets/app/seat-selection/page.tsx +486 -0
  59. package/templates/typescript-oauth/src/widgets/next-env.d.ts +5 -0
  60. package/templates/typescript-oauth/src/widgets/next.config.js +45 -0
  61. package/templates/typescript-oauth/src/widgets/package-lock.json +4493 -0
  62. package/templates/typescript-oauth/src/widgets/package.json +24 -0
  63. package/templates/typescript-oauth/src/widgets/tsconfig.json +28 -0
  64. package/templates/typescript-oauth/src/widgets/widget-manifest.json +395 -0
  65. package/templates/typescript-oauth/tsconfig.json +23 -0
  66. package/templates/typescript-pizzaz/README.md +252 -0
  67. package/templates/typescript-pizzaz/package.json +34 -0
  68. package/templates/typescript-pizzaz/src/app.module.ts +28 -0
  69. package/templates/typescript-pizzaz/src/index.ts +30 -0
  70. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.data.ts +106 -0
  71. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.module.ts +11 -0
  72. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.service.ts +60 -0
  73. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.tools.ts +197 -0
  74. package/templates/typescript-pizzaz/src/widgets/app/layout.tsx +18 -0
  75. package/templates/typescript-pizzaz/src/widgets/app/pizza-list/page.tsx +272 -0
  76. package/templates/typescript-pizzaz/src/widgets/app/pizza-map/page.tsx +216 -0
  77. package/templates/typescript-pizzaz/src/widgets/app/pizza-shop/page.tsx +374 -0
  78. package/templates/typescript-pizzaz/src/widgets/components/CompactShopCard.tsx +144 -0
  79. package/templates/typescript-pizzaz/src/widgets/components/PizzaCard.tsx +191 -0
  80. package/templates/typescript-pizzaz/src/widgets/next.config.js +45 -0
  81. package/templates/typescript-pizzaz/src/widgets/package.json +30 -0
  82. package/templates/typescript-pizzaz/src/widgets/tsconfig.json +28 -0
  83. package/templates/typescript-pizzaz/src/widgets/widget-manifest.json +253 -0
  84. package/templates/typescript-pizzaz/tsconfig.json +30 -0
  85. package/templates/typescript-starter/README.md +320 -0
  86. package/templates/typescript-starter/package.json +25 -0
  87. package/templates/typescript-starter/src/app.module.ts +34 -0
  88. package/templates/typescript-starter/src/health/system.health.ts +55 -0
  89. package/templates/typescript-starter/src/index.ts +29 -0
  90. package/templates/typescript-starter/src/modules/calculator/calculator.module.ts +12 -0
  91. package/templates/typescript-starter/src/modules/calculator/calculator.prompts.ts +73 -0
  92. package/templates/typescript-starter/src/modules/calculator/calculator.resources.ts +59 -0
  93. package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +166 -0
  94. package/templates/typescript-starter/src/widgets/app/calculator-result/page.tsx +180 -0
  95. package/templates/typescript-starter/src/widgets/app/layout.tsx +18 -0
  96. package/templates/typescript-starter/src/widgets/next.config.js +45 -0
  97. package/templates/typescript-starter/src/widgets/package.json +24 -0
  98. package/templates/typescript-starter/src/widgets/tsconfig.json +28 -0
  99. package/templates/typescript-starter/src/widgets/widget-manifest.json +48 -0
  100. 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
+ }