@sellable/mcp 0.1.143 → 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.
- package/README.md +5 -3
- package/agents/post-find-leads-message-scout.md +44 -0
- package/agents/registry.json +2 -2
- package/dist/index-dev.js +0 -0
- package/dist/index.js +0 -0
- package/dist/server.js +59 -29
- package/dist/tools/auth.js +5 -5
- package/dist/tools/leads.js +107 -8
- package/dist/tools/processing.d.ts +1 -0
- package/dist/tools/prompts.js +3 -3
- package/dist/tools/rubrics.js +14 -9
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +49 -29
- package/skills/create-campaign-v2/SKILL.md +59 -9
- package/skills/create-campaign-v2/SOUL.md +20 -12
- package/skills/create-campaign-v2/core/flow.v2.json +54 -18
- package/skills/create-campaign-v2/references/final-handoff-contract.md +5 -5
- package/skills/create-campaign-v2/references/message-review-safety-gate.md +88 -13
- package/skills/create-campaign-v2/references/sample-validation-loop.md +5 -2
- package/skills/create-campaign-v2/references/step-15-re-cascade.md +2 -3
- package/skills/create-campaign-v2/references/watch-guide-narration.md +37 -22
- package/skills/create-campaign-v2/references/watch-link-handoff.md +1 -1
- package/skills/create-campaign-v2-tail/SKILL.md +26 -13
- package/skills/research/config.json +9 -0
- package/dist/tools/registry.d.ts +0 -4186
- 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
|
|
259
|
-
|
|
260
|
-
scout
|
|
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
|
|
@@ -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.
|
package/agents/registry.json
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
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 {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
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;
|
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. When auth succeeds,
|
|
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
|
-
? `
|
|
96
|
-
: `
|
|
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
|
|
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 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,
|
package/dist/tools/leads.js
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: `
|
|
537
|
-
safety: "
|
|
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:
|
|
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>;
|
package/dist/tools/prompts.js
CHANGED
|
@@ -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
|
|
343
|
-
claude: "After
|
|
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
|
}
|
package/dist/tools/rubrics.js
CHANGED
|
@@ -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
|
|
95
|
-
visibleState: "
|
|
96
|
-
agentIntent: "Codex is
|
|
97
|
-
nextAction: "Review
|
|
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: "
|
|
100
|
+
safety: "Template approval comes before row execution.",
|
|
100
101
|
};
|
|
101
|
-
function shouldMoveToFilterLeads(
|
|
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(
|
|
117
|
+
function buildEnableIcpFiltersPayload(campaign) {
|
|
113
118
|
const payload = {
|
|
114
119
|
enableICPFilters: true,
|
|
115
120
|
};
|
|
116
|
-
if (shouldMoveToFilterLeads(
|
|
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
|
|
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";
|