@sellable/mcp 0.1.274 → 0.1.276

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,9 +16,10 @@ Each message gets 5+ minutes of Claude attention with deep research - no other t
16
16
 
17
17
  ### Prompt Source Of Truth
18
18
 
19
- There are four public Sellable entrypoints shared across hosts:
19
+ There are five public Sellable entrypoints shared across hosts:
20
20
 
21
21
  - `sellable:create-campaign`
22
+ - `sellable:create-ab-test`
22
23
  - `sellable:foundation`
23
24
  - `sellable:content`
24
25
  - `sellable:create-post`
@@ -29,6 +30,11 @@ the approval-gated workflow from:
29
30
 
30
31
  - `mcp/sellable/skills/create-campaign-v2/SKILL.md`
31
32
 
33
+ The create-ab-test public wrapper prepares clean A/B split lead lists and
34
+ review-copy campaigns from:
35
+
36
+ - `mcp/sellable/skills/create-ab-test/SKILL.md`
37
+
32
38
  The foundation public wrapper loads the core identity/company memory workflow
33
39
  from:
34
40
 
@@ -148,8 +154,9 @@ The installer does the full local setup:
148
154
  `mcp__sellable__*` tools into skill sessions
149
155
 
150
156
  After the installer passes, fully quit and reopen Codex Desktop. Start a new
151
- thread and select `Sellable Create Campaign`, `Sellable Foundation`,
152
- `Sellable Content`, or `Sellable Create Post`; or invoke `$sellable:create-campaign`,
157
+ thread and select `Sellable Create Campaign`, `Sellable Create A/B Test`,
158
+ `Sellable Foundation`, `Sellable Content`, or `Sellable Create Post`; or invoke
159
+ `$sellable:create-campaign`, `$sellable:create-ab-test`,
153
160
  `$sellable:foundation`, `$sellable:content`, or `$sellable:create-post`. If the app still says
154
161
  `mcp__sellable__*` tools are missing after the installer passes, check that
155
162
  `~/.codex/config.toml` contains both `[marketplaces.sellable]` and
@@ -162,19 +169,23 @@ the Sellable MCP tools for Codex Desktop.
162
169
  Use these names consistently:
163
170
 
164
171
  - Claude Code command: `/sellable:create-campaign`
172
+ - Claude Code command: `/sellable:create-ab-test`
165
173
  - Claude Code command: `/sellable:foundation`
166
174
  - Claude Code command: `/sellable:content`
167
175
  - Claude Code command: `/sellable:create-post`
168
176
  - Codex command: `$sellable:create-campaign`
177
+ - Codex command: `$sellable:create-ab-test`
169
178
  - Codex command: `$sellable:foundation`
170
179
  - Codex command: `$sellable:content`
171
180
  - Codex command: `$sellable:create-post`
172
181
  - Codex Desktop plugin: `sellable@sellable`
173
182
  - Codex visible skill: `Sellable Create Campaign`
183
+ - Codex visible skill: `Sellable Create A/B Test`
174
184
  - Codex visible skill: `Sellable Foundation`
175
185
  - Codex visible skill: `Sellable Content`
176
186
  - Codex visible skill: `Sellable Create Post`
177
187
  - Codex skill frontmatter name: `create-campaign`
188
+ - Codex skill frontmatter name: `create-ab-test`
178
189
  - Codex skill frontmatter name: `foundation`
179
190
  - Codex skill frontmatter name: `content`
180
191
  - Codex skill frontmatter name: `create-post`
package/dist/server.js CHANGED
@@ -6,7 +6,9 @@ import { getAuthStatus } from "./tools/auth.js";
6
6
  import { handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
7
  import { bootstrapCreateCampaign } from "./tools/bootstrap.js";
8
8
  import { getCampaignTableSchema, queueCampaignCells, reviseMessageTemplateAndRerun, selectCampaignCells, waitForCampaignProcessing, } from "./tools/campaign-processing.js";
9
+ import { cancelPrepareCampaignMessages, getPrepareCampaignMessagesStatus, startPrepareCampaignMessages, } from "./tools/campaign-message-preparation.js";
9
10
  import { createCampaign, duplicateCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
11
+ import { prepareCampaignAbTest } from "./tools/campaign-ab-test.js";
10
12
  import { queueCells, updateCell } from "./tools/cells.js";
11
13
  import { handleStartCliLogin, handleWaitForCliLogin, } from "./tools/cli-login.js";
12
14
  import { calculateLinkedInHookPreviewTool, capturePostIdeaTool, getPostDraftTool, getPostIdeaTool, getPublishedPostTool, listPostDraftIterationsTool, listPostDraftsTool, listPostIdeasTool, listPublishedPostsTool, markPostPublishedTool, renderLinkedInPostPreviewTool, saveHookResearchTool, savePostDraftTool, updatePostDraftTool, updatePublishedPostMetricsTool, } from "./tools/content-posts.js";
@@ -163,6 +165,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
163
165
  case "get_campaign_messages_preview":
164
166
  result = await getCampaignMessagesPreview(args);
165
167
  break;
168
+ case "start_prepare_campaign_messages":
169
+ result = await startPrepareCampaignMessages(args);
170
+ if (args?.campaignId) {
171
+ markCampaignContextDirty(args.campaignId, "start_prepare_campaign_messages");
172
+ }
173
+ break;
174
+ case "get_prepare_campaign_messages_status":
175
+ result = await getPrepareCampaignMessagesStatus(args);
176
+ if (args?.campaignId &&
177
+ ["succeeded", "cancelled", "failed"].includes(String(result?.status))) {
178
+ markCampaignContextDirty(args.campaignId, "get_prepare_campaign_messages_status");
179
+ }
180
+ break;
181
+ case "cancel_prepare_campaign_messages":
182
+ result = await cancelPrepareCampaignMessages(args);
183
+ if (args?.campaignId) {
184
+ markCampaignContextDirty(args.campaignId, "cancel_prepare_campaign_messages");
185
+ }
186
+ break;
166
187
  case "get_campaign_table_schema":
167
188
  result = await getCampaignTableSchema(args);
168
189
  break;
@@ -215,6 +236,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
215
236
  markCampaignContextDirty(result.campaignOfferId, "duplicate_campaign");
216
237
  }
217
238
  break;
239
+ case "prepare_campaign_ab_test":
240
+ result = await prepareCampaignAbTest(args);
241
+ if (result?.variants?.A?.campaignId) {
242
+ markCampaignContextDirty(result.variants.A.campaignId, "prepare_campaign_ab_test");
243
+ }
244
+ if (result?.variants?.B?.campaignId) {
245
+ markCampaignContextDirty(result.variants.B.campaignId, "prepare_campaign_ab_test");
246
+ }
247
+ break;
218
248
  case "update_campaign_brief":
219
249
  result = await updateCampaignBrief(args?.campaignId, args?.campaignBrief);
220
250
  if (args?.campaignId) {
@@ -0,0 +1,72 @@
1
+ export interface PrepareCampaignAbTestInput {
2
+ sourceCampaignId: string;
3
+ variantName: string;
4
+ variantBriefDelta: string;
5
+ variantALabel?: string;
6
+ variantABriefDelta?: string;
7
+ splitStrategy?: "alternating_stable_sort";
8
+ targetCounts?: {
9
+ a?: number;
10
+ b?: number;
11
+ };
12
+ dryRun?: boolean;
13
+ idempotencyKey?: string;
14
+ }
15
+ export declare const campaignAbTestToolDefinitions: {
16
+ name: string;
17
+ description: string;
18
+ inputSchema: {
19
+ type: string;
20
+ properties: {
21
+ sourceCampaignId: {
22
+ type: string;
23
+ description: string;
24
+ };
25
+ variantName: {
26
+ type: string;
27
+ description: string;
28
+ };
29
+ variantBriefDelta: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ variantALabel: {
34
+ type: string;
35
+ description: string;
36
+ };
37
+ variantABriefDelta: {
38
+ type: string;
39
+ description: string;
40
+ };
41
+ splitStrategy: {
42
+ type: string;
43
+ enum: string[];
44
+ description: string;
45
+ };
46
+ targetCounts: {
47
+ type: string;
48
+ properties: {
49
+ a: {
50
+ type: string;
51
+ };
52
+ b: {
53
+ type: string;
54
+ };
55
+ };
56
+ additionalProperties: boolean;
57
+ description: string;
58
+ };
59
+ dryRun: {
60
+ type: string;
61
+ description: string;
62
+ };
63
+ idempotencyKey: {
64
+ type: string;
65
+ description: string;
66
+ };
67
+ };
68
+ required: string[];
69
+ additionalProperties: boolean;
70
+ };
71
+ }[];
72
+ export declare function prepareCampaignAbTest(input: PrepareCampaignAbTestInput): Promise<unknown>;
@@ -0,0 +1,60 @@
1
+ import { getApi } from "../api.js";
2
+ export const campaignAbTestToolDefinitions = [
3
+ {
4
+ name: "prepare_campaign_ab_test",
5
+ description: "Prepare a campaign A/B test from an existing campaign using its clean source lead list. Creates deterministic A/B split lead lists and review-copy campaigns, updates the variant brief, and never launches either campaign.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ sourceCampaignId: {
10
+ type: "string",
11
+ description: "Existing source campaign ID.",
12
+ },
13
+ variantName: {
14
+ type: "string",
15
+ description: "Short label for variant B, e.g. New CTA.",
16
+ },
17
+ variantBriefDelta: {
18
+ type: "string",
19
+ description: "The specific campaign brief/copy change for variant B.",
20
+ },
21
+ variantALabel: {
22
+ type: "string",
23
+ description: "Optional label for the control variant.",
24
+ },
25
+ variantABriefDelta: {
26
+ type: "string",
27
+ description: "Optional explicit brief change for variant A. Omit for unchanged control.",
28
+ },
29
+ splitStrategy: {
30
+ type: "string",
31
+ enum: ["alternating_stable_sort"],
32
+ description: "Deterministic split strategy. Defaults to alternating_stable_sort.",
33
+ },
34
+ targetCounts: {
35
+ type: "object",
36
+ properties: {
37
+ a: { type: "number" },
38
+ b: { type: "number" },
39
+ },
40
+ additionalProperties: false,
41
+ description: "Optional exact split counts. Omit for an even alternating split.",
42
+ },
43
+ dryRun: {
44
+ type: "boolean",
45
+ description: "When true, returns split plan and review names without creating campaigns/lists.",
46
+ },
47
+ idempotencyKey: {
48
+ type: "string",
49
+ description: "Optional stable key for retrying the same A/B preparation safely.",
50
+ },
51
+ },
52
+ required: ["sourceCampaignId", "variantName", "variantBriefDelta"],
53
+ additionalProperties: false,
54
+ },
55
+ },
56
+ ];
57
+ export async function prepareCampaignAbTest(input) {
58
+ const api = getApi();
59
+ return api.post("/api/v3/mcp/campaign-ab-test", input);
60
+ }
@@ -0,0 +1,81 @@
1
+ type ApprovalMode = "mark_ready" | "approve";
2
+ type PrepareMessagesBaseInput = {
3
+ campaignId?: string;
4
+ tableId?: string;
5
+ jobId?: string;
6
+ };
7
+ type StartPrepareMessagesInput = PrepareMessagesBaseInput & {
8
+ campaignId: string;
9
+ targetPreparedMessages?: number;
10
+ maxRowsToCheck?: number;
11
+ batchSize?: number;
12
+ approvalMode?: ApprovalMode;
13
+ autoContinue?: boolean;
14
+ };
15
+ type StatusPrepareMessagesInput = PrepareMessagesBaseInput;
16
+ type CancelPrepareMessagesInput = PrepareMessagesBaseInput;
17
+ export declare const campaignMessagePreparationToolDefinitions: ({
18
+ name: string;
19
+ description: string;
20
+ inputSchema: {
21
+ type: string;
22
+ properties: {
23
+ campaignId: {
24
+ type: string;
25
+ description: string;
26
+ };
27
+ tableId: {
28
+ type: string;
29
+ };
30
+ targetPreparedMessages: {
31
+ type: string;
32
+ };
33
+ maxRowsToCheck: {
34
+ type: string;
35
+ };
36
+ batchSize: {
37
+ type: string;
38
+ };
39
+ approvalMode: {
40
+ type: string;
41
+ enum: string[];
42
+ description: string;
43
+ };
44
+ autoContinue: {
45
+ type: string;
46
+ };
47
+ jobId?: undefined;
48
+ };
49
+ required: string[];
50
+ additionalProperties: boolean;
51
+ };
52
+ } | {
53
+ name: string;
54
+ description: string;
55
+ inputSchema: {
56
+ type: string;
57
+ properties: {
58
+ jobId: {
59
+ type: string;
60
+ };
61
+ campaignId: {
62
+ type: string;
63
+ description?: undefined;
64
+ };
65
+ tableId: {
66
+ type: string;
67
+ };
68
+ targetPreparedMessages?: undefined;
69
+ maxRowsToCheck?: undefined;
70
+ batchSize?: undefined;
71
+ approvalMode?: undefined;
72
+ autoContinue?: undefined;
73
+ };
74
+ additionalProperties: boolean;
75
+ required?: undefined;
76
+ };
77
+ })[];
78
+ export declare function startPrepareCampaignMessages(input: StartPrepareMessagesInput): Promise<unknown>;
79
+ export declare function getPrepareCampaignMessagesStatus(input: StatusPrepareMessagesInput): Promise<unknown>;
80
+ export declare function cancelPrepareCampaignMessages(input: CancelPrepareMessagesInput): Promise<unknown>;
81
+ export {};
@@ -0,0 +1,86 @@
1
+ import { getApi } from "../api.js";
2
+ async function postPrepareMessages(body) {
3
+ const api = getApi();
4
+ return api.post("/api/v3/mcp/campaign-message-preparation", body);
5
+ }
6
+ export const campaignMessagePreparationToolDefinitions = [
7
+ {
8
+ name: "start_prepare_campaign_messages",
9
+ description: 'Start a bounded Prepare Messages job for a CampaignOffer campaignId. Use this after lead/message approval when the user asks for a target like "prepare 100 messages"; default approvalMode is mark_ready and it never starts the campaign.',
10
+ inputSchema: {
11
+ type: "object",
12
+ properties: {
13
+ campaignId: {
14
+ type: "string",
15
+ description: "CampaignOffer.id for the campaign whose workflow table should prepare messages.",
16
+ },
17
+ tableId: { type: "string" },
18
+ targetPreparedMessages: { type: "number" },
19
+ maxRowsToCheck: { type: "number" },
20
+ batchSize: { type: "number" },
21
+ approvalMode: {
22
+ type: "string",
23
+ enum: ["mark_ready", "approve"],
24
+ description: 'Defaults to mark_ready. Use approve only when the user explicitly asks to flip Approved cells.',
25
+ },
26
+ autoContinue: { type: "boolean" },
27
+ },
28
+ required: ["campaignId"],
29
+ additionalProperties: false,
30
+ },
31
+ },
32
+ {
33
+ name: "get_prepare_campaign_messages_status",
34
+ description: "Poll compact Prepare Messages progress: checked rows, prepared/approved count, target, remaining row budget, warnings, and stop reason. Does not return raw rows or message bodies.",
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {
38
+ jobId: { type: "string" },
39
+ campaignId: { type: "string" },
40
+ tableId: { type: "string" },
41
+ },
42
+ additionalProperties: false,
43
+ },
44
+ },
45
+ {
46
+ name: "cancel_prepare_campaign_messages",
47
+ description: "Cancel an active Prepare Messages job. Cancellation is cooperative and launch remains a separate explicit start_campaign step.",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {
51
+ jobId: { type: "string" },
52
+ campaignId: { type: "string" },
53
+ tableId: { type: "string" },
54
+ },
55
+ additionalProperties: false,
56
+ },
57
+ },
58
+ ];
59
+ export function startPrepareCampaignMessages(input) {
60
+ return postPrepareMessages({
61
+ action: "start",
62
+ campaignId: input.campaignId,
63
+ tableId: input.tableId,
64
+ targetPreparedMessages: input.targetPreparedMessages,
65
+ maxRowsToCheck: input.maxRowsToCheck,
66
+ batchSize: input.batchSize,
67
+ approvalMode: input.approvalMode,
68
+ autoContinue: input.autoContinue,
69
+ });
70
+ }
71
+ export function getPrepareCampaignMessagesStatus(input) {
72
+ return postPrepareMessages({
73
+ action: "status",
74
+ jobId: input.jobId,
75
+ campaignId: input.campaignId,
76
+ tableId: input.tableId,
77
+ });
78
+ }
79
+ export function cancelPrepareCampaignMessages(input) {
80
+ return postPrepareMessages({
81
+ action: "cancel",
82
+ jobId: input.jobId,
83
+ campaignId: input.campaignId,
84
+ tableId: input.tableId,
85
+ });
86
+ }
@@ -9,6 +9,18 @@ export type CsvLinkedinLimits = {
9
9
  maxRows: number;
10
10
  maxCarryColumns: number;
11
11
  };
12
+ export type CsvLinkedinIgnoredReservedColumn = {
13
+ originalHeader: string;
14
+ canonicalName: string;
15
+ reason: string;
16
+ };
17
+ export type CsvLinkedinContentFingerprint = {
18
+ fileSha256: string;
19
+ sanitizedHeaderSha256: string;
20
+ selectedColumnsSha256: string;
21
+ ignoredReservedColumnsSha256: string;
22
+ rowCount: number;
23
+ };
12
24
  export type CsvLinkedinPreview = {
13
25
  resolvedFilePath: string;
14
26
  suggestedLeadListName: string;
@@ -26,6 +38,8 @@ export type CsvLinkedinPreview = {
26
38
  duplicateLinkedInCount: number;
27
39
  emptyLinkedInRowCount: number;
28
40
  duplicatePolicy: "first_wins";
41
+ ignoredReservedColumns: CsvLinkedinIgnoredReservedColumn[];
42
+ reservedColumnWarning: string | null;
29
43
  limits: CsvLinkedinLimits;
30
44
  warnings: string[];
31
45
  blockingErrors: string[];
@@ -38,6 +52,8 @@ export type CsvLinkedinConfirmationPayload = {
38
52
  fileMtimeMs: number;
39
53
  linkedInColumn: string;
40
54
  selectedColumns: string[];
55
+ ignoredReservedColumns: CsvLinkedinIgnoredReservedColumn[];
56
+ contentFingerprint: CsvLinkedinContentFingerprint;
41
57
  };
42
58
  export type PreparedLinkedinLeadRow = {
43
59
  row: Record<string, string>;
@@ -76,6 +92,9 @@ type ParsedCsvLinkedinFile = {
76
92
  headers: string[];
77
93
  rows: Array<Record<string, string>>;
78
94
  totalRows: number;
95
+ ignoredReservedColumns: CsvLinkedinIgnoredReservedColumn[];
96
+ reservedColumnWarning: string | null;
97
+ fileSha256: string;
79
98
  blockingErrors: string[];
80
99
  warnings: string[];
81
100
  };
@@ -86,6 +105,10 @@ export type LinkedInValidationResult = {
86
105
  valid: false;
87
106
  reason: string;
88
107
  };
108
+ export declare function getReservedWorkflowColumnMatch(header: string): {
109
+ canonicalName: string;
110
+ reason: string;
111
+ } | null;
89
112
  export declare function validateAndNormalizeLinkedInUrl(value: string): LinkedInValidationResult;
90
113
  export declare function makeLinkedinConfirmationToken(payload: CsvLinkedinConfirmationPayload): string;
91
114
  export declare function parseLinkedinConfirmationToken(token: string): CsvLinkedinConfirmationPayload;