@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 +14 -3
- package/dist/server.js +30 -0
- package/dist/tools/campaign-ab-test.d.ts +72 -0
- package/dist/tools/campaign-ab-test.js +60 -0
- package/dist/tools/campaign-message-preparation.d.ts +81 -0
- package/dist/tools/campaign-message-preparation.js +86 -0
- package/dist/tools/csv-linkedin.d.ts +23 -0
- package/dist/tools/csv-linkedin.js +167 -7
- package/dist/tools/leads.d.ts +6 -0
- package/dist/tools/leads.js +8 -0
- package/dist/tools/prompts.d.ts +5 -3
- package/dist/tools/prompts.js +2 -0
- package/dist/tools/registry.d.ts +118 -2
- package/dist/tools/registry.js +4 -0
- package/package.json +1 -1
- package/skills/create-ab-test/SKILL.md +77 -0
- package/skills/create-campaign/SKILL.md +33 -1
- package/skills/create-campaign-v2/core/flow.v2.json +1 -1
- package/skills/research/config.json +9 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parse } from "csv-parse/sync";
|
|
2
|
-
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
3
3
|
import { readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, resolve } from "node:path";
|
|
5
5
|
import { resolveWorkspaceRoot } from "../utils/workspace-root.js";
|
|
@@ -49,6 +49,86 @@ const CSV_LINKEDIN_LIMITS = {
|
|
|
49
49
|
maxRows: MAX_CSV_LINKEDIN_UPLOAD_ROWS,
|
|
50
50
|
maxCarryColumns: MAX_CSV_LINKEDIN_UPLOAD_CARRY_COLUMNS,
|
|
51
51
|
};
|
|
52
|
+
const RESERVED_WORKFLOW_COLUMNS = new Map([
|
|
53
|
+
"DNC Check",
|
|
54
|
+
"Enable ICP Filtering",
|
|
55
|
+
"ICP Score",
|
|
56
|
+
"ICP Alignment Notes",
|
|
57
|
+
"Passes Rubric",
|
|
58
|
+
"Generate Message",
|
|
59
|
+
"Message",
|
|
60
|
+
"Subject",
|
|
61
|
+
"Approved",
|
|
62
|
+
"Enable Scheduling",
|
|
63
|
+
"Scheduled",
|
|
64
|
+
"Scheduled At",
|
|
65
|
+
"Scheduled For",
|
|
66
|
+
"Sent",
|
|
67
|
+
"Sent At",
|
|
68
|
+
"Send Status",
|
|
69
|
+
"Delivery Status",
|
|
70
|
+
"Reply Status",
|
|
71
|
+
"Connection Status",
|
|
72
|
+
"Invite Status",
|
|
73
|
+
"InMail Status",
|
|
74
|
+
"DM Status",
|
|
75
|
+
"Status",
|
|
76
|
+
"Result",
|
|
77
|
+
"Error",
|
|
78
|
+
"Last Error",
|
|
79
|
+
"Generated Message",
|
|
80
|
+
"Message Draft",
|
|
81
|
+
"Message Status",
|
|
82
|
+
"Approval Status",
|
|
83
|
+
"Queued At",
|
|
84
|
+
"Processing Status",
|
|
85
|
+
"Workflow Status",
|
|
86
|
+
"Cell Status",
|
|
87
|
+
].map((name) => [normalizeReservedWorkflowHeaderKey(name), name]));
|
|
88
|
+
function normalizeReservedWorkflowHeaderKey(value) {
|
|
89
|
+
return value
|
|
90
|
+
.trim()
|
|
91
|
+
.replace(/\s*(?:\(\d+\)|[._-]\d+|\s+\d+)$/u, "")
|
|
92
|
+
.toLowerCase()
|
|
93
|
+
.replace(/[\s_.-]+/g, " ")
|
|
94
|
+
.replace(/[^a-z0-9 ]+/g, "")
|
|
95
|
+
.trim();
|
|
96
|
+
}
|
|
97
|
+
export function getReservedWorkflowColumnMatch(header) {
|
|
98
|
+
const key = normalizeReservedWorkflowHeaderKey(header);
|
|
99
|
+
if (!key) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const canonicalName = RESERVED_WORKFLOW_COLUMNS.get(key);
|
|
103
|
+
if (!canonicalName) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
canonicalName,
|
|
108
|
+
reason: "Sellable workflow/output columns are recreated by each campaign table and cannot be imported as lead source fields.",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function buildReservedColumnWarning(ignored) {
|
|
112
|
+
if (ignored.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const names = Array.from(new Set(ignored.map((column) => column.originalHeader)));
|
|
116
|
+
return `Detected campaign workflow columns and stripped them because Sellable recreates those columns in each campaign table: ${names.join(", ")}.`;
|
|
117
|
+
}
|
|
118
|
+
function hashJson(value) {
|
|
119
|
+
return createHash("sha256")
|
|
120
|
+
.update(JSON.stringify(value))
|
|
121
|
+
.digest("hex");
|
|
122
|
+
}
|
|
123
|
+
function buildContentFingerprint(params) {
|
|
124
|
+
return {
|
|
125
|
+
fileSha256: params.fileSha256,
|
|
126
|
+
sanitizedHeaderSha256: hashJson(params.headers),
|
|
127
|
+
selectedColumnsSha256: hashJson(params.selectedColumns),
|
|
128
|
+
ignoredReservedColumnsSha256: hashJson(params.ignoredReservedColumns),
|
|
129
|
+
rowCount: params.rowCount,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
52
132
|
function normalizeHeaderKey(value) {
|
|
53
133
|
return value
|
|
54
134
|
.trim()
|
|
@@ -61,6 +141,17 @@ function compareStringArrays(left, right) {
|
|
|
61
141
|
}
|
|
62
142
|
return left.every((value, index) => value === right[index]);
|
|
63
143
|
}
|
|
144
|
+
function compareIgnoredReservedColumns(left, right) {
|
|
145
|
+
if (left.length !== right.length) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return left.every((column, index) => {
|
|
149
|
+
const other = right[index];
|
|
150
|
+
return (column.originalHeader === other.originalHeader &&
|
|
151
|
+
column.canonicalName === other.canonicalName &&
|
|
152
|
+
column.reason === other.reason);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
64
155
|
function getLimits() {
|
|
65
156
|
return { ...CSV_LINKEDIN_LIMITS };
|
|
66
157
|
}
|
|
@@ -100,6 +191,30 @@ function buildRowObjects(headers, dataRows) {
|
|
|
100
191
|
return out;
|
|
101
192
|
});
|
|
102
193
|
}
|
|
194
|
+
function stripReservedWorkflowColumns(headers, dataRows) {
|
|
195
|
+
const keepIndexes = [];
|
|
196
|
+
const ignoredReservedColumns = [];
|
|
197
|
+
headers.forEach((header, index) => {
|
|
198
|
+
const match = getReservedWorkflowColumnMatch(header);
|
|
199
|
+
if (!match) {
|
|
200
|
+
keepIndexes.push(index);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
ignoredReservedColumns.push({
|
|
204
|
+
originalHeader: header,
|
|
205
|
+
canonicalName: match.canonicalName,
|
|
206
|
+
reason: match.reason,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
if (ignoredReservedColumns.length === 0) {
|
|
210
|
+
return { headers, dataRows, ignoredReservedColumns };
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
headers: keepIndexes.map((index) => headers[index]),
|
|
214
|
+
dataRows: dataRows.map((row) => keepIndexes.map((index) => String(row[index] ?? ""))),
|
|
215
|
+
ignoredReservedColumns,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
103
218
|
function resolveHeaderSelection(headers, requested, kind) {
|
|
104
219
|
const trimmed = requested.trim();
|
|
105
220
|
if (!trimmed) {
|
|
@@ -516,7 +631,17 @@ export function matchesLinkedinConfirmationToken(token, payload) {
|
|
|
516
631
|
parsed.fileSizeBytes === payload.fileSizeBytes &&
|
|
517
632
|
parsed.fileMtimeMs === payload.fileMtimeMs &&
|
|
518
633
|
parsed.linkedInColumn === payload.linkedInColumn &&
|
|
519
|
-
compareStringArrays(parsed.selectedColumns, payload.selectedColumns)
|
|
634
|
+
compareStringArrays(parsed.selectedColumns, payload.selectedColumns) &&
|
|
635
|
+
compareIgnoredReservedColumns(parsed.ignoredReservedColumns, payload.ignoredReservedColumns) &&
|
|
636
|
+
parsed.contentFingerprint.fileSha256 ===
|
|
637
|
+
payload.contentFingerprint.fileSha256 &&
|
|
638
|
+
parsed.contentFingerprint.sanitizedHeaderSha256 ===
|
|
639
|
+
payload.contentFingerprint.sanitizedHeaderSha256 &&
|
|
640
|
+
parsed.contentFingerprint.selectedColumnsSha256 ===
|
|
641
|
+
payload.contentFingerprint.selectedColumnsSha256 &&
|
|
642
|
+
parsed.contentFingerprint.ignoredReservedColumnsSha256 ===
|
|
643
|
+
payload.contentFingerprint.ignoredReservedColumnsSha256 &&
|
|
644
|
+
parsed.contentFingerprint.rowCount === payload.contentFingerprint.rowCount);
|
|
520
645
|
}
|
|
521
646
|
export function parseCsvLinkedinFile(input) {
|
|
522
647
|
const resolvedFilePath = resolveCsvLinkedinPath(input.filePath, input.workspaceRoot);
|
|
@@ -536,8 +661,9 @@ export function parseCsvLinkedinFile(input) {
|
|
|
536
661
|
blockingErrors.push(`CSV file exceeds the ${MAX_CSV_LINKEDIN_UPLOAD_BYTES} byte limit.`);
|
|
537
662
|
}
|
|
538
663
|
let rawRows;
|
|
664
|
+
let content;
|
|
539
665
|
try {
|
|
540
|
-
|
|
666
|
+
content = readFileSync(resolvedFilePath, "utf8");
|
|
541
667
|
rawRows = parse(content, {
|
|
542
668
|
bom: true,
|
|
543
669
|
columns: false,
|
|
@@ -559,6 +685,9 @@ export function parseCsvLinkedinFile(input) {
|
|
|
559
685
|
headers: [],
|
|
560
686
|
rows: [],
|
|
561
687
|
totalRows: 0,
|
|
688
|
+
ignoredReservedColumns: [],
|
|
689
|
+
reservedColumnWarning: null,
|
|
690
|
+
fileSha256: "",
|
|
562
691
|
blockingErrors,
|
|
563
692
|
warnings,
|
|
564
693
|
};
|
|
@@ -567,6 +696,14 @@ export function parseCsvLinkedinFile(input) {
|
|
|
567
696
|
const dataRows = rawRows
|
|
568
697
|
.slice(1)
|
|
569
698
|
.map((row) => row.map((value) => String(value ?? "")));
|
|
699
|
+
const fileSha256 = createHash("sha256").update(content).digest("hex");
|
|
700
|
+
const stripped = stripReservedWorkflowColumns(headers, dataRows);
|
|
701
|
+
const sanitizedHeaders = stripped.headers;
|
|
702
|
+
const sanitizedDataRows = stripped.dataRows;
|
|
703
|
+
const reservedColumnWarning = buildReservedColumnWarning(stripped.ignoredReservedColumns);
|
|
704
|
+
if (reservedColumnWarning) {
|
|
705
|
+
warnings.push(reservedColumnWarning);
|
|
706
|
+
}
|
|
570
707
|
if (headers.length === 0) {
|
|
571
708
|
blockingErrors.push("CSV file must include at least one header column.");
|
|
572
709
|
}
|
|
@@ -574,7 +711,10 @@ export function parseCsvLinkedinFile(input) {
|
|
|
574
711
|
if (blankHeaderCount > 0) {
|
|
575
712
|
blockingErrors.push("CSV header row contains blank column names.");
|
|
576
713
|
}
|
|
577
|
-
|
|
714
|
+
if (sanitizedHeaders.length === 0 && headers.length > 0) {
|
|
715
|
+
blockingErrors.push("CSV looks like a campaign workflow export rather than a clean LinkedIn lead list; all usable columns were Sellable workflow/output columns.");
|
|
716
|
+
}
|
|
717
|
+
const duplicateHeaders = findDuplicateHeaders(sanitizedHeaders);
|
|
578
718
|
if (duplicateHeaders.length > 0) {
|
|
579
719
|
blockingErrors.push(`CSV header row contains duplicate columns: ${duplicateHeaders.join(", ")}`);
|
|
580
720
|
}
|
|
@@ -585,7 +725,9 @@ export function parseCsvLinkedinFile(input) {
|
|
|
585
725
|
if (dataRows.length > MAX_CSV_LINKEDIN_UPLOAD_ROWS) {
|
|
586
726
|
blockingErrors.push(`CSV contains ${dataRows.length} data rows, which exceeds the ${MAX_CSV_LINKEDIN_UPLOAD_ROWS} row limit.`);
|
|
587
727
|
}
|
|
588
|
-
const rows = blockingErrors.length > 0
|
|
728
|
+
const rows = blockingErrors.length > 0
|
|
729
|
+
? []
|
|
730
|
+
: buildRowObjects(sanitizedHeaders, sanitizedDataRows);
|
|
589
731
|
if (rows.length === 0 && dataRows.length === 0) {
|
|
590
732
|
warnings.push("CSV contains headers but no data rows.");
|
|
591
733
|
}
|
|
@@ -594,9 +736,12 @@ export function parseCsvLinkedinFile(input) {
|
|
|
594
736
|
suggestedLeadListName: buildLeadListNameSuggestion(resolvedFilePath),
|
|
595
737
|
fileSizeBytes: stats.size,
|
|
596
738
|
fileMtimeMs: stats.mtimeMs,
|
|
597
|
-
headers,
|
|
739
|
+
headers: sanitizedHeaders,
|
|
598
740
|
rows,
|
|
599
741
|
totalRows: dataRows.length,
|
|
742
|
+
ignoredReservedColumns: stripped.ignoredReservedColumns,
|
|
743
|
+
reservedColumnWarning,
|
|
744
|
+
fileSha256,
|
|
600
745
|
blockingErrors,
|
|
601
746
|
warnings,
|
|
602
747
|
};
|
|
@@ -752,8 +897,13 @@ export function buildCsvLinkedinPreview(input) {
|
|
|
752
897
|
duplicateCount: 0,
|
|
753
898
|
emptyCount: 0,
|
|
754
899
|
};
|
|
900
|
+
if (!resolvedLinkedInColumn && parsed.ignoredReservedColumns.length > 0) {
|
|
901
|
+
blockingErrors.push("CSV looks like a campaign workflow export rather than a clean LinkedIn lead list; after stripping Sellable workflow/output columns, no LinkedIn profile URL column was available.");
|
|
902
|
+
}
|
|
755
903
|
if (resolvedLinkedInColumn && prepared.rows.length === 0) {
|
|
756
|
-
blockingErrors.push(
|
|
904
|
+
blockingErrors.push(parsed.ignoredReservedColumns.length > 0
|
|
905
|
+
? "CSV looks like a campaign workflow export rather than a clean LinkedIn lead list; no valid LinkedIn profile URLs remained after stripping Sellable workflow/output columns."
|
|
906
|
+
: "CSV did not produce any valid LinkedIn profile URLs after validation.");
|
|
757
907
|
}
|
|
758
908
|
if (prepared.invalidCount > 0) {
|
|
759
909
|
warnings.push(`Skipped ${prepared.invalidCount} invalid LinkedIn URL row(s).`);
|
|
@@ -772,6 +922,14 @@ export function buildCsvLinkedinPreview(input) {
|
|
|
772
922
|
fileMtimeMs: parsed.fileMtimeMs,
|
|
773
923
|
linkedInColumn: resolvedLinkedInColumn,
|
|
774
924
|
selectedColumns,
|
|
925
|
+
ignoredReservedColumns: parsed.ignoredReservedColumns,
|
|
926
|
+
contentFingerprint: buildContentFingerprint({
|
|
927
|
+
fileSha256: parsed.fileSha256,
|
|
928
|
+
headers,
|
|
929
|
+
selectedColumns,
|
|
930
|
+
ignoredReservedColumns: parsed.ignoredReservedColumns,
|
|
931
|
+
rowCount: parsed.totalRows,
|
|
932
|
+
}),
|
|
775
933
|
}
|
|
776
934
|
: null;
|
|
777
935
|
return {
|
|
@@ -792,6 +950,8 @@ export function buildCsvLinkedinPreview(input) {
|
|
|
792
950
|
duplicateLinkedInCount: prepared.duplicateCount,
|
|
793
951
|
emptyLinkedInRowCount: prepared.emptyCount,
|
|
794
952
|
duplicatePolicy: "first_wins",
|
|
953
|
+
ignoredReservedColumns: parsed.ignoredReservedColumns,
|
|
954
|
+
reservedColumnWarning: parsed.reservedColumnWarning,
|
|
795
955
|
limits: getLimits(),
|
|
796
956
|
warnings,
|
|
797
957
|
blockingErrors,
|
package/dist/tools/leads.d.ts
CHANGED
|
@@ -4151,6 +4151,8 @@ export declare function loadCsvLinkedinLeads(input: LoadCsvLinkedinLeadsInput):
|
|
|
4151
4151
|
invalidCount: number;
|
|
4152
4152
|
duplicateCount: number;
|
|
4153
4153
|
emptyLinkedInRowCount: number;
|
|
4154
|
+
ignoredReservedColumns: import("./csv-linkedin.js").CsvLinkedinIgnoredReservedColumn[];
|
|
4155
|
+
reservedColumnWarning: string | null;
|
|
4154
4156
|
duplicatePolicy: "first_wins";
|
|
4155
4157
|
limits: import("./csv-linkedin.js").CsvLinkedinLimits;
|
|
4156
4158
|
warnings: string[];
|
|
@@ -4186,6 +4188,8 @@ export declare function loadCsvLinkedinLeads(input: LoadCsvLinkedinLeadsInput):
|
|
|
4186
4188
|
invalidCount?: undefined;
|
|
4187
4189
|
duplicateCount?: undefined;
|
|
4188
4190
|
emptyLinkedInRowCount?: undefined;
|
|
4191
|
+
ignoredReservedColumns?: undefined;
|
|
4192
|
+
reservedColumnWarning?: undefined;
|
|
4189
4193
|
duplicatePolicy?: undefined;
|
|
4190
4194
|
limits?: undefined;
|
|
4191
4195
|
warnings?: undefined;
|
|
@@ -4225,6 +4229,8 @@ export declare function loadCsvLinkedinLeads(input: LoadCsvLinkedinLeadsInput):
|
|
|
4225
4229
|
invalidCount?: undefined;
|
|
4226
4230
|
duplicateCount?: undefined;
|
|
4227
4231
|
emptyLinkedInRowCount?: undefined;
|
|
4232
|
+
ignoredReservedColumns?: undefined;
|
|
4233
|
+
reservedColumnWarning?: undefined;
|
|
4228
4234
|
duplicatePolicy?: undefined;
|
|
4229
4235
|
limits?: undefined;
|
|
4230
4236
|
warnings?: undefined;
|
package/dist/tools/leads.js
CHANGED
|
@@ -2348,6 +2348,7 @@ function normalizeLinkedinConfirmationPayload(payload) {
|
|
|
2348
2348
|
return {
|
|
2349
2349
|
...payload,
|
|
2350
2350
|
selectedColumns: [...payload.selectedColumns],
|
|
2351
|
+
ignoredReservedColumns: [...(payload.ignoredReservedColumns ?? [])],
|
|
2351
2352
|
};
|
|
2352
2353
|
}
|
|
2353
2354
|
function compareStringArrays(left, right) {
|
|
@@ -3367,6 +3368,11 @@ export async function loadCsvLinkedinLeads(input) {
|
|
|
3367
3368
|
fileMtimeMs: previewBuild.fileMtimeMs,
|
|
3368
3369
|
linkedInColumn: previewBuild.resolvedLinkedInColumn ?? "",
|
|
3369
3370
|
selectedColumns: previewBuild.preview.selectedColumns,
|
|
3371
|
+
ignoredReservedColumns: previewBuild.preview.ignoredReservedColumns,
|
|
3372
|
+
contentFingerprint: previewBuild.preview.confirmationToken
|
|
3373
|
+
? parseLinkedinConfirmationToken(previewBuild.preview.confirmationToken)
|
|
3374
|
+
.contentFingerprint
|
|
3375
|
+
: tokenPayload.contentFingerprint,
|
|
3370
3376
|
};
|
|
3371
3377
|
if (!matchesLinkedinConfirmationToken(input.confirmationToken, currentPayload)) {
|
|
3372
3378
|
throw new Error("CSV file changed after preview. Re-run load_csv_linkedin_leads preview before confirming.");
|
|
@@ -3480,6 +3486,8 @@ export async function loadCsvLinkedinLeads(input) {
|
|
|
3480
3486
|
invalidCount: previewBuild.preview.invalidLinkedInRowCount,
|
|
3481
3487
|
duplicateCount: previewBuild.preview.duplicateLinkedInCount,
|
|
3482
3488
|
emptyLinkedInRowCount: previewBuild.preview.emptyLinkedInRowCount,
|
|
3489
|
+
ignoredReservedColumns: previewBuild.preview.ignoredReservedColumns,
|
|
3490
|
+
reservedColumnWarning: previewBuild.preview.reservedColumnWarning,
|
|
3483
3491
|
duplicatePolicy: previewBuild.preview.duplicatePolicy,
|
|
3484
3492
|
limits: previewBuild.preview.limits,
|
|
3485
3493
|
warnings: previewBuild.preview.warnings,
|
package/dist/tools/prompts.d.ts
CHANGED
|
@@ -82,6 +82,7 @@ export interface SourceScoutRegistryResponse {
|
|
|
82
82
|
codex: string;
|
|
83
83
|
claude: string;
|
|
84
84
|
parentThreadRule: string;
|
|
85
|
+
prepareMessagesRule?: string;
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
88
|
export interface PostFindLeadsScoutRegistryResponse {
|
|
@@ -131,11 +132,12 @@ export interface PostFindLeadsScoutRegistryResponse {
|
|
|
131
132
|
codex: string;
|
|
132
133
|
claude: string;
|
|
133
134
|
parentThreadRule: string;
|
|
135
|
+
prepareMessagesRule?: string;
|
|
134
136
|
};
|
|
135
137
|
}
|
|
136
138
|
export declare const DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS = 48000;
|
|
137
139
|
export declare const MAX_SUBSKILL_PROMPT_CHUNK_CHARS = 48000;
|
|
138
|
-
export declare const ALLOWED_SUBSKILL_PROMPT_NAMES: readonly ["building-gtm-tables", "content", "create-campaign", "create-campaign-brief", "create-campaign-v2", "create-campaign-v2-tail", "create-campaign-v2-validation", "create-post", "create-rubric", "engage", "enrich-prospects", "find-leads", "foundation", "generate-messages", "interview", "load-voice", "research", "research-prospect", "research-sender", "workflow-sequences"];
|
|
140
|
+
export declare const ALLOWED_SUBSKILL_PROMPT_NAMES: readonly ["building-gtm-tables", "content", "create-ab-test", "create-campaign", "create-campaign-brief", "create-campaign-v2", "create-campaign-v2-tail", "create-campaign-v2-validation", "create-post", "create-rubric", "engage", "enrich-prospects", "find-leads", "foundation", "generate-messages", "interview", "load-voice", "research", "research-prospect", "research-sender", "workflow-sequences"];
|
|
139
141
|
export declare const promptToolDefinitions: ({
|
|
140
142
|
name: string;
|
|
141
143
|
description: string;
|
|
@@ -179,7 +181,7 @@ export declare const promptToolDefinitions: ({
|
|
|
179
181
|
properties: {
|
|
180
182
|
subskillName: {
|
|
181
183
|
type: string;
|
|
182
|
-
enum: readonly ["building-gtm-tables", "content", "create-campaign", "create-campaign-brief", "create-campaign-v2", "create-campaign-v2-tail", "create-campaign-v2-validation", "create-post", "create-rubric", "engage", "enrich-prospects", "find-leads", "foundation", "generate-messages", "interview", "load-voice", "research", "research-prospect", "research-sender", "workflow-sequences"];
|
|
184
|
+
enum: readonly ["building-gtm-tables", "content", "create-ab-test", "create-campaign", "create-campaign-brief", "create-campaign-v2", "create-campaign-v2-tail", "create-campaign-v2-validation", "create-post", "create-rubric", "engage", "enrich-prospects", "find-leads", "foundation", "generate-messages", "interview", "load-voice", "research", "research-prospect", "research-sender", "workflow-sequences"];
|
|
183
185
|
description: string;
|
|
184
186
|
};
|
|
185
187
|
offset: {
|
|
@@ -214,7 +216,7 @@ export declare const promptToolDefinitions: ({
|
|
|
214
216
|
properties: {
|
|
215
217
|
subskillName: {
|
|
216
218
|
type: string;
|
|
217
|
-
enum: readonly ["building-gtm-tables", "content", "create-campaign", "create-campaign-brief", "create-campaign-v2", "create-campaign-v2-tail", "create-campaign-v2-validation", "create-post", "create-rubric", "engage", "enrich-prospects", "find-leads", "foundation", "generate-messages", "interview", "load-voice", "research", "research-prospect", "research-sender", "workflow-sequences"];
|
|
219
|
+
enum: readonly ["building-gtm-tables", "content", "create-ab-test", "create-campaign", "create-campaign-brief", "create-campaign-v2", "create-campaign-v2-tail", "create-campaign-v2-validation", "create-post", "create-rubric", "engage", "enrich-prospects", "find-leads", "foundation", "generate-messages", "interview", "load-voice", "research", "research-prospect", "research-sender", "workflow-sequences"];
|
|
218
220
|
description: string;
|
|
219
221
|
};
|
|
220
222
|
assetPath: {
|
package/dist/tools/prompts.js
CHANGED
|
@@ -19,6 +19,7 @@ const DEPRECATED_SUBSKILL_PROMPT_REPLACEMENTS = {
|
|
|
19
19
|
export const ALLOWED_SUBSKILL_PROMPT_NAMES = [
|
|
20
20
|
"building-gtm-tables",
|
|
21
21
|
"content",
|
|
22
|
+
"create-ab-test",
|
|
22
23
|
"create-campaign",
|
|
23
24
|
"create-campaign-brief",
|
|
24
25
|
"create-campaign-v2",
|
|
@@ -365,6 +366,7 @@ export function getPostFindLeadsScoutRegistry() {
|
|
|
365
366
|
codex: "After confirm_lead_list copies source rows and the initial campaign-table execution slice exists, ask the filter-choice question immediately. Do not spawn anything before that question. After the answer, launch only Message Drafting whenever Codex agent-launch policy is satisfied. The registry lookup is not a launch: after get_post_find_leads_scout_registry, immediately invoke Task/spawn_agent or the host background-agent mechanism before loading filter-leads.md or saving rubrics. If filters are chosen, the parent stays on Filter Rules and drafts/saves rubrics with MCP tools while Message Drafting runs in the background. If filters are skipped, move to Messages/message review after Message Drafting is ready. Treat YOLO/autonomous mode as campaign-scoped permission for this single post-import worker; do not ask for another permission click in YOLO. If the user has not enabled YOLO and has not explicitly asked for background agents/subagents/parallel agents/delegation/message bg agent in this campaign, ask once before loading the long message prompt in the parent. If permission is granted and the named Message Drafting custom agent is unavailable, spawn a generic gpt-5.5 xhigh Message Drafting background agent with the same lean campaign/table basis. If no background-agent tool is callable, start the same full message branch inline before filter drafting, or return blocked/retry-needed; do not wait until filters are saved and then call the registry.",
|
|
366
367
|
claude: "After confirm_lead_list copies source rows and the initial campaign-table execution slice exists, ask the filter-choice question immediately. Do not invoke any Task/Agent before that question. After the answer, invoke only Message Drafting. If filters are chosen, parent drafts/saves rubrics with MCP tools while Message Drafting runs, asks filter approval, then joins Message Drafting. If filters are skipped, invoke only Message Drafting and move to Messages/message review.",
|
|
367
368
|
parentThreadRule: 'Named agents are optional acceleration, but message drafting is not optional. The only normal background worker is Message Drafting. YOLO/autonomous mode counts as campaign-scoped permission for this single post-import worker; do not ask for another permission click in YOLO. If a named agent is unavailable after permission, use a generic gpt-5.5 xhigh Message Drafting background agent. source work and filter work stay in the parent thread with MCP tools. If post-find-leads-message-scout is available, run it as the background Message Draft Builder after the filter-choice answer. The registry lookup is not a launch: get_post_find_leads_scout_registry only identifies the worker, and Message Drafting counts as started only after Task/spawn_agent or the host background-agent tool is invoked, or after the parent begins the same full message branch inline because no background-agent tool is callable. This launch must happen before loading filter-leads.md, save_rubrics, or filter approval. If post-find-leads-message-scout is absent, do not customer-surface install status. Do not silently fall back to parent-thread message drafting; the main thread must execute the same message branch from CampaignOffer state, selected source state, workflowTableId, and initial campaign-table execution slice rows. If no background-agent tool is callable, start that same full message branch inline before filter drafting, or return blocked/retry-needed; do not wait until filters are saved and then call the registry. The Message Drafting handoff must be lean. Do not paste copied row counts, brief hashes, review-batch hashes, full reviewBatchRowIds, broad row data, or local debug artifacts into the spawn prompt. Local markdown/json files are not normal-path inputs. The filter-choice question is the first post-import user gate; do not load post-lead registries or filter references before it. Message drafting starts after the filter-choice answer, must load get_subskill_prompt({ subskillName: "generate-messages" }), and must load every required message asset named by generate-messages Mode 0 through get_subskill_asset before drafting. Reference Asset Loading means loading the required pre-draft reference pack before drafting; return blocked/retry-needed if required assets cannot be loaded; load ai-tells.md because it is never optional. The branch loads the full generate-messages prompt and every referenced asset through get_subskill_asset. After generating/revising the candidate and before returning ready, must load get_subskill_prompt({ subskillName: "create-campaign-v2-validation" }) as the final internal validation gate, must read live campaign table state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. Do not block when filters were chosen but leadScoringRubrics are not yet visible in the branch read; the parent owns save_rubrics and filter approval in parallel, so Message Drafting should return status ready with basisStatus usable_initial when the campaign/list/table and non-empty execution slice match. Do not use any alternate, local-artifact, or examples-only message prompt. User copy feedback, message QA, or rewrite requests before approve-message must be routed back to Message Drafting with the current recommendation, lean campaign/table basis, and latest user text; the parent must not rewrite or QA the template from memory and must not call update_campaign_brief before approve-message. The worker validates internally and returns only templateRecommendation, tokenFillRules, renderedGoodSample, status, approveOrReviseRecommendation, validationStatus, outputAt, outputHash, and blocked/retry detail. Do not render renderedFallbackSample, risk notes, or a qaReceipt on the normal happy path. On the filter path, save_rubrics keeps the browser on Filter Rules after save_rubrics so the user can approve the saved criteria; only then move to Filter Leads, show `Filters saved + waiting for message approval`, and wait there for message approval. Enrichment, filtering, Generate Message cells, sender setup, sequence attach, and launch wait for template approval on the Use Template path. On the skip path, move to Messages/message review after Message Drafting is ready and wait for message approval before enrichment or Settings. Do not render message review from checklist or shortcut instructions; message review requires a messageDraftRecommendation whose basis proves the generate-messages prompt, required message assets, and validation gate ran for the current campaign/table execution slice. Do not automatically rerun Message Drafting after filters/enrichment finish; show the initial draft by default and offer an enriched rewrite only with explicit user opt-in. Handoff and recommendation output are Markdown with labeled fields, not raw JSON.',
|
|
369
|
+
prepareMessagesRule: 'After message approval, prefer start_prepare_campaign_messages for target-ready preparation such as prepare 100 messages checking up to 300 rows. campaignId is CampaignOffer.id. approvalMode defaults to mark_ready; approvalMode approve requires explicit user intent to flip Approved cells. Poll get_prepare_campaign_messages_status and summarize checked rows, prepared/approved count, target, rows remaining, and stop reason. If the user asks to stop preparation, the target is wrong, or status shows the wrong campaign/table, call cancel_prepare_campaign_messages; otherwise do not cancel a healthy prepare run. Low-level selectors are diagnostics and recovery only for this lane. start_campaign remains forbidden until final launch greenlight.',
|
|
368
370
|
},
|
|
369
371
|
};
|
|
370
372
|
}
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -225,7 +225,7 @@ export declare const allTools: ({
|
|
|
225
225
|
properties: {
|
|
226
226
|
subskillName: {
|
|
227
227
|
type: string;
|
|
228
|
-
enum: readonly ["building-gtm-tables", "content", "create-campaign", "create-campaign-brief", "create-campaign-v2", "create-campaign-v2-tail", "create-campaign-v2-validation", "create-post", "create-rubric", "engage", "enrich-prospects", "find-leads", "foundation", "generate-messages", "interview", "load-voice", "research", "research-prospect", "research-sender", "workflow-sequences"];
|
|
228
|
+
enum: readonly ["building-gtm-tables", "content", "create-ab-test", "create-campaign", "create-campaign-brief", "create-campaign-v2", "create-campaign-v2-tail", "create-campaign-v2-validation", "create-post", "create-rubric", "engage", "enrich-prospects", "find-leads", "foundation", "generate-messages", "interview", "load-voice", "research", "research-prospect", "research-sender", "workflow-sequences"];
|
|
229
229
|
description: string;
|
|
230
230
|
};
|
|
231
231
|
offset: {
|
|
@@ -260,7 +260,7 @@ export declare const allTools: ({
|
|
|
260
260
|
properties: {
|
|
261
261
|
subskillName: {
|
|
262
262
|
type: string;
|
|
263
|
-
enum: readonly ["building-gtm-tables", "content", "create-campaign", "create-campaign-brief", "create-campaign-v2", "create-campaign-v2-tail", "create-campaign-v2-validation", "create-post", "create-rubric", "engage", "enrich-prospects", "find-leads", "foundation", "generate-messages", "interview", "load-voice", "research", "research-prospect", "research-sender", "workflow-sequences"];
|
|
263
|
+
enum: readonly ["building-gtm-tables", "content", "create-ab-test", "create-campaign", "create-campaign-brief", "create-campaign-v2", "create-campaign-v2-tail", "create-campaign-v2-validation", "create-post", "create-rubric", "engage", "enrich-prospects", "find-leads", "foundation", "generate-messages", "interview", "load-voice", "research", "research-prospect", "research-sender", "workflow-sequences"];
|
|
264
264
|
description: string;
|
|
265
265
|
};
|
|
266
266
|
assetPath: {
|
|
@@ -659,6 +659,66 @@ export declare const allTools: ({
|
|
|
659
659
|
required: string[];
|
|
660
660
|
additionalProperties: boolean;
|
|
661
661
|
};
|
|
662
|
+
} | {
|
|
663
|
+
name: string;
|
|
664
|
+
description: string;
|
|
665
|
+
inputSchema: {
|
|
666
|
+
type: string;
|
|
667
|
+
properties: {
|
|
668
|
+
campaignId: {
|
|
669
|
+
type: string;
|
|
670
|
+
description: string;
|
|
671
|
+
};
|
|
672
|
+
tableId: {
|
|
673
|
+
type: string;
|
|
674
|
+
};
|
|
675
|
+
targetPreparedMessages: {
|
|
676
|
+
type: string;
|
|
677
|
+
};
|
|
678
|
+
maxRowsToCheck: {
|
|
679
|
+
type: string;
|
|
680
|
+
};
|
|
681
|
+
batchSize: {
|
|
682
|
+
type: string;
|
|
683
|
+
};
|
|
684
|
+
approvalMode: {
|
|
685
|
+
type: string;
|
|
686
|
+
enum: string[];
|
|
687
|
+
description: string;
|
|
688
|
+
};
|
|
689
|
+
autoContinue: {
|
|
690
|
+
type: string;
|
|
691
|
+
};
|
|
692
|
+
jobId?: undefined;
|
|
693
|
+
};
|
|
694
|
+
required: string[];
|
|
695
|
+
additionalProperties: boolean;
|
|
696
|
+
};
|
|
697
|
+
} | {
|
|
698
|
+
name: string;
|
|
699
|
+
description: string;
|
|
700
|
+
inputSchema: {
|
|
701
|
+
type: string;
|
|
702
|
+
properties: {
|
|
703
|
+
jobId: {
|
|
704
|
+
type: string;
|
|
705
|
+
};
|
|
706
|
+
campaignId: {
|
|
707
|
+
type: string;
|
|
708
|
+
description?: undefined;
|
|
709
|
+
};
|
|
710
|
+
tableId: {
|
|
711
|
+
type: string;
|
|
712
|
+
};
|
|
713
|
+
targetPreparedMessages?: undefined;
|
|
714
|
+
maxRowsToCheck?: undefined;
|
|
715
|
+
batchSize?: undefined;
|
|
716
|
+
approvalMode?: undefined;
|
|
717
|
+
autoContinue?: undefined;
|
|
718
|
+
};
|
|
719
|
+
additionalProperties: boolean;
|
|
720
|
+
required?: undefined;
|
|
721
|
+
};
|
|
662
722
|
} | {
|
|
663
723
|
name: string;
|
|
664
724
|
description: string;
|
|
@@ -1140,6 +1200,62 @@ export declare const allTools: ({
|
|
|
1140
1200
|
required: string[];
|
|
1141
1201
|
additionalProperties?: undefined;
|
|
1142
1202
|
};
|
|
1203
|
+
} | {
|
|
1204
|
+
name: string;
|
|
1205
|
+
description: string;
|
|
1206
|
+
inputSchema: {
|
|
1207
|
+
type: string;
|
|
1208
|
+
properties: {
|
|
1209
|
+
sourceCampaignId: {
|
|
1210
|
+
type: string;
|
|
1211
|
+
description: string;
|
|
1212
|
+
};
|
|
1213
|
+
variantName: {
|
|
1214
|
+
type: string;
|
|
1215
|
+
description: string;
|
|
1216
|
+
};
|
|
1217
|
+
variantBriefDelta: {
|
|
1218
|
+
type: string;
|
|
1219
|
+
description: string;
|
|
1220
|
+
};
|
|
1221
|
+
variantALabel: {
|
|
1222
|
+
type: string;
|
|
1223
|
+
description: string;
|
|
1224
|
+
};
|
|
1225
|
+
variantABriefDelta: {
|
|
1226
|
+
type: string;
|
|
1227
|
+
description: string;
|
|
1228
|
+
};
|
|
1229
|
+
splitStrategy: {
|
|
1230
|
+
type: string;
|
|
1231
|
+
enum: string[];
|
|
1232
|
+
description: string;
|
|
1233
|
+
};
|
|
1234
|
+
targetCounts: {
|
|
1235
|
+
type: string;
|
|
1236
|
+
properties: {
|
|
1237
|
+
a: {
|
|
1238
|
+
type: string;
|
|
1239
|
+
};
|
|
1240
|
+
b: {
|
|
1241
|
+
type: string;
|
|
1242
|
+
};
|
|
1243
|
+
};
|
|
1244
|
+
additionalProperties: boolean;
|
|
1245
|
+
description: string;
|
|
1246
|
+
};
|
|
1247
|
+
dryRun: {
|
|
1248
|
+
type: string;
|
|
1249
|
+
description: string;
|
|
1250
|
+
};
|
|
1251
|
+
idempotencyKey: {
|
|
1252
|
+
type: string;
|
|
1253
|
+
description: string;
|
|
1254
|
+
};
|
|
1255
|
+
};
|
|
1256
|
+
required: string[];
|
|
1257
|
+
additionalProperties: boolean;
|
|
1258
|
+
};
|
|
1143
1259
|
} | {
|
|
1144
1260
|
name: string;
|
|
1145
1261
|
description: string;
|
package/dist/tools/registry.js
CHANGED
|
@@ -2,6 +2,8 @@ import { authToolDefinitions } from "./auth.js";
|
|
|
2
2
|
import { blueprintCommitToolDefinitions } from "./blueprint-commit.js";
|
|
3
3
|
import { bootstrapToolDefinitions } from "./bootstrap.js";
|
|
4
4
|
import { campaignToolDefinitions } from "./campaigns.js";
|
|
5
|
+
import { campaignAbTestToolDefinitions } from "./campaign-ab-test.js";
|
|
6
|
+
import { campaignMessagePreparationToolDefinitions } from "./campaign-message-preparation.js";
|
|
5
7
|
import { campaignProcessingToolDefinitions } from "./campaign-processing.js";
|
|
6
8
|
import { cellToolDefinitions } from "./cells.js";
|
|
7
9
|
import { startCliLoginToolDef, waitForCliLoginToolDef } from "./cli-login.js";
|
|
@@ -31,6 +33,8 @@ import { verifyRowToolDefinitions } from "./verify-row.js";
|
|
|
31
33
|
import { workspaceToolDefinitions } from "./workspaces.js";
|
|
32
34
|
export const allTools = [
|
|
33
35
|
...campaignToolDefinitions,
|
|
36
|
+
...campaignAbTestToolDefinitions,
|
|
37
|
+
...campaignMessagePreparationToolDefinitions,
|
|
34
38
|
...campaignProcessingToolDefinitions,
|
|
35
39
|
...authToolDefinitions,
|
|
36
40
|
startCliLoginToolDef,
|
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-ab-test
|
|
3
|
+
description: Create a Sellable campaign A/B test from a clean source lead list.
|
|
4
|
+
visibility: public
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- mcp__sellable__get_auth_status
|
|
7
|
+
- mcp__sellable__get_active_workspace
|
|
8
|
+
- mcp__sellable__get_campaign
|
|
9
|
+
- mcp__sellable__get_campaign_context
|
|
10
|
+
- mcp__sellable__get_campaign_navigation_state
|
|
11
|
+
- mcp__sellable__get_campaign_messages_preview
|
|
12
|
+
- mcp__sellable__prepare_campaign_ab_test
|
|
13
|
+
- mcp__sellable__load_csv_linkedin_leads
|
|
14
|
+
- mcp__sellable__wait_for_lead_list_ready
|
|
15
|
+
- mcp__sellable__get_table_rows
|
|
16
|
+
- mcp__sellable__get_rows_minimal
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Sellable Create A/B Test
|
|
20
|
+
|
|
21
|
+
Use this workflow when the user asks to create an A/B campaign test, split a
|
|
22
|
+
campaign into variants, duplicate a campaign for copy testing, or compare two
|
|
23
|
+
campaign-message approaches from the same source list.
|
|
24
|
+
|
|
25
|
+
## Opening Contract
|
|
26
|
+
|
|
27
|
+
Start by identifying the source campaign, the one variable being tested, and
|
|
28
|
+
the clean source lead origin. Then prepare the split and stop for review. The
|
|
29
|
+
goal is two review-copy campaigns with clean split source lists, not a launched
|
|
30
|
+
campaign.
|
|
31
|
+
|
|
32
|
+
## Required Flow
|
|
33
|
+
|
|
34
|
+
1. Confirm the source campaign ID or resolve it with `get_campaign`.
|
|
35
|
+
2. Confirm the A/B variable in plain language. Keep variant B as the explicit
|
|
36
|
+
change and leave variant A as the control unless the user names an A change.
|
|
37
|
+
3. Verify the campaign has a clean source lead list. A clean source list is a
|
|
38
|
+
lead-list table, not the generated campaign workflow table.
|
|
39
|
+
4. Call `prepare_campaign_ab_test` with `dryRun: true` first.
|
|
40
|
+
5. Review split counts, duplicate/skipped counts, campaign names, and variant
|
|
41
|
+
brief deltas with the user.
|
|
42
|
+
6. Only after review, call `prepare_campaign_ab_test` without `dryRun` using the
|
|
43
|
+
same `idempotencyKey` if one was returned or supplied.
|
|
44
|
+
7. Return the A and B campaign IDs, split lead-list IDs, counts, and the
|
|
45
|
+
explicit note that both campaigns are `not_started`.
|
|
46
|
+
|
|
47
|
+
## Anti-Patterns
|
|
48
|
+
|
|
49
|
+
- Do not call `export_table_csv` from an enriched/generated campaign workflow
|
|
50
|
+
table as the lead source for A/B splitting.
|
|
51
|
+
- Do not reimport workflow output columns such as `ICP Score`, `Passes Rubric`,
|
|
52
|
+
`Generate Message`, `Message`, `Approved`, scheduling, status, result, or
|
|
53
|
+
error columns.
|
|
54
|
+
- Do not use broad workflow-table selectors to split decorated campaign rows.
|
|
55
|
+
- Do not call `start_campaign`, approve launch, attach senders for launch, or
|
|
56
|
+
trigger live sends in this workflow.
|
|
57
|
+
|
|
58
|
+
## Clean Source Fallback
|
|
59
|
+
|
|
60
|
+
If the source campaign has no clean source lead list, or the source list is
|
|
61
|
+
polluted with workflow/output columns, ask for the original CSV or clean source
|
|
62
|
+
list. Use `load_csv_linkedin_leads`; it strips Sellable workflow columns if a
|
|
63
|
+
contaminated CSV is supplied, then creates a clean lead list. After the clean
|
|
64
|
+
source is confirmed, retry `prepare_campaign_ab_test`.
|
|
65
|
+
|
|
66
|
+
## Output Contract
|
|
67
|
+
|
|
68
|
+
Report:
|
|
69
|
+
|
|
70
|
+
- source campaign ID
|
|
71
|
+
- source lead-list ID
|
|
72
|
+
- split strategy and counts
|
|
73
|
+
- duplicate/skipped lead counts
|
|
74
|
+
- A review campaign ID and split lead-list ID
|
|
75
|
+
- B review campaign ID and split lead-list ID
|
|
76
|
+
- variant difference
|
|
77
|
+
- `launchState: not_started` for both campaigns
|