@setluca/mcp 0.2.1 → 0.3.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 +331 -0
- package/LICENSE +21 -0
- package/README.md +19 -288
- package/dist/annotations.js +33 -18
- package/dist/client-address.js +64 -0
- package/dist/completions.js +24 -27
- package/dist/config.js +89 -38
- package/dist/errors.js +94 -13
- package/dist/fields.js +68 -0
- package/dist/fixed-window.js +46 -0
- package/dist/generated/openapi-inputs.js +216 -5820
- package/dist/generated/openapi-outputs.js +19014 -14001
- package/dist/generated/route-catalog.js +80 -0
- package/dist/http.js +107 -44
- package/dist/index.js +4 -1
- package/dist/oauth-challenge.js +74 -0
- package/dist/oauth-resolver.js +58 -61
- package/dist/observability.js +30 -15
- package/dist/openapi-schema.js +175 -0
- package/dist/operations/catalog.js +17 -0
- package/dist/operations/groups/analytics.js +20 -10
- package/dist/operations/groups/bookings.js +43 -15
- package/dist/operations/groups/broadcasts.js +29 -58
- package/dist/operations/groups/cadences.js +3 -1
- package/dist/operations/groups/callEvents.js +19 -72
- package/dist/operations/groups/campaigns.js +28 -32
- package/dist/operations/groups/channels.js +23 -0
- package/dist/operations/groups/conversations.js +14 -25
- package/dist/operations/groups/insights.js +4 -0
- package/dist/operations/groups/integrations.js +15 -17
- package/dist/operations/groups/knowledge.js +5 -1
- package/dist/operations/groups/leads.js +22 -23
- package/dist/operations/groups/learning.js +13 -1
- package/dist/operations/groups/memory.js +4 -6
- package/dist/operations/groups/reports.js +1 -1
- package/dist/operations/groups/reviewQueue.js +17 -14
- package/dist/operations/groups/safety.js +1 -1
- package/dist/operations/groups/voice.js +0 -1
- package/dist/operations/groups/webhooks.js +15 -3
- package/dist/operations/manifest.js +7 -6
- package/dist/operations/registry.js +102 -108
- package/dist/operations.js +4 -3
- package/dist/optional-field.js +15 -3
- package/dist/page-contract.js +74 -0
- package/dist/pagination.js +147 -67
- package/dist/prompts.js +122 -97
- package/dist/rate-limit.js +40 -0
- package/dist/remote.js +145 -65
- package/dist/resilience.js +107 -52
- package/dist/resolvers.js +125 -0
- package/dist/scopes.js +70 -17
- package/dist/serialization.js +32 -22
- package/dist/server.js +148 -132
- package/dist/standard-schema.js +215 -0
- package/dist/task-tools/analytics.js +145 -86
- package/dist/task-tools/bookings.js +168 -81
- package/dist/task-tools/cadences.js +30 -45
- package/dist/task-tools/calls.js +131 -124
- package/dist/task-tools/inbox.js +37 -47
- package/dist/task-tools/leads.js +49 -77
- package/dist/task-tools/runtime.js +75 -15
- package/dist/token-verification.js +47 -0
- package/dist/tool-cache.js +3 -2
- package/dist/tool.js +151 -96
- package/dist/version.js +1 -1
- package/package.json +37 -16
- package/dist/openapi-zod.js +0 -335
- package/dist/page-shape.js +0 -62
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as Arr from "effect/Array";
|
|
2
|
+
import * as EffectJsonSchema from "effect/JsonSchema";
|
|
3
|
+
import * as P from "effect/Predicate";
|
|
4
|
+
import * as R from "effect/Record";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import * as SchemaRepresentation from "effect/SchemaRepresentation";
|
|
7
|
+
import { ISO_DATE_TIME_PATTERN, UUID_PATTERN } from "./fields.js";
|
|
8
|
+
import { optionalField, optionalFields } from "./optional-field.js";
|
|
9
|
+
/**
|
|
10
|
+
* Patterns for the string formats a tool argument is checked against. Effect
|
|
11
|
+
* keeps `format` as an annotation and does not validate it, so each format the
|
|
12
|
+
* server enforces is restated as a pattern. These are the three formats the
|
|
13
|
+
* server checked before the move to Effect Schema; `date` and `email` stay
|
|
14
|
+
* advisory and the API validates them.
|
|
15
|
+
*/
|
|
16
|
+
const FORMAT_PATTERNS = {
|
|
17
|
+
uuid: UUID_PATTERN.source,
|
|
18
|
+
"date-time": ISO_DATE_TIME_PATTERN.source,
|
|
19
|
+
uri: "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$",
|
|
20
|
+
};
|
|
21
|
+
/** Whether a `format` is one {@link FORMAT_PATTERNS} enforces. */
|
|
22
|
+
const isEnforcedFormat = Schema.is(Schema.Literals(R.keys(FORMAT_PATTERNS)));
|
|
23
|
+
const isJsonLiteral = (value) => P.isString(value) ||
|
|
24
|
+
P.isNumber(value) ||
|
|
25
|
+
P.isBoolean(value) ||
|
|
26
|
+
P.isNull(value);
|
|
27
|
+
/**
|
|
28
|
+
* The shapes the API's OpenAPI 3.1 document does not use and this importer
|
|
29
|
+
* does not translate, each named so a schema that starts using one fails the
|
|
30
|
+
* generator with the keyword to handle rather than importing as something
|
|
31
|
+
* looser than it says.
|
|
32
|
+
*/
|
|
33
|
+
const UNSUPPORTED_KEYWORDS = [
|
|
34
|
+
["allOf", (node) => node.allOf !== undefined],
|
|
35
|
+
["nullable (OpenAPI 3.0)", (node) => node.nullable !== undefined],
|
|
36
|
+
[
|
|
37
|
+
"a boolean exclusive bound (OpenAPI 3.0)",
|
|
38
|
+
(node) => P.isBoolean(node.exclusiveMinimum) || P.isBoolean(node.exclusiveMaximum),
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"an object or array enum or const value",
|
|
42
|
+
(node) => {
|
|
43
|
+
const values = node.enum;
|
|
44
|
+
const constant = node.const;
|
|
45
|
+
return ((Arr.isArray(values) && !Arr.every(values, isJsonLiteral)) ||
|
|
46
|
+
(constant !== undefined && !isJsonLiteral(constant)));
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
"properties without type: object",
|
|
51
|
+
(node) => P.isObject(node.properties) && node.type === undefined,
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"an additionalProperties schema beside declared properties",
|
|
55
|
+
(node) => {
|
|
56
|
+
const extra = node.additionalProperties;
|
|
57
|
+
return (P.isObject(node.properties) &&
|
|
58
|
+
P.isObject(extra) &&
|
|
59
|
+
!R.isEmptyRecord(extra));
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
];
|
|
63
|
+
function unsupportedKeywords(node) {
|
|
64
|
+
return Arr.flatMap(UNSUPPORTED_KEYWORDS, ([keyword, appears]) => appears(node) ? [keyword] : []);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The extra-key rule for an object node. A declared object without the key is
|
|
68
|
+
* closed, `true` and a nested schema stay as they are, and any other value is
|
|
69
|
+
* malformed and closes the object rather than opening it.
|
|
70
|
+
*/
|
|
71
|
+
function closeObject(node) {
|
|
72
|
+
const extra = node.additionalProperties;
|
|
73
|
+
if (P.isBoolean(extra) || P.isObject(extra)) {
|
|
74
|
+
return node;
|
|
75
|
+
}
|
|
76
|
+
return extra === undefined && !P.isObject(node.properties)
|
|
77
|
+
? node
|
|
78
|
+
: { ...node, additionalProperties: false };
|
|
79
|
+
}
|
|
80
|
+
function formatPattern(node) {
|
|
81
|
+
const format = node.format;
|
|
82
|
+
if (!isEnforcedFormat(format) || node.pattern !== undefined) {
|
|
83
|
+
return node;
|
|
84
|
+
}
|
|
85
|
+
return { ...node, pattern: FORMAT_PATTERNS[format] };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Rewrites one JSON Schema node before Effect translates it, so the imported
|
|
89
|
+
* schema checks what this server has always checked:
|
|
90
|
+
*
|
|
91
|
+
* - `oneOf` is read as `anyOf`. Every `oneOf` in the manifest is a tagged
|
|
92
|
+
* union, so the two agree on real input, and `anyOf` cannot reject a value
|
|
93
|
+
* for matching two variants.
|
|
94
|
+
* - An object that declares properties and says nothing about others is
|
|
95
|
+
* closed, and so is one with a malformed `additionalProperties`.
|
|
96
|
+
* - The enforced formats gain the pattern from {@link FORMAT_PATTERNS}.
|
|
97
|
+
*
|
|
98
|
+
* A node using a shape {@link unsupportedKeywords} names throws instead.
|
|
99
|
+
* Every step leaves its own output alone, so running it twice on a node is the
|
|
100
|
+
* same as running it once.
|
|
101
|
+
*/
|
|
102
|
+
function tightenNode(node) {
|
|
103
|
+
const unsupported = unsupportedKeywords(node);
|
|
104
|
+
if (Arr.isReadonlyArrayNonEmpty(unsupported)) {
|
|
105
|
+
// oxlint-disable-next-line effect/avoid-untagged-errors -- a schema the importer cannot translate is a build-time programming error, not a runtime failure
|
|
106
|
+
throw new Error(`Unsupported OpenAPI schema shape: ${unsupported.join(", ")}. Teach src/openapi-schema.ts to translate it.`);
|
|
107
|
+
}
|
|
108
|
+
const { oneOf, ...rest } = node;
|
|
109
|
+
return formatPattern(closeObject(oneOf === undefined ? rest : { ...rest, anyOf: oneOf }));
|
|
110
|
+
}
|
|
111
|
+
function importNode(node) {
|
|
112
|
+
// SAFETY: a schema translated from JSON Schema is built from plain
|
|
113
|
+
// structural schemas and checks, none of which require a service.
|
|
114
|
+
return SchemaRepresentation.fromJsonSchemaDocument(EffectJsonSchema.fromSchemaOpenApi3_1(node), { patterns: "apply", onEnter: tightenNode });
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The Effect Schema for one OpenAPI JSON Schema node. Patterns in the document
|
|
118
|
+
* are applied, and {@link tightenNode} runs on every node first.
|
|
119
|
+
*
|
|
120
|
+
* Tool inputs go through this when `src/generated/openapi-inputs.ts` is
|
|
121
|
+
* generated, not at runtime, so a change here needs `bun run openapi:generate`.
|
|
122
|
+
* `openapi:inputs:check` fails until it runs.
|
|
123
|
+
*/
|
|
124
|
+
export function schemaFromOpenApi(schema) {
|
|
125
|
+
return importNode(tightenNode(schema));
|
|
126
|
+
}
|
|
127
|
+
/** The node's type, with `object` assumed for a node that declares properties. */
|
|
128
|
+
function relaxedType(schema) {
|
|
129
|
+
const declared = schema.type ?? (schema.properties === undefined ? undefined : "object");
|
|
130
|
+
return declared === undefined ? {} : { type: declared };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Relax a response JSON schema to a **structural, type-only** schema for output
|
|
134
|
+
* validation. The goal is a check that fails loud on a genuinely malformed
|
|
135
|
+
* payload (a field typed as the wrong primitive, `result` that is not an object
|
|
136
|
+
* at all) while never rejecting a well-formed real result.
|
|
137
|
+
*
|
|
138
|
+
* So it keeps each field's base `type` but drops value-level
|
|
139
|
+
* constraints, meaning `format`, `pattern`, length and range bounds, `enum`,
|
|
140
|
+
* and array-item schemas, and makes every object open with no required
|
|
141
|
+
* properties. Those constraints are the false-positive risk: they would fail a
|
|
142
|
+
* real tool call on a non-v4 uuid, an enum that drifted, or an omitted optional
|
|
143
|
+
* field, and they also break the pagination merge (which drops `nextCursor`
|
|
144
|
+
* and adds a `pagination` block).
|
|
145
|
+
*/
|
|
146
|
+
function relaxJsonSchema(schema) {
|
|
147
|
+
return {
|
|
148
|
+
...relaxedType(schema),
|
|
149
|
+
...optionalFields(schema.properties, (properties) => ({
|
|
150
|
+
properties: R.map(properties, relaxJsonSchema),
|
|
151
|
+
required: [],
|
|
152
|
+
additionalProperties: true,
|
|
153
|
+
})),
|
|
154
|
+
...optionalField("anyOf", schema.anyOf?.map(relaxJsonSchema)),
|
|
155
|
+
...optionalField("oneOf", schema.oneOf?.map(relaxJsonSchema)),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const UntrustedProvenance = Schema.Struct({
|
|
159
|
+
untrusted: Schema.Literal(true),
|
|
160
|
+
note: Schema.String,
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* The `structuredContent` fields for a tool: `{ result }`, plus an optional
|
|
164
|
+
* `provenance` marker for operations that carry lead-authored text. `result` is
|
|
165
|
+
* the operation's success-response body, relaxed by {@link relaxJsonSchema} so
|
|
166
|
+
* real results validate while malformed ones fail loud.
|
|
167
|
+
*/
|
|
168
|
+
export function openApiToolOutputFields(input) {
|
|
169
|
+
const result = input.body
|
|
170
|
+
? schemaFromOpenApi(relaxJsonSchema(input.body))
|
|
171
|
+
: Schema.Unknown;
|
|
172
|
+
return input.untrustedContent
|
|
173
|
+
? { result, provenance: Schema.optionalKey(UntrustedProvenance) }
|
|
174
|
+
: { result };
|
|
175
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as R from "effect/Record";
|
|
1
2
|
import { analyticsOperations } from "./groups/analytics.js";
|
|
2
3
|
import { bookingsOperations } from "./groups/bookings.js";
|
|
3
4
|
import { broadcastsOperations } from "./groups/broadcasts.js";
|
|
@@ -11,6 +12,7 @@ import { campaignsOperations } from "./groups/campaigns.js";
|
|
|
11
12
|
// summary) live in `manifest.ts`, so adding a route stays a one-entry edit in
|
|
12
13
|
// the file its group already owns.
|
|
13
14
|
import { capabilitiesOperations } from "./groups/capabilities.js";
|
|
15
|
+
import { channelsOperations } from "./groups/channels.js";
|
|
14
16
|
import { coachOperations } from "./groups/coach.js";
|
|
15
17
|
import { conversationsOperations } from "./groups/conversations.js";
|
|
16
18
|
import { insightsOperations } from "./groups/insights.js";
|
|
@@ -40,6 +42,7 @@ export const LUCA_OPERATIONS = [
|
|
|
40
42
|
...coachOperations,
|
|
41
43
|
...usageOperations,
|
|
42
44
|
...safetyOperations,
|
|
45
|
+
...channelsOperations,
|
|
43
46
|
...reportsOperations,
|
|
44
47
|
...insightsOperations,
|
|
45
48
|
...learningOperations,
|
|
@@ -48,3 +51,17 @@ export const LUCA_OPERATIONS = [
|
|
|
48
51
|
...cadencesOperations,
|
|
49
52
|
...analyticsOperations,
|
|
50
53
|
];
|
|
54
|
+
const OPERATIONS_BY_ID = R.fromEntries(LUCA_OPERATIONS.map((operation) => [operation.id, operation]));
|
|
55
|
+
/**
|
|
56
|
+
* The catalog entry for an id. Callers name ids in code, so a missing entry is
|
|
57
|
+
* a wiring bug rather than a runtime condition, and it throws where the first
|
|
58
|
+
* test that touches the caller will see it.
|
|
59
|
+
*/
|
|
60
|
+
export function operationById(id) {
|
|
61
|
+
const operation = OPERATIONS_BY_ID[id];
|
|
62
|
+
if (operation === undefined) {
|
|
63
|
+
// oxlint-disable-next-line effect/avoid-untagged-errors -- a missing catalog entry is a programming error, not a runtime failure
|
|
64
|
+
throw new Error(`Luca operation ${id} is not in the catalog.`);
|
|
65
|
+
}
|
|
66
|
+
return operation;
|
|
67
|
+
}
|
|
@@ -24,23 +24,27 @@ export const analyticsOperations = [
|
|
|
24
24
|
id: "analytics.callIntelligence",
|
|
25
25
|
title: "Read call intelligence",
|
|
26
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
27
|
}),
|
|
29
28
|
op({
|
|
30
29
|
id: "analytics.callIntelligenceCalls",
|
|
31
30
|
title: "List calls behind call intelligence",
|
|
32
31
|
description: "Most recent completed calls with a generated summary, newest first: the coach-facing summary, attendance outcome, rating, and lead name.",
|
|
33
|
-
|
|
32
|
+
// The summary is generated from what the lead said on the call and the
|
|
33
|
+
// lead name comes off their channel profile, so both are text the lead
|
|
34
|
+
// controls. Same reasoning as the `callEvents.summary.*` overrides.
|
|
35
|
+
untrustedContent: true,
|
|
34
36
|
}),
|
|
35
37
|
op({
|
|
36
38
|
id: "analytics.ghostedLeads",
|
|
37
39
|
title: "List ghosted leads",
|
|
38
40
|
description: "Ghosted-lead counts, rescue-recovery rate, lost-revenue estimate, and top conversation-killer clusters for a period.",
|
|
41
|
+
// Each top-killer cluster label is free text drawn from lead messages.
|
|
42
|
+
untrustedContent: true,
|
|
39
43
|
}),
|
|
40
44
|
op({
|
|
41
45
|
id: "analytics.trustScore",
|
|
42
46
|
title: "Read the trust score",
|
|
43
|
-
description: "Draft approval, edit, and rejection rates plus auto-send gate
|
|
47
|
+
description: "Draft approval, edit, and rejection rates plus the auto-send gate flag for a period.",
|
|
44
48
|
}),
|
|
45
49
|
op({
|
|
46
50
|
id: "analytics.leadScore",
|
|
@@ -86,7 +90,11 @@ export const analyticsOperations = [
|
|
|
86
90
|
id: "analytics.recommendations",
|
|
87
91
|
title: "Read recommendations",
|
|
88
92
|
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
|
-
|
|
93
|
+
// The recommender is handed a verbatim lead-message excerpt per
|
|
94
|
+
// conversation-killer cluster, which `packages/agents/src/recommendations.ts`
|
|
95
|
+
// labels UNTRUSTED and tells the model to use "for color", so the body is
|
|
96
|
+
// the one field here that can echo a lead's own words back out.
|
|
97
|
+
untrustedContent: true,
|
|
90
98
|
}),
|
|
91
99
|
op({
|
|
92
100
|
id: "analytics.benchmarkConsent",
|
|
@@ -97,7 +105,6 @@ export const analyticsOperations = [
|
|
|
97
105
|
id: "analytics.learningInsights",
|
|
98
106
|
title: "Read what Luca has learned",
|
|
99
107
|
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
108
|
}),
|
|
102
109
|
op({
|
|
103
110
|
id: "analytics.crmSyncHealth",
|
|
@@ -108,15 +115,18 @@ export const analyticsOperations = [
|
|
|
108
115
|
id: "analytics.objections",
|
|
109
116
|
title: "Read objection patterns",
|
|
110
117
|
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
|
-
|
|
118
|
+
// `topPhrases` is lifted verbatim out of the lead's own messages by
|
|
119
|
+
// `objectionRollup` in packages/db, so an objection a lead phrases as an
|
|
120
|
+
// instruction arrives here as one.
|
|
121
|
+
untrustedContent: true,
|
|
112
122
|
}),
|
|
113
123
|
op({
|
|
114
124
|
id: "analytics.objectionDrilldown",
|
|
115
125
|
title: "Drill into one objection type",
|
|
116
126
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
// The row-level half of the rollup above: `phrase` is the same verbatim
|
|
128
|
+
// lead quote, and `leadDisplayName` is whatever the lead set on their
|
|
129
|
+
// channel profile.
|
|
130
|
+
untrustedContent: true,
|
|
121
131
|
}),
|
|
122
132
|
];
|
|
@@ -4,19 +4,27 @@ export const bookingsOperations = [
|
|
|
4
4
|
id: "bookings.list",
|
|
5
5
|
title: "List bookings",
|
|
6
6
|
description: "List bookings. Query supports limit, cursor, status, and upcoming.",
|
|
7
|
-
|
|
7
|
+
// Each booking carries a cancellation reason the invitee may have written.
|
|
8
|
+
untrustedContent: true,
|
|
8
9
|
}),
|
|
9
10
|
op({
|
|
10
11
|
id: "bookings.create",
|
|
11
12
|
title: "Book a call",
|
|
13
|
+
// Moves the lead to booked and tells every webhook subscriber. Gated like
|
|
14
|
+
// reschedule and cancel so one approval covers each change to a lead's call.
|
|
15
|
+
confirm: "always",
|
|
16
|
+
// The booking webhook reaches subscribers outside Luca.
|
|
17
|
+
openWorld: true,
|
|
12
18
|
description: "Create a provider-neutral booking record. Body requires leadId and slotAt.",
|
|
13
|
-
|
|
19
|
+
// Each booking carries a cancellation reason the invitee may have written.
|
|
20
|
+
untrustedContent: true,
|
|
14
21
|
}),
|
|
15
22
|
op({
|
|
16
23
|
id: "bookings.get",
|
|
17
24
|
title: "Get a booking",
|
|
18
25
|
description: "Read one booking: its slot, status, provider, and lead.",
|
|
19
|
-
|
|
26
|
+
// Each booking carries a cancellation reason the invitee may have written.
|
|
27
|
+
untrustedContent: true,
|
|
20
28
|
}),
|
|
21
29
|
op({
|
|
22
30
|
id: "bookings.availability",
|
|
@@ -41,7 +49,6 @@ export const bookingsOperations = [
|
|
|
41
49
|
op({
|
|
42
50
|
id: "bookings.types.attendees",
|
|
43
51
|
title: "List a call type's attendees",
|
|
44
|
-
redactedFields: ["attendees[].email", "attendees[].name"],
|
|
45
52
|
description: "List the people invited to every call of this type, on top of the lead.",
|
|
46
53
|
}),
|
|
47
54
|
op({
|
|
@@ -68,60 +75,76 @@ export const bookingsOperations = [
|
|
|
68
75
|
id: "bookings.outcome.get",
|
|
69
76
|
title: "Get a booking's outcome",
|
|
70
77
|
description: "Read where the sale stands on a booking, plus the version you send back to correct it.",
|
|
71
|
-
redactedFields: ["episode.note"],
|
|
72
78
|
}),
|
|
73
79
|
op({
|
|
74
80
|
id: "bookings.outcome.history",
|
|
75
81
|
title: "Read a booking's outcome history",
|
|
76
82
|
description: "Read every outcome ever recorded on a booking, in order. Corrections append rather than overwrite.",
|
|
77
|
-
redactedFields: ["episode.note", "events[].note"],
|
|
78
83
|
}),
|
|
79
84
|
op({
|
|
80
85
|
id: "bookings.needsOutcome",
|
|
81
86
|
title: "List calls with no outcome recorded",
|
|
82
87
|
description: "List attended calls with no outcome recorded yet. The work list for closing revenue attribution.",
|
|
83
|
-
|
|
88
|
+
// Each row carries the lead's name, which the lead controls.
|
|
89
|
+
untrustedContent: true,
|
|
84
90
|
}),
|
|
85
91
|
op({
|
|
86
92
|
id: "bookings.lifecycleAttention",
|
|
87
93
|
title: "List bookings needing attention",
|
|
88
94
|
description: "List bookings where Luca's record and the provider's disagree and a human has to decide.",
|
|
89
|
-
|
|
95
|
+
// Each booking carries a cancellation reason the invitee may have written.
|
|
96
|
+
untrustedContent: true,
|
|
90
97
|
}),
|
|
91
98
|
op({
|
|
92
99
|
id: "bookings.brief",
|
|
93
100
|
title: "Read a pre-call brief",
|
|
94
101
|
description: "Read the pre-call brief: who the lead is, what they want, and the objections to expect.",
|
|
95
102
|
untrustedContent: true,
|
|
96
|
-
redactedFields: ["brief"],
|
|
97
103
|
}),
|
|
98
104
|
op({
|
|
99
105
|
id: "bookings.update",
|
|
100
106
|
title: "Reschedule or edit a booking",
|
|
107
|
+
openWorld: true,
|
|
108
|
+
mutatesExisting: true,
|
|
109
|
+
// Moves the call on the provider calendar and re-arms the lead's reminder.
|
|
110
|
+
confirm: "always",
|
|
101
111
|
description: "Reschedule or update booking status.",
|
|
102
|
-
|
|
112
|
+
// Each booking carries a cancellation reason the invitee may have written.
|
|
113
|
+
untrustedContent: true,
|
|
103
114
|
}),
|
|
104
115
|
op({
|
|
105
116
|
id: "bookings.cancel",
|
|
106
117
|
title: "Cancel a booking",
|
|
118
|
+
openWorld: true,
|
|
119
|
+
mutatesExisting: true,
|
|
120
|
+
// Cancels a real call on the lead's and the coach's calendars.
|
|
121
|
+
confirm: "always",
|
|
107
122
|
description: "Cancel a booking.",
|
|
108
|
-
|
|
123
|
+
// Each booking carries a cancellation reason the invitee may have written.
|
|
124
|
+
untrustedContent: true,
|
|
109
125
|
}),
|
|
110
126
|
op({
|
|
111
127
|
id: "bookings.outcome.record",
|
|
112
128
|
title: "Record a booking's outcome",
|
|
129
|
+
mutatesExisting: true,
|
|
113
130
|
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
131
|
}),
|
|
116
132
|
op({
|
|
117
133
|
id: "bookings.lifecycleResolution",
|
|
118
134
|
title: "Resolve a booking's lifecycle flag",
|
|
135
|
+
openWorld: true,
|
|
136
|
+
confirm: "always",
|
|
137
|
+
mutatesExisting: true,
|
|
119
138
|
description: "Settle one booking whose record disagrees with the provider's: retry the sync, dismiss the duplicate, or keep Luca's version.",
|
|
120
|
-
|
|
139
|
+
// Each booking carries a cancellation reason the invitee may have written.
|
|
140
|
+
untrustedContent: true,
|
|
121
141
|
}),
|
|
122
142
|
op({
|
|
123
143
|
id: "bookings.providerSync.retry",
|
|
124
144
|
title: "Retry a booking's provider sync",
|
|
145
|
+
openWorld: true,
|
|
146
|
+
confirm: "always",
|
|
147
|
+
mutatesExisting: true,
|
|
125
148
|
description: "Retry a failed write-back to the booking provider. A permanent failure returns 409 rather than retrying forever.",
|
|
126
149
|
}),
|
|
127
150
|
op({
|
|
@@ -132,6 +155,7 @@ export const bookingsOperations = [
|
|
|
132
155
|
op({
|
|
133
156
|
id: "bookings.guestLinks.revoke",
|
|
134
157
|
title: "Revoke a booking's guest links",
|
|
158
|
+
mutatesExisting: true,
|
|
135
159
|
description: "Kill every outstanding guest link on a booking at once.",
|
|
136
160
|
}),
|
|
137
161
|
op({
|
|
@@ -142,6 +166,7 @@ export const bookingsOperations = [
|
|
|
142
166
|
op({
|
|
143
167
|
id: "bookings.outcomeReasons.update",
|
|
144
168
|
title: "Update a loss reason",
|
|
169
|
+
mutatesExisting: true,
|
|
145
170
|
description: "Rename, reorder, or archive an outcome reason. Archiving hides it from the picker and leaves recorded outcomes alone.",
|
|
146
171
|
}),
|
|
147
172
|
op({
|
|
@@ -152,6 +177,7 @@ export const bookingsOperations = [
|
|
|
152
177
|
op({
|
|
153
178
|
id: "bookings.types.update",
|
|
154
179
|
title: "Update a booking type",
|
|
180
|
+
mutatesExisting: true,
|
|
155
181
|
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
182
|
}),
|
|
157
183
|
op({
|
|
@@ -162,28 +188,30 @@ export const bookingsOperations = [
|
|
|
162
188
|
op({
|
|
163
189
|
id: "bookings.types.schedule.replace",
|
|
164
190
|
title: "Replace a booking type schedule",
|
|
191
|
+
mutatesExisting: true,
|
|
165
192
|
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
193
|
}),
|
|
167
194
|
op({
|
|
168
195
|
id: "bookings.types.attendees.add",
|
|
169
196
|
title: "Add a booking type attendee",
|
|
170
|
-
redactedFields: ["attendees[].email", "attendees[].name"],
|
|
171
197
|
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
198
|
}),
|
|
173
199
|
op({
|
|
174
200
|
id: "bookings.types.attendees.remove",
|
|
175
201
|
title: "Remove a booking type attendee",
|
|
176
|
-
|
|
202
|
+
mutatesExisting: true,
|
|
177
203
|
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
204
|
}),
|
|
179
205
|
op({
|
|
180
206
|
id: "bookings.calendars.select",
|
|
181
207
|
title: "Select a booking calendar",
|
|
208
|
+
mutatesExisting: true,
|
|
182
209
|
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
210
|
}),
|
|
184
211
|
op({
|
|
185
212
|
id: "bookings.calendars.remove",
|
|
186
213
|
title: "Remove a booking calendar",
|
|
214
|
+
mutatesExisting: true,
|
|
187
215
|
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
216
|
}),
|
|
189
217
|
];
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { op } from "../registry.js";
|
|
2
|
+
// Operations that return a broadcast carry its recipient snapshot: each lead's
|
|
3
|
+
// display name and custom fields, which the lead wrote on their own profile.
|
|
4
|
+
// The coach-written message body alone would not need the untrusted frame.
|
|
2
5
|
export const broadcastsOperations = [
|
|
3
6
|
op({
|
|
4
7
|
id: "broadcasts.list",
|
|
@@ -8,99 +11,67 @@ export const broadcastsOperations = [
|
|
|
8
11
|
op({
|
|
9
12
|
id: "broadcasts.get",
|
|
10
13
|
title: "Get a broadcast",
|
|
14
|
+
// Recipient snapshot, see the note above this list.
|
|
15
|
+
untrustedContent: true,
|
|
11
16
|
description: "Get broadcast detail.",
|
|
12
|
-
redactedFields: [
|
|
13
|
-
"recipients[].messageBody",
|
|
14
|
-
"sampleMessages[].body",
|
|
15
|
-
"segmentSnapshot.excludedRecipients[].displayName",
|
|
16
|
-
"segmentSnapshot.leads[].customFields",
|
|
17
|
-
"segmentSnapshot.leads[].displayName",
|
|
18
|
-
],
|
|
19
17
|
}),
|
|
20
18
|
op({
|
|
21
19
|
id: "broadcasts.create",
|
|
22
20
|
title: "Create a broadcast",
|
|
21
|
+
// Recipient snapshot, see the note above this list.
|
|
22
|
+
untrustedContent: true,
|
|
23
23
|
description: "Create a broadcast draft. Body requires campaignId and name.",
|
|
24
|
-
redactedFields: [
|
|
25
|
-
"recipients[].messageBody",
|
|
26
|
-
"sampleMessages[].body",
|
|
27
|
-
"segmentSnapshot.excludedRecipients[].displayName",
|
|
28
|
-
"segmentSnapshot.leads[].customFields",
|
|
29
|
-
"segmentSnapshot.leads[].displayName",
|
|
30
|
-
],
|
|
31
24
|
}),
|
|
32
25
|
op({
|
|
33
26
|
id: "broadcasts.preview",
|
|
34
27
|
title: "Preview a broadcast",
|
|
28
|
+
mutatesExisting: true,
|
|
29
|
+
// Recipient snapshot, see the note above this list.
|
|
30
|
+
untrustedContent: true,
|
|
35
31
|
description: "Refresh broadcast recipient and sample-message preview.",
|
|
36
|
-
redactedFields: [
|
|
37
|
-
"recipients[].messageBody",
|
|
38
|
-
"sampleMessages[].body",
|
|
39
|
-
"segmentSnapshot.excludedRecipients[].displayName",
|
|
40
|
-
"segmentSnapshot.leads[].customFields",
|
|
41
|
-
"segmentSnapshot.leads[].displayName",
|
|
42
|
-
],
|
|
43
32
|
}),
|
|
44
33
|
op({
|
|
45
34
|
id: "broadcasts.approve",
|
|
46
35
|
title: "Approve a broadcast",
|
|
36
|
+
mutatesExisting: true,
|
|
37
|
+
// Recipient snapshot, see the note above this list.
|
|
38
|
+
untrustedContent: true,
|
|
47
39
|
description: "Approve a safe broadcast draft and snapshot recipients.",
|
|
48
|
-
redactedFields: [
|
|
49
|
-
"recipients[].messageBody",
|
|
50
|
-
"sampleMessages[].body",
|
|
51
|
-
"segmentSnapshot.excludedRecipients[].displayName",
|
|
52
|
-
"segmentSnapshot.leads[].customFields",
|
|
53
|
-
"segmentSnapshot.leads[].displayName",
|
|
54
|
-
],
|
|
55
40
|
}),
|
|
56
41
|
op({
|
|
57
42
|
id: "broadcasts.launch",
|
|
58
43
|
title: "Launch a broadcast",
|
|
59
|
-
|
|
44
|
+
openWorld: true,
|
|
45
|
+
mutatesExisting: true,
|
|
46
|
+
confirm: "always",
|
|
47
|
+
// Recipient snapshot, see the note above this list.
|
|
48
|
+
untrustedContent: true,
|
|
60
49
|
description: "Launch an approved broadcast. Sends messages to live leads.",
|
|
61
|
-
redactedFields: [
|
|
62
|
-
"recipients[].messageBody",
|
|
63
|
-
"sampleMessages[].body",
|
|
64
|
-
"segmentSnapshot.excludedRecipients[].displayName",
|
|
65
|
-
"segmentSnapshot.leads[].customFields",
|
|
66
|
-
"segmentSnapshot.leads[].displayName",
|
|
67
|
-
],
|
|
68
50
|
}),
|
|
69
51
|
op({
|
|
70
52
|
id: "broadcasts.pause",
|
|
71
53
|
title: "Pause a broadcast",
|
|
54
|
+
mutatesExisting: true,
|
|
55
|
+
// Recipient snapshot, see the note above this list.
|
|
56
|
+
untrustedContent: true,
|
|
72
57
|
description: "Pause a running broadcast with an optional operator reason.",
|
|
73
|
-
redactedFields: [
|
|
74
|
-
"recipients[].messageBody",
|
|
75
|
-
"sampleMessages[].body",
|
|
76
|
-
"segmentSnapshot.excludedRecipients[].displayName",
|
|
77
|
-
"segmentSnapshot.leads[].customFields",
|
|
78
|
-
"segmentSnapshot.leads[].displayName",
|
|
79
|
-
],
|
|
80
58
|
}),
|
|
81
59
|
op({
|
|
82
60
|
id: "broadcasts.retry",
|
|
83
61
|
title: "Retry a broadcast's failed sends",
|
|
84
|
-
|
|
62
|
+
openWorld: true,
|
|
63
|
+
mutatesExisting: true,
|
|
64
|
+
confirm: "always",
|
|
65
|
+
// Recipient snapshot, see the note above this list.
|
|
66
|
+
untrustedContent: true,
|
|
85
67
|
description: "Retry failed broadcast recipients after operator review. Re-sends messages to leads.",
|
|
86
|
-
redactedFields: [
|
|
87
|
-
"recipients[].messageBody",
|
|
88
|
-
"sampleMessages[].body",
|
|
89
|
-
"segmentSnapshot.excludedRecipients[].displayName",
|
|
90
|
-
"segmentSnapshot.leads[].customFields",
|
|
91
|
-
"segmentSnapshot.leads[].displayName",
|
|
92
|
-
],
|
|
93
68
|
}),
|
|
94
69
|
op({
|
|
95
70
|
id: "broadcasts.cancel",
|
|
96
71
|
title: "Cancel a broadcast",
|
|
72
|
+
mutatesExisting: true,
|
|
73
|
+
// Recipient snapshot, see the note above this list.
|
|
74
|
+
untrustedContent: true,
|
|
97
75
|
description: "Cancel a draft, approved, running, paused, or failed broadcast.",
|
|
98
|
-
redactedFields: [
|
|
99
|
-
"recipients[].messageBody",
|
|
100
|
-
"sampleMessages[].body",
|
|
101
|
-
"segmentSnapshot.excludedRecipients[].displayName",
|
|
102
|
-
"segmentSnapshot.leads[].customFields",
|
|
103
|
-
"segmentSnapshot.leads[].displayName",
|
|
104
|
-
],
|
|
105
76
|
}),
|
|
106
77
|
];
|
|
@@ -3,12 +3,14 @@ export const cadencesOperations = [
|
|
|
3
3
|
op({
|
|
4
4
|
id: "cadences.rescueStart",
|
|
5
5
|
title: "Start a silent-lead rescue",
|
|
6
|
-
|
|
6
|
+
openWorld: true,
|
|
7
|
+
confirm: "always",
|
|
7
8
|
description: "Enroll silent leads into the multi-day rescue cadence. Starts outbound touches to real leads.",
|
|
8
9
|
}),
|
|
9
10
|
op({
|
|
10
11
|
id: "cadences.pause",
|
|
11
12
|
title: "Pause a lead's cadence",
|
|
13
|
+
mutatesExisting: true,
|
|
12
14
|
description: "Pause (cancel) a lead's running rescue cadence with an optional reason.",
|
|
13
15
|
}),
|
|
14
16
|
];
|