@setluca/mcp 0.2.0 → 0.3.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/CHANGELOG.md +426 -0
- package/LICENSE +21 -0
- package/README.md +19 -258
- package/dist/annotations.js +35 -20
- package/dist/client-address.js +64 -0
- package/dist/completions.js +44 -27
- package/dist/config.js +89 -38
- package/dist/errors.js +99 -22
- package/dist/fields.js +68 -0
- package/dist/fixed-window.js +46 -0
- package/dist/generated/openapi-inputs.js +216 -5820
- package/dist/generated/openapi-outputs.js +19014 -14001
- package/dist/generated/route-catalog.js +80 -0
- package/dist/http.js +108 -44
- package/dist/index.js +4 -1
- package/dist/oauth-challenge.js +74 -0
- package/dist/oauth-resolver.js +58 -61
- package/dist/observability.js +31 -16
- package/dist/openapi-schema.js +175 -0
- package/dist/operations/catalog.js +17 -0
- package/dist/operations/groups/analytics.js +20 -10
- package/dist/operations/groups/bookings.js +43 -15
- package/dist/operations/groups/broadcasts.js +29 -58
- package/dist/operations/groups/cadences.js +3 -1
- package/dist/operations/groups/callEvents.js +19 -72
- package/dist/operations/groups/campaigns.js +28 -32
- package/dist/operations/groups/channels.js +23 -0
- package/dist/operations/groups/conversations.js +14 -25
- package/dist/operations/groups/insights.js +4 -0
- package/dist/operations/groups/integrations.js +15 -17
- package/dist/operations/groups/knowledge.js +5 -1
- package/dist/operations/groups/leads.js +22 -23
- package/dist/operations/groups/learning.js +13 -1
- package/dist/operations/groups/memory.js +4 -6
- package/dist/operations/groups/reports.js +1 -1
- package/dist/operations/groups/reviewQueue.js +17 -14
- package/dist/operations/groups/safety.js +1 -1
- package/dist/operations/groups/voice.js +0 -1
- package/dist/operations/groups/webhooks.js +16 -4
- package/dist/operations/manifest.js +9 -8
- package/dist/operations/registry.js +110 -116
- package/dist/operations.js +6 -5
- package/dist/optional-field.js +15 -3
- package/dist/page-contract.js +74 -0
- package/dist/pagination.js +149 -68
- package/dist/prompts.js +139 -88
- package/dist/provenance.js +2 -2
- package/dist/rate-limit.js +40 -0
- package/dist/remote.js +145 -65
- package/dist/resilience.js +120 -55
- package/dist/resolvers.js +125 -0
- package/dist/resources.js +17 -2
- package/dist/scopes.js +207 -0
- package/dist/serialization.js +32 -22
- package/dist/server.js +174 -191
- package/dist/standard-schema.js +215 -0
- package/dist/task-tools/analytics.js +145 -86
- package/dist/task-tools/bookings.js +176 -86
- package/dist/task-tools/cadences.js +30 -45
- package/dist/task-tools/calls.js +132 -125
- package/dist/task-tools/inbox.js +89 -53
- package/dist/task-tools/leads.js +49 -77
- package/dist/task-tools/runtime.js +80 -20
- package/dist/task-tools.js +1 -1
- package/dist/token-verification.js +42 -0
- package/dist/tool-cache.js +3 -2
- package/dist/tool.js +155 -100
- package/dist/version.js +1 -1
- package/package.json +40 -18
- package/dist/openapi-zod.js +0 -333
- package/dist/page-shape.js +0 -47
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -1,74 +1,154 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as
|
|
3
|
-
import
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { IsoDateTime, NonEmptyString, WORKSPACE_INPUT_FIELDS, } from "../fields.js";
|
|
4
5
|
import { optionalField } from "../optional-field.js";
|
|
5
|
-
import {
|
|
6
|
+
import { failureAsField, idempotencyKeyInput, isPartialFailure, numberInput, operationResult, resultOrError, stringInput, taskTool, } from "./runtime.js";
|
|
7
|
+
/** The providers the API accepts on an availability read and on a booking. */
|
|
8
|
+
const BOOKING_PROVIDERS = [
|
|
9
|
+
"manual",
|
|
10
|
+
"calendly",
|
|
11
|
+
"cal_com",
|
|
12
|
+
"google_calendar",
|
|
13
|
+
"iclosed",
|
|
14
|
+
"gohighlevel",
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Whether the caller is retrying under a key. A retry may follow a booking that
|
|
18
|
+
* landed while its response was lost, so the slot can be taken by that very
|
|
19
|
+
* booking. Checking availability first would answer slot_not_available and
|
|
20
|
+
* never reach the API's replay of the first result.
|
|
21
|
+
*/
|
|
22
|
+
function isRetry(input) {
|
|
23
|
+
return input.idempotencyKey !== undefined;
|
|
24
|
+
}
|
|
6
25
|
/** How far past the requested slot to look when offering alternatives. */
|
|
7
26
|
const AVAILABILITY_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
27
|
+
const AvailabilityPage = Schema.Struct({
|
|
28
|
+
source: Schema.String,
|
|
29
|
+
slots: Schema.Array(Schema.Struct({ startsAt: Schema.String })),
|
|
30
|
+
});
|
|
31
|
+
const decodeAvailabilityPage = Schema.decodeUnknownOption(AvailabilityPage);
|
|
32
|
+
const ManagedAvailabilityPage = Schema.Struct({
|
|
33
|
+
slots: Schema.Array(Schema.Struct({ startsAt: Schema.String })),
|
|
34
|
+
});
|
|
35
|
+
const decodeManagedAvailabilityPage = Schema.decodeUnknownOption(ManagedAvailabilityPage);
|
|
36
|
+
const BookingBookingType = Schema.Struct({
|
|
37
|
+
booking: Schema.Struct({
|
|
38
|
+
bookingTypeId: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
const decodeBookingBookingType = Schema.decodeUnknownOption(BookingBookingType);
|
|
8
42
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `Option.none()` means "we could not tell", which is different from "nothing
|
|
12
|
-
* is open". The availability route answers with `source: "fallback"` or
|
|
43
|
+
* Open slots from the provider route. It answers with `source: "fallback"` or
|
|
13
44
|
* `"provider_error"` when it could not reach the provider, and it defaults to
|
|
14
|
-
* Calendly when the caller names no provider
|
|
45
|
+
* Calendly when the caller names no provider, so a coach on Cal.com asking
|
|
15
46
|
* without one gets a confident-looking answer about the wrong calendar. Only a
|
|
16
|
-
* `live_provider` read
|
|
47
|
+
* `live_provider` read counts.
|
|
17
48
|
*/
|
|
18
|
-
const
|
|
49
|
+
const providerSlots = (call, input, window) => call("bookings.availability", {
|
|
50
|
+
query: { ...window, ...optionalField("provider", input.provider) },
|
|
51
|
+
}).pipe(Effect.map((page) => decodeAvailabilityPage(page).pipe(Option.filter((parsed) => parsed.source === "live_provider"), Option.map((parsed) => parsed.slots))));
|
|
52
|
+
/**
|
|
53
|
+
* Open slots for one booking type, from Luca's own scheduler. The provider
|
|
54
|
+
* route knows nothing of booking types, so it would judge the slot against
|
|
55
|
+
* the wrong calendar. Times are compared as instants, so the display timezone
|
|
56
|
+
* does not matter.
|
|
57
|
+
*/
|
|
58
|
+
const bookingTypeSlots = (call, bookingTypeId, window) => call("bookings.managedAvailability", {
|
|
59
|
+
query: { ...window, bookingTypeId, timezone: "UTC" },
|
|
60
|
+
}).pipe(Effect.map((page) => decodeManagedAvailabilityPage(page).pipe(Option.map((parsed) => parsed.slots))));
|
|
61
|
+
/**
|
|
62
|
+
* The booking type a booking was made under, read from the booking itself.
|
|
63
|
+
*
|
|
64
|
+
* `Option.none()` means the read failed, and `Option.some(null)` is a booking
|
|
65
|
+
* on no booking type. Both are judged against the provider calendar, as before
|
|
66
|
+
* this read existed. A refused token still ends the call, see
|
|
67
|
+
* isPartialFailure.
|
|
68
|
+
*/
|
|
69
|
+
const bookingTypeOf = (call, bookingId) => call("bookings.get", { pathParams: { id: bookingId } }).pipe(Effect.map((page) => decodeBookingBookingType(page).pipe(Option.map((parsed) => parsed.booking.bookingTypeId ?? null))), Effect.catchIf(isPartialFailure, () => Effect.succeedNone));
|
|
70
|
+
/**
|
|
71
|
+
* The slots `readSlots` reports open in the window starting at `slotAt`.
|
|
72
|
+
*
|
|
73
|
+
* `Option.none()` means "we could not tell", which is different from "nothing
|
|
74
|
+
* is open", and never blocks a booking. A call that carries an idempotency key
|
|
75
|
+
* skips the read, see {@link isRetry}.
|
|
76
|
+
*/
|
|
77
|
+
const openSlotsAt = (input, slotAt, readSlots) => Effect.gen(function* () {
|
|
78
|
+
// A keyed call is a retry, and the API replays the first answer for it.
|
|
79
|
+
if (isRetry(input)) {
|
|
80
|
+
return Option.none();
|
|
81
|
+
}
|
|
82
|
+
// oxlint-disable-next-line effect/use-clock-service -- parse date value from stored data
|
|
19
83
|
const start = new Date(slotAt);
|
|
20
84
|
if (Number.isNaN(start.getTime())) {
|
|
21
85
|
return Option.none();
|
|
22
86
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
})),
|
|
87
|
+
const window = {
|
|
88
|
+
start: start.toISOString(),
|
|
89
|
+
// oxlint-disable-next-line effect/use-clock-service -- parse date value from stored data
|
|
90
|
+
end: new Date(start.getTime() + AVAILABILITY_WINDOW_MS).toISOString(),
|
|
91
|
+
};
|
|
92
|
+
const slots = yield* readSlots(window).pipe(
|
|
30
93
|
// A failed availability read must not block the booking. The agent is no
|
|
31
|
-
// worse off than before this check existed.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return Option.none();
|
|
36
|
-
}
|
|
37
|
-
return Option.some(parsed.data.slots.map((slot) => slot.startsAt));
|
|
38
|
-
});
|
|
39
|
-
const AvailabilityPageSchema = z.object({
|
|
40
|
-
source: z.string(),
|
|
41
|
-
slots: z.array(z.object({ startsAt: z.string() })),
|
|
94
|
+
// worse off than before this check existed. A refused token still ends
|
|
95
|
+
// the call, see isPartialFailure.
|
|
96
|
+
Effect.catchIf(isPartialFailure, () => Effect.succeedNone));
|
|
97
|
+
return Option.map(slots, (open) => open.map((slot) => slot.startsAt));
|
|
42
98
|
});
|
|
43
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Whether two ISO-8601 instants name the same moment, whatever their offset.
|
|
101
|
+
*
|
|
102
|
+
* An unparseable instant is `NaN`, which is not equal to itself, so a garbled
|
|
103
|
+
* slot on either side answers false without a guard of its own.
|
|
104
|
+
*/
|
|
44
105
|
function sameInstant(left, right) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
// oxlint-disable-next-line effect/use-clock-service -- parse date value from stored data
|
|
107
|
+
return new Date(left).getTime() === new Date(right).getTime();
|
|
108
|
+
}
|
|
109
|
+
/** What a booking tool returns instead of writing when the slot is taken. */
|
|
110
|
+
function slotNotAvailable(key) {
|
|
111
|
+
return Schema.Struct({
|
|
112
|
+
[key]: Schema.Literal(false),
|
|
113
|
+
reason: Schema.Literal("slot_not_available"),
|
|
114
|
+
requestedSlotAt: Schema.String,
|
|
115
|
+
openSlots: Schema.Array(Schema.String),
|
|
116
|
+
});
|
|
48
117
|
}
|
|
49
|
-
const bookCall = {
|
|
118
|
+
const bookCall = taskTool({
|
|
50
119
|
name: "luca_book_call",
|
|
51
120
|
title: "Book a call with a lead",
|
|
52
|
-
description: 'Book a lead into a slot the provider confirms is open. Checks availability first and returns the open slots instead of booking when the requested one is taken. Example: call with { leadId, slotAt: "2026-07-14T15:00:00Z" } after the lead agreed to a time.',
|
|
121
|
+
description: 'Book a lead into a slot the provider confirms is open. Checks availability first, against the schedule of the booking type when bookingTypeId is given, and returns the open slots instead of booking when the requested one is taken. A call with an idempotencyKey skips that check so a retry gets the original result back. Booking moves the lead to booked and notifies the coach\'s webhook subscribers, so pass confirm: true only after the coach approves this exact slot. Example: call with { leadId, slotAt: "2026-07-14T15:00:00Z", confirm: true } after the lead agreed to a time.',
|
|
53
122
|
inputSchema: {
|
|
54
123
|
...WORKSPACE_INPUT_FIELDS,
|
|
55
|
-
leadId:
|
|
56
|
-
slotAt:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
|
|
124
|
+
leadId: NonEmptyString.annotate({ description: "The lead to book." }),
|
|
125
|
+
slotAt: IsoDateTime.annotate({
|
|
126
|
+
description: "ISO-8601 start time for the call, with an offset.",
|
|
127
|
+
}),
|
|
128
|
+
provider: Schema.optionalKey(Schema.Literals(BOOKING_PROVIDERS).annotate({
|
|
129
|
+
description: "The coach's booking provider. Sent with the booking and used for the availability check. Pass it when the coach is not on Calendly, or the availability check reads the wrong calendar. Left off, the check reads Calendly and the API records the booking as manual.",
|
|
130
|
+
})),
|
|
131
|
+
bookingTypeId: Schema.optionalKey(NonEmptyString.annotate({
|
|
132
|
+
description: "The booking type to book under, from luca_bookings_types_list. Required when provider is google_calendar, and it must be an active google_calendar type.",
|
|
133
|
+
})),
|
|
134
|
+
...idempotencyKeyInput("Reuse the same key when retrying this exact booking. A keyed call skips the availability check and lets the API replay the first result."),
|
|
65
135
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
136
|
+
outputSchema: Schema.Union([
|
|
137
|
+
slotNotAvailable("booked"),
|
|
138
|
+
operationResult("bookings.create"),
|
|
139
|
+
]),
|
|
140
|
+
composes: [
|
|
141
|
+
"bookings.availability",
|
|
142
|
+
"bookings.managedAvailability",
|
|
143
|
+
"bookings.create",
|
|
144
|
+
],
|
|
145
|
+
run: (call, input) => Effect.gen(function* () {
|
|
70
146
|
const slotAt = stringInput(input.slotAt);
|
|
71
|
-
|
|
147
|
+
// A booking type has its own schedule, which the provider route knows
|
|
148
|
+
// nothing of.
|
|
149
|
+
const open = yield* openSlotsAt(input, slotAt, (window) => input.bookingTypeId === undefined
|
|
150
|
+
? providerSlots(call, input, window)
|
|
151
|
+
: bookingTypeSlots(call, stringInput(input.bookingTypeId), window));
|
|
72
152
|
// Refuse only on a confident "that slot is not open". Handing back the
|
|
73
153
|
// real slots lets the agent pick again in one turn instead of writing a
|
|
74
154
|
// booking the provider will reject later.
|
|
@@ -81,48 +161,61 @@ const bookCall = {
|
|
|
81
161
|
openSlots: open.value,
|
|
82
162
|
};
|
|
83
163
|
}
|
|
84
|
-
return yield*
|
|
85
|
-
body: {
|
|
86
|
-
|
|
164
|
+
return yield* call("bookings.create", {
|
|
165
|
+
body: {
|
|
166
|
+
leadId: input.leadId,
|
|
167
|
+
slotAt: input.slotAt,
|
|
168
|
+
...optionalField("provider", input.provider),
|
|
169
|
+
...optionalField("bookingTypeId", input.bookingTypeId),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
87
172
|
}),
|
|
88
|
-
};
|
|
173
|
+
});
|
|
89
174
|
/** How long a reschedule link stays usable when the caller names no window. */
|
|
90
175
|
const DEFAULT_GUEST_LINK_HOURS = 168;
|
|
91
|
-
const rescheduleCall = {
|
|
176
|
+
const rescheduleCall = taskTool({
|
|
92
177
|
name: "luca_reschedule_call",
|
|
93
178
|
title: "Move a call to a new time",
|
|
94
|
-
description: 'Move a booked call to a new time and hand back a fresh reschedule link for the lead. Checks the new slot is open first and returns the open slots instead of moving the call when it is taken. Example: call with { bookingId, slotAt: "2026-07-16T15:00:00Z" } after the lead asked to push the call.
|
|
179
|
+
description: 'Move a booked call to a new time and hand back a fresh reschedule link for the lead. Checks the new slot is open first, against the schedule of its booking type when it has one, and returns the open slots instead of moving the call when it is taken. A call with an idempotencyKey skips that check so a retry gets the original result back. Moving the call updates the connected calendar and re-arms the lead\'s reminder for the new time, so pass confirm: true only after the coach approves the move. Example: call with { bookingId, slotAt: "2026-07-16T15:00:00Z", confirm: true } after the lead asked to push the call. No message about the move is sent: send the lead the link yourself, or draft the message through the review queue.',
|
|
95
180
|
inputSchema: {
|
|
96
181
|
...WORKSPACE_INPUT_FIELDS,
|
|
97
|
-
bookingId:
|
|
98
|
-
slotAt:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.optional()
|
|
109
|
-
.describe(`How long the new reschedule link stays usable. Defaults to ${DEFAULT_GUEST_LINK_HOURS} hours.`),
|
|
110
|
-
idempotencyKey: z
|
|
111
|
-
.string()
|
|
112
|
-
.optional()
|
|
113
|
-
.describe("Reuse the same key when retrying this exact reschedule."),
|
|
182
|
+
bookingId: NonEmptyString.annotate({ description: "The booking to move." }),
|
|
183
|
+
slotAt: IsoDateTime.annotate({
|
|
184
|
+
description: "ISO-8601 start time to move the call to, with an offset.",
|
|
185
|
+
}),
|
|
186
|
+
provider: Schema.optionalKey(Schema.Literals(BOOKING_PROVIDERS).annotate({
|
|
187
|
+
description: "The coach's booking provider. Used only for the availability check, and only when the booking has no booking type. The booking keeps the provider it already has. Pass it when the coach is not on Calendly, or the availability check reads the wrong calendar.",
|
|
188
|
+
})),
|
|
189
|
+
expiresInHours: Schema.optionalKey(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 720 })).annotate({
|
|
190
|
+
description: `How long the new reschedule link stays usable. Defaults to ${DEFAULT_GUEST_LINK_HOURS} hours.`,
|
|
191
|
+
})),
|
|
192
|
+
...idempotencyKeyInput("Reuse the same key when retrying this exact reschedule. A keyed call skips the availability check and lets the API replay the first result."),
|
|
114
193
|
},
|
|
115
|
-
|
|
116
|
-
|
|
194
|
+
outputSchema: Schema.Union([
|
|
195
|
+
slotNotAvailable("rescheduled"),
|
|
196
|
+
Schema.Struct({
|
|
197
|
+
rescheduled: Schema.Literal(true),
|
|
198
|
+
booking: operationResult("bookings.update"),
|
|
199
|
+
guestLinks: resultOrError("bookings.guestLinks.create", "guestLinksError"),
|
|
200
|
+
}),
|
|
201
|
+
]),
|
|
117
202
|
composes: [
|
|
203
|
+
"bookings.get",
|
|
118
204
|
"bookings.availability",
|
|
205
|
+
"bookings.managedAvailability",
|
|
119
206
|
"bookings.update",
|
|
120
207
|
"bookings.guestLinks.create",
|
|
121
208
|
],
|
|
122
|
-
run: (
|
|
209
|
+
run: (call, input) => Effect.gen(function* () {
|
|
123
210
|
const slotAt = stringInput(input.slotAt);
|
|
124
211
|
const bookingId = stringInput(input.bookingId);
|
|
125
|
-
|
|
212
|
+
// A booking made under a booking type keeps that type's own schedule, so
|
|
213
|
+
// the provider calendar would judge the new slot against the wrong
|
|
214
|
+
// availability. The read happens only when the availability check does.
|
|
215
|
+
const open = yield* openSlotsAt(input, slotAt, (window) => Effect.flatMap(bookingTypeOf(call, bookingId), (bookingType) => Option.match(Option.flatMapNullishOr(bookingType, (id) => id), {
|
|
216
|
+
onNone: () => providerSlots(call, input, window),
|
|
217
|
+
onSome: (bookingTypeId) => bookingTypeSlots(call, bookingTypeId, window),
|
|
218
|
+
})));
|
|
126
219
|
if (Option.isSome(open) &&
|
|
127
220
|
!open.value.some((candidate) => sameInstant(candidate, slotAt))) {
|
|
128
221
|
return {
|
|
@@ -132,23 +225,20 @@ const rescheduleCall = {
|
|
|
132
225
|
openSlots: open.value,
|
|
133
226
|
};
|
|
134
227
|
}
|
|
135
|
-
const booking = yield*
|
|
228
|
+
const booking = yield* call("bookings.update", {
|
|
136
229
|
pathParams: { id: bookingId },
|
|
137
230
|
body: { slotAt },
|
|
138
|
-
})
|
|
231
|
+
});
|
|
139
232
|
// The call has already moved by the time the link is minted. Failing the
|
|
140
233
|
// whole tool here would tell the agent the reschedule did not happen,
|
|
141
234
|
// when the only thing missing is a link it can mint again.
|
|
142
|
-
const guestLinks = yield*
|
|
235
|
+
const guestLinks = yield* failureAsField("guestLinksError", call("bookings.guestLinks.create", {
|
|
143
236
|
pathParams: { id: bookingId },
|
|
144
237
|
body: {
|
|
145
238
|
expiresInHours: numberInput(input.expiresInHours, DEFAULT_GUEST_LINK_HOURS),
|
|
146
239
|
},
|
|
147
|
-
}))
|
|
240
|
+
}));
|
|
148
241
|
return { rescheduled: true, booking, guestLinks };
|
|
149
242
|
}),
|
|
150
|
-
};
|
|
151
|
-
export const bookingTaskTools = [
|
|
152
|
-
bookCall,
|
|
153
|
-
rescheduleCall,
|
|
154
|
-
];
|
|
243
|
+
});
|
|
244
|
+
export const bookingTaskTools = [bookCall, rescheduleCall];
|
|
@@ -1,65 +1,50 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import {
|
|
3
|
-
import { stringInput,
|
|
4
|
-
const rescueSilentLeads = {
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { NonEmptyString, WORKSPACE_INPUT_FIELDS } from "../fields.js";
|
|
3
|
+
import { idempotencyKeyInput, operationResult, stringInput, taskTool, textBody, } from "./runtime.js";
|
|
4
|
+
const rescueSilentLeads = taskTool({
|
|
5
5
|
name: "luca_rescue_silent_leads",
|
|
6
6
|
title: "Enroll silent leads into the rescue cadence",
|
|
7
7
|
description: 'Enroll one or more silent leads into the multi-day rescue cadence. THIS STARTS OUTBOUND TOUCHES to real leads over the coming days — pass confirm: true to proceed. cadenceTemplate is light | standard | aggressive (default standard). Example: { leadIds: ["..."], cadenceTemplate: "standard", confirm: true }.',
|
|
8
8
|
inputSchema: {
|
|
9
9
|
...WORKSPACE_INPUT_FIELDS,
|
|
10
|
-
leadIds:
|
|
11
|
-
.
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
cadenceTemplate:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
...confirmInput,
|
|
20
|
-
idempotencyKey: z
|
|
21
|
-
.string()
|
|
22
|
-
.optional()
|
|
23
|
-
.describe("Reuse the same key when retrying this exact enrollment."),
|
|
10
|
+
leadIds: Schema.Array(NonEmptyString)
|
|
11
|
+
.check(Schema.isMinLength(1), Schema.isMaxLength(100))
|
|
12
|
+
.annotate({
|
|
13
|
+
description: "Lead ids to enroll (max 100). Non-tenant ids are skipped.",
|
|
14
|
+
}),
|
|
15
|
+
cadenceTemplate: Schema.optionalKey(Schema.Literals(["light", "standard", "aggressive"]).annotate({
|
|
16
|
+
description: "Cadence intensity (default standard).",
|
|
17
|
+
})),
|
|
18
|
+
...idempotencyKeyInput("Reuse the same key when retrying this exact enrollment."),
|
|
24
19
|
},
|
|
25
|
-
|
|
26
|
-
untrustedContent: false,
|
|
27
|
-
confirmRequired: true,
|
|
20
|
+
outputSchema: operationResult("cadences.rescueStart"),
|
|
28
21
|
composes: ["cadences.rescueStart"],
|
|
29
|
-
run: (
|
|
22
|
+
run: (call, input) => call("cadences.rescueStart", {
|
|
30
23
|
body: {
|
|
31
24
|
leadIds: input.leadIds,
|
|
32
25
|
...textBody("cadenceTemplate", input.cadenceTemplate),
|
|
33
26
|
},
|
|
34
|
-
})
|
|
35
|
-
};
|
|
36
|
-
const pauseCadence = {
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
const pauseCadence = taskTool({
|
|
37
30
|
name: "luca_pause_cadence",
|
|
38
31
|
title: "Pause a lead's rescue cadence",
|
|
39
32
|
description: 'Cancel a lead\'s running rescue cadence so no further touches are sent. Safe to call twice (a no-op once cancelled). Example: call with { leadId, reason: "lead replied elsewhere" }.',
|
|
40
33
|
inputSchema: {
|
|
41
34
|
...WORKSPACE_INPUT_FIELDS,
|
|
42
|
-
leadId:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
idempotencyKey: z
|
|
50
|
-
.string()
|
|
51
|
-
.optional()
|
|
52
|
-
.describe("Reuse the same key when retrying this exact pause."),
|
|
35
|
+
leadId: NonEmptyString.annotate({
|
|
36
|
+
description: "The lead whose cadence to pause.",
|
|
37
|
+
}),
|
|
38
|
+
reason: Schema.optionalKey(NonEmptyString.check(Schema.isMaxLength(500)).annotate({
|
|
39
|
+
description: "Optional operator reason, stored on the cadence.",
|
|
40
|
+
})),
|
|
41
|
+
...idempotencyKeyInput("Reuse the same key when retrying this exact pause."),
|
|
53
42
|
},
|
|
54
|
-
|
|
55
|
-
untrustedContent: false,
|
|
43
|
+
outputSchema: operationResult("cadences.pause"),
|
|
56
44
|
composes: ["cadences.pause"],
|
|
57
|
-
run: (
|
|
45
|
+
run: (call, input) => call("cadences.pause", {
|
|
58
46
|
pathParams: { id: stringInput(input.leadId) },
|
|
59
47
|
body: textBody("reason", input.reason),
|
|
60
|
-
})
|
|
61
|
-
};
|
|
62
|
-
export const cadenceTaskTools = [
|
|
63
|
-
rescueSilentLeads,
|
|
64
|
-
pauseCadence,
|
|
65
|
-
];
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
export const cadenceTaskTools = [rescueSilentLeads, pauseCadence];
|