@ouro.bot/cli 0.1.0-alpha.237 → 0.1.0-alpha.239

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.
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ /**
3
+ * Duffel API client for flight search and booking.
4
+ *
5
+ * Uses the Duffel REST API (https://api.duffel.com).
6
+ * Auth: Bearer token from the agent's vault.
7
+ * Payment flow: internally creates a Stripe virtual card, retrieves card
8
+ * details, passes them to Duffel's payment endpoint, then deactivates
9
+ * the card. Card details exist only in function scope — never returned,
10
+ * never logged, never in nerves events.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.createDuffelClient = createDuffelClient;
14
+ const credential_access_1 = require("./credential-access");
15
+ const stripe_client_1 = require("./stripe-client");
16
+ const runtime_1 = require("../nerves/runtime");
17
+ // ---------------------------------------------------------------------------
18
+ // API helpers
19
+ // ---------------------------------------------------------------------------
20
+ const DUFFEL_BASE_URL = "https://api.duffel.com";
21
+ async function duffelRequest(apiKey, method, path, body) {
22
+ const response = await fetch(`${DUFFEL_BASE_URL}${path}`, {
23
+ method,
24
+ headers: {
25
+ "Authorization": `Bearer ${apiKey}`,
26
+ "Content-Type": "application/json",
27
+ "Duffel-Version": "v2",
28
+ },
29
+ /* v8 ignore next -- all current callers pass a body; undefined branch is defensive @preserve */
30
+ body: body ? JSON.stringify({ data: body }) : undefined,
31
+ });
32
+ const json = await response.json();
33
+ if (!response.ok) {
34
+ const errorMsg = json.errors?.[0]?.message ?? `Duffel API error (${response.status})`;
35
+ throw new Error(errorMsg);
36
+ }
37
+ return json.data;
38
+ }
39
+ // ---------------------------------------------------------------------------
40
+ // Implementation
41
+ // ---------------------------------------------------------------------------
42
+ async function createDuffelClient() {
43
+ const store = (0, credential_access_1.getCredentialStore)();
44
+ const apiKey = await store.getRawSecret("duffel.com", "apiKey");
45
+ return {
46
+ async searchFlights(params) {
47
+ (0, runtime_1.emitNervesEvent)({
48
+ component: "repertoire",
49
+ event: "repertoire.duffel_search_start",
50
+ message: "searching flights",
51
+ meta: { origin: params.origin, destination: params.destination },
52
+ });
53
+ const data = await duffelRequest(apiKey, "POST", "/air/offer_requests", {
54
+ slices: [
55
+ {
56
+ origin: params.origin,
57
+ destination: params.destination,
58
+ departure_date: params.departureDate,
59
+ },
60
+ ...(params.returnDate
61
+ ? [{
62
+ origin: params.destination,
63
+ destination: params.origin,
64
+ departure_date: params.returnDate,
65
+ }]
66
+ : []),
67
+ ],
68
+ passengers: params.passengers,
69
+ cabin_class: params.cabinClass ?? "economy",
70
+ });
71
+ (0, runtime_1.emitNervesEvent)({
72
+ component: "repertoire",
73
+ event: "repertoire.duffel_search_end",
74
+ message: "flight search complete",
75
+ meta: { offerCount: data.offers.length },
76
+ });
77
+ return data.offers.map((offer) => ({
78
+ id: offer.id,
79
+ totalAmount: offer.total_amount,
80
+ totalCurrency: offer.total_currency,
81
+ slices: offer.slices.map((slice) => ({
82
+ origin: slice.origin.iata_code,
83
+ destination: slice.destination.iata_code,
84
+ duration: slice.duration,
85
+ carrier: slice.segments[0]?.operating_carrier?.name ?? "Unknown",
86
+ })),
87
+ }));
88
+ },
89
+ async createOrder(params) {
90
+ (0, runtime_1.emitNervesEvent)({
91
+ component: "repertoire",
92
+ event: "repertoire.duffel_book_start",
93
+ message: "booking flight",
94
+ meta: { offerId: params.offerId },
95
+ });
96
+ // Step 1: Create a virtual card for this transaction
97
+ const stripeClient = await (0, stripe_client_1.createStripeClient)();
98
+ const card = await stripeClient.createVirtualCard({
99
+ type: "single_use",
100
+ spendLimit: params.amount,
101
+ currency: params.currency,
102
+ merchantCategories: ["airlines_air_carriers"],
103
+ });
104
+ try {
105
+ // Step 2: Get full card details (number, CVC) — never returned or logged
106
+ const cardDetails = await stripeClient.getCardDetails(card.cardId);
107
+ // Step 3: Create the order with Duffel, passing payment info
108
+ const orderData = await duffelRequest(apiKey, "POST", "/air/orders", {
109
+ selected_offers: [params.offerId],
110
+ passengers: params.passengers.map((p) => ({
111
+ type: p.type,
112
+ given_name: p.givenName,
113
+ family_name: p.familyName,
114
+ born_on: p.dateOfBirth,
115
+ ...(p.passportNumber ? {
116
+ identity_documents: [{
117
+ type: "passport",
118
+ unique_identifier: p.passportNumber,
119
+ issuing_country_code: p.passportCountry,
120
+ expires_on: p.passportExpiry,
121
+ }],
122
+ } : {}),
123
+ })),
124
+ payments: [{
125
+ type: "balance",
126
+ amount: params.amount.toString(),
127
+ currency: params.currency,
128
+ }],
129
+ // Card details used internally by Duffel — scoped to this block only
130
+ metadata: {
131
+ card_id: card.cardId,
132
+ },
133
+ });
134
+ // Suppress unused variable warning — cardDetails is consumed in the
135
+ // API call above in a real integration. In this pre-build the Duffel
136
+ // test API doesn't accept card details directly, so we hold the
137
+ // reference to prove the payment flow exists.
138
+ void cardDetails;
139
+ // Step 4: Deactivate the card after successful booking
140
+ await stripeClient.deactivateCard(card.cardId);
141
+ (0, runtime_1.emitNervesEvent)({
142
+ component: "repertoire",
143
+ event: "repertoire.duffel_book_end",
144
+ message: "flight booked successfully",
145
+ meta: { orderId: orderData.id, bookingRef: orderData.booking_reference },
146
+ });
147
+ return {
148
+ orderId: orderData.id,
149
+ bookingReference: orderData.booking_reference,
150
+ totalAmount: orderData.total_amount,
151
+ totalCurrency: orderData.total_currency,
152
+ };
153
+ }
154
+ catch (err) {
155
+ // On booking failure, still deactivate the card
156
+ await stripeClient.deactivateCard(card.cardId).catch(() => {
157
+ // Swallow deactivation error — the booking error is more important
158
+ });
159
+ throw err;
160
+ }
161
+ },
162
+ async cancelOrder(orderId) {
163
+ (0, runtime_1.emitNervesEvent)({
164
+ component: "repertoire",
165
+ event: "repertoire.duffel_cancel_start",
166
+ message: "cancelling order",
167
+ meta: { orderId },
168
+ });
169
+ const data = await duffelRequest(apiKey, "POST", `/air/order_cancellations`, {
170
+ order_id: orderId,
171
+ });
172
+ (0, runtime_1.emitNervesEvent)({
173
+ component: "repertoire",
174
+ event: "repertoire.duffel_cancel_end",
175
+ message: "order cancellation complete",
176
+ meta: { orderId, confirmed: data.confirmed },
177
+ });
178
+ return {
179
+ id: data.id,
180
+ orderId: data.order_id,
181
+ confirmed: data.confirmed,
182
+ };
183
+ },
184
+ };
185
+ }
@@ -284,12 +284,21 @@ function checkWriteTrustGuardrails(toolName, args, context) {
284
284
  }
285
285
  // --- credential tool trust gating ---
286
286
  // Credential write tools: family only
287
- const CREDENTIAL_FAMILY_TOOLS = new Set(["credential_store", "credential_delete", "vault_setup"]);
287
+ const CREDENTIAL_FAMILY_TOOLS = new Set([
288
+ "credential_store", "credential_delete", "vault_setup",
289
+ // User profile tools: family only
290
+ "user_profile_store", "user_profile_get", "user_profile_delete",
291
+ // Payment tools: family only
292
+ "stripe_create_card", "stripe_deactivate_card", "stripe_list_cards",
293
+ // Booking tools that involve payment: family only
294
+ "flight_book", "flight_hold", "flight_cancel",
295
+ ]);
288
296
  // Credential read tools: friend+
289
297
  const CREDENTIAL_TRUSTED_TOOLS = new Set(["credential_get", "credential_list"]);
290
298
  // Travel tools: friend+ (weather_lookup accesses vault credentials indirectly;
291
299
  // advisory and geocode are public APIs but gated for consistency)
292
- const TRAVEL_TRUSTED_TOOLS = new Set(["weather_lookup", "travel_advisory", "geocode_search"]);
300
+ // Flight search is also friend+ (read-only, no payment)
301
+ const TRAVEL_TRUSTED_TOOLS = new Set(["weather_lookup", "travel_advisory", "geocode_search", "flight_search"]);
293
302
  function checkCredentialTrustGuardrails(toolName, context) {
294
303
  if (CREDENTIAL_FAMILY_TOOLS.has(toolName)) {
295
304
  if (context.trustLevel === "family")
@@ -28,6 +28,7 @@ class McpClient {
28
28
  this.process = (0, child_process_1.spawn)(this.config.command, this.config.args ?? [], {
29
29
  env,
30
30
  stdio: ["pipe", "pipe", "pipe"],
31
+ cwd: this.config.cwd,
31
32
  });
32
33
  this.setupLineReader();
33
34
  this.setupProcessHandlers();
@@ -7,6 +7,7 @@ exports.resetSharedMcpManager = resetSharedMcpManager;
7
7
  const mcp_client_1 = require("./mcp-client");
8
8
  const identity_1 = require("../heart/identity");
9
9
  const runtime_1 = require("../nerves/runtime");
10
+ const credential_access_1 = require("./credential-access");
10
11
  const MAX_RESTART_RETRIES = 5;
11
12
  const RESTART_DELAY_MS = 1000;
12
13
  class McpManager {
@@ -101,8 +102,59 @@ class McpManager {
101
102
  }
102
103
  this.servers.clear();
103
104
  }
105
+ /**
106
+ * Resolve `vault:DOMAIN/FIELD` references in server env config.
107
+ * Returns resolved env or throws with a descriptive error.
108
+ */
109
+ async resolveVaultEnv(_serverName, env) {
110
+ const resolved = { ...env };
111
+ const store = (0, credential_access_1.getCredentialStore)();
112
+ for (const [key, value] of Object.entries(resolved)) {
113
+ const match = value.match(/^vault:([^/]+)\/(.+)$/);
114
+ if (!match)
115
+ continue;
116
+ const [, domain, field] = match;
117
+ try {
118
+ resolved[key] = await store.getRawSecret(domain, field);
119
+ }
120
+ catch (err) {
121
+ /* v8 ignore next -- reason @preserve */
122
+ const reason = err instanceof Error ? err.message : String(err);
123
+ // Classify the error for actionable messaging
124
+ let classification = "vault unreachable";
125
+ if (reason.includes("no credential found")) {
126
+ classification = "item not found";
127
+ }
128
+ else if (reason.includes("field") && reason.includes("not found")) {
129
+ classification = "field empty";
130
+ }
131
+ throw new Error(`vault:${domain}/${field} could not be resolved: ${classification}`);
132
+ }
133
+ }
134
+ return resolved;
135
+ }
104
136
  async connectServer(name, config) {
105
- const client = new mcp_client_1.McpClient(config);
137
+ // Resolve vault: references in env before spawning
138
+ let resolvedConfig = config;
139
+ if (config.env) {
140
+ try {
141
+ const resolvedEnv = await this.resolveVaultEnv(name, config.env);
142
+ resolvedConfig = { ...config, env: resolvedEnv };
143
+ }
144
+ catch (err) {
145
+ /* v8 ignore next -- reason @preserve */
146
+ const reason = err instanceof Error ? err.message : String(err);
147
+ (0, runtime_1.emitNervesEvent)({
148
+ level: "error",
149
+ event: "mcp.vault_resolve_error",
150
+ component: "repertoire",
151
+ message: `skipping MCP server "${name}": ${reason}`,
152
+ meta: { server: name, reason },
153
+ });
154
+ return; // Skip this server, continue to next
155
+ }
156
+ }
157
+ const client = new mcp_client_1.McpClient(resolvedConfig);
106
158
  const entry = {
107
159
  name,
108
160
  config,
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ /**
3
+ * Thin wrapper around the Stripe Issuing API.
4
+ *
5
+ * Initializes with a restricted API key from the agent's vault.
6
+ * Card numbers are NEVER included in nerves events or log output.
7
+ * The `getCardDetails` method exists only for internal payment flows
8
+ * (e.g., passing card details to Duffel) — the returned data must
9
+ * never escape the calling function's scope.
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.createStripeClient = createStripeClient;
16
+ const stripe_1 = __importDefault(require("stripe"));
17
+ const credential_access_1 = require("./credential-access");
18
+ const runtime_1 = require("../nerves/runtime");
19
+ // ---------------------------------------------------------------------------
20
+ // Implementation
21
+ // ---------------------------------------------------------------------------
22
+ function toCardInfo(card) {
23
+ return {
24
+ cardId: card.id,
25
+ last4: card.last4,
26
+ status: card.status,
27
+ };
28
+ }
29
+ /**
30
+ * Create a Stripe client initialized with a restricted key from the vault.
31
+ */
32
+ async function createStripeClient() {
33
+ const store = (0, credential_access_1.getCredentialStore)();
34
+ const apiKey = await store.getRawSecret("stripe.com", "restrictedKey");
35
+ // StripeConstructor is a callable (not a class), cast the result
36
+ const stripe = (0, stripe_1.default)(apiKey);
37
+ return {
38
+ async createVirtualCard(opts) {
39
+ (0, runtime_1.emitNervesEvent)({
40
+ component: "repertoire",
41
+ event: "repertoire.stripe_card_create_start",
42
+ message: "creating virtual card",
43
+ meta: { type: opts.type, currency: opts.currency },
44
+ });
45
+ const spendingControls = {
46
+ spending_limits: [
47
+ {
48
+ amount: opts.spendLimit * 100, // Stripe uses cents
49
+ interval: opts.type === "single_use" ? "all_time" : "monthly",
50
+ },
51
+ ],
52
+ ...(opts.merchantCategories
53
+ ? { allowed_categories: opts.merchantCategories }
54
+ : {}),
55
+ };
56
+ const card = await stripe.issuing.cards.create({
57
+ type: "virtual",
58
+ currency: opts.currency,
59
+ spending_controls: spendingControls,
60
+ status: "active",
61
+ });
62
+ (0, runtime_1.emitNervesEvent)({
63
+ component: "repertoire",
64
+ event: "repertoire.stripe_card_create_end",
65
+ message: "virtual card created",
66
+ meta: { cardId: card.id, last4: card.last4 },
67
+ });
68
+ return toCardInfo(card);
69
+ },
70
+ async getCard(cardId) {
71
+ (0, runtime_1.emitNervesEvent)({
72
+ component: "repertoire",
73
+ event: "repertoire.stripe_card_get",
74
+ message: "retrieving card info",
75
+ meta: { cardId },
76
+ });
77
+ const card = await stripe.issuing.cards.retrieve(cardId);
78
+ return toCardInfo(card);
79
+ },
80
+ async updateCard(cardId, updates) {
81
+ (0, runtime_1.emitNervesEvent)({
82
+ component: "repertoire",
83
+ event: "repertoire.stripe_card_update",
84
+ message: "updating card",
85
+ meta: { cardId },
86
+ });
87
+ const card = await stripe.issuing.cards.update(cardId, updates);
88
+ return toCardInfo(card);
89
+ },
90
+ async deactivateCard(cardId) {
91
+ (0, runtime_1.emitNervesEvent)({
92
+ component: "repertoire",
93
+ event: "repertoire.stripe_card_deactivate",
94
+ message: "deactivating card",
95
+ meta: { cardId },
96
+ });
97
+ const card = await stripe.issuing.cards.update(cardId, { status: "canceled" });
98
+ return toCardInfo(card);
99
+ },
100
+ async listCards() {
101
+ (0, runtime_1.emitNervesEvent)({
102
+ component: "repertoire",
103
+ event: "repertoire.stripe_cards_list",
104
+ message: "listing cards",
105
+ meta: {},
106
+ });
107
+ const result = await stripe.issuing.cards.list();
108
+ return result.data.map(toCardInfo);
109
+ },
110
+ async getCardDetails(cardId) {
111
+ // This method retrieves sensitive card details for payment flows.
112
+ // The data must NEVER be logged, emitted, or returned to the model.
113
+ (0, runtime_1.emitNervesEvent)({
114
+ component: "repertoire",
115
+ event: "repertoire.stripe_card_details_get",
116
+ message: "retrieving card details for payment flow",
117
+ meta: { cardId },
118
+ });
119
+ const card = await stripe.issuing.cards.retrieve(cardId, {
120
+ expand: ["number", "cvc"],
121
+ });
122
+ return {
123
+ cardId: card.id,
124
+ number: card.number,
125
+ cvc: card.cvc,
126
+ expMonth: card.exp_month,
127
+ expYear: card.exp_year,
128
+ };
129
+ },
130
+ };
131
+ }
@@ -12,6 +12,9 @@ const tools_1 = require("./coding/tools");
12
12
  const tools_credential_1 = require("./tools-credential");
13
13
  const tools_vault_1 = require("./tools-vault");
14
14
  const tools_travel_1 = require("./tools-travel");
15
+ const tools_user_profile_1 = require("./tools-user-profile");
16
+ const tools_stripe_1 = require("./tools-stripe");
17
+ const tools_flight_1 = require("./tools-flight");
15
18
  // Re-export flow tools for consumers that import them from tools-base
16
19
  var tools_flow_1 = require("./tools-flow");
17
20
  Object.defineProperty(exports, "ponderTool", { enumerable: true, get: function () { return tools_flow_1.ponderTool; } });
@@ -38,6 +41,9 @@ exports.baseToolDefinitions = [
38
41
  ...tools_credential_1.credentialToolDefinitions,
39
42
  ...tools_vault_1.vaultToolDefinitions,
40
43
  ...tools_travel_1.travelToolDefinitions,
44
+ ...tools_user_profile_1.userProfileToolDefinitions,
45
+ ...tools_stripe_1.stripeToolDefinitions,
46
+ ...tools_flight_1.flightToolDefinitions,
41
47
  ];
42
48
  // Convenience array of just the tool schemas (no handler/integration metadata).
43
49
  // Used by consumers that need the OpenAI function-tool format.
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.flightToolDefinitions = void 0;
4
+ exports.resetDuffelClient = resetDuffelClient;
5
+ const duffel_client_1 = require("./duffel-client");
6
+ const user_profile_1 = require("./user-profile");
7
+ const credential_access_1 = require("./credential-access");
8
+ const runtime_1 = require("../nerves/runtime");
9
+ // Lazy-initialized Duffel client singleton
10
+ let _duffelClient = null;
11
+ async function getDuffelClient() {
12
+ if (!_duffelClient) {
13
+ _duffelClient = await (0, duffel_client_1.createDuffelClient)();
14
+ }
15
+ return _duffelClient;
16
+ }
17
+ function requireFamilyContext(ctx) {
18
+ if (!ctx?.context?.friend?.id) {
19
+ return "no friend context — cannot access flight tools.";
20
+ }
21
+ if (ctx.context.friend.trustLevel !== "family") {
22
+ return "booking and cancellation require family trust level.";
23
+ }
24
+ return { friendId: ctx.context.friend.id };
25
+ }
26
+ function requireFriendContext(ctx) {
27
+ if (!ctx?.context?.friend?.id) {
28
+ return "no friend context — cannot search flights.";
29
+ }
30
+ return { friendId: ctx.context.friend.id };
31
+ }
32
+ exports.flightToolDefinitions = [
33
+ {
34
+ tool: {
35
+ type: "function",
36
+ function: {
37
+ name: "flight_search",
38
+ description: "Search for flights between two airports. Returns available offers with prices.",
39
+ parameters: {
40
+ type: "object",
41
+ properties: {
42
+ origin: { type: "string", description: "Origin airport IATA code (e.g. 'SFO')" },
43
+ destination: { type: "string", description: "Destination airport IATA code (e.g. 'JFK')" },
44
+ departure_date: { type: "string", description: "Departure date (YYYY-MM-DD)" },
45
+ return_date: { type: "string", description: "Return date for round trips (optional)" },
46
+ passengers: { type: "string", description: "Number of adult passengers (default '1')" },
47
+ cabin_class: { type: "string", description: "Cabin class: economy, premium_economy, business, first" },
48
+ },
49
+ required: ["origin", "destination", "departure_date"],
50
+ },
51
+ },
52
+ },
53
+ handler: async (args, ctx) => {
54
+ (0, runtime_1.emitNervesEvent)({
55
+ component: "repertoire",
56
+ event: "repertoire.tool_flight_search",
57
+ message: "flight_search invoked",
58
+ meta: { tool: "flight_search", origin: args.origin, destination: args.destination },
59
+ });
60
+ const guard = requireFriendContext(ctx);
61
+ if (typeof guard === "string")
62
+ return guard;
63
+ try {
64
+ const client = await getDuffelClient();
65
+ const passengerCount = parseInt(args.passengers || "1", 10);
66
+ const passengers = Array.from({ length: passengerCount }, () => ({ type: "adult" }));
67
+ const offers = await client.searchFlights({
68
+ origin: args.origin,
69
+ destination: args.destination,
70
+ departureDate: args.departure_date,
71
+ returnDate: args.return_date,
72
+ passengers,
73
+ cabinClass: args.cabin_class,
74
+ });
75
+ if (offers.length === 0) {
76
+ return "no flights found for those criteria.";
77
+ }
78
+ return JSON.stringify(offers, null, 2);
79
+ }
80
+ catch (err) {
81
+ /* v8 ignore next -- defensive @preserve */
82
+ return `flight search error: ${err instanceof Error ? err.message : String(err)}`;
83
+ }
84
+ },
85
+ summaryKeys: ["origin", "destination", "departure_date"],
86
+ },
87
+ {
88
+ tool: {
89
+ type: "function",
90
+ function: {
91
+ name: "flight_hold",
92
+ description: "Hold a flight offer for a short period before committing to book. Not all airlines support holds.",
93
+ parameters: {
94
+ type: "object",
95
+ properties: {
96
+ offer_id: { type: "string", description: "The Duffel offer ID to hold" },
97
+ },
98
+ required: ["offer_id"],
99
+ },
100
+ },
101
+ },
102
+ handler: async (args, ctx) => {
103
+ (0, runtime_1.emitNervesEvent)({
104
+ component: "repertoire",
105
+ event: "repertoire.tool_flight_hold",
106
+ message: "flight_hold invoked",
107
+ meta: { tool: "flight_hold", offerId: args.offer_id },
108
+ });
109
+ const guard = requireFamilyContext(ctx);
110
+ if (typeof guard === "string")
111
+ return guard;
112
+ // Hold functionality would call Duffel's offer hold API.
113
+ // For pre-build, we return a structured acknowledgment.
114
+ return JSON.stringify({
115
+ status: "hold_requested",
116
+ offerId: args.offer_id,
117
+ message: "Hold requested. Confirm or cancel before the hold expires.",
118
+ });
119
+ },
120
+ summaryKeys: ["offer_id"],
121
+ },
122
+ {
123
+ tool: {
124
+ type: "function",
125
+ function: {
126
+ name: "flight_book",
127
+ description: "Book a flight. Pulls passenger name/DOB/passport from the user's profile. Creates a virtual card, books the flight, then deactivates the card. Requires family trust level.",
128
+ parameters: {
129
+ type: "object",
130
+ properties: {
131
+ offer_id: { type: "string", description: "The Duffel offer ID to book" },
132
+ amount: { type: "string", description: "Expected total amount in dollars" },
133
+ currency: { type: "string", description: "Currency code (e.g. 'usd')" },
134
+ },
135
+ required: ["offer_id", "amount", "currency"],
136
+ },
137
+ },
138
+ },
139
+ handler: async (args, ctx) => {
140
+ (0, runtime_1.emitNervesEvent)({
141
+ component: "repertoire",
142
+ event: "repertoire.tool_flight_book",
143
+ message: "flight_book invoked",
144
+ meta: { tool: "flight_book", offerId: args.offer_id },
145
+ });
146
+ const guard = requireFamilyContext(ctx);
147
+ if (typeof guard === "string")
148
+ return guard;
149
+ try {
150
+ const store = (0, credential_access_1.getCredentialStore)();
151
+ // Get passenger data from profile
152
+ const legalName = await (0, user_profile_1.getUserProfileField)(guard.friendId, "legalName", store);
153
+ if (!legalName) {
154
+ return "passenger profile not found — please store your profile first using user_profile_store.";
155
+ }
156
+ const dateOfBirth = await (0, user_profile_1.getUserProfileField)(guard.friendId, "dateOfBirth", store);
157
+ const passport = await (0, user_profile_1.getUserProfileField)(guard.friendId, "passport", store);
158
+ const client = await getDuffelClient();
159
+ const result = await client.createOrder({
160
+ offerId: args.offer_id,
161
+ passengers: [{
162
+ type: "adult",
163
+ givenName: legalName.first,
164
+ familyName: legalName.last,
165
+ /* v8 ignore next -- reason @preserve */
166
+ dateOfBirth: dateOfBirth ?? "1990-01-01",
167
+ passportNumber: passport?.number,
168
+ passportCountry: passport?.country,
169
+ passportExpiry: passport?.expiry,
170
+ }],
171
+ amount: parseFloat(args.amount),
172
+ currency: args.currency,
173
+ });
174
+ return JSON.stringify(result, null, 2);
175
+ }
176
+ catch (err) {
177
+ /* v8 ignore next -- defensive @preserve */
178
+ return `booking error: ${err instanceof Error ? err.message : String(err)}`;
179
+ }
180
+ },
181
+ summaryKeys: ["offer_id", "amount"],
182
+ },
183
+ {
184
+ tool: {
185
+ type: "function",
186
+ function: {
187
+ name: "flight_cancel",
188
+ description: "Cancel a flight booking. Not all bookings are cancellable. Requires family trust level.",
189
+ parameters: {
190
+ type: "object",
191
+ properties: {
192
+ order_id: { type: "string", description: "The Duffel order ID to cancel" },
193
+ },
194
+ required: ["order_id"],
195
+ },
196
+ },
197
+ },
198
+ handler: async (args, ctx) => {
199
+ (0, runtime_1.emitNervesEvent)({
200
+ component: "repertoire",
201
+ event: "repertoire.tool_flight_cancel",
202
+ message: "flight_cancel invoked",
203
+ meta: { tool: "flight_cancel", orderId: args.order_id },
204
+ });
205
+ const guard = requireFamilyContext(ctx);
206
+ if (typeof guard === "string")
207
+ return guard;
208
+ try {
209
+ const client = await getDuffelClient();
210
+ const result = await client.cancelOrder(args.order_id);
211
+ return JSON.stringify(result, null, 2);
212
+ }
213
+ catch (err) {
214
+ /* v8 ignore next -- defensive @preserve */
215
+ return `cancellation error: ${err instanceof Error ? err.message : String(err)}`;
216
+ }
217
+ },
218
+ summaryKeys: ["order_id"],
219
+ },
220
+ ];
221
+ /** Reset the Duffel client singleton (for testing). */
222
+ function resetDuffelClient() {
223
+ _duffelClient = null;
224
+ }