@sellable/mcp 0.1.144 → 0.1.145
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 +14 -3
- package/agents/source-scout-linkedin-engagement.md +4 -3
- package/agents/source-scout-prospeo-contact.md +1 -1
- package/agents/source-scout-sales-nav.md +3 -2
- package/dist/server.js +29 -59
- package/dist/tools/auth.js +5 -5
- package/dist/tools/leads.js +72 -17
- package/dist/tools/registry.d.ts +4186 -0
- package/dist/tools/registry.js +59 -0
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +10 -9
- package/skills/create-campaign-v2/SKILL.md +6 -6
- package/skills/create-campaign-v2/SOUL.md +2 -2
- package/skills/create-campaign-v2/core/auto-execute.README.md +4 -4
- package/skills/create-campaign-v2/core/auto-execute.yaml +4 -4
- package/skills/create-campaign-v2/core/flow.v2.json +2 -2
- package/skills/create-campaign-v2/references/approval-gate-framing.md +1 -1
- package/skills/create-campaign-v2/references/filter-leads.md +42 -0
- package/skills/create-campaign-v2/references/sample-validation-loop.md +13 -13
- package/skills/create-campaign-v2/references/step-13-import-leads.md +9 -6
- package/skills/create-campaign-v2/references/watch-guide-narration.md +2 -2
- package/skills/load-voice/SKILL.md +129 -0
- package/skills/providers/prospeo.md +2 -1
- package/skills/providers/sales-nav.md +4 -2
- package/skills/providers/signal-discovery.md +12 -10
package/README.md
CHANGED
|
@@ -16,10 +16,11 @@ Each message gets 5+ minutes of Claude attention with deep research - no other t
|
|
|
16
16
|
|
|
17
17
|
### Prompt Source Of Truth
|
|
18
18
|
|
|
19
|
-
There are
|
|
19
|
+
There are three public Sellable entrypoints shared across hosts:
|
|
20
20
|
|
|
21
21
|
- `sellable:create-campaign`
|
|
22
22
|
- `sellable:interview`
|
|
23
|
+
- `sellable:load-voice`
|
|
23
24
|
|
|
24
25
|
The create-campaign public wrapper at
|
|
25
26
|
`mcp/sellable/skills/create-campaign/SKILL.md` handles auth/bootstrap and loads
|
|
@@ -32,6 +33,11 @@ from:
|
|
|
32
33
|
|
|
33
34
|
- `mcp/sellable/skills/interview/SKILL.md`
|
|
34
35
|
|
|
36
|
+
The load-voice public wrapper loads the current voice/company memory for use in
|
|
37
|
+
writing, application answers, posts, replies, and reviews from:
|
|
38
|
+
|
|
39
|
+
- `mcp/sellable/skills/load-voice/SKILL.md`
|
|
40
|
+
|
|
35
41
|
Keep `create-campaign-v2` internal. Do not advertise it as a public command.
|
|
36
42
|
|
|
37
43
|
### 1. One-Command Agent Install
|
|
@@ -127,8 +133,9 @@ The installer does the full local setup:
|
|
|
127
133
|
`mcp__sellable__*` tools into skill sessions
|
|
128
134
|
|
|
129
135
|
After the installer passes, fully quit and reopen Codex Desktop. Start a new
|
|
130
|
-
thread and select `Sellable Create Campaign
|
|
131
|
-
invoke `$sellable:create-campaign
|
|
136
|
+
thread and select `Sellable Create Campaign`, `Sellable Identity Interview`, or
|
|
137
|
+
`Sellable Load Voice`; or invoke `$sellable:create-campaign`,
|
|
138
|
+
`$sellable:interview`, or `$sellable:load-voice`. If the app still says
|
|
132
139
|
`mcp__sellable__*` tools are missing after the installer passes, check that
|
|
133
140
|
`~/.codex/config.toml` contains both `[marketplaces.sellable]` and
|
|
134
141
|
`[plugins."sellable@sellable"]`.
|
|
@@ -141,13 +148,17 @@ Use these names consistently:
|
|
|
141
148
|
|
|
142
149
|
- Claude Code command: `/sellable:create-campaign`
|
|
143
150
|
- Claude Code command: `/sellable:interview`
|
|
151
|
+
- Claude Code command: `/sellable:load-voice`
|
|
144
152
|
- Codex command: `$sellable:create-campaign`
|
|
145
153
|
- Codex command: `$sellable:interview`
|
|
154
|
+
- Codex command: `$sellable:load-voice`
|
|
146
155
|
- Codex Desktop plugin: `sellable@sellable`
|
|
147
156
|
- Codex visible skill: `Sellable Create Campaign`
|
|
148
157
|
- Codex visible skill: `Sellable Identity Interview`
|
|
158
|
+
- Codex visible skill: `Sellable Load Voice`
|
|
149
159
|
- Codex skill frontmatter name: `create-campaign`
|
|
150
160
|
- Codex skill frontmatter name: `interview`
|
|
161
|
+
- Codex skill frontmatter name: `load-voice`
|
|
151
162
|
- MCP server name: `sellable`
|
|
152
163
|
- Internal workflow prompt: `create-campaign-v2`
|
|
153
164
|
|
|
@@ -51,7 +51,8 @@ currentStep: "signal-discovery" })` before sampling so the watched Signal
|
|
|
51
51
|
visible headline/display-name cues only. Do not enrich people during
|
|
52
52
|
viability estimation.
|
|
53
53
|
7. Compute capacity before recommending the source: source target good-fit
|
|
54
|
-
leads (default
|
|
54
|
+
leads (default 150 for Signal Discovery unless the parent supplies a target),
|
|
55
|
+
reachable engagers,
|
|
55
56
|
sampled ICP-fit rate as `n/N` plus an easy percentage/range, expected usable
|
|
56
57
|
leads per 100 engagers before and after a conservative dedupe/cleanup
|
|
57
58
|
factor, required engagers to scrape (`source target / fit rate`), average
|
|
@@ -93,8 +94,8 @@ Evidence standards:
|
|
|
93
94
|
exist, how many sampled engagers looked in-ICP, how many good-fit prospects
|
|
94
95
|
that implies per 100 engagers, how many usable prospects one right-content
|
|
95
96
|
post should yield after cleanup, how many engagers must be scraped for the
|
|
96
|
-
|
|
97
|
+
150-good-fit source target, how many posts are needed for that source target,
|
|
97
98
|
and which posts you would use. Also say the first campaign import remains the
|
|
98
|
-
bounded
|
|
99
|
+
bounded review batch.
|
|
99
100
|
- If `fetch_post_engagers` is unavailable or fails, report that explicitly and mark the estimate lower-confidence.
|
|
100
101
|
- Keep LinkedIn Engagement viable when selected posts can produce roughly 150+ ICP-fit warm prospects before final filtering, even if Sales Nav is more scalable.
|
|
@@ -27,7 +27,7 @@ Process:
|
|
|
27
27
|
1. Read the campaign brief, source intake, kickoff doc, or lane prompt supplied by the parent.
|
|
28
28
|
2. Identify whether this is domain/account targeting or broad persona expansion.
|
|
29
29
|
3. For domain targeting, use or create the standalone `domainFilterId` before searching; never pass raw domains directly into `search_prospeo`.
|
|
30
|
-
4. Run the narrowest useful Prospeo people preview and 1-2 refinements if quality or scale is unclear.
|
|
30
|
+
4. Run the narrowest useful Prospeo people preview and 1-2 refinements if quality or scale is unclear. Check scale against a default 300+ good-fit target, capped at 2,500 source candidates unless the parent supplies a different target.
|
|
31
31
|
5. Call out that Prospeo gives contact/account coverage but usually weaker LinkedIn intent than LinkedIn Engagement or Sales Nav activity slices.
|
|
32
32
|
|
|
33
33
|
Return a concise structured result with:
|
|
@@ -28,8 +28,9 @@ Process:
|
|
|
28
28
|
2. Preserve target role names with `CURRENT_TITLE` lookups; do not rely on seniority alone when the brief names concrete roles.
|
|
29
29
|
3. When `lookup_sales_nav_filter` returns multiple title options, choose the closest semantic title match instead of the first result.
|
|
30
30
|
4. Build a broad-but-reasonable baseline from role/title, geography, company size, industry/account context, and recent LinkedIn activity when relevant.
|
|
31
|
-
5. Check scale against the source target good-fit lead count (default 300
|
|
32
|
-
unless the parent supplies a target).
|
|
31
|
+
5. Check scale against the source target good-fit lead count (default 300+
|
|
32
|
+
for Sales Nav, capped at 2,500 source candidates unless the parent supplies a target).
|
|
33
|
+
If raw preview volume or projected usable volume
|
|
33
34
|
is below target, do not present the tiny result as the scale fallback yet.
|
|
34
35
|
Loosen nonessential filters in order: remove recent-activity first, widen
|
|
35
36
|
adjacent title variants, widen geography/company-size constraints, and only
|
package/dist/server.js
CHANGED
|
@@ -2,35 +2,36 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { getSkillByName, listSkills } from "./skills.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { bootstrapCreateCampaign
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { handleStartCliLogin, handleWaitForCliLogin,
|
|
11
|
-
import {
|
|
12
|
-
import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign,
|
|
13
|
-
import { bootstrapEngage, bootstrapEngageMulti,
|
|
14
|
-
import {
|
|
15
|
-
import { copySenderConfigTool,
|
|
16
|
-
import {
|
|
17
|
-
import { bulkEnrichWithProspeo,
|
|
18
|
-
import {
|
|
19
|
-
import { cancelLeadImport, confirmLeadList, getProviderPrompt, importLeads,
|
|
20
|
-
import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts,
|
|
21
|
-
import { getCampaignNavigationState
|
|
22
|
-
import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence,
|
|
23
|
-
import {
|
|
24
|
-
import { completeSenderResearch, getMessagePrompt, getPostFindLeadsScoutRegistry, getSourceScoutRegistry, getSubskillAsset, getSubskillPrompt, listSubskillPrompts,
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
5
|
+
import { getAuthStatus } from "./tools/auth.js";
|
|
6
|
+
import { handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
|
|
7
|
+
import { bootstrapCreateCampaign } from "./tools/bootstrap.js";
|
|
8
|
+
import { createCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
|
|
9
|
+
import { queueCells, updateCell } from "./tools/cells.js";
|
|
10
|
+
import { handleStartCliLogin, handleWaitForCliLogin, } from "./tools/cli-login.js";
|
|
11
|
+
import { getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
|
|
12
|
+
import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
|
|
13
|
+
import { bootstrapEngage, bootstrapEngageMulti, } from "./tools/engage-bootstrap.js";
|
|
14
|
+
import { searchEngagementPosts } from "./tools/engage-discovery.js";
|
|
15
|
+
import { copySenderConfigTool, getEngageMemoryTool, migrateFlatConfigsTool, recordEngageProvenSearchTool, setEngageStyleGuideTool, upsertEngageTrackedPersonTool, } from "./tools/engage-memory.js";
|
|
16
|
+
import { getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
|
|
17
|
+
import { bulkEnrichWithProspeo, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
|
|
18
|
+
import { getCampaignFramework } from "./tools/framework.js";
|
|
19
|
+
import { cancelLeadImport, confirmLeadList, getProviderPrompt, importLeads, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
|
|
20
|
+
import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, } from "./tools/linkedin.js";
|
|
21
|
+
import { getCampaignNavigationState } from "./tools/navigation.js";
|
|
22
|
+
import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
|
|
23
|
+
import { upsertRubric } from "./tools/processing.js";
|
|
24
|
+
import { completeSenderResearch, getMessagePrompt, getPostFindLeadsScoutRegistry, getSourceScoutRegistry, getSubskillAsset, getSubskillPrompt, listSubskillPrompts, searchSubskillPrompts, } from "./tools/prompts.js";
|
|
25
|
+
import { waitForCampaignTableReady, waitForLeadListReady, } from "./tools/readiness.js";
|
|
26
|
+
import { allTools } from "./tools/registry.js";
|
|
27
|
+
import { getRows, getTableRows, getTableRowsMinimal } from "./tools/rows.js";
|
|
28
|
+
import { addRubricItem, checkRubric, deleteRubricItem, draftRubrics, saveRubrics, selectNecessaryRubrics, updateRubricItem, waitForRubricResults, } from "./tools/rubrics.js";
|
|
29
|
+
import { getSender, listSenders } from "./tools/senders.js";
|
|
30
|
+
import { attachRecommendedSequence, attachSequence, createWorkflowTable, } from "./tools/sequencer.js";
|
|
31
|
+
import { listTables } from "./tools/tables.js";
|
|
32
|
+
import { handleVerifyTableRow } from "./tools/verify-row.js";
|
|
32
33
|
import { sanitizeWatchUrlsForMcpResult } from "./tools/watch-url-security.js";
|
|
33
|
-
import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace,
|
|
34
|
+
import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, } from "./tools/workspaces.js";
|
|
34
35
|
import { checkForUpdates, logUpdateNotice } from "./update-check.js";
|
|
35
36
|
const server = new Server({
|
|
36
37
|
name: "sellable-mcp",
|
|
@@ -41,37 +42,6 @@ const server = new Server({
|
|
|
41
42
|
prompts: {},
|
|
42
43
|
},
|
|
43
44
|
});
|
|
44
|
-
const allTools = [
|
|
45
|
-
...campaignToolDefinitions,
|
|
46
|
-
...authToolDefinitions,
|
|
47
|
-
startCliLoginToolDef,
|
|
48
|
-
waitForCliLoginToolDef,
|
|
49
|
-
...bootstrapToolDefinitions,
|
|
50
|
-
...engageBootstrapToolDefinitions,
|
|
51
|
-
...engageDiscoveryToolDefinitions,
|
|
52
|
-
...workspaceToolDefinitions,
|
|
53
|
-
...frameworkToolDefinitions,
|
|
54
|
-
...contextToolDefinitions,
|
|
55
|
-
...navigationToolDefinitions,
|
|
56
|
-
...leadToolDefinitions,
|
|
57
|
-
...enrichmentToolDefinitions,
|
|
58
|
-
...processingToolDefinitions,
|
|
59
|
-
...rubricToolDefinitions,
|
|
60
|
-
...readinessToolDefinitions,
|
|
61
|
-
...rowToolDefinitions,
|
|
62
|
-
...cellToolDefinitions,
|
|
63
|
-
...promptToolDefinitions,
|
|
64
|
-
...linkedinToolDefinitions,
|
|
65
|
-
...onDemandToolDefinitions,
|
|
66
|
-
...directCampaignToolDefinitions,
|
|
67
|
-
...senderToolDefinitions,
|
|
68
|
-
...engageStateToolDefinitions,
|
|
69
|
-
...engageMemoryToolDefinitions,
|
|
70
|
-
...sequencerToolDefinitions,
|
|
71
|
-
...tableToolDefinitions,
|
|
72
|
-
...blueprintCommitToolDefinitions,
|
|
73
|
-
...verifyRowToolDefinitions,
|
|
74
|
-
];
|
|
75
45
|
function parseOptionalNumber(value) {
|
|
76
46
|
if (typeof value === "number" && Number.isFinite(value))
|
|
77
47
|
return value;
|
package/dist/tools/auth.js
CHANGED
|
@@ -22,7 +22,7 @@ function appendUpdateNotice(notice, update) {
|
|
|
22
22
|
export const authToolDefinitions = [
|
|
23
23
|
{
|
|
24
24
|
name: "get_auth_status",
|
|
25
|
-
description: "Verify Sellable authentication early. Call this before any campaign work. Also performs a cached Sellable package update check. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set.
|
|
25
|
+
description: "Verify Sellable authentication early. Call this before any campaign work. Also performs a cached Sellable package update check. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. When auth succeeds, use _userNotice when workspace context is useful; it is written in customer-facing product language and may include update guidance. CRITICAL: When auth FAILS with a missing/invalid token (error.type is 'config' or 'auth'), the response includes an `agentInstruction` field with the verbatim FTUX magic-link signup flow. Follow agentInstruction VERBATIM. Do NOT show the user error.guidance — that's a fallback for non-interactive contexts only.",
|
|
26
26
|
inputSchema: {
|
|
27
27
|
type: "object",
|
|
28
28
|
properties: {},
|
|
@@ -92,8 +92,8 @@ export async function getAuthStatus() {
|
|
|
92
92
|
const envLabel = config.activeEnvName || null;
|
|
93
93
|
const wsLabel = workspaceName || activeWorkspaceId;
|
|
94
94
|
const notice = envLabel
|
|
95
|
-
? `
|
|
96
|
-
: `
|
|
95
|
+
? `I’m building this in ${wsLabel} (${envLabel}).`
|
|
96
|
+
: `I’m building this in ${wsLabel}.`;
|
|
97
97
|
return {
|
|
98
98
|
ok: true,
|
|
99
99
|
configPath,
|
|
@@ -138,8 +138,8 @@ export async function getAuthStatus() {
|
|
|
138
138
|
"5) Call `mcp__sellable__wait_for_cli_login({ sessionId })` using the sessionId returned by start_cli_login. " +
|
|
139
139
|
"6) If the result is `error.type === 'tool_timeout_guard'`, IMMEDIATELY re-call wait_for_cli_login with the SAME sessionId — do not narrate, do not call start_cli_login again. Loop until you get a different result. " +
|
|
140
140
|
"7) On `ok: true`, the user is signed in and `~/.sellable/config.json` has been written. Branch on `isReturningUser`: " +
|
|
141
|
-
"if true, say `You're in — {activeWorkspaceName} workspace, ready to roll.\\n\\nNow — paste the LinkedIn profile
|
|
142
|
-
"if false, say `You're set up — your {activeWorkspaceName} workspace is ready.\\n\\nNow — paste the LinkedIn profile
|
|
141
|
+
"if true, say `You're in — {activeWorkspaceName} workspace, ready to roll.\\n\\nNow — paste the LinkedIn profile or company website for the client/company this campaign is for. I’ll use that to resolve the campaign identity before we pick the target, offer, proof, and lead source.\\n\\ne.g. https://example.com or https://www.linkedin.com/in/client-handle`; " +
|
|
142
|
+
"if false, say `You're set up — your {activeWorkspaceName} workspace is ready.\\n\\nNow — paste the LinkedIn profile or company website for the client/company this campaign is for. I’ll use that to resolve the campaign identity before we pick the target, offer, proof, and lead source.\\n\\ne.g. https://example.com or https://www.linkedin.com/in/client-handle`";
|
|
143
143
|
if (error instanceof SellableApiError && error.isAuthError) {
|
|
144
144
|
return {
|
|
145
145
|
...base,
|
package/dist/tools/leads.js
CHANGED
|
@@ -14,10 +14,11 @@ const workspaceRoot = resolveWorkspaceRoot(entryDir);
|
|
|
14
14
|
const skillsRoot = resolveSkillsDir();
|
|
15
15
|
const signalProviderConfigPath = join(skillsRoot, "create-campaign/core/providers/signal-discovery.json");
|
|
16
16
|
const leadImportLimitsPath = join(workspaceRoot, "lead-import-limits.json");
|
|
17
|
+
const campaignSourceDefaultsPath = join(workspaceRoot, "campaign-source-defaults.json");
|
|
17
18
|
const defaultLeadImportLimits = {
|
|
18
19
|
apollo: { maxImportCount: 2500 },
|
|
19
20
|
"sales-nav": { maxImportCount: 2500 },
|
|
20
|
-
prospeo: { maxImportCount:
|
|
21
|
+
prospeo: { maxImportCount: 2500 },
|
|
21
22
|
"signal-discovery": { maxImportCount: 2500 },
|
|
22
23
|
};
|
|
23
24
|
const defaultSignalDiscoveryConfig = {
|
|
@@ -25,6 +26,28 @@ const defaultSignalDiscoveryConfig = {
|
|
|
25
26
|
promisingPostsTarget: 3,
|
|
26
27
|
},
|
|
27
28
|
};
|
|
29
|
+
const defaultCampaignSourceDefaults = {
|
|
30
|
+
reviewBatch: {
|
|
31
|
+
defaultSize: 25,
|
|
32
|
+
minProjectedPass: 5,
|
|
33
|
+
},
|
|
34
|
+
providers: {
|
|
35
|
+
"signal-discovery": {
|
|
36
|
+
targetGoodFitLeads: 150,
|
|
37
|
+
defaultFitRate: 0.15,
|
|
38
|
+
maxSourceCandidates: 2500,
|
|
39
|
+
postCoverageBuffer: 1.2,
|
|
40
|
+
},
|
|
41
|
+
"sales-nav": {
|
|
42
|
+
targetGoodFitLeads: 300,
|
|
43
|
+
maxSourceCandidates: 2500,
|
|
44
|
+
},
|
|
45
|
+
prospeo: {
|
|
46
|
+
targetGoodFitLeads: 300,
|
|
47
|
+
maxSourceCandidates: 2500,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
28
51
|
const prospeoFilterValueSchema = {
|
|
29
52
|
type: "object",
|
|
30
53
|
description: "Include/exclude list filter (values must match Prospeo enums)",
|
|
@@ -245,6 +268,39 @@ function loadLeadImportLimits() {
|
|
|
245
268
|
return defaultLeadImportLimits;
|
|
246
269
|
}
|
|
247
270
|
}
|
|
271
|
+
function loadCampaignSourceDefaults() {
|
|
272
|
+
if (!existsSync(campaignSourceDefaultsPath)) {
|
|
273
|
+
return defaultCampaignSourceDefaults;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const raw = readFileSync(campaignSourceDefaultsPath, "utf-8");
|
|
277
|
+
const parsed = JSON.parse(raw);
|
|
278
|
+
return {
|
|
279
|
+
reviewBatch: {
|
|
280
|
+
...defaultCampaignSourceDefaults.reviewBatch,
|
|
281
|
+
...(parsed.reviewBatch ?? {}),
|
|
282
|
+
},
|
|
283
|
+
providers: {
|
|
284
|
+
...defaultCampaignSourceDefaults.providers,
|
|
285
|
+
...(parsed.providers ?? {}),
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return defaultCampaignSourceDefaults;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function getSignalDiscoverySourcePlanDefaults() {
|
|
294
|
+
const defaults = loadCampaignSourceDefaults();
|
|
295
|
+
const signalDefaults = defaults.providers["signal-discovery"];
|
|
296
|
+
const sourceCandidateTarget = Math.min(Math.ceil(signalDefaults.targetGoodFitLeads / signalDefaults.defaultFitRate), signalDefaults.maxSourceCandidates);
|
|
297
|
+
return {
|
|
298
|
+
targetGoodFitLeads: signalDefaults.targetGoodFitLeads,
|
|
299
|
+
defaultFitRate: signalDefaults.defaultFitRate,
|
|
300
|
+
sourceCandidateTarget,
|
|
301
|
+
reviewBatchSize: defaults.reviewBatch.defaultSize,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
248
304
|
function getMaxImportCount(provider) {
|
|
249
305
|
const limits = loadLeadImportLimits();
|
|
250
306
|
const max = limits[provider]?.maxImportCount;
|
|
@@ -443,16 +499,16 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
|
|
|
443
499
|
: ""}`
|
|
444
500
|
: `the approved ${providerLabel} source`;
|
|
445
501
|
const targetDetail = typeof targetLeadCount === "number"
|
|
446
|
-
? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source
|
|
502
|
+
? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source candidates before the bounded review batch is cloned.`
|
|
447
503
|
: "";
|
|
448
504
|
return {
|
|
449
505
|
stage: "review-batch",
|
|
450
506
|
headline: provider === "signal-discovery"
|
|
451
|
-
? "Scraping source
|
|
452
|
-
: "Importing source
|
|
507
|
+
? "Scraping source candidates from posts"
|
|
508
|
+
: "Importing source candidates",
|
|
453
509
|
visibleState: `The browser is showing ${providerLabel} import progress for ${sourceDetail}.${targetDetail}`,
|
|
454
510
|
agentIntent: "Codex is materializing the approved source into a lead list before cloning only the bounded review batch into the campaign.",
|
|
455
|
-
nextAction: "Wait for source
|
|
511
|
+
nextAction: "Wait for source candidates, then import the review batch",
|
|
456
512
|
safety: "Import is limited to the review batch.",
|
|
457
513
|
};
|
|
458
514
|
}
|
|
@@ -537,8 +593,6 @@ function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
|
|
|
537
593
|
safety: "Scrape approval is the next gate.",
|
|
538
594
|
};
|
|
539
595
|
}
|
|
540
|
-
const SOURCE_RECOMMENDATION_TARGET = 300;
|
|
541
|
-
const SOURCE_RECOMMENDATION_FIT_RATE = 0.15;
|
|
542
596
|
function escapeMarkdownTableCell(value) {
|
|
543
597
|
return (value || "-").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
544
598
|
}
|
|
@@ -547,6 +601,7 @@ function formatApproxInteger(value) {
|
|
|
547
601
|
return `~${rounded.toLocaleString("en-US")}`;
|
|
548
602
|
}
|
|
549
603
|
function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
|
|
604
|
+
const { targetGoodFitLeads, defaultFitRate, sourceCandidateTarget, reviewBatchSize, } = getSignalDiscoverySourcePlanDefaults();
|
|
550
605
|
const selectedCount = selectedPosts.length;
|
|
551
606
|
const totalEngagement = selectedPosts.reduce((sum, post) => sum + (post.likes ?? 0) + (post.comments ?? 0), 0);
|
|
552
607
|
const tableRows = selectedPosts
|
|
@@ -555,15 +610,14 @@ function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
|
|
|
555
610
|
return `| ${escapeMarkdownTableCell(post.authorName)} | ${escapeMarkdownTableCell(post.reason)} | ${formatApproxInteger(engagement)} |`;
|
|
556
611
|
})
|
|
557
612
|
.join("\n");
|
|
558
|
-
const
|
|
559
|
-
const estimatedGoodFit = totalEngagement * SOURCE_RECOMMENDATION_FIT_RATE;
|
|
613
|
+
const estimatedGoodFit = totalEngagement * defaultFitRate;
|
|
560
614
|
return `## Source Recommendation
|
|
561
615
|
|
|
562
616
|
Use Signal Discovery first.
|
|
563
617
|
|
|
564
|
-
**
|
|
565
|
-
**
|
|
566
|
-
**
|
|
618
|
+
**Good-fit target:** ~${targetGoodFitLeads.toLocaleString("en-US")} prospects after cleanup, enrichment, and filters<br>
|
|
619
|
+
**Source-candidate plan:** scrape ~${sourceCandidateTarget.toLocaleString("en-US")} raw engagers using a conservative ${Math.round(defaultFitRate * 100)}% fit-rate assumption<br>
|
|
620
|
+
**Review checkpoint:** import the first ${reviewBatchSize.toLocaleString("en-US")} leads into the campaign for fit and message review before scaling
|
|
567
621
|
|
|
568
622
|
### Selected posts
|
|
569
623
|
|
|
@@ -572,15 +626,15 @@ Use Signal Discovery first.
|
|
|
572
626
|
${tableRows || "| Selected posts | Campaign-matched public engagement | - |"}
|
|
573
627
|
|
|
574
628
|
**Total visible pool:** ${formatApproxInteger(totalEngagement)} engagers<br>
|
|
575
|
-
**Estimated good-fit pool at ${Math.round(
|
|
629
|
+
**Estimated good-fit pool at ${Math.round(defaultFitRate * 100)}%:** ${formatApproxInteger(estimatedGoodFit)} prospects before dedupe/risk cleanup
|
|
576
630
|
|
|
577
631
|
### Recommendation
|
|
578
632
|
|
|
579
633
|
Approve scraping these ${selectedCount} posts.
|
|
580
634
|
|
|
581
|
-
This gives enough volume to
|
|
635
|
+
This gives enough volume to work toward ~${targetGoodFitLeads.toLocaleString("en-US")} good-fit prospects while keeping the source tied to people already engaging with the campaign's strongest public buying signals.
|
|
582
636
|
|
|
583
|
-
**First pass:** build the source list, then import only
|
|
637
|
+
**First pass:** build the source list, then import only the ${reviewBatchSize.toLocaleString("en-US")}-lead review batch so we can inspect fit and messages before scaling.
|
|
584
638
|
|
|
585
639
|
**Fallback:** if the review batch is too vendor-heavy, agency-heavy, or off-ICP, switch to Sales Nav recent activity.
|
|
586
640
|
|
|
@@ -1168,7 +1222,7 @@ export const leadToolDefinitions = [
|
|
|
1168
1222
|
},
|
|
1169
1223
|
targetEngagerCount: {
|
|
1170
1224
|
type: "number",
|
|
1171
|
-
description: "Signal Discovery: target number of post engagers to scrape. Compute from source target good-fit leads / sampled fit rate; e.g.
|
|
1225
|
+
description: "Signal Discovery: target number of post engagers/source candidates to scrape. Compute from source target good-fit leads / sampled fit rate; e.g. 150 good fits at 15% fit requires about 1000 engagers. Limits selected posts before starting scrape.",
|
|
1172
1226
|
},
|
|
1173
1227
|
maxPostsToScrape: {
|
|
1174
1228
|
type: "number",
|
|
@@ -2811,13 +2865,14 @@ export async function selectPromisingPosts(input) {
|
|
|
2811
2865
|
});
|
|
2812
2866
|
}
|
|
2813
2867
|
catch {
|
|
2868
|
+
const { reviewBatchSize } = getSignalDiscoverySourcePlanDefaults();
|
|
2814
2869
|
sourceRecommendation = `## Source Recommendation
|
|
2815
2870
|
|
|
2816
2871
|
Use Signal Discovery first.
|
|
2817
2872
|
|
|
2818
2873
|
**Recommendation:** approve scraping the ${selectionResult.selectedCount} selected Signal Discovery post${selectionResult.selectedCount === 1 ? "" : "s"}.
|
|
2819
2874
|
|
|
2820
|
-
**First pass:** build the source list, then import only
|
|
2875
|
+
**First pass:** build the source list, then import only the ${reviewBatchSize.toLocaleString("en-US")}-lead review batch so we can inspect fit and messages before scaling.
|
|
2821
2876
|
|
|
2822
2877
|
Approval card should say:
|
|
2823
2878
|
|