salesprompter-cli 0.1.17 → 0.1.18
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 +1 -0
- package/dist/bigquery.js +9 -4
- package/dist/cli.js +1179 -305
- package/dist/direct-path.js +310 -0
- package/dist/linkedin-products.js +715 -0
- package/dist/sales-navigator.js +475 -0
- package/package.json +2 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { parseHistoricalQuery } from "./historical-queries.js";
|
|
2
|
+
function marketCountries(market) {
|
|
3
|
+
if (market === "dach") {
|
|
4
|
+
return ["DE", "AT", "CH"];
|
|
5
|
+
}
|
|
6
|
+
if (market === "europe") {
|
|
7
|
+
return ["DE", "AT", "CH", "NL", "GB", "FR", "SE", "DK"];
|
|
8
|
+
}
|
|
9
|
+
return ["DE", "AT", "CH", "NL", "GB", "FR", "SE", "DK", "US"];
|
|
10
|
+
}
|
|
11
|
+
function sqlStringList(values) {
|
|
12
|
+
return values.map((value) => `'${value.replaceAll("'", "\\'")}'`).join(", ");
|
|
13
|
+
}
|
|
14
|
+
function broadTitlePredicateSql() {
|
|
15
|
+
return [
|
|
16
|
+
"LOWER(c.jobTitle) LIKE '%head of hr%'",
|
|
17
|
+
"LOWER(c.jobTitle) LIKE '%head of human resources%'",
|
|
18
|
+
"LOWER(c.jobTitle) LIKE '%head of people%'",
|
|
19
|
+
"LOWER(c.jobTitle) LIKE '%head of payroll%'",
|
|
20
|
+
"LOWER(c.jobTitle) LIKE '%people services%'",
|
|
21
|
+
"LOWER(c.jobTitle) LIKE '%shared services%'",
|
|
22
|
+
"LOWER(c.jobTitle) LIKE '%total rewards%'",
|
|
23
|
+
"LOWER(c.jobTitle) LIKE '%compensation%'",
|
|
24
|
+
"LOWER(c.jobTitle) LIKE '%benefits%'",
|
|
25
|
+
"LOWER(c.jobTitle) LIKE '%hr business partner%'",
|
|
26
|
+
"LOWER(c.jobTitle) LIKE '%hrbp%'",
|
|
27
|
+
"LOWER(c.jobTitle) LIKE '%people operations%'",
|
|
28
|
+
"LOWER(c.jobTitle) LIKE '%people ops%'",
|
|
29
|
+
"LOWER(c.jobTitle) LIKE '%hr operations%'",
|
|
30
|
+
"LOWER(c.jobTitle) LIKE '%hr administration%'",
|
|
31
|
+
"LOWER(c.jobTitle) LIKE '%hr director%'",
|
|
32
|
+
"LOWER(c.jobTitle) LIKE '%director hr%'",
|
|
33
|
+
"LOWER(c.jobTitle) LIKE '%chief people officer%'",
|
|
34
|
+
"LOWER(c.jobTitle) LIKE '%chro%'",
|
|
35
|
+
"LOWER(c.jobTitle) LIKE '%leiter personal%'",
|
|
36
|
+
"LOWER(c.jobTitle) LIKE '%leiter der personalabteilung%'",
|
|
37
|
+
"LOWER(c.jobTitle) LIKE '%personalleitung%'",
|
|
38
|
+
"LOWER(c.jobTitle) LIKE '%bereichsleiter personal%'",
|
|
39
|
+
"LOWER(c.jobTitle) LIKE '%geschäftsbereichsleiter personal%'",
|
|
40
|
+
"LOWER(c.jobTitle) LIKE '%geschaeftsbereichsleiter personal%'"
|
|
41
|
+
].join("\n OR ");
|
|
42
|
+
}
|
|
43
|
+
function titleExclusionPredicateSql() {
|
|
44
|
+
return [
|
|
45
|
+
"LOWER(c.jobTitle) NOT LIKE '%assistant%'",
|
|
46
|
+
"LOWER(c.jobTitle) NOT LIKE '%assistent%'",
|
|
47
|
+
"LOWER(c.jobTitle) NOT LIKE '%assistentin%'",
|
|
48
|
+
"LOWER(c.jobTitle) NOT LIKE '%recruitment%'",
|
|
49
|
+
"LOWER(c.jobTitle) NOT LIKE '%recruiter%'",
|
|
50
|
+
"LOWER(c.jobTitle) NOT LIKE '%talent acquisition specialist%'"
|
|
51
|
+
].join("\n AND ");
|
|
52
|
+
}
|
|
53
|
+
export function buildDirectPathLeadExportSql(vendor, market, limit) {
|
|
54
|
+
if (vendor !== "deel") {
|
|
55
|
+
throw new Error(`Unsupported direct-path vendor: ${vendor}`);
|
|
56
|
+
}
|
|
57
|
+
const countries = sqlStringList(marketCountries(market));
|
|
58
|
+
const broadTitlePredicate = broadTitlePredicateSql();
|
|
59
|
+
const titleExclusions = titleExclusionPredicateSql();
|
|
60
|
+
return `WITH hr_leadlists AS (
|
|
61
|
+
SELECT
|
|
62
|
+
CAST(leadListId AS STRING) AS leadListId,
|
|
63
|
+
CAST(query AS STRING) AS query,
|
|
64
|
+
leadList_container_ts
|
|
65
|
+
FROM \`icpidentifier.SalesPrompter.leadLists_raw\`
|
|
66
|
+
WHERE query IS NOT NULL
|
|
67
|
+
AND LOWER(CAST(query AS STRING)) LIKE '%/sales/search/people%'
|
|
68
|
+
AND LOWER(CAST(query AS STRING)) LIKE '%type%3afunction%2cvalues%3alist((id%3a12%'
|
|
69
|
+
QUALIFY ROW_NUMBER() OVER (PARTITION BY CAST(leadListId AS STRING) ORDER BY leadList_container_ts DESC) = 1
|
|
70
|
+
), contacts AS (
|
|
71
|
+
SELECT
|
|
72
|
+
CAST(leadListId AS STRING) AS leadListId,
|
|
73
|
+
CAST(contactId AS STRING) AS contactId,
|
|
74
|
+
CAST(companyId AS STRING) AS companyId,
|
|
75
|
+
firstName,
|
|
76
|
+
lastName,
|
|
77
|
+
jobTitle,
|
|
78
|
+
contact_companyName,
|
|
79
|
+
contact_location,
|
|
80
|
+
contact_companyLocation,
|
|
81
|
+
tenureAtCompany,
|
|
82
|
+
tenureAtPosition,
|
|
83
|
+
contact_summary,
|
|
84
|
+
contact_titleDescription
|
|
85
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_contacts\`
|
|
86
|
+
), companies AS (
|
|
87
|
+
SELECT
|
|
88
|
+
CAST(id AS STRING) AS companyId,
|
|
89
|
+
handle,
|
|
90
|
+
name,
|
|
91
|
+
countryCode,
|
|
92
|
+
companySize,
|
|
93
|
+
headquarters,
|
|
94
|
+
domain,
|
|
95
|
+
domain_linkedin,
|
|
96
|
+
website_linkedin,
|
|
97
|
+
industry,
|
|
98
|
+
timestamp AS company_ts
|
|
99
|
+
FROM \`icpidentifier.SalesPrompter.linkedin_companies\`
|
|
100
|
+
)
|
|
101
|
+
SELECT
|
|
102
|
+
c.leadListId,
|
|
103
|
+
c.contactId,
|
|
104
|
+
c.companyId,
|
|
105
|
+
CAST(NULL AS STRING) AS profileUrl,
|
|
106
|
+
c.firstName,
|
|
107
|
+
c.lastName,
|
|
108
|
+
CONCAT(COALESCE(c.firstName, ''), IF(c.firstName IS NOT NULL AND c.lastName IS NOT NULL, ' ', ''), COALESCE(c.lastName, '')) AS fullName,
|
|
109
|
+
c.jobTitle,
|
|
110
|
+
COALESCE(co.name, c.contact_companyName) AS companyName,
|
|
111
|
+
co.handle AS linkedin_companies_handle,
|
|
112
|
+
CONCAT('https://www.linkedin.com/company/', c.companyId) AS companyUrl,
|
|
113
|
+
co.companySize,
|
|
114
|
+
co.countryCode,
|
|
115
|
+
co.headquarters,
|
|
116
|
+
co.domain,
|
|
117
|
+
co.domain_linkedin,
|
|
118
|
+
co.website_linkedin,
|
|
119
|
+
co.industry,
|
|
120
|
+
c.contact_location,
|
|
121
|
+
c.contact_companyLocation,
|
|
122
|
+
c.tenureAtCompany,
|
|
123
|
+
c.tenureAtPosition,
|
|
124
|
+
c.contact_summary,
|
|
125
|
+
c.contact_titleDescription,
|
|
126
|
+
CAST(l.leadList_container_ts AS STRING) AS leadList_container_ts,
|
|
127
|
+
CAST(co.company_ts AS STRING) AS company_ts,
|
|
128
|
+
l.query AS leadListQuery
|
|
129
|
+
FROM contacts c
|
|
130
|
+
JOIN hr_leadlists l ON c.leadListId = l.leadListId
|
|
131
|
+
LEFT JOIN companies co ON c.companyId = co.companyId
|
|
132
|
+
WHERE co.countryCode IN (${countries})
|
|
133
|
+
AND co.domain IS NOT NULL
|
|
134
|
+
AND co.companySize IN ('200-499', '501-1000', '1001-5000', '5001-10.000', '10.000+')
|
|
135
|
+
AND (
|
|
136
|
+
${broadTitlePredicate}
|
|
137
|
+
)
|
|
138
|
+
AND ${titleExclusions}
|
|
139
|
+
QUALIFY ROW_NUMBER() OVER (PARTITION BY c.contactId ORDER BY co.company_ts DESC) = 1
|
|
140
|
+
ORDER BY co.company_ts DESC
|
|
141
|
+
LIMIT ${limit}`;
|
|
142
|
+
}
|
|
143
|
+
function classifySegment(title) {
|
|
144
|
+
const normalized = title.toLowerCase();
|
|
145
|
+
if (normalized.includes("payroll") ||
|
|
146
|
+
normalized.includes("people services") ||
|
|
147
|
+
normalized.includes("shared services") ||
|
|
148
|
+
normalized.includes("total rewards") ||
|
|
149
|
+
normalized.includes("compensation") ||
|
|
150
|
+
normalized.includes("benefits")) {
|
|
151
|
+
return {
|
|
152
|
+
segment: "payroll-people-services",
|
|
153
|
+
reason: "payroll, shared services, benefits, or people services scope"
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (normalized.includes("head of hr") ||
|
|
157
|
+
normalized.includes("head of human resources") ||
|
|
158
|
+
normalized.includes("hr director") ||
|
|
159
|
+
normalized.includes("director hr") ||
|
|
160
|
+
normalized.includes("chief people officer") ||
|
|
161
|
+
normalized.includes("chro") ||
|
|
162
|
+
normalized.includes("leiter personal") ||
|
|
163
|
+
normalized.includes("leiter der personalabteilung") ||
|
|
164
|
+
normalized.includes("personalleitung") ||
|
|
165
|
+
normalized.includes("bereichsleiter personal") ||
|
|
166
|
+
normalized.includes("geschäftsbereichsleiter personal") ||
|
|
167
|
+
normalized.includes("geschaeftsbereichsleiter personal")) {
|
|
168
|
+
return {
|
|
169
|
+
segment: "leadership",
|
|
170
|
+
reason: "top-level HR leadership title"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
segment: "broader-hr",
|
|
175
|
+
reason: "HR, people, or operations role outside the strict leadership/payroll buckets"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function toNumber(value) {
|
|
179
|
+
if (value === null || value === undefined) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const numeric = Number(value);
|
|
183
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
184
|
+
}
|
|
185
|
+
function toNullableString(value) {
|
|
186
|
+
if (value === null || value === undefined) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const normalized = String(value).trim();
|
|
190
|
+
return normalized.length > 0 ? normalized : null;
|
|
191
|
+
}
|
|
192
|
+
export function normalizeDirectPathRows(rows) {
|
|
193
|
+
return rows.map((row) => ({
|
|
194
|
+
leadListId: toNullableString(row.leadListId),
|
|
195
|
+
contactId: toNullableString(row.contactId),
|
|
196
|
+
companyId: toNullableString(row.companyId),
|
|
197
|
+
profileUrl: toNullableString(row.profileUrl),
|
|
198
|
+
firstName: toNullableString(row.firstName),
|
|
199
|
+
lastName: toNullableString(row.lastName),
|
|
200
|
+
fullName: toNullableString(row.fullName),
|
|
201
|
+
jobTitle: toNullableString(row.jobTitle),
|
|
202
|
+
companyName: toNullableString(row.companyName),
|
|
203
|
+
linkedin_companies_handle: toNullableString(row.linkedin_companies_handle),
|
|
204
|
+
companyUrl: toNullableString(row.companyUrl),
|
|
205
|
+
companySize: toNullableString(row.companySize),
|
|
206
|
+
countryCode: toNullableString(row.countryCode),
|
|
207
|
+
headquarters: toNullableString(row.headquarters),
|
|
208
|
+
domain: toNullableString(row.domain),
|
|
209
|
+
domain_linkedin: toNullableString(row.domain_linkedin),
|
|
210
|
+
website_linkedin: toNullableString(row.website_linkedin),
|
|
211
|
+
industry: toNullableString(row.industry),
|
|
212
|
+
contact_location: toNullableString(row.contact_location),
|
|
213
|
+
contact_companyLocation: toNullableString(row.contact_companyLocation),
|
|
214
|
+
tenureAtCompany: toNumber(row.tenureAtCompany),
|
|
215
|
+
tenureAtPosition: toNumber(row.tenureAtPosition),
|
|
216
|
+
contact_summary: toNullableString(row.contact_summary),
|
|
217
|
+
contact_titleDescription: toNullableString(row.contact_titleDescription),
|
|
218
|
+
leadList_container_ts: toNullableString(row.leadList_container_ts),
|
|
219
|
+
company_ts: toNullableString(row.company_ts),
|
|
220
|
+
leadListQuery: toNullableString(row.leadListQuery),
|
|
221
|
+
leadListQueryDecoded: (() => {
|
|
222
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
223
|
+
return rawQuery === null ? null : parseHistoricalQuery(rawQuery).decodedQuery;
|
|
224
|
+
})(),
|
|
225
|
+
leadListFunctionFilters: (() => {
|
|
226
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
227
|
+
return rawQuery === null
|
|
228
|
+
? []
|
|
229
|
+
: parseHistoricalQuery(rawQuery).functionFilters
|
|
230
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
231
|
+
.map((filter) => filter.text);
|
|
232
|
+
})(),
|
|
233
|
+
leadListTitleFilters: (() => {
|
|
234
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
235
|
+
return rawQuery === null
|
|
236
|
+
? []
|
|
237
|
+
: parseHistoricalQuery(rawQuery).titleFilters
|
|
238
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
239
|
+
.map((filter) => filter.text);
|
|
240
|
+
})(),
|
|
241
|
+
leadListRegionFilters: (() => {
|
|
242
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
243
|
+
return rawQuery === null
|
|
244
|
+
? []
|
|
245
|
+
: parseHistoricalQuery(rawQuery).regionFilters
|
|
246
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
247
|
+
.map((filter) => filter.text);
|
|
248
|
+
})(),
|
|
249
|
+
leadListHeadquartersFilters: (() => {
|
|
250
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
251
|
+
return rawQuery === null
|
|
252
|
+
? []
|
|
253
|
+
: parseHistoricalQuery(rawQuery).headquartersFilters
|
|
254
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
255
|
+
.map((filter) => filter.text);
|
|
256
|
+
})(),
|
|
257
|
+
leadListHeadcountFilters: (() => {
|
|
258
|
+
const rawQuery = toNullableString(row.leadListQuery);
|
|
259
|
+
return rawQuery === null
|
|
260
|
+
? []
|
|
261
|
+
: parseHistoricalQuery(rawQuery).headcountFilters
|
|
262
|
+
.filter((filter) => filter.selection === "INCLUDED")
|
|
263
|
+
.map((filter) => filter.text);
|
|
264
|
+
})()
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
export function segmentDirectPathRows(vendor, market, rows) {
|
|
268
|
+
if (vendor !== "deel") {
|
|
269
|
+
throw new Error(`Unsupported direct-path vendor: ${vendor}`);
|
|
270
|
+
}
|
|
271
|
+
const segments = {
|
|
272
|
+
leadership: [],
|
|
273
|
+
"payroll-people-services": [],
|
|
274
|
+
"broader-hr": []
|
|
275
|
+
};
|
|
276
|
+
for (const row of rows) {
|
|
277
|
+
const title = row.jobTitle ?? "";
|
|
278
|
+
const { segment, reason } = classifySegment(title);
|
|
279
|
+
segments[segment].push({
|
|
280
|
+
...row,
|
|
281
|
+
segment,
|
|
282
|
+
segmentReason: reason
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const countries = new Map();
|
|
286
|
+
const titles = new Map();
|
|
287
|
+
for (const row of rows) {
|
|
288
|
+
const country = row.countryCode ?? "unknown";
|
|
289
|
+
countries.set(country, (countries.get(country) ?? 0) + 1);
|
|
290
|
+
const title = row.jobTitle ?? "unknown";
|
|
291
|
+
titles.set(title, (titles.get(title) ?? 0) + 1);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
vendor,
|
|
295
|
+
market,
|
|
296
|
+
total: rows.length,
|
|
297
|
+
distinctContacts: new Set(rows.map((row) => row.contactId).filter((value) => value !== null)).size,
|
|
298
|
+
distinctCompanies: new Set(rows.map((row) => row.companyId).filter((value) => value !== null)).size,
|
|
299
|
+
segments,
|
|
300
|
+
summary: {
|
|
301
|
+
segmentCounts: {
|
|
302
|
+
leadership: segments.leadership.length,
|
|
303
|
+
"payroll-people-services": segments["payroll-people-services"].length,
|
|
304
|
+
"broader-hr": segments["broader-hr"].length
|
|
305
|
+
},
|
|
306
|
+
countries: [...countries.entries()].sort((a, b) => a[0].localeCompare(b[0])),
|
|
307
|
+
topTitles: [...titles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|