@sellable/mcp 0.1.318 → 0.1.319
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/dist/generated/column-schema-manifest.js +1 -1
- package/dist/server.js +1 -0
- package/dist/tools/blueprint-commit.js +1 -1
- package/dist/tools/inbox.d.ts +308 -0
- package/dist/tools/inbox.js +544 -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 +11 -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 +15 -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/research/config.json +9 -0
|
@@ -0,0 +1,544 @@
|
|
|
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: { type: "string", enum: ["all", "unread", "replied", "awaiting"] },
|
|
28
|
+
senderId: { type: "string" },
|
|
29
|
+
classification: { type: "string" },
|
|
30
|
+
search: { type: "string" },
|
|
31
|
+
includeEnrichment: { type: "boolean" },
|
|
32
|
+
includeReplyEligibility: { type: "boolean" },
|
|
33
|
+
},
|
|
34
|
+
required: [],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
outputSchema: resultOutputSchema,
|
|
38
|
+
annotations: {
|
|
39
|
+
readOnlyHint: true,
|
|
40
|
+
destructiveHint: false,
|
|
41
|
+
openWorldHint: false,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "get_inbox_thread",
|
|
46
|
+
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.",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
threadId: { type: "string" },
|
|
51
|
+
},
|
|
52
|
+
required: ["threadId"],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
outputSchema: resultOutputSchema,
|
|
56
|
+
annotations: {
|
|
57
|
+
readOnlyHint: true,
|
|
58
|
+
destructiveHint: false,
|
|
59
|
+
openWorldHint: false,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "check_inbox_reply_eligibility",
|
|
64
|
+
description: "Load one inbox thread with includeReplyEligibility=true through GET /api/v3/inbox/[threadId] and return the product-computed reply eligibility snapshot.",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
threadId: { type: "string" },
|
|
69
|
+
},
|
|
70
|
+
required: ["threadId"],
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
},
|
|
73
|
+
outputSchema: resultOutputSchema,
|
|
74
|
+
annotations: {
|
|
75
|
+
readOnlyHint: true,
|
|
76
|
+
destructiveHint: false,
|
|
77
|
+
openWorldHint: false,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "update_inbox_draft",
|
|
82
|
+
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.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
threadId: { type: "string" },
|
|
87
|
+
versionId: { type: "string" },
|
|
88
|
+
body: { type: "string" },
|
|
89
|
+
},
|
|
90
|
+
required: ["threadId", "versionId", "body"],
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
outputSchema: resultOutputSchema,
|
|
94
|
+
annotations: {
|
|
95
|
+
readOnlyHint: false,
|
|
96
|
+
destructiveHint: false,
|
|
97
|
+
openWorldHint: false,
|
|
98
|
+
idempotentHint: false,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "send_inbox_draft",
|
|
103
|
+
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.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
threadId: { type: "string" },
|
|
108
|
+
versionId: { type: "string" },
|
|
109
|
+
approvedBody: { type: "string" },
|
|
110
|
+
approvedBodyHash: { type: "string" },
|
|
111
|
+
approvedThreadSnapshot: snapshotSchema,
|
|
112
|
+
idempotencyKey: { type: "string" },
|
|
113
|
+
approval: { type: "string", enum: ["approved"] },
|
|
114
|
+
},
|
|
115
|
+
required: [
|
|
116
|
+
"threadId",
|
|
117
|
+
"versionId",
|
|
118
|
+
"approvedBody",
|
|
119
|
+
"approvedBodyHash",
|
|
120
|
+
"approvedThreadSnapshot",
|
|
121
|
+
"idempotencyKey",
|
|
122
|
+
"approval",
|
|
123
|
+
],
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
},
|
|
126
|
+
outputSchema: resultOutputSchema,
|
|
127
|
+
annotations: {
|
|
128
|
+
readOnlyHint: false,
|
|
129
|
+
destructiveHint: false,
|
|
130
|
+
openWorldHint: true,
|
|
131
|
+
idempotentHint: false,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "send_inbox_manual_reply",
|
|
136
|
+
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.",
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
threadId: { type: "string" },
|
|
141
|
+
message: { type: "string" },
|
|
142
|
+
approvedMessageHash: { type: "string" },
|
|
143
|
+
approvedThreadSnapshot: snapshotSchema,
|
|
144
|
+
idempotencyKey: { type: "string" },
|
|
145
|
+
approval: { type: "string", enum: ["approved"] },
|
|
146
|
+
},
|
|
147
|
+
required: [
|
|
148
|
+
"threadId",
|
|
149
|
+
"message",
|
|
150
|
+
"approvedMessageHash",
|
|
151
|
+
"approvedThreadSnapshot",
|
|
152
|
+
"idempotencyKey",
|
|
153
|
+
"approval",
|
|
154
|
+
],
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
},
|
|
157
|
+
outputSchema: resultOutputSchema,
|
|
158
|
+
annotations: {
|
|
159
|
+
readOnlyHint: false,
|
|
160
|
+
destructiveHint: false,
|
|
161
|
+
openWorldHint: true,
|
|
162
|
+
idempotentHint: false,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
function hashText(value) {
|
|
167
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
168
|
+
}
|
|
169
|
+
function textResult(structuredContent, text, isError = false) {
|
|
170
|
+
return {
|
|
171
|
+
structuredContent,
|
|
172
|
+
content: [{ type: "text", text }],
|
|
173
|
+
...(isError ? { isError: true } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function errorResult(error, message, extra = {}) {
|
|
177
|
+
return textResult({ ok: false, error, message, ...extra }, message, true);
|
|
178
|
+
}
|
|
179
|
+
function requireString(value, field) {
|
|
180
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
181
|
+
return errorResult("invalid_arguments", `${field} is required.`);
|
|
182
|
+
}
|
|
183
|
+
return value.trim();
|
|
184
|
+
}
|
|
185
|
+
function normalizeTimestamp(value) {
|
|
186
|
+
if (value == null)
|
|
187
|
+
return null;
|
|
188
|
+
if (value instanceof Date)
|
|
189
|
+
return value.toISOString();
|
|
190
|
+
if (typeof value === "string")
|
|
191
|
+
return value;
|
|
192
|
+
return String(value);
|
|
193
|
+
}
|
|
194
|
+
function getThread(response) {
|
|
195
|
+
return response.thread ?? null;
|
|
196
|
+
}
|
|
197
|
+
function getDraft(thread) {
|
|
198
|
+
return thread?.draft ?? null;
|
|
199
|
+
}
|
|
200
|
+
function getDraftVersionId(draft) {
|
|
201
|
+
return typeof draft?.id === "string"
|
|
202
|
+
? draft.id
|
|
203
|
+
: typeof draft?.versionId === "string"
|
|
204
|
+
? draft.versionId
|
|
205
|
+
: null;
|
|
206
|
+
}
|
|
207
|
+
function buildThreadSnapshot(thread) {
|
|
208
|
+
const reasonCodes = Array.isArray(thread?.replyEligibility?.reasonCodes)
|
|
209
|
+
? thread.replyEligibility.reasonCodes
|
|
210
|
+
: [];
|
|
211
|
+
return {
|
|
212
|
+
latestMessageTimestamp: normalizeTimestamp(thread?.latestMessageTimestamp),
|
|
213
|
+
lastInboundMessageAt: normalizeTimestamp(thread?.lastInboundMessageAt),
|
|
214
|
+
updatedAt: normalizeTimestamp(thread?.updatedAt),
|
|
215
|
+
reasonCodes,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function snapshotsMatch(current, approved) {
|
|
219
|
+
if (!approved || typeof approved !== "object")
|
|
220
|
+
return false;
|
|
221
|
+
const snapshot = approved;
|
|
222
|
+
return (current.latestMessageTimestamp ===
|
|
223
|
+
(snapshot.latestMessageTimestamp ?? null) &&
|
|
224
|
+
current.lastInboundMessageAt === (snapshot.lastInboundMessageAt ?? null) &&
|
|
225
|
+
current.updatedAt === (snapshot.updatedAt ?? null));
|
|
226
|
+
}
|
|
227
|
+
function chronologicalTranscript(thread) {
|
|
228
|
+
const messages = Array.isArray(thread?.messages) ? thread.messages : [];
|
|
229
|
+
return [...messages].sort((a, b) => {
|
|
230
|
+
const aTime = new Date(normalizeTimestamp(a.timestamp) ?? 0).getTime();
|
|
231
|
+
const bTime = new Date(normalizeTimestamp(b.timestamp) ?? 0).getTime();
|
|
232
|
+
return aTime - bTime;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function routeError(error, fallback) {
|
|
236
|
+
if (error instanceof SellableApiError) {
|
|
237
|
+
let parsed = null;
|
|
238
|
+
try {
|
|
239
|
+
parsed = JSON.parse(error.body);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
parsed = error.body;
|
|
243
|
+
}
|
|
244
|
+
return errorResult(fallback, error.message, {
|
|
245
|
+
status: error.status,
|
|
246
|
+
productError: parsed,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const message = error instanceof Error ? error.message : fallback;
|
|
250
|
+
return errorResult(fallback, message);
|
|
251
|
+
}
|
|
252
|
+
function appendParam(params, key, value) {
|
|
253
|
+
if (value === undefined || value === null || value === "")
|
|
254
|
+
return;
|
|
255
|
+
params.set(key, String(value));
|
|
256
|
+
}
|
|
257
|
+
export async function searchInboxThreads(input) {
|
|
258
|
+
const params = new URLSearchParams();
|
|
259
|
+
const limit = typeof input.limit === "number" && Number.isFinite(input.limit)
|
|
260
|
+
? Math.max(1, Math.min(Math.trunc(input.limit), 50))
|
|
261
|
+
: undefined;
|
|
262
|
+
appendParam(params, "limit", limit);
|
|
263
|
+
appendParam(params, "cursor", input.cursor);
|
|
264
|
+
appendParam(params, "filter", input.filter);
|
|
265
|
+
appendParam(params, "status", input.status);
|
|
266
|
+
appendParam(params, "senderId", input.senderId);
|
|
267
|
+
appendParam(params, "classification", input.classification);
|
|
268
|
+
appendParam(params, "search", input.search);
|
|
269
|
+
if (typeof input.includeEnrichment === "boolean") {
|
|
270
|
+
params.set("includeEnrichment", String(input.includeEnrichment));
|
|
271
|
+
}
|
|
272
|
+
if (typeof input.includeReplyEligibility === "boolean") {
|
|
273
|
+
params.set("includeReplyEligibility", String(input.includeReplyEligibility));
|
|
274
|
+
}
|
|
275
|
+
const query = params.toString();
|
|
276
|
+
const routeCalled = `/api/v3/inbox${query ? `?${query}` : ""}`;
|
|
277
|
+
const response = await getApi().get(routeCalled);
|
|
278
|
+
const threads = Array.isArray(response.threads) ? response.threads : [];
|
|
279
|
+
return textResult({
|
|
280
|
+
routeCalled,
|
|
281
|
+
...response,
|
|
282
|
+
}, `${threads.length} inbox thread${threads.length === 1 ? "" : "s"} returned.`);
|
|
283
|
+
}
|
|
284
|
+
export async function getInboxThread(input) {
|
|
285
|
+
const threadId = requireString(input.threadId, "threadId");
|
|
286
|
+
if (typeof threadId !== "string")
|
|
287
|
+
return threadId;
|
|
288
|
+
const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}`;
|
|
289
|
+
const response = await getApi().get(routeCalled);
|
|
290
|
+
const thread = getThread(response);
|
|
291
|
+
return textResult({
|
|
292
|
+
routeCalled,
|
|
293
|
+
thread,
|
|
294
|
+
messageOrder: thread?.messageOrder ?? "route_order",
|
|
295
|
+
chronologicalTranscript: chronologicalTranscript(thread),
|
|
296
|
+
}, thread
|
|
297
|
+
? `Loaded inbox thread ${thread.id ?? threadId}.`
|
|
298
|
+
: `Loaded inbox thread ${threadId}.`);
|
|
299
|
+
}
|
|
300
|
+
export async function checkInboxReplyEligibility(input) {
|
|
301
|
+
const threadId = requireString(input.threadId, "threadId");
|
|
302
|
+
if (typeof threadId !== "string")
|
|
303
|
+
return threadId;
|
|
304
|
+
const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}?includeReplyEligibility=true`;
|
|
305
|
+
const response = await getApi().get(routeCalled);
|
|
306
|
+
const thread = getThread(response);
|
|
307
|
+
if (!thread?.replyEligibility) {
|
|
308
|
+
return errorResult("reply_eligibility_missing", "Product inbox detail response did not include replyEligibility.", { routeCalled, threadId });
|
|
309
|
+
}
|
|
310
|
+
return textResult({
|
|
311
|
+
routeCalled,
|
|
312
|
+
threadId,
|
|
313
|
+
replyEligibility: thread.replyEligibility,
|
|
314
|
+
draftSnapshot: thread.draft
|
|
315
|
+
? {
|
|
316
|
+
threadId,
|
|
317
|
+
versionId: getDraftVersionId(thread.draft),
|
|
318
|
+
body: thread.draft.body,
|
|
319
|
+
bodyHash: typeof thread.draft.body === "string"
|
|
320
|
+
? hashText(thread.draft.body)
|
|
321
|
+
: null,
|
|
322
|
+
versionNumber: thread.draft.versionNumber ?? null,
|
|
323
|
+
}
|
|
324
|
+
: null,
|
|
325
|
+
threadSnapshot: buildThreadSnapshot(thread),
|
|
326
|
+
}, `Eligibility loaded for inbox thread ${threadId}.`);
|
|
327
|
+
}
|
|
328
|
+
export async function updateInboxDraft(input) {
|
|
329
|
+
const threadId = requireString(input.threadId, "threadId");
|
|
330
|
+
if (typeof threadId !== "string")
|
|
331
|
+
return threadId;
|
|
332
|
+
const versionId = requireString(input.versionId, "versionId");
|
|
333
|
+
if (typeof versionId !== "string")
|
|
334
|
+
return versionId;
|
|
335
|
+
const body = requireString(input.body, "body");
|
|
336
|
+
if (typeof body !== "string")
|
|
337
|
+
return body;
|
|
338
|
+
const bodyHash = hashText(body);
|
|
339
|
+
const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}/draft`;
|
|
340
|
+
try {
|
|
341
|
+
const patchResponse = await getApi().patch(routeCalled, {
|
|
342
|
+
versionId,
|
|
343
|
+
body,
|
|
344
|
+
mcpApproval: {
|
|
345
|
+
toolName: "update_inbox_draft",
|
|
346
|
+
threadId,
|
|
347
|
+
versionId,
|
|
348
|
+
bodyHash,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
const detail = await getApi().get(`/api/v3/inbox/${encodeURIComponent(threadId)}?includeReplyEligibility=true`);
|
|
352
|
+
const thread = getThread(detail);
|
|
353
|
+
const draft = getDraft(thread);
|
|
354
|
+
const currentVersionId = getDraftVersionId(draft);
|
|
355
|
+
const persistedBody = typeof draft?.body === "string"
|
|
356
|
+
? draft.body
|
|
357
|
+
: typeof patchResponse.body === "string"
|
|
358
|
+
? patchResponse.body
|
|
359
|
+
: null;
|
|
360
|
+
const returnedVersionId = typeof patchResponse.versionId === "string"
|
|
361
|
+
? patchResponse.versionId
|
|
362
|
+
: currentVersionId;
|
|
363
|
+
if (!draft || !persistedBody || returnedVersionId !== currentVersionId) {
|
|
364
|
+
return errorResult("persisted_draft_snapshot_missing", "Could not verify the persisted edited draft after PATCH.", { routeCalled, threadId, versionId: returnedVersionId ?? null });
|
|
365
|
+
}
|
|
366
|
+
if (persistedBody !== body || hashText(persistedBody) !== bodyHash) {
|
|
367
|
+
return errorResult("persisted_draft_body_mismatch", "Persisted edited draft did not match the requested body.", { routeCalled, threadId, versionId: currentVersionId });
|
|
368
|
+
}
|
|
369
|
+
return textResult({
|
|
370
|
+
routeCalled,
|
|
371
|
+
approvalRequired: true,
|
|
372
|
+
threadId,
|
|
373
|
+
versionId: currentVersionId,
|
|
374
|
+
versionNumber: draft.versionNumber ?? patchResponse.versionNumber ?? null,
|
|
375
|
+
draftSnapshot: {
|
|
376
|
+
threadId,
|
|
377
|
+
versionId: currentVersionId,
|
|
378
|
+
versionNumber: draft.versionNumber ?? patchResponse.versionNumber ?? null,
|
|
379
|
+
body: persistedBody,
|
|
380
|
+
bodyHash,
|
|
381
|
+
bodyLength: persistedBody.length,
|
|
382
|
+
routeCalled,
|
|
383
|
+
snapshotAt: new Date().toISOString(),
|
|
384
|
+
},
|
|
385
|
+
threadSnapshot: buildThreadSnapshot(thread),
|
|
386
|
+
}, `Edited inbox draft ${currentVersionId}; approval is required before send.`);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
return routeError(error, "update_inbox_draft_failed");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function hasAnyKey(input, keys) {
|
|
393
|
+
return keys.some((key) => Object.prototype.hasOwnProperty.call(input, key));
|
|
394
|
+
}
|
|
395
|
+
function validateApproval(value) {
|
|
396
|
+
return value === "approved";
|
|
397
|
+
}
|
|
398
|
+
export async function sendInboxDraft(input) {
|
|
399
|
+
const threadId = requireString(input.threadId, "threadId");
|
|
400
|
+
if (typeof threadId !== "string")
|
|
401
|
+
return threadId;
|
|
402
|
+
const versionId = requireString(input.versionId, "versionId");
|
|
403
|
+
if (typeof versionId !== "string")
|
|
404
|
+
return versionId;
|
|
405
|
+
const approvedBody = requireString(input.approvedBody, "approvedBody");
|
|
406
|
+
if (typeof approvedBody !== "string")
|
|
407
|
+
return approvedBody;
|
|
408
|
+
const approvedBodyHash = requireString(input.approvedBodyHash, "approvedBodyHash");
|
|
409
|
+
if (typeof approvedBodyHash !== "string")
|
|
410
|
+
return approvedBodyHash;
|
|
411
|
+
const idempotencyKey = requireString(input.idempotencyKey, "idempotencyKey");
|
|
412
|
+
if (typeof idempotencyKey !== "string")
|
|
413
|
+
return idempotencyKey;
|
|
414
|
+
if (!validateApproval(input.approval)) {
|
|
415
|
+
return errorResult("approval_required", "approval must be approved.");
|
|
416
|
+
}
|
|
417
|
+
if (hashText(approvedBody) !== approvedBodyHash) {
|
|
418
|
+
return errorResult("approved_body_hash_mismatch", "approvedBodyHash does not match approvedBody.");
|
|
419
|
+
}
|
|
420
|
+
const detailRoute = `/api/v3/inbox/${encodeURIComponent(threadId)}?includeReplyEligibility=true`;
|
|
421
|
+
const detail = await getApi().get(detailRoute);
|
|
422
|
+
const thread = getThread(detail);
|
|
423
|
+
const draft = getDraft(thread);
|
|
424
|
+
const currentVersionId = getDraftVersionId(draft);
|
|
425
|
+
if (!thread?.replyEligibility) {
|
|
426
|
+
return errorResult("reply_eligibility_missing", "Product inbox detail response did not include replyEligibility.", { routeCalled: detailRoute, threadId });
|
|
427
|
+
}
|
|
428
|
+
if (!snapshotsMatch(buildThreadSnapshot(thread), input.approvedThreadSnapshot)) {
|
|
429
|
+
return errorResult("stale_thread_snapshot", "Thread changed after approval; re-check eligibility before sending.", { routeCalled: detailRoute, threadId });
|
|
430
|
+
}
|
|
431
|
+
if (currentVersionId !== versionId) {
|
|
432
|
+
return errorResult("stale_draft_snapshot", "Current draft version does not match the approved version.", { threadId, versionId, currentVersionId });
|
|
433
|
+
}
|
|
434
|
+
if (typeof draft?.body !== "string" || hashText(draft.body) !== approvedBodyHash) {
|
|
435
|
+
return errorResult("approved_body_mismatch", "Current draft body does not match the approved body hash.", { threadId, versionId });
|
|
436
|
+
}
|
|
437
|
+
if (thread.replyEligibility.canSendDraft !== true) {
|
|
438
|
+
return errorResult("send_not_eligible", "Product eligibility blocks draft send.", {
|
|
439
|
+
threadId,
|
|
440
|
+
reasonCodes: thread.replyEligibility.reasonCodes ?? [],
|
|
441
|
+
replyEligibility: thread.replyEligibility,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}/send`;
|
|
445
|
+
try {
|
|
446
|
+
const response = await getApi().post(routeCalled, {
|
|
447
|
+
idempotencyKey,
|
|
448
|
+
mcpApproval: {
|
|
449
|
+
toolName: "send_inbox_draft",
|
|
450
|
+
threadId,
|
|
451
|
+
versionId,
|
|
452
|
+
approvedBodyHash,
|
|
453
|
+
approvedThreadSnapshot: input.approvedThreadSnapshot,
|
|
454
|
+
approval: "approved",
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
return textResult({
|
|
458
|
+
routeCalled,
|
|
459
|
+
threadId,
|
|
460
|
+
versionId,
|
|
461
|
+
idempotencyKey,
|
|
462
|
+
approvedBodyHash,
|
|
463
|
+
...response,
|
|
464
|
+
}, response.success === false
|
|
465
|
+
? `Draft send did not complete for thread ${threadId}.`
|
|
466
|
+
: `Draft send submitted for thread ${threadId}.`, response.success === false);
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
return routeError(error, "send_inbox_draft_failed");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
export async function sendInboxManualReply(input) {
|
|
473
|
+
if (hasAnyKey(input, [
|
|
474
|
+
"mode",
|
|
475
|
+
"filter",
|
|
476
|
+
"filters",
|
|
477
|
+
"search",
|
|
478
|
+
"limit",
|
|
479
|
+
"threadIds",
|
|
480
|
+
"messages",
|
|
481
|
+
])) {
|
|
482
|
+
return errorResult("batch_manual_reply_not_supported", "send_inbox_manual_reply supports one exact approved message for one thread.");
|
|
483
|
+
}
|
|
484
|
+
const threadId = requireString(input.threadId, "threadId");
|
|
485
|
+
if (typeof threadId !== "string")
|
|
486
|
+
return threadId;
|
|
487
|
+
const message = requireString(input.message, "message");
|
|
488
|
+
if (typeof message !== "string")
|
|
489
|
+
return message;
|
|
490
|
+
const approvedMessageHash = requireString(input.approvedMessageHash, "approvedMessageHash");
|
|
491
|
+
if (typeof approvedMessageHash !== "string")
|
|
492
|
+
return approvedMessageHash;
|
|
493
|
+
const idempotencyKey = requireString(input.idempotencyKey, "idempotencyKey");
|
|
494
|
+
if (typeof idempotencyKey !== "string")
|
|
495
|
+
return idempotencyKey;
|
|
496
|
+
if (!validateApproval(input.approval)) {
|
|
497
|
+
return errorResult("approval_required", "approval must be approved.");
|
|
498
|
+
}
|
|
499
|
+
if (hashText(message) !== approvedMessageHash) {
|
|
500
|
+
return errorResult("approved_message_hash_mismatch", "approvedMessageHash does not match message.");
|
|
501
|
+
}
|
|
502
|
+
const detailRoute = `/api/v3/inbox/${encodeURIComponent(threadId)}?includeReplyEligibility=true`;
|
|
503
|
+
const detail = await getApi().get(detailRoute);
|
|
504
|
+
const thread = getThread(detail);
|
|
505
|
+
if (!thread?.replyEligibility) {
|
|
506
|
+
return errorResult("reply_eligibility_missing", "Product inbox detail response did not include replyEligibility.", { routeCalled: detailRoute, threadId });
|
|
507
|
+
}
|
|
508
|
+
if (!snapshotsMatch(buildThreadSnapshot(thread), input.approvedThreadSnapshot)) {
|
|
509
|
+
return errorResult("stale_thread_snapshot", "Thread changed after approval; re-check eligibility before sending.", { routeCalled: detailRoute, threadId });
|
|
510
|
+
}
|
|
511
|
+
if (thread.replyEligibility.canSendManual !== true) {
|
|
512
|
+
return errorResult("send_not_eligible", "Product eligibility blocks manual reply.", {
|
|
513
|
+
threadId,
|
|
514
|
+
reasonCodes: thread.replyEligibility.reasonCodes ?? [],
|
|
515
|
+
replyEligibility: thread.replyEligibility,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}/reply`;
|
|
519
|
+
try {
|
|
520
|
+
const response = await getApi().post(routeCalled, {
|
|
521
|
+
message,
|
|
522
|
+
idempotencyKey,
|
|
523
|
+
mcpApproval: {
|
|
524
|
+
toolName: "send_inbox_manual_reply",
|
|
525
|
+
threadId,
|
|
526
|
+
messageHash: approvedMessageHash,
|
|
527
|
+
approvedThreadSnapshot: input.approvedThreadSnapshot,
|
|
528
|
+
approval: "approved",
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
return textResult({
|
|
532
|
+
routeCalled,
|
|
533
|
+
threadId,
|
|
534
|
+
idempotencyKey,
|
|
535
|
+
messageHash: approvedMessageHash,
|
|
536
|
+
...response,
|
|
537
|
+
}, response.success === false
|
|
538
|
+
? `Manual reply did not complete for thread ${threadId}.`
|
|
539
|
+
: `Manual reply submitted for thread ${threadId}.`, response.success === false);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
return routeError(error, "send_inbox_manual_reply_failed");
|
|
543
|
+
}
|
|
544
|
+
}
|
package/dist/tools/linkedin.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
16
16
|
postUrl?: undefined;
|
|
17
17
|
sources?: undefined;
|
|
18
18
|
full?: undefined;
|
|
19
|
+
fetchMinimal?: undefined;
|
|
19
20
|
companyUrl?: undefined;
|
|
20
21
|
sortBy?: undefined;
|
|
21
22
|
linkedin_url?: undefined;
|
|
@@ -46,6 +47,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
46
47
|
};
|
|
47
48
|
linkedinUrl?: undefined;
|
|
48
49
|
full?: undefined;
|
|
50
|
+
fetchMinimal?: undefined;
|
|
49
51
|
companyUrl?: undefined;
|
|
50
52
|
sortBy?: undefined;
|
|
51
53
|
linkedin_url?: undefined;
|
|
@@ -68,6 +70,11 @@ export declare const linkedinToolDefinitions: ({
|
|
|
68
70
|
description: string;
|
|
69
71
|
default: boolean;
|
|
70
72
|
};
|
|
73
|
+
fetchMinimal: {
|
|
74
|
+
type: string;
|
|
75
|
+
description: string;
|
|
76
|
+
default: boolean;
|
|
77
|
+
};
|
|
71
78
|
limit?: undefined;
|
|
72
79
|
postUrl?: undefined;
|
|
73
80
|
sources?: undefined;
|
|
@@ -93,6 +100,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
93
100
|
postUrl?: undefined;
|
|
94
101
|
sources?: undefined;
|
|
95
102
|
full?: undefined;
|
|
103
|
+
fetchMinimal?: undefined;
|
|
96
104
|
sortBy?: undefined;
|
|
97
105
|
linkedin_url?: undefined;
|
|
98
106
|
max_posts?: undefined;
|
|
@@ -124,6 +132,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
124
132
|
postUrl?: undefined;
|
|
125
133
|
sources?: undefined;
|
|
126
134
|
full?: undefined;
|
|
135
|
+
fetchMinimal?: undefined;
|
|
127
136
|
linkedin_url?: undefined;
|
|
128
137
|
max_posts?: undefined;
|
|
129
138
|
};
|
|
@@ -149,6 +158,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
149
158
|
postUrl?: undefined;
|
|
150
159
|
sources?: undefined;
|
|
151
160
|
full?: undefined;
|
|
161
|
+
fetchMinimal?: undefined;
|
|
152
162
|
companyUrl?: undefined;
|
|
153
163
|
sortBy?: undefined;
|
|
154
164
|
};
|
|
@@ -169,6 +179,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
169
179
|
postUrl?: undefined;
|
|
170
180
|
sources?: undefined;
|
|
171
181
|
full?: undefined;
|
|
182
|
+
fetchMinimal?: undefined;
|
|
172
183
|
companyUrl?: undefined;
|
|
173
184
|
sortBy?: undefined;
|
|
174
185
|
max_posts?: undefined;
|
|
@@ -237,6 +248,70 @@ interface SerializedPostMedia {
|
|
|
237
248
|
articleUrl?: string;
|
|
238
249
|
mediaTypes: string[];
|
|
239
250
|
}
|
|
251
|
+
interface CompactLinkedInProfile {
|
|
252
|
+
id?: string;
|
|
253
|
+
publicIdentifier?: string;
|
|
254
|
+
linkedinUrl?: string;
|
|
255
|
+
firstName?: string;
|
|
256
|
+
lastName?: string;
|
|
257
|
+
fullName?: string;
|
|
258
|
+
headline?: string;
|
|
259
|
+
about?: string;
|
|
260
|
+
location?: string;
|
|
261
|
+
followerCount?: number;
|
|
262
|
+
connectionCount?: number;
|
|
263
|
+
isPremium?: boolean;
|
|
264
|
+
isCreator?: boolean;
|
|
265
|
+
isVerified?: boolean;
|
|
266
|
+
currentRole?: CompactExperience;
|
|
267
|
+
company?: CompactCompany;
|
|
268
|
+
experience?: CompactExperience[];
|
|
269
|
+
education?: CompactEducation[];
|
|
270
|
+
skills?: string[];
|
|
271
|
+
languages?: CompactLanguage[];
|
|
272
|
+
certifications?: CompactNamedItem[];
|
|
273
|
+
projects?: CompactNamedItem[];
|
|
274
|
+
}
|
|
275
|
+
interface CompactCompany {
|
|
276
|
+
name?: string;
|
|
277
|
+
linkedinUrl?: string;
|
|
278
|
+
website?: string;
|
|
279
|
+
domain?: string;
|
|
280
|
+
industry?: string;
|
|
281
|
+
employeeCount?: number;
|
|
282
|
+
employeeRange?: string;
|
|
283
|
+
description?: string;
|
|
284
|
+
founded?: string | number;
|
|
285
|
+
}
|
|
286
|
+
interface CompactExperience {
|
|
287
|
+
title?: string;
|
|
288
|
+
companyName?: string;
|
|
289
|
+
companyLinkedinUrl?: string;
|
|
290
|
+
dateRange?: string;
|
|
291
|
+
duration?: string;
|
|
292
|
+
location?: string;
|
|
293
|
+
description?: string;
|
|
294
|
+
isCurrent?: boolean;
|
|
295
|
+
}
|
|
296
|
+
interface CompactEducation {
|
|
297
|
+
schoolName?: string;
|
|
298
|
+
schoolLinkedinUrl?: string;
|
|
299
|
+
degree?: string;
|
|
300
|
+
fieldOfStudy?: string;
|
|
301
|
+
dateRange?: string;
|
|
302
|
+
}
|
|
303
|
+
interface CompactLanguage {
|
|
304
|
+
name?: string;
|
|
305
|
+
proficiency?: string;
|
|
306
|
+
}
|
|
307
|
+
interface CompactNamedItem {
|
|
308
|
+
name?: string;
|
|
309
|
+
organization?: string;
|
|
310
|
+
dateRange?: string;
|
|
311
|
+
description?: string;
|
|
312
|
+
url?: string;
|
|
313
|
+
}
|
|
314
|
+
export declare function compactLinkedInProfile(profile: unknown): CompactLinkedInProfile;
|
|
240
315
|
export declare function serializeLinkedInPosts(rawPosts: RawLinkedInPost[] | undefined, context: string): SerializedPost[];
|
|
241
316
|
export declare function fetchLinkedInPosts(linkedinUrl: string, limit?: number): Promise<{
|
|
242
317
|
posts: SerializedPost[];
|
|
@@ -244,6 +319,7 @@ export declare function fetchLinkedInPosts(linkedinUrl: string, limit?: number):
|
|
|
244
319
|
}>;
|
|
245
320
|
export declare function fetchLinkedInProfile(linkedinUrl: string, options?: {
|
|
246
321
|
full?: boolean;
|
|
322
|
+
fetchMinimal?: boolean;
|
|
247
323
|
}): Promise<any>;
|
|
248
324
|
export declare function fetchCompany(companyUrl: string): Promise<unknown>;
|
|
249
325
|
export declare function fetchCompanyPosts(companyUrl: string, limit?: number, sortBy?: "recent" | "top"): Promise<{
|