@rcs-campaigns/agent-toolkit 0.1.5
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 +176 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +375 -0
- package/dist/client.d.ts +97 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +175 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +178 -0
- package/dist/openapi.d.ts +36 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +590 -0
- package/dist/tools.d.ts +93 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +1333 -0
- package/package.json +69 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,1333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TOOLS = exports.WEBHOOK_EVENT_NAMES = void 0;
|
|
4
|
+
exports.findTool = findTool;
|
|
5
|
+
exports.acceptsChannel = acceptsChannel;
|
|
6
|
+
exports.withDefaultChannel = withDefaultChannel;
|
|
7
|
+
/**
|
|
8
|
+
* The shared agent tool catalog.
|
|
9
|
+
*
|
|
10
|
+
* One declarative list of the gateway's capabilities — name, description, the
|
|
11
|
+
* JSON-Schema for inputs (used verbatim as the MCP `inputSchema`), and a pure
|
|
12
|
+
* function that turns validated args into a gateway RequestSpec. Both the MCP
|
|
13
|
+
* server (tools/list + tools/call) and the CLI build on this list, so adding a
|
|
14
|
+
* capability means editing ONE place and both skins gain it.
|
|
15
|
+
*/
|
|
16
|
+
const client_1 = require("./client");
|
|
17
|
+
/**
|
|
18
|
+
* Optional per-call channel, accepted by every tool that acts ON a channel.
|
|
19
|
+
*
|
|
20
|
+
* The gateway used to derive the channel from the org's signup brand alone, so
|
|
21
|
+
* a key belonging to a WhatsApp org could only ever drive the WhatsApp path —
|
|
22
|
+
* the RCS equivalents of these same tools were unreachable. Passing `channel`
|
|
23
|
+
* selects the path explicitly; the server validates it against the org's
|
|
24
|
+
* purchased add-on brands (403 when it is not entitled) and defaults to the
|
|
25
|
+
* org's own brand channel when omitted, so existing callers are unaffected.
|
|
26
|
+
*/
|
|
27
|
+
const CHANNEL_PROP = {
|
|
28
|
+
type: 'string',
|
|
29
|
+
enum: ['RCS', 'WHATSAPP'],
|
|
30
|
+
description: "Channel to act on (RCS or WHATSAPP). Defaults to your org's own brand channel; any other channel requires the matching add-on brand."
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The outbound webhook event types an endpoint may subscribe to — THE list,
|
|
34
|
+
* not a copy of one.
|
|
35
|
+
*
|
|
36
|
+
* It lives here because this is the package both ends can reach: the toolkit
|
|
37
|
+
* is a standalone npm dependency and cannot import from `apps/server`, but
|
|
38
|
+
* `apps/server` already depends on the toolkit (`workspace:*`) for `TOOLS` and
|
|
39
|
+
* the OpenAPI generator, so the arrow only points one way. `services/webhooks.ts`
|
|
40
|
+
* re-exports this array as `WEBHOOK_EVENTS`; there is no second array to keep
|
|
41
|
+
* in step, and **adding an event means editing this one list**.
|
|
42
|
+
*
|
|
43
|
+
* It used to be hand-listed here and hand-listed again in the server, kept
|
|
44
|
+
* honest by an exact-equality test across the two packages. That test could
|
|
45
|
+
* only fail AFTER a merge — a branch adding an eleventh event to one list and
|
|
46
|
+
* a branch adding the enum to the other are each green alone and red together,
|
|
47
|
+
* and `deploy.yml` runs the server suite before deploying, so the desync would
|
|
48
|
+
* have blocked the backend deploy rather than merely CI. Deriving removes the
|
|
49
|
+
* failure mode instead of detecting it later.
|
|
50
|
+
*
|
|
51
|
+
* The values are persisted on `WebhookEndpoint.events`, so a name here is a
|
|
52
|
+
* contract with every endpoint already registered: ADD, never rename.
|
|
53
|
+
*
|
|
54
|
+
* `contact.lead_captured` (MSG-1832) is the eleventh, appended never renamed.
|
|
55
|
+
*/
|
|
56
|
+
exports.WEBHOOK_EVENT_NAMES = [
|
|
57
|
+
// Surveys (the original two).
|
|
58
|
+
'survey.completed',
|
|
59
|
+
'response.received',
|
|
60
|
+
// Messaging. These are what make the platform integrable at all — a CRM or
|
|
61
|
+
// a Zapier trigger reacting to real delivery, not just to survey answers.
|
|
62
|
+
'message.received',
|
|
63
|
+
'message.delivered',
|
|
64
|
+
'message.failed',
|
|
65
|
+
'broadcast.completed',
|
|
66
|
+
'contact.opted_out',
|
|
67
|
+
// A recipient tapped a `webhook` suggested action. Delivered here rather
|
|
68
|
+
// than by POSTing the author's per-template URL directly: this path is
|
|
69
|
+
// already signed, retried and logged, and it keeps arbitrary
|
|
70
|
+
// author-supplied outbound requests off the backend entirely.
|
|
71
|
+
'action.tapped',
|
|
72
|
+
// Bots and journeys (MSG-1827 / MSG-1808): a person entered a flow, and a
|
|
73
|
+
// run ended (with its exit reason and collected vars).
|
|
74
|
+
'flow.started',
|
|
75
|
+
'flow.completed',
|
|
76
|
+
// Payments in chat (MSG-1831). Fired ONLY from the signed Stripe Connect
|
|
77
|
+
// webhook handler — never from the request-payment route and never from the
|
|
78
|
+
// click path, because those know only that we ASKED, not that money moved.
|
|
79
|
+
// There is deliberately no `payment.expired`: a lapsed link is nothing to
|
|
80
|
+
// notify a CRM about.
|
|
81
|
+
'payment.completed',
|
|
82
|
+
'payment.failed',
|
|
83
|
+
// A lead reached us from outside (MSG-1832) — a click-to-chat `ref:` inbound
|
|
84
|
+
// or a third-party form POSTing the lead webhook. APPENDED, never renamed:
|
|
85
|
+
// this array is a contract with every stored WebhookEndpoint.events value.
|
|
86
|
+
'contact.lead_captured'
|
|
87
|
+
];
|
|
88
|
+
exports.TOOLS = [
|
|
89
|
+
{
|
|
90
|
+
name: 'capabilities',
|
|
91
|
+
description: 'Describe the gateway: tool catalog, the scopes this key holds, and pricing posture. Call first to plan what you may do.',
|
|
92
|
+
scope: '(none)',
|
|
93
|
+
method: 'GET',
|
|
94
|
+
path: '/capabilities',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {},
|
|
98
|
+
additionalProperties: false
|
|
99
|
+
},
|
|
100
|
+
toRequest: () => ({ method: 'GET', path: '/capabilities' })
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'search_templates',
|
|
104
|
+
description: 'Search the published RCS template marketplace. Returns normalized listings (id, name, rating, cloneCount, categoryKeys).',
|
|
105
|
+
scope: 'templates:read',
|
|
106
|
+
method: 'GET',
|
|
107
|
+
path: '/templates',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
q: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'Free-text search over name + description.'
|
|
114
|
+
},
|
|
115
|
+
category: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
description: 'MarketplaceCategory key, e.g. EVENTS.'
|
|
118
|
+
},
|
|
119
|
+
channel: { type: 'string', enum: ['RCS', 'WHATSAPP', 'SMS'] },
|
|
120
|
+
sort: { type: 'string', enum: ['rating', 'used'], default: 'rating' },
|
|
121
|
+
page: { type: 'number', minimum: 1 },
|
|
122
|
+
pageSize: { type: 'number', minimum: 1, maximum: 100 }
|
|
123
|
+
},
|
|
124
|
+
additionalProperties: false
|
|
125
|
+
},
|
|
126
|
+
toRequest: (a) => ({
|
|
127
|
+
method: 'GET',
|
|
128
|
+
path: '/templates',
|
|
129
|
+
query: {
|
|
130
|
+
q: a.q,
|
|
131
|
+
category: a.category,
|
|
132
|
+
channel: a.channel,
|
|
133
|
+
sort: a.sort,
|
|
134
|
+
page: a.page,
|
|
135
|
+
pageSize: a.pageSize
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'get_template',
|
|
141
|
+
description: 'Fetch one template by id (a published marketplace listing or one your org owns).',
|
|
142
|
+
scope: 'templates:read',
|
|
143
|
+
method: 'GET',
|
|
144
|
+
path: '/templates/:id',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: { id: { type: 'string' } },
|
|
148
|
+
required: ['id'],
|
|
149
|
+
additionalProperties: false
|
|
150
|
+
},
|
|
151
|
+
toRequest: (a) => ({
|
|
152
|
+
method: 'GET',
|
|
153
|
+
path: `/templates/${encodeURIComponent(a.id)}`
|
|
154
|
+
})
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'render_template',
|
|
158
|
+
description: 'Validate a template and return the rendered RCS payload + variable-bound SMS/MMS fallback, without sending. Use to inspect before cloning/sending.',
|
|
159
|
+
scope: 'templates:read',
|
|
160
|
+
method: 'POST',
|
|
161
|
+
path: '/templates/:id/render',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
id: { type: 'string' },
|
|
166
|
+
variables: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
description: 'Merge fields ({{key}} -> value) to bind into the preview.',
|
|
169
|
+
additionalProperties: { type: 'string' }
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
required: ['id'],
|
|
173
|
+
additionalProperties: false
|
|
174
|
+
},
|
|
175
|
+
toRequest: (a) => ({
|
|
176
|
+
method: 'POST',
|
|
177
|
+
path: `/templates/${encodeURIComponent(a.id)}/render`,
|
|
178
|
+
body: { variables: a.variables ?? {} }
|
|
179
|
+
})
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'create_template',
|
|
183
|
+
description: 'Create a template draft in your org. Compose a TemplateContent (text/card/carousel) with a REQUIRED non-empty smsFallback; the shared RCS validator gates it. Submit it (submit_template) and await provider approval before send_message for rich RCS; an unsubmitted template delivers as SMS fallback.',
|
|
184
|
+
scope: 'templates:write',
|
|
185
|
+
method: 'POST',
|
|
186
|
+
path: '/templates',
|
|
187
|
+
successStatus: 201,
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
name: { type: 'string' },
|
|
192
|
+
description: { type: 'string' },
|
|
193
|
+
channel: { type: 'string', enum: ['RCS', 'WHATSAPP', 'SMS'] },
|
|
194
|
+
category: {
|
|
195
|
+
type: 'string',
|
|
196
|
+
enum: ['MARKETING', 'UTILITY', 'AUTHENTICATION']
|
|
197
|
+
},
|
|
198
|
+
content: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
description: 'TemplateContent. Must include a non-empty smsFallback. One of text/card/carousel is the primary body.'
|
|
201
|
+
},
|
|
202
|
+
tags: { type: 'array', items: { type: 'string' } }
|
|
203
|
+
},
|
|
204
|
+
required: ['name', 'content'],
|
|
205
|
+
additionalProperties: false
|
|
206
|
+
},
|
|
207
|
+
toRequest: (a) => ({
|
|
208
|
+
method: 'POST',
|
|
209
|
+
path: '/templates',
|
|
210
|
+
body: {
|
|
211
|
+
name: a.name,
|
|
212
|
+
description: a.description,
|
|
213
|
+
channel: a.channel,
|
|
214
|
+
category: a.category,
|
|
215
|
+
content: a.content,
|
|
216
|
+
tags: a.tags
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'update_template',
|
|
222
|
+
description: 'Partially update a template your org owns: name, description, category, tags, knowledgeDoc, and content (re-validated by the shared RCS validator — a non-empty smsFallback is still required). Only the fields you pass change. Editing the CONTENT of an already-approved template invalidates that approval: submit_template it again and await approval, or it delivers as SMS fallback until then. Use get_template first to edit incrementally rather than overwriting blind.',
|
|
223
|
+
scope: 'templates:write',
|
|
224
|
+
method: 'PUT',
|
|
225
|
+
path: '/templates/:id',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
id: { type: 'string' },
|
|
230
|
+
name: { type: 'string' },
|
|
231
|
+
description: { type: 'string' },
|
|
232
|
+
channel: {
|
|
233
|
+
type: 'string',
|
|
234
|
+
enum: ['RCS', 'WHATSAPP'],
|
|
235
|
+
description: "Move the template to another channel. Requires the matching add-on brand; omit to leave it where it is."
|
|
236
|
+
},
|
|
237
|
+
category: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
enum: ['MARKETING', 'UTILITY', 'AUTHENTICATION']
|
|
240
|
+
},
|
|
241
|
+
content: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
description: 'Full replacement TemplateContent (not a deep merge). Must include a non-empty smsFallback.'
|
|
244
|
+
},
|
|
245
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
246
|
+
knowledgeDoc: { type: 'string' }
|
|
247
|
+
},
|
|
248
|
+
required: ['id'],
|
|
249
|
+
additionalProperties: false
|
|
250
|
+
},
|
|
251
|
+
toRequest: (a) => ({
|
|
252
|
+
method: 'PUT',
|
|
253
|
+
path: `/templates/${encodeURIComponent(a.id)}`,
|
|
254
|
+
body: {
|
|
255
|
+
name: a.name,
|
|
256
|
+
description: a.description,
|
|
257
|
+
channel: a.channel,
|
|
258
|
+
category: a.category,
|
|
259
|
+
content: a.content,
|
|
260
|
+
tags: a.tags,
|
|
261
|
+
knowledgeDoc: a.knowledgeDoc
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'duplicate_template',
|
|
267
|
+
description: 'Duplicate a template your org OWNS into a fresh private draft (e.g. to iterate a variant without touching the original). The copy keeps the content, channel, category, tags and knowledge base, but starts unsubmitted — it has no provider approval of its own until you submit_template it. For copying someone else\'s PUBLISHED marketplace listing, use clone_template instead.',
|
|
268
|
+
scope: 'templates:write',
|
|
269
|
+
method: 'POST',
|
|
270
|
+
path: '/templates/:id/duplicate',
|
|
271
|
+
successStatus: 201,
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: 'object',
|
|
274
|
+
properties: { id: { type: 'string' } },
|
|
275
|
+
required: ['id'],
|
|
276
|
+
additionalProperties: false
|
|
277
|
+
},
|
|
278
|
+
toRequest: (a) => ({
|
|
279
|
+
method: 'POST',
|
|
280
|
+
path: `/templates/${encodeURIComponent(a.id)}/duplicate`
|
|
281
|
+
})
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'delete_template',
|
|
285
|
+
description: 'Delete a template your org owns. IRREVERSIBLE, and it also removes the template from the provider (Twilio), so an approved template loses its approval and can no longer be sent. Broadcasts that already went out are unaffected. Prefer this only for drafts and abandoned iterations; ask the user before deleting anything they have submitted or sent.',
|
|
286
|
+
scope: 'templates:write',
|
|
287
|
+
method: 'DELETE',
|
|
288
|
+
path: '/templates/:id',
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: { id: { type: 'string' } },
|
|
292
|
+
required: ['id'],
|
|
293
|
+
additionalProperties: false
|
|
294
|
+
},
|
|
295
|
+
toRequest: (a) => ({
|
|
296
|
+
method: 'DELETE',
|
|
297
|
+
path: `/templates/${encodeURIComponent(a.id)}`
|
|
298
|
+
})
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: 'clone_template',
|
|
302
|
+
description: 'Clone a published marketplace template into your org as a fresh private draft. Returns the new template. To deliver rich RCS rather than SMS fallback, submit_template it and await provider approval before send_message.',
|
|
303
|
+
scope: 'marketplace:clone',
|
|
304
|
+
method: 'POST',
|
|
305
|
+
path: '/marketplace/:id/clone',
|
|
306
|
+
successStatus: 201,
|
|
307
|
+
inputSchema: {
|
|
308
|
+
type: 'object',
|
|
309
|
+
properties: { id: { type: 'string' } },
|
|
310
|
+
required: ['id'],
|
|
311
|
+
additionalProperties: false
|
|
312
|
+
},
|
|
313
|
+
toRequest: (a) => ({
|
|
314
|
+
method: 'POST',
|
|
315
|
+
path: `/marketplace/${encodeURIComponent(a.id)}/clone`
|
|
316
|
+
})
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'submit_template',
|
|
320
|
+
description: 'Submit an org-owned template to the provider (Twilio) for approval. Returns the contentSid + approval status.',
|
|
321
|
+
scope: 'templates:submit',
|
|
322
|
+
method: 'POST',
|
|
323
|
+
path: '/templates/:id/submit',
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
properties: { id: { type: 'string' } },
|
|
327
|
+
required: ['id'],
|
|
328
|
+
additionalProperties: false
|
|
329
|
+
},
|
|
330
|
+
toRequest: (a) => ({
|
|
331
|
+
method: 'POST',
|
|
332
|
+
path: `/templates/${encodeURIComponent(a.id)}/submit`
|
|
333
|
+
})
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'send_message',
|
|
337
|
+
description: 'Send an org-owned template to a list of E.164 phone numbers (RCS-first with mandatory SMS/MMS fallback). Pass the template\'s blueprintId or id. The template sends as rich RCS only after submit_template + provider approval; until then it delivers as SMS fallback — the returned funnel\'s `fellBackToSms` count signals how many recipients got SMS instead of RCS. Bills your org. Returns the send funnel.',
|
|
338
|
+
scope: 'messages:send',
|
|
339
|
+
method: 'POST',
|
|
340
|
+
path: '/messages/send',
|
|
341
|
+
successStatus: 201,
|
|
342
|
+
inputSchema: {
|
|
343
|
+
type: 'object',
|
|
344
|
+
properties: {
|
|
345
|
+
blueprintId: { type: 'string' },
|
|
346
|
+
to: {
|
|
347
|
+
type: 'array',
|
|
348
|
+
items: { type: 'string' },
|
|
349
|
+
minItems: 1,
|
|
350
|
+
maxItems: 500
|
|
351
|
+
},
|
|
352
|
+
variables: { type: 'object', additionalProperties: { type: 'string' } },
|
|
353
|
+
name: { type: 'string' }
|
|
354
|
+
},
|
|
355
|
+
required: ['blueprintId', 'to'],
|
|
356
|
+
additionalProperties: false
|
|
357
|
+
},
|
|
358
|
+
toRequest: (a) => ({
|
|
359
|
+
method: 'POST',
|
|
360
|
+
path: '/messages/send',
|
|
361
|
+
body: {
|
|
362
|
+
blueprintId: a.blueprintId,
|
|
363
|
+
to: a.to,
|
|
364
|
+
variables: a.variables,
|
|
365
|
+
name: a.name
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
},
|
|
369
|
+
// ── Survey tools (the conversational-survey surface) ──
|
|
370
|
+
{
|
|
371
|
+
name: 'search_surveys',
|
|
372
|
+
description: 'List/search the surveys your org owns. Returns each survey with its recipients + responses counts.',
|
|
373
|
+
scope: 'surveys:read',
|
|
374
|
+
method: 'GET',
|
|
375
|
+
path: '/surveys',
|
|
376
|
+
inputSchema: {
|
|
377
|
+
type: 'object',
|
|
378
|
+
properties: {
|
|
379
|
+
q: { type: 'string', description: 'Free-text search over the name.' },
|
|
380
|
+
status: {
|
|
381
|
+
type: 'string',
|
|
382
|
+
enum: ['DRAFT', 'SCHEDULED', 'ACTIVE', 'CLOSED']
|
|
383
|
+
},
|
|
384
|
+
page: { type: 'number', minimum: 1 },
|
|
385
|
+
pageSize: { type: 'number', minimum: 1, maximum: 100 }
|
|
386
|
+
},
|
|
387
|
+
additionalProperties: false
|
|
388
|
+
},
|
|
389
|
+
toRequest: (a) => ({
|
|
390
|
+
method: 'GET',
|
|
391
|
+
path: '/surveys',
|
|
392
|
+
query: { q: a.q, status: a.status, page: a.page, pageSize: a.pageSize }
|
|
393
|
+
})
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: 'get_survey',
|
|
397
|
+
description: 'Fetch one survey your org owns by id (its full definition).',
|
|
398
|
+
scope: 'surveys:read',
|
|
399
|
+
method: 'GET',
|
|
400
|
+
path: '/surveys/:id',
|
|
401
|
+
inputSchema: {
|
|
402
|
+
type: 'object',
|
|
403
|
+
properties: { id: { type: 'string' } },
|
|
404
|
+
required: ['id'],
|
|
405
|
+
additionalProperties: false
|
|
406
|
+
},
|
|
407
|
+
toRequest: (a) => ({
|
|
408
|
+
method: 'GET',
|
|
409
|
+
path: `/surveys/${encodeURIComponent(a.id)}`
|
|
410
|
+
})
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: 'render_survey',
|
|
414
|
+
description: 'Preview a survey the way a respondent sees it: the text body + RCS chip titles for each question. Side-effect-free.',
|
|
415
|
+
scope: 'surveys:read',
|
|
416
|
+
method: 'POST',
|
|
417
|
+
path: '/surveys/:id/render',
|
|
418
|
+
inputSchema: {
|
|
419
|
+
type: 'object',
|
|
420
|
+
properties: { id: { type: 'string' } },
|
|
421
|
+
required: ['id'],
|
|
422
|
+
additionalProperties: false
|
|
423
|
+
},
|
|
424
|
+
toRequest: (a) => ({
|
|
425
|
+
method: 'POST',
|
|
426
|
+
path: `/surveys/${encodeURIComponent(a.id)}/render`
|
|
427
|
+
})
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: 'create_survey',
|
|
431
|
+
description: 'Create a survey draft in your org. Provide a SurveyDefinition (intro? + questions[] of type rating/single_select/text/comment); the shared validator gates it. PICTURES: every question should carry an `imageUrl`, and the definition should carry `headerImageUrl` (start picture) and `outroImageUrl` (finish picture). If the user provided no pictures, WEB-SEARCH for suitable, relevant images first and pass their https URLs — the server automatically re-hosts every image onto the platform GCS bucket, so external URLs are safe to pass and never hotlinked.',
|
|
432
|
+
scope: 'surveys:write',
|
|
433
|
+
method: 'POST',
|
|
434
|
+
path: '/surveys',
|
|
435
|
+
successStatus: 201,
|
|
436
|
+
inputSchema: {
|
|
437
|
+
type: 'object',
|
|
438
|
+
properties: {
|
|
439
|
+
name: { type: 'string' },
|
|
440
|
+
description: { type: 'string' },
|
|
441
|
+
definition: {
|
|
442
|
+
type: 'object',
|
|
443
|
+
description: 'SurveyDefinition: { intro?, outro?, questions: [{ questionId, prompt, type, options?, minRating?, maxRating?, required? }] }.'
|
|
444
|
+
},
|
|
445
|
+
startDate: { type: 'string', description: 'ISO date; schedules the survey.' },
|
|
446
|
+
endDate: { type: 'string' },
|
|
447
|
+
timezone: { type: 'string' },
|
|
448
|
+
languageCode: { type: 'array', items: { type: 'string' } },
|
|
449
|
+
isPreferencesSurvey: { type: 'boolean' },
|
|
450
|
+
channel: CHANNEL_PROP
|
|
451
|
+
},
|
|
452
|
+
required: ['name', 'definition'],
|
|
453
|
+
additionalProperties: false
|
|
454
|
+
},
|
|
455
|
+
toRequest: (a) => ({
|
|
456
|
+
method: 'POST',
|
|
457
|
+
path: '/surveys',
|
|
458
|
+
body: {
|
|
459
|
+
name: a.name,
|
|
460
|
+
description: a.description,
|
|
461
|
+
definition: a.definition,
|
|
462
|
+
startDate: a.startDate,
|
|
463
|
+
endDate: a.endDate,
|
|
464
|
+
timezone: a.timezone,
|
|
465
|
+
languageCode: a.languageCode,
|
|
466
|
+
isPreferencesSurvey: a.isPreferencesSurvey,
|
|
467
|
+
channel: a.channel
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: 'clone_survey',
|
|
473
|
+
description: 'Duplicate an org-owned survey into a fresh DRAFT (e.g. to iterate a variant). Returns the new survey.',
|
|
474
|
+
scope: 'marketplace:clone',
|
|
475
|
+
method: 'POST',
|
|
476
|
+
path: '/surveys/:id/clone',
|
|
477
|
+
successStatus: 201,
|
|
478
|
+
inputSchema: {
|
|
479
|
+
type: 'object',
|
|
480
|
+
properties: { id: { type: 'string' } },
|
|
481
|
+
required: ['id'],
|
|
482
|
+
additionalProperties: false
|
|
483
|
+
},
|
|
484
|
+
toRequest: (a) => ({
|
|
485
|
+
method: 'POST',
|
|
486
|
+
path: `/surveys/${encodeURIComponent(a.id)}/clone`
|
|
487
|
+
})
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: 'delete_survey',
|
|
491
|
+
description: 'Delete a survey your org owns. IRREVERSIBLE: its responses and sessions are deleted with it, so any results you have not already read are lost. Ask the user before deleting a survey that has been sent or has responses — read get_results first if in doubt.',
|
|
492
|
+
scope: 'surveys:write',
|
|
493
|
+
method: 'DELETE',
|
|
494
|
+
path: '/surveys/:id',
|
|
495
|
+
inputSchema: {
|
|
496
|
+
type: 'object',
|
|
497
|
+
properties: { id: { type: 'string' } },
|
|
498
|
+
required: ['id'],
|
|
499
|
+
additionalProperties: false
|
|
500
|
+
},
|
|
501
|
+
toRequest: (a) => ({
|
|
502
|
+
method: 'DELETE',
|
|
503
|
+
path: `/surveys/${encodeURIComponent(a.id)}`
|
|
504
|
+
})
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: 'submit_survey',
|
|
508
|
+
description: "Submit a survey for provider approval: on RCS its button-question templates, on WhatsApp its single Flow template. Returns the survey with the resulting approval state.",
|
|
509
|
+
scope: 'surveys:submit',
|
|
510
|
+
method: 'POST',
|
|
511
|
+
path: '/surveys/:id/submit',
|
|
512
|
+
inputSchema: {
|
|
513
|
+
type: 'object',
|
|
514
|
+
properties: { id: { type: 'string' }, channel: CHANNEL_PROP },
|
|
515
|
+
required: ['id'],
|
|
516
|
+
additionalProperties: false
|
|
517
|
+
},
|
|
518
|
+
toRequest: (a) => ({
|
|
519
|
+
method: 'POST',
|
|
520
|
+
path: `/surveys/${encodeURIComponent(a.id)}/submit`,
|
|
521
|
+
body: { channel: a.channel }
|
|
522
|
+
})
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: 'send_survey',
|
|
526
|
+
description: 'Send a survey to explicit E.164 numbers (the CRM-triggered single-customer send) and/or saved audiences. Flips the survey ACTIVE. Bills your org. Returns { recipients, sent }.',
|
|
527
|
+
scope: 'surveys:send',
|
|
528
|
+
method: 'POST',
|
|
529
|
+
path: '/surveys/:id/send',
|
|
530
|
+
inputSchema: {
|
|
531
|
+
type: 'object',
|
|
532
|
+
properties: {
|
|
533
|
+
id: { type: 'string' },
|
|
534
|
+
to: {
|
|
535
|
+
type: 'array',
|
|
536
|
+
items: { type: 'string' },
|
|
537
|
+
maxItems: 500,
|
|
538
|
+
description: 'E.164 destinations, e.g. ["+15551234567"].'
|
|
539
|
+
},
|
|
540
|
+
audienceIds: { type: 'array', items: { type: 'string' }, maxItems: 100 },
|
|
541
|
+
channel: CHANNEL_PROP
|
|
542
|
+
},
|
|
543
|
+
required: ['id'],
|
|
544
|
+
additionalProperties: false
|
|
545
|
+
},
|
|
546
|
+
toRequest: (a) => ({
|
|
547
|
+
method: 'POST',
|
|
548
|
+
path: `/surveys/${encodeURIComponent(a.id)}/send`,
|
|
549
|
+
body: { to: a.to, audienceIds: a.audienceIds, channel: a.channel }
|
|
550
|
+
})
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: 'update_survey',
|
|
554
|
+
description: 'Partially update a survey: name, description, definition (re-validated; editing a submitted WhatsApp survey requires resubmission), startDate/endDate, and audienceIds — setting audienceIds on a future-dated survey schedules the send to fire automatically at startDate. PICTURES: as with create_survey, keep every question\'s `imageUrl` plus `headerImageUrl`/`outroImageUrl` populated — web-search for images when none were provided; the server re-hosts all image URLs onto GCS.',
|
|
555
|
+
scope: 'surveys:write',
|
|
556
|
+
method: 'PATCH',
|
|
557
|
+
path: '/surveys/:id',
|
|
558
|
+
inputSchema: {
|
|
559
|
+
type: 'object',
|
|
560
|
+
properties: {
|
|
561
|
+
id: { type: 'string' },
|
|
562
|
+
name: { type: 'string' },
|
|
563
|
+
description: { type: 'string' },
|
|
564
|
+
definition: { type: 'object' },
|
|
565
|
+
status: {
|
|
566
|
+
type: 'string',
|
|
567
|
+
enum: ['DRAFT', 'SCHEDULED', 'ACTIVE', 'CLOSED']
|
|
568
|
+
},
|
|
569
|
+
startDate: { type: 'string' },
|
|
570
|
+
endDate: { type: 'string' },
|
|
571
|
+
audienceIds: { type: 'array', items: { type: 'string' } },
|
|
572
|
+
channel: CHANNEL_PROP
|
|
573
|
+
},
|
|
574
|
+
required: ['id'],
|
|
575
|
+
additionalProperties: false
|
|
576
|
+
},
|
|
577
|
+
toRequest: (a) => ({
|
|
578
|
+
method: 'PATCH',
|
|
579
|
+
path: `/surveys/${encodeURIComponent(a.id)}`,
|
|
580
|
+
body: {
|
|
581
|
+
name: a.name,
|
|
582
|
+
description: a.description,
|
|
583
|
+
definition: a.definition,
|
|
584
|
+
status: a.status,
|
|
585
|
+
startDate: a.startDate,
|
|
586
|
+
endDate: a.endDate,
|
|
587
|
+
audienceIds: a.audienceIds,
|
|
588
|
+
channel: a.channel
|
|
589
|
+
}
|
|
590
|
+
})
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
name: 'list_survey_sends',
|
|
594
|
+
description: "A survey's outbound send log (live + test sends), newest first, with per-message delivery status — including failed/stubbed attempts and the reason they did not deliver.",
|
|
595
|
+
scope: 'surveys:read',
|
|
596
|
+
method: 'GET',
|
|
597
|
+
path: '/surveys/:id/sends',
|
|
598
|
+
inputSchema: {
|
|
599
|
+
type: 'object',
|
|
600
|
+
properties: { id: { type: 'string' } },
|
|
601
|
+
required: ['id'],
|
|
602
|
+
additionalProperties: false
|
|
603
|
+
},
|
|
604
|
+
toRequest: (a) => ({
|
|
605
|
+
method: 'GET',
|
|
606
|
+
path: `/surveys/${encodeURIComponent(a.id)}/sends`
|
|
607
|
+
})
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
name: 'send_survey_test',
|
|
611
|
+
description: 'Send a one-off TEST of the survey to a single E.164 number (does not change status or audiences). Check list_survey_sends afterwards for the delivery outcome.',
|
|
612
|
+
scope: 'surveys:send',
|
|
613
|
+
method: 'POST',
|
|
614
|
+
path: '/surveys/:id/send-test',
|
|
615
|
+
inputSchema: {
|
|
616
|
+
type: 'object',
|
|
617
|
+
properties: {
|
|
618
|
+
id: { type: 'string' },
|
|
619
|
+
number: { type: 'string', description: 'E.164, e.g. +15551234567.' },
|
|
620
|
+
channel: CHANNEL_PROP
|
|
621
|
+
},
|
|
622
|
+
required: ['id', 'number'],
|
|
623
|
+
additionalProperties: false
|
|
624
|
+
},
|
|
625
|
+
toRequest: (a) => ({
|
|
626
|
+
method: 'POST',
|
|
627
|
+
path: `/surveys/${encodeURIComponent(a.id)}/send-test`,
|
|
628
|
+
body: { number: a.number, channel: a.channel }
|
|
629
|
+
})
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
name: 'get_results',
|
|
633
|
+
description: 'Read a survey’s aggregated results: per-question option counts / rating averages / text samples + completion totals (multi-part questions aggregate per part; free-text answers are auto-translated to English).',
|
|
634
|
+
scope: 'results:read',
|
|
635
|
+
method: 'GET',
|
|
636
|
+
path: '/surveys/:id/results',
|
|
637
|
+
inputSchema: {
|
|
638
|
+
type: 'object',
|
|
639
|
+
properties: { id: { type: 'string' } },
|
|
640
|
+
required: ['id'],
|
|
641
|
+
additionalProperties: false
|
|
642
|
+
},
|
|
643
|
+
toRequest: (a) => ({
|
|
644
|
+
method: 'GET',
|
|
645
|
+
path: `/surveys/${encodeURIComponent(a.id)}/results`
|
|
646
|
+
})
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: 'upload_media',
|
|
650
|
+
description: 'Store one picture on the platform GCS bucket and get its stable https URL — the only kind of URL survey/template media should use. Pass EITHER the image inline (base64 + mime) OR an https sourceUrl the server fetches and re-hosts (use this for web-searched pictures).',
|
|
651
|
+
scope: 'surveys:write',
|
|
652
|
+
method: 'POST',
|
|
653
|
+
path: '/media/upload',
|
|
654
|
+
successStatus: 201,
|
|
655
|
+
inputSchema: {
|
|
656
|
+
type: 'object',
|
|
657
|
+
properties: {
|
|
658
|
+
base64: {
|
|
659
|
+
type: 'string',
|
|
660
|
+
description: 'Raw base64 or data: URL of the image bytes.'
|
|
661
|
+
},
|
|
662
|
+
mime: {
|
|
663
|
+
type: 'string',
|
|
664
|
+
description: 'Image MIME type (required with base64), e.g. image/png.'
|
|
665
|
+
},
|
|
666
|
+
filename: { type: 'string' },
|
|
667
|
+
sourceUrl: {
|
|
668
|
+
type: 'string',
|
|
669
|
+
description: 'https image URL to fetch and re-host onto GCS.'
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
additionalProperties: false
|
|
673
|
+
},
|
|
674
|
+
toRequest: (a) => ({
|
|
675
|
+
method: 'POST',
|
|
676
|
+
path: '/media/upload',
|
|
677
|
+
body: {
|
|
678
|
+
base64: a.base64,
|
|
679
|
+
mime: a.mime,
|
|
680
|
+
filename: a.filename,
|
|
681
|
+
sourceUrl: a.sourceUrl
|
|
682
|
+
}
|
|
683
|
+
})
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
name: 'get_knowledge',
|
|
687
|
+
description: "Read a template's knowledge base: the grounding document text plus whether answering is enabled. Use before set_knowledge to edit a doc incrementally instead of overwriting it blind.",
|
|
688
|
+
scope: 'templates:read',
|
|
689
|
+
method: 'GET',
|
|
690
|
+
path: '/templates/:id/knowledge',
|
|
691
|
+
inputSchema: {
|
|
692
|
+
type: 'object',
|
|
693
|
+
properties: { id: { type: 'string' } },
|
|
694
|
+
required: ['id'],
|
|
695
|
+
additionalProperties: false
|
|
696
|
+
},
|
|
697
|
+
toRequest: (a) => ({
|
|
698
|
+
method: 'GET',
|
|
699
|
+
path: `/templates/${encodeURIComponent(a.id)}/knowledge`
|
|
700
|
+
})
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
name: 'set_knowledge',
|
|
704
|
+
description: 'Set/replace a template\'s knowledge base: the grounding document AI answers are generated from when recipients reply. Include operating instructions in the doc itself — e.g. "Any question not answered by this document must be answered exactly: …". Setting a doc re-enables answering.',
|
|
705
|
+
scope: 'templates:write',
|
|
706
|
+
method: 'PUT',
|
|
707
|
+
path: '/templates/:id/knowledge',
|
|
708
|
+
inputSchema: {
|
|
709
|
+
type: 'object',
|
|
710
|
+
properties: {
|
|
711
|
+
id: { type: 'string' },
|
|
712
|
+
doc: {
|
|
713
|
+
type: 'string',
|
|
714
|
+
description: 'The knowledge document (markdown or plain text).'
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
required: ['id', 'doc'],
|
|
718
|
+
additionalProperties: false
|
|
719
|
+
},
|
|
720
|
+
toRequest: (a) => ({
|
|
721
|
+
method: 'PUT',
|
|
722
|
+
path: `/templates/${encodeURIComponent(a.id)}/knowledge`,
|
|
723
|
+
body: { doc: a.doc }
|
|
724
|
+
})
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
name: 'delete_knowledge',
|
|
728
|
+
description: 'Delete a template\'s knowledge base document.',
|
|
729
|
+
scope: 'templates:write',
|
|
730
|
+
method: 'DELETE',
|
|
731
|
+
path: '/templates/:id/knowledge',
|
|
732
|
+
inputSchema: {
|
|
733
|
+
type: 'object',
|
|
734
|
+
properties: { id: { type: 'string' } },
|
|
735
|
+
required: ['id'],
|
|
736
|
+
additionalProperties: false
|
|
737
|
+
},
|
|
738
|
+
toRequest: (a) => ({
|
|
739
|
+
method: 'DELETE',
|
|
740
|
+
path: `/templates/${encodeURIComponent(a.id)}/knowledge`
|
|
741
|
+
})
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
name: 'enable_knowledge',
|
|
745
|
+
description: 'Enable knowledge answering on a template (the doc is used again).',
|
|
746
|
+
scope: 'templates:write',
|
|
747
|
+
method: 'POST',
|
|
748
|
+
path: '/templates/:id/knowledge/enable',
|
|
749
|
+
inputSchema: {
|
|
750
|
+
type: 'object',
|
|
751
|
+
properties: { id: { type: 'string' } },
|
|
752
|
+
required: ['id'],
|
|
753
|
+
additionalProperties: false
|
|
754
|
+
},
|
|
755
|
+
toRequest: (a) => ({
|
|
756
|
+
method: 'POST',
|
|
757
|
+
path: `/templates/${encodeURIComponent(a.id)}/knowledge/enable`
|
|
758
|
+
})
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
name: 'disable_knowledge',
|
|
762
|
+
description: 'Disable knowledge answering on a template WITHOUT deleting the doc (pause).',
|
|
763
|
+
scope: 'templates:write',
|
|
764
|
+
method: 'POST',
|
|
765
|
+
path: '/templates/:id/knowledge/disable',
|
|
766
|
+
inputSchema: {
|
|
767
|
+
type: 'object',
|
|
768
|
+
properties: { id: { type: 'string' } },
|
|
769
|
+
required: ['id'],
|
|
770
|
+
additionalProperties: false
|
|
771
|
+
},
|
|
772
|
+
toRequest: (a) => ({
|
|
773
|
+
method: 'POST',
|
|
774
|
+
path: `/templates/${encodeURIComponent(a.id)}/knowledge/disable`
|
|
775
|
+
})
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
name: 'get_usage',
|
|
779
|
+
description: 'Read your org’s month-to-date metered message usage.',
|
|
780
|
+
scope: 'usage:read',
|
|
781
|
+
method: 'GET',
|
|
782
|
+
path: '/usage',
|
|
783
|
+
inputSchema: {
|
|
784
|
+
type: 'object',
|
|
785
|
+
properties: {},
|
|
786
|
+
additionalProperties: false
|
|
787
|
+
},
|
|
788
|
+
toRequest: () => ({ method: 'GET', path: '/usage' })
|
|
789
|
+
},
|
|
790
|
+
// ── Audience tools (contact-list authoring, e.g. importing leads) ──
|
|
791
|
+
{
|
|
792
|
+
name: 'search_audiences',
|
|
793
|
+
description: 'List/search the audiences (saved contact lists) your org owns. Returns each audience with its name, source, and contactCount.',
|
|
794
|
+
scope: 'audiences:read',
|
|
795
|
+
method: 'GET',
|
|
796
|
+
path: '/audiences',
|
|
797
|
+
inputSchema: {
|
|
798
|
+
type: 'object',
|
|
799
|
+
properties: {
|
|
800
|
+
q: { type: 'string', description: 'Free-text search over the name.' },
|
|
801
|
+
page: { type: 'number', minimum: 1 },
|
|
802
|
+
pageSize: { type: 'number', minimum: 1, maximum: 100 }
|
|
803
|
+
},
|
|
804
|
+
additionalProperties: false
|
|
805
|
+
},
|
|
806
|
+
toRequest: (a) => ({
|
|
807
|
+
method: 'GET',
|
|
808
|
+
path: '/audiences',
|
|
809
|
+
query: { q: a.q, page: a.page, pageSize: a.pageSize }
|
|
810
|
+
})
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
name: 'get_audience',
|
|
814
|
+
description: 'Fetch one audience your org owns by id.',
|
|
815
|
+
scope: 'audiences:read',
|
|
816
|
+
method: 'GET',
|
|
817
|
+
path: '/audiences/:id',
|
|
818
|
+
inputSchema: {
|
|
819
|
+
type: 'object',
|
|
820
|
+
properties: { id: { type: 'string' } },
|
|
821
|
+
required: ['id'],
|
|
822
|
+
additionalProperties: false
|
|
823
|
+
},
|
|
824
|
+
toRequest: (a) => ({
|
|
825
|
+
method: 'GET',
|
|
826
|
+
path: `/audiences/${encodeURIComponent(a.id)}`
|
|
827
|
+
})
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: 'list_audience_contacts',
|
|
831
|
+
description: "Paginated list of an audience's contacts. Use `q` to find someone in a large imported batch instead of paging through it — it matches on the server, over the whole audience, so it keeps working on lists too big to page.",
|
|
832
|
+
scope: 'audiences:read',
|
|
833
|
+
method: 'GET',
|
|
834
|
+
path: '/audiences/:id/contacts',
|
|
835
|
+
inputSchema: {
|
|
836
|
+
type: 'object',
|
|
837
|
+
properties: {
|
|
838
|
+
id: { type: 'string' },
|
|
839
|
+
q: {
|
|
840
|
+
type: 'string',
|
|
841
|
+
maxLength: 120,
|
|
842
|
+
description: 'Free-text over number, names and email. A number matches however it is written — "(206) 300-0001" finds "+12063000001".'
|
|
843
|
+
},
|
|
844
|
+
page: { type: 'number', minimum: 1 },
|
|
845
|
+
pageSize: { type: 'number', minimum: 1, maximum: 200 }
|
|
846
|
+
},
|
|
847
|
+
required: ['id'],
|
|
848
|
+
additionalProperties: false
|
|
849
|
+
},
|
|
850
|
+
toRequest: (a) => ({
|
|
851
|
+
method: 'GET',
|
|
852
|
+
path: `/audiences/${encodeURIComponent(a.id)}/contacts`,
|
|
853
|
+
query: { q: a.q, page: a.page, pageSize: a.pageSize }
|
|
854
|
+
})
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
name: 'create_audience',
|
|
858
|
+
description: 'Create a saved audience (contact list) in your org, optionally seeded with contacts (e.g. leads parsed from a CSV). Every contact needs a reachable E.164-ish `number` — a contact with no valid number fails the whole create with a 400 (fix the row rather than silently dropping it). Use `attributes` for any extra CSV columns you want available as {{key}} merge fields later.',
|
|
859
|
+
scope: 'audiences:write',
|
|
860
|
+
method: 'POST',
|
|
861
|
+
path: '/audiences',
|
|
862
|
+
successStatus: 201,
|
|
863
|
+
inputSchema: {
|
|
864
|
+
type: 'object',
|
|
865
|
+
properties: {
|
|
866
|
+
name: { type: 'string' },
|
|
867
|
+
description: { type: 'string' },
|
|
868
|
+
source: { type: 'string', description: 'Free-text provenance, e.g. "Jaipur wedding planners leads.csv".' },
|
|
869
|
+
contacts: {
|
|
870
|
+
type: 'array',
|
|
871
|
+
items: {
|
|
872
|
+
type: 'object',
|
|
873
|
+
properties: {
|
|
874
|
+
firstName: { type: 'string' },
|
|
875
|
+
lastName: { type: 'string' },
|
|
876
|
+
number: { type: 'string', description: 'Phone number; required for the contact to be reachable.' },
|
|
877
|
+
email: { type: 'string' },
|
|
878
|
+
attributes: { type: 'object', additionalProperties: { type: 'string' } }
|
|
879
|
+
},
|
|
880
|
+
additionalProperties: false
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
required: ['name'],
|
|
885
|
+
additionalProperties: false
|
|
886
|
+
},
|
|
887
|
+
toRequest: (a) => ({
|
|
888
|
+
method: 'POST',
|
|
889
|
+
path: '/audiences',
|
|
890
|
+
body: {
|
|
891
|
+
name: a.name,
|
|
892
|
+
description: a.description,
|
|
893
|
+
source: a.source,
|
|
894
|
+
contacts: a.contacts
|
|
895
|
+
}
|
|
896
|
+
})
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
name: 'import_contacts',
|
|
900
|
+
description: 'Append contacts to an existing audience (e.g. more leads from a later CSV batch). Same reachability rule as create_audience: every contact needs a valid `number` or the whole import 400s.',
|
|
901
|
+
scope: 'audiences:write',
|
|
902
|
+
method: 'POST',
|
|
903
|
+
path: '/audiences/:id/contacts',
|
|
904
|
+
inputSchema: {
|
|
905
|
+
type: 'object',
|
|
906
|
+
properties: {
|
|
907
|
+
id: { type: 'string' },
|
|
908
|
+
contacts: {
|
|
909
|
+
type: 'array',
|
|
910
|
+
items: {
|
|
911
|
+
type: 'object',
|
|
912
|
+
properties: {
|
|
913
|
+
firstName: { type: 'string' },
|
|
914
|
+
lastName: { type: 'string' },
|
|
915
|
+
number: { type: 'string' },
|
|
916
|
+
email: { type: 'string' },
|
|
917
|
+
attributes: { type: 'object', additionalProperties: { type: 'string' } }
|
|
918
|
+
},
|
|
919
|
+
additionalProperties: false
|
|
920
|
+
},
|
|
921
|
+
minItems: 1
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
required: ['id', 'contacts'],
|
|
925
|
+
additionalProperties: false
|
|
926
|
+
},
|
|
927
|
+
toRequest: (a) => ({
|
|
928
|
+
method: 'POST',
|
|
929
|
+
path: `/audiences/${encodeURIComponent(a.id)}/contacts`,
|
|
930
|
+
body: { contacts: a.contacts }
|
|
931
|
+
})
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
name: 'delete_audience',
|
|
935
|
+
description: 'Delete an audience and all of its contacts (org-scoped, irreversible).',
|
|
936
|
+
scope: 'audiences:write',
|
|
937
|
+
method: 'DELETE',
|
|
938
|
+
path: '/audiences/:id',
|
|
939
|
+
inputSchema: {
|
|
940
|
+
type: 'object',
|
|
941
|
+
properties: { id: { type: 'string' } },
|
|
942
|
+
required: ['id'],
|
|
943
|
+
additionalProperties: false
|
|
944
|
+
},
|
|
945
|
+
toRequest: (a) => ({
|
|
946
|
+
method: 'DELETE',
|
|
947
|
+
path: `/audiences/${encodeURIComponent(a.id)}`
|
|
948
|
+
})
|
|
949
|
+
},
|
|
950
|
+
// ── Conversation tools (the Inbox: read a thread, answer it, hand it over) ──
|
|
951
|
+
{
|
|
952
|
+
name: 'list_conversations',
|
|
953
|
+
description: 'List the org\'s conversation threads (one per phone number), newest activity first. Archived threads are hidden. Filter by free text, unread, unassigned, a specific assignee, or the campaign a thread was messaged in.',
|
|
954
|
+
scope: 'conversations:read',
|
|
955
|
+
method: 'GET',
|
|
956
|
+
path: '/conversations',
|
|
957
|
+
inputSchema: {
|
|
958
|
+
type: 'object',
|
|
959
|
+
properties: {
|
|
960
|
+
search: {
|
|
961
|
+
type: 'string',
|
|
962
|
+
description: 'Match the number or the resolved display name.'
|
|
963
|
+
},
|
|
964
|
+
unread: {
|
|
965
|
+
type: 'boolean',
|
|
966
|
+
description: 'Only threads with unanswered arrivals (unreadCount > 0). Org-wide, not per-viewer: a key is not a person and has no read state.'
|
|
967
|
+
},
|
|
968
|
+
unassigned: { type: 'boolean', description: 'Only threads nobody owns.' },
|
|
969
|
+
assignedTo: {
|
|
970
|
+
type: 'string',
|
|
971
|
+
description: 'Only threads owned by this Clerk user id.'
|
|
972
|
+
},
|
|
973
|
+
broadcastId: {
|
|
974
|
+
type: 'string',
|
|
975
|
+
description: 'Only threads this campaign was sent to.'
|
|
976
|
+
},
|
|
977
|
+
page: { type: 'number', minimum: 1 },
|
|
978
|
+
pageSize: { type: 'number', minimum: 1, maximum: 100 }
|
|
979
|
+
},
|
|
980
|
+
additionalProperties: false
|
|
981
|
+
},
|
|
982
|
+
toRequest: (a) => ({
|
|
983
|
+
method: 'GET',
|
|
984
|
+
path: '/conversations',
|
|
985
|
+
query: {
|
|
986
|
+
search: a.search,
|
|
987
|
+
unread: a.unread,
|
|
988
|
+
unassigned: a.unassigned,
|
|
989
|
+
assignedTo: a.assignedTo,
|
|
990
|
+
broadcastId: a.broadcastId,
|
|
991
|
+
page: a.page,
|
|
992
|
+
pageSize: a.pageSize
|
|
993
|
+
}
|
|
994
|
+
})
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
name: 'get_conversation',
|
|
998
|
+
description: 'Read one thread in a single call: its meta, the most recent messages (ascending), the contact summary (marketing status, tags, audiences) and whether the WhatsApp 24h service window is open. Internal team notes are deliberately NOT included.',
|
|
999
|
+
scope: 'conversations:read',
|
|
1000
|
+
method: 'GET',
|
|
1001
|
+
path: '/conversations/:id',
|
|
1002
|
+
inputSchema: {
|
|
1003
|
+
type: 'object',
|
|
1004
|
+
properties: {
|
|
1005
|
+
id: { type: 'string' },
|
|
1006
|
+
limit: {
|
|
1007
|
+
type: 'number',
|
|
1008
|
+
minimum: 1,
|
|
1009
|
+
maximum: 200,
|
|
1010
|
+
default: 50,
|
|
1011
|
+
description: 'How many of the most recent messages to include.'
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
required: ['id'],
|
|
1015
|
+
additionalProperties: false
|
|
1016
|
+
},
|
|
1017
|
+
toRequest: (a) => ({
|
|
1018
|
+
method: 'GET',
|
|
1019
|
+
path: `/conversations/${encodeURIComponent(a.id)}`,
|
|
1020
|
+
query: { limit: a.limit }
|
|
1021
|
+
})
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
name: 'reply',
|
|
1025
|
+
description: 'Reply into a thread — free text (with an optional image) or an approved template. Reaches a real phone and bills your org. Every gate an operator hits applies unchanged: an opted-out number is refused (409), the org allowance is enforced, WhatsApp free text needs the 24h window open (an approved template otherwise), and an RCS send always carries its SMS fallback. When the thread has an outbound language set, free text is translated into it first, exactly as the human composer does.',
|
|
1026
|
+
scope: 'conversations:write',
|
|
1027
|
+
method: 'POST',
|
|
1028
|
+
path: '/conversations/:id/reply',
|
|
1029
|
+
successStatus: 201,
|
|
1030
|
+
inputSchema: {
|
|
1031
|
+
type: 'object',
|
|
1032
|
+
properties: {
|
|
1033
|
+
id: { type: 'string' },
|
|
1034
|
+
kind: { type: 'string', enum: ['text', 'template'] },
|
|
1035
|
+
text: {
|
|
1036
|
+
type: 'string',
|
|
1037
|
+
maxLength: 4096,
|
|
1038
|
+
description: 'kind=text: the body. May be empty when mediaUrl carries the message.'
|
|
1039
|
+
},
|
|
1040
|
+
mediaUrl: {
|
|
1041
|
+
type: 'string',
|
|
1042
|
+
description: 'kind=text: a public https image URL to attach.'
|
|
1043
|
+
},
|
|
1044
|
+
blueprintId: {
|
|
1045
|
+
type: 'string',
|
|
1046
|
+
description: "kind=template: the org template to send."
|
|
1047
|
+
},
|
|
1048
|
+
variables: {
|
|
1049
|
+
type: 'object',
|
|
1050
|
+
additionalProperties: { type: 'string' },
|
|
1051
|
+
description: 'kind=template: values for the template\'s {{variables}}.'
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
required: ['id', 'kind'],
|
|
1055
|
+
additionalProperties: false
|
|
1056
|
+
},
|
|
1057
|
+
toRequest: (a) => ({
|
|
1058
|
+
method: 'POST',
|
|
1059
|
+
path: `/conversations/${encodeURIComponent(a.id)}/reply`,
|
|
1060
|
+
body: {
|
|
1061
|
+
kind: a.kind,
|
|
1062
|
+
text: a.text,
|
|
1063
|
+
mediaUrl: a.mediaUrl,
|
|
1064
|
+
blueprintId: a.blueprintId,
|
|
1065
|
+
variables: a.variables
|
|
1066
|
+
}
|
|
1067
|
+
})
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
name: 'assign_conversation',
|
|
1071
|
+
description: 'Give a thread an owner, or release it back to the pool with `userId: null`. The assignee is a bare Clerk user id and is NOT validated against the org roster — a stale id degrades to "assigned to someone who left", visibly, rather than to an outage in a one-field write.',
|
|
1072
|
+
scope: 'conversations:write',
|
|
1073
|
+
method: 'POST',
|
|
1074
|
+
path: '/conversations/:id/assign',
|
|
1075
|
+
inputSchema: {
|
|
1076
|
+
type: 'object',
|
|
1077
|
+
properties: {
|
|
1078
|
+
id: { type: 'string' },
|
|
1079
|
+
userId: {
|
|
1080
|
+
type: ['string', 'null'],
|
|
1081
|
+
description: 'Clerk user id to hand the thread to; null releases it.'
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
required: ['id', 'userId'],
|
|
1085
|
+
additionalProperties: false
|
|
1086
|
+
},
|
|
1087
|
+
toRequest: (a) => ({
|
|
1088
|
+
method: 'POST',
|
|
1089
|
+
path: `/conversations/${encodeURIComponent(a.id)}/assign`,
|
|
1090
|
+
body: { userId: a.userId ?? null }
|
|
1091
|
+
})
|
|
1092
|
+
},
|
|
1093
|
+
// ── Webhook tools (subscribe yourself to platform events) ──
|
|
1094
|
+
{
|
|
1095
|
+
name: 'list_webhooks',
|
|
1096
|
+
description: "The org's registered outbound webhook endpoints. The signing secret is redacted — it is shown only once, by create_webhook.",
|
|
1097
|
+
scope: 'webhooks:manage',
|
|
1098
|
+
method: 'GET',
|
|
1099
|
+
path: '/webhooks',
|
|
1100
|
+
inputSchema: {
|
|
1101
|
+
type: 'object',
|
|
1102
|
+
properties: {},
|
|
1103
|
+
additionalProperties: false
|
|
1104
|
+
},
|
|
1105
|
+
toRequest: () => ({ method: 'GET', path: '/webhooks' })
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
name: 'create_webhook',
|
|
1109
|
+
description: 'Register an HTTPS endpoint to receive signed platform events. The response carries the signing `secret` EXACTLY ONCE — store it now; every later read redacts it and there is no way to retrieve it again. Each delivery is signed `X-RCS-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "<t>.<rawBody>">`.',
|
|
1110
|
+
scope: 'webhooks:manage',
|
|
1111
|
+
method: 'POST',
|
|
1112
|
+
path: '/webhooks',
|
|
1113
|
+
successStatus: 201,
|
|
1114
|
+
inputSchema: {
|
|
1115
|
+
type: 'object',
|
|
1116
|
+
properties: {
|
|
1117
|
+
url: { type: 'string', description: 'HTTPS endpoint to POST events to.' },
|
|
1118
|
+
events: {
|
|
1119
|
+
type: 'array',
|
|
1120
|
+
minItems: 1,
|
|
1121
|
+
items: { type: 'string', enum: [...exports.WEBHOOK_EVENT_NAMES] },
|
|
1122
|
+
description: 'Event types to subscribe to.'
|
|
1123
|
+
},
|
|
1124
|
+
description: { type: 'string', maxLength: 200 }
|
|
1125
|
+
},
|
|
1126
|
+
required: ['url', 'events'],
|
|
1127
|
+
additionalProperties: false
|
|
1128
|
+
},
|
|
1129
|
+
toRequest: (a) => ({
|
|
1130
|
+
method: 'POST',
|
|
1131
|
+
path: '/webhooks',
|
|
1132
|
+
body: { url: a.url, events: a.events, description: a.description }
|
|
1133
|
+
})
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
name: 'update_webhook',
|
|
1137
|
+
description: 'Update an endpoint\'s url, subscribed events, description, or active flag. Only the fields you pass change. The signing secret is never returned or rotated here.',
|
|
1138
|
+
scope: 'webhooks:manage',
|
|
1139
|
+
method: 'PUT',
|
|
1140
|
+
path: '/webhooks/:id',
|
|
1141
|
+
inputSchema: {
|
|
1142
|
+
type: 'object',
|
|
1143
|
+
properties: {
|
|
1144
|
+
id: { type: 'string' },
|
|
1145
|
+
url: { type: 'string' },
|
|
1146
|
+
events: {
|
|
1147
|
+
type: 'array',
|
|
1148
|
+
minItems: 1,
|
|
1149
|
+
items: { type: 'string', enum: [...exports.WEBHOOK_EVENT_NAMES] }
|
|
1150
|
+
},
|
|
1151
|
+
description: { type: ['string', 'null'], maxLength: 200 },
|
|
1152
|
+
active: { type: 'boolean' }
|
|
1153
|
+
},
|
|
1154
|
+
required: ['id'],
|
|
1155
|
+
additionalProperties: false
|
|
1156
|
+
},
|
|
1157
|
+
toRequest: (a) => ({
|
|
1158
|
+
method: 'PUT',
|
|
1159
|
+
path: `/webhooks/${encodeURIComponent(a.id)}`,
|
|
1160
|
+
body: {
|
|
1161
|
+
url: a.url,
|
|
1162
|
+
events: a.events,
|
|
1163
|
+
description: a.description,
|
|
1164
|
+
active: a.active
|
|
1165
|
+
}
|
|
1166
|
+
})
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
name: 'delete_webhook',
|
|
1170
|
+
description: 'Remove a webhook endpoint. Its delivery history is deleted with it; anything still queued for it stops.',
|
|
1171
|
+
scope: 'webhooks:manage',
|
|
1172
|
+
method: 'DELETE',
|
|
1173
|
+
path: '/webhooks/:id',
|
|
1174
|
+
inputSchema: {
|
|
1175
|
+
type: 'object',
|
|
1176
|
+
properties: { id: { type: 'string' } },
|
|
1177
|
+
required: ['id'],
|
|
1178
|
+
additionalProperties: false
|
|
1179
|
+
},
|
|
1180
|
+
toRequest: (a) => ({
|
|
1181
|
+
method: 'DELETE',
|
|
1182
|
+
path: `/webhooks/${encodeURIComponent(a.id)}`
|
|
1183
|
+
})
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
name: 'test_webhook',
|
|
1187
|
+
description: 'Fire a synthetic signed event at one endpoint to verify the receiver. Returns the delivery id and whether it was accepted.',
|
|
1188
|
+
scope: 'webhooks:manage',
|
|
1189
|
+
method: 'POST',
|
|
1190
|
+
path: '/webhooks/:id/test',
|
|
1191
|
+
inputSchema: {
|
|
1192
|
+
type: 'object',
|
|
1193
|
+
properties: { id: { type: 'string' } },
|
|
1194
|
+
required: ['id'],
|
|
1195
|
+
additionalProperties: false
|
|
1196
|
+
},
|
|
1197
|
+
toRequest: (a) => ({
|
|
1198
|
+
method: 'POST',
|
|
1199
|
+
path: `/webhooks/${encodeURIComponent(a.id)}/test`
|
|
1200
|
+
})
|
|
1201
|
+
},
|
|
1202
|
+
// ── Contact tools (the org-level person record behind every audience) ──
|
|
1203
|
+
{
|
|
1204
|
+
name: 'list_contacts',
|
|
1205
|
+
description: "Search the org's contact records (one person per number, across every audience). Returns consent, opt-out, number verdict, marketing status, merge fields and tags.",
|
|
1206
|
+
scope: 'contacts:read',
|
|
1207
|
+
method: 'GET',
|
|
1208
|
+
path: '/contacts',
|
|
1209
|
+
inputSchema: {
|
|
1210
|
+
type: 'object',
|
|
1211
|
+
properties: {
|
|
1212
|
+
q: {
|
|
1213
|
+
type: 'string',
|
|
1214
|
+
maxLength: 120,
|
|
1215
|
+
description: 'Free-text over number, names and email.'
|
|
1216
|
+
},
|
|
1217
|
+
status: {
|
|
1218
|
+
type: 'string',
|
|
1219
|
+
enum: ['SUBSCRIBED', 'UNSUBSCRIBED', 'UNDELIVERABLE', 'UNKNOWN'],
|
|
1220
|
+
description: 'Marketing status: SUBSCRIBED (consent, not opted out), UNSUBSCRIBED (opted out — sticky), UNDELIVERABLE (number known unreachable), UNKNOWN (no consent on record).'
|
|
1221
|
+
},
|
|
1222
|
+
tag: { type: 'string', maxLength: 64 },
|
|
1223
|
+
audienceId: {
|
|
1224
|
+
type: 'string',
|
|
1225
|
+
description: 'Only people who are members of this audience.'
|
|
1226
|
+
},
|
|
1227
|
+
page: { type: 'number', minimum: 1 },
|
|
1228
|
+
pageSize: { type: 'number', minimum: 1, maximum: 100 }
|
|
1229
|
+
},
|
|
1230
|
+
additionalProperties: false
|
|
1231
|
+
},
|
|
1232
|
+
toRequest: (a) => ({
|
|
1233
|
+
method: 'GET',
|
|
1234
|
+
path: '/contacts',
|
|
1235
|
+
query: {
|
|
1236
|
+
q: a.q,
|
|
1237
|
+
status: a.status,
|
|
1238
|
+
tag: a.tag,
|
|
1239
|
+
audienceId: a.audienceId,
|
|
1240
|
+
page: a.page,
|
|
1241
|
+
pageSize: a.pageSize
|
|
1242
|
+
}
|
|
1243
|
+
})
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
name: 'get_contact',
|
|
1247
|
+
description: 'Fetch one contact by its public contact id, with the audiences they belong to.',
|
|
1248
|
+
scope: 'contacts:read',
|
|
1249
|
+
method: 'GET',
|
|
1250
|
+
path: '/contacts/:id',
|
|
1251
|
+
inputSchema: {
|
|
1252
|
+
type: 'object',
|
|
1253
|
+
properties: { id: { type: 'string' } },
|
|
1254
|
+
required: ['id'],
|
|
1255
|
+
additionalProperties: false
|
|
1256
|
+
},
|
|
1257
|
+
toRequest: (a) => ({
|
|
1258
|
+
method: 'GET',
|
|
1259
|
+
path: `/contacts/${encodeURIComponent(a.id)}`
|
|
1260
|
+
})
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
name: 'update_contact',
|
|
1264
|
+
description: "Correct a contact's names, email, merge fields (`attributes`, a whole-map replace) or tags. Consent and opt-out are deliberately NOT reachable from here — recording or clearing them is a compliance-sensitive action that stays with a human operator.",
|
|
1265
|
+
scope: 'contacts:write',
|
|
1266
|
+
method: 'PATCH',
|
|
1267
|
+
path: '/contacts/:id',
|
|
1268
|
+
inputSchema: {
|
|
1269
|
+
type: 'object',
|
|
1270
|
+
properties: {
|
|
1271
|
+
id: { type: 'string' },
|
|
1272
|
+
firstName: { type: ['string', 'null'], maxLength: 120 },
|
|
1273
|
+
lastName: { type: ['string', 'null'], maxLength: 120 },
|
|
1274
|
+
email: { type: ['string', 'null'], maxLength: 200 },
|
|
1275
|
+
attributes: {
|
|
1276
|
+
type: ['object', 'null'],
|
|
1277
|
+
additionalProperties: { type: 'string' },
|
|
1278
|
+
description: 'Whole-map replace of the {{merge field}} values.'
|
|
1279
|
+
},
|
|
1280
|
+
tags: { type: 'array', items: { type: 'string' }, maxItems: 50 }
|
|
1281
|
+
},
|
|
1282
|
+
required: ['id'],
|
|
1283
|
+
additionalProperties: false
|
|
1284
|
+
},
|
|
1285
|
+
toRequest: (a) => ({
|
|
1286
|
+
method: 'PATCH',
|
|
1287
|
+
path: `/contacts/${encodeURIComponent(a.id)}`,
|
|
1288
|
+
body: {
|
|
1289
|
+
firstName: a.firstName,
|
|
1290
|
+
lastName: a.lastName,
|
|
1291
|
+
email: a.email,
|
|
1292
|
+
attributes: a.attributes,
|
|
1293
|
+
tags: a.tags
|
|
1294
|
+
}
|
|
1295
|
+
})
|
|
1296
|
+
}
|
|
1297
|
+
];
|
|
1298
|
+
/** Look up a tool by name. */
|
|
1299
|
+
function findTool(name) {
|
|
1300
|
+
return exports.TOOLS.find((t) => t.name === name);
|
|
1301
|
+
}
|
|
1302
|
+
/** Does this tool accept a `channel` argument at all? */
|
|
1303
|
+
function acceptsChannel(tool) {
|
|
1304
|
+
return Object.prototype.hasOwnProperty.call(tool.inputSchema.properties ?? {}, 'channel');
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Fill in the skin's default channel for a call that accepts one and did not
|
|
1308
|
+
* specify it.
|
|
1309
|
+
*
|
|
1310
|
+
* Both skins run the same catalog against the same gateway, so the only thing
|
|
1311
|
+
* that should differ is which channel you get when you don't ask: the `wa` bins
|
|
1312
|
+
* assume WhatsApp, the `rcs` bins stay silent and let the server fall back to
|
|
1313
|
+
* the org's own brand channel. An explicit `channel` from the caller always
|
|
1314
|
+
* wins, and tools that take no channel are returned untouched — so this can be
|
|
1315
|
+
* applied unconditionally at the CLI/MCP boundary.
|
|
1316
|
+
*/
|
|
1317
|
+
function withDefaultChannel(tool, args, defaultChannel) {
|
|
1318
|
+
if (!acceptsChannel(tool))
|
|
1319
|
+
return args;
|
|
1320
|
+
if (args.channel !== undefined && args.channel !== null) {
|
|
1321
|
+
// An explicit channel wins — but a brand id typed in its place (legacy
|
|
1322
|
+
// `wa-templates` or canonical `wa-campaigns`, MSG-1867 phase 3) is resolved
|
|
1323
|
+
// to the canonical channel enum the gateway expects. Unknown values pass
|
|
1324
|
+
// through untouched so the server can reject them with its own message.
|
|
1325
|
+
const canonical = (0, client_1.normalizeChannel)(args.channel);
|
|
1326
|
+
return canonical && canonical !== args.channel
|
|
1327
|
+
? { ...args, channel: canonical }
|
|
1328
|
+
: args;
|
|
1329
|
+
}
|
|
1330
|
+
if (!defaultChannel)
|
|
1331
|
+
return args;
|
|
1332
|
+
return { ...args, channel: defaultChannel };
|
|
1333
|
+
}
|