@sellable/mcp 0.1.318 → 0.1.320
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 +17 -0
- package/dist/generated/column-schema-manifest.js +1 -1
- package/dist/index-dev.js +0 -0
- package/dist/index.js +0 -0
- package/dist/server.js +28 -0
- package/dist/tools/blueprint-commit.js +1 -1
- package/dist/tools/inbox.d.ts +308 -0
- package/dist/tools/inbox.js +576 -0
- package/dist/tools/linkedin.d.ts +76 -0
- package/dist/tools/linkedin.js +351 -18
- package/dist/tools/readiness.js +44 -13
- package/dist/tools/registry.d.ts +239 -0
- package/dist/tools/registry.js +2 -0
- package/package.json +1 -1
- package/skills/building-gtm-tables/SKILL.md +26 -0
- package/skills/building-gtm-tables/references/column-type-catalog.md +30 -1
- package/skills/building-gtm-tables/references/common-blueprints.fixtures.ts +54 -5
- package/skills/building-gtm-tables/references/common-blueprints.md +9 -0
- package/skills/create-campaign/SKILL.md +8 -0
- package/skills/create-campaign-v2/SKILL.md +7 -0
- package/skills/research/config.json +9 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { getApi, SellableApiError } from "../api.js";
|
|
3
|
+
const snapshotSchema = {
|
|
4
|
+
type: "object",
|
|
5
|
+
properties: {
|
|
6
|
+
latestMessageTimestamp: { type: ["string", "null"] },
|
|
7
|
+
lastInboundMessageAt: { type: ["string", "null"] },
|
|
8
|
+
updatedAt: { type: ["string", "null"] },
|
|
9
|
+
reasonCodes: { type: "array", items: { type: "string" } },
|
|
10
|
+
},
|
|
11
|
+
additionalProperties: false,
|
|
12
|
+
};
|
|
13
|
+
const resultOutputSchema = {
|
|
14
|
+
type: "object",
|
|
15
|
+
additionalProperties: true,
|
|
16
|
+
};
|
|
17
|
+
export const inboxToolDefinitions = [
|
|
18
|
+
{
|
|
19
|
+
name: "search_inbox_threads",
|
|
20
|
+
description: "Search LinkedIn inbox threads using the existing Sellable product inbox route. This calls GET /api/v3/inbox and returns thread summaries for the active workspace.",
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
limit: { type: "number", minimum: 1, maximum: 50 },
|
|
25
|
+
cursor: { type: "string" },
|
|
26
|
+
filter: { type: "string", enum: ["all", "drafts"] },
|
|
27
|
+
status: {
|
|
28
|
+
type: "string",
|
|
29
|
+
enum: ["all", "unread", "replied", "awaiting"],
|
|
30
|
+
},
|
|
31
|
+
senderId: { type: "string" },
|
|
32
|
+
classification: { type: "string" },
|
|
33
|
+
search: { type: "string" },
|
|
34
|
+
includeEnrichment: { type: "boolean" },
|
|
35
|
+
includeReplyEligibility: { type: "boolean" },
|
|
36
|
+
},
|
|
37
|
+
required: [],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
},
|
|
40
|
+
outputSchema: resultOutputSchema,
|
|
41
|
+
annotations: {
|
|
42
|
+
readOnlyHint: true,
|
|
43
|
+
destructiveHint: false,
|
|
44
|
+
openWorldHint: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "get_inbox_thread",
|
|
49
|
+
description: "Fetch one LinkedIn inbox thread using the existing Sellable product route GET /api/v3/inbox/[threadId]. Returns the product payload plus a chronological transcript for MCP review.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
threadId: { type: "string" },
|
|
54
|
+
},
|
|
55
|
+
required: ["threadId"],
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
},
|
|
58
|
+
outputSchema: resultOutputSchema,
|
|
59
|
+
annotations: {
|
|
60
|
+
readOnlyHint: true,
|
|
61
|
+
destructiveHint: false,
|
|
62
|
+
openWorldHint: false,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "check_inbox_reply_eligibility",
|
|
67
|
+
description: "Load one inbox thread with includeReplyEligibility=true through GET /api/v3/inbox/[threadId] and return the product-computed reply eligibility snapshot.",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
threadId: { type: "string" },
|
|
72
|
+
},
|
|
73
|
+
required: ["threadId"],
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
outputSchema: resultOutputSchema,
|
|
77
|
+
annotations: {
|
|
78
|
+
readOnlyHint: true,
|
|
79
|
+
destructiveHint: false,
|
|
80
|
+
openWorldHint: false,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "update_inbox_draft",
|
|
85
|
+
description: "Edit an existing inbox draft through PATCH /api/v3/inbox/[threadId]/draft, then re-fetch the thread and return an approval-required persisted draft snapshot. This does not create a new draft.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
threadId: { type: "string" },
|
|
90
|
+
versionId: { type: "string" },
|
|
91
|
+
body: { type: "string" },
|
|
92
|
+
},
|
|
93
|
+
required: ["threadId", "versionId", "body"],
|
|
94
|
+
additionalProperties: false,
|
|
95
|
+
},
|
|
96
|
+
outputSchema: resultOutputSchema,
|
|
97
|
+
annotations: {
|
|
98
|
+
readOnlyHint: false,
|
|
99
|
+
destructiveHint: false,
|
|
100
|
+
openWorldHint: false,
|
|
101
|
+
idempotentHint: false,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "send_inbox_draft",
|
|
106
|
+
description: "Send the currently approved existing inbox draft through POST /api/v3/inbox/[threadId]/send after validating the exact approved body hash, draft version, idempotency key, and thread snapshot. No inline body is sent.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
threadId: { type: "string" },
|
|
111
|
+
versionId: { type: "string" },
|
|
112
|
+
approvedBody: { type: "string" },
|
|
113
|
+
approvedBodyHash: { type: "string" },
|
|
114
|
+
approvedThreadSnapshot: snapshotSchema,
|
|
115
|
+
idempotencyKey: { type: "string" },
|
|
116
|
+
approval: { type: "string", enum: ["approved"] },
|
|
117
|
+
},
|
|
118
|
+
required: [
|
|
119
|
+
"threadId",
|
|
120
|
+
"versionId",
|
|
121
|
+
"approvedBody",
|
|
122
|
+
"approvedBodyHash",
|
|
123
|
+
"approvedThreadSnapshot",
|
|
124
|
+
"idempotencyKey",
|
|
125
|
+
"approval",
|
|
126
|
+
],
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
},
|
|
129
|
+
outputSchema: resultOutputSchema,
|
|
130
|
+
annotations: {
|
|
131
|
+
readOnlyHint: false,
|
|
132
|
+
destructiveHint: false,
|
|
133
|
+
openWorldHint: true,
|
|
134
|
+
idempotentHint: false,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "send_inbox_manual_reply",
|
|
139
|
+
description: "Send one exact operator-approved manual reply through POST /api/v3/inbox/[threadId]/reply after validating the message hash, idempotency key, and thread snapshot. Batch sends are not supported.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
threadId: { type: "string" },
|
|
144
|
+
message: { type: "string" },
|
|
145
|
+
approvedMessageHash: { type: "string" },
|
|
146
|
+
approvedThreadSnapshot: snapshotSchema,
|
|
147
|
+
idempotencyKey: { type: "string" },
|
|
148
|
+
approval: { type: "string", enum: ["approved"] },
|
|
149
|
+
},
|
|
150
|
+
required: [
|
|
151
|
+
"threadId",
|
|
152
|
+
"message",
|
|
153
|
+
"approvedMessageHash",
|
|
154
|
+
"approvedThreadSnapshot",
|
|
155
|
+
"idempotencyKey",
|
|
156
|
+
"approval",
|
|
157
|
+
],
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
},
|
|
160
|
+
outputSchema: resultOutputSchema,
|
|
161
|
+
annotations: {
|
|
162
|
+
readOnlyHint: false,
|
|
163
|
+
destructiveHint: false,
|
|
164
|
+
openWorldHint: true,
|
|
165
|
+
idempotentHint: false,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
function hashText(value) {
|
|
170
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
171
|
+
}
|
|
172
|
+
function textResult(structuredContent, text, isError = false) {
|
|
173
|
+
return {
|
|
174
|
+
structuredContent,
|
|
175
|
+
content: [{ type: "text", text }],
|
|
176
|
+
...(isError ? { isError: true } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function errorResult(error, message, extra = {}) {
|
|
180
|
+
return textResult({ ok: false, error, message, ...extra }, message, true);
|
|
181
|
+
}
|
|
182
|
+
function requireString(value, field) {
|
|
183
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
184
|
+
return errorResult("invalid_arguments", `${field} is required.`);
|
|
185
|
+
}
|
|
186
|
+
return value.trim();
|
|
187
|
+
}
|
|
188
|
+
function requireStrings(input, fields) {
|
|
189
|
+
const values = {};
|
|
190
|
+
for (const field of fields) {
|
|
191
|
+
const value = requireString(input[field], field);
|
|
192
|
+
if (typeof value !== "string") {
|
|
193
|
+
return { ok: false, result: value };
|
|
194
|
+
}
|
|
195
|
+
values[field] = value;
|
|
196
|
+
}
|
|
197
|
+
return { ok: true, values };
|
|
198
|
+
}
|
|
199
|
+
function normalizeTimestamp(value) {
|
|
200
|
+
if (value == null)
|
|
201
|
+
return null;
|
|
202
|
+
if (value instanceof Date)
|
|
203
|
+
return value.toISOString();
|
|
204
|
+
if (typeof value === "string")
|
|
205
|
+
return value;
|
|
206
|
+
return String(value);
|
|
207
|
+
}
|
|
208
|
+
function getThread(response) {
|
|
209
|
+
return response.thread ?? null;
|
|
210
|
+
}
|
|
211
|
+
function getDraft(thread) {
|
|
212
|
+
return thread?.draft ?? null;
|
|
213
|
+
}
|
|
214
|
+
function getDraftVersionId(draft) {
|
|
215
|
+
return typeof draft?.id === "string"
|
|
216
|
+
? draft.id
|
|
217
|
+
: typeof draft?.versionId === "string"
|
|
218
|
+
? draft.versionId
|
|
219
|
+
: null;
|
|
220
|
+
}
|
|
221
|
+
function buildThreadSnapshot(thread) {
|
|
222
|
+
const reasonCodes = Array.isArray(thread?.replyEligibility?.reasonCodes)
|
|
223
|
+
? thread.replyEligibility.reasonCodes
|
|
224
|
+
: [];
|
|
225
|
+
return {
|
|
226
|
+
latestMessageTimestamp: normalizeTimestamp(thread?.latestMessageTimestamp),
|
|
227
|
+
lastInboundMessageAt: normalizeTimestamp(thread?.lastInboundMessageAt),
|
|
228
|
+
updatedAt: normalizeTimestamp(thread?.updatedAt),
|
|
229
|
+
reasonCodes,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function snapshotsMatch(current, approved) {
|
|
233
|
+
if (!approved || typeof approved !== "object")
|
|
234
|
+
return false;
|
|
235
|
+
const snapshot = approved;
|
|
236
|
+
return (current.latestMessageTimestamp ===
|
|
237
|
+
(snapshot.latestMessageTimestamp ?? null) &&
|
|
238
|
+
current.lastInboundMessageAt === (snapshot.lastInboundMessageAt ?? null));
|
|
239
|
+
}
|
|
240
|
+
function chronologicalTranscript(thread) {
|
|
241
|
+
const messages = Array.isArray(thread?.messages) ? thread.messages : [];
|
|
242
|
+
return [...messages].sort((a, b) => {
|
|
243
|
+
const aTime = new Date(normalizeTimestamp(a.timestamp) ?? 0).getTime();
|
|
244
|
+
const bTime = new Date(normalizeTimestamp(b.timestamp) ?? 0).getTime();
|
|
245
|
+
return aTime - bTime;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function routeError(error, fallback) {
|
|
249
|
+
if (error instanceof SellableApiError) {
|
|
250
|
+
let parsed = null;
|
|
251
|
+
try {
|
|
252
|
+
parsed = JSON.parse(error.body);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
parsed = error.body;
|
|
256
|
+
}
|
|
257
|
+
return errorResult(fallback, error.message, {
|
|
258
|
+
status: error.status,
|
|
259
|
+
productError: parsed,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const message = error instanceof Error ? error.message : fallback;
|
|
263
|
+
return errorResult(fallback, message);
|
|
264
|
+
}
|
|
265
|
+
function appendParam(params, key, value) {
|
|
266
|
+
if (value === undefined || value === null || value === "")
|
|
267
|
+
return;
|
|
268
|
+
params.set(key, String(value));
|
|
269
|
+
}
|
|
270
|
+
export async function searchInboxThreads(input) {
|
|
271
|
+
const params = new URLSearchParams();
|
|
272
|
+
const limit = typeof input.limit === "number" && Number.isFinite(input.limit)
|
|
273
|
+
? Math.max(1, Math.min(Math.trunc(input.limit), 50))
|
|
274
|
+
: undefined;
|
|
275
|
+
appendParam(params, "limit", limit);
|
|
276
|
+
appendParam(params, "cursor", input.cursor);
|
|
277
|
+
appendParam(params, "filter", input.filter);
|
|
278
|
+
appendParam(params, "status", input.status);
|
|
279
|
+
appendParam(params, "senderId", input.senderId);
|
|
280
|
+
appendParam(params, "classification", input.classification);
|
|
281
|
+
appendParam(params, "search", input.search);
|
|
282
|
+
if (typeof input.includeEnrichment === "boolean") {
|
|
283
|
+
params.set("includeEnrichment", String(input.includeEnrichment));
|
|
284
|
+
}
|
|
285
|
+
if (typeof input.includeReplyEligibility === "boolean") {
|
|
286
|
+
params.set("includeReplyEligibility", String(input.includeReplyEligibility));
|
|
287
|
+
}
|
|
288
|
+
const query = params.toString();
|
|
289
|
+
const routeCalled = `/api/v3/inbox${query ? `?${query}` : ""}`;
|
|
290
|
+
const response = await getApi().get(routeCalled);
|
|
291
|
+
const threads = Array.isArray(response.threads) ? response.threads : [];
|
|
292
|
+
return textResult({
|
|
293
|
+
routeCalled,
|
|
294
|
+
...response,
|
|
295
|
+
}, `${threads.length} inbox thread${threads.length === 1 ? "" : "s"} returned.`);
|
|
296
|
+
}
|
|
297
|
+
export async function getInboxThread(input) {
|
|
298
|
+
const required = requireStrings(input, ["threadId"]);
|
|
299
|
+
if (!required.ok)
|
|
300
|
+
return required.result;
|
|
301
|
+
const { threadId } = required.values;
|
|
302
|
+
const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}`;
|
|
303
|
+
const response = await getApi().get(routeCalled);
|
|
304
|
+
const thread = getThread(response);
|
|
305
|
+
return textResult({
|
|
306
|
+
routeCalled,
|
|
307
|
+
thread,
|
|
308
|
+
messageOrder: thread?.messageOrder ?? "route_order",
|
|
309
|
+
chronologicalTranscript: chronologicalTranscript(thread),
|
|
310
|
+
}, thread
|
|
311
|
+
? `Loaded inbox thread ${thread.id ?? threadId}.`
|
|
312
|
+
: `Loaded inbox thread ${threadId}.`);
|
|
313
|
+
}
|
|
314
|
+
export async function checkInboxReplyEligibility(input) {
|
|
315
|
+
const required = requireStrings(input, ["threadId"]);
|
|
316
|
+
if (!required.ok)
|
|
317
|
+
return required.result;
|
|
318
|
+
const { threadId } = required.values;
|
|
319
|
+
const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}?includeReplyEligibility=true`;
|
|
320
|
+
const response = await getApi().get(routeCalled);
|
|
321
|
+
const thread = getThread(response);
|
|
322
|
+
if (!thread?.replyEligibility) {
|
|
323
|
+
return errorResult("reply_eligibility_missing", "Product inbox detail response did not include replyEligibility.", { routeCalled, threadId });
|
|
324
|
+
}
|
|
325
|
+
return textResult({
|
|
326
|
+
routeCalled,
|
|
327
|
+
threadId,
|
|
328
|
+
replyEligibility: thread.replyEligibility,
|
|
329
|
+
draftSnapshot: thread.draft
|
|
330
|
+
? {
|
|
331
|
+
threadId,
|
|
332
|
+
versionId: getDraftVersionId(thread.draft),
|
|
333
|
+
body: thread.draft.body,
|
|
334
|
+
bodyHash: typeof thread.draft.body === "string"
|
|
335
|
+
? hashText(thread.draft.body)
|
|
336
|
+
: null,
|
|
337
|
+
versionNumber: thread.draft.versionNumber ?? null,
|
|
338
|
+
}
|
|
339
|
+
: null,
|
|
340
|
+
threadSnapshot: buildThreadSnapshot(thread),
|
|
341
|
+
}, `Eligibility loaded for inbox thread ${threadId}.`);
|
|
342
|
+
}
|
|
343
|
+
export async function updateInboxDraft(input) {
|
|
344
|
+
const required = requireStrings(input, ["threadId", "versionId", "body"]);
|
|
345
|
+
if (!required.ok)
|
|
346
|
+
return required.result;
|
|
347
|
+
const { threadId, versionId, body } = required.values;
|
|
348
|
+
const bodyHash = hashText(body);
|
|
349
|
+
const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}/draft`;
|
|
350
|
+
try {
|
|
351
|
+
const patchResponse = await getApi().patch(routeCalled, {
|
|
352
|
+
versionId,
|
|
353
|
+
body,
|
|
354
|
+
mcpApproval: {
|
|
355
|
+
toolName: "update_inbox_draft",
|
|
356
|
+
threadId,
|
|
357
|
+
versionId,
|
|
358
|
+
bodyHash,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
const detail = await getApi().get(`/api/v3/inbox/${encodeURIComponent(threadId)}`);
|
|
362
|
+
const thread = getThread(detail);
|
|
363
|
+
const draft = getDraft(thread);
|
|
364
|
+
const currentVersionId = getDraftVersionId(draft);
|
|
365
|
+
const returnedVersionId = typeof patchResponse.versionId === "string"
|
|
366
|
+
? patchResponse.versionId
|
|
367
|
+
: currentVersionId;
|
|
368
|
+
const returnedBody = typeof patchResponse.body === "string" ? patchResponse.body : null;
|
|
369
|
+
const currentBody = typeof draft?.body === "string" ? draft.body : null;
|
|
370
|
+
if (!returnedVersionId || !returnedBody) {
|
|
371
|
+
return errorResult("persisted_draft_snapshot_missing", "Could not verify the persisted edited draft after PATCH.", { routeCalled, threadId, versionId: returnedVersionId ?? null });
|
|
372
|
+
}
|
|
373
|
+
if (!draft || returnedVersionId !== currentVersionId) {
|
|
374
|
+
return textResult({
|
|
375
|
+
routeCalled,
|
|
376
|
+
approvalRequired: false,
|
|
377
|
+
status: "applied_but_superseded",
|
|
378
|
+
threadId,
|
|
379
|
+
versionId: returnedVersionId,
|
|
380
|
+
appliedDraftSnapshot: {
|
|
381
|
+
threadId,
|
|
382
|
+
versionId: returnedVersionId,
|
|
383
|
+
versionNumber: patchResponse.versionNumber ?? null,
|
|
384
|
+
body: returnedBody,
|
|
385
|
+
bodyHash: hashText(returnedBody),
|
|
386
|
+
bodyLength: returnedBody.length,
|
|
387
|
+
routeCalled,
|
|
388
|
+
snapshotAt: new Date().toISOString(),
|
|
389
|
+
},
|
|
390
|
+
currentDraftSnapshot: draft && currentVersionId && currentBody
|
|
391
|
+
? {
|
|
392
|
+
threadId,
|
|
393
|
+
versionId: currentVersionId,
|
|
394
|
+
versionNumber: draft.versionNumber ?? null,
|
|
395
|
+
body: currentBody,
|
|
396
|
+
bodyHash: hashText(currentBody),
|
|
397
|
+
bodyLength: currentBody.length,
|
|
398
|
+
}
|
|
399
|
+
: null,
|
|
400
|
+
threadSnapshot: buildThreadSnapshot(thread),
|
|
401
|
+
}, `Edited inbox draft ${returnedVersionId}, but a newer draft is now current. Re-load before approving send.`);
|
|
402
|
+
}
|
|
403
|
+
if (!currentBody ||
|
|
404
|
+
currentBody !== body ||
|
|
405
|
+
hashText(currentBody) !== bodyHash) {
|
|
406
|
+
return errorResult("persisted_draft_body_mismatch", "Persisted edited draft did not match the requested body.", { routeCalled, threadId, versionId: currentVersionId });
|
|
407
|
+
}
|
|
408
|
+
return textResult({
|
|
409
|
+
routeCalled,
|
|
410
|
+
approvalRequired: true,
|
|
411
|
+
threadId,
|
|
412
|
+
versionId: currentVersionId,
|
|
413
|
+
versionNumber: draft.versionNumber ?? patchResponse.versionNumber ?? null,
|
|
414
|
+
draftSnapshot: {
|
|
415
|
+
threadId,
|
|
416
|
+
versionId: currentVersionId,
|
|
417
|
+
versionNumber: draft.versionNumber ?? patchResponse.versionNumber ?? null,
|
|
418
|
+
body: currentBody,
|
|
419
|
+
bodyHash,
|
|
420
|
+
bodyLength: currentBody.length,
|
|
421
|
+
routeCalled,
|
|
422
|
+
snapshotAt: new Date().toISOString(),
|
|
423
|
+
},
|
|
424
|
+
threadSnapshot: buildThreadSnapshot(thread),
|
|
425
|
+
}, `Edited inbox draft ${currentVersionId}; approval is required before send.`);
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
return routeError(error, "update_inbox_draft_failed");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function hasAnyKey(input, keys) {
|
|
432
|
+
return keys.some((key) => Object.prototype.hasOwnProperty.call(input, key));
|
|
433
|
+
}
|
|
434
|
+
function validateApproval(value) {
|
|
435
|
+
return value === "approved";
|
|
436
|
+
}
|
|
437
|
+
export async function sendInboxDraft(input) {
|
|
438
|
+
const required = requireStrings(input, [
|
|
439
|
+
"threadId",
|
|
440
|
+
"versionId",
|
|
441
|
+
"approvedBody",
|
|
442
|
+
"approvedBodyHash",
|
|
443
|
+
"idempotencyKey",
|
|
444
|
+
]);
|
|
445
|
+
if (!required.ok)
|
|
446
|
+
return required.result;
|
|
447
|
+
const { threadId, versionId, approvedBody, approvedBodyHash, idempotencyKey, } = required.values;
|
|
448
|
+
if (!validateApproval(input.approval)) {
|
|
449
|
+
return errorResult("approval_required", "approval must be approved.");
|
|
450
|
+
}
|
|
451
|
+
if (hashText(approvedBody) !== approvedBodyHash) {
|
|
452
|
+
return errorResult("approved_body_hash_mismatch", "approvedBodyHash does not match approvedBody.");
|
|
453
|
+
}
|
|
454
|
+
const detailRoute = `/api/v3/inbox/${encodeURIComponent(threadId)}?includeReplyEligibility=true`;
|
|
455
|
+
const detail = await getApi().get(detailRoute);
|
|
456
|
+
const thread = getThread(detail);
|
|
457
|
+
const draft = getDraft(thread);
|
|
458
|
+
const currentVersionId = getDraftVersionId(draft);
|
|
459
|
+
if (!thread?.replyEligibility) {
|
|
460
|
+
return errorResult("reply_eligibility_missing", "Product inbox detail response did not include replyEligibility.", { routeCalled: detailRoute, threadId });
|
|
461
|
+
}
|
|
462
|
+
if (!snapshotsMatch(buildThreadSnapshot(thread), input.approvedThreadSnapshot)) {
|
|
463
|
+
return errorResult("stale_thread_snapshot", "Thread changed after approval; re-check eligibility before sending.", { routeCalled: detailRoute, threadId });
|
|
464
|
+
}
|
|
465
|
+
if (currentVersionId !== versionId) {
|
|
466
|
+
return errorResult("stale_draft_snapshot", "Current draft version does not match the approved version.", { threadId, versionId, currentVersionId });
|
|
467
|
+
}
|
|
468
|
+
if (typeof draft?.body !== "string" ||
|
|
469
|
+
hashText(draft.body) !== approvedBodyHash) {
|
|
470
|
+
return errorResult("approved_body_mismatch", "Current draft body does not match the approved body hash.", { threadId, versionId });
|
|
471
|
+
}
|
|
472
|
+
if (thread.replyEligibility.canSendDraft !== true) {
|
|
473
|
+
return errorResult("send_not_eligible", "Product eligibility blocks draft send.", {
|
|
474
|
+
threadId,
|
|
475
|
+
reasonCodes: thread.replyEligibility.reasonCodes ?? [],
|
|
476
|
+
replyEligibility: thread.replyEligibility,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}/send`;
|
|
480
|
+
try {
|
|
481
|
+
const response = await getApi().post(routeCalled, {
|
|
482
|
+
idempotencyKey,
|
|
483
|
+
mcpApproval: {
|
|
484
|
+
toolName: "send_inbox_draft",
|
|
485
|
+
threadId,
|
|
486
|
+
versionId,
|
|
487
|
+
approvedBodyHash,
|
|
488
|
+
approvedThreadSnapshot: input.approvedThreadSnapshot,
|
|
489
|
+
approval: "approved",
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
return textResult({
|
|
493
|
+
routeCalled,
|
|
494
|
+
threadId,
|
|
495
|
+
versionId,
|
|
496
|
+
idempotencyKey,
|
|
497
|
+
approvedBodyHash,
|
|
498
|
+
...response,
|
|
499
|
+
}, response.success === false
|
|
500
|
+
? `Draft send did not complete for thread ${threadId}.`
|
|
501
|
+
: `Draft send submitted for thread ${threadId}.`, response.success === false);
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
return routeError(error, "send_inbox_draft_failed");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
export async function sendInboxManualReply(input) {
|
|
508
|
+
if (hasAnyKey(input, [
|
|
509
|
+
"mode",
|
|
510
|
+
"filter",
|
|
511
|
+
"filters",
|
|
512
|
+
"search",
|
|
513
|
+
"limit",
|
|
514
|
+
"threadIds",
|
|
515
|
+
"messages",
|
|
516
|
+
])) {
|
|
517
|
+
return errorResult("batch_manual_reply_not_supported", "send_inbox_manual_reply supports one exact approved message for one thread.");
|
|
518
|
+
}
|
|
519
|
+
const required = requireStrings(input, [
|
|
520
|
+
"threadId",
|
|
521
|
+
"message",
|
|
522
|
+
"approvedMessageHash",
|
|
523
|
+
"idempotencyKey",
|
|
524
|
+
]);
|
|
525
|
+
if (!required.ok)
|
|
526
|
+
return required.result;
|
|
527
|
+
const { threadId, message, approvedMessageHash, idempotencyKey } = required.values;
|
|
528
|
+
if (!validateApproval(input.approval)) {
|
|
529
|
+
return errorResult("approval_required", "approval must be approved.");
|
|
530
|
+
}
|
|
531
|
+
if (hashText(message) !== approvedMessageHash) {
|
|
532
|
+
return errorResult("approved_message_hash_mismatch", "approvedMessageHash does not match message.");
|
|
533
|
+
}
|
|
534
|
+
const detailRoute = `/api/v3/inbox/${encodeURIComponent(threadId)}?includeReplyEligibility=true`;
|
|
535
|
+
const detail = await getApi().get(detailRoute);
|
|
536
|
+
const thread = getThread(detail);
|
|
537
|
+
if (!thread?.replyEligibility) {
|
|
538
|
+
return errorResult("reply_eligibility_missing", "Product inbox detail response did not include replyEligibility.", { routeCalled: detailRoute, threadId });
|
|
539
|
+
}
|
|
540
|
+
if (!snapshotsMatch(buildThreadSnapshot(thread), input.approvedThreadSnapshot)) {
|
|
541
|
+
return errorResult("stale_thread_snapshot", "Thread changed after approval; re-check eligibility before sending.", { routeCalled: detailRoute, threadId });
|
|
542
|
+
}
|
|
543
|
+
if (thread.replyEligibility.canSendManual !== true) {
|
|
544
|
+
return errorResult("send_not_eligible", "Product eligibility blocks manual reply.", {
|
|
545
|
+
threadId,
|
|
546
|
+
reasonCodes: thread.replyEligibility.reasonCodes ?? [],
|
|
547
|
+
replyEligibility: thread.replyEligibility,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}/reply`;
|
|
551
|
+
try {
|
|
552
|
+
const response = await getApi().post(routeCalled, {
|
|
553
|
+
message,
|
|
554
|
+
idempotencyKey,
|
|
555
|
+
mcpApproval: {
|
|
556
|
+
toolName: "send_inbox_manual_reply",
|
|
557
|
+
threadId,
|
|
558
|
+
messageHash: approvedMessageHash,
|
|
559
|
+
approvedThreadSnapshot: input.approvedThreadSnapshot,
|
|
560
|
+
approval: "approved",
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
return textResult({
|
|
564
|
+
routeCalled,
|
|
565
|
+
threadId,
|
|
566
|
+
idempotencyKey,
|
|
567
|
+
messageHash: approvedMessageHash,
|
|
568
|
+
...response,
|
|
569
|
+
}, response.success === false
|
|
570
|
+
? `Manual reply did not complete for thread ${threadId}.`
|
|
571
|
+
: `Manual reply submitted for thread ${threadId}.`, response.success === false);
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
return routeError(error, "send_inbox_manual_reply_failed");
|
|
575
|
+
}
|
|
576
|
+
}
|