@sellable/mcp 0.1.30 → 0.1.32

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.
@@ -0,0 +1,207 @@
1
+ import { writeNewConfig } from "../auth.js";
2
+ /**
3
+ * Sellable CLI login tools.
4
+ *
5
+ * Two-phase magic-link handoff used by the create-campaign skill when a
6
+ * brand-new user has no `~/.sellable/config.json` yet:
7
+ *
8
+ * 1. start_cli_login({ email })
9
+ * → POSTs to /api/v3/cli-session, server emails a magic link.
10
+ *
11
+ * 2. wait_for_cli_login({ sessionId })
12
+ * → Polls /api/v3/cli-session/[id]/poll every 2s until the user clicks
13
+ * the link, completes browser auth, and the server transitions the
14
+ * session to status="ready". Persists the resulting token via
15
+ * writeNewConfig().
16
+ *
17
+ * Constants:
18
+ * POLL_INTERVAL_MS — 2s between polls
19
+ * DEFAULT_TIMEOUT_MS — 5min outer session budget (matches CliSession TTL)
20
+ * TOOL_CALL_TIMEOUT_GUARD — 55s, mirrors mcp/sellable/src/tools/readiness.ts:31.
21
+ * MCP hosts (esp. Claude Code) cap individual tool
22
+ * calls at ~60s; without this guard the host kills
23
+ * the loop before our outer 5min budget elapses.
24
+ * Agent re-invokes wait_for_cli_login with the SAME
25
+ * sessionId on tool_timeout_guard until the outer
26
+ * budget runs out or sign-in completes.
27
+ *
28
+ * Security:
29
+ * No tool here logs `confirmUrl`, the plaintext token, or `sessionId`.
30
+ * See Phase 116 RESEARCH.md Pitfall 6.
31
+ */
32
+ const POLL_INTERVAL_MS = 2_000;
33
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
34
+ const TOOL_CALL_TIMEOUT_GUARD_MS = 55_000;
35
+ const MAX_NETWORK_RETRIES = 3;
36
+ // Exposed for tests so the 55s guard doesn't cause real-time waits.
37
+ export const __TEST_INTERNALS__ = {
38
+ POLL_INTERVAL_MS,
39
+ DEFAULT_TIMEOUT_MS,
40
+ TOOL_CALL_TIMEOUT_GUARD_MS,
41
+ MAX_NETWORK_RETRIES,
42
+ };
43
+ function errorPayload(type, message, guidance) {
44
+ return { ok: false, error: { type, message, guidance } };
45
+ }
46
+ function sleep(ms) {
47
+ return new Promise((resolve) => setTimeout(resolve, ms));
48
+ }
49
+ /**
50
+ * Resolve API base URL for the un-authed handoff. Matches the resolution chain
51
+ * documented in Plan 09 UAT step 1: env override > production default. We do
52
+ * NOT auto-read a partial ~/.sellable/config.json here — that's an
53
+ * over-engineering trap for a single edge case fully addressed by an env-var
54
+ * doc.
55
+ */
56
+ function resolveApiUrl() {
57
+ return (process.env.SELLABLE_API_URL ??
58
+ process.env.SELLABLE_BASE_URL ??
59
+ "https://app.sellable.dev");
60
+ }
61
+ export const startCliLoginToolDef = {
62
+ name: "start_cli_login",
63
+ description: "Start the CLI login handoff. Triggers a magic-link email to the supplied email address. " +
64
+ "Returns the confirm URL the user will be redirected to after clicking the link. " +
65
+ "Pair with wait_for_cli_login to block until the user finishes browser sign-in.",
66
+ inputSchema: {
67
+ type: "object",
68
+ properties: {
69
+ email: {
70
+ type: "string",
71
+ description: "User's email — magic link is sent here.",
72
+ },
73
+ },
74
+ required: ["email"],
75
+ additionalProperties: false,
76
+ },
77
+ };
78
+ export const waitForCliLoginToolDef = {
79
+ name: "wait_for_cli_login",
80
+ description: "Poll the CLI login session every 2s until the user finishes browser sign-in or 5 minutes elapse. " +
81
+ "On success, writes ~/.sellable/config.json so subsequent MCP calls are authenticated. " +
82
+ "Re-call with the SAME sessionId on tool_timeout_guard — do NOT call start_cli_login again.",
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {
86
+ sessionId: {
87
+ type: "string",
88
+ description: "Session ID from start_cli_login.",
89
+ },
90
+ timeoutMs: {
91
+ type: "number",
92
+ description: "Outer session budget (ms). Default 300000 (5min). Clamped at 5min.",
93
+ },
94
+ },
95
+ required: ["sessionId"],
96
+ additionalProperties: false,
97
+ },
98
+ };
99
+ export async function handleStartCliLogin(args) {
100
+ const apiUrl = resolveApiUrl();
101
+ let res;
102
+ try {
103
+ res = await fetch(`${apiUrl}/api/v3/cli-session`, {
104
+ method: "POST",
105
+ headers: { "Content-Type": "application/json" },
106
+ body: JSON.stringify({ email: args.email }),
107
+ });
108
+ }
109
+ catch (err) {
110
+ const message = err instanceof Error ? err.message : String(err);
111
+ return errorPayload("network_error", "Could not reach Sellable to start CLI login.", `Check your internet connection, then retry. Underlying error: ${message.slice(0, 200)}`);
112
+ }
113
+ if (res.status === 429) {
114
+ return errorPayload("rate_limited", "Too many login attempts in the last minute. Wait 60s and try again.", "Magic-link signups are rate-limited per IP to 10 per minute. This is normal during testing.");
115
+ }
116
+ if (!res.ok) {
117
+ let detail = "";
118
+ try {
119
+ detail = await res.text();
120
+ }
121
+ catch {
122
+ // ignore
123
+ }
124
+ return errorPayload("send_failed", "Could not start CLI login.", detail.slice(0, 500) ||
125
+ `Server returned ${res.status}. Try again or contact support.`);
126
+ }
127
+ const body = (await res.json());
128
+ // SECURITY: do NOT log body.confirmUrl, body.sessionId, or any token.
129
+ return {
130
+ ok: true,
131
+ sessionId: body.sessionId,
132
+ confirmUrl: body.confirmUrl,
133
+ expiresInSec: body.expiresInSec,
134
+ humanMessage: `Magic link sent to ${args.email}. Click it from your inbox — I'll wait. (If your team already has a Sellable workspace, ask an admin to invite you instead — that gets you straight into their data.)`,
135
+ };
136
+ }
137
+ export async function handleWaitForCliLogin(args) {
138
+ const timeoutMs = Math.min(args.timeoutMs ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
139
+ const toolGuardMs = args.toolGuardMs ?? TOOL_CALL_TIMEOUT_GUARD_MS;
140
+ const pollIntervalMs = args.pollIntervalMs ?? POLL_INTERVAL_MS;
141
+ const apiUrl = resolveApiUrl();
142
+ const startedAt = Date.now();
143
+ const work = (async () => {
144
+ let consecutiveNetworkFailures = 0;
145
+ while (Date.now() - startedAt < timeoutMs) {
146
+ let res;
147
+ try {
148
+ res = await fetch(`${apiUrl}/api/v3/cli-session/${args.sessionId}/poll`);
149
+ consecutiveNetworkFailures = 0;
150
+ }
151
+ catch (err) {
152
+ consecutiveNetworkFailures += 1;
153
+ if (consecutiveNetworkFailures >= MAX_NETWORK_RETRIES) {
154
+ const message = err instanceof Error ? err.message : String(err);
155
+ return errorPayload("network_error", "Lost connection to Sellable while waiting for sign-in.", `Tried ${MAX_NETWORK_RETRIES} times. Check your internet and retry. Underlying error: ${message.slice(0, 200)}`);
156
+ }
157
+ await sleep(pollIntervalMs);
158
+ continue;
159
+ }
160
+ if (res.status === 410) {
161
+ return errorPayload("expired", "That magic link expired.", "Run /sellable:create-campaign again to retry.");
162
+ }
163
+ if (!res.ok) {
164
+ // Transient — back off and retry
165
+ await sleep(pollIntervalMs);
166
+ continue;
167
+ }
168
+ let body;
169
+ try {
170
+ body = (await res.json());
171
+ }
172
+ catch {
173
+ await sleep(pollIntervalMs);
174
+ continue;
175
+ }
176
+ if (body.status === "ready") {
177
+ const { configPath } = writeNewConfig({
178
+ token: body.token,
179
+ activeWorkspaceId: body.activeWorkspaceId,
180
+ activeWorkspaceName: body.activeWorkspaceName,
181
+ apiUrl: body.apiUrl,
182
+ });
183
+ return {
184
+ ok: true,
185
+ activeWorkspaceId: body.activeWorkspaceId,
186
+ ...(body.activeWorkspaceName
187
+ ? { activeWorkspaceName: body.activeWorkspaceName }
188
+ : {}),
189
+ configPath,
190
+ humanMessage: "Signed in. Continuing.",
191
+ };
192
+ }
193
+ if (body.status === "claimed") {
194
+ return errorPayload("already_consumed", "Login already completed in another window.", "Your terminal config has been written; try the next step.");
195
+ }
196
+ // status === 'pending' — keep polling
197
+ await sleep(pollIntervalMs);
198
+ }
199
+ return errorPayload("timeout", "Login timed out after 5 minutes.", "Run /sellable:create-campaign again, or paste the manual fallback shown in the browser.");
200
+ })();
201
+ const guard = new Promise((resolve) => {
202
+ setTimeout(() => {
203
+ resolve(errorPayload("tool_timeout_guard", "Tool call hit the 55s host-cap guard.", "Re-call wait_for_cli_login with the SAME sessionId — the underlying CliSession is still pending; this does NOT create a new session."));
204
+ }, toolGuardMs).unref?.();
205
+ });
206
+ return Promise.race([work, guard]);
207
+ }
@@ -13,7 +13,7 @@ type SenderEnrichmentSnapshot = {
13
13
  hasCaseStudies: boolean;
14
14
  hasProof: boolean;
15
15
  };
16
- type SenderResearchDepth = "minimal-verification" | "deep-proof";
16
+ type SenderResearchDepth = "minimal-verification" | "deep-proof" | "parallel-batch";
17
17
  export declare function markCreateCampaignPromptLoaded(): CreateCampaignPromptPreflightState;
18
18
  export declare function markResearchPromptLoaded(mode?: ResearchPromptMode, subskillName?: string): ResearchPromptPreflightState;
19
19
  export declare function markSenderEnrichmentObserved(input: {
@@ -48,21 +48,8 @@ export declare function assertResearchPromptLoaded(mode?: ResearchPromptMode | "
48
48
  };
49
49
  export declare function assertNetNewCreateCampaignResearchReady(): {
50
50
  readonly ok: true;
51
- readonly enrichmentStatus: "partial" | "complete";
52
- readonly hasCaseStudies: boolean;
53
- readonly hasProof: boolean;
54
- readonly enrichmentLooksComplete: boolean;
55
- readonly usedResearchPrompt: boolean;
56
- readonly researchPromptMode: ResearchPromptMode;
57
- readonly researchPromptSubskillName: string;
58
- readonly researchCompletion: {
59
- readonly depth: SenderResearchDepth;
60
- readonly proofItemsFound: number;
61
- readonly caseStudyItemsFound: number;
62
- readonly credibilitySignalsFound: number;
63
- readonly completedAt: string;
64
- };
65
- readonly needsResearch: true;
51
+ readonly senderResearchPromptLoadedAt: string;
52
+ readonly senderResearchCompletedAt: string;
66
53
  };
67
54
  export declare function resetFlowPreflightState(): void;
68
55
  export {};
@@ -93,40 +93,21 @@ export function assertResearchPromptLoaded(mode = "any") {
93
93
  throw new Error("FLOW_PRECONDITION: research preload missing. Call get_subskill_prompt with the matching research wrapper (research-sender or research-prospect), then run research before continuing.");
94
94
  }
95
95
  export function assertNetNewCreateCampaignResearchReady() {
96
- if (!senderEnrichmentSnapshot) {
97
- throw new Error("FLOW_PRECONDITION: net-new create_campaign requires sender enrichment context. Call enrich_sender first.");
98
- }
99
- const enrichmentLooksComplete = senderEnrichmentSnapshot.enrichmentStatus === "complete" &&
100
- senderEnrichmentSnapshot.hasCaseStudies &&
101
- senderEnrichmentSnapshot.hasProof;
102
96
  const senderResearchPromptState = researchPromptStates.sender;
103
97
  if (!senderResearchPromptState) {
104
- throw new Error('FLOW_PRECONDITION: net-new create_campaign requires sender research after enrich_sender. Call get_subskill_prompt({ subskillName: "research-sender" }) and run sender research before create_campaign.');
98
+ throw new Error('FLOW_PRECONDITION: net-new create_campaign requires sender research preload. Call get_subskill_prompt({ subskillName: "research-sender" }) and run sender research before create_campaign.');
105
99
  }
106
100
  if (!senderResearchCompletionState) {
107
- throw new Error("FLOW_PRECONDITION: completed sender research is required before create_campaign. After running research-sender, call complete_sender_research({ depth, proofItemsFound, caseStudyItemsFound, credibilitySignalsFound, notes? }).");
101
+ throw new Error("FLOW_PRECONDITION: completed sender research is required before create_campaign. After running research-sender (parallel batch of fetch_linkedin_profile + fetch_company + WebSearches), call complete_sender_research({ depth, proofItemsFound, caseStudyItemsFound, credibilitySignalsFound, notes? }).");
108
102
  }
109
103
  if (new Date(senderResearchCompletionState.completedAt).getTime() <
110
- new Date(senderEnrichmentSnapshot.observedAt).getTime()) {
111
- throw new Error("FLOW_PRECONDITION: sender research completion is stale for the latest enrichment. Rerun research-sender after enrich_sender, then call complete_sender_research before create_campaign.");
104
+ new Date(senderResearchPromptState.loadedAt).getTime()) {
105
+ throw new Error("FLOW_PRECONDITION: sender research completion is older than the latest research-sender prompt load. Re-run sender research and call complete_sender_research again.");
112
106
  }
113
107
  return {
114
108
  ok: true,
115
- enrichmentStatus: senderEnrichmentSnapshot.enrichmentStatus,
116
- hasCaseStudies: senderEnrichmentSnapshot.hasCaseStudies,
117
- hasProof: senderEnrichmentSnapshot.hasProof,
118
- enrichmentLooksComplete,
119
- usedResearchPrompt: Boolean(senderResearchPromptState),
120
- researchPromptMode: senderResearchPromptState?.mode ?? null,
121
- researchPromptSubskillName: senderResearchPromptState?.subskillName ?? null,
122
- researchCompletion: {
123
- depth: senderResearchCompletionState.depth,
124
- proofItemsFound: senderResearchCompletionState.proofItemsFound,
125
- caseStudyItemsFound: senderResearchCompletionState.caseStudyItemsFound,
126
- credibilitySignalsFound: senderResearchCompletionState.credibilitySignalsFound,
127
- completedAt: senderResearchCompletionState.completedAt,
128
- },
129
- needsResearch: true,
109
+ senderResearchPromptLoadedAt: senderResearchPromptState.loadedAt,
110
+ senderResearchCompletedAt: senderResearchCompletionState.completedAt,
130
111
  };
131
112
  }
132
113
  export function resetFlowPreflightState() {
@@ -138,8 +138,8 @@ function checkCampaignCreated(campaign) {
138
138
  const missing = [];
139
139
  if (!campaign.id)
140
140
  missing.push("campaignId");
141
- if (!campaign.clientProspectId)
142
- missing.push("clientProspectId");
141
+ // Phase 114: clientProspectId is optional. A persisted campaign without
142
+ // clientProspectId is a valid Phase-114 net-new campaign.
143
143
  if (!getBriefContent(campaign.campaignBrief).trim()) {
144
144
  missing.push("campaignBrief.content");
145
145
  }
@@ -1,7 +1,8 @@
1
1
  export type CreateOnDemandTableInput = {
2
2
  name: string;
3
3
  senderIds?: string[];
4
- clientProspectId: string;
4
+ clientProspectId?: string;
5
+ senderLinkedinUrl?: string;
5
6
  offerPositioning?: Record<string, unknown>;
6
7
  campaignBrief?: unknown;
7
8
  };
@@ -26,7 +27,8 @@ export type InitOnDemandSequenceInput = {
26
27
  };
27
28
  export type CreateOnDemandCampaignInput = {
28
29
  name: string;
29
- clientProspectId: string;
30
+ clientProspectId?: string;
31
+ senderLinkedinUrl?: string;
30
32
  senderIds: string[];
31
33
  campaignBrief?: unknown;
32
34
  offerPositioning?: Record<string, unknown>;
@@ -54,6 +56,10 @@ export declare const onDemandToolDefinitions: ({
54
56
  type: string;
55
57
  description: string;
56
58
  };
59
+ senderLinkedinUrl: {
60
+ type: string;
61
+ description: string;
62
+ };
57
63
  offerPositioning: {
58
64
  type: string;
59
65
  description: string;
@@ -84,6 +90,10 @@ export declare const onDemandToolDefinitions: ({
84
90
  type: string;
85
91
  description: string;
86
92
  };
93
+ senderLinkedinUrl: {
94
+ type: string;
95
+ description: string;
96
+ };
87
97
  senderIds: {
88
98
  type: string;
89
99
  items: {
@@ -155,6 +165,7 @@ export declare const onDemandToolDefinitions: ({
155
165
  name?: undefined;
156
166
  senderIds?: undefined;
157
167
  clientProspectId?: undefined;
168
+ senderLinkedinUrl?: undefined;
158
169
  offerPositioning?: undefined;
159
170
  campaignBrief?: undefined;
160
171
  sequenceTemplate?: undefined;
@@ -176,6 +187,7 @@ export declare const onDemandToolDefinitions: ({
176
187
  name?: undefined;
177
188
  senderIds?: undefined;
178
189
  clientProspectId?: undefined;
190
+ senderLinkedinUrl?: undefined;
179
191
  offerPositioning?: undefined;
180
192
  campaignBrief?: undefined;
181
193
  sequenceTemplate?: undefined;
@@ -207,6 +219,7 @@ export declare const onDemandToolDefinitions: ({
207
219
  name?: undefined;
208
220
  senderIds?: undefined;
209
221
  clientProspectId?: undefined;
222
+ senderLinkedinUrl?: undefined;
210
223
  offerPositioning?: undefined;
211
224
  campaignBrief?: undefined;
212
225
  sequenceTemplate?: undefined;
@@ -38,7 +38,8 @@ const SIMPLE_LINEAR_TEMPLATE = {
38
38
  export const onDemandToolDefinitions = [
39
39
  {
40
40
  name: "create_on_demand_table",
41
- description: "Create an on-demand campaign table backed by a lightweight CampaignOffer. Returns tableId + campaignOfferId.",
41
+ description: "Create an on-demand campaign table backed by a lightweight CampaignOffer. Returns tableId + campaignOfferId.\n\n" +
42
+ "INPUTS: Pass EITHER clientProspectId (existing prospect ID) OR senderLinkedinUrl (sender's LinkedIn profile URL). One is required.",
42
43
  inputSchema: {
43
44
  type: "object",
44
45
  properties: {
@@ -53,7 +54,11 @@ export const onDemandToolDefinitions = [
53
54
  },
54
55
  clientProspectId: {
55
56
  type: "string",
56
- description: "Client prospect ID for CampaignOffer (use enrich_sender to obtain it)",
57
+ description: "Optional. Pass this OR senderLinkedinUrl.",
58
+ },
59
+ senderLinkedinUrl: {
60
+ type: "string",
61
+ description: "Optional. Sender's LinkedIn profile URL — used to lazy-resolve the prospect at send-time when clientProspectId is not yet available.",
57
62
  },
58
63
  offerPositioning: {
59
64
  type: "object",
@@ -63,12 +68,13 @@ export const onDemandToolDefinitions = [
63
68
  description: "Optional campaign brief content (markdown or JSON)",
64
69
  },
65
70
  },
66
- required: ["name", "clientProspectId"],
71
+ required: ["name"],
67
72
  },
68
73
  },
69
74
  {
70
75
  name: "create_on_demand_campaign",
71
- description: "Create a lightweight CampaignOffer + workflow table, set senders, and initialize sequence columns. Auto-selects template by sender tiers unless provided.",
76
+ description: "Create a lightweight CampaignOffer + workflow table, set senders, and initialize sequence columns. Auto-selects template by sender tiers unless provided.\n\n" +
77
+ "INPUTS: Pass EITHER clientProspectId (existing prospect ID) OR senderLinkedinUrl (sender's LinkedIn profile URL). One is required.",
72
78
  inputSchema: {
73
79
  type: "object",
74
80
  properties: {
@@ -78,7 +84,11 @@ export const onDemandToolDefinitions = [
78
84
  },
79
85
  clientProspectId: {
80
86
  type: "string",
81
- description: "Client prospect ID from enrich_sender",
87
+ description: "Optional. Pass this OR senderLinkedinUrl.",
88
+ },
89
+ senderLinkedinUrl: {
90
+ type: "string",
91
+ description: "Optional. Sender's LinkedIn profile URL — used to lazy-resolve the prospect at send-time when clientProspectId is not yet available.",
82
92
  },
83
93
  senderIds: {
84
94
  type: "array",
@@ -101,7 +111,7 @@ export const onDemandToolDefinitions = [
101
111
  description: "Set true to overwrite existing sequence columns",
102
112
  },
103
113
  },
104
- required: ["name", "clientProspectId", "senderIds"],
114
+ required: ["name", "senderIds"],
105
115
  },
106
116
  },
107
117
  {
@@ -180,6 +190,9 @@ export const onDemandToolDefinitions = [
180
190
  },
181
191
  ];
182
192
  export async function createOnDemandTable(input) {
193
+ if (!input.clientProspectId && !input.senderLinkedinUrl) {
194
+ throw new Error("VALIDATION_ERROR: create_on_demand_table requires either clientProspectId (existing prospect ID) or senderLinkedinUrl (sender's LinkedIn profile URL). Pass one.");
195
+ }
183
196
  const api = getApi();
184
197
  return api.post("/api/v3/on-demand-campaigns", input);
185
198
  }
@@ -221,6 +234,9 @@ async function initCampaignSequence(input) {
221
234
  return api.put(`/api/v3/campaigns/${input.campaignOfferId}/sequence`, payload);
222
235
  }
223
236
  export async function createOnDemandCampaign(input) {
237
+ if (!input.clientProspectId && !input.senderLinkedinUrl) {
238
+ throw new Error("VALIDATION_ERROR: create_on_demand_campaign requires either clientProspectId (existing prospect ID) or senderLinkedinUrl (sender's LinkedIn profile URL). Pass one.");
239
+ }
224
240
  if (!Array.isArray(input.senderIds) || input.senderIds.length === 0) {
225
241
  throw new Error("senderIds must be a non-empty array");
226
242
  }
@@ -228,6 +244,7 @@ export async function createOnDemandCampaign(input) {
228
244
  name: input.name,
229
245
  senderIds: input.senderIds,
230
246
  clientProspectId: input.clientProspectId,
247
+ senderLinkedinUrl: input.senderLinkedinUrl,
231
248
  offerPositioning: input.offerPositioning,
232
249
  campaignBrief: input.campaignBrief,
233
250
  });
@@ -41,7 +41,7 @@ export interface SearchSubskillPromptsResponse {
41
41
  }>;
42
42
  }
43
43
  export interface CompleteSenderResearchInput {
44
- depth?: "minimal-verification" | "deep-proof";
44
+ depth?: "minimal-verification" | "deep-proof" | "parallel-batch";
45
45
  proofItemsFound?: number;
46
46
  caseStudyItemsFound?: number;
47
47
  credibilitySignalsFound?: number;
@@ -213,7 +213,7 @@ export declare function listSubskillPrompts(limit?: number, includePublic?: bool
213
213
  export declare function getSubskillPrompt(subskillName: string, offset?: number, limit?: number): SubskillPromptResponse;
214
214
  export declare function completeSenderResearch(input?: CompleteSenderResearchInput): {
215
215
  readonly completedAt: string;
216
- readonly depth: "minimal-verification" | "deep-proof";
216
+ readonly depth: "minimal-verification" | "deep-proof" | "parallel-batch";
217
217
  readonly proofItemsFound: number;
218
218
  readonly caseStudyItemsFound: number;
219
219
  readonly credibilitySignalsFound: number;
@@ -97,8 +97,8 @@ export const promptToolDefinitions = [
97
97
  properties: {
98
98
  depth: {
99
99
  type: "string",
100
- enum: ["minimal-verification", "deep-proof"],
101
- description: "Research depth used: minimal verification or deeper proof-focused pass.",
100
+ enum: ["minimal-verification", "deep-proof", "parallel-batch"],
101
+ description: "Research depth used: minimal-verification (proofDigest already had strong signals), deep-proof (extra search agents spawned), or parallel-batch (Phase 114+ default — single 5-call parallel batch with fetch_linkedin_profile + fetch_company + 3x WebSearch).",
102
102
  },
103
103
  proofItemsFound: {
104
104
  type: "number",
@@ -20,6 +20,7 @@ export declare function resolveWaitTimeout(timeoutMs?: number): {
20
20
  effectiveTimeoutMs: number;
21
21
  guardApplied: boolean;
22
22
  };
23
+ export declare function hasNonWaitableMissing(missing: string[]): boolean;
23
24
  export declare const readinessToolDefinitions: ({
24
25
  name: string;
25
26
  description: string;
@@ -32,9 +32,11 @@ export function resolveWaitTimeout(timeoutMs) {
32
32
  guardApplied: requestedTimeoutMs > effectiveTimeoutMs,
33
33
  };
34
34
  }
35
- function hasNonWaitableMissing(missing) {
35
+ export function hasNonWaitableMissing(missing) {
36
+ // Phase 114: clientProspectId is no longer non-waitable. Net-new campaigns
37
+ // can be created with senderLinkedinUrl instead, with downstream resolution
38
+ // happening at send-time.
36
39
  const nonWaitable = new Set([
37
- "clientProspectId",
38
40
  "campaignBrief.content",
39
41
  "leadSourceType",
40
42
  'leadSourceType="new"',
@@ -2,6 +2,7 @@ export type CreateWorkflowTableInput = {
2
2
  name: string;
3
3
  senderId: string;
4
4
  clientProspectId?: string;
5
+ senderLinkedinUrl?: string;
5
6
  sequenceActions?: string[];
6
7
  };
7
8
  export type CreateWorkflowTableResponse = {
@@ -58,6 +59,10 @@ export declare const sequencerToolDefinitions: ({
58
59
  type: string;
59
60
  description: string;
60
61
  };
62
+ senderLinkedinUrl: {
63
+ type: string;
64
+ description: string;
65
+ };
61
66
  sequenceActions: {
62
67
  type: string;
63
68
  items: {
@@ -93,6 +98,7 @@ export declare const sequencerToolDefinitions: ({
93
98
  name?: undefined;
94
99
  senderId?: undefined;
95
100
  clientProspectId?: undefined;
101
+ senderLinkedinUrl?: undefined;
96
102
  sequenceActions?: undefined;
97
103
  campaignId?: undefined;
98
104
  };
@@ -115,6 +121,7 @@ export declare const sequencerToolDefinitions: ({
115
121
  name?: undefined;
116
122
  senderId?: undefined;
117
123
  clientProspectId?: undefined;
124
+ senderLinkedinUrl?: undefined;
118
125
  sequenceActions?: undefined;
119
126
  tableId?: undefined;
120
127
  template?: undefined;
@@ -7,7 +7,8 @@ export const sequencerToolDefinitions = [
7
7
  "All tables include LinkedIn URL, Name, and Approved. " +
8
8
  "Additional columns depend on the planned actions: INVITE/DM -> Message, " +
9
9
  "INMAIL_* -> Subject + Message, COMMENT -> Post URL + Comment + Reaction Type. " +
10
- "Campaign brief, rubrics, positioning, and prospect linkage remain optional follow-on metadata.",
10
+ "Campaign brief, rubrics, positioning, and prospect linkage remain optional follow-on metadata.\n\n" +
11
+ "INPUTS: Pass EITHER clientProspectId OR senderLinkedinUrl. Both optional — if neither is supplied, the table is created without prospect linkage.",
11
12
  inputSchema: {
12
13
  type: "object",
13
14
  properties: {
@@ -21,7 +22,11 @@ export const sequencerToolDefinitions = [
21
22
  },
22
23
  clientProspectId: {
23
24
  type: "string",
24
- description: "Optional client prospect ID (from enrich_sender)",
25
+ description: "Optional. EnrichedProspect ID. Pass this OR senderLinkedinUrl.",
26
+ },
27
+ senderLinkedinUrl: {
28
+ type: "string",
29
+ description: "Optional. Sender's LinkedIn profile URL — used to lazy-resolve the prospect at send-time when clientProspectId is not yet available.",
25
30
  },
26
31
  sequenceActions: {
27
32
  type: "array",
@@ -87,6 +92,7 @@ export async function createWorkflowTable(input) {
87
92
  name: input.name,
88
93
  senderId: input.senderId,
89
94
  clientProspectId: input.clientProspectId,
95
+ senderLinkedinUrl: input.senderLinkedinUrl,
90
96
  ...(input.sequenceActions
91
97
  ? { sequenceActions: input.sequenceActions }
92
98
  : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: building-gtm-tables
3
3
  description: Turn a GTM idea into a fully configured, verified workflow table. Use this skill whenever the user asks to "build a table that..." / "set up an enrichment + scoring pipeline" / "add columns that run end-to-end against my rows." Blueprints the whole table in memory, validates composition, commits atomically in topological order, then runs single-row verify + a user-approved 2-5 row batch before declaring done. Do NOT use this skill for pure messaging or for editing an already-committed table one column at a time.
4
+ visibility: internal
4
5
  ---
5
6
 
6
7
  # Building GTM Tables