@setluca/mcp 0.1.0 → 0.2.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/README.md +240 -124
- package/dist/annotations.js +45 -0
- package/dist/completions.js +90 -0
- package/dist/config.js +55 -29
- package/dist/errors.js +59 -14
- package/dist/generated/openapi-inputs.js +4324 -1429
- package/dist/generated/openapi-outputs.js +21992 -8516
- package/dist/generated/route-catalog.js +1480 -0
- package/dist/http.js +95 -103
- package/dist/index.js +16 -10
- package/dist/oauth-resolver.js +58 -36
- package/dist/observability.js +11 -3
- package/dist/openapi-zod.js +129 -65
- package/dist/operations/catalog.js +50 -0
- package/dist/operations/groups/analytics.js +122 -0
- package/dist/operations/groups/bookings.js +189 -0
- package/dist/operations/groups/broadcasts.js +106 -0
- package/dist/operations/groups/cadences.js +14 -0
- package/dist/operations/groups/callEvents.js +131 -0
- package/dist/operations/groups/campaigns.js +132 -0
- package/dist/operations/groups/capabilities.js +8 -0
- package/dist/operations/groups/coach.js +43 -0
- package/dist/operations/groups/conversations.js +48 -0
- package/dist/operations/groups/insights.js +33 -0
- package/dist/operations/groups/integrations.js +80 -0
- package/dist/operations/groups/knowledge.js +34 -0
- package/dist/operations/groups/leads.js +101 -0
- package/dist/operations/groups/learning.js +24 -0
- package/dist/operations/groups/memory.js +27 -0
- package/dist/operations/groups/reports.js +14 -0
- package/dist/operations/groups/reviewQueue.js +57 -0
- package/dist/operations/groups/safety.js +8 -0
- package/dist/operations/groups/usage.js +13 -0
- package/dist/operations/groups/voice.js +14 -0
- package/dist/operations/groups/webhooks.js +76 -0
- package/dist/operations/manifest.js +40 -0
- package/dist/operations/registry.js +331 -0
- package/dist/operations.js +11 -780
- package/dist/optional-field.js +12 -0
- package/dist/page-shape.js +47 -0
- package/dist/pagination.js +164 -0
- package/dist/prompts.js +63 -8
- package/dist/provenance.js +18 -2
- package/dist/remote.js +80 -49
- package/dist/resilience.js +117 -0
- package/dist/serialization.js +36 -0
- package/dist/server.js +256 -281
- package/dist/task-tools/analytics.js +168 -0
- package/dist/task-tools/bookings.js +154 -0
- package/dist/task-tools/cadences.js +65 -0
- package/dist/task-tools/calls.js +202 -0
- package/dist/task-tools/inbox.js +74 -0
- package/dist/task-tools/leads.js +123 -0
- package/dist/task-tools/runtime.js +92 -0
- package/dist/task-tools.js +21 -0
- package/dist/tool-cache.js +39 -0
- package/dist/tool.js +228 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.js +14 -0
- package/package.json +36 -32
package/dist/openapi-zod.js
CHANGED
|
@@ -15,37 +15,70 @@ function nonNullTypes(schema) {
|
|
|
15
15
|
function withDescription(schema, description) {
|
|
16
16
|
return description ? schema.describe(description) : schema;
|
|
17
17
|
}
|
|
18
|
+
const JsonLiteralSchema = z.union([
|
|
19
|
+
z.string(),
|
|
20
|
+
z.number(),
|
|
21
|
+
z.boolean(),
|
|
22
|
+
z.null(),
|
|
23
|
+
]);
|
|
18
24
|
function enumSchema(values) {
|
|
19
25
|
if (values.length === 0) {
|
|
20
26
|
return z.never();
|
|
21
27
|
}
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
const strings = z.array(z.string()).safeParse(values);
|
|
29
|
+
if (strings.success) {
|
|
30
|
+
const [first, ...rest] = strings.data;
|
|
31
|
+
return first === undefined ? z.never() : z.enum([first, ...rest]);
|
|
24
32
|
}
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
const literals = z.array(JsonLiteralSchema).safeParse(values);
|
|
34
|
+
if (!literals.success) {
|
|
35
|
+
return z.never();
|
|
36
|
+
}
|
|
37
|
+
const [firstValue, secondValue, ...restValues] = literals.data;
|
|
38
|
+
if (firstValue === undefined) {
|
|
39
|
+
return z.never();
|
|
27
40
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
41
|
+
const first = z.literal(firstValue);
|
|
42
|
+
if (secondValue === undefined) {
|
|
43
|
+
return first;
|
|
44
|
+
}
|
|
45
|
+
const second = z.literal(secondValue);
|
|
46
|
+
const rest = restValues.map((value) => z.literal(value));
|
|
47
|
+
return z.union([first, second, ...rest]);
|
|
30
48
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Zod 4 moved the string formats to top-level constructors: the `.uuid()` /
|
|
51
|
+
* `.url()` / `.datetime()` methods on a ZodString are deprecated. They all
|
|
52
|
+
* still subclass ZodString, so the length and pattern refinements below chain
|
|
53
|
+
* onto whichever base a format picks.
|
|
54
|
+
*/
|
|
55
|
+
function formatBase(format) {
|
|
56
|
+
if (format === "uuid") {
|
|
57
|
+
return z.uuid();
|
|
35
58
|
}
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
if (format === "uri") {
|
|
60
|
+
return z.url();
|
|
38
61
|
}
|
|
39
|
-
|
|
40
|
-
|
|
62
|
+
if (format === "date-time") {
|
|
63
|
+
return z.iso.datetime({ offset: true });
|
|
41
64
|
}
|
|
65
|
+
return z.string();
|
|
66
|
+
}
|
|
67
|
+
function stringSchema(schema) {
|
|
68
|
+
let result = formatBase(schema.format);
|
|
42
69
|
if (schema.minLength !== undefined) {
|
|
43
70
|
result = result.min(schema.minLength);
|
|
44
71
|
}
|
|
45
72
|
if (schema.maxLength !== undefined) {
|
|
46
73
|
result = result.max(schema.maxLength);
|
|
47
74
|
}
|
|
75
|
+
// Forcing this guard true is an equivalent mutation rather than an untested
|
|
76
|
+
// branch: with no pattern the body builds `new RegExp(undefined)`, which is
|
|
77
|
+
// the empty pattern `/(?:)/` and matches every string, so the constraint it
|
|
78
|
+
// adds rejects nothing. Forcing it false is a real change and is covered.
|
|
79
|
+
// Stryker disable next-line ConditionalExpression: an absent pattern compiles to /(?:)/, which constrains nothing
|
|
48
80
|
if (schema.pattern) {
|
|
81
|
+
// fallow-ignore-next-line security-sink -- patterns come from Luca's checked-in generated OpenAPI document at startup, never from MCP tool arguments or request data
|
|
49
82
|
result = result.regex(new RegExp(schema.pattern));
|
|
50
83
|
}
|
|
51
84
|
return result;
|
|
@@ -100,18 +133,25 @@ function scalarSchemaWithoutConstraints(schema, type) {
|
|
|
100
133
|
function objectSchema(schema) {
|
|
101
134
|
const properties = schema.properties ?? {};
|
|
102
135
|
const required = new Set(schema.required ?? []);
|
|
103
|
-
const
|
|
136
|
+
const fields = {};
|
|
104
137
|
for (const [key, property] of Object.entries(properties)) {
|
|
105
138
|
const propertySchema = schemaToZod(property);
|
|
106
|
-
|
|
139
|
+
fields[key] = required.has(key)
|
|
140
|
+
? propertySchema
|
|
141
|
+
: propertySchema.optional();
|
|
107
142
|
}
|
|
108
|
-
const result = z.object(
|
|
143
|
+
const result = z.object(fields);
|
|
109
144
|
if (schema.additionalProperties === true) {
|
|
110
145
|
return result.catchall(z.unknown());
|
|
111
146
|
}
|
|
112
|
-
if (schema.additionalProperties &&
|
|
113
|
-
|
|
114
|
-
|
|
147
|
+
if (schema.additionalProperties !== undefined &&
|
|
148
|
+
schema.additionalProperties !== false) {
|
|
149
|
+
const object = z
|
|
150
|
+
.record(z.string(), z.unknown())
|
|
151
|
+
.safeParse(schema.additionalProperties);
|
|
152
|
+
if (object.success) {
|
|
153
|
+
return result.catchall(schemaToZod(schema.additionalProperties));
|
|
154
|
+
}
|
|
115
155
|
}
|
|
116
156
|
return result;
|
|
117
157
|
}
|
|
@@ -128,7 +168,8 @@ function schemaToZod(schema) {
|
|
|
128
168
|
}
|
|
129
169
|
function literalOrEnumSchema(schema) {
|
|
130
170
|
if (schema.const !== undefined) {
|
|
131
|
-
|
|
171
|
+
const literal = JsonLiteralSchema.safeParse(schema.const);
|
|
172
|
+
return literal.success ? z.literal(literal.data) : z.never();
|
|
132
173
|
}
|
|
133
174
|
if (schema.enum) {
|
|
134
175
|
return withDescription(enumSchema(schema.enum), schema.description);
|
|
@@ -152,7 +193,14 @@ function variantSchema(schema, variants) {
|
|
|
152
193
|
const single = unionItems[0] ?? z.never();
|
|
153
194
|
return withDescription(isNullable(schema) ? single.nullable() : single, schema.description);
|
|
154
195
|
}
|
|
155
|
-
const
|
|
196
|
+
const [first, second, ...rest] = unionItems;
|
|
197
|
+
if (first === undefined) {
|
|
198
|
+
return z.never();
|
|
199
|
+
}
|
|
200
|
+
if (second === undefined) {
|
|
201
|
+
return first;
|
|
202
|
+
}
|
|
203
|
+
const union = z.union([first, second, ...rest]);
|
|
156
204
|
return withDescription(isNullable(schema) ? union.nullable() : union, schema.description);
|
|
157
205
|
}
|
|
158
206
|
function describedBaseSchema(schema) {
|
|
@@ -170,37 +218,37 @@ function mergeAllOf(allOf) {
|
|
|
170
218
|
}
|
|
171
219
|
return {
|
|
172
220
|
...merged,
|
|
173
|
-
...(Object.keys(properties).length > 0 ? { properties } :
|
|
174
|
-
|
|
221
|
+
...(Object.keys(properties).length > 0 ? { properties } : undefined),
|
|
222
|
+
// Equivalent under this module's public contract: an empty `required: []`
|
|
223
|
+
// and an absent `required` produce the identical Zod object schema, because
|
|
224
|
+
// `objectSchema` derives its required-set from `schema.required ?? []` — both
|
|
225
|
+
// yield an all-optional object. The guard only avoids emitting a noise key.
|
|
226
|
+
// Stryker disable next-line ConditionalExpression,EqualityOperator: empty and absent `required` are indistinguishable downstream
|
|
227
|
+
...(required.length > 0 ? { required } : undefined),
|
|
175
228
|
};
|
|
176
229
|
}
|
|
177
230
|
function baseSchema(schema) {
|
|
178
231
|
const types = nonNullTypes(schema);
|
|
179
|
-
|
|
232
|
+
const [first, second, ...rest] = types;
|
|
233
|
+
if (first === undefined) {
|
|
180
234
|
return schema.properties ? objectSchema(schema) : z.unknown();
|
|
181
235
|
}
|
|
182
|
-
if (
|
|
183
|
-
return scalarSchema(schema,
|
|
236
|
+
if (second === undefined) {
|
|
237
|
+
return scalarSchema(schema, first);
|
|
184
238
|
}
|
|
185
|
-
return z.union(
|
|
239
|
+
return z.union([
|
|
240
|
+
scalarSchema(schema, first),
|
|
241
|
+
scalarSchema(schema, second),
|
|
242
|
+
...rest.map((type) => scalarSchema(schema, type)),
|
|
243
|
+
]);
|
|
186
244
|
}
|
|
187
|
-
/**
|
|
188
|
-
* Relax a response JSON schema to a **structural, type-only** shape for output
|
|
189
|
-
* validation. The goal is a check that fails loud on a genuinely malformed
|
|
190
|
-
* payload (a field typed as the wrong primitive, `result` that is not an object
|
|
191
|
-
* at all) while never rejecting a well-formed real result.
|
|
192
|
-
*
|
|
193
|
-
* So it keeps each field's base `type` (and `nullable`) but drops value-level
|
|
194
|
-
* constraints — `format` (uuid/date-time), `pattern`, length/range bounds,
|
|
195
|
-
* `enum`, and deep array-item schemas — and makes every object open with no
|
|
196
|
-
* required properties. Those constraints are the false-positive risk: they would
|
|
197
|
-
* 500 a real tool call on a non-v4 uuid, an enum that drifted, or an omitted
|
|
198
|
-
* optional field, and they also break the pagination merge (which drops
|
|
199
|
-
* `nextCursor` and adds a `pagination` block). The trade: array element shapes
|
|
200
|
-
* and formats are not deeply validated; the structural contract still is.
|
|
201
|
-
*/
|
|
202
245
|
function relaxJsonSchema(schema) {
|
|
203
246
|
const relaxed = {};
|
|
247
|
+
// Forcing this guard true assigns `relaxed.type = undefined` when there is no
|
|
248
|
+
// type, which is indistinguishable downstream from omitting the key: both
|
|
249
|
+
// `nonNullTypes` and the object-routing in `baseSchema` treat an undefined
|
|
250
|
+
// `type` and an absent `type` identically, producing the same Zod schema.
|
|
251
|
+
// Stryker disable next-line ConditionalExpression: an undefined `type` and an absent one yield the identical relaxed schema
|
|
204
252
|
if (schema.type !== undefined) {
|
|
205
253
|
relaxed.type = schema.type;
|
|
206
254
|
}
|
|
@@ -215,15 +263,24 @@ function relaxJsonSchema(schema) {
|
|
|
215
263
|
relaxed.properties = properties;
|
|
216
264
|
relaxed.required = [];
|
|
217
265
|
relaxed.additionalProperties = true;
|
|
266
|
+
// Defensive belt-and-suspenders: with `properties` present, `baseSchema`
|
|
267
|
+
// routes an absent type and an explicit `"object"` type to the same
|
|
268
|
+
// `objectSchema` call, so setting the type here never changes the produced
|
|
269
|
+
// Zod schema. Verified equivalent through both public entry points.
|
|
270
|
+
// Stryker disable next-line ConditionalExpression,BlockStatement: object routing is identical with or without an explicit type when properties exist
|
|
218
271
|
if (relaxed.type === undefined) {
|
|
272
|
+
// Stryker disable next-line StringLiteral: type value is inert here — properties already force objectSchema downstream
|
|
219
273
|
relaxed.type = "object";
|
|
220
274
|
}
|
|
221
275
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
276
|
+
if (schema.anyOf) {
|
|
277
|
+
relaxed.anyOf = schema.anyOf.map(relaxJsonSchema);
|
|
278
|
+
}
|
|
279
|
+
if (schema.oneOf) {
|
|
280
|
+
relaxed.oneOf = schema.oneOf.map(relaxJsonSchema);
|
|
281
|
+
}
|
|
282
|
+
if (schema.allOf) {
|
|
283
|
+
relaxed.allOf = schema.allOf.map(relaxJsonSchema);
|
|
227
284
|
}
|
|
228
285
|
// Array items and scalar formats/patterns/ranges/enums are intentionally
|
|
229
286
|
// dropped — see the doc comment.
|
|
@@ -233,37 +290,44 @@ const untrustedProvenanceSchema = z
|
|
|
233
290
|
.object({ untrusted: z.literal(true), note: z.string() })
|
|
234
291
|
.loose();
|
|
235
292
|
/**
|
|
236
|
-
* The `structuredContent`
|
|
293
|
+
* The `structuredContent` fields for a tool: `{ result }`, plus an optional
|
|
237
294
|
* `provenance` marker for operations that carry lead-authored text. `result` is
|
|
238
295
|
* the operation's success-response body, deep-relaxed (see
|
|
239
296
|
* {@link relaxJsonSchema}) so real results validate while malformed ones fail
|
|
240
297
|
* loud.
|
|
241
298
|
*/
|
|
242
|
-
export function
|
|
299
|
+
export function openApiToolOutputFields(input) {
|
|
243
300
|
const resultSchema = input.body
|
|
244
301
|
? schemaToZod(relaxJsonSchema(input.body))
|
|
245
302
|
: z.unknown();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
303
|
+
return input.untrustedContent
|
|
304
|
+
? {
|
|
305
|
+
result: resultSchema,
|
|
306
|
+
provenance: untrustedProvenanceSchema.optional(),
|
|
307
|
+
}
|
|
308
|
+
: { result: resultSchema };
|
|
251
309
|
}
|
|
252
|
-
export function
|
|
253
|
-
const
|
|
254
|
-
Object.assign(shape, input.workspaceInput);
|
|
310
|
+
export function openApiToolInputFields(input) {
|
|
311
|
+
const fields = new Map(Object.entries(input.workspaceInput));
|
|
255
312
|
if (input.idempotencyRequired) {
|
|
256
|
-
Object.
|
|
313
|
+
for (const [key, schema] of Object.entries(input.idempotencyInput)) {
|
|
314
|
+
fields.set(key, schema);
|
|
315
|
+
}
|
|
257
316
|
}
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
317
|
+
const pathSchema = input.spec?.path
|
|
318
|
+
? schemaToZod(input.spec.path)
|
|
319
|
+
: undefined;
|
|
320
|
+
if (pathSchema instanceof z.ZodObject) {
|
|
321
|
+
// oxlint-disable-next-line typescript/dot-notation -- Zod owns this public property name; bracket access avoids classifying the library member as a local shape symbol.
|
|
322
|
+
for (const [key, schema] of Object.entries(pathSchema["shape"])) {
|
|
323
|
+
fields.set(key, schema);
|
|
324
|
+
}
|
|
261
325
|
}
|
|
262
326
|
if (input.spec?.query) {
|
|
263
|
-
|
|
327
|
+
fields.set("query", schemaToZod(input.spec.query).optional());
|
|
264
328
|
}
|
|
265
329
|
if (input.spec?.body) {
|
|
266
|
-
|
|
330
|
+
fields.set("body", schemaToZod(input.spec.body));
|
|
267
331
|
}
|
|
268
|
-
return
|
|
332
|
+
return Object.fromEntries(fields);
|
|
269
333
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { analyticsOperations } from "./groups/analytics.js";
|
|
2
|
+
import { bookingsOperations } from "./groups/bookings.js";
|
|
3
|
+
import { broadcastsOperations } from "./groups/broadcasts.js";
|
|
4
|
+
import { cadencesOperations } from "./groups/cadences.js";
|
|
5
|
+
import { callEventsOperations } from "./groups/callEvents.js";
|
|
6
|
+
import { campaignsOperations } from "./groups/campaigns.js";
|
|
7
|
+
// The operation table: every public Luca API route this server exposes as a
|
|
8
|
+
// tool, one `op({...})` entry per route, split into one file per group under
|
|
9
|
+
// `groups/`. Data only. The model that gives those entries their shape lives in
|
|
10
|
+
// `registry.ts`, and the views derived from the table (the manifest, the group
|
|
11
|
+
// summary) live in `manifest.ts`, so adding a route stays a one-entry edit in
|
|
12
|
+
// the file its group already owns.
|
|
13
|
+
import { capabilitiesOperations } from "./groups/capabilities.js";
|
|
14
|
+
import { coachOperations } from "./groups/coach.js";
|
|
15
|
+
import { conversationsOperations } from "./groups/conversations.js";
|
|
16
|
+
import { insightsOperations } from "./groups/insights.js";
|
|
17
|
+
import { integrationsOperations } from "./groups/integrations.js";
|
|
18
|
+
import { knowledgeOperations } from "./groups/knowledge.js";
|
|
19
|
+
import { leadsOperations } from "./groups/leads.js";
|
|
20
|
+
import { learningOperations } from "./groups/learning.js";
|
|
21
|
+
import { memoryOperations } from "./groups/memory.js";
|
|
22
|
+
import { reportsOperations } from "./groups/reports.js";
|
|
23
|
+
import { reviewQueueOperations } from "./groups/reviewQueue.js";
|
|
24
|
+
import { safetyOperations } from "./groups/safety.js";
|
|
25
|
+
import { usageOperations } from "./groups/usage.js";
|
|
26
|
+
import { voiceOperations } from "./groups/voice.js";
|
|
27
|
+
import { webhooksOperations } from "./groups/webhooks.js";
|
|
28
|
+
export const LUCA_OPERATIONS = [
|
|
29
|
+
...capabilitiesOperations,
|
|
30
|
+
...leadsOperations,
|
|
31
|
+
...conversationsOperations,
|
|
32
|
+
...reviewQueueOperations,
|
|
33
|
+
...bookingsOperations,
|
|
34
|
+
...callEventsOperations,
|
|
35
|
+
...campaignsOperations,
|
|
36
|
+
...broadcastsOperations,
|
|
37
|
+
...webhooksOperations,
|
|
38
|
+
...integrationsOperations,
|
|
39
|
+
...voiceOperations,
|
|
40
|
+
...coachOperations,
|
|
41
|
+
...usageOperations,
|
|
42
|
+
...safetyOperations,
|
|
43
|
+
...reportsOperations,
|
|
44
|
+
...insightsOperations,
|
|
45
|
+
...learningOperations,
|
|
46
|
+
...knowledgeOperations,
|
|
47
|
+
...memoryOperations,
|
|
48
|
+
...cadencesOperations,
|
|
49
|
+
...analyticsOperations,
|
|
50
|
+
];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const analyticsOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "analytics.funnel",
|
|
5
|
+
title: "Read the funnel",
|
|
6
|
+
description: "Pipeline funnel stage counts with period and channel filters. Returns counts and conversion rates for each funnel stage plus period-over-period deltas.",
|
|
7
|
+
}),
|
|
8
|
+
op({
|
|
9
|
+
id: "analytics.revenue",
|
|
10
|
+
title: "Read revenue",
|
|
11
|
+
description: "Revenue attribution and plan ROI for a period. Returns total revenue, ROI, and a per-channel revenue breakdown.",
|
|
12
|
+
}),
|
|
13
|
+
op({
|
|
14
|
+
id: "analytics.speedImpact",
|
|
15
|
+
title: "Read what reply speed is worth",
|
|
16
|
+
description: "Fast-vs-slow first-reply booking-rate impact for a period. Returns fast- and slow-reply conversion rates, a lost-opportunity estimate, and the peak performance reply window.",
|
|
17
|
+
}),
|
|
18
|
+
op({
|
|
19
|
+
id: "analytics.forecast",
|
|
20
|
+
title: "Read the revenue forecast",
|
|
21
|
+
description: "Point-in-time pipeline revenue forecast with on-track, optimistic, and conservative scenarios based on pipeline stage probabilities and historical conversion rates.",
|
|
22
|
+
}),
|
|
23
|
+
op({
|
|
24
|
+
id: "analytics.callIntelligence",
|
|
25
|
+
title: "Read call intelligence",
|
|
26
|
+
description: "Post-call intelligence for a period: show rate, close rate, average rating and rating distribution, call-to-close lag, repeat-call cohort, day-of-week and call-sequence show rates, and surprise patterns, derived from post-call events and coach-recorded outcomes.",
|
|
27
|
+
redactedFields: ["surprisePatterns"],
|
|
28
|
+
}),
|
|
29
|
+
op({
|
|
30
|
+
id: "analytics.callIntelligenceCalls",
|
|
31
|
+
title: "List calls behind call intelligence",
|
|
32
|
+
description: "Most recent completed calls with a generated summary, newest first: the coach-facing summary, attendance outcome, rating, and lead name.",
|
|
33
|
+
redactedFields: ["calls[].leadName", "calls[].summary"],
|
|
34
|
+
}),
|
|
35
|
+
op({
|
|
36
|
+
id: "analytics.ghostedLeads",
|
|
37
|
+
title: "List ghosted leads",
|
|
38
|
+
description: "Ghosted-lead counts, rescue-recovery rate, lost-revenue estimate, and top conversation-killer clusters for a period.",
|
|
39
|
+
}),
|
|
40
|
+
op({
|
|
41
|
+
id: "analytics.trustScore",
|
|
42
|
+
title: "Read the trust score",
|
|
43
|
+
description: "Draft approval, edit, and rejection rates plus auto-send gate state (auto-send flag and any open human-approval window) for a period.",
|
|
44
|
+
}),
|
|
45
|
+
op({
|
|
46
|
+
id: "analytics.leadScore",
|
|
47
|
+
title: "Read a lead's score",
|
|
48
|
+
description: "Predictive booking-likelihood score (0-100) for one lead from the coach's trained bucket-rate model, with a confidence tier and the top contributing features. Honest about insufficient data: an under-trained model returns a null score and low confidence instead of a fabricated number.",
|
|
49
|
+
}),
|
|
50
|
+
op({
|
|
51
|
+
id: "analytics.lossReasons",
|
|
52
|
+
title: "Read why deals are lost",
|
|
53
|
+
description: "Category breakdown of automatic 'why you lost' analyses for a period: count and estimated lost revenue per category (price objection, ghosted after a call-to-action, competitor, slow reply, bad fit, other), derived from a per-lead loss analysis generated when a lead goes silent or lost.",
|
|
54
|
+
}),
|
|
55
|
+
op({
|
|
56
|
+
id: "analytics.contentAttribution",
|
|
57
|
+
title: "Read which content brings leads",
|
|
58
|
+
description: "Per-content rollup (leads touched, qualified, booked, revenue) of attributed touches for a period. Content is attributed automatically from an Instagram comment or Story reply that carries a real media id, or tagged manually; an honest empty list means no content is attributed yet, not zero performance.",
|
|
59
|
+
}),
|
|
60
|
+
op({
|
|
61
|
+
id: "analytics.contentPerformance",
|
|
62
|
+
title: "Read content performance",
|
|
63
|
+
description: "Per-post rollup (reach, engagement rate, likes/comments/saves/shares, attributed leads/bookings/revenue) for each Instagram post, reel, story, or carousel over a period. Insight fields stay null until the coach's account grants Instagram insights access — read that as not connected yet, not zero engagement.",
|
|
64
|
+
}),
|
|
65
|
+
op({
|
|
66
|
+
id: "analytics.topicPerformance",
|
|
67
|
+
title: "Read topic performance",
|
|
68
|
+
description: "Weekly rollup by normalized content topic (post count, average engagement rate, attributed leads/bookings/revenue per theme). Defaults to the most recently computed week when weekStart is omitted; an empty items list is the honest, expected state until content topics are extracted, not zero performance.",
|
|
69
|
+
}),
|
|
70
|
+
op({
|
|
71
|
+
id: "analytics.benchmarks",
|
|
72
|
+
title: "Read peer benchmarks",
|
|
73
|
+
description: "This coach's own cross-coach benchmark percentiles (e.g. 'top 15% for response speed among fitness coaches'), never another coach's data. `comingSoon: true` with an empty metrics list is the honest, expected state until the cross-coach benchmark pipeline populates this coach's row — never a fabricated comparison.",
|
|
74
|
+
}),
|
|
75
|
+
op({
|
|
76
|
+
id: "analytics.insights",
|
|
77
|
+
title: "List insights",
|
|
78
|
+
description: "The coach's proactive-insights feed (metric drops/spikes, opportunities, risks, milestones) surfaced by the daily insight-detection cron, newest first. Optionally filter to one status via the status query param (e.g. unread). An empty items list is the honest, expected state for a coach nothing has been flagged for yet — never a fabricated alert.",
|
|
79
|
+
}),
|
|
80
|
+
op({
|
|
81
|
+
id: "analytics.trends",
|
|
82
|
+
title: "Read trends",
|
|
83
|
+
description: "The coach's weekly metrics as a trend series (leads, qualified, booked, revenue, and rates week over week) for charting direction of travel. An empty series is the honest, expected state until enough weeks of history accrue — never fabricated points.",
|
|
84
|
+
}),
|
|
85
|
+
op({
|
|
86
|
+
id: "analytics.recommendations",
|
|
87
|
+
title: "Read recommendations",
|
|
88
|
+
description: "The coach's active prescriptive recommendations for a week (what to change and why), generated weekly from detected insights and deduped by title. An empty list is the honest, expected state when nothing is recommended for the window — never a fabricated action.",
|
|
89
|
+
redactedFields: ["recommendations[].body"],
|
|
90
|
+
}),
|
|
91
|
+
op({
|
|
92
|
+
id: "analytics.benchmarkConsent",
|
|
93
|
+
title: "Check benchmark sharing consent",
|
|
94
|
+
description: "The coach's cross-coach benchmark opt-in state (whether this workspace contributes to and sees anonymized niche cohort percentiles). Read-only; the opt-in is changed from the app, not through this key surface.",
|
|
95
|
+
}),
|
|
96
|
+
op({
|
|
97
|
+
id: "analytics.learningInsights",
|
|
98
|
+
title: "Read what Luca has learned",
|
|
99
|
+
description: "Read-only rollup of the agent-improvement signals mined from post-call feedback for a period: total/applied/pending signal counts, a per-type breakdown (qualification, objection handling, lead quality, general), and recent example notes. An empty rollup is the honest, expected state until the coach submits call feedback — never fabricated learning.",
|
|
100
|
+
redactedFields: ["recentExamples[].note"],
|
|
101
|
+
}),
|
|
102
|
+
op({
|
|
103
|
+
id: "analytics.crmSyncHealth",
|
|
104
|
+
title: "Check CRM sync health",
|
|
105
|
+
description: "Aggregate CRM-sync health for the coach's post-call summaries: counts of synced, pending, local-only, failed, and permanently-failed pushes. All zeros is the honest, expected state for a coach with no synced summaries yet — never a fabricated status.",
|
|
106
|
+
}),
|
|
107
|
+
op({
|
|
108
|
+
id: "analytics.objections",
|
|
109
|
+
title: "Read objection patterns",
|
|
110
|
+
description: "Objection-bucket rollup over the review queue's qualifier signals for a period (price, timing, trust, fit, authority, smokescreen): per-bucket occurrence count, distinct leads, handled rate, wins, and up to three of the most frequent verbatim lead quotes. Filter by channel. An empty bucket list is the honest, expected state until objections are raised — never fabricated.",
|
|
111
|
+
redactedFields: ["buckets[].topPhrases"],
|
|
112
|
+
}),
|
|
113
|
+
op({
|
|
114
|
+
id: "analytics.objectionDrilldown",
|
|
115
|
+
title: "Drill into one objection type",
|
|
116
|
+
description: "Per-conversation drilldown for one objection bucket: one row per matching conversation with the lead's display name, the verbatim objection quote, whether the draft was sent without an edit, and the lead's most recent call outcome. Bounded by a limit with a hasMore truncation flag; an empty list is the honest, expected state for a bucket with no conversations in the window.",
|
|
117
|
+
redactedFields: [
|
|
118
|
+
"conversations[].leadDisplayName",
|
|
119
|
+
"conversations[].phrase",
|
|
120
|
+
],
|
|
121
|
+
}),
|
|
122
|
+
];
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const bookingsOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "bookings.list",
|
|
5
|
+
title: "List bookings",
|
|
6
|
+
description: "List bookings. Query supports limit, cursor, status, and upcoming.",
|
|
7
|
+
redactedFields: ["bookings[].rawPayload"],
|
|
8
|
+
}),
|
|
9
|
+
op({
|
|
10
|
+
id: "bookings.create",
|
|
11
|
+
title: "Book a call",
|
|
12
|
+
description: "Create a provider-neutral booking record. Body requires leadId and slotAt.",
|
|
13
|
+
redactedFields: ["booking.rawPayload"],
|
|
14
|
+
}),
|
|
15
|
+
op({
|
|
16
|
+
id: "bookings.get",
|
|
17
|
+
title: "Get a booking",
|
|
18
|
+
description: "Read one booking: its slot, status, provider, and lead.",
|
|
19
|
+
redactedFields: ["booking.rawPayload"],
|
|
20
|
+
}),
|
|
21
|
+
op({
|
|
22
|
+
id: "bookings.availability",
|
|
23
|
+
title: "Read open call slots",
|
|
24
|
+
description: "List open slots the coach can actually be booked into. Check this before booking.",
|
|
25
|
+
}),
|
|
26
|
+
op({
|
|
27
|
+
id: "bookings.managedAvailability",
|
|
28
|
+
title: "Read Luca-managed call slots",
|
|
29
|
+
description: "List open slots for one of Luca's own call types, with calendar conflicts already removed.",
|
|
30
|
+
}),
|
|
31
|
+
op({
|
|
32
|
+
id: "bookings.types.list",
|
|
33
|
+
title: "List call types",
|
|
34
|
+
description: "List the call types a booking can use, with their duration and provider.",
|
|
35
|
+
}),
|
|
36
|
+
op({
|
|
37
|
+
id: "bookings.types.schedule",
|
|
38
|
+
title: "Read a call type's schedule",
|
|
39
|
+
description: "Read a call type's weekly schedule: the hours it can be booked and its time zone.",
|
|
40
|
+
}),
|
|
41
|
+
op({
|
|
42
|
+
id: "bookings.types.attendees",
|
|
43
|
+
title: "List a call type's attendees",
|
|
44
|
+
redactedFields: ["attendees[].email", "attendees[].name"],
|
|
45
|
+
description: "List the people invited to every call of this type, on top of the lead.",
|
|
46
|
+
}),
|
|
47
|
+
op({
|
|
48
|
+
id: "bookings.providers.list",
|
|
49
|
+
title: "List booking providers",
|
|
50
|
+
description: "List every booking provider, what each can do, and whether this coach's connection is healthy.",
|
|
51
|
+
}),
|
|
52
|
+
op({
|
|
53
|
+
id: "bookings.providers.get",
|
|
54
|
+
title: "Get a booking provider",
|
|
55
|
+
description: "List the event types and calendars in one connected provider account.",
|
|
56
|
+
}),
|
|
57
|
+
op({
|
|
58
|
+
id: "bookings.calendars.list",
|
|
59
|
+
title: "List connected calendars",
|
|
60
|
+
description: "List the calendars Luca reads for conflicts, and which one it writes new events to.",
|
|
61
|
+
}),
|
|
62
|
+
op({
|
|
63
|
+
id: "bookings.outcomeReasons.list",
|
|
64
|
+
title: "List loss reasons",
|
|
65
|
+
description: "List the reasons a sale can be lost or a call cancelled. Use one of these codes when recording an outcome.",
|
|
66
|
+
}),
|
|
67
|
+
op({
|
|
68
|
+
id: "bookings.outcome.get",
|
|
69
|
+
title: "Get a booking's outcome",
|
|
70
|
+
description: "Read where the sale stands on a booking, plus the version you send back to correct it.",
|
|
71
|
+
redactedFields: ["episode.note"],
|
|
72
|
+
}),
|
|
73
|
+
op({
|
|
74
|
+
id: "bookings.outcome.history",
|
|
75
|
+
title: "Read a booking's outcome history",
|
|
76
|
+
description: "Read every outcome ever recorded on a booking, in order. Corrections append rather than overwrite.",
|
|
77
|
+
redactedFields: ["episode.note", "events[].note"],
|
|
78
|
+
}),
|
|
79
|
+
op({
|
|
80
|
+
id: "bookings.needsOutcome",
|
|
81
|
+
title: "List calls with no outcome recorded",
|
|
82
|
+
description: "List attended calls with no outcome recorded yet. The work list for closing revenue attribution.",
|
|
83
|
+
redactedFields: ["bookings[].leadName"],
|
|
84
|
+
}),
|
|
85
|
+
op({
|
|
86
|
+
id: "bookings.lifecycleAttention",
|
|
87
|
+
title: "List bookings needing attention",
|
|
88
|
+
description: "List bookings where Luca's record and the provider's disagree and a human has to decide.",
|
|
89
|
+
redactedFields: ["bookings[].rawPayload"],
|
|
90
|
+
}),
|
|
91
|
+
op({
|
|
92
|
+
id: "bookings.brief",
|
|
93
|
+
title: "Read a pre-call brief",
|
|
94
|
+
description: "Read the pre-call brief: who the lead is, what they want, and the objections to expect.",
|
|
95
|
+
untrustedContent: true,
|
|
96
|
+
redactedFields: ["brief"],
|
|
97
|
+
}),
|
|
98
|
+
op({
|
|
99
|
+
id: "bookings.update",
|
|
100
|
+
title: "Reschedule or edit a booking",
|
|
101
|
+
description: "Reschedule or update booking status.",
|
|
102
|
+
redactedFields: ["booking.rawPayload"],
|
|
103
|
+
}),
|
|
104
|
+
op({
|
|
105
|
+
id: "bookings.cancel",
|
|
106
|
+
title: "Cancel a booking",
|
|
107
|
+
description: "Cancel a booking.",
|
|
108
|
+
redactedFields: ["booking.rawPayload"],
|
|
109
|
+
}),
|
|
110
|
+
op({
|
|
111
|
+
id: "bookings.outcome.record",
|
|
112
|
+
title: "Record a booking's outcome",
|
|
113
|
+
description: "Record what the call was worth: won, lost with a reason, or still open. To correct an earlier outcome send the expectedVersion you read; a stale version returns 409 with the current one.",
|
|
114
|
+
redactedFields: ["episode.note"],
|
|
115
|
+
}),
|
|
116
|
+
op({
|
|
117
|
+
id: "bookings.lifecycleResolution",
|
|
118
|
+
title: "Resolve a booking's lifecycle flag",
|
|
119
|
+
description: "Settle one booking whose record disagrees with the provider's: retry the sync, dismiss the duplicate, or keep Luca's version.",
|
|
120
|
+
redactedFields: ["booking.rawPayload"],
|
|
121
|
+
}),
|
|
122
|
+
op({
|
|
123
|
+
id: "bookings.providerSync.retry",
|
|
124
|
+
title: "Retry a booking's provider sync",
|
|
125
|
+
description: "Retry a failed write-back to the booking provider. A permanent failure returns 409 rather than retrying forever.",
|
|
126
|
+
}),
|
|
127
|
+
op({
|
|
128
|
+
id: "bookings.guestLinks.create",
|
|
129
|
+
title: "Create a guest link for a booking",
|
|
130
|
+
description: "Mint revocable reschedule and cancel links for one booking, so a lead can move the call without an account.",
|
|
131
|
+
}),
|
|
132
|
+
op({
|
|
133
|
+
id: "bookings.guestLinks.revoke",
|
|
134
|
+
title: "Revoke a booking's guest links",
|
|
135
|
+
description: "Kill every outstanding guest link on a booking at once.",
|
|
136
|
+
}),
|
|
137
|
+
op({
|
|
138
|
+
id: "bookings.outcomeReasons.create",
|
|
139
|
+
title: "Create a loss reason",
|
|
140
|
+
description: "Add a lost-sale or cancellation reason in the workspace's own wording. A code already in use returns 409.",
|
|
141
|
+
}),
|
|
142
|
+
op({
|
|
143
|
+
id: "bookings.outcomeReasons.update",
|
|
144
|
+
title: "Update a loss reason",
|
|
145
|
+
description: "Rename, reorder, or archive an outcome reason. Archiving hides it from the picker and leaves recorded outcomes alone.",
|
|
146
|
+
}),
|
|
147
|
+
op({
|
|
148
|
+
id: "bookings.types.create",
|
|
149
|
+
title: "Create a booking type",
|
|
150
|
+
description: "Define a bookable call: how long it runs, where it happens, and how it looks to the lead. Give it a schedule next \u2014 a booking type with no schedule offers no slots and will never appear in availability.",
|
|
151
|
+
}),
|
|
152
|
+
op({
|
|
153
|
+
id: "bookings.types.update",
|
|
154
|
+
title: "Update a booking type",
|
|
155
|
+
description: "Update only the fields you send on one booking type. Sending `location` replaces the whole location, because its shape depends on the type.",
|
|
156
|
+
}),
|
|
157
|
+
op({
|
|
158
|
+
id: "bookings.types.delete",
|
|
159
|
+
title: "Delete a booking type",
|
|
160
|
+
description: "Remove a booking type and get back the types that are left. Calls already booked against it are not cancelled.",
|
|
161
|
+
}),
|
|
162
|
+
op({
|
|
163
|
+
id: "bookings.types.schedule.replace",
|
|
164
|
+
title: "Replace a booking type schedule",
|
|
165
|
+
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.",
|
|
166
|
+
}),
|
|
167
|
+
op({
|
|
168
|
+
id: "bookings.types.attendees.add",
|
|
169
|
+
title: "Add a booking type attendee",
|
|
170
|
+
redactedFields: ["attendees[].email", "attendees[].name"],
|
|
171
|
+
description: "Add a standing attendee to one booking type, so they are invited to every call booked against it. Returns the full attendee list.",
|
|
172
|
+
}),
|
|
173
|
+
op({
|
|
174
|
+
id: "bookings.types.attendees.remove",
|
|
175
|
+
title: "Remove a booking type attendee",
|
|
176
|
+
redactedFields: ["attendees[].email", "attendees[].name"],
|
|
177
|
+
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.",
|
|
178
|
+
}),
|
|
179
|
+
op({
|
|
180
|
+
id: "bookings.calendars.select",
|
|
181
|
+
title: "Select a booking calendar",
|
|
182
|
+
description: "Add or update a calendar selection, either for conflict checks or as the destination new bookings are written to. The destination has to be writable: a read-only calendar returns 400, and a Google connection needing attention returns 409.",
|
|
183
|
+
}),
|
|
184
|
+
op({
|
|
185
|
+
id: "bookings.calendars.remove",
|
|
186
|
+
title: "Remove a booking calendar",
|
|
187
|
+
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.",
|
|
188
|
+
}),
|
|
189
|
+
];
|