@sellable/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +12 -0
- package/.mcp.json +9 -0
- package/README.md +355 -0
- package/dist/api.d.ts +21 -0
- package/dist/api.js +73 -0
- package/dist/auth.d.ts +60 -0
- package/dist/auth.js +246 -0
- package/dist/engage-memory.d.ts +63 -0
- package/dist/engage-memory.js +354 -0
- package/dist/index-dev.d.ts +2 -0
- package/dist/index-dev.js +17 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +499 -0
- package/dist/skills.d.ts +11 -0
- package/dist/skills.js +97 -0
- package/dist/tools/auth.d.ts +30 -0
- package/dist/tools/auth.js +124 -0
- package/dist/tools/blueprint-commit.d.ts +174 -0
- package/dist/tools/blueprint-commit.js +286 -0
- package/dist/tools/bootstrap.d.ts +64 -0
- package/dist/tools/bootstrap.js +246 -0
- package/dist/tools/campaigns.d.ts +589 -0
- package/dist/tools/campaigns.js +892 -0
- package/dist/tools/cells.d.ts +58 -0
- package/dist/tools/cells.js +48 -0
- package/dist/tools/context.d.ts +88 -0
- package/dist/tools/context.js +271 -0
- package/dist/tools/csv-domains.d.ts +73 -0
- package/dist/tools/csv-domains.js +464 -0
- package/dist/tools/csv-linkedin.d.ts +102 -0
- package/dist/tools/csv-linkedin.js +712 -0
- package/dist/tools/direct-campaigns.d.ts +240 -0
- package/dist/tools/direct-campaigns.js +250 -0
- package/dist/tools/engage-bootstrap.d.ts +94 -0
- package/dist/tools/engage-bootstrap.js +205 -0
- package/dist/tools/engage-discovery.d.ts +78 -0
- package/dist/tools/engage-discovery.js +150 -0
- package/dist/tools/engage-memory.d.ts +181 -0
- package/dist/tools/engage-memory.js +143 -0
- package/dist/tools/engage-state.d.ts +72 -0
- package/dist/tools/engage-state.js +62 -0
- package/dist/tools/enrichment.d.ts +167 -0
- package/dist/tools/enrichment.js +174 -0
- package/dist/tools/flow-preflight.d.ts +68 -0
- package/dist/tools/flow-preflight.js +138 -0
- package/dist/tools/framework.d.ts +44 -0
- package/dist/tools/framework.js +153 -0
- package/dist/tools/interaction-mode.d.ts +27 -0
- package/dist/tools/interaction-mode.js +102 -0
- package/dist/tools/leads.d.ts +2417 -0
- package/dist/tools/leads.js +2307 -0
- package/dist/tools/linkedin.d.ts +210 -0
- package/dist/tools/linkedin.js +229 -0
- package/dist/tools/navigation.d.ts +91 -0
- package/dist/tools/navigation.js +381 -0
- package/dist/tools/one-off.d.ts +229 -0
- package/dist/tools/one-off.js +273 -0
- package/dist/tools/processing.d.ts +70 -0
- package/dist/tools/processing.js +56 -0
- package/dist/tools/prompts.d.ts +211 -0
- package/dist/tools/prompts.js +210 -0
- package/dist/tools/provider-preflight.d.ts +21 -0
- package/dist/tools/provider-preflight.js +59 -0
- package/dist/tools/readiness.d.ts +261 -0
- package/dist/tools/readiness.js +510 -0
- package/dist/tools/rows.d.ts +126 -0
- package/dist/tools/rows.js +105 -0
- package/dist/tools/rubrics.d.ts +497 -0
- package/dist/tools/rubrics.js +681 -0
- package/dist/tools/senders.d.ts +44 -0
- package/dist/tools/senders.js +69 -0
- package/dist/tools/sequencer.d.ts +127 -0
- package/dist/tools/sequencer.js +194 -0
- package/dist/tools/tables.d.ts +35 -0
- package/dist/tools/tables.js +36 -0
- package/dist/tools/verify-row.d.ts +36 -0
- package/dist/tools/verify-row.js +38 -0
- package/dist/tools/workspaces.d.ts +140 -0
- package/dist/tools/workspaces.js +139 -0
- package/dist/utils/workspace-root.d.ts +1 -0
- package/dist/utils/workspace-root.js +39 -0
- package/package.json +46 -0
- package/skills/building-gtm-tables/SKILL.md +216 -0
- package/skills/building-gtm-tables/core/auto-execute.yaml +19 -0
- package/skills/building-gtm-tables/core/blueprint-schema.json +72 -0
- package/skills/building-gtm-tables/references/brief-to-blueprint.md +334 -0
- package/skills/building-gtm-tables/references/column-type-catalog.md +318 -0
- package/skills/building-gtm-tables/references/common-blueprints.fixtures.ts +199 -0
- package/skills/building-gtm-tables/references/common-blueprints.md +44 -0
- package/skills/building-gtm-tables/references/failure-taxonomy.md +197 -0
- package/skills/building-gtm-tables/references/uat-seed-prompts.md +37 -0
- package/skills/building-gtm-tables/references/verify-loop.md +74 -0
- package/skills/campaign-messages/SKILL.md +173 -0
- package/skills/campaign-messages/flow.v1.json +75 -0
- package/skills/craft-message/SKILL.md +401 -0
- package/skills/create-campaign/ARCHITECTURE.md +232 -0
- package/skills/create-campaign/DISCUSS.md +296 -0
- package/skills/create-campaign/FLOW_ASCII.md +240 -0
- package/skills/create-campaign/HOST-PARITY-CHECKLIST.md +49 -0
- package/skills/create-campaign/README.md +142 -0
- package/skills/create-campaign/SKILL.md +286 -0
- package/skills/create-campaign/context/README.md +67 -0
- package/skills/create-campaign/context/_TEMPLATE.md +12 -0
- package/skills/create-campaign/context/context.md +35 -0
- package/skills/create-campaign/context/learnings.md +16 -0
- package/skills/create-campaign/context/registry.json +19 -0
- package/skills/create-campaign/core/flow.v1.json +217 -0
- package/skills/create-campaign/core/policy.md +191 -0
- package/skills/create-campaign/core/providers/apollo.json +35 -0
- package/skills/create-campaign/core/providers/prospeo.json +34 -0
- package/skills/create-campaign/core/providers/registry.json +31 -0
- package/skills/create-campaign/core/providers/sales-nav.json +37 -0
- package/skills/create-campaign/core/providers/signal-discovery.json +42 -0
- package/skills/create-campaign/references/brief-template.md +64 -0
- package/skills/create-campaign/references/campaign-quality.md +84 -0
- package/skills/create-campaign/references/copy-calibration-examples.md +120 -0
- package/skills/create-campaign/references/offer-patterns.md +108 -0
- package/skills/create-campaign/references/provider-selection-strategy.md +212 -0
- package/skills/create-campaign/references/question-examples.md +167 -0
- package/skills/create-campaign/references/token-fill-examples.md +81 -0
- package/skills/create-campaign-brief/ARCHITECTURE.md +72 -0
- package/skills/create-campaign-brief/DISCUSS.md +64 -0
- package/skills/create-campaign-brief/README.md +176 -0
- package/skills/create-campaign-brief/SKILL.md +537 -0
- package/skills/create-campaign-brief/references/brief-synthesis-rules.md +100 -0
- package/skills/create-campaign-brief/references/brief-template.md +220 -0
- package/skills/create-campaign-brief/references/campaign-idea-options.md +30 -0
- package/skills/create-campaign-brief/references/copy-appendix-template.md +62 -0
- package/skills/create-campaign-brief/references/draft-lifecycle.md +23 -0
- package/skills/create-campaign-brief/references/examples/MANIFEST.json +89 -0
- package/skills/create-campaign-brief/references/examples/briefs/clover.md +223 -0
- package/skills/create-campaign-brief/references/examples/briefs/galley.md +222 -0
- package/skills/create-campaign-brief/references/examples/briefs/gelee.md +220 -0
- package/skills/create-campaign-brief/references/examples/briefs/hey-digital.md +234 -0
- package/skills/create-campaign-brief/references/examples/briefs/persona.md +231 -0
- package/skills/create-campaign-brief/references/examples/briefs/revvix.md +220 -0
- package/skills/create-campaign-brief/references/examples/briefs/sellable-dev.md +220 -0
- package/skills/create-campaign-brief/references/examples/briefs/superposition.md +233 -0
- package/skills/create-campaign-brief/references/examples/briefs/superpower.md +219 -0
- package/skills/create-campaign-brief/references/examples/briefs/westpark-villas.md +220 -0
- package/skills/create-campaign-brief/references/icp-lock-question-bank.md +43 -0
- package/skills/create-campaign-brief/references/messaging-inputs.md +58 -0
- package/skills/create-campaign-brief/references/output-acceptance-rubric.md +62 -0
- package/skills/create-campaign-brief/references/phase75-active-runtime-message-pack.md +248 -0
- package/skills/create-campaign-brief/references/phase75-canonical-brief-template.md +319 -0
- package/skills/create-campaign-brief/references/phase75-good-brief-and-messaging-examples.md +445 -0
- package/skills/create-campaign-brief/references/quick-research-protocol.md +39 -0
- package/skills/create-campaign-brief/references/reference-sheet-protocol.md +60 -0
- package/skills/create-campaign-brief/references/zero-shot-iteration-rules.md +66 -0
- package/skills/create-campaign-v2/SKILL.md +1619 -0
- package/skills/create-campaign-v2/core/auto-execute.README.md +219 -0
- package/skills/create-campaign-v2/core/auto-execute.yaml +121 -0
- package/skills/create-campaign-v2/core/flow.v2.json +1643 -0
- package/skills/create-campaign-v2/core/policy.md +82 -0
- package/skills/create-campaign-v2/references/ai-tells.md +253 -0
- package/skills/create-campaign-v2/references/approval-gate-framing.md +346 -0
- package/skills/create-campaign-v2/references/draft-lifecycle.md +110 -0
- package/skills/create-campaign-v2/references/escalation-ladder.md +119 -0
- package/skills/create-campaign-v2/references/filter-leads.md +495 -0
- package/skills/create-campaign-v2/references/final-handoff-contract.md +176 -0
- package/skills/create-campaign-v2/references/gold-standard-message-examples.md +394 -0
- package/skills/create-campaign-v2/references/gold-standard-message-patterns.md +314 -0
- package/skills/create-campaign-v2/references/gold-standard-message-validation-example.md +212 -0
- package/skills/create-campaign-v2/references/lead-validation-preview.md +172 -0
- package/skills/create-campaign-v2/references/parallel-critique-protocol.md +368 -0
- package/skills/create-campaign-v2/references/sample-validation-loop.md +289 -0
- package/skills/create-campaign-v2/references/step-13-import-leads.md +151 -0
- package/skills/create-campaign-v2/references/step-15-re-cascade.md +90 -0
- package/skills/create-campaign-v2/references/thomas-revision-filters.md +521 -0
- package/skills/create-campaign-v2/references/thomas-variant-selection.md +202 -0
- package/skills/create-campaign-v2/references/tier-routing-matrix.md +66 -0
- package/skills/create-campaign-v2/references/validation-criteria.md +367 -0
- package/skills/create-campaign-v2/references/watch-link-handoff.md +106 -0
- package/skills/create-campaign-v2-validation/SKILL.md +296 -0
- package/skills/create-post/SKILL.md +1308 -0
- package/skills/create-rubric/SKILL.md +251 -0
- package/skills/engage/SKILL.md +549 -0
- package/skills/engage/core/README.md +23 -0
- package/skills/engage/core/proven-searches.json +11 -0
- package/skills/engage/core/style-guide.template.md +47 -0
- package/skills/engage/core/tracked-people.json +10 -0
- package/skills/enrich-prospects/SKILL.md +97 -0
- package/skills/find-leads/SKILL.md +467 -0
- package/skills/generate-messages/SKILL.md +2361 -0
- package/skills/interview/SKILL.md +132 -0
- package/skills/interview/core/ENGAGE_STYLE_GUIDE.template.md +54 -0
- package/skills/interview/core/ICP.template.md +54 -0
- package/skills/interview/core/VOICE_PROFILE.template.md +101 -0
- package/skills/providers/apollo.md +520 -0
- package/skills/providers/prospeo.md +398 -0
- package/skills/providers/sales-nav.md +372 -0
- package/skills/providers/signal-discovery.md +495 -0
- package/skills/research/SKILL.md +258 -0
- package/skills/research/config.json +9 -0
- package/skills/research/override.md +13 -0
- package/skills/research-prospect/SKILL.md +99 -0
- package/skills/research-sender/SKILL.md +158 -0
- package/skills/workflow-sequences/SKILL.md +85 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { parse } from "csv-parse/sync";
|
|
2
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
3
|
+
import { readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
5
|
+
import { resolveWorkspaceRoot } from "../utils/workspace-root.js";
|
|
6
|
+
const entryPath = process.argv[1] ? resolve(process.argv[1]) : process.cwd();
|
|
7
|
+
const entryDir = dirname(entryPath);
|
|
8
|
+
const defaultWorkspaceRoot = resolveWorkspaceRoot(entryDir);
|
|
9
|
+
export const MAX_CSV_DOMAIN_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
export const MAX_CSV_DOMAIN_UPLOAD_ROWS = 7500;
|
|
11
|
+
export const MAX_CSV_DOMAIN_UPLOAD_UNIQUE_DOMAINS = 1000;
|
|
12
|
+
export const MAX_CSV_DOMAIN_CARRY_COLUMNS = 20;
|
|
13
|
+
const CONFIRMATION_TOKEN_VERSION = "csv-domain-preview-v1";
|
|
14
|
+
const confirmationSecret = randomBytes(32).toString("hex");
|
|
15
|
+
const DOMAIN_REGEX = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i;
|
|
16
|
+
const STRONG_DOMAIN_HEADER_KEYS = new Set([
|
|
17
|
+
"domain",
|
|
18
|
+
"website",
|
|
19
|
+
"companydomain",
|
|
20
|
+
"companywebsite",
|
|
21
|
+
"url",
|
|
22
|
+
"homepage",
|
|
23
|
+
]);
|
|
24
|
+
const CANDIDATE_DOMAIN_HEADER_TOKENS = [
|
|
25
|
+
"domain",
|
|
26
|
+
"website",
|
|
27
|
+
"url",
|
|
28
|
+
"homepage",
|
|
29
|
+
"site",
|
|
30
|
+
];
|
|
31
|
+
const CARRYOVER_JUNK_KEYS = new Set([
|
|
32
|
+
"id",
|
|
33
|
+
"_id",
|
|
34
|
+
"rowid",
|
|
35
|
+
"row_id",
|
|
36
|
+
"createdat",
|
|
37
|
+
"updatedat",
|
|
38
|
+
"timestamp",
|
|
39
|
+
"time",
|
|
40
|
+
"date",
|
|
41
|
+
"created",
|
|
42
|
+
"updated",
|
|
43
|
+
]);
|
|
44
|
+
const CSV_DOMAIN_LIMITS = {
|
|
45
|
+
maxBytes: MAX_CSV_DOMAIN_UPLOAD_BYTES,
|
|
46
|
+
maxRows: MAX_CSV_DOMAIN_UPLOAD_ROWS,
|
|
47
|
+
maxUniqueDomains: MAX_CSV_DOMAIN_UPLOAD_UNIQUE_DOMAINS,
|
|
48
|
+
maxCarryColumns: MAX_CSV_DOMAIN_CARRY_COLUMNS,
|
|
49
|
+
};
|
|
50
|
+
function normalizeHeaderKey(value) {
|
|
51
|
+
return value
|
|
52
|
+
.trim()
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/[^a-z0-9]+/g, "");
|
|
55
|
+
}
|
|
56
|
+
function compareStringArrays(left, right) {
|
|
57
|
+
if (left.length !== right.length) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return left.every((value, index) => value === right[index]);
|
|
61
|
+
}
|
|
62
|
+
function getLimits() {
|
|
63
|
+
return { ...CSV_DOMAIN_LIMITS };
|
|
64
|
+
}
|
|
65
|
+
function resolveCsvDomainPath(filePath, workspaceRoot = defaultWorkspaceRoot) {
|
|
66
|
+
if (!filePath || typeof filePath !== "string") {
|
|
67
|
+
throw new Error("load_csv_domains requires a non-empty filePath.");
|
|
68
|
+
}
|
|
69
|
+
return isAbsolute(filePath)
|
|
70
|
+
? resolve(filePath)
|
|
71
|
+
: resolve(workspaceRoot, filePath);
|
|
72
|
+
}
|
|
73
|
+
function sanitizeCsvDomainCandidate(value) {
|
|
74
|
+
let cleaned = String(value ?? "")
|
|
75
|
+
.trim()
|
|
76
|
+
.toLowerCase();
|
|
77
|
+
if (!cleaned) {
|
|
78
|
+
return { valid: false, reason: "Empty domain value" };
|
|
79
|
+
}
|
|
80
|
+
if (cleaned.includes("@")) {
|
|
81
|
+
return { valid: false, reason: "Email addresses are not allowed" };
|
|
82
|
+
}
|
|
83
|
+
if (/^https?:\/\//.test(cleaned)) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = new URL(cleaned);
|
|
86
|
+
cleaned = parsed.host || cleaned;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
cleaned = cleaned.replace(/^https?:\/\//, "");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
cleaned = cleaned.replace(/^www\./, "");
|
|
93
|
+
cleaned = cleaned.split(/[/?#]/)[0] ?? cleaned;
|
|
94
|
+
cleaned = cleaned.split(":")[0] ?? cleaned;
|
|
95
|
+
cleaned = cleaned.replace(/\.$/, "");
|
|
96
|
+
if (!cleaned) {
|
|
97
|
+
return { valid: false, reason: "Empty domain value" };
|
|
98
|
+
}
|
|
99
|
+
if (cleaned.includes(" ")) {
|
|
100
|
+
return { valid: false, reason: "Contains spaces" };
|
|
101
|
+
}
|
|
102
|
+
if (cleaned === "linkedin.com" ||
|
|
103
|
+
cleaned.endsWith(".linkedin.com") ||
|
|
104
|
+
cleaned.includes("linkedin.com/")) {
|
|
105
|
+
return {
|
|
106
|
+
valid: false,
|
|
107
|
+
reason: "LinkedIn URLs are not allowed; use the company website domain",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (!cleaned.includes(".")) {
|
|
111
|
+
return { valid: false, reason: "Missing top-level domain" };
|
|
112
|
+
}
|
|
113
|
+
if (!DOMAIN_REGEX.test(cleaned)) {
|
|
114
|
+
return { valid: false, reason: "Invalid domain format" };
|
|
115
|
+
}
|
|
116
|
+
return { valid: true, cleaned };
|
|
117
|
+
}
|
|
118
|
+
function findDuplicateHeaders(headers) {
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const duplicates = new Set();
|
|
121
|
+
for (const header of headers) {
|
|
122
|
+
const normalized = header.trim().toLowerCase();
|
|
123
|
+
if (!normalized) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (seen.has(normalized)) {
|
|
127
|
+
duplicates.add(header);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
seen.add(normalized);
|
|
131
|
+
}
|
|
132
|
+
return Array.from(duplicates);
|
|
133
|
+
}
|
|
134
|
+
function buildRowObjects(headers, dataRows) {
|
|
135
|
+
return dataRows.map((row) => {
|
|
136
|
+
const out = {};
|
|
137
|
+
headers.forEach((header, index) => {
|
|
138
|
+
out[header] = String(row[index] ?? "");
|
|
139
|
+
});
|
|
140
|
+
return out;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function resolveHeaderSelection(headers, requested, kind) {
|
|
144
|
+
const trimmed = requested.trim();
|
|
145
|
+
if (!trimmed) {
|
|
146
|
+
throw new Error(`${kind} entries must be non-empty strings.`);
|
|
147
|
+
}
|
|
148
|
+
const exact = headers.find((header) => header === trimmed);
|
|
149
|
+
if (exact) {
|
|
150
|
+
return exact;
|
|
151
|
+
}
|
|
152
|
+
const lowered = trimmed.toLowerCase();
|
|
153
|
+
const matches = headers.filter((header) => header.trim().toLowerCase() === lowered);
|
|
154
|
+
if (matches.length === 1) {
|
|
155
|
+
return matches[0];
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`Unknown CSV column: ${requested}`);
|
|
158
|
+
}
|
|
159
|
+
function getStrongDomainHeaderMatches(headers) {
|
|
160
|
+
return headers.filter((header) => STRONG_DOMAIN_HEADER_KEYS.has(normalizeHeaderKey(header)));
|
|
161
|
+
}
|
|
162
|
+
function getCandidateDomainColumns(headers) {
|
|
163
|
+
const strongMatches = getStrongDomainHeaderMatches(headers);
|
|
164
|
+
if (strongMatches.length > 0) {
|
|
165
|
+
return strongMatches;
|
|
166
|
+
}
|
|
167
|
+
return headers.filter((header) => {
|
|
168
|
+
const normalized = normalizeHeaderKey(header);
|
|
169
|
+
return CANDIDATE_DOMAIN_HEADER_TOKENS.some((token) => normalized.includes(token));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function isCarryoverJunkHeader(header) {
|
|
173
|
+
const normalized = normalizeHeaderKey(header);
|
|
174
|
+
if (!normalized) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (CARRYOVER_JUNK_KEYS.has(normalized)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return (normalized.endsWith("timestamp") ||
|
|
181
|
+
normalized.endsWith("createdat") ||
|
|
182
|
+
normalized.endsWith("updatedat"));
|
|
183
|
+
}
|
|
184
|
+
function getSuggestedCarryoverColumns(headers, domainColumn) {
|
|
185
|
+
return headers
|
|
186
|
+
.filter((header) => header !== domainColumn)
|
|
187
|
+
.filter((header) => !isCarryoverJunkHeader(header))
|
|
188
|
+
.slice(0, MAX_CSV_DOMAIN_CARRY_COLUMNS);
|
|
189
|
+
}
|
|
190
|
+
function makeSignature(payload) {
|
|
191
|
+
return createHmac("sha256", confirmationSecret)
|
|
192
|
+
.update(JSON.stringify(payload))
|
|
193
|
+
.digest("base64url");
|
|
194
|
+
}
|
|
195
|
+
function makeConfirmationEnvelope(payload) {
|
|
196
|
+
return {
|
|
197
|
+
payload,
|
|
198
|
+
signature: makeSignature(payload),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export function makeConfirmationToken(payload) {
|
|
202
|
+
return Buffer.from(JSON.stringify(makeConfirmationEnvelope(payload)), "utf8").toString("base64url");
|
|
203
|
+
}
|
|
204
|
+
export function parseConfirmationToken(token) {
|
|
205
|
+
let decoded;
|
|
206
|
+
try {
|
|
207
|
+
decoded = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
throw new Error("Invalid confirmationToken. Run load_csv_domains preview again.");
|
|
211
|
+
}
|
|
212
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
213
|
+
throw new Error("Invalid confirmationToken. Run load_csv_domains preview again.");
|
|
214
|
+
}
|
|
215
|
+
const envelope = decoded;
|
|
216
|
+
if (!envelope.payload ||
|
|
217
|
+
envelope.payload.version !== CONFIRMATION_TOKEN_VERSION ||
|
|
218
|
+
typeof envelope.signature !== "string") {
|
|
219
|
+
throw new Error("Invalid confirmationToken. Run load_csv_domains preview again.");
|
|
220
|
+
}
|
|
221
|
+
const expectedSignature = makeSignature(envelope.payload);
|
|
222
|
+
if (expectedSignature !== envelope.signature) {
|
|
223
|
+
throw new Error("Preview token is no longer valid. Re-run load_csv_domains preview.");
|
|
224
|
+
}
|
|
225
|
+
return envelope.payload;
|
|
226
|
+
}
|
|
227
|
+
export function matchesConfirmationToken(token, payload) {
|
|
228
|
+
const parsed = parseConfirmationToken(token);
|
|
229
|
+
return (parsed.filePath === payload.filePath &&
|
|
230
|
+
parsed.fileSizeBytes === payload.fileSizeBytes &&
|
|
231
|
+
parsed.fileMtimeMs === payload.fileMtimeMs &&
|
|
232
|
+
parsed.domainColumn === payload.domainColumn &&
|
|
233
|
+
compareStringArrays(parsed.selectedColumns, payload.selectedColumns));
|
|
234
|
+
}
|
|
235
|
+
export function parseCsvDomainFile(input) {
|
|
236
|
+
const resolvedFilePath = resolveCsvDomainPath(input.filePath, input.workspaceRoot);
|
|
237
|
+
const blockingErrors = [];
|
|
238
|
+
const warnings = [];
|
|
239
|
+
let stats;
|
|
240
|
+
try {
|
|
241
|
+
stats = statSync(resolvedFilePath);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
throw new Error(`Could not read CSV file at ${resolvedFilePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
245
|
+
}
|
|
246
|
+
if (!stats.isFile()) {
|
|
247
|
+
throw new Error(`CSV path must point to a file: ${resolvedFilePath}`);
|
|
248
|
+
}
|
|
249
|
+
if (stats.size > MAX_CSV_DOMAIN_UPLOAD_BYTES) {
|
|
250
|
+
blockingErrors.push(`CSV file exceeds the ${MAX_CSV_DOMAIN_UPLOAD_BYTES} byte limit.`);
|
|
251
|
+
}
|
|
252
|
+
let rawRows;
|
|
253
|
+
try {
|
|
254
|
+
const content = readFileSync(resolvedFilePath, "utf8");
|
|
255
|
+
rawRows = parse(content, {
|
|
256
|
+
bom: true,
|
|
257
|
+
columns: false,
|
|
258
|
+
skip_empty_lines: true,
|
|
259
|
+
trim: true,
|
|
260
|
+
relax_column_count: true,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
throw new Error(`Could not parse CSV file at ${resolvedFilePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
265
|
+
}
|
|
266
|
+
if (rawRows.length === 0) {
|
|
267
|
+
blockingErrors.push("CSV file must include a header row.");
|
|
268
|
+
return {
|
|
269
|
+
resolvedFilePath,
|
|
270
|
+
fileSizeBytes: stats.size,
|
|
271
|
+
fileMtimeMs: stats.mtimeMs,
|
|
272
|
+
headers: [],
|
|
273
|
+
rows: [],
|
|
274
|
+
totalRows: 0,
|
|
275
|
+
blockingErrors,
|
|
276
|
+
warnings,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const headers = rawRows[0].map((value) => String(value ?? "").trim());
|
|
280
|
+
const dataRows = rawRows
|
|
281
|
+
.slice(1)
|
|
282
|
+
.map((row) => row.map((value) => String(value ?? "")));
|
|
283
|
+
if (headers.length === 0) {
|
|
284
|
+
blockingErrors.push("CSV file must include at least one header column.");
|
|
285
|
+
}
|
|
286
|
+
const blankHeaderCount = headers.filter((header) => header.length === 0).length;
|
|
287
|
+
if (blankHeaderCount > 0) {
|
|
288
|
+
blockingErrors.push("CSV header row contains blank column names.");
|
|
289
|
+
}
|
|
290
|
+
const duplicateHeaders = findDuplicateHeaders(headers);
|
|
291
|
+
if (duplicateHeaders.length > 0) {
|
|
292
|
+
blockingErrors.push(`CSV header row contains duplicate columns: ${duplicateHeaders.join(", ")}`);
|
|
293
|
+
}
|
|
294
|
+
const mismatchedRows = dataRows.filter((row) => row.length !== headers.length);
|
|
295
|
+
if (mismatchedRows.length > 0) {
|
|
296
|
+
blockingErrors.push(`CSV contains ${mismatchedRows.length} row(s) whose column count does not match the header row.`);
|
|
297
|
+
}
|
|
298
|
+
if (dataRows.length > MAX_CSV_DOMAIN_UPLOAD_ROWS) {
|
|
299
|
+
blockingErrors.push(`CSV contains ${dataRows.length} data rows, which exceeds the ${MAX_CSV_DOMAIN_UPLOAD_ROWS} row limit.`);
|
|
300
|
+
}
|
|
301
|
+
const rows = blockingErrors.length > 0 ? [] : buildRowObjects(headers, dataRows);
|
|
302
|
+
if (rows.length === 0 && dataRows.length === 0) {
|
|
303
|
+
warnings.push("CSV contains headers but no data rows.");
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
resolvedFilePath,
|
|
307
|
+
fileSizeBytes: stats.size,
|
|
308
|
+
fileMtimeMs: stats.mtimeMs,
|
|
309
|
+
headers,
|
|
310
|
+
rows,
|
|
311
|
+
totalRows: dataRows.length,
|
|
312
|
+
blockingErrors,
|
|
313
|
+
warnings,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
export function buildCsvDomainPreview(input) {
|
|
317
|
+
const parsed = parseCsvDomainFile(input);
|
|
318
|
+
const blockingErrors = [...parsed.blockingErrors];
|
|
319
|
+
const warnings = [...parsed.warnings];
|
|
320
|
+
const headers = parsed.headers;
|
|
321
|
+
let resolvedDomainColumn = null;
|
|
322
|
+
const strongMatches = getStrongDomainHeaderMatches(headers);
|
|
323
|
+
if (input.domainColumn) {
|
|
324
|
+
resolvedDomainColumn = resolveHeaderSelection(headers, input.domainColumn, "domainColumn");
|
|
325
|
+
}
|
|
326
|
+
else if (strongMatches.length === 1) {
|
|
327
|
+
resolvedDomainColumn = strongMatches[0];
|
|
328
|
+
}
|
|
329
|
+
else if (strongMatches.length > 1) {
|
|
330
|
+
warnings.push(`Multiple strong domain columns found: ${strongMatches.join(", ")}. Choose one explicitly.`);
|
|
331
|
+
}
|
|
332
|
+
const candidateDomainColumns = getCandidateDomainColumns(headers);
|
|
333
|
+
const suggestedCarryoverColumns = getSuggestedCarryoverColumns(headers, resolvedDomainColumn);
|
|
334
|
+
let resolvedSelectedColumns = [];
|
|
335
|
+
if (input.selectedColumns !== undefined) {
|
|
336
|
+
const seen = new Set();
|
|
337
|
+
resolvedSelectedColumns = input.selectedColumns.map((column) => {
|
|
338
|
+
const resolvedColumn = resolveHeaderSelection(headers, column, "selectedColumns");
|
|
339
|
+
if (resolvedDomainColumn && resolvedColumn === resolvedDomainColumn) {
|
|
340
|
+
throw new Error(`selectedColumns cannot include the chosen domain column (${resolvedDomainColumn}).`);
|
|
341
|
+
}
|
|
342
|
+
if (seen.has(resolvedColumn)) {
|
|
343
|
+
throw new Error(`selectedColumns contains duplicate column selection: ${resolvedColumn}`);
|
|
344
|
+
}
|
|
345
|
+
seen.add(resolvedColumn);
|
|
346
|
+
return resolvedColumn;
|
|
347
|
+
});
|
|
348
|
+
if (resolvedSelectedColumns.length > MAX_CSV_DOMAIN_CARRY_COLUMNS) {
|
|
349
|
+
throw new Error(`selectedColumns cannot contain more than ${MAX_CSV_DOMAIN_CARRY_COLUMNS} columns.`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
resolvedSelectedColumns = suggestedCarryoverColumns;
|
|
354
|
+
}
|
|
355
|
+
const domainRows = parsed.rows;
|
|
356
|
+
let rowsWithChosenDomainValue = 0;
|
|
357
|
+
let duplicateDomainCount = 0;
|
|
358
|
+
let invalidDomainCount = 0;
|
|
359
|
+
let emptySelectedDomainRowCount = 0;
|
|
360
|
+
let uniqueCandidateDomains = 0;
|
|
361
|
+
let truncatedDomainCount = 0;
|
|
362
|
+
const keptCandidateDomains = [];
|
|
363
|
+
const seenDomains = new Set();
|
|
364
|
+
if (!resolvedDomainColumn) {
|
|
365
|
+
warnings.push("No strong domain column could be auto-detected. Re-run with domainColumn.");
|
|
366
|
+
}
|
|
367
|
+
if (resolvedDomainColumn && blockingErrors.length === 0) {
|
|
368
|
+
for (const row of domainRows) {
|
|
369
|
+
const rawValue = row[resolvedDomainColumn] ?? "";
|
|
370
|
+
const trimmedValue = rawValue.trim();
|
|
371
|
+
if (!trimmedValue) {
|
|
372
|
+
emptySelectedDomainRowCount += 1;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
rowsWithChosenDomainValue += 1;
|
|
376
|
+
const sanitized = sanitizeCsvDomainCandidate(trimmedValue);
|
|
377
|
+
if (!sanitized.valid) {
|
|
378
|
+
invalidDomainCount += 1;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (seenDomains.has(sanitized.cleaned)) {
|
|
382
|
+
duplicateDomainCount += 1;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
seenDomains.add(sanitized.cleaned);
|
|
386
|
+
uniqueCandidateDomains += 1;
|
|
387
|
+
if (keptCandidateDomains.length < MAX_CSV_DOMAIN_UPLOAD_UNIQUE_DOMAINS) {
|
|
388
|
+
keptCandidateDomains.push(sanitized.cleaned);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
truncatedDomainCount += 1;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (resolvedDomainColumn && rowsWithChosenDomainValue === 0) {
|
|
396
|
+
blockingErrors.push(`No rows contained a value in the selected domain column (${resolvedDomainColumn}).`);
|
|
397
|
+
}
|
|
398
|
+
if (resolvedDomainColumn && emptySelectedDomainRowCount > 0) {
|
|
399
|
+
blockingErrors.push(`Selected domain column (${resolvedDomainColumn}) contains ${emptySelectedDomainRowCount} empty row value(s).`);
|
|
400
|
+
}
|
|
401
|
+
if (resolvedDomainColumn && uniqueCandidateDomains === 0) {
|
|
402
|
+
blockingErrors.push("No candidate domains could be extracted from the selected domain column.");
|
|
403
|
+
}
|
|
404
|
+
const confirmationToken = resolvedDomainColumn && blockingErrors.length === 0
|
|
405
|
+
? makeConfirmationToken({
|
|
406
|
+
version: CONFIRMATION_TOKEN_VERSION,
|
|
407
|
+
filePath: parsed.resolvedFilePath,
|
|
408
|
+
fileSizeBytes: parsed.fileSizeBytes,
|
|
409
|
+
fileMtimeMs: parsed.fileMtimeMs,
|
|
410
|
+
domainColumn: resolvedDomainColumn,
|
|
411
|
+
selectedColumns: resolvedSelectedColumns,
|
|
412
|
+
})
|
|
413
|
+
: null;
|
|
414
|
+
return {
|
|
415
|
+
preview: {
|
|
416
|
+
resolvedFilePath: parsed.resolvedFilePath,
|
|
417
|
+
headers,
|
|
418
|
+
detectedDomainColumn: resolvedDomainColumn,
|
|
419
|
+
candidateDomainColumns,
|
|
420
|
+
needsDomainColumnSelection: resolvedDomainColumn === null,
|
|
421
|
+
suggestedCarryoverColumns,
|
|
422
|
+
selectedColumns: resolvedSelectedColumns,
|
|
423
|
+
totalRows: parsed.totalRows,
|
|
424
|
+
rowsWithChosenDomainValue,
|
|
425
|
+
uniqueCandidateDomains,
|
|
426
|
+
duplicateDomainCount,
|
|
427
|
+
invalidDomainCount,
|
|
428
|
+
truncatedDomainCount,
|
|
429
|
+
emptySelectedDomainRowCount,
|
|
430
|
+
duplicatePolicy: "last_wins",
|
|
431
|
+
limits: getLimits(),
|
|
432
|
+
warnings,
|
|
433
|
+
blockingErrors,
|
|
434
|
+
confirmationToken,
|
|
435
|
+
},
|
|
436
|
+
fileSizeBytes: parsed.fileSizeBytes,
|
|
437
|
+
fileMtimeMs: parsed.fileMtimeMs,
|
|
438
|
+
resolvedDomainColumn,
|
|
439
|
+
parsedRows: domainRows,
|
|
440
|
+
keptCandidateDomains,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
export function projectCsvCarryRows(previewBuild) {
|
|
444
|
+
if (!previewBuild.resolvedDomainColumn) {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
const allowedDomains = new Set(previewBuild.keptCandidateDomains);
|
|
448
|
+
const columns = [
|
|
449
|
+
previewBuild.resolvedDomainColumn,
|
|
450
|
+
...previewBuild.preview.selectedColumns,
|
|
451
|
+
];
|
|
452
|
+
return previewBuild.parsedRows
|
|
453
|
+
.filter((row) => {
|
|
454
|
+
const sanitized = sanitizeCsvDomainCandidate(row[previewBuild.resolvedDomainColumn ?? ""] ?? "");
|
|
455
|
+
return sanitized.valid && allowedDomains.has(sanitized.cleaned);
|
|
456
|
+
})
|
|
457
|
+
.map((row) => {
|
|
458
|
+
const projected = {};
|
|
459
|
+
for (const column of columns) {
|
|
460
|
+
projected[column] = row[column] ?? "";
|
|
461
|
+
}
|
|
462
|
+
return projected;
|
|
463
|
+
});
|
|
464
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { SellableApi } from "../api.js";
|
|
2
|
+
export declare const MAX_CSV_LINKEDIN_UPLOAD_BYTES: number;
|
|
3
|
+
export declare const MAX_CSV_LINKEDIN_UPLOAD_ROWS = 7500;
|
|
4
|
+
export declare const MAX_CSV_LINKEDIN_UPLOAD_CARRY_COLUMNS = 20;
|
|
5
|
+
declare const CONFIRMATION_TOKEN_VERSION = "csv-linkedin-preview-v1";
|
|
6
|
+
type StandardLeadListColumn = "Name" | "Title" | "Company Website" | "Location";
|
|
7
|
+
export type CsvLinkedinLimits = {
|
|
8
|
+
maxBytes: number;
|
|
9
|
+
maxRows: number;
|
|
10
|
+
maxCarryColumns: number;
|
|
11
|
+
};
|
|
12
|
+
export type CsvLinkedinPreview = {
|
|
13
|
+
resolvedFilePath: string;
|
|
14
|
+
suggestedLeadListName: string;
|
|
15
|
+
headers: string[];
|
|
16
|
+
detectedLinkedInColumn: string | null;
|
|
17
|
+
candidateLinkedInColumns: string[];
|
|
18
|
+
needsLinkedInColumnSelection: boolean;
|
|
19
|
+
standardMappings: Partial<Record<StandardLeadListColumn, string | string[]>>;
|
|
20
|
+
customColumnTargets: string[];
|
|
21
|
+
suggestedCarryoverColumns: string[];
|
|
22
|
+
selectedColumns: string[];
|
|
23
|
+
totalRows: number;
|
|
24
|
+
validLinkedInRowCount: number;
|
|
25
|
+
invalidLinkedInRowCount: number;
|
|
26
|
+
duplicateLinkedInCount: number;
|
|
27
|
+
emptyLinkedInRowCount: number;
|
|
28
|
+
duplicatePolicy: "first_wins";
|
|
29
|
+
limits: CsvLinkedinLimits;
|
|
30
|
+
warnings: string[];
|
|
31
|
+
blockingErrors: string[];
|
|
32
|
+
confirmationToken: string | null;
|
|
33
|
+
};
|
|
34
|
+
export type CsvLinkedinConfirmationPayload = {
|
|
35
|
+
version: typeof CONFIRMATION_TOKEN_VERSION;
|
|
36
|
+
filePath: string;
|
|
37
|
+
fileSizeBytes: number;
|
|
38
|
+
fileMtimeMs: number;
|
|
39
|
+
linkedInColumn: string;
|
|
40
|
+
selectedColumns: string[];
|
|
41
|
+
};
|
|
42
|
+
export type PreparedLinkedinLeadRow = {
|
|
43
|
+
row: Record<string, string>;
|
|
44
|
+
normalizedLinkedInUrl: string;
|
|
45
|
+
};
|
|
46
|
+
export type CsvLinkedinPreviewBuild = {
|
|
47
|
+
preview: CsvLinkedinPreview;
|
|
48
|
+
fileSizeBytes: number;
|
|
49
|
+
fileMtimeMs: number;
|
|
50
|
+
resolvedLinkedInColumn: string | null;
|
|
51
|
+
parsedRows: Array<Record<string, string>>;
|
|
52
|
+
preparedRows: PreparedLinkedinLeadRow[];
|
|
53
|
+
};
|
|
54
|
+
export type CsvLinkedinUploadResult = {
|
|
55
|
+
bucket: string;
|
|
56
|
+
objectPath: string;
|
|
57
|
+
token: string;
|
|
58
|
+
signedUrl: string;
|
|
59
|
+
originalFilename: string;
|
|
60
|
+
contentType: string;
|
|
61
|
+
fileSizeBytes: number;
|
|
62
|
+
};
|
|
63
|
+
type ParseCsvLinkedinFileInput = {
|
|
64
|
+
filePath: string;
|
|
65
|
+
workspaceRoot?: string;
|
|
66
|
+
};
|
|
67
|
+
type BuildCsvLinkedinPreviewInput = ParseCsvLinkedinFileInput & {
|
|
68
|
+
linkedInColumn?: string;
|
|
69
|
+
selectedColumns?: string[];
|
|
70
|
+
};
|
|
71
|
+
type ParsedCsvLinkedinFile = {
|
|
72
|
+
resolvedFilePath: string;
|
|
73
|
+
suggestedLeadListName: string;
|
|
74
|
+
fileSizeBytes: number;
|
|
75
|
+
fileMtimeMs: number;
|
|
76
|
+
headers: string[];
|
|
77
|
+
rows: Array<Record<string, string>>;
|
|
78
|
+
totalRows: number;
|
|
79
|
+
blockingErrors: string[];
|
|
80
|
+
warnings: string[];
|
|
81
|
+
};
|
|
82
|
+
export declare function makeLinkedinConfirmationToken(payload: CsvLinkedinConfirmationPayload): string;
|
|
83
|
+
export declare function parseLinkedinConfirmationToken(token: string): CsvLinkedinConfirmationPayload;
|
|
84
|
+
export declare function matchesLinkedinConfirmationToken(token: string, payload: CsvLinkedinConfirmationPayload): boolean;
|
|
85
|
+
export declare function parseCsvLinkedinFile(input: ParseCsvLinkedinFileInput): ParsedCsvLinkedinFile;
|
|
86
|
+
export declare function buildCsvLinkedinPreview(input: BuildCsvLinkedinPreviewInput): CsvLinkedinPreviewBuild;
|
|
87
|
+
export declare function uploadCsvLinkedinFile(params: {
|
|
88
|
+
api: Pick<SellableApi, "post">;
|
|
89
|
+
leadListId: string;
|
|
90
|
+
filePath: string;
|
|
91
|
+
workspaceRoot?: string;
|
|
92
|
+
fileSizeBytes?: number;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
originalFilename: string;
|
|
95
|
+
contentType: string;
|
|
96
|
+
fileSizeBytes: number;
|
|
97
|
+
bucket: string;
|
|
98
|
+
objectPath: string;
|
|
99
|
+
token: string;
|
|
100
|
+
signedUrl: string;
|
|
101
|
+
}>;
|
|
102
|
+
export {};
|