salesprompter-cli 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,368 @@
1
+ const COMPANY_SUFFIX_PATTERNS = [
2
+ /\s+GmbH\s*&\s*Co\.?\s+KG\.?$/i,
3
+ /\s+GmbH\.?$/i,
4
+ /\s+AG\.?$/i,
5
+ /\s+SE\.?$/i,
6
+ /\s+KG\.?$/i,
7
+ /\s+OHG\.?$/i,
8
+ /\s+GbR\.?$/i,
9
+ /\s+S\.A\.?$/i,
10
+ /\s+SA\.?$/i,
11
+ /\s+SARL\.?$/i,
12
+ /\s+LLC\.?$/i,
13
+ /\s+Inc\.?$/i,
14
+ /\s+Corp\.?$/i,
15
+ /\s+Ltd\.?$/i,
16
+ /\s+Group$/i
17
+ ];
18
+ const DACH_SIGNAL_MATCHERS = [
19
+ { label: "Germany", pattern: /(^|[^a-z])(germany|deutschland|bundesrepublik)([^a-z]|$)/i },
20
+ { label: "Austria", pattern: /(^|[^a-z])(austria|österreich|oesterreich)([^a-z]|$)/i },
21
+ { label: "Switzerland", pattern: /(^|[^a-z])(switzerland|schweiz|suisse|svizzera)([^a-z]|$)/i },
22
+ { label: "Berlin", pattern: /(^|[^a-z])berlin([^a-z]|$)/i },
23
+ { label: "Hamburg", pattern: /(^|[^a-z])hamburg([^a-z]|$)/i },
24
+ { label: "Munich", pattern: /(^|[^a-z])(munich|münchen|muenchen)([^a-z]|$)/i },
25
+ { label: "Frankfurt", pattern: /(^|[^a-z])frankfurt([^a-z]|$)/i },
26
+ { label: "Cologne", pattern: /(^|[^a-z])(cologne|köln|koeln)([^a-z]|$)/i },
27
+ { label: "Stuttgart", pattern: /(^|[^a-z])stuttgart([^a-z]|$)/i },
28
+ { label: "Düsseldorf", pattern: /(^|[^a-z])(düsseldorf|duesseldorf)([^a-z]|$)/i },
29
+ { label: "Vienna", pattern: /(^|[^a-z])(vienna|wien)([^a-z]|$)/i },
30
+ { label: "Graz", pattern: /(^|[^a-z])graz([^a-z]|$)/i },
31
+ { label: "Linz", pattern: /(^|[^a-z])linz([^a-z]|$)/i },
32
+ { label: "Salzburg", pattern: /(^|[^a-z])salzburg([^a-z]|$)/i },
33
+ { label: "Zurich", pattern: /(^|[^a-z])(zurich|zürich|zuerich)([^a-z]|$)/i },
34
+ { label: "Geneva", pattern: /(^|[^a-z])(geneva|genf)([^a-z]|$)/i },
35
+ { label: "Basel", pattern: /(^|[^a-z])basel([^a-z]|$)/i },
36
+ { label: "Bern", pattern: /(^|[^a-z])bern([^a-z]|$)/i },
37
+ { label: "Lausanne", pattern: /(^|[^a-z])lausanne([^a-z]|$)/i },
38
+ { label: "Zug", pattern: /(^|[^a-z])zug([^a-z]|$)/i }
39
+ ];
40
+ const DEEL_RELEVANT_TITLE_MATCHERS = [
41
+ /\bhead of (hr|human resources|people|people operations|people ops|people services|payroll|total rewards)\b/i,
42
+ /\b(head of compensation|head of benefits|head of people and culture)\b/i,
43
+ /\b(chief people officer|chro)\b/i,
44
+ /\b(vp|vice president)\b.*\b(hr|human resources|people|payroll|total rewards|compensation|benefits)\b/i,
45
+ /\bdirector\b.*\b(hr|human resources|people|payroll|total rewards|compensation|benefits|global mobility)\b/i,
46
+ /\b(hr|human resources|people|payroll)\b.*\bdirector\b/i,
47
+ /\b(hr business partner|hrbp)\b/i,
48
+ /\b(people operations manager|people ops manager|hr operations manager|payroll manager|benefits manager|compensation manager|global mobility manager)\b/i,
49
+ /\b(global payroll|global mobility|compensation and benefits)\b/i,
50
+ /\b(leiter personal|leiter hr|leiter human resources|personalleitung|personalleitung|bereichsleiter personal)\b/i
51
+ ];
52
+ const DEEL_TITLE_EXCLUSION_MATCHERS = [
53
+ /\bassistant\b/i,
54
+ /\bassistent/i,
55
+ /\brecruit/i,
56
+ /\btalent acquisition\b/i,
57
+ /\bhead chef\b/i,
58
+ /\bceo\b/i,
59
+ /\bchief executive officer\b/i,
60
+ /\bfounder\b/i,
61
+ /\bco-founder\b/i,
62
+ /\bpresident\b/i,
63
+ /\bhead of sales\b/i,
64
+ /\bmarketing\b/i
65
+ ];
66
+ function normalizeNullableString(value) {
67
+ const trimmed = value?.trim() ?? "";
68
+ return trimmed.length > 0 ? trimmed : null;
69
+ }
70
+ export function cleanDeelCompanyName(value) {
71
+ let next = normalizeNullableString(value);
72
+ if (!next) {
73
+ return null;
74
+ }
75
+ for (const pattern of COMPANY_SUFFIX_PATTERNS) {
76
+ next = next.replace(pattern, "").trim();
77
+ }
78
+ return next.length > 0 ? next : normalizeNullableString(value);
79
+ }
80
+ export function isDeelRelevantSalesNavTitle(value) {
81
+ const normalized = normalizeNullableString(value);
82
+ if (!normalized) {
83
+ return false;
84
+ }
85
+ if (DEEL_TITLE_EXCLUSION_MATCHERS.some((pattern) => pattern.test(normalized))) {
86
+ return false;
87
+ }
88
+ return DEEL_RELEVANT_TITLE_MATCHERS.some((pattern) => pattern.test(normalized));
89
+ }
90
+ export function extractLinkedInCompanyHandleFromUrl(value) {
91
+ const normalized = normalizeNullableString(value);
92
+ if (!normalized) {
93
+ return null;
94
+ }
95
+ try {
96
+ const url = new URL(normalized);
97
+ if (!/(^|\.)linkedin\.com$/i.test(url.hostname)) {
98
+ return null;
99
+ }
100
+ const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
101
+ const companyIndex = segments.findIndex((segment) => segment.toLowerCase() === "company");
102
+ const handle = companyIndex >= 0 ? segments[companyIndex + 1] ?? "" : "";
103
+ return handle.trim().length > 0 ? handle.trim().toLowerCase() : null;
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ function preferredProfileUrl(row) {
110
+ return (normalizeNullableString(row.linkedin_profile_url) ??
111
+ normalizeNullableString(row.default_profile_url) ??
112
+ row.sales_nav_profile_url);
113
+ }
114
+ function collectDachSignals(value) {
115
+ if (!value) {
116
+ return [];
117
+ }
118
+ const matches = [];
119
+ for (const matcher of DACH_SIGNAL_MATCHERS) {
120
+ if (matcher.pattern.test(value)) {
121
+ matches.push(matcher.label);
122
+ }
123
+ }
124
+ return matches;
125
+ }
126
+ export function classifyDeelSalesNavLanguage(row) {
127
+ const signalSources = [
128
+ { field: "location", value: normalizeNullableString(row.location) },
129
+ { field: "companyLocation", value: normalizeNullableString(row.company_location) },
130
+ { field: "searchQuery", value: normalizeNullableString(row.search_query) }
131
+ ];
132
+ const languageSignals = [];
133
+ const signalFields = [];
134
+ for (const source of signalSources) {
135
+ const sourceSignals = collectDachSignals(source.value);
136
+ if (sourceSignals.length > 0) {
137
+ languageSignals.push(...sourceSignals);
138
+ signalFields.push(source.field);
139
+ }
140
+ }
141
+ const uniqueSignals = [...new Set(languageSignals)];
142
+ const uniqueFields = [...new Set(signalFields)];
143
+ if (uniqueSignals.length > 0) {
144
+ return {
145
+ language: "de",
146
+ marketSegment: "dach",
147
+ languageReason: `clear_dach_signal:${uniqueSignals.join("|")}`,
148
+ languageSignals: uniqueSignals,
149
+ signalFields: uniqueFields
150
+ };
151
+ }
152
+ return {
153
+ language: "en",
154
+ marketSegment: "non-dach",
155
+ languageReason: "fallback_non_dach",
156
+ languageSignals: [],
157
+ signalFields: []
158
+ };
159
+ }
160
+ export function normalizeDeelSalesNavRow(row) {
161
+ const classification = classifyDeelSalesNavLanguage(row);
162
+ return {
163
+ id: row.id,
164
+ orgId: row.org_id,
165
+ runId: normalizeNullableString(row.run_id),
166
+ language: classification.language,
167
+ marketSegment: classification.marketSegment,
168
+ languageReason: classification.languageReason,
169
+ languageSignals: classification.languageSignals,
170
+ signalFields: classification.signalFields,
171
+ fullName: normalizeNullableString(row.full_name),
172
+ firstName: normalizeNullableString(row.first_name),
173
+ lastName: normalizeNullableString(row.last_name),
174
+ title: normalizeNullableString(row.title),
175
+ industry: normalizeNullableString(row.industry),
176
+ companyName: normalizeNullableString(row.company_name),
177
+ companyNameCleaned: cleanDeelCompanyName(row.company_name),
178
+ location: normalizeNullableString(row.location),
179
+ companyLocation: normalizeNullableString(row.company_location),
180
+ preferredProfileUrl: preferredProfileUrl(row),
181
+ linkedinProfileUrl: normalizeNullableString(row.linkedin_profile_url),
182
+ defaultProfileUrl: normalizeNullableString(row.default_profile_url),
183
+ salesNavProfileUrl: row.sales_nav_profile_url,
184
+ companyUrl: normalizeNullableString(row.company_url),
185
+ regularCompanyUrl: normalizeNullableString(row.regular_company_url),
186
+ companyLinkedInHandle: extractLinkedInCompanyHandleFromUrl(row.company_url) ??
187
+ extractLinkedInCompanyHandleFromUrl(row.regular_company_url),
188
+ searchQuery: normalizeNullableString(row.search_query),
189
+ scrapedAt: normalizeNullableString(row.scraped_at)
190
+ };
191
+ }
192
+ function percentage(count, total) {
193
+ if (total <= 0) {
194
+ return 0;
195
+ }
196
+ return Number(((count / total) * 100).toFixed(2));
197
+ }
198
+ export function buildDeelSalesNavPack(orgId, rows) {
199
+ const locales = { de: [], en: [] };
200
+ const titleCounts = new Map();
201
+ const signalFieldCounts = {
202
+ location: 0,
203
+ companyLocation: 0,
204
+ searchQuery: 0,
205
+ none: 0
206
+ };
207
+ const fieldCounts = {
208
+ firstName: 0,
209
+ lastName: 0,
210
+ fullName: 0,
211
+ companyName: 0,
212
+ companyNameCleaned: 0,
213
+ preferredProfileUrl: 0,
214
+ linkedinProfileUrl: 0,
215
+ companyLinkedInHandle: 0,
216
+ location: 0,
217
+ companyLocation: 0,
218
+ searchQuery: 0
219
+ };
220
+ for (const row of rows) {
221
+ const prepared = normalizeDeelSalesNavRow(row);
222
+ locales[prepared.language].push(prepared);
223
+ if (prepared.title) {
224
+ titleCounts.set(prepared.title, (titleCounts.get(prepared.title) ?? 0) + 1);
225
+ }
226
+ if (prepared.signalFields.length === 0) {
227
+ signalFieldCounts.none += 1;
228
+ }
229
+ else {
230
+ for (const field of prepared.signalFields) {
231
+ if (field === "location") {
232
+ signalFieldCounts.location += 1;
233
+ }
234
+ else if (field === "companyLocation") {
235
+ signalFieldCounts.companyLocation += 1;
236
+ }
237
+ else if (field === "searchQuery") {
238
+ signalFieldCounts.searchQuery += 1;
239
+ }
240
+ }
241
+ }
242
+ if (prepared.firstName)
243
+ fieldCounts.firstName += 1;
244
+ if (prepared.lastName)
245
+ fieldCounts.lastName += 1;
246
+ if (prepared.fullName)
247
+ fieldCounts.fullName += 1;
248
+ if (prepared.companyName)
249
+ fieldCounts.companyName += 1;
250
+ if (prepared.companyNameCleaned)
251
+ fieldCounts.companyNameCleaned += 1;
252
+ if (prepared.preferredProfileUrl)
253
+ fieldCounts.preferredProfileUrl += 1;
254
+ if (prepared.linkedinProfileUrl)
255
+ fieldCounts.linkedinProfileUrl += 1;
256
+ if (prepared.companyLinkedInHandle)
257
+ fieldCounts.companyLinkedInHandle += 1;
258
+ if (prepared.location)
259
+ fieldCounts.location += 1;
260
+ if (prepared.companyLocation)
261
+ fieldCounts.companyLocation += 1;
262
+ if (prepared.searchQuery)
263
+ fieldCounts.searchQuery += 1;
264
+ }
265
+ const total = rows.length;
266
+ const localeCounts = {
267
+ de: locales.de.length,
268
+ en: locales.en.length
269
+ };
270
+ const fieldCoverage = {
271
+ firstName: { count: fieldCounts.firstName, percentage: percentage(fieldCounts.firstName, total) },
272
+ lastName: { count: fieldCounts.lastName, percentage: percentage(fieldCounts.lastName, total) },
273
+ fullName: { count: fieldCounts.fullName, percentage: percentage(fieldCounts.fullName, total) },
274
+ companyName: { count: fieldCounts.companyName, percentage: percentage(fieldCounts.companyName, total) },
275
+ companyNameCleaned: { count: fieldCounts.companyNameCleaned, percentage: percentage(fieldCounts.companyNameCleaned, total) },
276
+ preferredProfileUrl: { count: fieldCounts.preferredProfileUrl, percentage: percentage(fieldCounts.preferredProfileUrl, total) },
277
+ linkedinProfileUrl: { count: fieldCounts.linkedinProfileUrl, percentage: percentage(fieldCounts.linkedinProfileUrl, total) },
278
+ companyLinkedInHandle: { count: fieldCounts.companyLinkedInHandle, percentage: percentage(fieldCounts.companyLinkedInHandle, total) },
279
+ location: { count: fieldCounts.location, percentage: percentage(fieldCounts.location, total) },
280
+ companyLocation: { count: fieldCounts.companyLocation, percentage: percentage(fieldCounts.companyLocation, total) },
281
+ searchQuery: { count: fieldCounts.searchQuery, percentage: percentage(fieldCounts.searchQuery, total) }
282
+ };
283
+ return {
284
+ vendor: "deel",
285
+ source: "salesnav-supabase",
286
+ orgId,
287
+ total,
288
+ locales,
289
+ summary: {
290
+ localeCounts,
291
+ localePercentages: {
292
+ de: percentage(localeCounts.de, total),
293
+ en: percentage(localeCounts.en, total)
294
+ },
295
+ fieldCoverage,
296
+ signalFieldCounts,
297
+ topTitles: [...titleCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)
298
+ }
299
+ };
300
+ }
301
+ function csvEscape(value) {
302
+ const normalized = value ?? "";
303
+ return `"${normalized.replaceAll('"', '""')}"`;
304
+ }
305
+ export function buildDeelSalesNavCsvHeader() {
306
+ return [
307
+ "id",
308
+ "orgId",
309
+ "runId",
310
+ "language",
311
+ "marketSegment",
312
+ "languageReason",
313
+ "languageSignals",
314
+ "signalFields",
315
+ "fullName",
316
+ "firstName",
317
+ "lastName",
318
+ "title",
319
+ "industry",
320
+ "companyName",
321
+ "companyNameCleaned",
322
+ "location",
323
+ "companyLocation",
324
+ "preferredProfileUrl",
325
+ "linkedinProfileUrl",
326
+ "defaultProfileUrl",
327
+ "salesNavProfileUrl",
328
+ "companyUrl",
329
+ "regularCompanyUrl",
330
+ "companyLinkedInHandle",
331
+ "searchQuery",
332
+ "scrapedAt"
333
+ ].join(",");
334
+ }
335
+ export function buildDeelSalesNavCsvLines(rows) {
336
+ return rows
337
+ .map((row) => [
338
+ row.id,
339
+ row.orgId,
340
+ row.runId ?? "",
341
+ row.language,
342
+ row.marketSegment,
343
+ row.languageReason,
344
+ row.languageSignals.join("|"),
345
+ row.signalFields.join("|"),
346
+ row.fullName ?? "",
347
+ row.firstName ?? "",
348
+ row.lastName ?? "",
349
+ row.title ?? "",
350
+ row.industry ?? "",
351
+ row.companyName ?? "",
352
+ row.companyNameCleaned ?? "",
353
+ row.location ?? "",
354
+ row.companyLocation ?? "",
355
+ row.preferredProfileUrl,
356
+ row.linkedinProfileUrl ?? "",
357
+ row.defaultProfileUrl ?? "",
358
+ row.salesNavProfileUrl,
359
+ row.companyUrl ?? "",
360
+ row.regularCompanyUrl ?? "",
361
+ row.companyLinkedInHandle ?? "",
362
+ row.searchQuery ?? "",
363
+ row.scrapedAt ?? ""
364
+ ]
365
+ .map(csvEscape)
366
+ .join(","))
367
+ .join("\n");
368
+ }
@@ -6,7 +6,7 @@ function marketCountries(market) {
6
6
  if (market === "europe") {
7
7
  return ["DE", "AT", "CH", "NL", "GB", "FR", "SE", "DK"];
8
8
  }
9
- return ["DE", "AT", "CH", "NL", "GB", "FR", "SE", "DK", "US"];
9
+ return [];
10
10
  }
11
11
  function sqlStringList(values) {
12
12
  return values.map((value) => `'${value.replaceAll("'", "\\'")}'`).join(", ");
@@ -54,7 +54,8 @@ export function buildDirectPathLeadExportSql(vendor, market, limit) {
54
54
  if (vendor !== "deel") {
55
55
  throw new Error(`Unsupported direct-path vendor: ${vendor}`);
56
56
  }
57
- const countries = sqlStringList(marketCountries(market));
57
+ const countries = marketCountries(market);
58
+ const countryClause = countries.length > 0 ? `co.countryCode IN (${sqlStringList(countries)})` : "TRUE";
58
59
  const broadTitlePredicate = broadTitlePredicateSql();
59
60
  const titleExclusions = titleExclusionPredicateSql();
60
61
  return `WITH hr_leadlists AS (
@@ -129,7 +130,7 @@ SELECT
129
130
  FROM contacts c
130
131
  JOIN hr_leadlists l ON c.leadListId = l.leadListId
131
132
  LEFT JOIN companies co ON c.companyId = co.companyId
132
- WHERE co.countryCode IN (${countries})
133
+ WHERE ${countryClause}
133
134
  AND co.domain IS NOT NULL
134
135
  AND co.companySize IN ('200-499', '501-1000', '1001-5000', '5001-10.000', '10.000+')
135
136
  AND (
@@ -140,7 +141,7 @@ QUALIFY ROW_NUMBER() OVER (PARTITION BY c.contactId ORDER BY co.company_ts DESC)
140
141
  ORDER BY co.company_ts DESC
141
142
  LIMIT ${limit}`;
142
143
  }
143
- function classifySegment(title) {
144
+ export function classifyDirectPathSegment(title) {
144
145
  const normalized = title.toLowerCase();
145
146
  if (normalized.includes("payroll") ||
146
147
  normalized.includes("people services") ||
@@ -275,7 +276,7 @@ export function segmentDirectPathRows(vendor, market, rows) {
275
276
  };
276
277
  for (const row of rows) {
277
278
  const title = row.jobTitle ?? "";
278
- const { segment, reason } = classifySegment(title);
279
+ const { segment, reason } = classifyDirectPathSegment(title);
279
280
  segments[segment].push({
280
281
  ...row,
281
282
  segment,
package/dist/domain.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ export const LeadCustomVariableValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
3
+ export const LeadCustomVariablesSchema = z.record(z.string(), LeadCustomVariableValueSchema);
2
4
  export const IcpSchema = z.object({
3
5
  name: z.string().min(1),
4
6
  description: z.string().default(""),
@@ -33,6 +35,7 @@ export const LeadSchema = z.object({
33
35
  email: z.string().email(),
34
36
  source: z.string().min(1),
35
37
  signals: z.array(z.string().min(1)).default([]),
38
+ customVariables: LeadCustomVariablesSchema.optional(),
36
39
  });
37
40
  export const EnrichedLeadSchema = LeadSchema.extend({
38
41
  techStack: z.array(z.string().min(1)).default([]),