@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +426 -0
  2. package/LICENSE +21 -0
  3. package/README.md +19 -258
  4. package/dist/annotations.js +35 -20
  5. package/dist/client-address.js +64 -0
  6. package/dist/completions.js +44 -27
  7. package/dist/config.js +89 -38
  8. package/dist/errors.js +99 -22
  9. package/dist/fields.js +68 -0
  10. package/dist/fixed-window.js +46 -0
  11. package/dist/generated/openapi-inputs.js +216 -5820
  12. package/dist/generated/openapi-outputs.js +19014 -14001
  13. package/dist/generated/route-catalog.js +80 -0
  14. package/dist/http.js +108 -44
  15. package/dist/index.js +4 -1
  16. package/dist/oauth-challenge.js +74 -0
  17. package/dist/oauth-resolver.js +58 -61
  18. package/dist/observability.js +31 -16
  19. package/dist/openapi-schema.js +175 -0
  20. package/dist/operations/catalog.js +17 -0
  21. package/dist/operations/groups/analytics.js +20 -10
  22. package/dist/operations/groups/bookings.js +43 -15
  23. package/dist/operations/groups/broadcasts.js +29 -58
  24. package/dist/operations/groups/cadences.js +3 -1
  25. package/dist/operations/groups/callEvents.js +19 -72
  26. package/dist/operations/groups/campaigns.js +28 -32
  27. package/dist/operations/groups/channels.js +23 -0
  28. package/dist/operations/groups/conversations.js +14 -25
  29. package/dist/operations/groups/insights.js +4 -0
  30. package/dist/operations/groups/integrations.js +15 -17
  31. package/dist/operations/groups/knowledge.js +5 -1
  32. package/dist/operations/groups/leads.js +22 -23
  33. package/dist/operations/groups/learning.js +13 -1
  34. package/dist/operations/groups/memory.js +4 -6
  35. package/dist/operations/groups/reports.js +1 -1
  36. package/dist/operations/groups/reviewQueue.js +17 -14
  37. package/dist/operations/groups/safety.js +1 -1
  38. package/dist/operations/groups/voice.js +0 -1
  39. package/dist/operations/groups/webhooks.js +16 -4
  40. package/dist/operations/manifest.js +9 -8
  41. package/dist/operations/registry.js +110 -116
  42. package/dist/operations.js +6 -5
  43. package/dist/optional-field.js +15 -3
  44. package/dist/page-contract.js +74 -0
  45. package/dist/pagination.js +149 -68
  46. package/dist/prompts.js +139 -88
  47. package/dist/provenance.js +2 -2
  48. package/dist/rate-limit.js +40 -0
  49. package/dist/remote.js +145 -65
  50. package/dist/resilience.js +120 -55
  51. package/dist/resolvers.js +125 -0
  52. package/dist/resources.js +17 -2
  53. package/dist/scopes.js +207 -0
  54. package/dist/serialization.js +32 -22
  55. package/dist/server.js +174 -191
  56. package/dist/standard-schema.js +215 -0
  57. package/dist/task-tools/analytics.js +145 -86
  58. package/dist/task-tools/bookings.js +176 -86
  59. package/dist/task-tools/cadences.js +30 -45
  60. package/dist/task-tools/calls.js +132 -125
  61. package/dist/task-tools/inbox.js +89 -53
  62. package/dist/task-tools/leads.js +49 -77
  63. package/dist/task-tools/runtime.js +80 -20
  64. package/dist/task-tools.js +1 -1
  65. package/dist/token-verification.js +42 -0
  66. package/dist/tool-cache.js +3 -2
  67. package/dist/tool.js +155 -100
  68. package/dist/version.js +1 -1
  69. package/package.json +40 -18
  70. package/dist/openapi-zod.js +0 -333
  71. package/dist/page-shape.js +0 -47
  72. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -1,74 +1,154 @@
1
- import { Effect, Option } from "effect";
2
- import * as z from "zod/v4";
3
- import { WORKSPACE_INPUT_FIELDS } from "../operations.js";
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 { independentWrite, numberInput, stringInput, taskRequest, } from "./runtime.js";
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
- * The slots the provider reports as open in the window starting at `slotAt`.
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 — so a coach on Cal.com asking
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 is worth blocking a booking over.
47
+ * `live_provider` read counts.
17
48
  */
18
- const openSlotsAt = (api, operationById, input, slotAt) => Effect.gen(function* () {
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 page = yield* Effect.catchAll(api.request(taskRequest(operationById("bookings.availability"), input, {
24
- query: {
25
- start: start.toISOString(),
26
- end: new Date(start.getTime() + AVAILABILITY_WINDOW_MS).toISOString(),
27
- ...optionalField("provider", input.provider),
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
- () => Effect.succeed(null));
33
- const parsed = AvailabilityPageSchema.safeParse(page);
34
- if (!parsed.success || parsed.data.source !== "live_provider") {
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
- /** Whether two ISO-8601 instants name the same moment, whatever their offset. */
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
- const a = new Date(left).getTime();
46
- const b = new Date(right).getTime();
47
- return !(Number.isNaN(a) || Number.isNaN(b)) && a === b;
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: z.string().min(1).describe("The lead to book."),
56
- slotAt: z.string().describe("ISO-8601 start time for the call."),
57
- provider: z
58
- .string()
59
- .optional()
60
- .describe("The coach's booking provider. Pass it when the coach is not on Calendly, or the availability check reads the wrong calendar."),
61
- idempotencyKey: z
62
- .string()
63
- .optional()
64
- .describe("Reuse the same key when retrying this exact booking."),
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
- readOnly: false,
67
- untrustedContent: false,
68
- composes: ["bookings.availability", "bookings.create"],
69
- run: (api, operationById, input) => Effect.gen(function* () {
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
- const open = yield* openSlotsAt(api, operationById, input, slotAt);
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* api.request(taskRequest(operationById("bookings.create"), input, {
85
- body: { leadId: input.leadId, slotAt: input.slotAt },
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. The lead is not messaged \u2014 send them the link yourself, or draft the message through the review queue.',
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: z.string().min(1).describe("The booking to move."),
98
- slotAt: z.string().describe("ISO-8601 start time to move the call to."),
99
- provider: z
100
- .string()
101
- .optional()
102
- .describe("The coach's booking provider. Pass it when the coach is not on Calendly, or the availability check reads the wrong calendar."),
103
- expiresInHours: z
104
- .number()
105
- .int()
106
- .min(1)
107
- .max(720)
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
- readOnly: false,
116
- untrustedContent: false,
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: (api, operationById, input) => Effect.gen(function* () {
209
+ run: (call, input) => Effect.gen(function* () {
123
210
  const slotAt = stringInput(input.slotAt);
124
211
  const bookingId = stringInput(input.bookingId);
125
- const open = yield* openSlotsAt(api, operationById, input, slotAt);
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* api.request(taskRequest(operationById("bookings.update"), input, {
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* independentWrite("guestLinksError", api.request(taskRequest(operationById("bookings.guestLinks.create"), input, {
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 z from "zod/v4";
2
- import { confirmInput, WORKSPACE_INPUT_FIELDS } from "../operations.js";
3
- import { stringInput, taskRequest, textBody, } from "./runtime.js";
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: z
11
- .array(z.string())
12
- .min(1)
13
- .max(100)
14
- .describe("Lead ids to enroll (max 100). Non-tenant ids are skipped."),
15
- cadenceTemplate: z
16
- .enum(["light", "standard", "aggressive"])
17
- .optional()
18
- .describe("Cadence intensity (default standard)."),
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
- readOnly: false,
26
- untrustedContent: false,
27
- confirmRequired: true,
20
+ outputSchema: operationResult("cadences.rescueStart"),
28
21
  composes: ["cadences.rescueStart"],
29
- run: (api, operationById, input) => api.request(taskRequest(operationById("cadences.rescueStart"), input, {
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: z.string().min(1).describe("The lead whose cadence to pause."),
43
- reason: z
44
- .string()
45
- .min(1)
46
- .max(500)
47
- .optional()
48
- .describe("Optional operator reason, stored on the cadence."),
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
- readOnly: false,
55
- untrustedContent: false,
43
+ outputSchema: operationResult("cadences.pause"),
56
44
  composes: ["cadences.pause"],
57
- run: (api, operationById, input) => api.request(taskRequest(operationById("cadences.pause"), input, {
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];