@sellable/mcp 0.1.258 → 0.1.259
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/index-dev.js +0 -0
- package/dist/index.js +0 -0
- package/dist/server.js +4 -1
- package/dist/tools/csv-dnc.d.ts +36 -0
- package/dist/tools/csv-dnc.js +94 -2
- package/dist/tools/engage-discovery.d.ts +0 -21
- package/dist/tools/engage-discovery.js +9 -136
- package/dist/tools/leads.d.ts +381 -21
- package/dist/tools/leads.js +219 -5
- package/dist/tools/registry.d.ts +207 -23
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +6 -0
- package/skills/create-campaign-v2/references/filter-leads.md +2 -0
- package/skills/create-campaign-v2/references/lead-validation-preview.md +2 -0
- package/skills/create-campaign-v2/references/step-13-import-leads.md +3 -1
- package/skills/create-post/SKILL.md +28 -122
- package/skills/create-post/references/gold-standard-post-pack.md +0 -11
- package/skills/create-post/references/hook-research-playbook.md +15 -314
- package/skills/create-post/references/post-file-contract.md +0 -21
- package/skills/create-post/references/post-validation.md +15 -205
- package/skills/find-leads/SKILL.md +6 -0
- package/skills/create-post/references/linkedin-preview-rendering.md +0 -176
- package/skills/research/config.json +0 -9
package/dist/index-dev.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/server.js
CHANGED
|
@@ -18,7 +18,7 @@ import { copySenderConfigTool, getEngageMemoryTool, migrateFlatConfigsTool, reco
|
|
|
18
18
|
import { getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
|
|
19
19
|
import { bulkEnrichWithProspeo, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
|
|
20
20
|
import { getCampaignFramework } from "./tools/framework.js";
|
|
21
|
-
import { cancelLeadImport, confirmLeadList, confirmProspeoCompanyAccounts, getProviderPrompt, importLeads, loadCsvDncEntriesTool, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchProspeoCompanies, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
|
|
21
|
+
import { cancelLeadImport, confirmLeadList, confirmProspeoCompanyAccounts, getProviderPrompt, importLeads, listDncEntriesTool, loadCsvDncEntriesTool, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchProspeoCompanies, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
|
|
22
22
|
import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, } from "./tools/linkedin.js";
|
|
23
23
|
import { getCampaignNavigationState } from "./tools/navigation.js";
|
|
24
24
|
import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
|
|
@@ -375,6 +375,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
375
375
|
case "load_csv_dnc_entries":
|
|
376
376
|
result = await loadCsvDncEntriesTool(args);
|
|
377
377
|
break;
|
|
378
|
+
case "list_dnc_entries":
|
|
379
|
+
result = await listDncEntriesTool(args);
|
|
380
|
+
break;
|
|
378
381
|
case "save_domain_filters":
|
|
379
382
|
result = await saveDomainFilters(args);
|
|
380
383
|
break;
|
package/dist/tools/csv-dnc.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
type DncSourceType = "all" | "manual" | "hubspot";
|
|
1
2
|
type InvalidDncRow = {
|
|
2
3
|
row: number;
|
|
3
4
|
column: string | null;
|
|
@@ -14,6 +15,41 @@ export type LoadCsvDncEntriesInput = {
|
|
|
14
15
|
confirmationToken?: string;
|
|
15
16
|
confirmed?: boolean;
|
|
16
17
|
};
|
|
18
|
+
export type ListDncEntriesInput = {
|
|
19
|
+
page?: number;
|
|
20
|
+
limit?: number;
|
|
21
|
+
search?: string;
|
|
22
|
+
listName?: string;
|
|
23
|
+
sourceType?: DncSourceType;
|
|
24
|
+
includeDeleted?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare function listDncEntries(input?: ListDncEntriesInput): Promise<{
|
|
27
|
+
ok: boolean;
|
|
28
|
+
workspaceId: string;
|
|
29
|
+
workspaceName: string;
|
|
30
|
+
pagination: {
|
|
31
|
+
total: number;
|
|
32
|
+
page: number;
|
|
33
|
+
limit: number;
|
|
34
|
+
totalPages: number;
|
|
35
|
+
};
|
|
36
|
+
listNames: string[];
|
|
37
|
+
entries: {
|
|
38
|
+
id: string | null;
|
|
39
|
+
type: string;
|
|
40
|
+
domain: string | null;
|
|
41
|
+
linkedin: string | null;
|
|
42
|
+
linkedinUsername: string | null;
|
|
43
|
+
name: string | null;
|
|
44
|
+
source: {
|
|
45
|
+
type: string;
|
|
46
|
+
label: string;
|
|
47
|
+
listName: string | null;
|
|
48
|
+
};
|
|
49
|
+
createdAt: string | null;
|
|
50
|
+
}[];
|
|
51
|
+
guidance: string;
|
|
52
|
+
}>;
|
|
17
53
|
export declare function loadCsvDncEntries(input: LoadCsvDncEntriesInput): Promise<{
|
|
18
54
|
guidance: string;
|
|
19
55
|
createdCount: number;
|
package/dist/tools/csv-dnc.js
CHANGED
|
@@ -14,6 +14,7 @@ const CONFIRMATION_TOKEN_VERSION = "csv-dnc-preview-v1";
|
|
|
14
14
|
const confirmationSecret = randomBytes(32).toString("hex");
|
|
15
15
|
const MAX_DNC_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
16
16
|
const MAX_DNC_CANDIDATES = 7500;
|
|
17
|
+
const MAX_DNC_LIST_LIMIT = 100;
|
|
17
18
|
const STRONG_DOMAIN_HEADER_KEYS = new Set([
|
|
18
19
|
"domain",
|
|
19
20
|
"website",
|
|
@@ -325,16 +326,84 @@ async function getVerifiedActiveWorkspace() {
|
|
|
325
326
|
const config = getConfig();
|
|
326
327
|
const activeWorkspaceId = config.activeWorkspaceId || config.workspaceId;
|
|
327
328
|
if (!activeWorkspaceId) {
|
|
328
|
-
throw new Error("No active workspace selected. Run list_workspaces then set_active_workspace
|
|
329
|
+
throw new Error("No active workspace selected. Run list_workspaces then set_active_workspace for the exact workspace whose Sellable DNC list should be read or updated.");
|
|
329
330
|
}
|
|
330
331
|
const api = getApi();
|
|
331
332
|
const { workspaces } = await api.get("/api/v3/workspaces");
|
|
332
333
|
const match = workspaces.find((workspace) => workspace.id === activeWorkspaceId);
|
|
333
334
|
if (!match) {
|
|
334
|
-
throw new Error(`Active workspace ${activeWorkspaceId} is not in the server access list. Run list_workspaces then set_active_workspace for the workspace
|
|
335
|
+
throw new Error(`Active workspace ${activeWorkspaceId} is not in the server access list. Run list_workspaces then set_active_workspace for the exact workspace whose Sellable DNC list should be read or updated.`);
|
|
335
336
|
}
|
|
336
337
|
return match;
|
|
337
338
|
}
|
|
339
|
+
function clampListPositiveInt(value, fallback, max) {
|
|
340
|
+
const parsed = typeof value === "number"
|
|
341
|
+
? value
|
|
342
|
+
: Number.parseInt(String(value ?? ""), 10);
|
|
343
|
+
if (!Number.isFinite(parsed) || parsed < 1)
|
|
344
|
+
return fallback;
|
|
345
|
+
return Math.min(Math.floor(parsed), max);
|
|
346
|
+
}
|
|
347
|
+
function normalizeDncSourceType(value) {
|
|
348
|
+
if (value === undefined || value === null || value === "")
|
|
349
|
+
return "all";
|
|
350
|
+
if (value === "all" || value === "manual" || value === "hubspot") {
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
throw new Error("sourceType must be one of all, manual, or hubspot.");
|
|
354
|
+
}
|
|
355
|
+
function optionalTrimmed(value) {
|
|
356
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
357
|
+
}
|
|
358
|
+
function normalizeCreatedAt(value) {
|
|
359
|
+
if (value instanceof Date)
|
|
360
|
+
return value.toISOString();
|
|
361
|
+
return optionalTrimmed(value);
|
|
362
|
+
}
|
|
363
|
+
function compactDncListEntry(entry) {
|
|
364
|
+
const domain = optionalTrimmed(entry.domain);
|
|
365
|
+
const linkedin = optionalTrimmed(entry.normalizedLinkedinUrl);
|
|
366
|
+
const sourceType = optionalTrimmed(entry.sourceType) ?? "manual";
|
|
367
|
+
return {
|
|
368
|
+
id: optionalTrimmed(entry.id),
|
|
369
|
+
type: domain ? "domain" : "linkedin",
|
|
370
|
+
domain,
|
|
371
|
+
linkedin,
|
|
372
|
+
linkedinUsername: optionalTrimmed(entry.linkedinUsername),
|
|
373
|
+
name: optionalTrimmed(entry.name),
|
|
374
|
+
source: {
|
|
375
|
+
type: sourceType,
|
|
376
|
+
label: optionalTrimmed(entry.sourceLabel) ??
|
|
377
|
+
(sourceType === "hubspot" ? "HubSpot" : "Manual"),
|
|
378
|
+
listName: optionalTrimmed(entry.sourceListName),
|
|
379
|
+
},
|
|
380
|
+
createdAt: normalizeCreatedAt(entry.createdAt),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function buildDncListPath(input) {
|
|
384
|
+
const page = clampListPositiveInt(input.page, 1, Number.MAX_SAFE_INTEGER);
|
|
385
|
+
const limit = clampListPositiveInt(input.limit, 25, MAX_DNC_LIST_LIMIT);
|
|
386
|
+
const sourceType = normalizeDncSourceType(input.sourceType);
|
|
387
|
+
const params = new URLSearchParams({
|
|
388
|
+
page: String(page),
|
|
389
|
+
limit: String(limit),
|
|
390
|
+
});
|
|
391
|
+
const search = optionalTrimmed(input.search);
|
|
392
|
+
const listName = optionalTrimmed(input.listName);
|
|
393
|
+
if (search)
|
|
394
|
+
params.set("search", search);
|
|
395
|
+
if (listName)
|
|
396
|
+
params.set("listName", listName);
|
|
397
|
+
if (sourceType !== "all")
|
|
398
|
+
params.set("sourceType", sourceType);
|
|
399
|
+
if (input.includeDeleted === true)
|
|
400
|
+
params.set("includeDeleted", "true");
|
|
401
|
+
return {
|
|
402
|
+
path: `/api/v3/dnc-list?${params.toString()}`,
|
|
403
|
+
page,
|
|
404
|
+
limit,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
338
407
|
function makeSignature(payload) {
|
|
339
408
|
return createHmac("sha256", confirmationSecret)
|
|
340
409
|
.update(JSON.stringify(payload))
|
|
@@ -556,6 +625,29 @@ async function runDncSpotChecks(entries) {
|
|
|
556
625
|
}
|
|
557
626
|
return checks;
|
|
558
627
|
}
|
|
628
|
+
export async function listDncEntries(input = {}) {
|
|
629
|
+
const workspace = await getVerifiedActiveWorkspace();
|
|
630
|
+
const api = getApi();
|
|
631
|
+
const request = buildDncListPath(input);
|
|
632
|
+
const response = await api.get(request.path);
|
|
633
|
+
const pagination = response.pagination ?? {};
|
|
634
|
+
return {
|
|
635
|
+
ok: true,
|
|
636
|
+
workspaceId: workspace.id,
|
|
637
|
+
workspaceName: workspace.name,
|
|
638
|
+
pagination: {
|
|
639
|
+
total: pagination.total ?? 0,
|
|
640
|
+
page: pagination.page ?? request.page,
|
|
641
|
+
limit: pagination.limit ?? request.limit,
|
|
642
|
+
totalPages: pagination.totalPages ?? 0,
|
|
643
|
+
},
|
|
644
|
+
listNames: Array.isArray(response.listNames) ? response.listNames : [],
|
|
645
|
+
entries: Array.isArray(response.entries)
|
|
646
|
+
? response.entries.map(compactDncListEntry)
|
|
647
|
+
: [],
|
|
648
|
+
guidance: "This is Sellable's workspace DNC list used by DNC Check.",
|
|
649
|
+
};
|
|
650
|
+
}
|
|
559
651
|
export async function loadCsvDncEntries(input) {
|
|
560
652
|
const workspace = await getVerifiedActiveWorkspace();
|
|
561
653
|
const api = getApi();
|
|
@@ -7,7 +7,6 @@ export type EngagementPost = {
|
|
|
7
7
|
name: string;
|
|
8
8
|
headline: string;
|
|
9
9
|
profileUrl: string;
|
|
10
|
-
followerCount: number | null;
|
|
11
10
|
};
|
|
12
11
|
engagement: {
|
|
13
12
|
likes: number;
|
|
@@ -15,16 +14,6 @@ export type EngagementPost = {
|
|
|
15
14
|
shares: number;
|
|
16
15
|
total: number;
|
|
17
16
|
};
|
|
18
|
-
reachSignals: {
|
|
19
|
-
targetFollowerMin: number | null;
|
|
20
|
-
targetFollowerMax: number | null;
|
|
21
|
-
followerBandFit: "in_target_band" | "below_target_band" | "above_target_band" | "unknown";
|
|
22
|
-
weightedEngagement: number;
|
|
23
|
-
engagementPer1kFollowers: number | null;
|
|
24
|
-
weightedEngagementPer1kFollowers: number | null;
|
|
25
|
-
reachPenaltyMultiplier: number;
|
|
26
|
-
reachAdjustedScore: number | null;
|
|
27
|
-
};
|
|
28
17
|
contentPreview: string;
|
|
29
18
|
};
|
|
30
19
|
export type SearchEngagementPostsInput = {
|
|
@@ -33,8 +22,6 @@ export type SearchEngagementPostsInput = {
|
|
|
33
22
|
maxAgeDays?: number;
|
|
34
23
|
minTotalEngagement?: number;
|
|
35
24
|
maxPosts?: number;
|
|
36
|
-
targetFollowerMin?: number;
|
|
37
|
-
targetFollowerMax?: number;
|
|
38
25
|
excludePostUrls?: string[];
|
|
39
26
|
};
|
|
40
27
|
export type SearchEngagementPostsResponse = {
|
|
@@ -76,14 +63,6 @@ export declare const engageDiscoveryToolDefinitions: {
|
|
|
76
63
|
type: string;
|
|
77
64
|
description: string;
|
|
78
65
|
};
|
|
79
|
-
targetFollowerMin: {
|
|
80
|
-
type: string;
|
|
81
|
-
description: string;
|
|
82
|
-
};
|
|
83
|
-
targetFollowerMax: {
|
|
84
|
-
type: string;
|
|
85
|
-
description: string;
|
|
86
|
-
};
|
|
87
66
|
excludePostUrls: {
|
|
88
67
|
type: string;
|
|
89
68
|
items: {
|
|
@@ -27,14 +27,6 @@ export const engageDiscoveryToolDefinitions = [
|
|
|
27
27
|
type: "number",
|
|
28
28
|
description: "Max posts to return after filtering (default 25).",
|
|
29
29
|
},
|
|
30
|
-
targetFollowerMin: {
|
|
31
|
-
type: "number",
|
|
32
|
-
description: "Optional lower bound for the author's follower count when comparing reach-normalized hook performance. Does not hard-filter; adds reach signals and prioritizes the target band when follower data is available.",
|
|
33
|
-
},
|
|
34
|
-
targetFollowerMax: {
|
|
35
|
-
type: "number",
|
|
36
|
-
description: "Optional upper bound for the author's follower count when comparing reach-normalized hook performance. Does not hard-filter; adds reach signals and prioritizes the target band when follower data is available.",
|
|
37
|
-
},
|
|
38
30
|
excludePostUrls: {
|
|
39
31
|
type: "array",
|
|
40
32
|
items: { type: "string" },
|
|
@@ -71,101 +63,6 @@ function safeNumber(value) {
|
|
|
71
63
|
const n = typeof value === "number" ? value : Number(value);
|
|
72
64
|
return Number.isFinite(n) ? n : 0;
|
|
73
65
|
}
|
|
74
|
-
function parseFollowerCount(value) {
|
|
75
|
-
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
76
|
-
return Math.round(value);
|
|
77
|
-
}
|
|
78
|
-
if (typeof value !== "string")
|
|
79
|
-
return null;
|
|
80
|
-
const cleaned = value.trim().toLowerCase().replace(/,/g, "");
|
|
81
|
-
if (!cleaned)
|
|
82
|
-
return null;
|
|
83
|
-
const match = cleaned.match(/(\d+(?:\.\d+)?)\s*([kmb])?/);
|
|
84
|
-
if (!match)
|
|
85
|
-
return null;
|
|
86
|
-
const base = Number(match[1]);
|
|
87
|
-
if (!Number.isFinite(base) || base <= 0)
|
|
88
|
-
return null;
|
|
89
|
-
const suffix = match[2] || "";
|
|
90
|
-
const multiplier = suffix === "b" ? 1_000_000_000 : suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : 1;
|
|
91
|
-
return Math.round(base * multiplier);
|
|
92
|
-
}
|
|
93
|
-
function extractFollowerCount(post) {
|
|
94
|
-
const candidates = [
|
|
95
|
-
post?.author?.followerCount,
|
|
96
|
-
post?.author?.followersCount,
|
|
97
|
-
post?.author?.followers,
|
|
98
|
-
post?.author?.follower_count,
|
|
99
|
-
post?.author?.linkedinFollowers,
|
|
100
|
-
post?.author?.stats?.followers,
|
|
101
|
-
post?.followerCount,
|
|
102
|
-
post?.followersCount,
|
|
103
|
-
post?.followers,
|
|
104
|
-
post?.authorFollowerCount,
|
|
105
|
-
];
|
|
106
|
-
for (const candidate of candidates) {
|
|
107
|
-
const parsed = parseFollowerCount(candidate);
|
|
108
|
-
if (parsed !== null)
|
|
109
|
-
return parsed;
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
function normalizeFollowerBound(value) {
|
|
114
|
-
const parsed = parseFollowerCount(value);
|
|
115
|
-
return parsed && parsed > 0 ? parsed : null;
|
|
116
|
-
}
|
|
117
|
-
function followerBandFit(followerCount, targetFollowerMin, targetFollowerMax) {
|
|
118
|
-
if (!followerCount)
|
|
119
|
-
return "unknown";
|
|
120
|
-
if (targetFollowerMin && followerCount < targetFollowerMin) {
|
|
121
|
-
return "below_target_band";
|
|
122
|
-
}
|
|
123
|
-
if (targetFollowerMax && followerCount > targetFollowerMax) {
|
|
124
|
-
return "above_target_band";
|
|
125
|
-
}
|
|
126
|
-
if (targetFollowerMin || targetFollowerMax)
|
|
127
|
-
return "in_target_band";
|
|
128
|
-
return "unknown";
|
|
129
|
-
}
|
|
130
|
-
function bandMultiplier(fit, followerCount, targetFollowerMax) {
|
|
131
|
-
if (fit === "in_target_band")
|
|
132
|
-
return 1;
|
|
133
|
-
if (fit === "below_target_band")
|
|
134
|
-
return 0.75;
|
|
135
|
-
if (fit === "unknown")
|
|
136
|
-
return 0.4;
|
|
137
|
-
if (!followerCount || !targetFollowerMax)
|
|
138
|
-
return 0.45;
|
|
139
|
-
if (followerCount <= targetFollowerMax * 2)
|
|
140
|
-
return 0.65;
|
|
141
|
-
if (followerCount <= targetFollowerMax * 5)
|
|
142
|
-
return 0.35;
|
|
143
|
-
return 0.15;
|
|
144
|
-
}
|
|
145
|
-
function reachSignals(engagement, followerCount, targetFollowerMin, targetFollowerMax) {
|
|
146
|
-
const weightedEngagement = engagement.likes + engagement.comments * 4 + engagement.shares * 12;
|
|
147
|
-
const fit = followerBandFit(followerCount, targetFollowerMin, targetFollowerMax);
|
|
148
|
-
const engagementPer1kFollowers = followerCount
|
|
149
|
-
? Number(((engagement.total / followerCount) * 1000).toFixed(3))
|
|
150
|
-
: null;
|
|
151
|
-
const weightedEngagementPer1kFollowers = followerCount
|
|
152
|
-
? Number(((weightedEngagement / followerCount) * 1000).toFixed(3))
|
|
153
|
-
: null;
|
|
154
|
-
const reachPenaltyMultiplier = bandMultiplier(fit, followerCount, targetFollowerMax);
|
|
155
|
-
const reachAdjustedScore = weightedEngagementPer1kFollowers === null
|
|
156
|
-
? null
|
|
157
|
-
: Number((weightedEngagementPer1kFollowers * reachPenaltyMultiplier).toFixed(3));
|
|
158
|
-
return {
|
|
159
|
-
targetFollowerMin,
|
|
160
|
-
targetFollowerMax,
|
|
161
|
-
followerBandFit: fit,
|
|
162
|
-
weightedEngagement,
|
|
163
|
-
engagementPer1kFollowers,
|
|
164
|
-
weightedEngagementPer1kFollowers,
|
|
165
|
-
reachPenaltyMultiplier,
|
|
166
|
-
reachAdjustedScore,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
66
|
export async function searchEngagementPosts(input) {
|
|
170
67
|
const api = getApi();
|
|
171
68
|
const keywords = (input.keywords || [])
|
|
@@ -182,10 +79,6 @@ export async function searchEngagementPosts(input) {
|
|
|
182
79
|
const maxPosts = typeof input.maxPosts === "number" && input.maxPosts > 0
|
|
183
80
|
? Math.min(50, input.maxPosts)
|
|
184
81
|
: 25;
|
|
185
|
-
const targetFollowerMin = normalizeFollowerBound(input.targetFollowerMin);
|
|
186
|
-
const targetFollowerMax = normalizeFollowerBound(input.targetFollowerMax);
|
|
187
|
-
const useTargetFollowerBand = Boolean(targetFollowerMin || targetFollowerMax);
|
|
188
|
-
const collectLimit = useTargetFollowerBand ? 50 : maxPosts;
|
|
189
82
|
const exclude = new Set((input.excludePostUrls || [])
|
|
190
83
|
.map((u) => normalizePostUrl(u))
|
|
191
84
|
.filter(Boolean));
|
|
@@ -194,13 +87,11 @@ export async function searchEngagementPosts(input) {
|
|
|
194
87
|
keywords: keywords.map((keyword) => ({ keyword })),
|
|
195
88
|
page,
|
|
196
89
|
});
|
|
197
|
-
const rawPosts =
|
|
198
|
-
? response.
|
|
199
|
-
: Array.isArray(response?.
|
|
200
|
-
? response.
|
|
201
|
-
:
|
|
202
|
-
? response.posts
|
|
203
|
-
: [];
|
|
90
|
+
const rawPosts = Array.isArray(response?.topPostsForLLM)
|
|
91
|
+
? response.topPostsForLLM
|
|
92
|
+
: Array.isArray(response?.posts)
|
|
93
|
+
? response.posts
|
|
94
|
+
: [];
|
|
204
95
|
const now = Date.now();
|
|
205
96
|
const oldestMs = now - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
206
97
|
let tooOld = 0;
|
|
@@ -220,7 +111,6 @@ export async function searchEngagementPosts(input) {
|
|
|
220
111
|
const comments = safeNumber(p?.engagement?.comments);
|
|
221
112
|
const shares = safeNumber(p?.engagement?.shares);
|
|
222
113
|
const total = likes + comments + shares;
|
|
223
|
-
const engagement = { likes, comments, shares, total };
|
|
224
114
|
if (total < minTotalEngagement) {
|
|
225
115
|
tooLowEngagement += 1;
|
|
226
116
|
continue;
|
|
@@ -231,7 +121,6 @@ export async function searchEngagementPosts(input) {
|
|
|
231
121
|
tooOld += 1;
|
|
232
122
|
continue;
|
|
233
123
|
}
|
|
234
|
-
const followerCount = extractFollowerCount(p);
|
|
235
124
|
kept.push({
|
|
236
125
|
postId: String(p?.id || ""),
|
|
237
126
|
url,
|
|
@@ -243,31 +132,15 @@ export async function searchEngagementPosts(input) {
|
|
|
243
132
|
name: String(p?.author?.name || ""),
|
|
244
133
|
headline: String(p?.author?.headline || ""),
|
|
245
134
|
profileUrl: String(p?.author?.profileUrl || ""),
|
|
246
|
-
followerCount,
|
|
247
135
|
},
|
|
248
|
-
engagement,
|
|
249
|
-
reachSignals: reachSignals(engagement, followerCount, targetFollowerMin, targetFollowerMax),
|
|
136
|
+
engagement: { likes, comments, shares, total },
|
|
250
137
|
contentPreview: previewText(String(p?.content || ""), 220),
|
|
251
138
|
});
|
|
252
|
-
if (kept.length >=
|
|
139
|
+
if (kept.length >= maxPosts)
|
|
253
140
|
break;
|
|
254
141
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const scoreDelta = (b.reachSignals.reachAdjustedScore ?? -1) -
|
|
258
|
-
(a.reachSignals.reachAdjustedScore ?? -1);
|
|
259
|
-
if (scoreDelta !== 0)
|
|
260
|
-
return scoreDelta;
|
|
261
|
-
return b.engagement.total - a.engagement.total;
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
// Sort by total engagement desc for a predictable shortlist.
|
|
266
|
-
kept.sort((a, b) => b.engagement.total - a.engagement.total);
|
|
267
|
-
}
|
|
268
|
-
if (kept.length > maxPosts) {
|
|
269
|
-
kept.length = maxPosts;
|
|
270
|
-
}
|
|
142
|
+
// Sort by total engagement desc for a predictable shortlist.
|
|
143
|
+
kept.sort((a, b) => b.engagement.total - a.engagement.total);
|
|
271
144
|
return {
|
|
272
145
|
success: true,
|
|
273
146
|
posts: kept,
|