@sellable/mcp 0.1.0
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/.claude-plugin/plugin.json +12 -0
- package/.mcp.json +9 -0
- package/README.md +355 -0
- package/dist/api.d.ts +21 -0
- package/dist/api.js +73 -0
- package/dist/auth.d.ts +60 -0
- package/dist/auth.js +246 -0
- package/dist/engage-memory.d.ts +63 -0
- package/dist/engage-memory.js +354 -0
- package/dist/index-dev.d.ts +2 -0
- package/dist/index-dev.js +17 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +499 -0
- package/dist/skills.d.ts +11 -0
- package/dist/skills.js +97 -0
- package/dist/tools/auth.d.ts +30 -0
- package/dist/tools/auth.js +124 -0
- package/dist/tools/blueprint-commit.d.ts +174 -0
- package/dist/tools/blueprint-commit.js +286 -0
- package/dist/tools/bootstrap.d.ts +64 -0
- package/dist/tools/bootstrap.js +246 -0
- package/dist/tools/campaigns.d.ts +589 -0
- package/dist/tools/campaigns.js +892 -0
- package/dist/tools/cells.d.ts +58 -0
- package/dist/tools/cells.js +48 -0
- package/dist/tools/context.d.ts +88 -0
- package/dist/tools/context.js +271 -0
- package/dist/tools/csv-domains.d.ts +73 -0
- package/dist/tools/csv-domains.js +464 -0
- package/dist/tools/csv-linkedin.d.ts +102 -0
- package/dist/tools/csv-linkedin.js +712 -0
- package/dist/tools/direct-campaigns.d.ts +240 -0
- package/dist/tools/direct-campaigns.js +250 -0
- package/dist/tools/engage-bootstrap.d.ts +94 -0
- package/dist/tools/engage-bootstrap.js +205 -0
- package/dist/tools/engage-discovery.d.ts +78 -0
- package/dist/tools/engage-discovery.js +150 -0
- package/dist/tools/engage-memory.d.ts +181 -0
- package/dist/tools/engage-memory.js +143 -0
- package/dist/tools/engage-state.d.ts +72 -0
- package/dist/tools/engage-state.js +62 -0
- package/dist/tools/enrichment.d.ts +167 -0
- package/dist/tools/enrichment.js +174 -0
- package/dist/tools/flow-preflight.d.ts +68 -0
- package/dist/tools/flow-preflight.js +138 -0
- package/dist/tools/framework.d.ts +44 -0
- package/dist/tools/framework.js +153 -0
- package/dist/tools/interaction-mode.d.ts +27 -0
- package/dist/tools/interaction-mode.js +102 -0
- package/dist/tools/leads.d.ts +2417 -0
- package/dist/tools/leads.js +2307 -0
- package/dist/tools/linkedin.d.ts +210 -0
- package/dist/tools/linkedin.js +229 -0
- package/dist/tools/navigation.d.ts +91 -0
- package/dist/tools/navigation.js +381 -0
- package/dist/tools/one-off.d.ts +229 -0
- package/dist/tools/one-off.js +273 -0
- package/dist/tools/processing.d.ts +70 -0
- package/dist/tools/processing.js +56 -0
- package/dist/tools/prompts.d.ts +211 -0
- package/dist/tools/prompts.js +210 -0
- package/dist/tools/provider-preflight.d.ts +21 -0
- package/dist/tools/provider-preflight.js +59 -0
- package/dist/tools/readiness.d.ts +261 -0
- package/dist/tools/readiness.js +510 -0
- package/dist/tools/rows.d.ts +126 -0
- package/dist/tools/rows.js +105 -0
- package/dist/tools/rubrics.d.ts +497 -0
- package/dist/tools/rubrics.js +681 -0
- package/dist/tools/senders.d.ts +44 -0
- package/dist/tools/senders.js +69 -0
- package/dist/tools/sequencer.d.ts +127 -0
- package/dist/tools/sequencer.js +194 -0
- package/dist/tools/tables.d.ts +35 -0
- package/dist/tools/tables.js +36 -0
- package/dist/tools/verify-row.d.ts +36 -0
- package/dist/tools/verify-row.js +38 -0
- package/dist/tools/workspaces.d.ts +140 -0
- package/dist/tools/workspaces.js +139 -0
- package/dist/utils/workspace-root.d.ts +1 -0
- package/dist/utils/workspace-root.js +39 -0
- package/package.json +46 -0
- package/skills/building-gtm-tables/SKILL.md +216 -0
- package/skills/building-gtm-tables/core/auto-execute.yaml +19 -0
- package/skills/building-gtm-tables/core/blueprint-schema.json +72 -0
- package/skills/building-gtm-tables/references/brief-to-blueprint.md +334 -0
- package/skills/building-gtm-tables/references/column-type-catalog.md +318 -0
- package/skills/building-gtm-tables/references/common-blueprints.fixtures.ts +199 -0
- package/skills/building-gtm-tables/references/common-blueprints.md +44 -0
- package/skills/building-gtm-tables/references/failure-taxonomy.md +197 -0
- package/skills/building-gtm-tables/references/uat-seed-prompts.md +37 -0
- package/skills/building-gtm-tables/references/verify-loop.md +74 -0
- package/skills/campaign-messages/SKILL.md +173 -0
- package/skills/campaign-messages/flow.v1.json +75 -0
- package/skills/craft-message/SKILL.md +401 -0
- package/skills/create-campaign/ARCHITECTURE.md +232 -0
- package/skills/create-campaign/DISCUSS.md +296 -0
- package/skills/create-campaign/FLOW_ASCII.md +240 -0
- package/skills/create-campaign/HOST-PARITY-CHECKLIST.md +49 -0
- package/skills/create-campaign/README.md +142 -0
- package/skills/create-campaign/SKILL.md +286 -0
- package/skills/create-campaign/context/README.md +67 -0
- package/skills/create-campaign/context/_TEMPLATE.md +12 -0
- package/skills/create-campaign/context/context.md +35 -0
- package/skills/create-campaign/context/learnings.md +16 -0
- package/skills/create-campaign/context/registry.json +19 -0
- package/skills/create-campaign/core/flow.v1.json +217 -0
- package/skills/create-campaign/core/policy.md +191 -0
- package/skills/create-campaign/core/providers/apollo.json +35 -0
- package/skills/create-campaign/core/providers/prospeo.json +34 -0
- package/skills/create-campaign/core/providers/registry.json +31 -0
- package/skills/create-campaign/core/providers/sales-nav.json +37 -0
- package/skills/create-campaign/core/providers/signal-discovery.json +42 -0
- package/skills/create-campaign/references/brief-template.md +64 -0
- package/skills/create-campaign/references/campaign-quality.md +84 -0
- package/skills/create-campaign/references/copy-calibration-examples.md +120 -0
- package/skills/create-campaign/references/offer-patterns.md +108 -0
- package/skills/create-campaign/references/provider-selection-strategy.md +212 -0
- package/skills/create-campaign/references/question-examples.md +167 -0
- package/skills/create-campaign/references/token-fill-examples.md +81 -0
- package/skills/create-campaign-brief/ARCHITECTURE.md +72 -0
- package/skills/create-campaign-brief/DISCUSS.md +64 -0
- package/skills/create-campaign-brief/README.md +176 -0
- package/skills/create-campaign-brief/SKILL.md +537 -0
- package/skills/create-campaign-brief/references/brief-synthesis-rules.md +100 -0
- package/skills/create-campaign-brief/references/brief-template.md +220 -0
- package/skills/create-campaign-brief/references/campaign-idea-options.md +30 -0
- package/skills/create-campaign-brief/references/copy-appendix-template.md +62 -0
- package/skills/create-campaign-brief/references/draft-lifecycle.md +23 -0
- package/skills/create-campaign-brief/references/examples/MANIFEST.json +89 -0
- package/skills/create-campaign-brief/references/examples/briefs/clover.md +223 -0
- package/skills/create-campaign-brief/references/examples/briefs/galley.md +222 -0
- package/skills/create-campaign-brief/references/examples/briefs/gelee.md +220 -0
- package/skills/create-campaign-brief/references/examples/briefs/hey-digital.md +234 -0
- package/skills/create-campaign-brief/references/examples/briefs/persona.md +231 -0
- package/skills/create-campaign-brief/references/examples/briefs/revvix.md +220 -0
- package/skills/create-campaign-brief/references/examples/briefs/sellable-dev.md +220 -0
- package/skills/create-campaign-brief/references/examples/briefs/superposition.md +233 -0
- package/skills/create-campaign-brief/references/examples/briefs/superpower.md +219 -0
- package/skills/create-campaign-brief/references/examples/briefs/westpark-villas.md +220 -0
- package/skills/create-campaign-brief/references/icp-lock-question-bank.md +43 -0
- package/skills/create-campaign-brief/references/messaging-inputs.md +58 -0
- package/skills/create-campaign-brief/references/output-acceptance-rubric.md +62 -0
- package/skills/create-campaign-brief/references/phase75-active-runtime-message-pack.md +248 -0
- package/skills/create-campaign-brief/references/phase75-canonical-brief-template.md +319 -0
- package/skills/create-campaign-brief/references/phase75-good-brief-and-messaging-examples.md +445 -0
- package/skills/create-campaign-brief/references/quick-research-protocol.md +39 -0
- package/skills/create-campaign-brief/references/reference-sheet-protocol.md +60 -0
- package/skills/create-campaign-brief/references/zero-shot-iteration-rules.md +66 -0
- package/skills/create-campaign-v2/SKILL.md +1619 -0
- package/skills/create-campaign-v2/core/auto-execute.README.md +219 -0
- package/skills/create-campaign-v2/core/auto-execute.yaml +121 -0
- package/skills/create-campaign-v2/core/flow.v2.json +1643 -0
- package/skills/create-campaign-v2/core/policy.md +82 -0
- package/skills/create-campaign-v2/references/ai-tells.md +253 -0
- package/skills/create-campaign-v2/references/approval-gate-framing.md +346 -0
- package/skills/create-campaign-v2/references/draft-lifecycle.md +110 -0
- package/skills/create-campaign-v2/references/escalation-ladder.md +119 -0
- package/skills/create-campaign-v2/references/filter-leads.md +495 -0
- package/skills/create-campaign-v2/references/final-handoff-contract.md +176 -0
- package/skills/create-campaign-v2/references/gold-standard-message-examples.md +394 -0
- package/skills/create-campaign-v2/references/gold-standard-message-patterns.md +314 -0
- package/skills/create-campaign-v2/references/gold-standard-message-validation-example.md +212 -0
- package/skills/create-campaign-v2/references/lead-validation-preview.md +172 -0
- package/skills/create-campaign-v2/references/parallel-critique-protocol.md +368 -0
- package/skills/create-campaign-v2/references/sample-validation-loop.md +289 -0
- package/skills/create-campaign-v2/references/step-13-import-leads.md +151 -0
- package/skills/create-campaign-v2/references/step-15-re-cascade.md +90 -0
- package/skills/create-campaign-v2/references/thomas-revision-filters.md +521 -0
- package/skills/create-campaign-v2/references/thomas-variant-selection.md +202 -0
- package/skills/create-campaign-v2/references/tier-routing-matrix.md +66 -0
- package/skills/create-campaign-v2/references/validation-criteria.md +367 -0
- package/skills/create-campaign-v2/references/watch-link-handoff.md +106 -0
- package/skills/create-campaign-v2-validation/SKILL.md +296 -0
- package/skills/create-post/SKILL.md +1308 -0
- package/skills/create-rubric/SKILL.md +251 -0
- package/skills/engage/SKILL.md +549 -0
- package/skills/engage/core/README.md +23 -0
- package/skills/engage/core/proven-searches.json +11 -0
- package/skills/engage/core/style-guide.template.md +47 -0
- package/skills/engage/core/tracked-people.json +10 -0
- package/skills/enrich-prospects/SKILL.md +97 -0
- package/skills/find-leads/SKILL.md +467 -0
- package/skills/generate-messages/SKILL.md +2361 -0
- package/skills/interview/SKILL.md +132 -0
- package/skills/interview/core/ENGAGE_STYLE_GUIDE.template.md +54 -0
- package/skills/interview/core/ICP.template.md +54 -0
- package/skills/interview/core/VOICE_PROFILE.template.md +101 -0
- package/skills/providers/apollo.md +520 -0
- package/skills/providers/prospeo.md +398 -0
- package/skills/providers/sales-nav.md +372 -0
- package/skills/providers/signal-discovery.md +495 -0
- package/skills/research/SKILL.md +258 -0
- package/skills/research/config.json +9 -0
- package/skills/research/override.md +13 -0
- package/skills/research-prospect/SKILL.md +99 -0
- package/skills/research-sender/SKILL.md +158 -0
- package/skills/workflow-sequences/SKILL.md +85 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
import { getApi } from "../api.js";
|
|
2
|
+
import { getConfig } from "../auth.js";
|
|
3
|
+
import { assertCreateCampaignPromptLoaded, assertNetNewCreateCampaignResearchReady, markSenderEnrichmentObserved, } from "./flow-preflight.js";
|
|
4
|
+
import { setCampaignInteractionMode, } from "./interaction-mode.js";
|
|
5
|
+
import { fetchCampaignRubrics } from "./processing.js";
|
|
6
|
+
const LEAD_SOURCE_PROVIDERS = {
|
|
7
|
+
APOLLO: "apollo-ai",
|
|
8
|
+
SALES_NAV: "sales-nav",
|
|
9
|
+
PROSPEO: "prospeo",
|
|
10
|
+
SIGNAL_DISCOVERY: "signal-discovery",
|
|
11
|
+
};
|
|
12
|
+
function normalizeLeadSourceProvider(input) {
|
|
13
|
+
if (!input)
|
|
14
|
+
return null;
|
|
15
|
+
if (input === "apollo")
|
|
16
|
+
return LEAD_SOURCE_PROVIDERS.APOLLO;
|
|
17
|
+
const values = Object.values(LEAD_SOURCE_PROVIDERS);
|
|
18
|
+
return values.includes(input)
|
|
19
|
+
? input
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
function buildWatchUrl(config, redirect) {
|
|
23
|
+
const workspaceId = config.activeWorkspaceId || config.workspaceId;
|
|
24
|
+
const workspaceParam = workspaceId
|
|
25
|
+
? `&workspaceId=${encodeURIComponent(workspaceId)}`
|
|
26
|
+
: "";
|
|
27
|
+
return `${config.apiUrl}/auth/continue?token=${config.token}&redirect=${redirect}${workspaceParam}`;
|
|
28
|
+
}
|
|
29
|
+
export const campaignToolDefinitions = [
|
|
30
|
+
{
|
|
31
|
+
name: "enrich_sender",
|
|
32
|
+
description: "Enrich a LinkedIn profile to get the clientProspectId needed for create_campaign. Fast return (~5-8s) with background enrichment. Call this first with the sender's LinkedIn URL before creating a campaign.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
linkedinUrl: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Full LinkedIn profile URL (e.g., https://linkedin.com/in/username)",
|
|
39
|
+
},
|
|
40
|
+
companyDomain: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Optional company domain to use instead of resolving from profile",
|
|
43
|
+
},
|
|
44
|
+
forceRefresh: {
|
|
45
|
+
type: "boolean",
|
|
46
|
+
description: "Skip cache and fetch fresh LinkedIn data. Use when you need the latest profile info.",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
required: ["linkedinUrl"],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "get_campaigns",
|
|
54
|
+
description: "List campaigns for the authenticated user. Returns id, name, createdAt. Ordered by most recent first.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
limit: {
|
|
59
|
+
type: "number",
|
|
60
|
+
description: "Number of campaigns to return (default 10, max 50)",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: [],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "get_campaign",
|
|
68
|
+
description: "Get detailed campaign context including brief, positioning, sender info, lead scoring rubrics, and stats. Returns workflowTableId for fetching rows.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
campaignId: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Campaign ID (e.g., cmp_abc123)",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["campaignId"],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "get_campaign_messages_preview",
|
|
82
|
+
description: "Resolve campaign workflow table and return table meta + lead preview for messaging selection (supports filters + pagination).",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
campaignId: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "Campaign ID (e.g., cmp_abc123)",
|
|
89
|
+
},
|
|
90
|
+
tableId: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Optional override table ID (uses campaign workflowTableId if omitted)",
|
|
93
|
+
},
|
|
94
|
+
leadLimit: {
|
|
95
|
+
type: "number",
|
|
96
|
+
description: "Number of leads to preview (default 25, max 25)",
|
|
97
|
+
},
|
|
98
|
+
page: {
|
|
99
|
+
type: "number",
|
|
100
|
+
description: "Page number for pagination (default 1)",
|
|
101
|
+
},
|
|
102
|
+
filters: {
|
|
103
|
+
type: "array",
|
|
104
|
+
description: "Optional column filters (Clay-style). Use column name or id.",
|
|
105
|
+
items: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
column: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description: "Column name or column id",
|
|
111
|
+
},
|
|
112
|
+
op: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "Operator: eq, neq, contains, starts_with, ends_with, gt, gte, lt, lte, is_empty, is_not_empty, in",
|
|
115
|
+
},
|
|
116
|
+
value: {
|
|
117
|
+
oneOf: [
|
|
118
|
+
{ type: "string" },
|
|
119
|
+
{ type: "number" },
|
|
120
|
+
{ type: "boolean" },
|
|
121
|
+
{
|
|
122
|
+
type: "array",
|
|
123
|
+
items: {
|
|
124
|
+
oneOf: [
|
|
125
|
+
{ type: "string" },
|
|
126
|
+
{ type: "number" },
|
|
127
|
+
{ type: "boolean" },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
caseSensitive: {
|
|
134
|
+
type: "boolean",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
required: ["column", "op"],
|
|
138
|
+
additionalProperties: false,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
required: ["campaignId"],
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "create_campaign",
|
|
148
|
+
description: 'Create a new campaign offer OR resume an existing one. Low-level write tool: load create-campaign workflow instructions first via get_subskill_prompt({ subskillName: "create-campaign" }) (or bootstrap_create_campaign + nextStep). If campaignId is provided, this tool returns the watchUrl + state for that campaign instead of creating a new campaign.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
campaignId: {
|
|
153
|
+
type: "string",
|
|
154
|
+
description: "Optional campaign ID to resume (e.g., cmp_abc123). When provided, other create fields are ignored.",
|
|
155
|
+
},
|
|
156
|
+
name: {
|
|
157
|
+
type: "string",
|
|
158
|
+
description: "Campaign name",
|
|
159
|
+
},
|
|
160
|
+
clientProspectId: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "Enriched client prospect ID",
|
|
163
|
+
},
|
|
164
|
+
offerPositioning: {
|
|
165
|
+
type: "object",
|
|
166
|
+
description: "Offer positioning JSON",
|
|
167
|
+
},
|
|
168
|
+
campaignBrief: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "Campaign brief as markdown string. Use actual newline characters (not escaped \\n). Example: '# Campaign\\n\\n## ICP\\n\\nTech founders...'",
|
|
171
|
+
},
|
|
172
|
+
messageGenerationMode: {
|
|
173
|
+
type: "string",
|
|
174
|
+
enum: ["template", "ai-generated"],
|
|
175
|
+
description: "Defaults to template for autonomous messaging steps. Use ai-generated only when the user explicitly approves freeform AI-generated messages instead of an approved {{token}} template.",
|
|
176
|
+
},
|
|
177
|
+
currentStep: {
|
|
178
|
+
type: ["string", "null"],
|
|
179
|
+
description: "Workflow step ID (headless or UI step ID such as filter-rules)",
|
|
180
|
+
},
|
|
181
|
+
leadSourceType: {
|
|
182
|
+
type: ["string", "null"],
|
|
183
|
+
description: "Lead source type (existing | new)",
|
|
184
|
+
},
|
|
185
|
+
leadSourceProvider: {
|
|
186
|
+
type: ["string", "null"],
|
|
187
|
+
enum: [
|
|
188
|
+
LEAD_SOURCE_PROVIDERS.APOLLO,
|
|
189
|
+
LEAD_SOURCE_PROVIDERS.SALES_NAV,
|
|
190
|
+
LEAD_SOURCE_PROVIDERS.PROSPEO,
|
|
191
|
+
LEAD_SOURCE_PROVIDERS.SIGNAL_DISCOVERY,
|
|
192
|
+
null,
|
|
193
|
+
],
|
|
194
|
+
description: "Lead source provider (apollo-ai | sales-nav | prospeo | signal-discovery)",
|
|
195
|
+
},
|
|
196
|
+
selectedLeadListId: {
|
|
197
|
+
type: ["string", "null"],
|
|
198
|
+
description: "Lead list ID when using existing list",
|
|
199
|
+
},
|
|
200
|
+
senderIds: {
|
|
201
|
+
type: "array",
|
|
202
|
+
items: { type: "string" },
|
|
203
|
+
description: "Outbound sender IDs to attach to the campaign",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
required: [],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "update_campaign",
|
|
211
|
+
description: "Update an existing campaign offer with positioning, brief, or workflow state fields. Returns currentStep plus navigation diagnostics.",
|
|
212
|
+
inputSchema: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
campaignId: {
|
|
216
|
+
type: "string",
|
|
217
|
+
description: "Campaign ID to update",
|
|
218
|
+
},
|
|
219
|
+
offerPositioning: {
|
|
220
|
+
type: "object",
|
|
221
|
+
description: "Updated offer positioning JSON",
|
|
222
|
+
},
|
|
223
|
+
campaignBrief: {
|
|
224
|
+
description: "Updated campaign brief (markdown or JSON)",
|
|
225
|
+
},
|
|
226
|
+
leadSourceType: {
|
|
227
|
+
type: ["string", "null"],
|
|
228
|
+
description: "Lead source type (existing | new)",
|
|
229
|
+
},
|
|
230
|
+
leadSourceProvider: {
|
|
231
|
+
type: ["string", "null"],
|
|
232
|
+
enum: [
|
|
233
|
+
LEAD_SOURCE_PROVIDERS.APOLLO,
|
|
234
|
+
LEAD_SOURCE_PROVIDERS.SALES_NAV,
|
|
235
|
+
LEAD_SOURCE_PROVIDERS.PROSPEO,
|
|
236
|
+
LEAD_SOURCE_PROVIDERS.SIGNAL_DISCOVERY,
|
|
237
|
+
null,
|
|
238
|
+
],
|
|
239
|
+
description: "Lead source provider (apollo-ai | sales-nav | prospeo | signal-discovery)",
|
|
240
|
+
},
|
|
241
|
+
selectedLeadListId: {
|
|
242
|
+
type: ["string", "null"],
|
|
243
|
+
description: "Lead list ID when using existing list",
|
|
244
|
+
},
|
|
245
|
+
currentStep: {
|
|
246
|
+
type: ["string", "null"],
|
|
247
|
+
description: "Headless workflow step ID",
|
|
248
|
+
},
|
|
249
|
+
interactionMode: {
|
|
250
|
+
type: "string",
|
|
251
|
+
enum: ["step-by-step", "autonomous", "ask-when-needed"],
|
|
252
|
+
description: "Execution style for lead sourcing steps (step-by-step | autonomous | ask-when-needed).",
|
|
253
|
+
},
|
|
254
|
+
enableICPFilters: {
|
|
255
|
+
type: ["boolean", "null"],
|
|
256
|
+
description: "Whether ICP filtering is enabled (true/false)",
|
|
257
|
+
},
|
|
258
|
+
useMessagingTemplate: {
|
|
259
|
+
type: ["boolean", "null"],
|
|
260
|
+
description: "Whether to use the messaging template step",
|
|
261
|
+
},
|
|
262
|
+
senderIds: {
|
|
263
|
+
type: "array",
|
|
264
|
+
items: { type: "string" },
|
|
265
|
+
description: "Outbound sender IDs to attach to the campaign",
|
|
266
|
+
},
|
|
267
|
+
rubric: {
|
|
268
|
+
type: "array",
|
|
269
|
+
description: "Lead scoring rubric entries",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
required: ["campaignId"],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "start_campaign",
|
|
277
|
+
description: "Start a paused campaign, enabling the sweeper to send messages.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: {
|
|
281
|
+
campaignId: {
|
|
282
|
+
type: "string",
|
|
283
|
+
description: "Campaign ID to start",
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
required: ["campaignId"],
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "pause_campaign",
|
|
291
|
+
description: "Pause an active campaign, stopping the sweeper from sending messages.",
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: "object",
|
|
294
|
+
properties: {
|
|
295
|
+
campaignId: {
|
|
296
|
+
type: "string",
|
|
297
|
+
description: "Campaign ID to pause",
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
required: ["campaignId"],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "update_campaign_brief",
|
|
305
|
+
description: "Update the campaign brief markdown document. This is a living canvas where you accumulate knowledge during crafting. Use it to: add social proof/case studies discovered during research, note common objections, record successful hooks, or reorganize when user changes direction. You control the full markdown content - read the current brief first, then update it intelligently.",
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: "object",
|
|
308
|
+
properties: {
|
|
309
|
+
campaignId: {
|
|
310
|
+
type: "string",
|
|
311
|
+
description: "Campaign ID to update",
|
|
312
|
+
},
|
|
313
|
+
campaignBrief: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "The updated campaign brief as markdown. You should read the current brief first (from get_campaign), then intelligently edit it to add new sections, update existing content, or reorganize as needed.",
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
required: ["campaignId", "campaignBrief"],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
export async function getCampaigns(limit) {
|
|
323
|
+
const api = getApi();
|
|
324
|
+
const params = limit ? `?limit=${limit}` : "";
|
|
325
|
+
return api.get(`/api/v3/mcp/campaigns${params}`);
|
|
326
|
+
}
|
|
327
|
+
export async function getCampaign(campaignId) {
|
|
328
|
+
const api = getApi();
|
|
329
|
+
const config = getConfig();
|
|
330
|
+
// Fetch campaign context and rubrics in parallel
|
|
331
|
+
const [result, rubricsResult] = await Promise.all([
|
|
332
|
+
api.get(`/api/v3/mcp/campaigns/${campaignId}`),
|
|
333
|
+
fetchCampaignRubrics(campaignId).catch(() => null),
|
|
334
|
+
]);
|
|
335
|
+
// Build watch URL with auto-auth token
|
|
336
|
+
const redirect = encodeURIComponent(`/campaign-builder/${campaignId}?mode=claude`);
|
|
337
|
+
const watchUrl = buildWatchUrl(config, redirect);
|
|
338
|
+
// Merge rubrics into campaignOffer
|
|
339
|
+
const rubrics = (rubricsResult?.rubrics || []).map((r) => ({
|
|
340
|
+
id: r.id || "",
|
|
341
|
+
checkName: r.checkName || "",
|
|
342
|
+
description: r.description || "",
|
|
343
|
+
criterion: r.criterion || "",
|
|
344
|
+
reason: r.reason || "",
|
|
345
|
+
useCheckForScoring: r.useCheckForScoring !== false,
|
|
346
|
+
isRequiredCheck: r.isRequiredCheck === true,
|
|
347
|
+
allowPartialCredit: r.allowPartialCredit !== false,
|
|
348
|
+
strictMatching: r.strictMatching === true,
|
|
349
|
+
}));
|
|
350
|
+
return {
|
|
351
|
+
...result,
|
|
352
|
+
campaignOffer: {
|
|
353
|
+
...result.campaignOffer,
|
|
354
|
+
leadScoringRubrics: rubrics,
|
|
355
|
+
},
|
|
356
|
+
watchUrl,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function normalizeCellValue(cell) {
|
|
360
|
+
if (!cell)
|
|
361
|
+
return null;
|
|
362
|
+
return cell.isOverridden ? cell.overrideValue : cell.result;
|
|
363
|
+
}
|
|
364
|
+
function toText(value) {
|
|
365
|
+
if (value === null || value === undefined)
|
|
366
|
+
return null;
|
|
367
|
+
if (typeof value === "string")
|
|
368
|
+
return value;
|
|
369
|
+
if (typeof value === "number")
|
|
370
|
+
return String(value);
|
|
371
|
+
if (typeof value === "boolean")
|
|
372
|
+
return value ? "true" : "false";
|
|
373
|
+
if (Array.isArray(value)) {
|
|
374
|
+
const parts = value
|
|
375
|
+
.map((item) => toText(item))
|
|
376
|
+
.filter((item) => Boolean(item));
|
|
377
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
378
|
+
}
|
|
379
|
+
if (typeof value === "object") {
|
|
380
|
+
const record = value;
|
|
381
|
+
const preferredKeys = ["display", "value", "label", "name"];
|
|
382
|
+
for (const key of preferredKeys) {
|
|
383
|
+
if (key in record) {
|
|
384
|
+
const resolved = toText(record[key]);
|
|
385
|
+
if (resolved)
|
|
386
|
+
return resolved;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if ("linkedin_url" in record) {
|
|
390
|
+
const resolved = toText(record.linkedin_url);
|
|
391
|
+
if (resolved)
|
|
392
|
+
return resolved;
|
|
393
|
+
}
|
|
394
|
+
const firstValue = Object.values(record)[0];
|
|
395
|
+
return toText(firstValue);
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
function isTruthyValue(value) {
|
|
400
|
+
if (value === true)
|
|
401
|
+
return true;
|
|
402
|
+
if (value === false || value === null || value === undefined)
|
|
403
|
+
return false;
|
|
404
|
+
if (typeof value === "number")
|
|
405
|
+
return value === 1;
|
|
406
|
+
if (typeof value === "string") {
|
|
407
|
+
const normalized = value.trim().toLowerCase();
|
|
408
|
+
return normalized === "true" || normalized === "1" || normalized === "yes";
|
|
409
|
+
}
|
|
410
|
+
if (typeof value === "object") {
|
|
411
|
+
const record = value;
|
|
412
|
+
if ("value" in record)
|
|
413
|
+
return isTruthyValue(record.value);
|
|
414
|
+
if ("checked" in record)
|
|
415
|
+
return isTruthyValue(record.checked);
|
|
416
|
+
if ("selected" in record)
|
|
417
|
+
return isTruthyValue(record.selected);
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
function normalizeColumnName(value) {
|
|
422
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
423
|
+
}
|
|
424
|
+
function findColumnByName(columns, names) {
|
|
425
|
+
const normalized = new Set(names.map((name) => normalizeColumnName(name)));
|
|
426
|
+
return (columns.find((col) => normalized.has(normalizeColumnName(col.name))) || null);
|
|
427
|
+
}
|
|
428
|
+
function findColumnByType(columns, type) {
|
|
429
|
+
return columns.find((col) => col.columnType === type) || null;
|
|
430
|
+
}
|
|
431
|
+
export async function getCampaignMessagesPreview(input) {
|
|
432
|
+
const api = getApi();
|
|
433
|
+
const leadLimitRaw = input.leadLimit ?? 25;
|
|
434
|
+
const leadLimit = Math.min(25, Math.max(1, leadLimitRaw));
|
|
435
|
+
const page = Math.max(1, input.page ?? 1);
|
|
436
|
+
let tableId = input.tableId;
|
|
437
|
+
if (!tableId) {
|
|
438
|
+
const offer = await api.get(`/api/v2/campaign-offers/${input.campaignId}`);
|
|
439
|
+
tableId = offer.workflowTableId || undefined;
|
|
440
|
+
}
|
|
441
|
+
if (!tableId) {
|
|
442
|
+
throw new Error("No workflowTableId found for this campaign. Import leads into the campaign table first.");
|
|
443
|
+
}
|
|
444
|
+
let stats = null;
|
|
445
|
+
try {
|
|
446
|
+
const statsResponse = await api.get(`/api/v3/workflow-tables/${tableId}/stats`);
|
|
447
|
+
stats = {
|
|
448
|
+
totalRows: statsResponse.totalRows ?? 0,
|
|
449
|
+
enrichedCount: statsResponse.enrichedCount ?? 0,
|
|
450
|
+
needsEnrichCount: statsResponse.needsEnrichCount ?? 0,
|
|
451
|
+
messagesCount: statsResponse.messagesCount ?? 0,
|
|
452
|
+
approvedCount: statsResponse.approvedCount ?? 0,
|
|
453
|
+
needsApprovalCount: statsResponse.needsApprovalCount ?? 0,
|
|
454
|
+
processingCount: statsResponse.processingCount ?? 0,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
stats = null;
|
|
459
|
+
}
|
|
460
|
+
const metaResponse = await api.get(`/api/v3/workflow-tables/${tableId}?mode=meta`);
|
|
461
|
+
const columns = (metaResponse.columns || []).map((col) => ({
|
|
462
|
+
id: col.id,
|
|
463
|
+
name: col.name,
|
|
464
|
+
columnType: col.columnType,
|
|
465
|
+
}));
|
|
466
|
+
const nameColumn = findColumnByName(columns, [
|
|
467
|
+
"name",
|
|
468
|
+
"full name",
|
|
469
|
+
"full_name",
|
|
470
|
+
"contact name",
|
|
471
|
+
"lead name",
|
|
472
|
+
"person name",
|
|
473
|
+
]) || findColumnByType(columns, "text");
|
|
474
|
+
const titleColumn = findColumnByName(columns, [
|
|
475
|
+
"title",
|
|
476
|
+
"job title",
|
|
477
|
+
"job_title",
|
|
478
|
+
"position",
|
|
479
|
+
"role",
|
|
480
|
+
"headline",
|
|
481
|
+
]);
|
|
482
|
+
const companyColumn = findColumnByName(columns, [
|
|
483
|
+
"company",
|
|
484
|
+
"company name",
|
|
485
|
+
"company_name",
|
|
486
|
+
"organization",
|
|
487
|
+
"org",
|
|
488
|
+
"employer",
|
|
489
|
+
"current company",
|
|
490
|
+
"company website",
|
|
491
|
+
"company site",
|
|
492
|
+
"website",
|
|
493
|
+
]);
|
|
494
|
+
const linkedinColumn = findColumnByType(columns, "linkedin_url") ||
|
|
495
|
+
columns.find((col) => col.name.toLowerCase().includes("linkedin")) ||
|
|
496
|
+
null;
|
|
497
|
+
const messageColumn = findColumnByName(columns, ["message", "message body"]) ||
|
|
498
|
+
findColumnByType(columns, "message") ||
|
|
499
|
+
findColumnByName(columns, ["generate message", "generate_message"]);
|
|
500
|
+
const approveColumn = findColumnByName(columns, [
|
|
501
|
+
"approved",
|
|
502
|
+
"enable scheduling",
|
|
503
|
+
"enable_scheduling",
|
|
504
|
+
]) ||
|
|
505
|
+
findColumnByType(columns, "approve") ||
|
|
506
|
+
null;
|
|
507
|
+
const filtersParam = input.filters && input.filters.length > 0
|
|
508
|
+
? `&filters=${encodeURIComponent(JSON.stringify(input.filters))}`
|
|
509
|
+
: "";
|
|
510
|
+
const rowsResponse = await api.get(`/api/v3/workflow-tables/${tableId}/rows?page=${page}&limit=${leadLimit}${filtersParam}`);
|
|
511
|
+
const previewRows = (rowsResponse.rows || []).map((row, index) => {
|
|
512
|
+
const cells = row.cells || [];
|
|
513
|
+
const cellByColumnId = new Map(cells
|
|
514
|
+
.filter((cell) => cell.columnId)
|
|
515
|
+
.map((cell) => [cell.columnId, cell]));
|
|
516
|
+
const getCellText = (columnId) => {
|
|
517
|
+
if (!columnId)
|
|
518
|
+
return null;
|
|
519
|
+
const cell = cellByColumnId.get(columnId);
|
|
520
|
+
return toText(normalizeCellValue(cell || null));
|
|
521
|
+
};
|
|
522
|
+
const name = getCellText(nameColumn?.id) || "Unknown";
|
|
523
|
+
const title = getCellText(titleColumn?.id) || "";
|
|
524
|
+
const company = getCellText(companyColumn?.id) || "";
|
|
525
|
+
const linkedinUrl = getCellText(linkedinColumn?.id) || null;
|
|
526
|
+
const messageCell = messageColumn
|
|
527
|
+
? cellByColumnId.get(messageColumn.id) || null
|
|
528
|
+
: null;
|
|
529
|
+
const messageValue = toText(normalizeCellValue(messageCell || null)) || "";
|
|
530
|
+
const messagePreview = messageValue.length > 140
|
|
531
|
+
? `${messageValue.slice(0, 140)}...`
|
|
532
|
+
: messageValue || null;
|
|
533
|
+
const approveCell = approveColumn
|
|
534
|
+
? cellByColumnId.get(approveColumn.id) || null
|
|
535
|
+
: null;
|
|
536
|
+
const approveValue = normalizeCellValue(approveCell || null);
|
|
537
|
+
const isApproved = messageValue.trim().length > 0 && isTruthyValue(approveValue);
|
|
538
|
+
return {
|
|
539
|
+
index: index + 1,
|
|
540
|
+
rowId: row.id,
|
|
541
|
+
name,
|
|
542
|
+
title,
|
|
543
|
+
company,
|
|
544
|
+
linkedinUrl,
|
|
545
|
+
messagePreview,
|
|
546
|
+
messageCellId: messageCell?.id ?? null,
|
|
547
|
+
hasMessage: messageValue.trim().length > 0,
|
|
548
|
+
approveCellId: approveCell?.id ?? null,
|
|
549
|
+
isApproved,
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
return {
|
|
553
|
+
campaignId: input.campaignId,
|
|
554
|
+
tableId,
|
|
555
|
+
stats,
|
|
556
|
+
meta: {
|
|
557
|
+
columns,
|
|
558
|
+
keyColumns: {
|
|
559
|
+
nameColumnId: nameColumn?.id || null,
|
|
560
|
+
titleColumnId: titleColumn?.id || null,
|
|
561
|
+
companyColumnId: companyColumn?.id || null,
|
|
562
|
+
linkedinUrlColumnId: linkedinColumn?.id || null,
|
|
563
|
+
messageColumnId: messageColumn?.id || null,
|
|
564
|
+
approveColumnId: approveColumn?.id || null,
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
leads: {
|
|
568
|
+
limit: leadLimit,
|
|
569
|
+
rows: previewRows,
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function deriveOfferValue(name, briefContent) {
|
|
574
|
+
const lines = briefContent
|
|
575
|
+
.split(/\r?\n/)
|
|
576
|
+
.map((line) => line.trim())
|
|
577
|
+
.filter(Boolean);
|
|
578
|
+
if (lines.length === 0)
|
|
579
|
+
return name;
|
|
580
|
+
const headingMatch = lines[0].match(/^#{1,6}\s+(.*)$/);
|
|
581
|
+
const heading = headingMatch?.[1]?.trim();
|
|
582
|
+
if (heading)
|
|
583
|
+
return heading.slice(0, 120);
|
|
584
|
+
return lines[0].slice(0, 120);
|
|
585
|
+
}
|
|
586
|
+
export async function createCampaign(input) {
|
|
587
|
+
const api = getApi();
|
|
588
|
+
const config = getConfig();
|
|
589
|
+
// Idempotent resume path
|
|
590
|
+
if (input.campaignId) {
|
|
591
|
+
const existing = await api.get(`/api/v2/campaign-offers/${input.campaignId}`);
|
|
592
|
+
const redirect = encodeURIComponent(`/campaign-builder/${existing.id}?mode=claude`);
|
|
593
|
+
const watchUrl = buildWatchUrl(config, redirect);
|
|
594
|
+
const hasCreateFields = input.name ||
|
|
595
|
+
input.clientProspectId ||
|
|
596
|
+
input.offerPositioning !== undefined ||
|
|
597
|
+
input.campaignBrief;
|
|
598
|
+
return {
|
|
599
|
+
id: existing.id,
|
|
600
|
+
name: existing.name,
|
|
601
|
+
createdAt: existing.createdAt,
|
|
602
|
+
currentStep: existing.currentStep ?? null,
|
|
603
|
+
watchUrl,
|
|
604
|
+
resumed: true,
|
|
605
|
+
warning: hasCreateFields
|
|
606
|
+
? "campaignId provided; resume mode ignores create fields."
|
|
607
|
+
: undefined,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
assertCreateCampaignPromptLoaded();
|
|
611
|
+
const missing = [];
|
|
612
|
+
if (!input.name)
|
|
613
|
+
missing.push("name");
|
|
614
|
+
if (!input.clientProspectId)
|
|
615
|
+
missing.push("clientProspectId");
|
|
616
|
+
if (!input.campaignBrief)
|
|
617
|
+
missing.push("campaignBrief");
|
|
618
|
+
if (missing.length > 0) {
|
|
619
|
+
throw new Error("VALIDATION_ERROR: create_campaign requires either campaignId (resume) " +
|
|
620
|
+
`or all create fields. Missing: ${missing.join(", ")}.\n\n` +
|
|
621
|
+
"Remediation:\n" +
|
|
622
|
+
'- For full workflow, call get_subskill_prompt({ subskillName: "create-campaign" }) and follow it.\n' +
|
|
623
|
+
'- For net-new campaign creation, call enrich_sender first, then call get_subskill_prompt({ subskillName: "research-sender" }), run sender research, call complete_sender_research(...), and only then call create_campaign.\n' +
|
|
624
|
+
"- For resume, call create_campaign with campaignId only.");
|
|
625
|
+
}
|
|
626
|
+
assertNetNewCreateCampaignResearchReady();
|
|
627
|
+
const name = input.name;
|
|
628
|
+
const clientProspectId = input.clientProspectId;
|
|
629
|
+
const campaignBrief = input.campaignBrief;
|
|
630
|
+
// Validate required campaignBrief - must be non-empty markdown
|
|
631
|
+
// Normalize escaped newlines - MCP tool calls pass literal "\n" text (backslash + n)
|
|
632
|
+
// because JSON serialization escapes the backslash
|
|
633
|
+
const rawBrief = typeof campaignBrief === "string"
|
|
634
|
+
? campaignBrief.trim()
|
|
635
|
+
: String(campaignBrief);
|
|
636
|
+
// Replace literal \n (backslash + n) with actual newline characters
|
|
637
|
+
// In regex: /\\n/ matches literal backslash followed by n
|
|
638
|
+
const briefContent = rawBrief.replace(/\\n/g, "\n");
|
|
639
|
+
if (briefContent.length < 50) {
|
|
640
|
+
throw new Error("VALIDATION_ERROR: campaignBrief is too short (min 50 chars).\n\n" +
|
|
641
|
+
"Please provide a comprehensive campaign brief with:\n" +
|
|
642
|
+
"- Target audience description\n" +
|
|
643
|
+
"- Value proposition\n" +
|
|
644
|
+
"- Key messages\n" +
|
|
645
|
+
"- Tone guidelines");
|
|
646
|
+
}
|
|
647
|
+
const currentStep = input.currentStep ?? null;
|
|
648
|
+
const isAutonomousMessageMint = currentStep === "auto-execute-leads" ||
|
|
649
|
+
currentStep === "validate-sample" ||
|
|
650
|
+
currentStep === "auto-execute-messaging";
|
|
651
|
+
const explicitlyAllowsAiGenerated = input.messageGenerationMode === "ai-generated";
|
|
652
|
+
const hasTemplateToken = /\{\{\s*\S[^}]*\}\}/.test(briefContent);
|
|
653
|
+
if (isAutonomousMessageMint &&
|
|
654
|
+
!explicitlyAllowsAiGenerated &&
|
|
655
|
+
!hasTemplateToken) {
|
|
656
|
+
throw new Error("VALIDATION_ERROR: campaignBrief must include at least one supported {{token}} before minting into autonomous messaging.\n\n" +
|
|
657
|
+
"Why this is blocked:\n" +
|
|
658
|
+
"- Generate Message enters template mode only when campaignBrief.content contains {{...}}.\n" +
|
|
659
|
+
"- Without a token, the message cascade uses freeform AI generation and may rewrite the approved message.\n\n" +
|
|
660
|
+
"Remediation:\n" +
|
|
661
|
+
"- Add an `## Approved Message Template` section to campaignBrief with the exact live template, e.g. `hey {{first_name}},`.\n" +
|
|
662
|
+
"- Add `### Token Fill Rules` for every prospect-row token.\n" +
|
|
663
|
+
'- Only pass messageGenerationMode: "ai-generated" if the user explicitly approved freeform AI-generated messages.');
|
|
664
|
+
}
|
|
665
|
+
const offerPositioning = input.offerPositioning ??
|
|
666
|
+
{
|
|
667
|
+
offer: {
|
|
668
|
+
value: deriveOfferValue(name, briefContent),
|
|
669
|
+
isAnchor: true,
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
// Format campaignBrief as expected object structure { name, content }
|
|
673
|
+
// Default currentStep to "create-offer" (Plan step) so watch link shows brief first
|
|
674
|
+
// Default leadSourceType to "new" to skip choose-source step (most headless users want new leads)
|
|
675
|
+
const normalizedLeadSourceProvider = input.leadSourceProvider === undefined
|
|
676
|
+
? undefined
|
|
677
|
+
: normalizeLeadSourceProvider(input.leadSourceProvider);
|
|
678
|
+
const { messageGenerationMode: _messageGenerationMode, ...apiInput } = input;
|
|
679
|
+
const formattedInput = {
|
|
680
|
+
...apiInput,
|
|
681
|
+
name,
|
|
682
|
+
clientProspectId,
|
|
683
|
+
offerPositioning,
|
|
684
|
+
...(input.leadSourceProvider !== undefined
|
|
685
|
+
? { leadSourceProvider: normalizedLeadSourceProvider }
|
|
686
|
+
: {}),
|
|
687
|
+
currentStep: currentStep ?? "create-offer",
|
|
688
|
+
leadSourceType: input.leadSourceType ?? "new",
|
|
689
|
+
campaignBrief: {
|
|
690
|
+
name, // Use campaign name as brief name
|
|
691
|
+
content: briefContent, // The markdown content
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
const result = await api.post(`/api/v2/campaign-offers`, formattedInput);
|
|
695
|
+
// Build watch URL for Claude Code to share with user
|
|
696
|
+
const redirect = encodeURIComponent(`/campaign-builder/${result.id}?mode=claude`);
|
|
697
|
+
const watchUrl = buildWatchUrl(config, redirect);
|
|
698
|
+
// Serialize to only essential fields for context efficiency
|
|
699
|
+
return {
|
|
700
|
+
id: result.id,
|
|
701
|
+
name: result.name,
|
|
702
|
+
createdAt: result.createdAt,
|
|
703
|
+
currentStep: result.currentStep,
|
|
704
|
+
watchUrl,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
export async function updateCampaign(campaignId, input) {
|
|
708
|
+
const api = getApi();
|
|
709
|
+
const { campaignId: _ignore, interactionMode, ...updates } = input;
|
|
710
|
+
// Normalize campaignBrief: if a plain string is passed, wrap it in the
|
|
711
|
+
// { name, content } structure the UI expects. createCampaign already does
|
|
712
|
+
// this, but updateCampaign was passing the string straight through.
|
|
713
|
+
if (typeof updates.campaignBrief === "string") {
|
|
714
|
+
updates.campaignBrief = {
|
|
715
|
+
content: updates.campaignBrief,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
if (typeof interactionMode === "string" && interactionMode.trim()) {
|
|
719
|
+
setCampaignInteractionMode({
|
|
720
|
+
campaignId,
|
|
721
|
+
interactionMode,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
if ("leadSourceProvider" in updates) {
|
|
725
|
+
if (updates.leadSourceProvider === null) {
|
|
726
|
+
updates.leadSourceProvider = null;
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
const normalizedLeadSourceProvider = normalizeLeadSourceProvider(updates.leadSourceProvider);
|
|
730
|
+
if (normalizedLeadSourceProvider) {
|
|
731
|
+
updates.leadSourceProvider = normalizedLeadSourceProvider;
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
delete updates.leadSourceProvider;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// Hotfix: when rubrics are saved with enableICPFilters, ensure the UI step
|
|
739
|
+
// is set to "filter-rules" so the frontend can render the filter state.
|
|
740
|
+
// Without this, the MCP caller can save rubrics + enableICPFilters on the
|
|
741
|
+
// backend while the UI currentStep points somewhere else (e.g. "messages"),
|
|
742
|
+
// causing the UI to be out of sync with the backend filter state.
|
|
743
|
+
if (updates.enableICPFilters === true &&
|
|
744
|
+
Array.isArray(updates.rubric) &&
|
|
745
|
+
updates.rubric.length > 0 &&
|
|
746
|
+
!updates.currentStep) {
|
|
747
|
+
updates.currentStep = "filter-rules";
|
|
748
|
+
}
|
|
749
|
+
const result = await api.put(`/api/v2/campaign-offers/${campaignId}`, updates);
|
|
750
|
+
// Serialize to only essential fields for context efficiency
|
|
751
|
+
return {
|
|
752
|
+
id: result.id,
|
|
753
|
+
name: result.name,
|
|
754
|
+
currentStep: result.currentStep,
|
|
755
|
+
updatedAt: result.updatedAt,
|
|
756
|
+
// Provide full campaign snapshot for server-side context hydration.
|
|
757
|
+
_campaign: result,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
export async function startCampaign(campaignId) {
|
|
761
|
+
const api = getApi();
|
|
762
|
+
return api.post(`/api/v3/campaigns/${campaignId}/start`);
|
|
763
|
+
}
|
|
764
|
+
export async function pauseCampaign(campaignId) {
|
|
765
|
+
const api = getApi();
|
|
766
|
+
return api.post(`/api/v3/campaigns/${campaignId}/pause`);
|
|
767
|
+
}
|
|
768
|
+
export async function updateCampaignBrief(campaignId, campaignBrief) {
|
|
769
|
+
const api = getApi();
|
|
770
|
+
return api.patch(`/api/v3/mcp/campaigns/${campaignId}`, { campaignBrief });
|
|
771
|
+
}
|
|
772
|
+
export async function enrichSender(linkedinUrl, companyDomain, forceRefresh) {
|
|
773
|
+
const api = getApi();
|
|
774
|
+
const result = await api.post(`/api/v4/enrich-prospect`, {
|
|
775
|
+
linkedinUrl,
|
|
776
|
+
companyDomain,
|
|
777
|
+
force: forceRefresh,
|
|
778
|
+
});
|
|
779
|
+
markSenderEnrichmentObserved({
|
|
780
|
+
enrichmentStatus: result.enrichmentStatus,
|
|
781
|
+
caseStudies: result.caseStudies,
|
|
782
|
+
proof: result.reviews,
|
|
783
|
+
});
|
|
784
|
+
// Serialize to compact digests — strip raw blobs to save context tokens
|
|
785
|
+
const personData = (result.personData || {});
|
|
786
|
+
const companyData = (result.companyData || {});
|
|
787
|
+
const positioning = (result.positioning || {});
|
|
788
|
+
const caseStudies = result.caseStudies;
|
|
789
|
+
const reviews = result.reviews;
|
|
790
|
+
const base = {
|
|
791
|
+
clientProspectId: result.id,
|
|
792
|
+
sender: {
|
|
793
|
+
fullName: personData.fullName || "",
|
|
794
|
+
headline: personData.headline,
|
|
795
|
+
title: personData.currentPosition?.title,
|
|
796
|
+
company: personData.currentPosition?.company?.name,
|
|
797
|
+
},
|
|
798
|
+
companyDomain: result.companyDomain,
|
|
799
|
+
enrichmentStatus: result.enrichmentStatus,
|
|
800
|
+
};
|
|
801
|
+
if (result.enrichmentStatus !== "complete")
|
|
802
|
+
return base;
|
|
803
|
+
// Build compact companySnapshot
|
|
804
|
+
base.companySnapshot = {
|
|
805
|
+
name: companyData.name || "",
|
|
806
|
+
domain: companyData.domain || result.companyDomain,
|
|
807
|
+
industry: companyData.industry,
|
|
808
|
+
employeeRange: companyData.employeeRange,
|
|
809
|
+
description: companyData.description,
|
|
810
|
+
linkedinUrl: companyData.linkedinUrl,
|
|
811
|
+
};
|
|
812
|
+
// Build compact senderBackground
|
|
813
|
+
const experience = personData.experience || [];
|
|
814
|
+
base.senderBackground = {
|
|
815
|
+
experience: experience.slice(0, 4).map((exp) => ({
|
|
816
|
+
title: exp.title || "",
|
|
817
|
+
company: exp.companyName || exp.company || "",
|
|
818
|
+
duration: exp.duration,
|
|
819
|
+
})),
|
|
820
|
+
education: personData.education?.[0]?.schoolName,
|
|
821
|
+
followerCount: personData.followerCount,
|
|
822
|
+
connectionCount: personData.connectionCount,
|
|
823
|
+
};
|
|
824
|
+
// Build compact proofDigest from caseStudies + reviews + positioning
|
|
825
|
+
let caseStudyCount = 0;
|
|
826
|
+
let caseStudySummary = null;
|
|
827
|
+
if (caseStudies) {
|
|
828
|
+
try {
|
|
829
|
+
const parsed = JSON.parse(caseStudies);
|
|
830
|
+
const studies = parsed.caseStudies || [];
|
|
831
|
+
caseStudyCount = studies.length;
|
|
832
|
+
caseStudySummary = parsed.summary || null;
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
// ignore parse errors
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
let reviewHighlight = null;
|
|
839
|
+
if (reviews) {
|
|
840
|
+
try {
|
|
841
|
+
const parsed = JSON.parse(reviews);
|
|
842
|
+
// Cross-check review sites against confirmed company domain
|
|
843
|
+
const sites = (parsed.reviewSites || []);
|
|
844
|
+
const relevantSites = sites.filter((s) => {
|
|
845
|
+
const url = s.url || "";
|
|
846
|
+
const domain = result.companyDomain.replace(/^www\./, "");
|
|
847
|
+
const companyName = (companyData.name || "").toLowerCase();
|
|
848
|
+
return (url.includes(domain) ||
|
|
849
|
+
url.includes(companyName.replace(/\s+/g, "-")) ||
|
|
850
|
+
url.includes(companyName.replace(/\s+/g, "")));
|
|
851
|
+
});
|
|
852
|
+
if (relevantSites.length > 0) {
|
|
853
|
+
reviewHighlight = relevantSites
|
|
854
|
+
.map((s) => `${s.platform}: ${s.reviewCount || "reviews found"} (${s.url})`)
|
|
855
|
+
.join("; ");
|
|
856
|
+
}
|
|
857
|
+
// Fall back to summary if no site-level match but summary exists
|
|
858
|
+
if (!reviewHighlight && parsed.summary) {
|
|
859
|
+
reviewHighlight = parsed.summary;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
// ignore parse errors
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
let positioningOneLiner = null;
|
|
867
|
+
let keyDifferentiators = [];
|
|
868
|
+
if (positioning) {
|
|
869
|
+
try {
|
|
870
|
+
const desc = typeof positioning === "string" ? JSON.parse(positioning) : positioning;
|
|
871
|
+
const descObj = typeof desc.description === "string"
|
|
872
|
+
? JSON.parse(desc.description)
|
|
873
|
+
: desc.description || {};
|
|
874
|
+
positioningOneLiner =
|
|
875
|
+
descObj.UniqueValueProposition ||
|
|
876
|
+
descObj.ExamplePositioningStatement ||
|
|
877
|
+
null;
|
|
878
|
+
keyDifferentiators = descObj.keyDifferentiators || [];
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
// ignore parse errors
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
base.proofDigest = {
|
|
885
|
+
caseStudyCount,
|
|
886
|
+
caseStudySummary,
|
|
887
|
+
reviewHighlight,
|
|
888
|
+
positioningOneLiner,
|
|
889
|
+
keyDifferentiators,
|
|
890
|
+
};
|
|
891
|
+
return base;
|
|
892
|
+
}
|