indiecrm-cli 0.1.0 → 0.2.1

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