@sellable/mcp 0.1.144 → 0.1.146
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/post-find-leads-message-scout.md +8 -14
- 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 +70 -14
- 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 +12 -6
- package/skills/create-campaign-v2/SOUL.md +6 -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 +27 -5
- 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/message-review-safety-gate.md +22 -44
- package/skills/create-campaign-v2/references/sample-validation-loop.md +13 -13
- package/skills/create-campaign-v2/references/step-13-import-leads.md +14 -9
- 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
|
|
|
@@ -137,8 +137,12 @@ Return a concise status with:
|
|
|
137
137
|
- quality-gate pass/fail summary
|
|
138
138
|
- whether final template review is ready or needs revision
|
|
139
139
|
|
|
140
|
-
When the parent will show the recommendation in chat,
|
|
141
|
-
message review
|
|
140
|
+
When the parent will show the recommendation in chat, keep the customer-facing
|
|
141
|
+
message review lightweight. Format only the approval target and one strong
|
|
142
|
+
good-fill example as Markdown with distinct copy blocks. Keep token rules,
|
|
143
|
+
omit/fallback examples, and bad-fill avoidance notes in your internal
|
|
144
|
+
recommendation so the parent can persist them to the campaign brief after
|
|
145
|
+
approval; do not print them in the default chat approval packet.
|
|
142
146
|
|
|
143
147
|
````markdown
|
|
144
148
|
## Message Template
|
|
@@ -155,19 +159,9 @@ message review as Markdown with distinct copy blocks:
|
|
|
155
159
|
{{tokenized_message_body}}
|
|
156
160
|
```
|
|
157
161
|
|
|
158
|
-
## Rendered
|
|
162
|
+
## Rendered Example
|
|
159
163
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
```text
|
|
163
|
-
Subject: ...
|
|
164
|
-
|
|
165
|
-
Hey First,
|
|
166
|
-
|
|
167
|
-
...
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Good omit / fallback
|
|
164
|
+
Good token fill:
|
|
171
165
|
|
|
172
166
|
```text
|
|
173
167
|
Subject: ...
|
|
@@ -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;
|
|
@@ -446,7 +502,7 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
|
|
|
446
502
|
? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source leads before the bounded review batch is cloned.`
|
|
447
503
|
: "";
|
|
448
504
|
return {
|
|
449
|
-
stage: "
|
|
505
|
+
stage: "find-leads",
|
|
450
506
|
headline: provider === "signal-discovery"
|
|
451
507
|
? "Scraping source leads from posts"
|
|
452
508
|
: "Importing source leads",
|
|
@@ -454,6 +510,7 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
|
|
|
454
510
|
agentIntent: "Codex is materializing the approved source into a lead list before cloning only the bounded review batch into the campaign.",
|
|
455
511
|
nextAction: "Wait for source leads, then import the 15-row review batch",
|
|
456
512
|
safety: "Import is limited to the review batch.",
|
|
513
|
+
progressLabel: "Source scouting",
|
|
457
514
|
};
|
|
458
515
|
}
|
|
459
516
|
function buildSourceImportRecoveryWatchNarration(args) {
|
|
@@ -537,8 +594,6 @@ function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
|
|
|
537
594
|
safety: "Scrape approval is the next gate.",
|
|
538
595
|
};
|
|
539
596
|
}
|
|
540
|
-
const SOURCE_RECOMMENDATION_TARGET = 300;
|
|
541
|
-
const SOURCE_RECOMMENDATION_FIT_RATE = 0.15;
|
|
542
597
|
function escapeMarkdownTableCell(value) {
|
|
543
598
|
return (value || "-").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
544
599
|
}
|
|
@@ -547,6 +602,7 @@ function formatApproxInteger(value) {
|
|
|
547
602
|
return `~${rounded.toLocaleString("en-US")}`;
|
|
548
603
|
}
|
|
549
604
|
function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
|
|
605
|
+
const { targetGoodFitLeads, defaultFitRate, sourceCandidateTarget, reviewBatchSize, } = getSignalDiscoverySourcePlanDefaults();
|
|
550
606
|
const selectedCount = selectedPosts.length;
|
|
551
607
|
const totalEngagement = selectedPosts.reduce((sum, post) => sum + (post.likes ?? 0) + (post.comments ?? 0), 0);
|
|
552
608
|
const tableRows = selectedPosts
|
|
@@ -555,15 +611,14 @@ function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
|
|
|
555
611
|
return `| ${escapeMarkdownTableCell(post.authorName)} | ${escapeMarkdownTableCell(post.reason)} | ${formatApproxInteger(engagement)} |`;
|
|
556
612
|
})
|
|
557
613
|
.join("\n");
|
|
558
|
-
const
|
|
559
|
-
const estimatedGoodFit = totalEngagement * SOURCE_RECOMMENDATION_FIT_RATE;
|
|
614
|
+
const estimatedGoodFit = totalEngagement * defaultFitRate;
|
|
560
615
|
return `## Source Recommendation
|
|
561
616
|
|
|
562
617
|
Use Signal Discovery first.
|
|
563
618
|
|
|
564
|
-
**
|
|
565
|
-
**
|
|
566
|
-
**
|
|
619
|
+
**Good-fit target:** ~${targetGoodFitLeads.toLocaleString("en-US")} prospects after cleanup, enrichment, and filters<br>
|
|
620
|
+
**Source-candidate plan:** scrape ~${sourceCandidateTarget.toLocaleString("en-US")} raw engagers using a conservative ${Math.round(defaultFitRate * 100)}% fit-rate assumption<br>
|
|
621
|
+
**Review checkpoint:** import the first ${reviewBatchSize.toLocaleString("en-US")} leads into the campaign for fit and message review before scaling
|
|
567
622
|
|
|
568
623
|
### Selected posts
|
|
569
624
|
|
|
@@ -572,15 +627,15 @@ Use Signal Discovery first.
|
|
|
572
627
|
${tableRows || "| Selected posts | Campaign-matched public engagement | - |"}
|
|
573
628
|
|
|
574
629
|
**Total visible pool:** ${formatApproxInteger(totalEngagement)} engagers<br>
|
|
575
|
-
**Estimated good-fit pool at ${Math.round(
|
|
630
|
+
**Estimated good-fit pool at ${Math.round(defaultFitRate * 100)}%:** ${formatApproxInteger(estimatedGoodFit)} prospects before dedupe/risk cleanup
|
|
576
631
|
|
|
577
632
|
### Recommendation
|
|
578
633
|
|
|
579
634
|
Approve scraping these ${selectedCount} posts.
|
|
580
635
|
|
|
581
|
-
This gives enough volume to
|
|
636
|
+
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
637
|
|
|
583
|
-
**First pass:** build the source list, then import only
|
|
638
|
+
**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
639
|
|
|
585
640
|
**Fallback:** if the review batch is too vendor-heavy, agency-heavy, or off-ICP, switch to Sales Nav recent activity.
|
|
586
641
|
|
|
@@ -1168,7 +1223,7 @@ export const leadToolDefinitions = [
|
|
|
1168
1223
|
},
|
|
1169
1224
|
targetEngagerCount: {
|
|
1170
1225
|
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.
|
|
1226
|
+
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
1227
|
},
|
|
1173
1228
|
maxPostsToScrape: {
|
|
1174
1229
|
type: "number",
|
|
@@ -2811,13 +2866,14 @@ export async function selectPromisingPosts(input) {
|
|
|
2811
2866
|
});
|
|
2812
2867
|
}
|
|
2813
2868
|
catch {
|
|
2869
|
+
const { reviewBatchSize } = getSignalDiscoverySourcePlanDefaults();
|
|
2814
2870
|
sourceRecommendation = `## Source Recommendation
|
|
2815
2871
|
|
|
2816
2872
|
Use Signal Discovery first.
|
|
2817
2873
|
|
|
2818
2874
|
**Recommendation:** approve scraping the ${selectionResult.selectedCount} selected Signal Discovery post${selectionResult.selectedCount === 1 ? "" : "s"}.
|
|
2819
2875
|
|
|
2820
|
-
**First pass:** build the source list, then import only
|
|
2876
|
+
**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
2877
|
|
|
2822
2878
|
Approval card should say:
|
|
2823
2879
|
|