@sellable/mcp 0.1.158 → 0.1.159
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/agents/source-scout-linkedin-engagement.md +8 -3
- package/dist/tools/bootstrap.js +1 -1
- package/dist/tools/leads.d.ts +20 -2
- package/dist/tools/leads.js +92 -31
- package/dist/tools/registry.d.ts +8 -2
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +45 -32
- package/skills/create-campaign-v2/SKILL.md +41 -16
- package/skills/create-campaign-v2/SOUL.md +9 -8
- package/skills/create-campaign-v2/core/flow.v2.json +1 -1441
- package/skills/create-campaign-v2/references/approval-gate-framing.md +12 -1
- package/skills/create-campaign-v2/references/filter-leads.md +26 -0
- package/skills/create-campaign-v2/references/final-handoff-contract.md +15 -0
- package/skills/create-campaign-v2/references/sample-validation-loop.md +26 -0
- package/skills/create-campaign-v2/references/watch-link-handoff.md +10 -5
- package/skills/providers/signal-discovery.md +5 -3
|
@@ -60,9 +60,14 @@ currentStep: "signal-discovery" })` before sampling so the watched Signal
|
|
|
60
60
|
sampled/projected headline-fit rate clears the 10% planning floor. Treat the
|
|
61
61
|
10% floor as a reject threshold, not as the scrape-count denominator when the
|
|
62
62
|
actual sample rate is higher.
|
|
63
|
-
8. Select/promote enough right-content posts to plausibly hit the target.
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
8. Select/promote enough right-content posts to plausibly hit the target. After
|
|
64
|
+
the sample math is known, treat the promoted sample set and final scrape set
|
|
65
|
+
as separate: recommend the smallest right-content post subset whose
|
|
66
|
+
scrapable/reachable engagers clears the required engager count, with a modest
|
|
67
|
+
buffer when needed. If one 1,200+ engager post clears a ~1,000-engager target,
|
|
68
|
+
recommend scraping that one post, not all 3 sample posts. If the warm Signals
|
|
69
|
+
pool is useful but too small, return the expected warm range and recommend
|
|
70
|
+
Sales Nav/Prospeo for scale instead of padding with noisy posts.
|
|
66
71
|
9. Return false positives and dead ends explicitly.
|
|
67
72
|
|
|
68
73
|
Return a concise structured result with:
|
package/dist/tools/bootstrap.js
CHANGED
|
@@ -270,7 +270,7 @@ export async function bootstrapCreateCampaign(input = {}) {
|
|
|
270
270
|
? resumeDetected
|
|
271
271
|
? `Bootstrap complete.${workspaceNotice} Resume from campaign state and navigation diagnostics first; treat local draft artifacts as debug-only evidence. Then load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }); if the response has hasMore=true, continue with nextOffset until hasMore=false.`
|
|
272
272
|
: flowVersion === "v2"
|
|
273
|
-
? `Bootstrap complete.${workspaceNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status,
|
|
273
|
+
? `Bootstrap complete.${workspaceNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status, ask for the LinkedIn profile URL first with company website as fallback, run lightweight profile/company lookup, and ask whether the user has a current offer or wants the researched recommendation. Do not call list_senders or sender discovery during setup; sender availability belongs only to Settings after message approval. Then run the short setup/intake, write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link, and hand off to lead finding.`
|
|
274
274
|
: `Bootstrap complete.${workspaceNotice} Load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }); if the response has hasMore=true, continue with nextOffset until hasMore=false. Follow that flow before calling create_campaign.`
|
|
275
275
|
: "Bootstrap incomplete. Resolve blockingErrors and rerun bootstrap_create_campaign before provider/search/import tools.";
|
|
276
276
|
// Strip prompt body from createCampaignSubskill — it's loaded via the host
|
package/dist/tools/leads.d.ts
CHANGED
|
@@ -163,6 +163,8 @@ export type SelectPromisingPostsInput = {
|
|
|
163
163
|
currentStep?: string | null;
|
|
164
164
|
selectionMode?: "add" | "replace";
|
|
165
165
|
mode?: "add" | "replace";
|
|
166
|
+
targetEngagerCount?: number;
|
|
167
|
+
maxPostsToScrape?: number;
|
|
166
168
|
};
|
|
167
169
|
export type SetHeadlineICPCriteriaInput = {
|
|
168
170
|
campaignOfferId: string;
|
|
@@ -2000,6 +2002,14 @@ export declare const leadToolDefinitions: ({
|
|
|
2000
2002
|
enum: string[];
|
|
2001
2003
|
description: string;
|
|
2002
2004
|
};
|
|
2005
|
+
targetEngagerCount: {
|
|
2006
|
+
type: string;
|
|
2007
|
+
description: string;
|
|
2008
|
+
};
|
|
2009
|
+
maxPostsToScrape: {
|
|
2010
|
+
type: string;
|
|
2011
|
+
description: string;
|
|
2012
|
+
};
|
|
2003
2013
|
currentStep: {
|
|
2004
2014
|
type: string[];
|
|
2005
2015
|
description: string;
|
|
@@ -2051,8 +2061,6 @@ export declare const leadToolDefinitions: ({
|
|
|
2051
2061
|
sourceLeadListId?: undefined;
|
|
2052
2062
|
targetLeadCount?: undefined;
|
|
2053
2063
|
mode?: undefined;
|
|
2054
|
-
targetEngagerCount?: undefined;
|
|
2055
|
-
maxPostsToScrape?: undefined;
|
|
2056
2064
|
tableId?: undefined;
|
|
2057
2065
|
campaignName?: undefined;
|
|
2058
2066
|
keepInSync?: undefined;
|
|
@@ -2650,6 +2658,16 @@ export declare function selectPromisingPosts(input: SelectPromisingPostsInput):
|
|
|
2650
2658
|
unselectedCount: number;
|
|
2651
2659
|
criteriaCount: number;
|
|
2652
2660
|
message: string;
|
|
2661
|
+
recommendedPostCount?: undefined;
|
|
2662
|
+
recommendedTargetEngagerCount?: undefined;
|
|
2663
|
+
} | {
|
|
2664
|
+
success: boolean;
|
|
2665
|
+
selectedCount: number;
|
|
2666
|
+
recommendedPostCount: number;
|
|
2667
|
+
recommendedTargetEngagerCount: number | null;
|
|
2668
|
+
unselectedCount: number;
|
|
2669
|
+
criteriaCount: number;
|
|
2670
|
+
message: string;
|
|
2653
2671
|
}>;
|
|
2654
2672
|
export declare function setHeadlineICPCriteria(input: SetHeadlineICPCriteriaInput): Promise<{
|
|
2655
2673
|
success: boolean;
|
package/dist/tools/leads.js
CHANGED
|
@@ -745,13 +745,21 @@ function buildSignalDiscoveryResultsWatchNarration(postsReturned) {
|
|
|
745
745
|
safety: "Scrape approval is the next gate.",
|
|
746
746
|
};
|
|
747
747
|
}
|
|
748
|
-
function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
|
|
748
|
+
function buildSelectedPostApprovalWatchNarration(selectedPostCount, recommendedPostCount = selectedPostCount, targetEngagerCount) {
|
|
749
|
+
const selectedPostCopy = `${selectedPostCount.toLocaleString("en-US")} LinkedIn post${selectedPostCount === 1 ? "" : "s"} selected for the source test`;
|
|
750
|
+
const recommendedPostCopy = `${recommendedPostCount.toLocaleString("en-US")} recommended LinkedIn post${recommendedPostCount === 1 ? "" : "s"}`;
|
|
751
|
+
const targetCopy = targetEngagerCount
|
|
752
|
+
? ` to cover about ${targetEngagerCount.toLocaleString("en-US")} source candidates`
|
|
753
|
+
: "";
|
|
754
|
+
const visibleState = recommendedPostCount < selectedPostCount
|
|
755
|
+
? `${selectedPostCopy}; the scrape plan only needs ${recommendedPostCopy}${targetCopy}.`
|
|
756
|
+
: `${selectedPostCopy}.`;
|
|
749
757
|
return {
|
|
750
758
|
stage: "find-leads",
|
|
751
759
|
headline: "Approve selected-post scrape",
|
|
752
|
-
visibleState
|
|
760
|
+
visibleState,
|
|
753
761
|
agentIntent: "Codex is asking before scraping this selected engager pool into a source list.",
|
|
754
|
-
nextAction: `Approve scraping ${
|
|
762
|
+
nextAction: `Approve scraping ${recommendedPostCopy}`,
|
|
755
763
|
safety: "Scrape approval is the next gate.",
|
|
756
764
|
};
|
|
757
765
|
}
|
|
@@ -762,41 +770,68 @@ function formatApproxInteger(value) {
|
|
|
762
770
|
const rounded = value >= 100 ? Math.round(value / 10) * 10 : Math.round(value);
|
|
763
771
|
return `~${rounded.toLocaleString("en-US")}`;
|
|
764
772
|
}
|
|
765
|
-
function
|
|
773
|
+
function normalizeEngagementCount(value) {
|
|
774
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
775
|
+
? value
|
|
776
|
+
: 0;
|
|
777
|
+
}
|
|
778
|
+
function buildSignalDiscoverySourceRecommendation({ selectedPosts, targetEngagerCount, maxPostsToScrape, }) {
|
|
766
779
|
const { targetGoodFitLeads, defaultFitRate, minPlanningFitRate, sourceCandidateTarget, reviewBatchSize, } = getSignalDiscoverySourcePlanDefaults();
|
|
767
|
-
const
|
|
768
|
-
const
|
|
769
|
-
|
|
780
|
+
const effectiveTargetEngagerCount = normalizePositiveInteger(targetEngagerCount) ?? sourceCandidateTarget;
|
|
781
|
+
const normalizedSelectedPosts = selectedPosts.map((post) => ({
|
|
782
|
+
...post,
|
|
783
|
+
likes: normalizeEngagementCount(post.likes),
|
|
784
|
+
comments: normalizeEngagementCount(post.comments),
|
|
785
|
+
}));
|
|
786
|
+
const scrapePlan = selectSignalPostsForImport(normalizedSelectedPosts, {
|
|
787
|
+
targetEngagerCount: effectiveTargetEngagerCount,
|
|
788
|
+
maxPostsToScrape: maxPostsToScrape ?? undefined,
|
|
789
|
+
});
|
|
790
|
+
const recommendedPosts = scrapePlan.posts;
|
|
791
|
+
const selectedCount = normalizedSelectedPosts.length;
|
|
792
|
+
const recommendedCount = recommendedPosts.length;
|
|
793
|
+
const totalVisibleEngagement = normalizedSelectedPosts.reduce((sum, post) => sum + post.likes + post.comments, 0);
|
|
794
|
+
const recommendedVisibleEngagement = recommendedPosts.reduce((sum, post) => sum + post.likes + post.comments, 0);
|
|
795
|
+
const tableRows = recommendedPosts
|
|
770
796
|
.map((post) => {
|
|
771
|
-
const engagement =
|
|
772
|
-
return `| ${escapeMarkdownTableCell(post.authorName)} | ${escapeMarkdownTableCell(post.reason)} | ${formatApproxInteger(engagement)} |`;
|
|
797
|
+
const engagement = post.likes + post.comments;
|
|
798
|
+
return `| ${escapeMarkdownTableCell(post.authorName)} | ${escapeMarkdownTableCell(post.reason)} | ${formatApproxInteger(engagement)} | ${formatApproxInteger(estimateScrapableSignalEngagers(post))} |`;
|
|
773
799
|
})
|
|
774
800
|
.join("\n");
|
|
775
|
-
const
|
|
776
|
-
|
|
801
|
+
const fitRateForEstimate = targetEngagerCount && effectiveTargetEngagerCount > 0
|
|
802
|
+
? Math.min(1, targetGoodFitLeads / effectiveTargetEngagerCount)
|
|
803
|
+
: defaultFitRate;
|
|
804
|
+
const estimatedGoodFit = scrapePlan.estimatedEngagers * fitRateForEstimate;
|
|
805
|
+
const fitRateLabel = targetEngagerCount && effectiveTargetEngagerCount > 0
|
|
806
|
+
? "approved source math"
|
|
807
|
+
: `${Math.round(defaultFitRate * 100)}% working assumption`;
|
|
808
|
+
const selectedPoolCopy = recommendedCount < selectedCount
|
|
809
|
+
? `**Promoted sample pool:** ${selectedCount.toLocaleString("en-US")} selected posts, ${formatApproxInteger(totalVisibleEngagement)} visible / ${formatApproxInteger(scrapePlan.availableEngagers)} scrapable engagers<br>\n**Recommended scrape set:** ${recommendedCount.toLocaleString("en-US")} post${recommendedCount === 1 ? "" : "s"}, ${formatApproxInteger(recommendedVisibleEngagement)} visible / ${formatApproxInteger(scrapePlan.estimatedEngagers)} scrapable engagers<br>`
|
|
810
|
+
: `**Total visible pool:** ${formatApproxInteger(totalVisibleEngagement)} engagers<br>\n**Scrape-capacity pool:** ${formatApproxInteger(scrapePlan.estimatedEngagers)} scrapable engagers after endpoint caps<br>`;
|
|
811
|
+
const message = `## Source Recommendation
|
|
777
812
|
|
|
778
813
|
Use LinkedIn engagement first.
|
|
779
814
|
|
|
780
815
|
**Goal:** ~${targetGoodFitLeads.toLocaleString("en-US")} headline-fit prospects from relevant LinkedIn engagement<br>
|
|
781
816
|
**Working assumption:** ~${Math.round(defaultFitRate * 100)}% of raw post engagers pass headline filtering unless a real sample supports a different rate<br>
|
|
782
|
-
**Engagers needed:** ~${
|
|
817
|
+
**Engagers needed:** ~${effectiveTargetEngagerCount.toLocaleString("en-US")} raw engagers<br>
|
|
783
818
|
**Planning floor:** continue with LinkedIn engagement only when sampled/projected headline-fit rate is at least ${Math.round(minPlanningFitRate * 100)}%; below that, switch to Sales Nav recent activity<br>
|
|
784
819
|
**Review checkpoint:** copy the confirmed source list into the campaign, then process the first ${reviewBatchSize.toLocaleString("en-US")} leads for fit and message review before scaling
|
|
785
820
|
|
|
786
|
-
###
|
|
821
|
+
### Recommended scrape set
|
|
787
822
|
|
|
788
|
-
| Post | Why it fits | Visible engagement |
|
|
789
|
-
|
|
790
|
-
${tableRows || "|
|
|
823
|
+
| Post | Why it fits | Visible engagement | Scrape capacity |
|
|
824
|
+
|---|---|---:|---:|
|
|
825
|
+
${tableRows || "| Recommended posts | Campaign-matched public engagement | - | - |"}
|
|
791
826
|
|
|
792
|
-
|
|
793
|
-
**Estimated headline-fit pool
|
|
827
|
+
${selectedPoolCopy}
|
|
828
|
+
**Estimated headline-fit pool from ${fitRateLabel}:** ${formatApproxInteger(estimatedGoodFit)} prospects before enrichment and deeper fit review
|
|
794
829
|
|
|
795
830
|
### Recommendation
|
|
796
831
|
|
|
797
|
-
Approve scraping these ${
|
|
832
|
+
Approve scraping these ${recommendedCount} recommended post${recommendedCount === 1 ? "" : "s"}.
|
|
798
833
|
|
|
799
|
-
This gives enough volume to target ~${targetGoodFitLeads.toLocaleString("en-US")} headline-fit prospects, while keeping the
|
|
834
|
+
This gives enough volume to target ~${targetGoodFitLeads.toLocaleString("en-US")} headline-fit prospects, while keeping the scrape limited to the smallest selected right-content post set that covers the source-candidate target.
|
|
800
835
|
|
|
801
836
|
**First pass:** build the source list, copy it into the campaign, then use the first ${reviewBatchSize.toLocaleString("en-US")} campaign rows as the internal setup slice for filters and messages before scaling.
|
|
802
837
|
|
|
@@ -804,7 +839,13 @@ This gives enough volume to target ~${targetGoodFitLeads.toLocaleString("en-US")
|
|
|
804
839
|
|
|
805
840
|
Approval card should say:
|
|
806
841
|
|
|
807
|
-
**Approve scraping ${
|
|
842
|
+
**Approve scraping ${recommendedCount} recommended LinkedIn post${recommendedCount === 1 ? "" : "s"}?**`;
|
|
843
|
+
return {
|
|
844
|
+
message,
|
|
845
|
+
recommendedPostCount: recommendedCount,
|
|
846
|
+
estimatedEngagers: scrapePlan.estimatedEngagers,
|
|
847
|
+
targetEngagerCount: effectiveTargetEngagerCount,
|
|
848
|
+
};
|
|
808
849
|
}
|
|
809
850
|
function normalizeImportProvider(provider) {
|
|
810
851
|
if (provider === "apollo-ai" || provider === "apollo")
|
|
@@ -1480,7 +1521,7 @@ export const leadToolDefinitions = [
|
|
|
1480
1521
|
},
|
|
1481
1522
|
{
|
|
1482
1523
|
name: "select_promising_posts",
|
|
1483
|
-
description: "Select the most promising LinkedIn posts for lead scraping AND provide headline ICP criteria. Use the selectionTarget returned by search_signals (default 3).
|
|
1524
|
+
description: "Select the most promising LinkedIn posts for lead scraping AND provide headline ICP criteria. Use the selectionTarget returned by search_signals (default 3) for the sampling/promoted set. After sample math exists, pass targetEngagerCount so the approval recommendation uses the smallest right-content post subset that covers the source-candidate target instead of scraping every promoted sample post.",
|
|
1484
1525
|
inputSchema: {
|
|
1485
1526
|
type: "object",
|
|
1486
1527
|
properties: {
|
|
@@ -1520,6 +1561,14 @@ export const leadToolDefinitions = [
|
|
|
1520
1561
|
enum: ["add", "replace"],
|
|
1521
1562
|
description: 'How to apply selections. "add" keeps existing selections; "replace" clears previous selections not included in this call.',
|
|
1522
1563
|
},
|
|
1564
|
+
targetEngagerCount: {
|
|
1565
|
+
type: "number",
|
|
1566
|
+
description: "Optional source-candidate target from sample math. When provided, the recommendation ranks selected posts by scrapable engagement and asks approval for only enough posts to cover this target. If omitted, uses the default Signal Discovery source target.",
|
|
1567
|
+
},
|
|
1568
|
+
maxPostsToScrape: {
|
|
1569
|
+
type: "number",
|
|
1570
|
+
description: "Optional hard cap for the recommended scrape set after ranking selected posts by scrapable engagement. Values above the backend hard cap are clamped.",
|
|
1571
|
+
},
|
|
1523
1572
|
currentStep: {
|
|
1524
1573
|
type: ["string", "null"],
|
|
1525
1574
|
description: "Headless workflow step ID",
|
|
@@ -3016,7 +3065,7 @@ export function getProviderPrompt(input) {
|
|
|
3016
3065
|
}
|
|
3017
3066
|
export async function selectPromisingPosts(input) {
|
|
3018
3067
|
const api = getApi();
|
|
3019
|
-
const { campaignOfferId, selections, headlineICPCriteria, selectionMode, mode, } = input;
|
|
3068
|
+
const { campaignOfferId, selections, headlineICPCriteria, selectionMode, mode, targetEngagerCount, maxPostsToScrape, } = input;
|
|
3020
3069
|
const effectiveMode = selectionMode ?? mode ?? "add";
|
|
3021
3070
|
if (selections.length > MAX_SIGNAL_DISCOVERY_POSTS) {
|
|
3022
3071
|
return {
|
|
@@ -3053,11 +3102,9 @@ export async function selectPromisingPosts(input) {
|
|
|
3053
3102
|
message: "No Signal Discovery posts were selected for this campaign. Do not import yet. Re-run a campaign-scoped search_signals call, use recommendedPostIds from the current campaign search state, or ask the user to promote posts in the UI before retrying select_promising_posts.",
|
|
3054
3103
|
};
|
|
3055
3104
|
}
|
|
3056
|
-
await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
|
|
3057
|
-
currentStep: "signal-discovery",
|
|
3058
|
-
watchNarration: buildSelectedPostApprovalWatchNarration(selectionResult.selectedCount),
|
|
3059
|
-
});
|
|
3060
3105
|
let sourceRecommendation = "";
|
|
3106
|
+
let recommendedPostCount = selectionResult.selectedCount;
|
|
3107
|
+
let recommendationTargetEngagerCount = null;
|
|
3061
3108
|
try {
|
|
3062
3109
|
const tabsResponse = await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`);
|
|
3063
3110
|
const reasonsByPostId = new Map(selections.map((selection) => [selection.postId, selection.reason]));
|
|
@@ -3086,9 +3133,17 @@ export async function selectPromisingPosts(input) {
|
|
|
3086
3133
|
});
|
|
3087
3134
|
}
|
|
3088
3135
|
}
|
|
3089
|
-
|
|
3136
|
+
if (selectedByUrl.size === 0) {
|
|
3137
|
+
throw new Error("No selected Signal Discovery posts found in tabs");
|
|
3138
|
+
}
|
|
3139
|
+
const recommendation = buildSignalDiscoverySourceRecommendation({
|
|
3090
3140
|
selectedPosts: Array.from(selectedByUrl.values()),
|
|
3141
|
+
targetEngagerCount,
|
|
3142
|
+
maxPostsToScrape,
|
|
3091
3143
|
});
|
|
3144
|
+
sourceRecommendation = recommendation.message;
|
|
3145
|
+
recommendedPostCount = recommendation.recommendedPostCount;
|
|
3146
|
+
recommendationTargetEngagerCount = recommendation.targetEngagerCount;
|
|
3092
3147
|
}
|
|
3093
3148
|
catch {
|
|
3094
3149
|
const { reviewBatchSize } = getSignalDiscoverySourcePlanDefaults();
|
|
@@ -3096,22 +3151,28 @@ export async function selectPromisingPosts(input) {
|
|
|
3096
3151
|
|
|
3097
3152
|
Use LinkedIn engagement first.
|
|
3098
3153
|
|
|
3099
|
-
**Recommendation:** approve scraping
|
|
3154
|
+
**Recommendation:** approve scraping ${selectionResult.selectedCount} recommended LinkedIn post${selectionResult.selectedCount === 1 ? "" : "s"}.
|
|
3100
3155
|
|
|
3101
3156
|
**First pass:** build the source list, copy it into the campaign, then process only the first ${reviewBatchSize.toLocaleString("en-US")} leads so we can inspect quality before scaling.
|
|
3102
3157
|
|
|
3103
3158
|
Approval card should say:
|
|
3104
3159
|
|
|
3105
|
-
**Approve scraping ${selectionResult.selectedCount}
|
|
3160
|
+
**Approve scraping ${selectionResult.selectedCount} recommended LinkedIn post${selectionResult.selectedCount === 1 ? "" : "s"}?**`;
|
|
3106
3161
|
}
|
|
3162
|
+
await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
|
|
3163
|
+
currentStep: "signal-discovery",
|
|
3164
|
+
watchNarration: buildSelectedPostApprovalWatchNarration(selectionResult.selectedCount, recommendedPostCount, recommendationTargetEngagerCount),
|
|
3165
|
+
});
|
|
3107
3166
|
return {
|
|
3108
3167
|
success: true,
|
|
3109
3168
|
selectedCount: selectionResult.selectedCount,
|
|
3169
|
+
recommendedPostCount,
|
|
3170
|
+
recommendedTargetEngagerCount: recommendationTargetEngagerCount,
|
|
3110
3171
|
unselectedCount: selectionResult.unselectedCount,
|
|
3111
3172
|
criteriaCount: selectionResult.criteriaCount,
|
|
3112
3173
|
message: `${sourceRecommendation}
|
|
3113
3174
|
|
|
3114
|
-
Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria persisted. Ask the user to approve
|
|
3175
|
+
Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria persisted. Ask the user to approve the recommended scrape set once; after approval, call import_leads with targetEngagerCount ${recommendationTargetEngagerCount ?? "from the approved source math"} immediately and do not repeat this source card.`,
|
|
3115
3176
|
};
|
|
3116
3177
|
}
|
|
3117
3178
|
export async function setHeadlineICPCriteria(input) {
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -3150,6 +3150,14 @@ export declare const allTools: ({
|
|
|
3150
3150
|
enum: string[];
|
|
3151
3151
|
description: string;
|
|
3152
3152
|
};
|
|
3153
|
+
targetEngagerCount: {
|
|
3154
|
+
type: string;
|
|
3155
|
+
description: string;
|
|
3156
|
+
};
|
|
3157
|
+
maxPostsToScrape: {
|
|
3158
|
+
type: string;
|
|
3159
|
+
description: string;
|
|
3160
|
+
};
|
|
3153
3161
|
currentStep: {
|
|
3154
3162
|
type: string[];
|
|
3155
3163
|
description: string;
|
|
@@ -3201,8 +3209,6 @@ export declare const allTools: ({
|
|
|
3201
3209
|
sourceLeadListId?: undefined;
|
|
3202
3210
|
targetLeadCount?: undefined;
|
|
3203
3211
|
mode?: undefined;
|
|
3204
|
-
targetEngagerCount?: undefined;
|
|
3205
|
-
maxPostsToScrape?: undefined;
|
|
3206
3212
|
tableId?: undefined;
|
|
3207
3213
|
campaignName?: undefined;
|
|
3208
3214
|
keepInSync?: undefined;
|
package/package.json
CHANGED
|
@@ -86,8 +86,8 @@ instruction loading, file lookup, plugin cache versions, missing linked files,
|
|
|
86
86
|
or tool discovery. Start in product language:
|
|
87
87
|
|
|
88
88
|
```text
|
|
89
|
-
I’ll help you launch this as a Sellable campaign. First I’ll
|
|
90
|
-
|
|
89
|
+
I’ll help you launch this as a Sellable campaign. First I’ll research the
|
|
90
|
+
person/company this campaign is for, then I’ll turn that into a campaign brief
|
|
91
91
|
before we move into lead sourcing.
|
|
92
92
|
```
|
|
93
93
|
|
|
@@ -163,11 +163,13 @@ precision, and referral paths, but it does not provide hiring-by-role filters;
|
|
|
163
163
|
say that distinction plainly in the source-plan gate.
|
|
164
164
|
|
|
165
165
|
After scouting, ask for a second approval on the concrete source action. For
|
|
166
|
-
LinkedIn engagement (`signal-discovery` internally), name how many
|
|
167
|
-
posts will be scraped and the target engager/source-candidate
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
source
|
|
166
|
+
LinkedIn engagement (`signal-discovery` internally), name how many
|
|
167
|
+
recommended posts will be scraped and the target engager/source-candidate
|
|
168
|
+
volume. N must be the smallest right-content post set that clears the source
|
|
169
|
+
target, not the default 3 promoted sample posts. For Sales Nav or Prospeo,
|
|
170
|
+
name the specific approved import lane and source lead count. Keep the
|
|
171
|
+
internal 15-row campaign-table execution slice separate from source
|
|
172
|
+
sampling.
|
|
171
173
|
|
|
172
174
|
Do not call `import_leads` or `confirm_lead_list` until this second approval is
|
|
173
175
|
granted.
|
|
@@ -185,8 +187,8 @@ granted.
|
|
|
185
187
|
the campaign table and return the initial campaign-table execution slice rows.
|
|
186
188
|
|
|
187
189
|
For LinkedIn engagement, the customer-facing approval card must use the exact
|
|
188
|
-
action shape "Approve scraping N
|
|
189
|
-
should be a compact `## Source Recommendation` block with:
|
|
190
|
+
action shape "Approve scraping N recommended LinkedIn posts?" and the chat
|
|
191
|
+
summary should be a compact `## Source Recommendation` block with:
|
|
190
192
|
|
|
191
193
|
- goal: about 300 headline-fit prospects from relevant LinkedIn engagement
|
|
192
194
|
- source-candidate plan: use sample math first: target headline-fit prospects
|
|
@@ -396,10 +398,10 @@ customer-facing progress copy.
|
|
|
396
398
|
|
|
397
399
|
Do not treat the active Sellable workspace as the campaign subject. The
|
|
398
400
|
workspace only tells you where the campaign will be saved. Before buyer, CTA,
|
|
399
|
-
proof, or source questions, identify the
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
`
|
|
401
|
+
proof, or source questions, identify the person/profile or company this
|
|
402
|
+
campaign is for, plus enough current company/product context to build the
|
|
403
|
+
brief. This client/company lookup feeds `clientProspectId` or
|
|
404
|
+
`senderLinkedinUrl`; it is not a connected-sender check.
|
|
403
405
|
|
|
404
406
|
Do not call `mcp__sellable__list_senders`, `mcp__sellable__get_sender`, or
|
|
405
407
|
surface connected/missing sender state during setup, brief, source, filter, or
|
|
@@ -423,19 +425,20 @@ first:
|
|
|
423
425
|
only if a URL/domain is also available.
|
|
424
426
|
|
|
425
427
|
Then summarize what you found in one or two lines and ask the user to confirm
|
|
426
|
-
the
|
|
427
|
-
availability in this confirmation.
|
|
428
|
+
the current company/focus before continuing, especially if public website data
|
|
429
|
+
may be stale. Do not mention connected sender availability in this confirmation.
|
|
428
430
|
|
|
429
431
|
If the user did not provide the launch identity, ask in normal chat for the
|
|
430
|
-
LinkedIn profile
|
|
432
|
+
LinkedIn profile URL first, with the company website as the fallback. Do not ask
|
|
431
433
|
them to choose an input type with the structured question tool:
|
|
432
434
|
|
|
433
435
|
```text
|
|
434
436
|
I’m ready to build this in {workspace}.
|
|
435
437
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
the target, offer, proof, and
|
|
438
|
+
What is your LinkedIn profile URL? If you do not have it handy, send your
|
|
439
|
+
company website instead. I’ll research the person and company from that, then
|
|
440
|
+
ask you to correct anything stale before we pick the target, offer, proof, and
|
|
441
|
+
lead source.
|
|
439
442
|
```
|
|
440
443
|
|
|
441
444
|
After the user pastes a URL/domain, do the lightweight lookup. For a LinkedIn
|
|
@@ -445,10 +448,19 @@ most recent company from the profile. For a company website, call
|
|
|
445
448
|
LinkedIn profile URL is available, retain it as `senderLinkedinUrl` for
|
|
446
449
|
`create_campaign`; if a `clientProspectId` is available, pass that instead.
|
|
447
450
|
|
|
448
|
-
After the user confirms the
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
451
|
+
After the user confirms the company/focus, run one lightweight company lookup
|
|
452
|
+
if it has not already run, then ask an offer-readiness question before inferred
|
|
453
|
+
strategy hardens:
|
|
454
|
+
|
|
455
|
+
```text
|
|
456
|
+
Do you already know the offer for this campaign, should I use the researched
|
|
457
|
+
recommendation, or should we shape the offer together? If the website is stale,
|
|
458
|
+
tell me what is current before I build the brief.
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The setup questions should use the confirmed company context so they do not feel
|
|
462
|
+
generic. When you present a recommendation, introduce it as based on the
|
|
463
|
+
research you just did and keep it editable.
|
|
452
464
|
|
|
453
465
|
### Sufficient Intake Bypass
|
|
454
466
|
|
|
@@ -476,9 +488,9 @@ If the invocation or any later user message explicitly asks for "yolo mode",
|
|
|
476
488
|
me", "use best estimates", or "just run it", enable YOLO mode for the rest of
|
|
477
489
|
the run. Treat YOLO as `interactionMode: "autonomous"` plus an intake policy:
|
|
478
490
|
|
|
479
|
-
- If campaign
|
|
480
|
-
website in normal chat; do not ask buyer,
|
|
481
|
-
questions before that.
|
|
491
|
+
- If the campaign subject is missing, ask only for the LinkedIn profile URL
|
|
492
|
+
first, with company website as the fallback, in normal chat; do not ask buyer,
|
|
493
|
+
offer, proof, source, or filter setup questions before that.
|
|
482
494
|
- Treat any freeform directions already provided, or added later by the user, as
|
|
483
495
|
operator directions for the rest of the run. If directions conflict, the newest
|
|
484
496
|
user direction wins.
|
|
@@ -500,8 +512,9 @@ Before the identity gate, use this customer-facing shape:
|
|
|
500
512
|
```text
|
|
501
513
|
I’m ready to build the campaign in {workspace}.
|
|
502
514
|
|
|
503
|
-
First I’ll
|
|
504
|
-
context to
|
|
515
|
+
First I’ll research the person/company this campaign is for. I’ll use that
|
|
516
|
+
context to recommend a target, offer, proof, and lead source, then you can
|
|
517
|
+
correct anything stale before I build from it.
|
|
505
518
|
|
|
506
519
|
Then I’ll turn that into a campaign brief for you to approve before any leads
|
|
507
520
|
are sourced.
|
|
@@ -636,9 +649,9 @@ updates.
|
|
|
636
649
|
```text
|
|
637
650
|
You're in — {activeWorkspaceName} workspace, ready to roll.
|
|
638
651
|
|
|
639
|
-
|
|
652
|
+
Now — what is your LinkedIn profile URL? If you do not have it handy, send your company website instead. I’ll research the person and company from that, then ask you to correct anything stale before we pick the target, offer, proof, and lead source.
|
|
640
653
|
|
|
641
|
-
|
|
654
|
+
e.g. https://www.linkedin.com/in/client-handle or https://example.com
|
|
642
655
|
```
|
|
643
656
|
|
|
644
657
|
- If `isReturningUser === false`, prepend ONE line confirming the new
|
|
@@ -647,9 +660,9 @@ updates.
|
|
|
647
660
|
```text
|
|
648
661
|
You're set up — your {activeWorkspaceName} workspace is ready.
|
|
649
662
|
|
|
650
|
-
|
|
663
|
+
Now — what is your LinkedIn profile URL? If you do not have it handy, send your company website instead. I’ll research the person and company from that, then ask you to correct anything stale before we pick the target, offer, proof, and lead source.
|
|
651
664
|
|
|
652
|
-
|
|
665
|
+
e.g. https://www.linkedin.com/in/client-handle or https://example.com
|
|
653
666
|
```
|
|
654
667
|
|
|
655
668
|
No other lines. No "all set", no "signed in", no other acknowledgement.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: create-campaign-v2
|
|
3
|
-
description: Execute the compact JSON-gated shell-first campaign flow
|
|
3
|
+
description: Execute the compact JSON-gated shell-first campaign flow from client/offer resolution through source approval, sample/message review, Settings, sequence, and explicit start.
|
|
4
4
|
visibility: internal
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -28,15 +28,14 @@ handoff read campaign state first: `campaignId`, `watchUrl`, `campaignBrief`,
|
|
|
28
28
|
`currentStep`, source/search association, `selectedLeadListId`,
|
|
29
29
|
`workflowTableId`, `leadScoringRubrics`, `approvedMessageTemplate`,
|
|
30
30
|
`senderIds`, `sequenceTemplate`, and running state. Local draft files are
|
|
31
|
-
legacy debug/UAT
|
|
32
|
-
|
|
33
|
-
for debug output.
|
|
31
|
+
legacy debug/UAT only; do not create, link, or surface them in normal runs
|
|
32
|
+
unless the user asks for debug output.
|
|
34
33
|
|
|
35
34
|
## Normal Flow
|
|
36
35
|
|
|
37
36
|
1. Bootstrap and tell the user the active Sellable workspace.
|
|
38
|
-
2. Resolve
|
|
39
|
-
3. Research the client/company enough to draft a concrete brief.
|
|
37
|
+
2. Resolve the client/company before strategy questions.
|
|
38
|
+
3. Research the client/company and current offer enough to draft a concrete brief.
|
|
40
39
|
4. Create the watchable campaign shell with `create_campaign` and the v1 brief.
|
|
41
40
|
5. Surface the direct watch link.
|
|
42
41
|
6. Choose and approve the lead source.
|
|
@@ -61,18 +60,22 @@ flows and the separate `create-campaign-v2-validation` subskill.
|
|
|
61
60
|
## Identity-First Campaign Setup
|
|
62
61
|
|
|
63
62
|
Do not treat the active Sellable workspace as the campaign subject. Resolve the
|
|
64
|
-
client/company first,
|
|
63
|
+
client/company first, confirm whether the user already has a current offer, then
|
|
64
|
+
draft the buyer, offer, proof, and source plan.
|
|
65
65
|
|
|
66
66
|
First visible request when no identity is known:
|
|
67
67
|
|
|
68
68
|
```text
|
|
69
|
-
|
|
69
|
+
What is your LinkedIn profile URL? If you do not have it handy, send your company website instead.
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
After the user pastes a URL/domain, retain it as `senderLinkedinUrl` or the
|
|
73
73
|
resolved `clientProspectId` input for `create_campaign`. Use one lightweight
|
|
74
|
-
profile/company lookup before strategy questions
|
|
75
|
-
|
|
74
|
+
profile/company lookup before strategy questions, then say that public research
|
|
75
|
+
may be stale and invite the user to correct it. Ask whether they already know
|
|
76
|
+
the offer for this campaign, want Sellable's researched recommendation, or want
|
|
77
|
+
to shape one together before buyer/offer/proof hardens. If multiple product
|
|
78
|
+
lines or offers are plausible, ask one campaign-focus choice before the brief.
|
|
76
79
|
|
|
77
80
|
Do not call `list_senders`, do not infer the campaign from connected senders,
|
|
78
81
|
and do not show a sender picker during setup. Sender availability belongs only
|
|
@@ -83,13 +86,33 @@ Use `research-sender` for concise identity/proof research and call
|
|
|
83
86
|
use the available Sellable profile/company/post tools and carry explicit proof
|
|
84
87
|
gaps into the brief.
|
|
85
88
|
|
|
89
|
+
## Brief Provenance
|
|
90
|
+
|
|
91
|
+
When rendering the first brief, label the major strategic choices with compact
|
|
92
|
+
source tags where the basis is known: `[from you]`, `[from LinkedIn]`,
|
|
93
|
+
`[from website]`, `[from case study]`, or `[Sellable recommendation]`. Keep the
|
|
94
|
+
tags lightweight; do not turn the brief into a research dump.
|
|
95
|
+
|
|
96
|
+
Before asking for approval, say:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
This is based on what I found. If your site or LinkedIn is stale, tell me what
|
|
100
|
+
to update before I build the lead list.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Ask: "Do you agree with this researched campaign direction?" The early brief
|
|
104
|
+
must not include quoted first-message copy. If a message-shape hint is useful,
|
|
105
|
+
keep it as non-final bullets and say: "This is not the message yet; I will write
|
|
106
|
+
real examples after we find and filter leads."
|
|
107
|
+
|
|
86
108
|
## YOLO Mode
|
|
87
109
|
|
|
88
110
|
Enable YOLO mode when the user asks for yolo/autopilot, passes `--yolo` or
|
|
89
111
|
`mode=yolo`, or says to use best guesses/estimates and answer for them. Ask only
|
|
90
|
-
for the LinkedIn profile
|
|
91
|
-
|
|
92
|
-
direction from company evidence plus user
|
|
112
|
+
for the LinkedIn profile URL first, with company website as the fallback, if the
|
|
113
|
+
client/company is missing. After the lookup, infer buyer, offer/CTA, proof,
|
|
114
|
+
source, filters, and message direction from company evidence plus user
|
|
115
|
+
directions; newest directions win.
|
|
93
116
|
Set `interactionMode: "autonomous"` once `campaignId` exists. Auto-select
|
|
94
117
|
pre-launch choices when confidence is sufficient, but show the assumed choice
|
|
95
118
|
briefly. Pause for missing credentials/data, failed quality floors, or final
|
|
@@ -186,9 +209,11 @@ After scouting, show a second approval gate for the concrete source action.
|
|
|
186
209
|
For LinkedIn engagement (`signal-discovery` internally), state selected-post
|
|
187
210
|
count, target engager/source-candidate volume, internal campaign-table
|
|
188
211
|
execution-slice size, cleanup risk, and fallback; label the approval like
|
|
189
|
-
"Approve scraping
|
|
190
|
-
|
|
191
|
-
`
|
|
212
|
+
"Approve scraping N recommended LinkedIn posts?" where N is the smallest
|
|
213
|
+
right-content post set that clears the approved source-candidate target. Do not
|
|
214
|
+
default to 3 just because `selectionTarget` was 3 for sampling. For Sales Nav or
|
|
215
|
+
Prospeo, name the specific search/import lane and source lead count. Do not call
|
|
216
|
+
`import_leads` or `confirm_lead_list` until this gate is approved.
|
|
192
217
|
|
|
193
218
|
For Sales Nav and Prospeo, do not ask to import only the internal 15-row
|
|
194
219
|
campaign-table execution slice at the source-action gate. First-page samples are
|