@setluca/mcp 0.4.0 → 0.5.1
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 +111 -0
- package/LICENSE +202 -21
- package/README.md +13 -11
- package/dist/annotations.js +6 -5
- package/dist/generated/openapi-inputs.js +44 -23
- package/dist/generated/openapi-outputs.js +4895 -684
- package/dist/generated/route-catalog.js +168 -0
- package/dist/landing.js +97 -0
- package/dist/operations/groups/bookings.js +31 -0
- package/dist/operations/groups/broadcasts.js +4 -0
- package/dist/operations/groups/cadences.js +1 -0
- package/dist/operations/groups/callEvents.js +15 -1
- package/dist/operations/groups/channels.js +13 -0
- package/dist/operations/groups/conversations.js +34 -0
- package/dist/operations/groups/insights.js +5 -2
- package/dist/operations/groups/integrations.js +17 -4
- package/dist/operations/groups/knowledge.js +2 -0
- package/dist/operations/groups/leads.js +36 -1
- package/dist/operations/groups/learning.js +1 -0
- package/dist/operations/groups/memory.js +1 -0
- package/dist/operations/groups/reviewQueue.js +80 -1
- package/dist/operations/groups/webhooks.js +1 -0
- package/dist/operations/registry.js +11 -4
- package/dist/prompts.js +2 -2
- package/dist/remote.js +6 -0
- package/dist/resources.js +1 -1
- package/dist/scopes.js +13 -11
- package/dist/server.js +5 -3
- package/dist/task-tools/analytics.js +1 -1
- package/dist/task-tools/bookings.js +112 -44
- package/dist/task-tools/calls.js +1 -1
- package/dist/task-tools/inbox.js +1 -1
- package/dist/task-tools/leads.js +1 -1
- package/dist/tool.js +3 -0
- package/dist/version.js +1 -1
- package/package.json +5 -3
|
@@ -141,6 +141,30 @@ export const ROUTE_CATALOG = {
|
|
|
141
141
|
],
|
|
142
142
|
"idempotencyRequired": true
|
|
143
143
|
},
|
|
144
|
+
"leads.drafting.pause": {
|
|
145
|
+
"method": "POST",
|
|
146
|
+
"path": "/api/leads/{id}/drafting/pause",
|
|
147
|
+
"scopes": [
|
|
148
|
+
"leads:write"
|
|
149
|
+
],
|
|
150
|
+
"idempotencyRequired": true
|
|
151
|
+
},
|
|
152
|
+
"leads.drafting.resume": {
|
|
153
|
+
"method": "POST",
|
|
154
|
+
"path": "/api/leads/{id}/drafting/resume",
|
|
155
|
+
"scopes": [
|
|
156
|
+
"leads:write"
|
|
157
|
+
],
|
|
158
|
+
"idempotencyRequired": true
|
|
159
|
+
},
|
|
160
|
+
"conversations.send_status": {
|
|
161
|
+
"method": "GET",
|
|
162
|
+
"path": "/api/conversations/{id}/send-status",
|
|
163
|
+
"scopes": [
|
|
164
|
+
"conversations:read"
|
|
165
|
+
],
|
|
166
|
+
"idempotencyRequired": false
|
|
167
|
+
},
|
|
144
168
|
"conversations.summary": {
|
|
145
169
|
"method": "GET",
|
|
146
170
|
"path": "/api/conversations/summary",
|
|
@@ -197,6 +221,30 @@ export const ROUTE_CATALOG = {
|
|
|
197
221
|
],
|
|
198
222
|
"idempotencyRequired": true
|
|
199
223
|
},
|
|
224
|
+
"conversations.reply_failure_retry": {
|
|
225
|
+
"method": "POST",
|
|
226
|
+
"path": "/api/conversations/{id}/reply-failures/{failureId}/retry",
|
|
227
|
+
"scopes": [
|
|
228
|
+
"conversations:write"
|
|
229
|
+
],
|
|
230
|
+
"idempotencyRequired": false
|
|
231
|
+
},
|
|
232
|
+
"conversations.reply_failure_dismiss": {
|
|
233
|
+
"method": "POST",
|
|
234
|
+
"path": "/api/conversations/{id}/reply-failures/{failureId}/dismiss",
|
|
235
|
+
"scopes": [
|
|
236
|
+
"conversations:write"
|
|
237
|
+
],
|
|
238
|
+
"idempotencyRequired": false
|
|
239
|
+
},
|
|
240
|
+
"conversations.media_retry": {
|
|
241
|
+
"method": "POST",
|
|
242
|
+
"path": "/api/conversations/{id}/messages/{messageId}/media/{mediaId}/retry",
|
|
243
|
+
"scopes": [
|
|
244
|
+
"conversations:write"
|
|
245
|
+
],
|
|
246
|
+
"idempotencyRequired": true
|
|
247
|
+
},
|
|
200
248
|
"reviewQueue.composer": {
|
|
201
249
|
"method": "GET",
|
|
202
250
|
"path": "/api/review-queue/{id}/composer",
|
|
@@ -229,6 +277,22 @@ export const ROUTE_CATALOG = {
|
|
|
229
277
|
],
|
|
230
278
|
"idempotencyRequired": true
|
|
231
279
|
},
|
|
280
|
+
"reviewQueue.bulkApprove": {
|
|
281
|
+
"method": "POST",
|
|
282
|
+
"path": "/api/review-queue/bulk-approve",
|
|
283
|
+
"scopes": [
|
|
284
|
+
"review_queue:write"
|
|
285
|
+
],
|
|
286
|
+
"idempotencyRequired": true
|
|
287
|
+
},
|
|
288
|
+
"reviewQueue.bulkReject": {
|
|
289
|
+
"method": "POST",
|
|
290
|
+
"path": "/api/review-queue/bulk-reject",
|
|
291
|
+
"scopes": [
|
|
292
|
+
"review_queue:write"
|
|
293
|
+
],
|
|
294
|
+
"idempotencyRequired": true
|
|
295
|
+
},
|
|
232
296
|
"reviewQueue.media.retry": {
|
|
233
297
|
"method": "POST",
|
|
234
298
|
"path": "/api/review-queue/{id}/media/{mediaId}/retry",
|
|
@@ -261,6 +325,30 @@ export const ROUTE_CATALOG = {
|
|
|
261
325
|
],
|
|
262
326
|
"idempotencyRequired": false
|
|
263
327
|
},
|
|
328
|
+
"reviewQueue.summary": {
|
|
329
|
+
"method": "GET",
|
|
330
|
+
"path": "/api/review-queue/summary",
|
|
331
|
+
"scopes": [
|
|
332
|
+
"review_queue:read"
|
|
333
|
+
],
|
|
334
|
+
"idempotencyRequired": false
|
|
335
|
+
},
|
|
336
|
+
"reviewQueue.changes": {
|
|
337
|
+
"method": "GET",
|
|
338
|
+
"path": "/api/review-queue/changes",
|
|
339
|
+
"scopes": [
|
|
340
|
+
"review_queue:read"
|
|
341
|
+
],
|
|
342
|
+
"idempotencyRequired": false
|
|
343
|
+
},
|
|
344
|
+
"reviewQueue.drafting": {
|
|
345
|
+
"method": "GET",
|
|
346
|
+
"path": "/api/review-queue/drafting",
|
|
347
|
+
"scopes": [
|
|
348
|
+
"review_queue:read"
|
|
349
|
+
],
|
|
350
|
+
"idempotencyRequired": false
|
|
351
|
+
},
|
|
264
352
|
"reviewQueue.objectionVariant": {
|
|
265
353
|
"method": "POST",
|
|
266
354
|
"path": "/api/review-queue/{id}/objection-variant",
|
|
@@ -269,6 +357,38 @@ export const ROUTE_CATALOG = {
|
|
|
269
357
|
],
|
|
270
358
|
"idempotencyRequired": true
|
|
271
359
|
},
|
|
360
|
+
"reviewQueue.drafting.cancel": {
|
|
361
|
+
"method": "POST",
|
|
362
|
+
"path": "/api/review-queue/drafting/{turnId}/cancel",
|
|
363
|
+
"scopes": [
|
|
364
|
+
"review_queue:write"
|
|
365
|
+
],
|
|
366
|
+
"idempotencyRequired": true
|
|
367
|
+
},
|
|
368
|
+
"reviewQueue.drafting.retry": {
|
|
369
|
+
"method": "POST",
|
|
370
|
+
"path": "/api/review-queue/drafting/{turnId}/retry",
|
|
371
|
+
"scopes": [
|
|
372
|
+
"review_queue:write"
|
|
373
|
+
],
|
|
374
|
+
"idempotencyRequired": true
|
|
375
|
+
},
|
|
376
|
+
"reviewQueue.drafting.dismiss": {
|
|
377
|
+
"method": "POST",
|
|
378
|
+
"path": "/api/review-queue/drafting/{turnId}/dismiss",
|
|
379
|
+
"scopes": [
|
|
380
|
+
"review_queue:write"
|
|
381
|
+
],
|
|
382
|
+
"idempotencyRequired": true
|
|
383
|
+
},
|
|
384
|
+
"reviewQueue.regenerate": {
|
|
385
|
+
"method": "POST",
|
|
386
|
+
"path": "/api/review-queue/{id}/regenerate",
|
|
387
|
+
"scopes": [
|
|
388
|
+
"review_queue:write"
|
|
389
|
+
],
|
|
390
|
+
"idempotencyRequired": true
|
|
391
|
+
},
|
|
272
392
|
"bookings.list": {
|
|
273
393
|
"method": "GET",
|
|
274
394
|
"path": "/api/bookings",
|
|
@@ -381,6 +501,22 @@ export const ROUTE_CATALOG = {
|
|
|
381
501
|
],
|
|
382
502
|
"idempotencyRequired": false
|
|
383
503
|
},
|
|
504
|
+
"bookings.recovery.get": {
|
|
505
|
+
"method": "GET",
|
|
506
|
+
"path": "/api/bookings/{id}/recovery",
|
|
507
|
+
"scopes": [
|
|
508
|
+
"bookings:read"
|
|
509
|
+
],
|
|
510
|
+
"idempotencyRequired": false
|
|
511
|
+
},
|
|
512
|
+
"bookings.recovery.stop": {
|
|
513
|
+
"method": "POST",
|
|
514
|
+
"path": "/api/bookings/{id}/recovery/stop",
|
|
515
|
+
"scopes": [
|
|
516
|
+
"bookings:write"
|
|
517
|
+
],
|
|
518
|
+
"idempotencyRequired": true
|
|
519
|
+
},
|
|
384
520
|
"bookings.outcome.get": {
|
|
385
521
|
"method": "GET",
|
|
386
522
|
"path": "/api/bookings/{id}/outcome",
|
|
@@ -421,6 +557,22 @@ export const ROUTE_CATALOG = {
|
|
|
421
557
|
],
|
|
422
558
|
"idempotencyRequired": true
|
|
423
559
|
},
|
|
560
|
+
"bookings.providerRequests.create": {
|
|
561
|
+
"method": "POST",
|
|
562
|
+
"path": "/api/bookings/provider-requests",
|
|
563
|
+
"scopes": [
|
|
564
|
+
"bookings:write"
|
|
565
|
+
],
|
|
566
|
+
"idempotencyRequired": true
|
|
567
|
+
},
|
|
568
|
+
"bookings.providerRequests.get": {
|
|
569
|
+
"method": "GET",
|
|
570
|
+
"path": "/api/bookings/provider-requests/{id}",
|
|
571
|
+
"scopes": [
|
|
572
|
+
"bookings:read"
|
|
573
|
+
],
|
|
574
|
+
"idempotencyRequired": false
|
|
575
|
+
},
|
|
424
576
|
"bookings.outcome.record": {
|
|
425
577
|
"method": "POST",
|
|
426
578
|
"path": "/api/bookings/{id}/outcome",
|
|
@@ -981,6 +1133,14 @@ export const ROUTE_CATALOG = {
|
|
|
981
1133
|
],
|
|
982
1134
|
"idempotencyRequired": false
|
|
983
1135
|
},
|
|
1136
|
+
"integrations.crm.connections.disconnect": {
|
|
1137
|
+
"method": "POST",
|
|
1138
|
+
"path": "/api/integrations/crm/connections/{id}/disconnect",
|
|
1139
|
+
"scopes": [
|
|
1140
|
+
"integrations:write"
|
|
1141
|
+
],
|
|
1142
|
+
"idempotencyRequired": true
|
|
1143
|
+
},
|
|
984
1144
|
"integrations.crm.connections.health": {
|
|
985
1145
|
"method": "POST",
|
|
986
1146
|
"path": "/api/integrations/crm/connections/{id}/health",
|
|
@@ -1181,6 +1341,14 @@ export const ROUTE_CATALOG = {
|
|
|
1181
1341
|
],
|
|
1182
1342
|
"idempotencyRequired": false
|
|
1183
1343
|
},
|
|
1344
|
+
"channels.set_ai_dm_auto_send": {
|
|
1345
|
+
"method": "PATCH",
|
|
1346
|
+
"path": "/api/channels/{id}/auto-send",
|
|
1347
|
+
"scopes": [
|
|
1348
|
+
"channels:write"
|
|
1349
|
+
],
|
|
1350
|
+
"idempotencyRequired": true
|
|
1351
|
+
},
|
|
1184
1352
|
"reports.morning": {
|
|
1185
1353
|
"method": "GET",
|
|
1186
1354
|
"path": "/api/reports/morning",
|
package/dist/landing.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page a person gets when they open the server in a browser.
|
|
3
|
+
*
|
|
4
|
+
* Links to `https://mcp.setluca.com/mcp` used to answer a browser with the
|
|
5
|
+
* OAuth 401, so crawlers marked every one of them broken. MCP clients never
|
|
6
|
+
* see this page: they POST, ask for `text/event-stream`, or carry a bearer
|
|
7
|
+
* token, and each of those still reaches the MCP handler and its challenge.
|
|
8
|
+
*/
|
|
9
|
+
/** Where a person learns how to connect an assistant. */
|
|
10
|
+
const CONNECT_GUIDE_URL = "https://setluca.com/connect";
|
|
11
|
+
/**
|
|
12
|
+
* A browser or link checker asking for a page: a GET or HEAD that accepts
|
|
13
|
+
* HTML, does not ask for an event stream, and carries no credentials. The
|
|
14
|
+
* branch is kept this narrow because handing an HTML 200 to an MCP client
|
|
15
|
+
* would break its connection. No MCP client sends HEAD.
|
|
16
|
+
*/
|
|
17
|
+
function isBrowserPageRequest(request) {
|
|
18
|
+
const pageMethod = request.method === "GET" || request.method === "HEAD";
|
|
19
|
+
if (!pageMethod || request.headers.has("authorization")) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const accept = request.headers.get("accept") ?? "";
|
|
23
|
+
return accept.includes("text/html") && !accept.includes("text/event-stream");
|
|
24
|
+
}
|
|
25
|
+
function page(origin, mcpPath, intro) {
|
|
26
|
+
return `<!DOCTYPE html>
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<head>
|
|
29
|
+
<meta charset="utf-8">
|
|
30
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
31
|
+
<meta name="robots" content="noindex">
|
|
32
|
+
<title>SetLuca MCP server</title>
|
|
33
|
+
<link rel="icon" href="/favicon.ico" sizes="any">
|
|
34
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
35
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
36
|
+
<style>
|
|
37
|
+
body {
|
|
38
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
39
|
+
max-width: 600px;
|
|
40
|
+
margin: 60px auto;
|
|
41
|
+
padding: 0 20px;
|
|
42
|
+
line-height: 1.6;
|
|
43
|
+
color: #1f2937;
|
|
44
|
+
background: #fcfbf9;
|
|
45
|
+
}
|
|
46
|
+
a { color: #1d4ed8; }
|
|
47
|
+
code {
|
|
48
|
+
background: #f3f4f6;
|
|
49
|
+
padding: 2px 6px;
|
|
50
|
+
border-radius: 3px;
|
|
51
|
+
font-size: 0.9em;
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
54
|
+
</head>
|
|
55
|
+
<body>
|
|
56
|
+
<h1>SetLuca MCP server</h1>
|
|
57
|
+
<p>${intro}</p>
|
|
58
|
+
<p>To connect Claude, ChatGPT, Cursor, or another MCP client, follow the <a href="${CONNECT_GUIDE_URL}">connection guide</a>.</p>
|
|
59
|
+
<ul>
|
|
60
|
+
<li><a href="${CONNECT_GUIDE_URL}">Connect your AI assistant</a></li>
|
|
61
|
+
<li><a href="https://setluca.com">SetLuca</a></li>
|
|
62
|
+
<li><a href="https://github.com/setluca/luca-mcp">Source on GitHub: setluca/luca-mcp</a></li>
|
|
63
|
+
<li><a href="${origin}/.well-known/mcp/server-card.json">MCP server card</a></li>
|
|
64
|
+
</ul>
|
|
65
|
+
<p>MCP endpoint: <code>${origin}${mcpPath}</code></p>
|
|
66
|
+
</body>
|
|
67
|
+
</html>`;
|
|
68
|
+
}
|
|
69
|
+
const ROOT_INTRO = "This server connects AI assistants to SetLuca over the Model Context Protocol (MCP).";
|
|
70
|
+
const MCP_INTRO = "This is the Model Context Protocol (MCP) endpoint for SetLuca. It answers MCP clients and AI assistants, not web browsers.";
|
|
71
|
+
/** The opening line of the page at `pathname`, absent for any other path. */
|
|
72
|
+
function introFor(pathname, mcpPath) {
|
|
73
|
+
if (pathname === "/") {
|
|
74
|
+
return ROOT_INTRO;
|
|
75
|
+
}
|
|
76
|
+
return pathname === mcpPath ? MCP_INTRO : undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The landing page for a browser opening `/` or the MCP path, or `undefined`
|
|
80
|
+
* for every other request, which the caller routes as before.
|
|
81
|
+
*/
|
|
82
|
+
export function browserLandingPage(request, mcpPath) {
|
|
83
|
+
const url = new URL(request.url);
|
|
84
|
+
const intro = introFor(url.pathname, mcpPath);
|
|
85
|
+
if (intro === undefined || !isBrowserPageRequest(request)) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const body = request.method === "HEAD" ? null : page(url.origin, mcpPath, intro);
|
|
89
|
+
return new Response(body, {
|
|
90
|
+
headers: {
|
|
91
|
+
"content-type": "text/html; charset=utf-8",
|
|
92
|
+
// The same URL answers MCP clients with a 401, so a cache must not hand
|
|
93
|
+
// this page to a request with other headers.
|
|
94
|
+
vary: "Accept, Authorization",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -169,6 +169,7 @@ export const bookingsOperations = [
|
|
|
169
169
|
op({
|
|
170
170
|
id: "bookings.guestLinks.revoke",
|
|
171
171
|
title: "Revoke a booking's guest links",
|
|
172
|
+
idempotent: true,
|
|
172
173
|
mutatesExisting: true,
|
|
173
174
|
description: "Kill every outstanding guest link on a booking at once.",
|
|
174
175
|
}),
|
|
@@ -197,11 +198,13 @@ export const bookingsOperations = [
|
|
|
197
198
|
op({
|
|
198
199
|
id: "bookings.types.delete",
|
|
199
200
|
title: "Delete a booking type",
|
|
201
|
+
idempotent: true,
|
|
200
202
|
description: "Remove a booking type and get back the types that are left. Calls already booked against it are not cancelled.",
|
|
201
203
|
}),
|
|
202
204
|
op({
|
|
203
205
|
id: "bookings.types.schedule.replace",
|
|
204
206
|
title: "Replace a booking type schedule",
|
|
207
|
+
idempotent: true,
|
|
205
208
|
mutatesExisting: true,
|
|
206
209
|
description: "Replace a booking type's schedule outright. Send every rule and override you want to keep \u2014 anything left out is dropped, which is also how you clear a day. Read the current schedule first unless you mean to start over.",
|
|
207
210
|
}),
|
|
@@ -213,12 +216,14 @@ export const bookingsOperations = [
|
|
|
213
216
|
op({
|
|
214
217
|
id: "bookings.types.attendees.remove",
|
|
215
218
|
title: "Remove a booking type attendee",
|
|
219
|
+
idempotent: true,
|
|
216
220
|
mutatesExisting: true,
|
|
217
221
|
description: "Remove a standing attendee from one booking type and get back the ones that are left. Calls already booked keep the invitees they were created with.",
|
|
218
222
|
}),
|
|
219
223
|
op({
|
|
220
224
|
id: "bookings.calendars.select",
|
|
221
225
|
title: "Select a booking calendar",
|
|
226
|
+
idempotent: true,
|
|
222
227
|
mutatesExisting: true,
|
|
223
228
|
// Selection checks that the calendar exists in the connected Google account.
|
|
224
229
|
openWorld: true,
|
|
@@ -227,7 +232,33 @@ export const bookingsOperations = [
|
|
|
227
232
|
op({
|
|
228
233
|
id: "bookings.calendars.remove",
|
|
229
234
|
title: "Remove a booking calendar",
|
|
235
|
+
idempotent: true,
|
|
230
236
|
mutatesExisting: true,
|
|
231
237
|
description: "Stop using a calendar for conflict checks or as the booking destination. The calendar itself is left alone in Google; only Luca's selection changes.",
|
|
232
238
|
}),
|
|
239
|
+
op({
|
|
240
|
+
id: "bookings.recovery.get",
|
|
241
|
+
title: "Read a no-show recovery",
|
|
242
|
+
description: "Read a booking's no-show recovery: whether it is active, paused for review, stopped, or done, which follow-up is next, and when the last one went out.",
|
|
243
|
+
}),
|
|
244
|
+
op({
|
|
245
|
+
id: "bookings.recovery.stop",
|
|
246
|
+
title: "Stop a no-show recovery",
|
|
247
|
+
idempotent: true,
|
|
248
|
+
mutatesExisting: true,
|
|
249
|
+
description: "Stop a booking's no-show recovery so no further follow-up is scheduled. A follow-up already being sent may still arrive. Send recovery.id as recoveryCaseId and recovery.outcomeVersion from luca_bookings_recovery_get; a recovery that changed since that read returns 409 booking_recovery_changed.",
|
|
250
|
+
}),
|
|
251
|
+
op({
|
|
252
|
+
id: "bookings.providerRequests.create",
|
|
253
|
+
title: "Book a call on the coach's calendar",
|
|
254
|
+
// Creates the event on the coach's calendar provider and invites the lead.
|
|
255
|
+
openWorld: true,
|
|
256
|
+
confirm: "always",
|
|
257
|
+
description: "Book one exact slot from luca_bookings_managed_availability on the coach's Calendly, Cal.com, or Google Calendar, which sends the invite to the lead's email; the lead also gets Luca's booking confirmation. Pass the slot's id, startsAt, endsAt, and timezone as returned by a read of exactly that slot's window, since a slot id is tied to the window it was read in. The answer is usually pending while the provider confirms; read it again with luca_bookings_provider_requests_get. A slot taken in the meantime returns 409 slot_unavailable.",
|
|
258
|
+
}),
|
|
259
|
+
op({
|
|
260
|
+
id: "bookings.providerRequests.get",
|
|
261
|
+
title: "Check a calendar booking",
|
|
262
|
+
description: "Read where a calendar booking request stands. pending_provider turns into confirmed, handoff_required, or failed once the provider answers, and a confirmed request carries the booking id and meeting link.",
|
|
263
|
+
}),
|
|
233
264
|
];
|
|
@@ -51,6 +51,9 @@ export const broadcastsOperations = [
|
|
|
51
51
|
op({
|
|
52
52
|
id: "broadcasts.pause",
|
|
53
53
|
title: "Pause a broadcast",
|
|
54
|
+
idempotent: true,
|
|
55
|
+
// Sends broadcast.paused to the coach's webhook subscribers.
|
|
56
|
+
openWorld: true,
|
|
54
57
|
mutatesExisting: true,
|
|
55
58
|
// Recipient snapshot, see the note above this list.
|
|
56
59
|
untrustedContent: true,
|
|
@@ -69,6 +72,7 @@ export const broadcastsOperations = [
|
|
|
69
72
|
op({
|
|
70
73
|
id: "broadcasts.cancel",
|
|
71
74
|
title: "Cancel a broadcast",
|
|
75
|
+
idempotent: true,
|
|
72
76
|
mutatesExisting: true,
|
|
73
77
|
// Recipient snapshot, see the note above this list.
|
|
74
78
|
untrustedContent: true,
|
|
@@ -37,11 +37,16 @@ export const callEventsOperations = [
|
|
|
37
37
|
applies: (input) => isNoShowReport(input.body),
|
|
38
38
|
description: "outcome is no_show (can start recovery messaging)",
|
|
39
39
|
},
|
|
40
|
-
|
|
40
|
+
// The API refuses a no_show from a key below queue_ops, so the tool states
|
|
41
|
+
// the tier that can file every report it accepts.
|
|
42
|
+
capability: "queue_ops",
|
|
43
|
+
description: "File the coach's report on how a call went. A completed or no_show outcome also records booking attendance, and a completed call queues its summary. A no_show can start recovery messages to the lead, so it needs the queue_ops grant and confirm: true.",
|
|
41
44
|
}),
|
|
42
45
|
op({
|
|
43
46
|
id: "callEvents.summary.generate",
|
|
44
47
|
title: "Generate a call summary",
|
|
48
|
+
// The summary is pushed to the coach's connected CRM after its review window.
|
|
49
|
+
openWorld: true,
|
|
45
50
|
mutatesExisting: true,
|
|
46
51
|
untrustedContent: true,
|
|
47
52
|
description: "Queue a fresh summary and return straight away. Poll the call event for the result.",
|
|
@@ -51,6 +56,9 @@ export const callEventsOperations = [
|
|
|
51
56
|
title: "Replace a call summary",
|
|
52
57
|
openWorld: true,
|
|
53
58
|
confirm: "always",
|
|
59
|
+
// The API asks full of a key for this route, since the summary is pushed
|
|
60
|
+
// to the connected CRM.
|
|
61
|
+
capability: "full",
|
|
54
62
|
mutatesExisting: true,
|
|
55
63
|
untrustedContent: true,
|
|
56
64
|
description: "Replace the generated summary with the coach's own wording and push it. A summary already in the CRM updates that same note.",
|
|
@@ -60,6 +68,8 @@ export const callEventsOperations = [
|
|
|
60
68
|
title: "Push a call to the CRM",
|
|
61
69
|
openWorld: true,
|
|
62
70
|
confirm: "always",
|
|
71
|
+
// The API asks full of a key for a write into the connected CRM.
|
|
72
|
+
capability: "full",
|
|
63
73
|
mutatesExisting: true,
|
|
64
74
|
untrustedContent: true,
|
|
65
75
|
description: "Send the current summary to the CRM now, without waiting out the review window. Re-pushing updates the existing note.",
|
|
@@ -67,6 +77,7 @@ export const callEventsOperations = [
|
|
|
67
77
|
op({
|
|
68
78
|
id: "callEvents.undoOutcome",
|
|
69
79
|
title: "Undo a call's outcome",
|
|
80
|
+
idempotent: true,
|
|
70
81
|
mutatesExisting: true,
|
|
71
82
|
untrustedContent: true,
|
|
72
83
|
description: "Clear a mistaken attendance mark and put the call back in the queue. The window is 48 hours from when the outcome was set.",
|
|
@@ -74,6 +85,9 @@ export const callEventsOperations = [
|
|
|
74
85
|
op({
|
|
75
86
|
id: "callEvents.backfill",
|
|
76
87
|
title: "Backfill a booking's call event",
|
|
88
|
+
idempotent: true,
|
|
89
|
+
// The summary it queues later syncs to the coach's connected CRM.
|
|
90
|
+
openWorld: true,
|
|
77
91
|
untrustedContent: true,
|
|
78
92
|
description: "Create a call event for a booking that predates automatic creation, then queue its summary. Safe to call twice: an existing call event comes back as is.",
|
|
79
93
|
}),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isJsonObject } from "../../serialization.js";
|
|
1
2
|
import { op } from "../registry.js";
|
|
2
3
|
export const channelsOperations = [
|
|
3
4
|
op({
|
|
@@ -20,4 +21,16 @@ export const channelsOperations = [
|
|
|
20
21
|
title: "Get a channel account",
|
|
21
22
|
description: "Read one channel account: status, health, cleanup state after a disconnect, and the actions the coach can take on it. Older rows stay readable by ID.",
|
|
22
23
|
}),
|
|
24
|
+
op({
|
|
25
|
+
id: "channels.set_ai_dm_auto_send",
|
|
26
|
+
title: "Turn auto-send on or off for a channel",
|
|
27
|
+
// Turning it on lets Luca's replies reach leads without review, on this
|
|
28
|
+
// account and on every other account whose own switch is on.
|
|
29
|
+
openWorld: true,
|
|
30
|
+
confirm: {
|
|
31
|
+
applies: (input) => isJsonObject(input.body) && input.body.enabled === true,
|
|
32
|
+
description: "enabled is true (lets Luca's replies reach leads without review)",
|
|
33
|
+
},
|
|
34
|
+
description: "Turn Luca's DM auto-send on or off for one channel account. Off puts every Luca draft on that account in the review queue. On lets drafts that pass Luca's safety checks send without review, and also turns on the workspace switch, so every other account with its own switch on starts sending too. Campaign sends follow their own policy. WhatsApp and disconnected accounts answer 404.",
|
|
35
|
+
}),
|
|
23
36
|
];
|
|
@@ -34,4 +34,38 @@ export const conversationsOperations = [
|
|
|
34
34
|
confirm: "always",
|
|
35
35
|
description: "Retry only the parts of a partially sent message that are known not to have reached the provider.",
|
|
36
36
|
}),
|
|
37
|
+
op({
|
|
38
|
+
id: "conversations.send_status",
|
|
39
|
+
// Answers with a status and an id only; no lead-authored text.
|
|
40
|
+
untrustedContent: false,
|
|
41
|
+
title: "Check whether a reply was accepted",
|
|
42
|
+
description: "Look up a manual reply by the idempotency key it was sent with, to learn whether the API accepted it for sending before you send it again. accepted means it is queued to go out, not that it was delivered; not_found means no accepted send exists under that key.",
|
|
43
|
+
}),
|
|
44
|
+
op({
|
|
45
|
+
id: "conversations.reply_failure_retry",
|
|
46
|
+
// Answers with ids and status flags only; no lead-authored text.
|
|
47
|
+
untrustedContent: false,
|
|
48
|
+
title: "Resend a failed reply",
|
|
49
|
+
openWorld: true,
|
|
50
|
+
mutatesExisting: true,
|
|
51
|
+
confirm: "always",
|
|
52
|
+
description: "Send a reply the coach wrote that failed to go out, under the key it first went out with, so the lead never gets it twice. Only a failure that is safe to repeat can be retried. Sends a real message.",
|
|
53
|
+
}),
|
|
54
|
+
op({
|
|
55
|
+
id: "conversations.reply_failure_dismiss",
|
|
56
|
+
idempotent: true,
|
|
57
|
+
// Answers with a flag only; no lead-authored text.
|
|
58
|
+
untrustedContent: false,
|
|
59
|
+
title: "Dismiss a failed reply",
|
|
60
|
+
mutatesExisting: true,
|
|
61
|
+
description: "Close a failed reply the coach has given up on, so it leaves the thread on every device. Sends nothing. Dismissing twice changes nothing.",
|
|
62
|
+
}),
|
|
63
|
+
op({
|
|
64
|
+
id: "conversations.media_retry",
|
|
65
|
+
// Answers with a job id and state only; no lead-authored text.
|
|
66
|
+
untrustedContent: false,
|
|
67
|
+
title: "Retry a voice-note transcription",
|
|
68
|
+
mutatesExisting: true,
|
|
69
|
+
description: "Queue another transcription attempt for a voice note in this conversation. Only re-transcribes; never drafts or sends. A refusal says why: a retry is already running, the limit is reached, or the audio is gone.",
|
|
70
|
+
}),
|
|
37
71
|
];
|
|
@@ -8,30 +8,33 @@ export const insightsOperations = [
|
|
|
8
8
|
op({
|
|
9
9
|
id: "insights.markRead",
|
|
10
10
|
title: "Mark an insight read",
|
|
11
|
+
idempotent: true,
|
|
11
12
|
mutatesExisting: true,
|
|
12
13
|
description: "Mark one insight read, so it stops coming back at the top of the feed.",
|
|
13
14
|
}),
|
|
14
15
|
op({
|
|
15
16
|
id: "insights.dismiss",
|
|
16
17
|
title: "Dismiss an insight",
|
|
18
|
+
idempotent: true,
|
|
17
19
|
mutatesExisting: true,
|
|
18
20
|
description: "Dismiss one insight the coach does not want to act on. Use this rather than leaving it unread forever.",
|
|
19
21
|
}),
|
|
20
22
|
op({
|
|
21
23
|
id: "insights.markActioned",
|
|
22
24
|
title: "Mark an insight actioned",
|
|
25
|
+
idempotent: true,
|
|
23
26
|
mutatesExisting: true,
|
|
24
27
|
description: "Record that the thing an insight asked for was actually done. Use it after doing the work, not instead of it.",
|
|
25
28
|
}),
|
|
26
29
|
op({
|
|
27
30
|
id: "insights.settings.get",
|
|
28
31
|
title: "Read alert delivery settings",
|
|
29
|
-
|
|
30
|
-
description: "Read which insight alerts reach the coach and how. The row is created with defaults on the first read, so this never 404s. Check it before concluding a quiet feed means nothing is happening \u2014 it may just be muted.",
|
|
32
|
+
description: "Read which insight alerts reach the coach and how. Before the coach changes anything this returns the defaults, so it never 404s. Check it before concluding a quiet feed means nothing is happening, since alerts may be muted.",
|
|
31
33
|
}),
|
|
32
34
|
op({
|
|
33
35
|
id: "insights.settings.update",
|
|
34
36
|
title: "Update alert delivery settings",
|
|
37
|
+
idempotent: true,
|
|
35
38
|
mutatesExisting: true,
|
|
36
39
|
description: "Merge-patch the coach's alert-delivery settings. Send only what you are changing; anything left out keeps its current value.",
|
|
37
40
|
}),
|
|
@@ -20,12 +20,16 @@ export const integrationsOperations = [
|
|
|
20
20
|
op({
|
|
21
21
|
id: "integrations.crm.connections.create",
|
|
22
22
|
title: "Create a CRM connection",
|
|
23
|
+
idempotent: true,
|
|
23
24
|
mutatesExisting: true,
|
|
24
25
|
description: "Create or update a workspace-scoped CRM connection.",
|
|
25
26
|
}),
|
|
26
27
|
op({
|
|
27
28
|
id: "integrations.crm.connections.schema",
|
|
28
29
|
title: "Read a CRM's field schema",
|
|
30
|
+
// Refreshes Luca's cached copy of the CRM schema, the same copy every time.
|
|
31
|
+
readOnly: false,
|
|
32
|
+
idempotent: true,
|
|
29
33
|
openWorld: true,
|
|
30
34
|
// sampleRecords are literal contact and deal rows from the coach's CRM, which leads and other people filled in.
|
|
31
35
|
untrustedContent: true,
|
|
@@ -48,11 +52,13 @@ export const integrationsOperations = [
|
|
|
48
52
|
op({
|
|
49
53
|
id: "integrations.crm.mappings.suggest",
|
|
50
54
|
title: "Suggest CRM field mappings",
|
|
55
|
+
idempotent: true,
|
|
51
56
|
description: "Suggest CRM field mappings.",
|
|
52
57
|
}),
|
|
53
58
|
op({
|
|
54
59
|
id: "integrations.crm.mappings.confirm",
|
|
55
60
|
title: "Confirm CRM field mappings",
|
|
61
|
+
idempotent: true,
|
|
56
62
|
mutatesExisting: true,
|
|
57
63
|
description: "Confirm CRM field mappings.",
|
|
58
64
|
}),
|
|
@@ -65,12 +71,10 @@ export const integrationsOperations = [
|
|
|
65
71
|
}),
|
|
66
72
|
op({
|
|
67
73
|
id: "integrations.crm.syncRuns.create",
|
|
68
|
-
title: "
|
|
69
|
-
openWorld: true,
|
|
70
|
-
confirm: "always",
|
|
74
|
+
title: "Preview a CRM sync",
|
|
71
75
|
// Mutations carry CRM record values: names, emails, notes.
|
|
72
76
|
untrustedContent: true,
|
|
73
|
-
description: "
|
|
77
|
+
description: "Record a dry-run preview of the changes a CRM sync would make, as a new sync run. Nothing is written to the CRM: the API refuses a live sync.",
|
|
74
78
|
}),
|
|
75
79
|
op({
|
|
76
80
|
id: "integrations.events.list",
|
|
@@ -79,4 +83,13 @@ export const integrationsOperations = [
|
|
|
79
83
|
untrustedContent: true,
|
|
80
84
|
description: "List integration event log.",
|
|
81
85
|
}),
|
|
86
|
+
op({
|
|
87
|
+
id: "integrations.crm.connections.disconnect",
|
|
88
|
+
title: "Disconnect a CRM",
|
|
89
|
+
idempotent: true,
|
|
90
|
+
mutatesExisting: true,
|
|
91
|
+
// Stops every sync with that CRM until the coach signs in to it again.
|
|
92
|
+
confirm: "always",
|
|
93
|
+
description: "Disable a CRM connection and clear the credentials Luca holds, which stops every sync with that CRM until the coach connects it again. Saved mappings and event history stay. It does not revoke access on the CRM's side.",
|
|
94
|
+
}),
|
|
82
95
|
];
|
|
@@ -15,6 +15,7 @@ export const knowledgeOperations = [
|
|
|
15
15
|
op({
|
|
16
16
|
id: "knowledge.rejectSuggestion",
|
|
17
17
|
title: "Reject a knowledge suggestion",
|
|
18
|
+
idempotent: true,
|
|
18
19
|
mutatesExisting: true,
|
|
19
20
|
description: "Reject a profile suggestion so it stops being offered. The profile is left as it stands.",
|
|
20
21
|
}),
|
|
@@ -33,6 +34,7 @@ export const knowledgeOperations = [
|
|
|
33
34
|
op({
|
|
34
35
|
id: "knowledge.sources.disconnect",
|
|
35
36
|
title: "Disconnect a knowledge source",
|
|
37
|
+
idempotent: true,
|
|
36
38
|
mutatesExisting: true,
|
|
37
39
|
description: "Delete a knowledge source. Its pending suggestions go with it; values the coach already approved stay in the profile, because approving copies them there.",
|
|
38
40
|
}),
|