@sellable/mcp 0.1.31 → 0.1.33
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/dist/auth.d.ts +25 -0
- package/dist/auth.js +30 -0
- package/dist/server.js +13 -5
- package/dist/tools/auth.d.ts +8 -0
- package/dist/tools/auth.js +20 -1
- package/dist/tools/campaigns.d.ts +10 -97
- package/dist/tools/campaigns.js +29 -150
- package/dist/tools/cli-login.d.ts +86 -0
- package/dist/tools/cli-login.js +208 -0
- package/dist/tools/flow-preflight.d.ts +3 -16
- package/dist/tools/flow-preflight.js +6 -25
- package/dist/tools/linkedin.d.ts +14 -1
- package/dist/tools/linkedin.js +10 -2
- package/dist/tools/navigation.js +2 -2
- package/dist/tools/one-off.d.ts +15 -2
- package/dist/tools/one-off.js +23 -6
- package/dist/tools/prompts.d.ts +2 -2
- package/dist/tools/prompts.js +2 -2
- package/dist/tools/readiness.d.ts +1 -0
- package/dist/tools/readiness.js +4 -2
- package/dist/tools/sequencer.d.ts +7 -0
- package/dist/tools/sequencer.js +8 -2
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +82 -5
- package/skills/create-campaign/references/ai-native-tokens.md +266 -0
- package/skills/create-campaign/references/token-fill-examples.md +93 -0
- package/skills/create-campaign-brief/references/brief-template.md +11 -0
- package/skills/create-campaign-v2/SKILL.md +44 -24
- package/skills/create-campaign-v2/core/flow.v2.json +0 -2
- package/skills/generate-messages/SKILL.md +38 -0
- package/skills/research-sender/SKILL.md +78 -36
package/dist/auth.d.ts
CHANGED
|
@@ -44,6 +44,31 @@ export declare function updateActiveWorkspace(params: {
|
|
|
44
44
|
workspaceId: string;
|
|
45
45
|
workspaceName?: string | null;
|
|
46
46
|
}): void;
|
|
47
|
+
/**
|
|
48
|
+
* Write a fresh Sellable config to disk in the canonical flat shape.
|
|
49
|
+
*
|
|
50
|
+
* Used by the FTUX `wait_for_cli_login` MCP tool after a successful magic-link
|
|
51
|
+
* sign-in. Composes `getConfigPath()` + `writeRawConfigFile()` and writes the
|
|
52
|
+
* same on-disk shape that `packages/sellable-install/bin/sellable-install.mjs`
|
|
53
|
+
* `writeAuth(opts)` produces, and that the auth resolution chain in
|
|
54
|
+
* `getConfig()` reads.
|
|
55
|
+
*
|
|
56
|
+
* Field set is intentionally minimal — `{ token, activeWorkspaceId,
|
|
57
|
+
* activeWorkspaceName?, apiUrl }` — matching the legacy flat format. Do NOT
|
|
58
|
+
* introduce new fields here; that's a config-shape migration, not this helper's
|
|
59
|
+
* job.
|
|
60
|
+
*
|
|
61
|
+
* Because `DISABLE_CONFIG_CACHE = true`, the next `getConfig()` call sees the
|
|
62
|
+
* new config without an MCP server restart.
|
|
63
|
+
*/
|
|
64
|
+
export declare function writeNewConfig(opts: {
|
|
65
|
+
token: string;
|
|
66
|
+
activeWorkspaceId: string;
|
|
67
|
+
activeWorkspaceName?: string;
|
|
68
|
+
apiUrl: string;
|
|
69
|
+
}): {
|
|
70
|
+
configPath: string;
|
|
71
|
+
};
|
|
47
72
|
export declare function getEngageState(): {
|
|
48
73
|
activeWorkspaceId: string | null;
|
|
49
74
|
state: EngageWorkspaceState | null;
|
package/dist/auth.js
CHANGED
|
@@ -178,6 +178,36 @@ function writeRawConfigFile(configPath, raw) {
|
|
|
178
178
|
fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
179
179
|
cachedConfig = null;
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Write a fresh Sellable config to disk in the canonical flat shape.
|
|
183
|
+
*
|
|
184
|
+
* Used by the FTUX `wait_for_cli_login` MCP tool after a successful magic-link
|
|
185
|
+
* sign-in. Composes `getConfigPath()` + `writeRawConfigFile()` and writes the
|
|
186
|
+
* same on-disk shape that `packages/sellable-install/bin/sellable-install.mjs`
|
|
187
|
+
* `writeAuth(opts)` produces, and that the auth resolution chain in
|
|
188
|
+
* `getConfig()` reads.
|
|
189
|
+
*
|
|
190
|
+
* Field set is intentionally minimal — `{ token, activeWorkspaceId,
|
|
191
|
+
* activeWorkspaceName?, apiUrl }` — matching the legacy flat format. Do NOT
|
|
192
|
+
* introduce new fields here; that's a config-shape migration, not this helper's
|
|
193
|
+
* job.
|
|
194
|
+
*
|
|
195
|
+
* Because `DISABLE_CONFIG_CACHE = true`, the next `getConfig()` call sees the
|
|
196
|
+
* new config without an MCP server restart.
|
|
197
|
+
*/
|
|
198
|
+
export function writeNewConfig(opts) {
|
|
199
|
+
const configPath = getConfigPath();
|
|
200
|
+
const raw = {
|
|
201
|
+
token: opts.token,
|
|
202
|
+
activeWorkspaceId: opts.activeWorkspaceId,
|
|
203
|
+
apiUrl: opts.apiUrl,
|
|
204
|
+
};
|
|
205
|
+
if (opts.activeWorkspaceName) {
|
|
206
|
+
raw.activeWorkspaceName = opts.activeWorkspaceName;
|
|
207
|
+
}
|
|
208
|
+
writeRawConfigFile(configPath, raw);
|
|
209
|
+
return { configPath };
|
|
210
|
+
}
|
|
181
211
|
function getActiveEnvConfigRef(raw) {
|
|
182
212
|
if (raw?.activeEnv && raw?.environments) {
|
|
183
213
|
const envName = raw.activeEnv;
|
package/dist/server.js
CHANGED
|
@@ -5,8 +5,9 @@ import { getSkillByName, listSkills } from "./skills.js";
|
|
|
5
5
|
import { authToolDefinitions, getAuthStatus } from "./tools/auth.js";
|
|
6
6
|
import { blueprintCommitToolDefinitions, handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
|
|
7
7
|
import { bootstrapCreateCampaign, bootstrapToolDefinitions, } from "./tools/bootstrap.js";
|
|
8
|
-
import { campaignToolDefinitions, createCampaign,
|
|
8
|
+
import { campaignToolDefinitions, createCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
|
|
9
9
|
import { cellToolDefinitions, queueCells, updateCell } from "./tools/cells.js";
|
|
10
|
+
import { handleStartCliLogin, handleWaitForCliLogin, startCliLoginToolDef, waitForCliLoginToolDef, } from "./tools/cli-login.js";
|
|
10
11
|
import { contextToolDefinitions, getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
|
|
11
12
|
import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, directCampaignToolDefinitions, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
|
|
12
13
|
import { bootstrapEngage, bootstrapEngageMulti, engageBootstrapToolDefinitions, } from "./tools/engage-bootstrap.js";
|
|
@@ -42,6 +43,8 @@ const server = new Server({
|
|
|
42
43
|
const allTools = [
|
|
43
44
|
...campaignToolDefinitions,
|
|
44
45
|
...authToolDefinitions,
|
|
46
|
+
startCliLoginToolDef,
|
|
47
|
+
waitForCliLoginToolDef,
|
|
45
48
|
...bootstrapToolDefinitions,
|
|
46
49
|
...engageBootstrapToolDefinitions,
|
|
47
50
|
...engageDiscoveryToolDefinitions,
|
|
@@ -108,6 +111,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
108
111
|
case "get_auth_status":
|
|
109
112
|
result = await getAuthStatus();
|
|
110
113
|
break;
|
|
114
|
+
case "start_cli_login":
|
|
115
|
+
result = await handleStartCliLogin(args);
|
|
116
|
+
break;
|
|
117
|
+
case "wait_for_cli_login":
|
|
118
|
+
result = await handleWaitForCliLogin(args);
|
|
119
|
+
break;
|
|
111
120
|
case "bootstrap_engage":
|
|
112
121
|
result = await bootstrapEngage(args);
|
|
113
122
|
break;
|
|
@@ -168,9 +177,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
168
177
|
case "add_teammate":
|
|
169
178
|
result = await addTeammate(args);
|
|
170
179
|
break;
|
|
171
|
-
case "enrich_sender":
|
|
172
|
-
result = await enrichSender(args?.linkedinUrl, args?.companyDomain, args?.forceRefresh);
|
|
173
|
-
break;
|
|
174
180
|
case "get_campaigns":
|
|
175
181
|
result = await getCampaigns(args?.limit);
|
|
176
182
|
break;
|
|
@@ -406,7 +412,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
406
412
|
result = await fetchLinkedInPosts(args?.linkedinUrl, args?.limit);
|
|
407
413
|
break;
|
|
408
414
|
case "fetch_linkedin_profile":
|
|
409
|
-
result = await fetchLinkedInProfile(args?.linkedinUrl
|
|
415
|
+
result = await fetchLinkedInProfile(args?.linkedinUrl, {
|
|
416
|
+
full: args?.full === true,
|
|
417
|
+
});
|
|
410
418
|
break;
|
|
411
419
|
case "fetch_company":
|
|
412
420
|
result = await fetchCompany(args?.companyUrl);
|
package/dist/tools/auth.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ export type AuthStatus = {
|
|
|
18
18
|
message: string;
|
|
19
19
|
guidance: string;
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Verbatim instruction for the AGENT (not the user) when auth is missing.
|
|
23
|
+
* The agent MUST follow this verbatim and MUST NOT show error.guidance to
|
|
24
|
+
* the user — that's a non-interactive fallback. This field is the single
|
|
25
|
+
* source of truth for the FTUX magic-link flow so even a stale skill copy
|
|
26
|
+
* does the right thing.
|
|
27
|
+
*/
|
|
28
|
+
agentInstruction?: string;
|
|
21
29
|
};
|
|
22
30
|
export declare const authToolDefinitions: {
|
|
23
31
|
name: string;
|
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. IMPORTANT: When auth succeeds, ALWAYS tell the user the _userNotice message so they know which workspace is active and whether an update is available.",
|
|
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: {},
|
|
@@ -110,6 +110,23 @@ export async function getAuthStatus() {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
catch (error) {
|
|
113
|
+
// FTUX directive: when the user is not yet signed in, tell the AGENT
|
|
114
|
+
// (not the user) to run the magic-link CLI handoff verbatim. Treat this
|
|
115
|
+
// as the single source of truth so even a stale skill does the right
|
|
116
|
+
// thing instead of dumping the technical fallback guidance.
|
|
117
|
+
const ftuxAgentInstruction = "AGENT INSTRUCTION (verbatim, do not paraphrase, do not show the user `error.guidance`): " +
|
|
118
|
+
"The user has not signed in to Sellable yet. Run the magic-link CLI handoff. " +
|
|
119
|
+
"1) Say to the user verbatim: `Welcome to Sellable. What's your email?` " +
|
|
120
|
+
"2) Wait for the user to type their email in normal chat (do NOT use AskUserQuestion / request_user_input). " +
|
|
121
|
+
"3) Call `mcp__sellable__start_cli_login({ email })` with that email. " +
|
|
122
|
+
"4) On `ok: true`, say verbatim (substituting the email exactly as typed): " +
|
|
123
|
+
"`Magic link sent to {email}. Click it from your inbox — I'll wait. " +
|
|
124
|
+
"(If your team already has a Sellable workspace, ask an admin to invite you instead — that gets you straight into their data.)` " +
|
|
125
|
+
"5) Call `mcp__sellable__wait_for_cli_login({ sessionId })` using the sessionId returned by start_cli_login. " +
|
|
126
|
+
"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. " +
|
|
127
|
+
"7) On `ok: true`, the user is signed in and `~/.sellable/config.json` has been written. Branch on `isReturningUser`: " +
|
|
128
|
+
"if true, say `You're in — using your {activeWorkspaceName} workspace.\\n\\nNow — paste the LinkedIn profile URL of the person you want to send from.`; " +
|
|
129
|
+
"if false, say `Welcome to Sellable — created {activeWorkspaceName} for you.\\n\\nNow — paste the LinkedIn profile URL of the person you want to send from.`";
|
|
113
130
|
if (error instanceof SellableApiError && error.isAuthError) {
|
|
114
131
|
return {
|
|
115
132
|
...base,
|
|
@@ -120,6 +137,7 @@ export async function getAuthStatus() {
|
|
|
120
137
|
guidance: error.guidance ||
|
|
121
138
|
`Update ${configPath} with a valid token, then retry get_auth_status.`,
|
|
122
139
|
},
|
|
140
|
+
agentInstruction: ftuxAgentInstruction,
|
|
123
141
|
};
|
|
124
142
|
}
|
|
125
143
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -136,6 +154,7 @@ export async function getAuthStatus() {
|
|
|
136
154
|
message,
|
|
137
155
|
guidance,
|
|
138
156
|
},
|
|
157
|
+
agentInstruction: isConfigError ? ftuxAgentInstruction : undefined,
|
|
139
158
|
};
|
|
140
159
|
}
|
|
141
160
|
}
|
|
@@ -117,6 +117,7 @@ export interface CreateCampaignInput {
|
|
|
117
117
|
campaignId?: string;
|
|
118
118
|
name?: string;
|
|
119
119
|
clientProspectId?: string;
|
|
120
|
+
senderLinkedinUrl?: string;
|
|
120
121
|
offerPositioning?: unknown;
|
|
121
122
|
campaignBrief?: string;
|
|
122
123
|
messageGenerationMode?: "template" | "ai-generated";
|
|
@@ -140,48 +141,6 @@ export interface UpdateCampaignInput {
|
|
|
140
141
|
rubric?: unknown[];
|
|
141
142
|
}
|
|
142
143
|
export declare const campaignToolDefinitions: ({
|
|
143
|
-
name: string;
|
|
144
|
-
description: string;
|
|
145
|
-
inputSchema: {
|
|
146
|
-
type: string;
|
|
147
|
-
properties: {
|
|
148
|
-
linkedinUrl: {
|
|
149
|
-
type: string;
|
|
150
|
-
description: string;
|
|
151
|
-
};
|
|
152
|
-
companyDomain: {
|
|
153
|
-
type: string;
|
|
154
|
-
description: string;
|
|
155
|
-
};
|
|
156
|
-
forceRefresh: {
|
|
157
|
-
type: string;
|
|
158
|
-
description: string;
|
|
159
|
-
};
|
|
160
|
-
limit?: undefined;
|
|
161
|
-
campaignId?: undefined;
|
|
162
|
-
tableId?: undefined;
|
|
163
|
-
leadLimit?: undefined;
|
|
164
|
-
page?: undefined;
|
|
165
|
-
filters?: undefined;
|
|
166
|
-
name?: undefined;
|
|
167
|
-
clientProspectId?: undefined;
|
|
168
|
-
offerPositioning?: undefined;
|
|
169
|
-
campaignBrief?: undefined;
|
|
170
|
-
messageGenerationMode?: undefined;
|
|
171
|
-
currentStep?: undefined;
|
|
172
|
-
leadSourceType?: undefined;
|
|
173
|
-
leadSourceProvider?: undefined;
|
|
174
|
-
selectedLeadListId?: undefined;
|
|
175
|
-
senderIds?: undefined;
|
|
176
|
-
interactionMode?: undefined;
|
|
177
|
-
enableICPFilters?: undefined;
|
|
178
|
-
useMessagingTemplate?: undefined;
|
|
179
|
-
rubric?: undefined;
|
|
180
|
-
};
|
|
181
|
-
required: string[];
|
|
182
|
-
additionalProperties?: undefined;
|
|
183
|
-
};
|
|
184
|
-
} | {
|
|
185
144
|
name: string;
|
|
186
145
|
description: string;
|
|
187
146
|
inputSchema: {
|
|
@@ -191,9 +150,6 @@ export declare const campaignToolDefinitions: ({
|
|
|
191
150
|
type: string;
|
|
192
151
|
description: string;
|
|
193
152
|
};
|
|
194
|
-
linkedinUrl?: undefined;
|
|
195
|
-
companyDomain?: undefined;
|
|
196
|
-
forceRefresh?: undefined;
|
|
197
153
|
campaignId?: undefined;
|
|
198
154
|
tableId?: undefined;
|
|
199
155
|
leadLimit?: undefined;
|
|
@@ -201,6 +157,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
201
157
|
filters?: undefined;
|
|
202
158
|
name?: undefined;
|
|
203
159
|
clientProspectId?: undefined;
|
|
160
|
+
senderLinkedinUrl?: undefined;
|
|
204
161
|
offerPositioning?: undefined;
|
|
205
162
|
campaignBrief?: undefined;
|
|
206
163
|
messageGenerationMode?: undefined;
|
|
@@ -227,9 +184,6 @@ export declare const campaignToolDefinitions: ({
|
|
|
227
184
|
type: string;
|
|
228
185
|
description: string;
|
|
229
186
|
};
|
|
230
|
-
linkedinUrl?: undefined;
|
|
231
|
-
companyDomain?: undefined;
|
|
232
|
-
forceRefresh?: undefined;
|
|
233
187
|
limit?: undefined;
|
|
234
188
|
tableId?: undefined;
|
|
235
189
|
leadLimit?: undefined;
|
|
@@ -237,6 +191,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
237
191
|
filters?: undefined;
|
|
238
192
|
name?: undefined;
|
|
239
193
|
clientProspectId?: undefined;
|
|
194
|
+
senderLinkedinUrl?: undefined;
|
|
240
195
|
offerPositioning?: undefined;
|
|
241
196
|
campaignBrief?: undefined;
|
|
242
197
|
messageGenerationMode?: undefined;
|
|
@@ -310,12 +265,10 @@ export declare const campaignToolDefinitions: ({
|
|
|
310
265
|
additionalProperties: boolean;
|
|
311
266
|
};
|
|
312
267
|
};
|
|
313
|
-
linkedinUrl?: undefined;
|
|
314
|
-
companyDomain?: undefined;
|
|
315
|
-
forceRefresh?: undefined;
|
|
316
268
|
limit?: undefined;
|
|
317
269
|
name?: undefined;
|
|
318
270
|
clientProspectId?: undefined;
|
|
271
|
+
senderLinkedinUrl?: undefined;
|
|
319
272
|
offerPositioning?: undefined;
|
|
320
273
|
campaignBrief?: undefined;
|
|
321
274
|
messageGenerationMode?: undefined;
|
|
@@ -350,6 +303,10 @@ export declare const campaignToolDefinitions: ({
|
|
|
350
303
|
type: string;
|
|
351
304
|
description: string;
|
|
352
305
|
};
|
|
306
|
+
senderLinkedinUrl: {
|
|
307
|
+
type: string;
|
|
308
|
+
description: string;
|
|
309
|
+
};
|
|
353
310
|
offerPositioning: {
|
|
354
311
|
type: string;
|
|
355
312
|
description: string;
|
|
@@ -387,9 +344,6 @@ export declare const campaignToolDefinitions: ({
|
|
|
387
344
|
};
|
|
388
345
|
description: string;
|
|
389
346
|
};
|
|
390
|
-
linkedinUrl?: undefined;
|
|
391
|
-
companyDomain?: undefined;
|
|
392
|
-
forceRefresh?: undefined;
|
|
393
347
|
limit?: undefined;
|
|
394
348
|
tableId?: undefined;
|
|
395
349
|
leadLimit?: undefined;
|
|
@@ -462,9 +416,6 @@ export declare const campaignToolDefinitions: ({
|
|
|
462
416
|
type: string;
|
|
463
417
|
description: string;
|
|
464
418
|
};
|
|
465
|
-
linkedinUrl?: undefined;
|
|
466
|
-
companyDomain?: undefined;
|
|
467
|
-
forceRefresh?: undefined;
|
|
468
419
|
limit?: undefined;
|
|
469
420
|
tableId?: undefined;
|
|
470
421
|
leadLimit?: undefined;
|
|
@@ -472,6 +423,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
472
423
|
filters?: undefined;
|
|
473
424
|
name?: undefined;
|
|
474
425
|
clientProspectId?: undefined;
|
|
426
|
+
senderLinkedinUrl?: undefined;
|
|
475
427
|
messageGenerationMode?: undefined;
|
|
476
428
|
};
|
|
477
429
|
required: string[];
|
|
@@ -491,9 +443,6 @@ export declare const campaignToolDefinitions: ({
|
|
|
491
443
|
type: string;
|
|
492
444
|
description: string;
|
|
493
445
|
};
|
|
494
|
-
linkedinUrl?: undefined;
|
|
495
|
-
companyDomain?: undefined;
|
|
496
|
-
forceRefresh?: undefined;
|
|
497
446
|
limit?: undefined;
|
|
498
447
|
tableId?: undefined;
|
|
499
448
|
leadLimit?: undefined;
|
|
@@ -501,6 +450,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
501
450
|
filters?: undefined;
|
|
502
451
|
name?: undefined;
|
|
503
452
|
clientProspectId?: undefined;
|
|
453
|
+
senderLinkedinUrl?: undefined;
|
|
504
454
|
offerPositioning?: undefined;
|
|
505
455
|
messageGenerationMode?: undefined;
|
|
506
456
|
currentStep?: undefined;
|
|
@@ -549,41 +499,4 @@ export declare function updateCampaignBrief(campaignId: string, campaignBrief: s
|
|
|
549
499
|
success: boolean;
|
|
550
500
|
campaignBrief: string;
|
|
551
501
|
}>;
|
|
552
|
-
export interface EnrichSenderResult {
|
|
553
|
-
clientProspectId: string;
|
|
554
|
-
sender: {
|
|
555
|
-
fullName: string;
|
|
556
|
-
headline?: string;
|
|
557
|
-
title?: string;
|
|
558
|
-
company?: string;
|
|
559
|
-
};
|
|
560
|
-
companyDomain: string;
|
|
561
|
-
enrichmentStatus: "partial" | "complete";
|
|
562
|
-
companySnapshot?: {
|
|
563
|
-
name: string;
|
|
564
|
-
domain: string;
|
|
565
|
-
industry?: string;
|
|
566
|
-
employeeRange?: string;
|
|
567
|
-
description?: string;
|
|
568
|
-
linkedinUrl?: string;
|
|
569
|
-
} | null;
|
|
570
|
-
senderBackground?: {
|
|
571
|
-
experience: {
|
|
572
|
-
title: string;
|
|
573
|
-
company: string;
|
|
574
|
-
duration?: string;
|
|
575
|
-
}[];
|
|
576
|
-
education?: string;
|
|
577
|
-
followerCount?: number;
|
|
578
|
-
connectionCount?: number;
|
|
579
|
-
} | null;
|
|
580
|
-
proofDigest?: {
|
|
581
|
-
caseStudyCount: number;
|
|
582
|
-
caseStudySummary: string | null;
|
|
583
|
-
reviewHighlight: string | null;
|
|
584
|
-
positioningOneLiner: string | null;
|
|
585
|
-
keyDifferentiators: string[];
|
|
586
|
-
} | null;
|
|
587
|
-
}
|
|
588
|
-
export declare function enrichSender(linkedinUrl: string, companyDomain?: string, forceRefresh?: boolean): Promise<EnrichSenderResult>;
|
|
589
502
|
export {};
|
package/dist/tools/campaigns.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getApi } from "../api.js";
|
|
2
2
|
import { getConfig } from "../auth.js";
|
|
3
|
-
import { assertCreateCampaignPromptLoaded, assertNetNewCreateCampaignResearchReady,
|
|
3
|
+
import { assertCreateCampaignPromptLoaded, assertNetNewCreateCampaignResearchReady, } from "./flow-preflight.js";
|
|
4
4
|
import { setCampaignInteractionMode, } from "./interaction-mode.js";
|
|
5
5
|
import { fetchCampaignRubrics } from "./processing.js";
|
|
6
6
|
const LEAD_SOURCE_PROVIDERS = {
|
|
@@ -27,28 +27,6 @@ function buildWatchUrl(config, redirect) {
|
|
|
27
27
|
return `${config.apiUrl}/auth/continue?token=${config.token}&redirect=${redirect}${workspaceParam}`;
|
|
28
28
|
}
|
|
29
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
30
|
{
|
|
53
31
|
name: "get_campaigns",
|
|
54
32
|
description: "List campaigns for the authenticated user. Returns id, name, createdAt. Ordered by most recent first.",
|
|
@@ -145,7 +123,7 @@ export const campaignToolDefinitions = [
|
|
|
145
123
|
},
|
|
146
124
|
{
|
|
147
125
|
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.',
|
|
126
|
+
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.\n\nINPUTS:\n- `clientProspectId` is REQUIRED for net-new campaigns. It is the EnrichedProspect row ID for the campaign sender (used by ICP scoring, message generation, brief rendering — every downstream consumer reads sender context from `campaign.clientProspect`).\n- To obtain a `clientProspectId`, run the sender-enrichment flow first: this materializes an EnrichedProspect row from the sender\'s LinkedIn profile URL and returns its ID.\n- `senderLinkedinUrl` may be passed as informational metadata but is NOT a substitute for `clientProspectId` — the API will reject the create if `clientProspectId` is missing.\n\nPREREQUISITES:\n- The /research-sender skill must have been run for this sender (produces enrichment data and the EnrichedProspect row whose ID is `clientProspectId`).',
|
|
149
127
|
inputSchema: {
|
|
150
128
|
type: "object",
|
|
151
129
|
properties: {
|
|
@@ -159,7 +137,13 @@ export const campaignToolDefinitions = [
|
|
|
159
137
|
},
|
|
160
138
|
clientProspectId: {
|
|
161
139
|
type: "string",
|
|
162
|
-
description: "
|
|
140
|
+
description: "REQUIRED for net-new campaigns. EnrichedProspect row ID for the campaign sender. Produced by the sender-enrichment flow (research-sender). The API will reject the create with a 400 if this is missing.",
|
|
141
|
+
},
|
|
142
|
+
// `senderLinkedinUrl` (not `linkedinUrl`) to disambiguate from
|
|
143
|
+
// prospect-side `linkedinUrl` fields used in other tools (research-prospect etc.).
|
|
144
|
+
senderLinkedinUrl: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "Optional informational metadata: the sender's LinkedIn profile URL. NOT a substitute for clientProspectId — to materialize a prospect from a URL, run the sender-enrichment flow first.",
|
|
163
147
|
},
|
|
164
148
|
offerPositioning: {
|
|
165
149
|
type: "object",
|
|
@@ -611,21 +595,34 @@ export async function createCampaign(input) {
|
|
|
611
595
|
const missing = [];
|
|
612
596
|
if (!input.name)
|
|
613
597
|
missing.push("name");
|
|
614
|
-
if (!input.clientProspectId)
|
|
615
|
-
missing.push("clientProspectId");
|
|
616
598
|
if (!input.campaignBrief)
|
|
617
599
|
missing.push("campaignBrief");
|
|
600
|
+
// clientProspectId is required for net-new campaigns. Every downstream
|
|
601
|
+
// consumer (ICP scoring, message generation, brief rendering) reads sender
|
|
602
|
+
// context from `campaign.clientProspect`. senderLinkedinUrl is informational
|
|
603
|
+
// metadata only — it is NOT a substitute and the API will reject the create
|
|
604
|
+
// if clientProspectId is missing.
|
|
605
|
+
if (!input.clientProspectId) {
|
|
606
|
+
missing.push("clientProspectId");
|
|
607
|
+
}
|
|
608
|
+
// Cheap URL sanity check on senderLinkedinUrl when supplied.
|
|
609
|
+
if (input.senderLinkedinUrl &&
|
|
610
|
+
typeof input.senderLinkedinUrl === "string" &&
|
|
611
|
+
!input.senderLinkedinUrl.includes("linkedin.com")) {
|
|
612
|
+
throw new Error("VALIDATION_ERROR: senderLinkedinUrl must be a LinkedIn URL (must contain 'linkedin.com'). Got: " +
|
|
613
|
+
input.senderLinkedinUrl);
|
|
614
|
+
}
|
|
618
615
|
if (missing.length > 0) {
|
|
619
616
|
throw new Error("VALIDATION_ERROR: create_campaign requires either campaignId (resume) " +
|
|
620
617
|
`or all create fields. Missing: ${missing.join(", ")}.\n\n` +
|
|
621
618
|
"Remediation:\n" +
|
|
622
619
|
'- For full workflow, call get_subskill_prompt({ subskillName: "create-campaign" }) and follow it.\n' +
|
|
623
|
-
|
|
620
|
+
"- For net-new campaign creation: run the `research-sender` flow first to materialize an EnrichedProspect for the sender from their LinkedIn profile URL, then pass the resulting prospect ID as `clientProspectId`. The campaign sender context (used by ICP scoring, message generation, brief rendering) is read from `campaign.clientProspect`, so this row must exist before the campaign is minted.\n" +
|
|
624
621
|
"- For resume, call create_campaign with campaignId only.");
|
|
625
622
|
}
|
|
626
623
|
assertNetNewCreateCampaignResearchReady();
|
|
627
624
|
const name = input.name;
|
|
628
|
-
const clientProspectId = input.clientProspectId;
|
|
625
|
+
const clientProspectId = input.clientProspectId ?? null;
|
|
629
626
|
const campaignBrief = input.campaignBrief;
|
|
630
627
|
// Validate required campaignBrief - must be non-empty markdown
|
|
631
628
|
// Normalize escaped newlines - MCP tool calls pass literal "\n" text (backslash + n)
|
|
@@ -680,6 +677,9 @@ export async function createCampaign(input) {
|
|
|
680
677
|
...apiInput,
|
|
681
678
|
name,
|
|
682
679
|
clientProspectId,
|
|
680
|
+
// Forward senderLinkedinUrl alongside clientProspectId. Downstream API may
|
|
681
|
+
// ignore it for now; future phase wires it.
|
|
682
|
+
senderLinkedinUrl: input.senderLinkedinUrl ?? null,
|
|
683
683
|
offerPositioning,
|
|
684
684
|
...(input.leadSourceProvider !== undefined
|
|
685
685
|
? { leadSourceProvider: normalizedLeadSourceProvider }
|
|
@@ -769,124 +769,3 @@ export async function updateCampaignBrief(campaignId, campaignBrief) {
|
|
|
769
769
|
const api = getApi();
|
|
770
770
|
return api.patch(`/api/v3/mcp/campaigns/${campaignId}`, { campaignBrief });
|
|
771
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
|
-
}
|