@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.
- package/changelog.json +8 -1
- package/dist/heart/hatch/hatch-flow.js +1 -1
- package/dist/mind/friends/resolver.js +36 -0
- package/dist/nerves/coverage/file-completeness.js +2 -0
- package/dist/repertoire/bitwarden-store.js +3 -0
- package/dist/repertoire/bw-installer.js +79 -0
- package/dist/repertoire/commerce-errors.js +109 -0
- package/dist/repertoire/commerce-self-test.js +156 -0
- package/dist/repertoire/credential-access.js +38 -3
- package/dist/repertoire/duffel-client.js +185 -0
- package/dist/repertoire/guardrails.js +11 -2
- package/dist/repertoire/mcp-client.js +1 -0
- package/dist/repertoire/mcp-manager.js +53 -1
- package/dist/repertoire/stripe-client.js +131 -0
- package/dist/repertoire/tools-base.js +6 -0
- package/dist/repertoire/tools-flight.js +224 -0
- package/dist/repertoire/tools-stripe.js +180 -0
- package/dist/repertoire/tools-user-profile.js +144 -0
- package/dist/repertoire/user-profile.js +118 -0
- package/dist/senses/cli.js +1 -2
- package/dist/senses/shared-turn.js +1 -2
- package/package.json +3 -2
- package/skills/agent-commerce.md +106 -0
- package/skills/commerce-setup-guide.md +116 -0
- package/skills/commerce-setup.md +84 -0
|
@@ -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([
|
|
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
|
-
|
|
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")
|
|
@@ -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
|
-
|
|
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
|
+
}
|