@sellable/mcp 0.1.142 → 0.1.144

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.
Files changed (30) hide show
  1. package/README.md +5 -3
  2. package/agents/post-find-leads-message-scout.md +47 -3
  3. package/agents/registry.json +3 -7
  4. package/dist/index-dev.js +0 -0
  5. package/dist/index.js +0 -0
  6. package/dist/server.js +59 -29
  7. package/dist/tools/auth.js +5 -5
  8. package/dist/tools/leads.js +107 -8
  9. package/dist/tools/processing.d.ts +1 -0
  10. package/dist/tools/prompts.js +3 -3
  11. package/dist/tools/rubrics.js +14 -9
  12. package/package.json +1 -1
  13. package/skills/create-campaign/SKILL.md +52 -32
  14. package/skills/create-campaign-brief/SKILL.md +4 -0
  15. package/skills/create-campaign-v2/SKILL.md +59 -9
  16. package/skills/create-campaign-v2/SOUL.md +21 -12
  17. package/skills/create-campaign-v2/core/flow.v2.json +54 -18
  18. package/skills/create-campaign-v2/core/policy.md +35 -39
  19. package/skills/create-campaign-v2/references/draft-lifecycle.md +20 -104
  20. package/skills/create-campaign-v2/references/filter-leads.md +199 -525
  21. package/skills/create-campaign-v2/references/final-handoff-contract.md +5 -5
  22. package/skills/create-campaign-v2/references/message-review-safety-gate.md +109 -28
  23. package/skills/create-campaign-v2/references/sample-validation-loop.md +5 -2
  24. package/skills/create-campaign-v2/references/step-15-re-cascade.md +2 -3
  25. package/skills/create-campaign-v2/references/watch-guide-narration.md +37 -22
  26. package/skills/create-campaign-v2/references/watch-link-handoff.md +1 -1
  27. package/skills/create-campaign-v2-tail/SKILL.md +26 -13
  28. package/skills/research/config.json +9 -0
  29. package/dist/tools/registry.d.ts +0 -4186
  30. package/dist/tools/registry.js +0 -59
package/README.md CHANGED
@@ -255,9 +255,11 @@ Parallel execution contract:
255
255
  through `get_source_scout_registry`. Add new scouts there first; installer,
256
256
  Codex config, Claude agent files, and prompts should consume the registry.
257
257
  - Post-find-leads scout names also come from `agents/registry.json` and are
258
- exposed through `get_post_find_leads_scout_registry`. After source approval,
259
- use that registry to launch the filter-leads scout and message-generation
260
- scout together when the host supports real subagents.
258
+ exposed through `get_post_find_leads_scout_registry`. After
259
+ `confirm_lead_list` imports a non-empty review batch and rows are proven, use
260
+ that registry to launch the message-generation scout immediately when the host
261
+ supports real subagents. Launch the filter-leads scout later, only after the
262
+ user chooses filters.
261
263
  - Claude host: use the installed `source-scout-linkedin-engagement`,
262
264
  `source-scout-sales-nav`, and `source-scout-prospeo-contact` Task/Agent
263
265
  subagents for parallel lead-source scouting only when the current session
@@ -61,9 +61,9 @@ Return the following to the parent thread:
61
61
  choice, and rubric/filter basis when present
62
62
  - output timestamp/hash and any retry/error detail
63
63
 
64
- Write `message-validation.md`, `message-prep.md`, or
65
- `message-candidate-drafts.md` only when the parent explicitly asks for debug
66
- artifacts. Normal live campaign runs can return the same content directly.
64
+ Do not write local markdown/json artifacts in normal live campaign runs. Return
65
+ the recommendation directly to the parent thread. Emit debug artifacts only when
66
+ the parent explicitly asks for debug/UAT output.
67
67
 
68
68
  When reporting branch runtime proof, use this shape under
69
69
  `watchNarration.workerDetails.messageDraftBuilder`:
@@ -136,3 +136,47 @@ Return a concise status with:
136
136
  - one rendered omit/fallback sample
137
137
  - quality-gate pass/fail summary
138
138
  - whether final template review is ready or needs revision
139
+
140
+ When the parent will show the recommendation in chat, format the customer-facing
141
+ message review as Markdown with distinct copy blocks:
142
+
143
+ ````markdown
144
+ ## Message Template
145
+
146
+ **Subject**
147
+
148
+ ```text
149
+ {{tokenized_subject}}
150
+ ```
151
+
152
+ **Body**
153
+
154
+ ```text
155
+ {{tokenized_message_body}}
156
+ ```
157
+
158
+ ## Rendered Examples
159
+
160
+ ### Good token fill
161
+
162
+ ```text
163
+ Subject: ...
164
+
165
+ Hey First,
166
+
167
+ ...
168
+ ```
169
+
170
+ ### Good omit / fallback
171
+
172
+ ```text
173
+ Subject: ...
174
+
175
+ Hey First,
176
+
177
+ ...
178
+ ```
179
+ ````
180
+
181
+ Use a table for token rules. Keep explanations outside the fenced `text` blocks
182
+ so the user can quickly see exactly what copy is being approved.
@@ -168,7 +168,7 @@
168
168
  "optionalProducesArtifacts": [],
169
169
  "ownership": "lead quality, false-positive patterns, keep/exclude rules, ability-to-pay checks, and production rubric translation only",
170
170
  "codex": {
171
- "description": "Lead Fit Builder for campaign-backed lead filtering and rubric persistence after source approval.",
171
+ "description": "Lead Fit Builder for campaign-backed lead filtering and rubric persistence after review-batch import and filter approval.",
172
172
  "model": "gpt-5.5",
173
173
  "modelReasoningEffort": "high",
174
174
  "sandboxMode": "read-only",
@@ -179,7 +179,7 @@
179
179
  ]
180
180
  },
181
181
  "claude": {
182
- "description": "Use proactively as Lead Fit Builder after lead source approval to persist campaign rubrics from campaign state.",
182
+ "description": "Use proactively as Lead Fit Builder after review-batch import and filter approval to persist campaign rubrics from campaign state.",
183
183
  "model": "inherit",
184
184
  "background": true,
185
185
  "maxTurns": 8,
@@ -211,11 +211,7 @@
211
211
  "token fill rules",
212
212
  "rendered sample"
213
213
  ],
214
- "optionalProducesArtifacts": [
215
- "message-validation.md",
216
- "message-prep.md",
217
- "message-candidate-drafts.md"
218
- ],
214
+ "optionalProducesArtifacts": [],
219
215
  "ownership": "proof inventory, token strategy, angle drafting, skeptical-prospect review, and selected winner only",
220
216
  "codex": {
221
217
  "description": "Message Draft Builder for campaign-backed template proposals after confirm_lead_list imports a non-empty bounded review batch.",
package/dist/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -2,36 +2,35 @@ 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 { 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";
5
+ import { authToolDefinitions, getAuthStatus } from "./tools/auth.js";
6
+ import { blueprintCommitToolDefinitions, handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
+ import { bootstrapCreateCampaign, bootstrapToolDefinitions, } from "./tools/bootstrap.js";
8
+ import { campaignToolDefinitions, createCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
9
+ import { cellToolDefinitions, queueCells, updateCell } from "./tools/cells.js";
10
+ import { handleStartCliLogin, handleWaitForCliLogin, startCliLoginToolDef, waitForCliLoginToolDef, } from "./tools/cli-login.js";
11
+ import { contextToolDefinitions, getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
12
+ import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, directCampaignToolDefinitions, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
13
+ import { bootstrapEngage, bootstrapEngageMulti, engageBootstrapToolDefinitions, } from "./tools/engage-bootstrap.js";
14
+ import { engageDiscoveryToolDefinitions, searchEngagementPosts, } from "./tools/engage-discovery.js";
15
+ import { copySenderConfigTool, engageMemoryToolDefinitions, getEngageMemoryTool, migrateFlatConfigsTool, recordEngageProvenSearchTool, setEngageStyleGuideTool, upsertEngageTrackedPersonTool, } from "./tools/engage-memory.js";
16
+ import { engageStateToolDefinitions, getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
17
+ import { bulkEnrichWithProspeo, enrichmentToolDefinitions, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
18
+ import { frameworkToolDefinitions, getCampaignFramework, } from "./tools/framework.js";
19
+ import { cancelLeadImport, confirmLeadList, getProviderPrompt, importLeads, leadToolDefinitions, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
20
+ import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, linkedinToolDefinitions, } from "./tools/linkedin.js";
21
+ import { getCampaignNavigationState, navigationToolDefinitions, } from "./tools/navigation.js";
22
+ import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, onDemandToolDefinitions, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
23
+ import { processingToolDefinitions, upsertRubric } from "./tools/processing.js";
24
+ import { completeSenderResearch, getMessagePrompt, getPostFindLeadsScoutRegistry, getSourceScoutRegistry, getSubskillAsset, getSubskillPrompt, listSubskillPrompts, promptToolDefinitions, searchSubskillPrompts, } from "./tools/prompts.js";
25
+ import { readinessToolDefinitions, waitForCampaignTableReady, waitForLeadListReady, } from "./tools/readiness.js";
26
+ import { getRows, getTableRows, getTableRowsMinimal, rowToolDefinitions, } from "./tools/rows.js";
27
+ import { addRubricItem, checkRubric, deleteRubricItem, draftRubrics, rubricToolDefinitions, saveRubrics, selectNecessaryRubrics, updateRubricItem, waitForRubricResults, } from "./tools/rubrics.js";
28
+ import { getSender, listSenders, senderToolDefinitions, } from "./tools/senders.js";
29
+ import { attachRecommendedSequence, attachSequence, createWorkflowTable, sequencerToolDefinitions, } from "./tools/sequencer.js";
30
+ import { listTables, tableToolDefinitions } from "./tools/tables.js";
31
+ import { handleVerifyTableRow, verifyRowToolDefinitions, } from "./tools/verify-row.js";
33
32
  import { sanitizeWatchUrlsForMcpResult } from "./tools/watch-url-security.js";
34
- import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, } from "./tools/workspaces.js";
33
+ import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, workspaceToolDefinitions, } from "./tools/workspaces.js";
35
34
  import { checkForUpdates, logUpdateNotice } from "./update-check.js";
36
35
  const server = new Server({
37
36
  name: "sellable-mcp",
@@ -42,6 +41,37 @@ const server = new Server({
42
41
  prompts: {},
43
42
  },
44
43
  });
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
+ ];
45
75
  function parseOptionalNumber(value) {
46
76
  if (typeof value === "number" && Number.isFinite(value))
47
77
  return value;
@@ -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. 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.",
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. IMPORTANT: When auth succeeds, ALWAYS tell the user the _userNotice message so they know which workspace is active and whether an update is available. 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
- ? `I’m building this in ${wsLabel} (${envLabel}).`
96
- : `I’m building this in ${wsLabel}.`;
95
+ ? `Workspace: ${wsLabel} (${envLabel})`
96
+ : `Workspace: ${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 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`";
141
+ "if true, say `You're in — {activeWorkspaceName} workspace, ready to roll.\\n\\nNow — paste the LinkedIn profile URL of the person you'll be sending campaigns from. Usually that's you (the founder), or whoever's voice the messages should sound like.\\n\\ne.g. https://www.linkedin.com/in/your-handle`; " +
142
+ "if false, say `You're set up — your {activeWorkspaceName} workspace is ready.\\n\\nNow — paste the LinkedIn profile URL of the person you'll be sending campaigns from. Usually that's you (the founder), or whoever's voice the messages should sound like.\\n\\ne.g. https://www.linkedin.com/in/your-handle`";
143
143
  if (error instanceof SellableApiError && error.isAuthError) {
144
144
  return {
145
145
  ...base,
@@ -453,7 +453,7 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
453
453
  visibleState: `The browser is showing ${providerLabel} import progress for ${sourceDetail}.${targetDetail}`,
454
454
  agentIntent: "Codex is materializing the approved source into a lead list before cloning only the bounded review batch into the campaign.",
455
455
  nextAction: "Wait for source leads, then import the 15-row review batch",
456
- safety: "No enrichment, sequence, or sending starts during source import.",
456
+ safety: "Import is limited to the review batch.",
457
457
  };
458
458
  }
459
459
  function buildSourceImportRecoveryWatchNarration(args) {
@@ -481,7 +481,7 @@ function buildSourceImportRecoveryWatchNarration(args) {
481
481
  nextAction: args.reason === "failed" || args.reason === "zero"
482
482
  ? "Retry the import or change the approved source"
483
483
  : "Wait again, retry readiness, or change the source",
484
- safety: "No enrichment, filtering, message cells, sequence, or sending has started.",
484
+ safety: "The campaign is still at source recovery.",
485
485
  };
486
486
  }
487
487
  function buildFilterChoiceWatchNarration({ sourceLeadCount, reviewRowCount, }) {
@@ -497,7 +497,7 @@ function buildFilterChoiceWatchNarration({ sourceLeadCount, reviewRowCount, }) {
497
497
  visibleState: `${reviewCopy} The browser is showing the filter-choice screen and sample rows.`,
498
498
  agentIntent: "Codex is pausing on this sample because a mixed review list should be filtered before message review. Skip filters only if the visible rows already look clean.",
499
499
  nextAction: "Choose filters or skip",
500
- safety: "No enrichment, filtering, Generate Message cells, sequence, or sending starts from this choice.",
500
+ safety: "This choice sets the filter path.",
501
501
  workerStatuses: {
502
502
  leadFitBuilder: "idle",
503
503
  messageDraftBuilder: "idle",
@@ -511,7 +511,7 @@ function buildSignalDiscoverySearchWatchNarration() {
511
511
  visibleState: "The browser is showing Signal Discovery while LinkedIn post searches run.",
512
512
  agentIntent: "Codex is searching the approved post themes and will ask before scraping selected engagers into a source list.",
513
513
  nextAction: "Review selected posts before scraping",
514
- safety: "Search only stores post candidates; it does not import leads, enrich, generate messages, create a sequence, or send.",
514
+ safety: "Scrape approval is the next gate.",
515
515
  };
516
516
  }
517
517
  function buildSignalDiscoveryResultsWatchNarration(postsReturned) {
@@ -524,7 +524,7 @@ function buildSignalDiscoveryResultsWatchNarration(postsReturned) {
524
524
  visibleState: postCopy,
525
525
  agentIntent: "Codex is promoting the strongest posts, then it will ask before scraping the selected engager pool.",
526
526
  nextAction: "Approve selected posts before scraping",
527
- safety: "Post results do not import leads, enrich, generate messages, create a sequence, or send.",
527
+ safety: "Scrape approval is the next gate.",
528
528
  };
529
529
  }
530
530
  function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
@@ -533,10 +533,61 @@ function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
533
533
  headline: "Approve selected-post scrape",
534
534
  visibleState: `${selectedPostCount.toLocaleString("en-US")} LinkedIn post${selectedPostCount === 1 ? "" : "s"} selected in Signal Discovery.`,
535
535
  agentIntent: "Codex is asking before scraping this selected engager pool into a source list and cloning only the bounded review batch into the campaign.",
536
- nextAction: `Scrape ${selectedPostCount.toLocaleString("en-US")} selected post${selectedPostCount === 1 ? "" : "s"} for the bounded review batch`,
537
- safety: "Post selection does not import leads, enrich, generate messages, create a sequence, or send.",
536
+ nextAction: `Approve scraping ${selectedPostCount.toLocaleString("en-US")} Signal Discovery post${selectedPostCount === 1 ? "" : "s"}`,
537
+ safety: "Scrape approval is the next gate.",
538
538
  };
539
539
  }
540
+ const SOURCE_RECOMMENDATION_TARGET = 300;
541
+ const SOURCE_RECOMMENDATION_FIT_RATE = 0.15;
542
+ function escapeMarkdownTableCell(value) {
543
+ return (value || "-").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
544
+ }
545
+ function formatApproxInteger(value) {
546
+ const rounded = value >= 100 ? Math.round(value / 10) * 10 : Math.round(value);
547
+ return `~${rounded.toLocaleString("en-US")}`;
548
+ }
549
+ function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
550
+ const selectedCount = selectedPosts.length;
551
+ const totalEngagement = selectedPosts.reduce((sum, post) => sum + (post.likes ?? 0) + (post.comments ?? 0), 0);
552
+ const tableRows = selectedPosts
553
+ .map((post) => {
554
+ const engagement = (post.likes ?? 0) + (post.comments ?? 0);
555
+ return `| ${escapeMarkdownTableCell(post.authorName)} | ${escapeMarkdownTableCell(post.reason)} | ${formatApproxInteger(engagement)} |`;
556
+ })
557
+ .join("\n");
558
+ const engagersNeeded = Math.ceil(SOURCE_RECOMMENDATION_TARGET / SOURCE_RECOMMENDATION_FIT_RATE);
559
+ const estimatedGoodFit = totalEngagement * SOURCE_RECOMMENDATION_FIT_RATE;
560
+ return `## Source Recommendation
561
+
562
+ Use Signal Discovery first.
563
+
564
+ **Goal:** ~${SOURCE_RECOMMENDATION_TARGET.toLocaleString("en-US")} good-fit prospects after cleanup, enrichment, and filters<br>
565
+ **Working assumption:** ~${Math.round(SOURCE_RECOMMENDATION_FIT_RATE * 100)}% of raw post engagers become good-fit prospects<br>
566
+ **Engagers needed:** ~${engagersNeeded.toLocaleString("en-US")} raw engagers
567
+
568
+ ### Selected posts
569
+
570
+ | Post | Why it fits | Visible engagement |
571
+ |---|---|---:|
572
+ ${tableRows || "| Selected posts | Campaign-matched public engagement | - |"}
573
+
574
+ **Total visible pool:** ${formatApproxInteger(totalEngagement)} engagers<br>
575
+ **Estimated good-fit pool at ${Math.round(SOURCE_RECOMMENDATION_FIT_RATE * 100)}%:** ${formatApproxInteger(estimatedGoodFit)} prospects before dedupe/risk cleanup
576
+
577
+ ### Recommendation
578
+
579
+ Approve scraping these ${selectedCount} posts.
580
+
581
+ This gives enough volume to target ~${SOURCE_RECOMMENDATION_TARGET.toLocaleString("en-US")} good-fit prospects after cleanup, while keeping the source tied to people already engaging with the campaign's strongest public buying signals.
582
+
583
+ **First pass:** build the source list, then import only 15 leads into the review batch so we can inspect quality before scaling.
584
+
585
+ **Fallback:** if the review batch is too vendor-heavy, agency-heavy, or off-ICP, switch to Sales Nav recent activity.
586
+
587
+ Approval card should say:
588
+
589
+ **Approve scraping ${selectedCount} Signal Discovery post${selectedCount === 1 ? "" : "s"}?**`;
590
+ }
540
591
  function normalizeImportProvider(provider) {
541
592
  if (provider === "apollo-ai" || provider === "apollo")
542
593
  return "apollo";
@@ -2726,12 +2777,60 @@ export async function selectPromisingPosts(input) {
2726
2777
  currentStep: "signal-discovery",
2727
2778
  watchNarration: buildSelectedPostApprovalWatchNarration(selectionResult.selectedCount),
2728
2779
  });
2780
+ let sourceRecommendation = "";
2781
+ try {
2782
+ const tabsResponse = await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`);
2783
+ const reasonsByPostId = new Map(selections.map((selection) => [selection.postId, selection.reason]));
2784
+ const selectedByUrl = new Map();
2785
+ for (const tab of tabsResponse.tabs ?? []) {
2786
+ for (const post of tab.posts ?? []) {
2787
+ if (!post.isSelected)
2788
+ continue;
2789
+ const urlKey = normalizePostUrl(post.postUrl ?? undefined) ||
2790
+ post.postUrl ||
2791
+ post.id ||
2792
+ "";
2793
+ if (!urlKey)
2794
+ continue;
2795
+ const directReason = post.id ? reasonsByPostId.get(post.id) : undefined;
2796
+ const urlReason = post.postUrl && reasonsByPostId.get(post.postUrl)
2797
+ ? reasonsByPostId.get(post.postUrl)
2798
+ : undefined;
2799
+ selectedByUrl.set(urlKey, {
2800
+ authorName: post.authorName,
2801
+ reason: directReason ||
2802
+ urlReason ||
2803
+ (post.postContent ? truncate(post.postContent, 70) : null),
2804
+ likes: post.likes,
2805
+ comments: post.comments,
2806
+ });
2807
+ }
2808
+ }
2809
+ sourceRecommendation = buildSignalDiscoverySourceRecommendation({
2810
+ selectedPosts: Array.from(selectedByUrl.values()),
2811
+ });
2812
+ }
2813
+ catch {
2814
+ sourceRecommendation = `## Source Recommendation
2815
+
2816
+ Use Signal Discovery first.
2817
+
2818
+ **Recommendation:** approve scraping the ${selectionResult.selectedCount} selected Signal Discovery post${selectionResult.selectedCount === 1 ? "" : "s"}.
2819
+
2820
+ **First pass:** build the source list, then import only 15 leads into the review batch so we can inspect quality before scaling.
2821
+
2822
+ Approval card should say:
2823
+
2824
+ **Approve scraping ${selectionResult.selectedCount} Signal Discovery post${selectionResult.selectedCount === 1 ? "" : "s"}?**`;
2825
+ }
2729
2826
  return {
2730
2827
  success: true,
2731
2828
  selectedCount: selectionResult.selectedCount,
2732
2829
  unselectedCount: selectionResult.unselectedCount,
2733
2830
  criteriaCount: selectionResult.criteriaCount,
2734
- message: `Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria (persisted). Criteria are already saved to the campaign. Ask the user to approve scraping these selected posts before calling import_leads; the earlier source-plan approval only authorized search/scouting.`,
2831
+ message: `${sourceRecommendation}
2832
+
2833
+ Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria persisted. Ask the user to approve the specific scraping action before calling import_leads; the earlier source-plan approval only authorized search/scouting.`,
2735
2834
  };
2736
2835
  }
2737
2836
  export async function setHeadlineICPCriteria(input) {
@@ -60,6 +60,7 @@ export type CampaignRubricsResponse = {
60
60
  enableICPFilters: boolean | null;
61
61
  currentStep?: string | null;
62
62
  workflowTableId: string | null;
63
+ hasApprovedMessageTemplate?: boolean | null;
63
64
  rubrics: RubricItem[];
64
65
  };
65
66
  export declare function fetchCampaignRubrics(campaignOfferId: string): Promise<CampaignRubricsResponse>;
@@ -339,9 +339,9 @@ export function getPostFindLeadsScoutRegistry() {
339
339
  reusePolicy: "The first completed Message Draft Builder recommendation remains the default review candidate. Later Lead Fit Builder, Filter Leads, enrichment, or rubric completion may make an enriched rewrite available, but does not automatically retry or replace the initial draft unless campaign/brief/source/list/table/review-batch identity mismatches or the initial output failed.",
340
340
  },
341
341
  usage: {
342
- codex: "After the user approves or auto-confirms the lead source, spawn both returned scout `name` values in one assistant turn only when the current Codex host exposes those custom agents.",
343
- claude: "After lead source approval, invoke both returned Task/Agent subagents in one assistant message only when the current Claude session lists those agents, so filter-leads and message generation run concurrently.",
344
- parentThreadRule: "Named agents are optional acceleration. If they are absent, do not customer-surface install status; the main thread still orchestrates filter and message branches from CampaignOffer state, selected source state, workflowTableId, and imported review-batch rows. Local markdown/json files are not normal-path inputs. The message drafting branch must load the full generate-messages prompt, must read live campaign/review-batch state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. Template approval in the parent thread uses the create-campaign-v2 message-review safety gate; do not load the full generate-messages prompt for approval-only review. Join before message review. Do not automatically rerun Message Draft Builder after filters/enrichment finish; show the initial draft by default and offer an enriched rewrite only with explicit user opt-in.",
342
+ codex: "After confirm_lead_list imports a non-empty bounded review batch and get_rows_minimal proves rows for workflowTableId, spawn the returned Message Draft Builder scout name immediately when the current Codex host exposes that custom agent. Spawn the Lead Fit Builder scout only after the user chooses filters.",
343
+ claude: "After confirm_lead_list imports a non-empty bounded review batch and get_rows_minimal proves rows for workflowTableId, invoke the returned Message Draft Builder Task/Agent immediately when the current Claude session lists that agent. Invoke the Lead Fit Builder only after the user chooses filters.",
344
+ parentThreadRule: "Named agents are optional acceleration. If they are absent, do not customer-surface install status; the main thread still orchestrates filter and message branches from CampaignOffer state, selected source state, workflowTableId, and imported review-batch rows. Local markdown/json files are not normal-path inputs. The message drafting branch starts at the review-batch import gate, must load the full generate-messages prompt, must read live campaign/review-batch state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. Template approval in the parent thread uses the create-campaign-v2 message-review safety gate; do not load the full generate-messages prompt for approval-only review. Join before message review. Do not automatically rerun Message Draft Builder after filters/enrichment finish; show the initial draft by default and offer an enriched rewrite only with explicit user opt-in.",
345
345
  },
346
346
  };
347
347
  }
@@ -86,21 +86,26 @@ async function fetchCampaignOffer(campaignOfferId) {
86
86
  workflowTableId: v3.workflowTableId,
87
87
  enableICPFilters: v3.enableICPFilters,
88
88
  currentStep: v3.currentStep,
89
+ hasApprovedMessageTemplate: v3.hasApprovedMessageTemplate,
89
90
  leadScoringRubrics: v3.rubrics,
90
91
  };
91
92
  }
92
93
  const filterLeadsReadyWatchNarration = {
93
94
  stage: "fit-message",
94
- headline: "Filter Leads is ready",
95
- visibleState: "Filters are enabled, rubrics are saved, and the browser is showing Filter Leads.",
96
- agentIntent: "Codex is waiting for the approved message template before running enrichment, filtering, or message cells.",
97
- nextAction: "Review or approve the message template before filtering",
95
+ headline: "Filter rules saved",
96
+ visibleState: "The browser is staying on Filter Leads with the fit rules saved.",
97
+ agentIntent: "Codex is preparing the message template for approval before enrichment, filtering, or message cells run.",
98
+ nextAction: "Review the message template in chat",
98
99
  progressLabel: "Fit + message",
99
- safety: "Saved rules do not start enrichment, filtering, Generate Message cells, sequence setup, or sending.",
100
+ safety: "Template approval comes before row execution.",
100
101
  };
101
- function shouldMoveToFilterLeads(currentStep) {
102
+ function shouldMoveToFilterLeads(campaign) {
103
+ const currentStep = campaign.currentStep;
102
104
  if (!currentStep)
103
105
  return true;
106
+ if (currentStep === "messages") {
107
+ return campaign.hasApprovedMessageTemplate === false;
108
+ }
104
109
  return [
105
110
  "confirm-lead-list",
106
111
  "filter-choice",
@@ -109,11 +114,11 @@ function shouldMoveToFilterLeads(currentStep) {
109
114
  "apply-icp-rubric",
110
115
  ].includes(currentStep);
111
116
  }
112
- function buildEnableIcpFiltersPayload(currentStep) {
117
+ function buildEnableIcpFiltersPayload(campaign) {
113
118
  const payload = {
114
119
  enableICPFilters: true,
115
120
  };
116
- if (shouldMoveToFilterLeads(currentStep)) {
121
+ if (shouldMoveToFilterLeads(campaign)) {
117
122
  payload.currentStep = "apply-icp-rubric";
118
123
  payload.watchNarration = filterLeadsReadyWatchNarration;
119
124
  }
@@ -568,7 +573,7 @@ export async function saveRubrics(input) {
568
573
  let currentStepSet = false;
569
574
  try {
570
575
  const api = getApi();
571
- const payload = buildEnableIcpFiltersPayload(campaign.currentStep);
576
+ const payload = buildEnableIcpFiltersPayload(campaign);
572
577
  await api.put(`/api/v2/campaign-offers/${input.campaignOfferId}`, payload);
573
578
  enableICPFiltersSet = true;
574
579
  currentStepSet = payload.currentStep === "apply-icp-rubric";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.142",
3
+ "version": "0.1.144",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",