salesprompter-cli 0.1.18 → 0.1.20
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 +53 -24
- package/dist/cli.js +1431 -119
- package/dist/linkedin-products.js +29 -8
- package/dist/linkedin-session.js +751 -0
- package/dist/sales-navigator.js +207 -36
- package/dist/salesnav-backfill.js +710 -0
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { access } from "node:fs/promises";
|
|
3
|
+
import { access, appendFile, mkdir } from "node:fs/promises";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { emitKeypressEvents } from "node:readline";
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
8
|
import { setTimeout as delay } from "node:timers/promises";
|
|
9
|
+
import { createClient } from "@supabase/supabase-js";
|
|
9
10
|
import { Command } from "commander";
|
|
10
11
|
import { z } from "zod";
|
|
11
12
|
import { clearAuthSession, loginWithBrowserConnect, loginWithDeviceFlow, loginWithToken, requireAuthSession, shouldBypassAuth, verifySession } from "./auth.js";
|
|
@@ -20,7 +21,8 @@ import { InstantlySyncProvider } from "./instantly.js";
|
|
|
20
21
|
import { crawlLinkedInProductCategory } from "./linkedin-products.js";
|
|
21
22
|
import { buildLeadlistsFunnelQueries } from "./leadlists-funnel.js";
|
|
22
23
|
import { readJsonFile, splitCsv, writeJsonFile, writeTextFile } from "./io.js";
|
|
23
|
-
import { buildSalesNavigatorCrawlPreview, createSalesNavigatorCrawlSeed, DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS, buildSalesNavigatorPeopleSlice, expandSalesNavigatorCrawlAttempt, SalesNavigatorSliceTooBroadError } from "./sales-navigator.js";
|
|
24
|
+
import { buildSalesNavigatorCrawlPreview, createSalesNavigatorCrawlSeed, DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS, buildSalesNavigatorPeopleSlice, deriveSalesNavigatorTitleQuerySeeds, expandSalesNavigatorCrawlAttempt, SalesNavigatorSliceTooBroadError } from "./sales-navigator.js";
|
|
25
|
+
import { buildSalesNavigatorHistoricalBackfillPlan, ensureSalesNavigatorPeopleCount, resolveSalesNavigatorHistoricalBackfillConfig, resolveSalesNavigatorHistoricalBackfillResumeState, resolveSalesNavigatorHistoricalBackfillOrgId, salesNavigatorHistoricalBackfillDefaults } from "./salesnav-backfill.js";
|
|
24
26
|
const require = createRequire(import.meta.url);
|
|
25
27
|
const { version: packageVersion } = require("../package.json");
|
|
26
28
|
const program = new Command();
|
|
@@ -50,12 +52,31 @@ const LinkedInProductIngestResponseSchema = z.object({
|
|
|
50
52
|
upserted: z.number().int().nonnegative(),
|
|
51
53
|
totalInCatalog: z.number().int().nonnegative().optional()
|
|
52
54
|
});
|
|
55
|
+
const SalesNavigatorLaunchDiagnosticsSchema = z.object({
|
|
56
|
+
orderedCandidateAgentIds: z.array(z.string().min(1)),
|
|
57
|
+
runningAgentIds: z.array(z.string().min(1)),
|
|
58
|
+
busyAgentIds: z.array(z.string().min(1)),
|
|
59
|
+
selectedAgent: z.object({
|
|
60
|
+
id: z.string().min(1),
|
|
61
|
+
name: z.string().min(1),
|
|
62
|
+
maxParallelism: z.number().int().nullable(),
|
|
63
|
+
fileMgmt: z.string().min(1).nullable(),
|
|
64
|
+
hasWebhook: z.boolean(),
|
|
65
|
+
hasStoredSessionCookie: z.boolean(),
|
|
66
|
+
storedIdentityCount: z.number().int().nonnegative(),
|
|
67
|
+
supportsDirectSessionInjection: z.boolean()
|
|
68
|
+
})
|
|
69
|
+
});
|
|
53
70
|
const SalesNavigatorExportStartResponseSchema = z.object({
|
|
54
71
|
status: z.literal("accepted"),
|
|
55
72
|
runId: z.string().min(1),
|
|
56
73
|
exportStatus: z.literal("pending"),
|
|
57
74
|
agentId: z.string().min(1),
|
|
58
75
|
containerId: z.string().min(1),
|
|
76
|
+
selectedSessionCookieSha256: z.string().min(1).nullable().optional(),
|
|
77
|
+
selectedSessionUserEmail: z.string().min(1).nullable().optional(),
|
|
78
|
+
selectedSessionUserHandle: z.string().min(1).nullable().optional(),
|
|
79
|
+
launchDiagnostics: SalesNavigatorLaunchDiagnosticsSchema.nullable().optional(),
|
|
59
80
|
sourceQueryUrl: z.string().url(),
|
|
60
81
|
slicedQueryUrl: z.string().url(),
|
|
61
82
|
previousContainerId: z.string().min(1).nullable().optional()
|
|
@@ -79,6 +100,10 @@ const SalesNavigatorExportRunSchema = z.object({
|
|
|
79
100
|
resultCsvUrl: z.string().url().nullable().optional(),
|
|
80
101
|
agentId: z.string().min(1),
|
|
81
102
|
containerId: z.string().min(1),
|
|
103
|
+
selectedSessionCookieSha256: z.string().min(1).nullable().optional(),
|
|
104
|
+
selectedSessionUserEmail: z.string().min(1).nullable().optional(),
|
|
105
|
+
selectedSessionUserHandle: z.string().min(1).nullable().optional(),
|
|
106
|
+
launchDiagnostics: SalesNavigatorLaunchDiagnosticsSchema.nullable().optional(),
|
|
82
107
|
sourceQueryUrl: z.string().url(),
|
|
83
108
|
slicedQueryUrl: z.string().url(),
|
|
84
109
|
createdAt: z.string().datetime(),
|
|
@@ -99,6 +124,10 @@ const SalesNavigatorExportResponseSchema = z.object({
|
|
|
99
124
|
resultCsvUrl: z.string().url().nullable().optional(),
|
|
100
125
|
agentId: z.string().min(1),
|
|
101
126
|
containerId: z.string().min(1),
|
|
127
|
+
selectedSessionCookieSha256: z.string().min(1).nullable().optional(),
|
|
128
|
+
selectedSessionUserEmail: z.string().min(1).nullable().optional(),
|
|
129
|
+
selectedSessionUserHandle: z.string().min(1).nullable().optional(),
|
|
130
|
+
launchDiagnostics: SalesNavigatorLaunchDiagnosticsSchema.nullable().optional(),
|
|
102
131
|
sourceQueryUrl: z.string().url(),
|
|
103
132
|
slicedQueryUrl: z.string().url()
|
|
104
133
|
});
|
|
@@ -188,6 +217,12 @@ function printOutput(value) {
|
|
|
188
217
|
const space = runtimeOutputOptions.json ? undefined : 2;
|
|
189
218
|
process.stdout.write(`${JSON.stringify(value, null, space)}\n`);
|
|
190
219
|
}
|
|
220
|
+
function writeProgress(message) {
|
|
221
|
+
if (runtimeOutputOptions.json || runtimeOutputOptions.quiet) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
process.stderr.write(`${message}\n`);
|
|
225
|
+
}
|
|
191
226
|
function applyGlobalOutputOptions(actionCommand) {
|
|
192
227
|
const globalOptions = actionCommand.optsWithGlobals();
|
|
193
228
|
runtimeOutputOptions.json = Boolean(globalOptions.json);
|
|
@@ -687,6 +722,449 @@ async function fetchWorkspaceLeadSearch(session, requestBody) {
|
|
|
687
722
|
function buildLinkedInProductsOutputPath(categorySlug) {
|
|
688
723
|
return `./data/linkedin-products-${categorySlug}.json`;
|
|
689
724
|
}
|
|
725
|
+
function buildLinkedInProductCategorySalesNavigatorOutputPath(categorySlug) {
|
|
726
|
+
return `./data/salesnav-product-category-${categorySlug}.json`;
|
|
727
|
+
}
|
|
728
|
+
const SALES_NAVIGATOR_TERMINAL_JOB_STATUSES = new Set(["completed", "completed_with_failures"]);
|
|
729
|
+
function isSalesNavigatorCrawlJobTerminal(status) {
|
|
730
|
+
return SALES_NAVIGATOR_TERMINAL_JOB_STATUSES.has(status);
|
|
731
|
+
}
|
|
732
|
+
function buildWorkflowTraceId(prefix) {
|
|
733
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
734
|
+
}
|
|
735
|
+
function buildSalesNavigatorWorkflowLogPath(input) {
|
|
736
|
+
const slug = slugify(input) || "salesnav-product-category";
|
|
737
|
+
return `./data/${slug}-salesnav.log.jsonl`;
|
|
738
|
+
}
|
|
739
|
+
function buildSalesNavigatorCrawlLogPath(input) {
|
|
740
|
+
const slug = slugify(input) || "salesnav-crawl";
|
|
741
|
+
return `./data/${slug}-crawl.log.jsonl`;
|
|
742
|
+
}
|
|
743
|
+
function decodeSalesNavigatorQueryParam(url) {
|
|
744
|
+
try {
|
|
745
|
+
const encoded = new URL(url).searchParams.get("query");
|
|
746
|
+
if (!encoded) {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
return decodeURIComponent(encoded);
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
return encoded;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async function createWorkflowLogger(options) {
|
|
761
|
+
const traceId = options.traceId ?? buildWorkflowTraceId("salesprompter-cli");
|
|
762
|
+
const logPath = options.logPath;
|
|
763
|
+
await mkdir(path.dirname(logPath), { recursive: true });
|
|
764
|
+
return {
|
|
765
|
+
traceId,
|
|
766
|
+
logPath,
|
|
767
|
+
log: async (event, metadata = {}) => {
|
|
768
|
+
const entry = {
|
|
769
|
+
timestamp: new Date().toISOString(),
|
|
770
|
+
traceId,
|
|
771
|
+
event,
|
|
772
|
+
metadata
|
|
773
|
+
};
|
|
774
|
+
await appendFile(logPath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
775
|
+
writeProgress(`[${entry.timestamp}] ${event}`);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function summarizeSalesNavigatorQuery(url, appliedFilters) {
|
|
780
|
+
return {
|
|
781
|
+
url,
|
|
782
|
+
decodedQuery: decodeSalesNavigatorQueryParam(url),
|
|
783
|
+
appliedFilters
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function extractSalesNavigatorFilterTypes(url, appliedFilters) {
|
|
787
|
+
const filterTypes = new Set(appliedFilters.map((filter) => filter.type));
|
|
788
|
+
const decodedQuery = decodeSalesNavigatorQueryParam(url) ?? "";
|
|
789
|
+
for (const match of decodedQuery.matchAll(/type:([A-Z_]+)/g)) {
|
|
790
|
+
const value = match[1]?.trim();
|
|
791
|
+
if (value) {
|
|
792
|
+
filterTypes.add(value);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return [...filterTypes];
|
|
796
|
+
}
|
|
797
|
+
function shouldPreSplitSalesNavigatorRootSlice(slice, maxSplitDepth) {
|
|
798
|
+
if (slice.depth !== 0 || slice.splitTrail.length > 0) {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
if (!nextSalesNavigatorSplitDimension(slice, maxSplitDepth)) {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
const filterTypes = new Set(extractSalesNavigatorFilterTypes(slice.slicedQueryUrl, slice.appliedFilters));
|
|
805
|
+
if (!filterTypes.has("CURRENT_TITLE")) {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
return !DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS.some((dimension) => filterTypes.has(dimension.filterType));
|
|
809
|
+
}
|
|
810
|
+
function buildTraceHeaders(traceId) {
|
|
811
|
+
return traceId ? { "X-Salesprompter-Trace-Id": traceId } : {};
|
|
812
|
+
}
|
|
813
|
+
function buildSalesNavigatorWorkflowCrawlSummary(crawl) {
|
|
814
|
+
const successful = crawl.job.status === "completed" && !crawl.truncated;
|
|
815
|
+
return {
|
|
816
|
+
jobStatus: crawl.job.status,
|
|
817
|
+
importedPeople: crawl.job.importedPeople,
|
|
818
|
+
exportedSlices: crawl.job.exportedSlices,
|
|
819
|
+
failedSlices: crawl.job.failedSlices,
|
|
820
|
+
queuedSlices: crawl.job.queuedSlices,
|
|
821
|
+
runningSlices: crawl.job.runningSlices,
|
|
822
|
+
truncated: crawl.truncated,
|
|
823
|
+
successful
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function buildSalesNavigatorWorkflowSummary(crawls) {
|
|
827
|
+
return crawls.reduce((summary, crawl) => {
|
|
828
|
+
summary.totalImportedPeople += crawl.summary.importedPeople;
|
|
829
|
+
summary.totalExportedSlices += crawl.summary.exportedSlices;
|
|
830
|
+
summary.totalFailedSlices += crawl.summary.failedSlices;
|
|
831
|
+
if (crawl.summary.truncated) {
|
|
832
|
+
summary.truncatedTitles += 1;
|
|
833
|
+
}
|
|
834
|
+
if (crawl.summary.jobStatus === "completed") {
|
|
835
|
+
summary.completedTitles += 1;
|
|
836
|
+
}
|
|
837
|
+
else if (crawl.summary.jobStatus === "completed_with_failures") {
|
|
838
|
+
summary.completedWithFailuresTitles += 1;
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
summary.runningTitles += 1;
|
|
842
|
+
}
|
|
843
|
+
if (!crawl.summary.successful) {
|
|
844
|
+
summary.workflowStatus = "completed_with_failures";
|
|
845
|
+
}
|
|
846
|
+
return summary;
|
|
847
|
+
}, {
|
|
848
|
+
workflowStatus: "completed",
|
|
849
|
+
totalImportedPeople: 0,
|
|
850
|
+
totalExportedSlices: 0,
|
|
851
|
+
totalFailedSlices: 0,
|
|
852
|
+
completedTitles: 0,
|
|
853
|
+
completedWithFailuresTitles: 0,
|
|
854
|
+
runningTitles: 0,
|
|
855
|
+
truncatedTitles: 0
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
function buildSalesNavigatorWorkflowFailureMessage(summary) {
|
|
859
|
+
return [
|
|
860
|
+
"Sales Navigator workflow completed with failures.",
|
|
861
|
+
`completedTitles=${summary.completedTitles}`,
|
|
862
|
+
`completedWithFailuresTitles=${summary.completedWithFailuresTitles}`,
|
|
863
|
+
`runningTitles=${summary.runningTitles}`,
|
|
864
|
+
`truncatedTitles=${summary.truncatedTitles}`,
|
|
865
|
+
`totalFailedSlices=${summary.totalFailedSlices}`
|
|
866
|
+
].join(" ");
|
|
867
|
+
}
|
|
868
|
+
function validateSalesNavigatorSeedQuery(seed) {
|
|
869
|
+
const decodedQuery = decodeSalesNavigatorQueryParam(seed.queryUrl);
|
|
870
|
+
const haystack = decodedQuery?.toLowerCase() ?? "";
|
|
871
|
+
const missingFilters = seed.appliedFilters.flatMap((filter) => {
|
|
872
|
+
const missingValues = filter.values.filter((value) => !haystack.includes(value.text.toLowerCase()));
|
|
873
|
+
if (!haystack.includes(filter.type.toLowerCase()) || missingValues.length > 0) {
|
|
874
|
+
return `${filter.type}:${missingValues.map((value) => value.text).join(",") || "*"}`;
|
|
875
|
+
}
|
|
876
|
+
return [];
|
|
877
|
+
});
|
|
878
|
+
return {
|
|
879
|
+
valid: missingFilters.length === 0,
|
|
880
|
+
missingFilters,
|
|
881
|
+
decodedQuery
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
async function runSalesNavigatorFromProductCategoryWorkflow(options) {
|
|
885
|
+
const logger = await createWorkflowLogger({
|
|
886
|
+
logPath: options.logPath ?? buildSalesNavigatorWorkflowLogPath(options.input)
|
|
887
|
+
});
|
|
888
|
+
await logger.log("workflow.started", {
|
|
889
|
+
input: options.input,
|
|
890
|
+
maxPages: options.maxPages,
|
|
891
|
+
productLimit: options.productLimit ?? null,
|
|
892
|
+
titleLimit: options.titleLimit ?? null,
|
|
893
|
+
maxResultsPerSearch: options.maxResultsPerSearch,
|
|
894
|
+
numberOfProfiles: options.numberOfProfiles,
|
|
895
|
+
slicePreset: options.slicePreset,
|
|
896
|
+
maxSplitDepth: options.maxSplitDepth,
|
|
897
|
+
maxSlicesPerTitle: options.maxSlicesPerTitle,
|
|
898
|
+
maxRetries: options.maxRetries,
|
|
899
|
+
probeProfiles: options.probeProfiles,
|
|
900
|
+
agentBusyWaitSeconds: options.agentBusyWaitSeconds,
|
|
901
|
+
agentBusyMaxWaits: options.agentBusyMaxWaits,
|
|
902
|
+
idlePollSeconds: options.idlePollSeconds,
|
|
903
|
+
idleMaxPolls: options.idleMaxPolls,
|
|
904
|
+
parallelExports: options.parallelExports,
|
|
905
|
+
skipProductUpload: options.skipProductUpload,
|
|
906
|
+
dryRun: options.dryRun
|
|
907
|
+
});
|
|
908
|
+
try {
|
|
909
|
+
const scrape = await crawlLinkedInProductCategory({
|
|
910
|
+
input: options.input,
|
|
911
|
+
maxPages: options.maxPages,
|
|
912
|
+
limit: options.productLimit
|
|
913
|
+
});
|
|
914
|
+
await logger.log("linkedin.category.scraped", {
|
|
915
|
+
source: scrape.source,
|
|
916
|
+
totalPagesFetched: scrape.totalPagesFetched,
|
|
917
|
+
discoveredProducts: scrape.items.length,
|
|
918
|
+
productNames: scrape.items.map((item) => item.productName)
|
|
919
|
+
});
|
|
920
|
+
const titleSeeds = deriveSalesNavigatorTitleQuerySeeds({
|
|
921
|
+
sourceProductUrl: scrape.source.productUrl,
|
|
922
|
+
items: scrape.items,
|
|
923
|
+
titleLimit: options.titleLimit
|
|
924
|
+
});
|
|
925
|
+
if (titleSeeds.length === 0) {
|
|
926
|
+
throw new Error(`No intended-role job titles were found while crawling the LinkedIn product category ${scrape.source.category.name}.`);
|
|
927
|
+
}
|
|
928
|
+
const outPath = options.outPath ?? buildLinkedInProductCategorySalesNavigatorOutputPath(scrape.source.category.slug);
|
|
929
|
+
const previewQueries = titleSeeds.map((seed) => {
|
|
930
|
+
const preview = buildSalesNavigatorCrawlPreview({
|
|
931
|
+
sourceQueryUrl: seed.queryUrl,
|
|
932
|
+
maxResultsPerSearch: options.maxResultsPerSearch,
|
|
933
|
+
numberOfProfiles: options.numberOfProfiles,
|
|
934
|
+
slicePreset: options.slicePreset
|
|
935
|
+
});
|
|
936
|
+
return {
|
|
937
|
+
title: seed.title,
|
|
938
|
+
queryUrl: seed.queryUrl,
|
|
939
|
+
appliedFilters: seed.appliedFilters,
|
|
940
|
+
sourceProduct: seed.sourceProduct,
|
|
941
|
+
matchedProductCount: seed.matchedProductCount,
|
|
942
|
+
firstSplitQueries: preview.firstSplit.map((attempt) => ({
|
|
943
|
+
slicedQueryUrl: attempt.slicedQueryUrl,
|
|
944
|
+
appliedFilters: attempt.appliedFilters,
|
|
945
|
+
splitTrail: formatSalesNavigatorSplitTrail(attempt.splitTrail.map((entry) => ({
|
|
946
|
+
...entry,
|
|
947
|
+
value: {
|
|
948
|
+
id: entry.value.id,
|
|
949
|
+
text: entry.value.text,
|
|
950
|
+
selectionType: entry.value.selectionType
|
|
951
|
+
}
|
|
952
|
+
})))
|
|
953
|
+
}))
|
|
954
|
+
};
|
|
955
|
+
});
|
|
956
|
+
await logger.log("salesnav.title-seeds.derived", {
|
|
957
|
+
titleCount: titleSeeds.length,
|
|
958
|
+
titles: titleSeeds.map((seed) => ({
|
|
959
|
+
title: seed.title,
|
|
960
|
+
sourceProduct: seed.sourceProduct,
|
|
961
|
+
matchedProductCount: seed.matchedProductCount,
|
|
962
|
+
...summarizeSalesNavigatorQuery(seed.queryUrl, seed.appliedFilters)
|
|
963
|
+
}))
|
|
964
|
+
});
|
|
965
|
+
const firstSeedValidation = validateSalesNavigatorSeedQuery(titleSeeds[0]);
|
|
966
|
+
await logger.log("salesnav.first-query.validated", {
|
|
967
|
+
title: titleSeeds[0]?.title ?? null,
|
|
968
|
+
valid: firstSeedValidation.valid,
|
|
969
|
+
missingFilters: firstSeedValidation.missingFilters,
|
|
970
|
+
decodedQuery: firstSeedValidation.decodedQuery
|
|
971
|
+
});
|
|
972
|
+
if (!firstSeedValidation.valid) {
|
|
973
|
+
throw new Error(`Generated Sales Navigator seed query for "${titleSeeds[0]?.title ?? "unknown"}" is missing expected filters: ${firstSeedValidation.missingFilters.join(", ")}.`);
|
|
974
|
+
}
|
|
975
|
+
await logger.log("salesnav.first-split.preview", {
|
|
976
|
+
titles: previewQueries.map((query) => ({
|
|
977
|
+
title: query.title,
|
|
978
|
+
sourceProduct: query.sourceProduct,
|
|
979
|
+
matchedProductCount: query.matchedProductCount,
|
|
980
|
+
...summarizeSalesNavigatorQuery(query.queryUrl, query.appliedFilters),
|
|
981
|
+
firstSplitQueries: query.firstSplitQueries.map((split) => ({
|
|
982
|
+
splitTrail: split.splitTrail,
|
|
983
|
+
...summarizeSalesNavigatorQuery(split.slicedQueryUrl, split.appliedFilters)
|
|
984
|
+
}))
|
|
985
|
+
}))
|
|
986
|
+
});
|
|
987
|
+
if (options.dryRun) {
|
|
988
|
+
const payload = {
|
|
989
|
+
status: "ok",
|
|
990
|
+
dryRun: true,
|
|
991
|
+
mode: "linkedin-product-category-to-salesnav",
|
|
992
|
+
traceId: logger.traceId,
|
|
993
|
+
logPath: logger.logPath,
|
|
994
|
+
source: scrape.source,
|
|
995
|
+
totalPagesFetched: scrape.totalPagesFetched,
|
|
996
|
+
discoveredProducts: scrape.items.length,
|
|
997
|
+
titleCount: titleSeeds.length,
|
|
998
|
+
summary: {
|
|
999
|
+
workflowStatus: "completed",
|
|
1000
|
+
totalImportedPeople: 0,
|
|
1001
|
+
totalExportedSlices: 0,
|
|
1002
|
+
totalFailedSlices: 0,
|
|
1003
|
+
completedTitles: 0,
|
|
1004
|
+
completedWithFailuresTitles: 0,
|
|
1005
|
+
runningTitles: 0,
|
|
1006
|
+
truncatedTitles: 0
|
|
1007
|
+
},
|
|
1008
|
+
uploaded: null,
|
|
1009
|
+
queries: previewQueries
|
|
1010
|
+
};
|
|
1011
|
+
await writeJsonFile(outPath, payload);
|
|
1012
|
+
await logger.log("workflow.completed", {
|
|
1013
|
+
outPath,
|
|
1014
|
+
dryRun: true,
|
|
1015
|
+
discoveredProducts: payload.discoveredProducts,
|
|
1016
|
+
titleCount: payload.titleCount
|
|
1017
|
+
});
|
|
1018
|
+
return { outPath, payload };
|
|
1019
|
+
}
|
|
1020
|
+
let session = await requireAuthSession();
|
|
1021
|
+
let uploaded = null;
|
|
1022
|
+
if (!options.skipProductUpload) {
|
|
1023
|
+
await logger.log("linkedin.catalog.upload.started", {
|
|
1024
|
+
itemCount: scrape.items.length
|
|
1025
|
+
});
|
|
1026
|
+
uploaded = await uploadLinkedInProductsCatalog(session, {
|
|
1027
|
+
source: {
|
|
1028
|
+
input: scrape.source.input,
|
|
1029
|
+
kind: scrape.source.kind,
|
|
1030
|
+
query: scrape.source.query,
|
|
1031
|
+
companyUrl: scrape.source.companyUrl,
|
|
1032
|
+
productUrl: scrape.source.productUrl,
|
|
1033
|
+
category: scrape.source.category
|
|
1034
|
+
},
|
|
1035
|
+
items: scrape.items
|
|
1036
|
+
}, 100, logger.traceId);
|
|
1037
|
+
await logger.log("linkedin.catalog.upload.completed", uploaded);
|
|
1038
|
+
}
|
|
1039
|
+
const crawls = [];
|
|
1040
|
+
for (const seed of titleSeeds) {
|
|
1041
|
+
writeProgress(`Starting durable Sales Navigator crawl for intended role "${seed.title}".`);
|
|
1042
|
+
const rootSlice = createSalesNavigatorCrawlSeed({
|
|
1043
|
+
sourceQueryUrl: seed.queryUrl,
|
|
1044
|
+
maxResultsPerSearch: options.maxResultsPerSearch,
|
|
1045
|
+
numberOfProfiles: options.numberOfProfiles,
|
|
1046
|
+
slicePreset: options.slicePreset
|
|
1047
|
+
});
|
|
1048
|
+
const created = await createOrResumeSalesNavigatorCrawlJob(session, {
|
|
1049
|
+
sourceQueryUrl: seed.queryUrl,
|
|
1050
|
+
slicePreset: options.slicePreset,
|
|
1051
|
+
maxResultsPerSearch: options.maxResultsPerSearch,
|
|
1052
|
+
numberOfProfiles: options.numberOfProfiles,
|
|
1053
|
+
rawPayload: {
|
|
1054
|
+
workflow: "linkedin-product-category-to-salesnav",
|
|
1055
|
+
traceId: logger.traceId,
|
|
1056
|
+
source: scrape.source,
|
|
1057
|
+
titleSeed: {
|
|
1058
|
+
title: seed.title,
|
|
1059
|
+
queryUrl: seed.queryUrl,
|
|
1060
|
+
appliedFilters: seed.appliedFilters,
|
|
1061
|
+
sourceProduct: seed.sourceProduct,
|
|
1062
|
+
matchedProductCount: seed.matchedProductCount
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
rootSlice: {
|
|
1066
|
+
slicedQueryUrl: rootSlice.slicedQueryUrl,
|
|
1067
|
+
appliedFilters: rootSlice.appliedFilters,
|
|
1068
|
+
depth: rootSlice.depth,
|
|
1069
|
+
splitTrail: rootSlice.splitTrail,
|
|
1070
|
+
rawPayload: {
|
|
1071
|
+
traceId: logger.traceId,
|
|
1072
|
+
title: seed.title,
|
|
1073
|
+
sourceProduct: seed.sourceProduct,
|
|
1074
|
+
matchedProductCount: seed.matchedProductCount,
|
|
1075
|
+
source: scrape.source
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}, logger.traceId);
|
|
1079
|
+
session = created.session;
|
|
1080
|
+
await logger.log("salesnav.crawl.job.ready", {
|
|
1081
|
+
title: seed.title,
|
|
1082
|
+
sourceProduct: seed.sourceProduct,
|
|
1083
|
+
matchedProductCount: seed.matchedProductCount,
|
|
1084
|
+
resumed: created.value.resumed,
|
|
1085
|
+
jobId: created.value.job.id,
|
|
1086
|
+
rootSlice: {
|
|
1087
|
+
depth: rootSlice.depth,
|
|
1088
|
+
splitTrail: formatSalesNavigatorSplitTrail(rootSlice.splitTrail),
|
|
1089
|
+
...summarizeSalesNavigatorQuery(rootSlice.slicedQueryUrl, rootSlice.appliedFilters)
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
const crawl = await executeSalesNavigatorCrawlJob(session, created.value.job.id, {
|
|
1093
|
+
maxSplitDepth: options.maxSplitDepth,
|
|
1094
|
+
maxSlices: options.maxSlicesPerTitle,
|
|
1095
|
+
maxRetries: options.maxRetries,
|
|
1096
|
+
probeProfiles: options.probeProfiles,
|
|
1097
|
+
agentBusyWaitSeconds: options.agentBusyWaitSeconds,
|
|
1098
|
+
agentBusyMaxWaits: options.agentBusyMaxWaits,
|
|
1099
|
+
idlePollSeconds: options.idlePollSeconds,
|
|
1100
|
+
idleMaxPolls: options.idleMaxPolls,
|
|
1101
|
+
parallelExports: options.parallelExports,
|
|
1102
|
+
traceId: logger.traceId,
|
|
1103
|
+
logger
|
|
1104
|
+
});
|
|
1105
|
+
session = crawl.session;
|
|
1106
|
+
const crawlSummary = buildSalesNavigatorWorkflowCrawlSummary(crawl);
|
|
1107
|
+
await logger.log("salesnav.crawl.job.finished", {
|
|
1108
|
+
title: seed.title,
|
|
1109
|
+
jobId: created.value.job.id,
|
|
1110
|
+
summary: crawlSummary,
|
|
1111
|
+
lastOutcome: crawl.lastOutcome
|
|
1112
|
+
});
|
|
1113
|
+
crawls.push({
|
|
1114
|
+
title: seed.title,
|
|
1115
|
+
sourceProduct: seed.sourceProduct,
|
|
1116
|
+
matchedProductCount: seed.matchedProductCount,
|
|
1117
|
+
queryUrl: seed.queryUrl,
|
|
1118
|
+
jobId: created.value.job.id,
|
|
1119
|
+
resumed: created.value.resumed,
|
|
1120
|
+
claimedSlices: crawl.claimedSlices,
|
|
1121
|
+
truncated: crawl.truncated,
|
|
1122
|
+
activeSlice: crawl.activeSlice
|
|
1123
|
+
? {
|
|
1124
|
+
id: crawl.activeSlice.id,
|
|
1125
|
+
slicedQueryUrl: crawl.activeSlice.slicedQueryUrl,
|
|
1126
|
+
depth: crawl.activeSlice.depth,
|
|
1127
|
+
splitTrail: formatSalesNavigatorSplitTrail(crawl.activeSlice.splitTrail)
|
|
1128
|
+
}
|
|
1129
|
+
: null,
|
|
1130
|
+
lastOutcome: crawl.lastOutcome,
|
|
1131
|
+
job: crawl.job,
|
|
1132
|
+
summary: crawlSummary
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
const summary = buildSalesNavigatorWorkflowSummary(crawls);
|
|
1136
|
+
const payload = {
|
|
1137
|
+
status: "ok",
|
|
1138
|
+
dryRun: false,
|
|
1139
|
+
mode: "linkedin-product-category-to-salesnav",
|
|
1140
|
+
traceId: logger.traceId,
|
|
1141
|
+
logPath: logger.logPath,
|
|
1142
|
+
source: scrape.source,
|
|
1143
|
+
totalPagesFetched: scrape.totalPagesFetched,
|
|
1144
|
+
discoveredProducts: scrape.items.length,
|
|
1145
|
+
titleCount: titleSeeds.length,
|
|
1146
|
+
summary,
|
|
1147
|
+
uploaded,
|
|
1148
|
+
crawls
|
|
1149
|
+
};
|
|
1150
|
+
await writeJsonFile(outPath, payload);
|
|
1151
|
+
await logger.log("workflow.completed", {
|
|
1152
|
+
outPath,
|
|
1153
|
+
dryRun: false,
|
|
1154
|
+
uploaded,
|
|
1155
|
+
crawlCount: crawls.length,
|
|
1156
|
+
summary
|
|
1157
|
+
});
|
|
1158
|
+
return { outPath, payload };
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
await logger.log("workflow.failed", {
|
|
1162
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1163
|
+
stack: error instanceof Error ? error.stack ?? null : null
|
|
1164
|
+
});
|
|
1165
|
+
throw error;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
690
1168
|
function collectStringOptionValue(value, previous = []) {
|
|
691
1169
|
return [...previous, value];
|
|
692
1170
|
}
|
|
@@ -696,6 +1174,7 @@ class SalesNavigatorExportRequestError extends Error {
|
|
|
696
1174
|
runId;
|
|
697
1175
|
agentId;
|
|
698
1176
|
containerId;
|
|
1177
|
+
launchDiagnostics;
|
|
699
1178
|
statusCode;
|
|
700
1179
|
constructor(message, options) {
|
|
701
1180
|
super(message);
|
|
@@ -706,8 +1185,10 @@ class SalesNavigatorExportRequestError extends Error {
|
|
|
706
1185
|
this.runId = options.runId;
|
|
707
1186
|
this.agentId = options.agentId;
|
|
708
1187
|
this.containerId = options.containerId;
|
|
1188
|
+
this.launchDiagnostics = options.launchDiagnostics ?? null;
|
|
709
1189
|
}
|
|
710
1190
|
}
|
|
1191
|
+
const SALES_NAVIGATOR_EXPORT_START_TIMEOUT_MS = 90_000;
|
|
711
1192
|
async function withRefreshableAuthSession(session, run, contextLabel = "Salesprompter session expired during crawl. Refreshing login...") {
|
|
712
1193
|
let currentSession = session;
|
|
713
1194
|
let authRefreshCount = 0;
|
|
@@ -748,7 +1229,7 @@ async function fetchCliJson(session, request, schema) {
|
|
|
748
1229
|
return schema.parse(parsed);
|
|
749
1230
|
});
|
|
750
1231
|
}
|
|
751
|
-
async function uploadLinkedInProductsCatalog(session, payload, batchSize = 100) {
|
|
1232
|
+
async function uploadLinkedInProductsCatalog(session, payload, batchSize = 100, traceId) {
|
|
752
1233
|
let imported = 0;
|
|
753
1234
|
let upserted = 0;
|
|
754
1235
|
for (let startIndex = 0; startIndex < payload.items.length; startIndex += batchSize) {
|
|
@@ -757,7 +1238,8 @@ async function uploadLinkedInProductsCatalog(session, payload, batchSize = 100)
|
|
|
757
1238
|
method: "POST",
|
|
758
1239
|
headers: {
|
|
759
1240
|
"Content-Type": "application/json",
|
|
760
|
-
Authorization: `Bearer ${session.accessToken}
|
|
1241
|
+
Authorization: `Bearer ${session.accessToken}`,
|
|
1242
|
+
...buildTraceHeaders(traceId)
|
|
761
1243
|
},
|
|
762
1244
|
body: JSON.stringify({
|
|
763
1245
|
source: payload.source,
|
|
@@ -788,17 +1270,149 @@ function serializeSalesNavigatorFiltersForApi(filters) {
|
|
|
788
1270
|
}))
|
|
789
1271
|
}));
|
|
790
1272
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1273
|
+
function buildSalesNavigatorSliceRawPayload(slice, extra = {}) {
|
|
1274
|
+
return {
|
|
1275
|
+
...extra,
|
|
1276
|
+
sourceQueryUrl: slice.sourceQueryUrl,
|
|
1277
|
+
slicedQueryUrl: slice.slicedQueryUrl,
|
|
1278
|
+
appliedFilters: slice.appliedFilters,
|
|
1279
|
+
depth: slice.depth,
|
|
1280
|
+
splitTrail: slice.splitTrail,
|
|
1281
|
+
slicePreset: slice.slicePreset,
|
|
1282
|
+
maxResultsPerSearch: slice.maxResultsPerSearch,
|
|
1283
|
+
numberOfProfiles: slice.numberOfProfiles,
|
|
1284
|
+
retryCount: slice.retryCount ?? null,
|
|
1285
|
+
cookieRetryCount: slice.cookieRetryCount ?? null,
|
|
1286
|
+
resultRetryCount: slice.resultRetryCount ?? null
|
|
1287
|
+
};
|
|
795
1288
|
}
|
|
796
|
-
|
|
1289
|
+
function buildSalesNavigatorCrawlReportRawPayload(slice, traceId, extra = {}) {
|
|
1290
|
+
return buildSalesNavigatorSliceRawPayload({
|
|
1291
|
+
sourceQueryUrl: slice.sourceQueryUrl,
|
|
1292
|
+
slicedQueryUrl: slice.slicedQueryUrl,
|
|
1293
|
+
appliedFilters: slice.appliedFilters,
|
|
1294
|
+
depth: slice.depth,
|
|
1295
|
+
splitTrail: slice.splitTrail,
|
|
1296
|
+
slicePreset: slice.slicePreset,
|
|
1297
|
+
maxResultsPerSearch: slice.maxResultsPerSearch,
|
|
1298
|
+
numberOfProfiles: slice.numberOfProfiles,
|
|
1299
|
+
retryCount: slice.retryCount,
|
|
1300
|
+
cookieRetryCount: slice.cookieRetryCount,
|
|
1301
|
+
resultRetryCount: slice.resultRetryCount
|
|
1302
|
+
}, {
|
|
1303
|
+
traceId: traceId ?? null,
|
|
1304
|
+
sliceId: slice.id,
|
|
1305
|
+
jobId: slice.jobId,
|
|
1306
|
+
...extra
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
function describeSalesNavigatorLaunchDiagnostics(diagnostics) {
|
|
1310
|
+
if (!diagnostics) {
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
const parts = [
|
|
1314
|
+
`agent ${diagnostics.selectedAgent.name} (${diagnostics.selectedAgent.id})`,
|
|
1315
|
+
diagnostics.runningAgentIds.length > 0
|
|
1316
|
+
? `running: ${diagnostics.runningAgentIds.join(", ")}`
|
|
1317
|
+
: null,
|
|
1318
|
+
diagnostics.busyAgentIds.length > 0
|
|
1319
|
+
? `busy fallback: ${diagnostics.busyAgentIds.join(", ")}`
|
|
1320
|
+
: null,
|
|
1321
|
+
diagnostics.selectedAgent.maxParallelism !== null
|
|
1322
|
+
? `parallelism ${diagnostics.selectedAgent.maxParallelism}`
|
|
1323
|
+
: null,
|
|
1324
|
+
diagnostics.selectedAgent.fileMgmt
|
|
1325
|
+
? `file mgmt ${diagnostics.selectedAgent.fileMgmt}`
|
|
1326
|
+
: null,
|
|
1327
|
+
diagnostics.selectedAgent.hasWebhook ? "webhook on" : "webhook off",
|
|
1328
|
+
diagnostics.selectedAgent.hasStoredSessionCookie
|
|
1329
|
+
? "stored phantom cookie present"
|
|
1330
|
+
: "stored phantom cookie cleared at launch",
|
|
1331
|
+
].filter((value) => Boolean(value));
|
|
1332
|
+
return parts.join("; ");
|
|
1333
|
+
}
|
|
1334
|
+
function writeSalesNavigatorLaunchDiagnosticsProgress(diagnostics, selectedSessionUserEmail) {
|
|
1335
|
+
if (runtimeOutputOptions.json || runtimeOutputOptions.quiet || !diagnostics) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const details = describeSalesNavigatorLaunchDiagnostics(diagnostics);
|
|
1339
|
+
const operator = selectedSessionUserEmail ? ` using ${selectedSessionUserEmail}` : "";
|
|
1340
|
+
process.stderr.write(`Phantombuster launch selected ${diagnostics.selectedAgent.id}${operator}.${details ? ` ${details}` : ""}\n`);
|
|
1341
|
+
}
|
|
1342
|
+
async function runSalesNavigatorExport(session, payload, traceId, logOptions = {}) {
|
|
1343
|
+
const baseMetadata = {
|
|
1344
|
+
sourceQueryUrl: payload.sourceQueryUrl,
|
|
1345
|
+
slicedQueryUrl: payload.slicedQueryUrl,
|
|
1346
|
+
slicePreset: payload.slicePreset,
|
|
1347
|
+
maxResultsPerSearch: payload.maxResultsPerSearch,
|
|
1348
|
+
numberOfProfiles: payload.numberOfProfiles,
|
|
1349
|
+
filterTypes: payload.appliedFilters.map((filter) => filter.type),
|
|
1350
|
+
...logOptions.metadata
|
|
1351
|
+
};
|
|
1352
|
+
await logOptions.logger?.log("salesnav.export.started", baseMetadata);
|
|
1353
|
+
try {
|
|
1354
|
+
const started = await startSalesNavigatorExport(session, payload, traceId);
|
|
1355
|
+
await logOptions.logger?.log("salesnav.export.accepted", {
|
|
1356
|
+
...baseMetadata,
|
|
1357
|
+
runId: started.value.runId,
|
|
1358
|
+
agentId: started.value.agentId,
|
|
1359
|
+
containerId: started.value.containerId,
|
|
1360
|
+
previousContainerId: started.value.previousContainerId ?? null,
|
|
1361
|
+
selectedSessionCookieSha256: started.value.selectedSessionCookieSha256 ?? null,
|
|
1362
|
+
selectedSessionUserEmail: started.value.selectedSessionUserEmail ?? null,
|
|
1363
|
+
selectedSessionUserHandle: started.value.selectedSessionUserHandle ?? null,
|
|
1364
|
+
launchDiagnostics: started.value.launchDiagnostics ?? null
|
|
1365
|
+
});
|
|
1366
|
+
writeSalesNavigatorLaunchDiagnosticsProgress(started.value.launchDiagnostics ?? null, started.value.selectedSessionUserEmail ?? null);
|
|
1367
|
+
const completed = await waitForSalesNavigatorExportRunCompletion(started.session, started.value.runId, {}, traceId, {
|
|
1368
|
+
logger: logOptions.logger,
|
|
1369
|
+
metadata: baseMetadata
|
|
1370
|
+
});
|
|
1371
|
+
await logOptions.logger?.log("salesnav.export.completed", {
|
|
1372
|
+
...baseMetadata,
|
|
1373
|
+
runId: completed.value.run.id,
|
|
1374
|
+
status: completed.value.run.status,
|
|
1375
|
+
resultClassification: completed.value.run.resultClassification,
|
|
1376
|
+
totalResults: completed.value.run.totalResults ?? null,
|
|
1377
|
+
imported: completed.value.run.imported,
|
|
1378
|
+
upserted: completed.value.run.upserted,
|
|
1379
|
+
updatedAt: completed.value.run.updatedAt,
|
|
1380
|
+
finishedAt: completed.value.run.finishedAt ?? null
|
|
1381
|
+
});
|
|
1382
|
+
const mapped = mapCompletedSalesNavigatorExportRun(completed.value.run);
|
|
1383
|
+
return SalesNavigatorExportResponseSchema.parse({
|
|
1384
|
+
...mapped,
|
|
1385
|
+
launchDiagnostics: mapped.launchDiagnostics ?? started.value.launchDiagnostics ?? null,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
catch (error) {
|
|
1389
|
+
await logOptions.logger?.log("salesnav.export.failed", {
|
|
1390
|
+
...baseMetadata,
|
|
1391
|
+
name: error instanceof Error ? error.name : "Error",
|
|
1392
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1393
|
+
...(error instanceof SalesNavigatorExportRequestError
|
|
1394
|
+
? {
|
|
1395
|
+
runId: error.runId ?? null,
|
|
1396
|
+
agentId: error.agentId ?? null,
|
|
1397
|
+
containerId: error.containerId ?? null,
|
|
1398
|
+
errorCode: error.errorCode ?? null,
|
|
1399
|
+
totalResults: error.totalResults ?? null,
|
|
1400
|
+
launchDiagnostics: error.launchDiagnostics ?? null,
|
|
1401
|
+
statusCode: error.statusCode
|
|
1402
|
+
}
|
|
1403
|
+
: {})
|
|
1404
|
+
});
|
|
1405
|
+
throw error;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
async function startSalesNavigatorExport(session, payload, traceId) {
|
|
797
1409
|
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/export`, {
|
|
798
1410
|
method: "POST",
|
|
1411
|
+
signal: AbortSignal.timeout(SALES_NAVIGATOR_EXPORT_START_TIMEOUT_MS),
|
|
799
1412
|
headers: {
|
|
800
1413
|
"Content-Type": "application/json",
|
|
801
|
-
Authorization: `Bearer ${currentSession.accessToken}
|
|
1414
|
+
Authorization: `Bearer ${currentSession.accessToken}`,
|
|
1415
|
+
...buildTraceHeaders(traceId)
|
|
802
1416
|
},
|
|
803
1417
|
body: JSON.stringify({
|
|
804
1418
|
...payload,
|
|
@@ -806,11 +1420,12 @@ async function startSalesNavigatorExport(session, payload) {
|
|
|
806
1420
|
})
|
|
807
1421
|
}), SalesNavigatorExportStartResponseSchema);
|
|
808
1422
|
}
|
|
809
|
-
async function getSalesNavigatorExportRunStatus(session, runId) {
|
|
1423
|
+
async function getSalesNavigatorExportRunStatus(session, runId, traceId) {
|
|
810
1424
|
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/export-runs/${runId}?refresh=1`, {
|
|
811
1425
|
method: "GET",
|
|
812
1426
|
headers: {
|
|
813
|
-
Authorization: `Bearer ${currentSession.accessToken}
|
|
1427
|
+
Authorization: `Bearer ${currentSession.accessToken}`,
|
|
1428
|
+
...buildTraceHeaders(traceId)
|
|
814
1429
|
}
|
|
815
1430
|
}), SalesNavigatorExportRunStatusResponseSchema);
|
|
816
1431
|
}
|
|
@@ -831,7 +1446,8 @@ function mapCompletedSalesNavigatorExportRun(run) {
|
|
|
831
1446
|
totalResults: run.totalResults ?? null,
|
|
832
1447
|
runId: run.id,
|
|
833
1448
|
agentId: run.agentId,
|
|
834
|
-
containerId: run.containerId
|
|
1449
|
+
containerId: run.containerId,
|
|
1450
|
+
launchDiagnostics: run.launchDiagnostics ?? null
|
|
835
1451
|
});
|
|
836
1452
|
}
|
|
837
1453
|
return SalesNavigatorExportResponseSchema.parse({
|
|
@@ -844,18 +1460,35 @@ function mapCompletedSalesNavigatorExportRun(run) {
|
|
|
844
1460
|
resultCsvUrl: run.resultCsvUrl ?? null,
|
|
845
1461
|
agentId: run.agentId,
|
|
846
1462
|
containerId: run.containerId,
|
|
1463
|
+
selectedSessionCookieSha256: run.selectedSessionCookieSha256 ?? null,
|
|
1464
|
+
selectedSessionUserEmail: run.selectedSessionUserEmail ?? null,
|
|
1465
|
+
selectedSessionUserHandle: run.selectedSessionUserHandle ?? null,
|
|
1466
|
+
launchDiagnostics: run.launchDiagnostics ?? null,
|
|
847
1467
|
sourceQueryUrl: run.sourceQueryUrl,
|
|
848
1468
|
slicedQueryUrl: run.slicedQueryUrl
|
|
849
1469
|
});
|
|
850
1470
|
}
|
|
851
|
-
async function waitForSalesNavigatorExportRunCompletion(session, runId, options = {}) {
|
|
1471
|
+
async function waitForSalesNavigatorExportRunCompletion(session, runId, options = {}, traceId, logOptions = {}) {
|
|
852
1472
|
const timeoutSeconds = options.timeoutSeconds ?? 960;
|
|
853
1473
|
const pollIntervalMs = options.pollIntervalMs ?? 5000;
|
|
854
1474
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
855
1475
|
let currentSession = session;
|
|
1476
|
+
let pollCount = 0;
|
|
856
1477
|
while (Date.now() < deadline) {
|
|
857
|
-
const status = await getSalesNavigatorExportRunStatus(currentSession, runId);
|
|
1478
|
+
const status = await getSalesNavigatorExportRunStatus(currentSession, runId, traceId);
|
|
858
1479
|
currentSession = status.session;
|
|
1480
|
+
pollCount += 1;
|
|
1481
|
+
await logOptions.logger?.log("salesnav.export.polled", {
|
|
1482
|
+
runId,
|
|
1483
|
+
pollCount,
|
|
1484
|
+
status: status.value.run.status,
|
|
1485
|
+
resultClassification: status.value.run.resultClassification,
|
|
1486
|
+
totalResults: status.value.run.totalResults ?? null,
|
|
1487
|
+
imported: status.value.run.imported,
|
|
1488
|
+
upserted: status.value.run.upserted,
|
|
1489
|
+
updatedAt: status.value.run.updatedAt,
|
|
1490
|
+
...logOptions.metadata
|
|
1491
|
+
});
|
|
859
1492
|
if (status.value.run.status !== "pending") {
|
|
860
1493
|
return status;
|
|
861
1494
|
}
|
|
@@ -871,12 +1504,16 @@ function isSalesNavigatorAgentBusyError(error) {
|
|
|
871
1504
|
return /parallel executions limit/i.test(message);
|
|
872
1505
|
}
|
|
873
1506
|
function isSalesNavigatorSessionError(error) {
|
|
874
|
-
if (error instanceof SalesNavigatorExportRequestError
|
|
875
|
-
|
|
876
|
-
|
|
1507
|
+
if (error instanceof SalesNavigatorExportRequestError) {
|
|
1508
|
+
if (error.errorCode === "invalid_session") {
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
if (["phantombuster_cant_connect_profile", "salesnav_upsell_detected", "linkedin_session_invalid"].includes(error.errorCode ?? "")) {
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
877
1514
|
}
|
|
878
1515
|
const message = error instanceof Error ? error.message : String(error);
|
|
879
|
-
return /can't connect profile|sales navigator account|upsell|linkedin session invalid/i.test(message);
|
|
1516
|
+
return /can't connect profile|sales navigator account|upsell|linkedin session invalid|linkedin_rate_limited|too many requests|rate.?limit|invalid session cookie/i.test(message);
|
|
880
1517
|
}
|
|
881
1518
|
function isSalesNavigatorResultArtifactError(error) {
|
|
882
1519
|
if (error instanceof SalesNavigatorExportRequestError && error.errorCode === "phantombuster_result_invalid") {
|
|
@@ -899,13 +1536,16 @@ function isRefreshableAuthError(error) {
|
|
|
899
1536
|
const message = error instanceof Error ? error.message : String(error);
|
|
900
1537
|
return /token expired|session expired|not logged in|missing bearer token/i.test(message);
|
|
901
1538
|
}
|
|
902
|
-
async function runSalesNavigatorExportWithAgentWait(session, payload, options) {
|
|
1539
|
+
async function runSalesNavigatorExportWithAgentWait(session, payload, options, traceId) {
|
|
903
1540
|
let busyWaitCount = 0;
|
|
904
1541
|
let currentSession = session;
|
|
905
1542
|
let authRefreshCount = 0;
|
|
906
1543
|
while (true) {
|
|
907
1544
|
try {
|
|
908
|
-
return await runSalesNavigatorExport(currentSession, payload
|
|
1545
|
+
return await runSalesNavigatorExport(currentSession, payload, traceId, {
|
|
1546
|
+
logger: options.logger,
|
|
1547
|
+
metadata: options.logMetadata
|
|
1548
|
+
});
|
|
909
1549
|
}
|
|
910
1550
|
catch (error) {
|
|
911
1551
|
if (isRefreshableAuthError(error)) {
|
|
@@ -916,6 +1556,12 @@ async function runSalesNavigatorExportWithAgentWait(session, payload, options) {
|
|
|
916
1556
|
if (!runtimeOutputOptions.quiet) {
|
|
917
1557
|
process.stderr.write("Salesprompter session expired during crawl. Refreshing login...\n");
|
|
918
1558
|
}
|
|
1559
|
+
await options.logger?.log("salesnav.export.auth.refresh", {
|
|
1560
|
+
authRefreshCount,
|
|
1561
|
+
waitSeconds: options.waitSeconds,
|
|
1562
|
+
maxWaits: options.maxWaits,
|
|
1563
|
+
...options.logMetadata
|
|
1564
|
+
});
|
|
919
1565
|
await ensureInteractiveAuthSession(currentSession.apiBaseUrl);
|
|
920
1566
|
currentSession = await requireAuthSession();
|
|
921
1567
|
continue;
|
|
@@ -928,6 +1574,12 @@ async function runSalesNavigatorExportWithAgentWait(session, payload, options) {
|
|
|
928
1574
|
if (!runtimeOutputOptions.json && !runtimeOutputOptions.quiet) {
|
|
929
1575
|
process.stderr.write(`Sales Navigator export agent is busy. Waiting ${options.waitSeconds}s before retrying...\n`);
|
|
930
1576
|
}
|
|
1577
|
+
await options.logger?.log("salesnav.export.agent.busy", {
|
|
1578
|
+
busyWaitCount,
|
|
1579
|
+
waitSeconds: options.waitSeconds,
|
|
1580
|
+
maxWaits: options.maxWaits,
|
|
1581
|
+
...options.logMetadata
|
|
1582
|
+
});
|
|
931
1583
|
await delay(options.waitSeconds * 1000);
|
|
932
1584
|
continue;
|
|
933
1585
|
}
|
|
@@ -940,6 +1592,14 @@ async function runSalesNavigatorCrawlAttempt(session, attempt, options, context)
|
|
|
940
1592
|
options.probeProfiles < attempt.numberOfProfiles &&
|
|
941
1593
|
attempt.depth < options.maxSplitDepth;
|
|
942
1594
|
const probeProfiles = shouldProbe ? Math.max(1, options.probeProfiles) : attempt.numberOfProfiles;
|
|
1595
|
+
const logMetadata = {
|
|
1596
|
+
crawlJobId: context?.crawlJobId ?? null,
|
|
1597
|
+
crawlSliceId: context?.crawlSliceId ?? null,
|
|
1598
|
+
sliceDepth: attempt.depth,
|
|
1599
|
+
splitTrail: formatSalesNavigatorSplitTrail(attempt.splitTrail),
|
|
1600
|
+
sourceQueryUrl: attempt.sourceQueryUrl,
|
|
1601
|
+
slicedQueryUrl: attempt.slicedQueryUrl
|
|
1602
|
+
};
|
|
943
1603
|
const probeResult = await runSalesNavigatorExportWithAgentWait(session, {
|
|
944
1604
|
sourceQueryUrl: attempt.sourceQueryUrl,
|
|
945
1605
|
slicedQueryUrl: attempt.slicedQueryUrl,
|
|
@@ -948,11 +1608,23 @@ async function runSalesNavigatorCrawlAttempt(session, attempt, options, context)
|
|
|
948
1608
|
numberOfProfiles: probeProfiles,
|
|
949
1609
|
slicePreset: attempt.slicePreset,
|
|
950
1610
|
crawlJobId: context?.crawlJobId,
|
|
951
|
-
crawlSliceId: context?.crawlSliceId
|
|
1611
|
+
crawlSliceId: context?.crawlSliceId,
|
|
1612
|
+
rawPayload: buildSalesNavigatorSliceRawPayload(attempt, {
|
|
1613
|
+
traceId: context?.traceId ?? null,
|
|
1614
|
+
phase: shouldProbe ? "probe" : "full_export",
|
|
1615
|
+
requestedProfiles: probeProfiles,
|
|
1616
|
+
crawlJobId: context?.crawlJobId ?? null,
|
|
1617
|
+
crawlSliceId: context?.crawlSliceId ?? null
|
|
1618
|
+
})
|
|
952
1619
|
}, {
|
|
953
1620
|
waitSeconds: options.agentBusyWaitSeconds,
|
|
954
|
-
maxWaits: options.agentBusyMaxWaits
|
|
955
|
-
|
|
1621
|
+
maxWaits: options.agentBusyMaxWaits,
|
|
1622
|
+
logger: options.logger,
|
|
1623
|
+
logMetadata: {
|
|
1624
|
+
...logMetadata,
|
|
1625
|
+
phase: shouldProbe ? "probe" : "full_export"
|
|
1626
|
+
}
|
|
1627
|
+
}, context?.traceId);
|
|
956
1628
|
if (!shouldProbe) {
|
|
957
1629
|
return probeResult;
|
|
958
1630
|
}
|
|
@@ -968,11 +1640,27 @@ async function runSalesNavigatorCrawlAttempt(session, attempt, options, context)
|
|
|
968
1640
|
numberOfProfiles: attempt.numberOfProfiles,
|
|
969
1641
|
slicePreset: attempt.slicePreset,
|
|
970
1642
|
crawlJobId: context?.crawlJobId,
|
|
971
|
-
crawlSliceId: context?.crawlSliceId
|
|
1643
|
+
crawlSliceId: context?.crawlSliceId,
|
|
1644
|
+
rawPayload: buildSalesNavigatorSliceRawPayload(attempt, {
|
|
1645
|
+
traceId: context?.traceId ?? null,
|
|
1646
|
+
phase: "full_export_after_probe",
|
|
1647
|
+
requestedProfiles: attempt.numberOfProfiles,
|
|
1648
|
+
crawlJobId: context?.crawlJobId ?? null,
|
|
1649
|
+
crawlSliceId: context?.crawlSliceId ?? null,
|
|
1650
|
+
probeProfiles,
|
|
1651
|
+
probeTotalResults: totalResults
|
|
1652
|
+
})
|
|
972
1653
|
}, {
|
|
973
1654
|
waitSeconds: options.agentBusyWaitSeconds,
|
|
974
|
-
maxWaits: options.agentBusyMaxWaits
|
|
975
|
-
|
|
1655
|
+
maxWaits: options.agentBusyMaxWaits,
|
|
1656
|
+
logger: options.logger,
|
|
1657
|
+
logMetadata: {
|
|
1658
|
+
...logMetadata,
|
|
1659
|
+
phase: "full_export_after_probe",
|
|
1660
|
+
probeProfiles,
|
|
1661
|
+
probeTotalResults: totalResults
|
|
1662
|
+
}
|
|
1663
|
+
}, context?.traceId);
|
|
976
1664
|
}
|
|
977
1665
|
function buildSalesNavigatorCrawlAttemptFromClaimedSlice(slice) {
|
|
978
1666
|
return {
|
|
@@ -987,12 +1675,13 @@ function buildSalesNavigatorCrawlAttemptFromClaimedSlice(slice) {
|
|
|
987
1675
|
splitTrail: slice.splitTrail
|
|
988
1676
|
};
|
|
989
1677
|
}
|
|
990
|
-
async function createOrResumeSalesNavigatorCrawlJob(session, payload) {
|
|
1678
|
+
async function createOrResumeSalesNavigatorCrawlJob(session, payload, traceId) {
|
|
991
1679
|
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/crawls`, {
|
|
992
1680
|
method: "POST",
|
|
993
1681
|
headers: {
|
|
994
1682
|
"Content-Type": "application/json",
|
|
995
|
-
Authorization: `Bearer ${currentSession.accessToken}
|
|
1683
|
+
Authorization: `Bearer ${currentSession.accessToken}`,
|
|
1684
|
+
...buildTraceHeaders(traceId)
|
|
996
1685
|
},
|
|
997
1686
|
body: JSON.stringify({
|
|
998
1687
|
...payload,
|
|
@@ -1003,28 +1692,31 @@ async function createOrResumeSalesNavigatorCrawlJob(session, payload) {
|
|
|
1003
1692
|
})
|
|
1004
1693
|
}), SalesNavigatorCrawlCreateResponseSchema);
|
|
1005
1694
|
}
|
|
1006
|
-
async function getSalesNavigatorCrawlStatus(session, jobId) {
|
|
1695
|
+
async function getSalesNavigatorCrawlStatus(session, jobId, traceId) {
|
|
1007
1696
|
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/crawls/${jobId}`, {
|
|
1008
1697
|
method: "GET",
|
|
1009
1698
|
headers: {
|
|
1010
|
-
Authorization: `Bearer ${currentSession.accessToken}
|
|
1699
|
+
Authorization: `Bearer ${currentSession.accessToken}`,
|
|
1700
|
+
...buildTraceHeaders(traceId)
|
|
1011
1701
|
}
|
|
1012
1702
|
}), SalesNavigatorCrawlStatusResponseSchema);
|
|
1013
1703
|
}
|
|
1014
|
-
async function claimNextSalesNavigatorCrawlSlice(session, jobId) {
|
|
1704
|
+
async function claimNextSalesNavigatorCrawlSlice(session, jobId, traceId) {
|
|
1015
1705
|
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/crawls/${jobId}/claim-next`, {
|
|
1016
1706
|
method: "POST",
|
|
1017
1707
|
headers: {
|
|
1018
|
-
Authorization: `Bearer ${currentSession.accessToken}
|
|
1708
|
+
Authorization: `Bearer ${currentSession.accessToken}`,
|
|
1709
|
+
...buildTraceHeaders(traceId)
|
|
1019
1710
|
}
|
|
1020
1711
|
}), SalesNavigatorCrawlClaimResponseSchema);
|
|
1021
1712
|
}
|
|
1022
|
-
async function reportSalesNavigatorCrawlSlice(session, jobId, payload) {
|
|
1713
|
+
async function reportSalesNavigatorCrawlSlice(session, jobId, payload, traceId) {
|
|
1023
1714
|
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/crawls/${jobId}/report`, {
|
|
1024
1715
|
method: "POST",
|
|
1025
1716
|
headers: {
|
|
1026
1717
|
"Content-Type": "application/json",
|
|
1027
|
-
Authorization: `Bearer ${currentSession.accessToken}
|
|
1718
|
+
Authorization: `Bearer ${currentSession.accessToken}`,
|
|
1719
|
+
...buildTraceHeaders(traceId)
|
|
1028
1720
|
},
|
|
1029
1721
|
body: JSON.stringify({
|
|
1030
1722
|
...payload,
|
|
@@ -1049,7 +1741,13 @@ function buildSalesNavigatorSplitChildren(slice, dimension) {
|
|
|
1049
1741
|
slicedQueryUrl: child.slicedQueryUrl,
|
|
1050
1742
|
appliedFilters: child.appliedFilters,
|
|
1051
1743
|
depth: child.depth,
|
|
1052
|
-
splitTrail: child.splitTrail
|
|
1744
|
+
splitTrail: child.splitTrail,
|
|
1745
|
+
rawPayload: buildSalesNavigatorSliceRawPayload(child, {
|
|
1746
|
+
parentSliceId: slice.id,
|
|
1747
|
+
parentSlicedQueryUrl: slice.slicedQueryUrl,
|
|
1748
|
+
splitDimensionKey: child.splitTrail.at(-1)?.key ?? null,
|
|
1749
|
+
splitDimensionFilterType: child.splitTrail.at(-1)?.filterType ?? null
|
|
1750
|
+
})
|
|
1053
1751
|
}));
|
|
1054
1752
|
}
|
|
1055
1753
|
function buildSalesNavigatorSliceFailureReport(slice, error, options) {
|
|
@@ -1114,73 +1812,301 @@ function buildSalesNavigatorSliceFailureReport(slice, error, options) {
|
|
|
1114
1812
|
function formatSalesNavigatorSplitTrail(splitTrail) {
|
|
1115
1813
|
return splitTrail.map((entry) => `${entry.key}:${entry.value.text}`);
|
|
1116
1814
|
}
|
|
1117
|
-
async function
|
|
1815
|
+
async function processSalesNavigatorClaimedCrawlSlice(session, jobId, slice, options) {
|
|
1118
1816
|
let currentSession = session;
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
claimedSlices
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
const
|
|
1138
|
-
maxSplitDepth: options.maxSplitDepth,
|
|
1139
|
-
probeProfiles: options.probeProfiles,
|
|
1140
|
-
agentBusyWaitSeconds: options.agentBusyWaitSeconds,
|
|
1141
|
-
agentBusyMaxWaits: options.agentBusyMaxWaits
|
|
1142
|
-
}, {
|
|
1143
|
-
crawlJobId: jobId,
|
|
1144
|
-
crawlSliceId: slice.id
|
|
1145
|
-
});
|
|
1817
|
+
await options.logger?.log("salesnav.crawl.slice.claimed", {
|
|
1818
|
+
jobId,
|
|
1819
|
+
sliceId: slice.id,
|
|
1820
|
+
isNewSlice: true,
|
|
1821
|
+
claimedSlices: options.claimedSlices,
|
|
1822
|
+
depth: slice.depth,
|
|
1823
|
+
retryCount: slice.retryCount,
|
|
1824
|
+
cookieRetryCount: slice.cookieRetryCount,
|
|
1825
|
+
resultRetryCount: slice.resultRetryCount,
|
|
1826
|
+
splitTrail: formatSalesNavigatorSplitTrail(slice.splitTrail),
|
|
1827
|
+
...summarizeSalesNavigatorQuery(slice.slicedQueryUrl, slice.appliedFilters)
|
|
1828
|
+
});
|
|
1829
|
+
if (!runtimeOutputOptions.json && !runtimeOutputOptions.quiet) {
|
|
1830
|
+
process.stderr.write(`Processing Sales Navigator slice ${options.claimedSlices}: ${slice.slicedQueryUrl}\n`);
|
|
1831
|
+
}
|
|
1832
|
+
if (shouldPreSplitSalesNavigatorRootSlice(slice, options.maxSplitDepth)) {
|
|
1833
|
+
const nextDimension = nextSalesNavigatorSplitDimension(slice, options.maxSplitDepth);
|
|
1834
|
+
if (nextDimension) {
|
|
1835
|
+
const children = buildSalesNavigatorSplitChildren(slice, nextDimension);
|
|
1146
1836
|
const reported = await reportSalesNavigatorCrawlSlice(currentSession, jobId, {
|
|
1147
1837
|
sliceId: slice.id,
|
|
1148
|
-
outcome: "
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1838
|
+
outcome: "split",
|
|
1839
|
+
error: `Pre-splitting broad Sales Navigator title query by ${nextDimension.key} before the first export attempt.`,
|
|
1840
|
+
errorCode: "presplit_root_title_query",
|
|
1841
|
+
children,
|
|
1842
|
+
rawPayload: buildSalesNavigatorCrawlReportRawPayload(slice, options.traceId, {
|
|
1843
|
+
phase: "presplit",
|
|
1844
|
+
reason: "broad_root_title_query",
|
|
1845
|
+
nextDimensionKey: nextDimension.key,
|
|
1846
|
+
nextDimensionFilterType: nextDimension.filterType,
|
|
1847
|
+
childCount: children.length
|
|
1848
|
+
})
|
|
1849
|
+
}, options.traceId);
|
|
1154
1850
|
currentSession = reported.session;
|
|
1155
|
-
|
|
1156
|
-
|
|
1851
|
+
await options.logger?.log("salesnav.crawl.slice.presplit", {
|
|
1852
|
+
jobId,
|
|
1853
|
+
sliceId: slice.id,
|
|
1854
|
+
nextDimension: nextDimension.key,
|
|
1855
|
+
childCount: children.length,
|
|
1856
|
+
splitTrail: formatSalesNavigatorSplitTrail(slice.splitTrail),
|
|
1857
|
+
...summarizeSalesNavigatorQuery(slice.slicedQueryUrl, slice.appliedFilters),
|
|
1858
|
+
childQueries: children.map((child) => ({
|
|
1859
|
+
splitTrail: formatSalesNavigatorSplitTrail(child.splitTrail),
|
|
1860
|
+
...summarizeSalesNavigatorQuery(child.slicedQueryUrl, child.appliedFilters)
|
|
1861
|
+
}))
|
|
1862
|
+
});
|
|
1863
|
+
return {
|
|
1864
|
+
session: currentSession,
|
|
1865
|
+
job: reported.value.job,
|
|
1866
|
+
activeSlice: slice,
|
|
1867
|
+
lastOutcome: {
|
|
1868
|
+
outcome: "split",
|
|
1869
|
+
error: `Pre-split by ${nextDimension.key}`,
|
|
1870
|
+
errorCode: "presplit_root_title_query",
|
|
1871
|
+
totalResults: null
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
try {
|
|
1877
|
+
const result = await runSalesNavigatorCrawlAttempt(currentSession, buildSalesNavigatorCrawlAttemptFromClaimedSlice(slice), {
|
|
1878
|
+
maxSplitDepth: options.maxSplitDepth,
|
|
1879
|
+
probeProfiles: options.probeProfiles,
|
|
1880
|
+
agentBusyWaitSeconds: options.agentBusyWaitSeconds,
|
|
1881
|
+
agentBusyMaxWaits: options.agentBusyMaxWaits,
|
|
1882
|
+
logger: options.logger
|
|
1883
|
+
}, {
|
|
1884
|
+
crawlJobId: jobId,
|
|
1885
|
+
crawlSliceId: slice.id,
|
|
1886
|
+
traceId: options.traceId
|
|
1887
|
+
});
|
|
1888
|
+
const reported = await reportSalesNavigatorCrawlSlice(currentSession, jobId, {
|
|
1889
|
+
sliceId: slice.id,
|
|
1890
|
+
outcome: "exported",
|
|
1891
|
+
totalResults: result.totalResults ?? null,
|
|
1892
|
+
exportRunId: result.runId,
|
|
1893
|
+
importedPeople: result.imported,
|
|
1894
|
+
upsertedPeople: result.upserted,
|
|
1895
|
+
rawPayload: buildSalesNavigatorCrawlReportRawPayload(slice, options.traceId, {
|
|
1896
|
+
phase: "exported",
|
|
1897
|
+
export: {
|
|
1898
|
+
runId: result.runId,
|
|
1899
|
+
totalResults: result.totalResults ?? null,
|
|
1900
|
+
imported: result.imported,
|
|
1901
|
+
upserted: result.upserted,
|
|
1902
|
+
resultJsonUrl: result.resultJsonUrl ?? null,
|
|
1903
|
+
resultCsvUrl: result.resultCsvUrl ?? null,
|
|
1904
|
+
selectedSessionCookieSha256: result.selectedSessionCookieSha256 ?? null,
|
|
1905
|
+
selectedSessionUserEmail: result.selectedSessionUserEmail ?? null,
|
|
1906
|
+
selectedSessionUserHandle: result.selectedSessionUserHandle ?? null,
|
|
1907
|
+
launchDiagnostics: result.launchDiagnostics ?? null
|
|
1908
|
+
}
|
|
1909
|
+
})
|
|
1910
|
+
}, options.traceId);
|
|
1911
|
+
currentSession = reported.session;
|
|
1912
|
+
await options.logger?.log("salesnav.crawl.slice.exported", {
|
|
1913
|
+
jobId,
|
|
1914
|
+
sliceId: slice.id,
|
|
1915
|
+
exportRunId: result.runId,
|
|
1916
|
+
totalResults: result.totalResults ?? null,
|
|
1917
|
+
imported: result.imported,
|
|
1918
|
+
upserted: result.upserted,
|
|
1919
|
+
selectedAgentId: result.launchDiagnostics?.selectedAgent.id ?? result.agentId,
|
|
1920
|
+
selectedSessionUserEmail: result.selectedSessionUserEmail ?? null
|
|
1921
|
+
});
|
|
1922
|
+
return {
|
|
1923
|
+
session: currentSession,
|
|
1924
|
+
job: reported.value.job,
|
|
1925
|
+
activeSlice: slice,
|
|
1926
|
+
lastOutcome: {
|
|
1157
1927
|
outcome: "exported",
|
|
1158
1928
|
runId: result.runId,
|
|
1159
1929
|
totalResults: result.totalResults ?? null
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
catch (error) {
|
|
1934
|
+
const payload = buildSalesNavigatorSliceFailureReport(slice, error, {
|
|
1935
|
+
maxSplitDepth: options.maxSplitDepth,
|
|
1936
|
+
maxRetries: options.maxRetries
|
|
1937
|
+
});
|
|
1938
|
+
payload.rawPayload = buildSalesNavigatorCrawlReportRawPayload(slice, options.traceId, {
|
|
1939
|
+
phase: payload.outcome,
|
|
1940
|
+
error: error instanceof Error
|
|
1941
|
+
? {
|
|
1942
|
+
name: error.name,
|
|
1943
|
+
message: error.message,
|
|
1944
|
+
...(error instanceof SalesNavigatorExportRequestError
|
|
1945
|
+
? {
|
|
1946
|
+
launchDiagnostics: error.launchDiagnostics ?? null,
|
|
1947
|
+
agentId: error.agentId ?? null,
|
|
1948
|
+
containerId: error.containerId ?? null
|
|
1949
|
+
}
|
|
1950
|
+
: {})
|
|
1951
|
+
}
|
|
1952
|
+
: {
|
|
1953
|
+
name: "Error",
|
|
1954
|
+
message: String(error)
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
const reported = await reportSalesNavigatorCrawlSlice(currentSession, jobId, payload, options.traceId);
|
|
1958
|
+
currentSession = reported.session;
|
|
1959
|
+
await options.logger?.log("salesnav.crawl.slice.reported", {
|
|
1960
|
+
jobId,
|
|
1961
|
+
sliceId: slice.id,
|
|
1962
|
+
outcome: payload.outcome,
|
|
1963
|
+
error: payload.error ?? null,
|
|
1964
|
+
errorCode: payload.errorCode ?? null,
|
|
1965
|
+
totalResults: payload.totalResults ?? null,
|
|
1966
|
+
exportRunId: payload.exportRunId ?? null,
|
|
1967
|
+
childCount: payload.children?.length ?? 0
|
|
1968
|
+
});
|
|
1969
|
+
return {
|
|
1970
|
+
session: currentSession,
|
|
1971
|
+
job: reported.value.job,
|
|
1972
|
+
activeSlice: slice,
|
|
1973
|
+
lastOutcome: {
|
|
1171
1974
|
outcome: payload.outcome,
|
|
1172
1975
|
runId: payload.exportRunId,
|
|
1173
1976
|
error: payload.error,
|
|
1174
1977
|
errorCode: payload.errorCode,
|
|
1175
1978
|
totalResults: payload.totalResults
|
|
1176
|
-
}
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
async function executeSalesNavigatorCrawlJob(session, jobId, options) {
|
|
1984
|
+
let currentSession = session;
|
|
1985
|
+
let claimedSlices = 0;
|
|
1986
|
+
const seenSliceIds = new Set();
|
|
1987
|
+
let activeSlice = null;
|
|
1988
|
+
let job = null;
|
|
1989
|
+
let idlePollCount = 0;
|
|
1990
|
+
let lastOutcome = null;
|
|
1991
|
+
const parallelExports = Math.max(1, options.parallelExports);
|
|
1992
|
+
const inFlight = new Map();
|
|
1993
|
+
let nextSlot = 0;
|
|
1994
|
+
let noMoreClaimableWork = false;
|
|
1995
|
+
while (true) {
|
|
1996
|
+
while (!noMoreClaimableWork && inFlight.size < parallelExports) {
|
|
1997
|
+
if (claimedSlices >= options.maxSlices) {
|
|
1998
|
+
break;
|
|
1999
|
+
}
|
|
2000
|
+
const claimed = await claimNextSalesNavigatorCrawlSlice(currentSession, jobId, options.traceId);
|
|
2001
|
+
currentSession = claimed.session;
|
|
2002
|
+
job = claimed.value.job;
|
|
2003
|
+
if (!claimed.value.slice) {
|
|
2004
|
+
const shouldWaitForRemoteWork = !isSalesNavigatorCrawlJobTerminal(job.status) &&
|
|
2005
|
+
options.idleMaxPolls > 0 &&
|
|
2006
|
+
job.runningSlices > 0;
|
|
2007
|
+
if (shouldWaitForRemoteWork && inFlight.size === 0) {
|
|
2008
|
+
if (idlePollCount >= options.idleMaxPolls) {
|
|
2009
|
+
lastOutcome = {
|
|
2010
|
+
outcome: "terminal_failed",
|
|
2011
|
+
error: `Sales Navigator crawl job ${jobId} stayed non-terminal without a claimable slice after ${options.idleMaxPolls} polls.`,
|
|
2012
|
+
errorCode: "crawl_idle_timeout"
|
|
2013
|
+
};
|
|
2014
|
+
await options.logger?.log("salesnav.crawl.job.stalled", {
|
|
2015
|
+
jobId,
|
|
2016
|
+
status: job.status,
|
|
2017
|
+
queuedSlices: job.queuedSlices,
|
|
2018
|
+
runningSlices: job.runningSlices,
|
|
2019
|
+
idlePollCount,
|
|
2020
|
+
idleMaxPolls: options.idleMaxPolls
|
|
2021
|
+
});
|
|
2022
|
+
noMoreClaimableWork = true;
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
idlePollCount += 1;
|
|
2026
|
+
await options.logger?.log("salesnav.crawl.job.waiting", {
|
|
2027
|
+
jobId,
|
|
2028
|
+
status: job.status,
|
|
2029
|
+
queuedSlices: job.queuedSlices,
|
|
2030
|
+
runningSlices: job.runningSlices,
|
|
2031
|
+
idlePollCount,
|
|
2032
|
+
idlePollSeconds: options.idlePollSeconds
|
|
2033
|
+
});
|
|
2034
|
+
if (!runtimeOutputOptions.json && !runtimeOutputOptions.quiet) {
|
|
2035
|
+
process.stderr.write(`Sales Navigator crawl job ${jobId} has no claimable slice yet. Waiting ${options.idlePollSeconds}s for remote work to settle...\n`);
|
|
2036
|
+
}
|
|
2037
|
+
await delay(options.idlePollSeconds * 1000);
|
|
2038
|
+
const status = await getSalesNavigatorCrawlStatus(currentSession, jobId, options.traceId);
|
|
2039
|
+
currentSession = status.session;
|
|
2040
|
+
job = status.value.job;
|
|
2041
|
+
await options.logger?.log("salesnav.crawl.job.status.polled", {
|
|
2042
|
+
jobId,
|
|
2043
|
+
status: job.status,
|
|
2044
|
+
queuedSlices: job.queuedSlices,
|
|
2045
|
+
runningSlices: job.runningSlices,
|
|
2046
|
+
exportedSlices: job.exportedSlices,
|
|
2047
|
+
failedSlices: job.failedSlices,
|
|
2048
|
+
importedPeople: job.importedPeople,
|
|
2049
|
+
idlePollCount
|
|
2050
|
+
});
|
|
2051
|
+
if (isSalesNavigatorCrawlJobTerminal(job.status)) {
|
|
2052
|
+
noMoreClaimableWork = true;
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
if (!shouldWaitForRemoteWork) {
|
|
2058
|
+
noMoreClaimableWork = true;
|
|
2059
|
+
}
|
|
2060
|
+
break;
|
|
2061
|
+
}
|
|
2062
|
+
const slice = claimed.value.slice;
|
|
2063
|
+
idlePollCount = 0;
|
|
2064
|
+
activeSlice = slice;
|
|
2065
|
+
const isNewSlice = !seenSliceIds.has(slice.id);
|
|
2066
|
+
if (isNewSlice) {
|
|
2067
|
+
seenSliceIds.add(slice.id);
|
|
2068
|
+
claimedSlices += 1;
|
|
2069
|
+
}
|
|
2070
|
+
const claimedSliceNumber = claimedSlices;
|
|
2071
|
+
const slot = nextSlot++;
|
|
2072
|
+
inFlight.set(slot, processSalesNavigatorClaimedCrawlSlice(currentSession, jobId, slice, {
|
|
2073
|
+
maxSplitDepth: options.maxSplitDepth,
|
|
2074
|
+
maxRetries: options.maxRetries,
|
|
2075
|
+
probeProfiles: options.probeProfiles,
|
|
2076
|
+
agentBusyWaitSeconds: options.agentBusyWaitSeconds,
|
|
2077
|
+
agentBusyMaxWaits: options.agentBusyMaxWaits,
|
|
2078
|
+
claimedSlices: claimedSliceNumber,
|
|
2079
|
+
traceId: options.traceId,
|
|
2080
|
+
logger: options.logger
|
|
2081
|
+
}).then((value) => ({ slot, value })));
|
|
1177
2082
|
}
|
|
2083
|
+
if (inFlight.size === 0) {
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
const completed = await Promise.race(inFlight.values());
|
|
2087
|
+
inFlight.delete(completed.slot);
|
|
2088
|
+
currentSession = completed.value.session;
|
|
2089
|
+
job = completed.value.job;
|
|
2090
|
+
activeSlice = completed.value.activeSlice;
|
|
2091
|
+
lastOutcome = completed.value.lastOutcome;
|
|
1178
2092
|
}
|
|
1179
2093
|
if (!job) {
|
|
1180
|
-
const status = await getSalesNavigatorCrawlStatus(currentSession, jobId);
|
|
2094
|
+
const status = await getSalesNavigatorCrawlStatus(currentSession, jobId, options.traceId);
|
|
1181
2095
|
currentSession = status.session;
|
|
1182
2096
|
job = status.value.job;
|
|
1183
2097
|
}
|
|
2098
|
+
await options.logger?.log("salesnav.crawl.job.completed", {
|
|
2099
|
+
jobId,
|
|
2100
|
+
status: job.status,
|
|
2101
|
+
queuedSlices: job.queuedSlices,
|
|
2102
|
+
runningSlices: job.runningSlices,
|
|
2103
|
+
exportedSlices: job.exportedSlices,
|
|
2104
|
+
failedSlices: job.failedSlices,
|
|
2105
|
+
importedPeople: job.importedPeople,
|
|
2106
|
+
claimedSlices,
|
|
2107
|
+
truncated: claimedSlices >= options.maxSlices && (job.queuedSlices > 0 || job.runningSlices > 0),
|
|
2108
|
+
lastOutcome
|
|
2109
|
+
});
|
|
1184
2110
|
return {
|
|
1185
2111
|
session: currentSession,
|
|
1186
2112
|
job,
|
|
@@ -1190,22 +2116,6 @@ async function executeSalesNavigatorCrawlJob(session, jobId, options) {
|
|
|
1190
2116
|
lastOutcome
|
|
1191
2117
|
};
|
|
1192
2118
|
}
|
|
1193
|
-
async function searchReferenceCompanyLeads(reference, icp, limit) {
|
|
1194
|
-
if (shouldBypassAuth()) {
|
|
1195
|
-
const fallbackTargetDomain = reference.domain ?? `${reference.slug}.com`;
|
|
1196
|
-
const result = await leadProvider.generateLeads(icp, limit, {
|
|
1197
|
-
companyDomain: fallbackTargetDomain,
|
|
1198
|
-
companyName: reference.companyName
|
|
1199
|
-
});
|
|
1200
|
-
return result.leads;
|
|
1201
|
-
}
|
|
1202
|
-
const session = await requireAuthSession();
|
|
1203
|
-
return await fetchWorkspaceLeadSearch(session, {
|
|
1204
|
-
mode: "reference-company",
|
|
1205
|
-
icp,
|
|
1206
|
-
limit
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
2119
|
async function searchTargetCompanyLeads(reference, limit) {
|
|
1210
2120
|
if (shouldBypassAuth()) {
|
|
1211
2121
|
const fallbackTargetDomain = reference.domain ?? `${reference.slug}.com`;
|
|
@@ -1223,16 +2133,90 @@ async function searchTargetCompanyLeads(reference, limit) {
|
|
|
1223
2133
|
limit
|
|
1224
2134
|
});
|
|
1225
2135
|
}
|
|
1226
|
-
async function
|
|
1227
|
-
writeWizardSection("
|
|
1228
|
-
const
|
|
2136
|
+
async function runProductMarketWizard(rl) {
|
|
2137
|
+
writeWizardSection("Find leads from a product market", "Start from a company website, LinkedIn company page, product page, or category page. I will turn that into intended job titles and durable Sales Navigator crawls.");
|
|
2138
|
+
const input = await promptText(rl, "What company website or LinkedIn page should I start from?", {
|
|
2139
|
+
required: true
|
|
2140
|
+
});
|
|
2141
|
+
const productLimit = z.coerce.number().int().min(1).max(5000).parse(await promptText(rl, "How many products should I inspect?", { defaultValue: "25", required: true }));
|
|
2142
|
+
const titleLimit = z.coerce.number().int().min(1).max(1000).parse(await promptText(rl, "How many job titles should I turn into Sales Navigator crawls?", {
|
|
2143
|
+
defaultValue: "5",
|
|
2144
|
+
required: true
|
|
2145
|
+
}));
|
|
2146
|
+
writeWizardLine();
|
|
2147
|
+
const dryRun = shouldBypassAuth();
|
|
2148
|
+
if (dryRun) {
|
|
2149
|
+
writeWizardLine("Auth bypass is enabled, so I will preview the crawl plan instead of launching Phantombuster.");
|
|
2150
|
+
writeWizardLine();
|
|
2151
|
+
}
|
|
2152
|
+
const result = await runSalesNavigatorFromProductCategoryWorkflow({
|
|
2153
|
+
input,
|
|
2154
|
+
maxPages: 25,
|
|
2155
|
+
productLimit,
|
|
2156
|
+
titleLimit,
|
|
2157
|
+
maxResultsPerSearch: 2500,
|
|
2158
|
+
numberOfProfiles: 2500,
|
|
2159
|
+
slicePreset: "wizard-linkedin-product-category",
|
|
2160
|
+
maxSplitDepth: DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS.length,
|
|
2161
|
+
maxSlicesPerTitle: 1000,
|
|
2162
|
+
maxRetries: 3,
|
|
2163
|
+
probeProfiles: 100,
|
|
2164
|
+
agentBusyWaitSeconds: 30,
|
|
2165
|
+
agentBusyMaxWaits: 20,
|
|
2166
|
+
idlePollSeconds: 10,
|
|
2167
|
+
idleMaxPolls: 180,
|
|
2168
|
+
parallelExports: 3,
|
|
2169
|
+
skipProductUpload: false,
|
|
2170
|
+
dryRun
|
|
2171
|
+
});
|
|
2172
|
+
writeWizardLine(`LinkedIn product category: ${result.payload.source.category.name}.`);
|
|
2173
|
+
writeWizardLine(`Inspected ${result.payload.discoveredProducts} product${result.payload.discoveredProducts === 1 ? "" : "s"} and derived ${result.payload.titleCount} intended job title${result.payload.titleCount === 1 ? "" : "s"}.`);
|
|
2174
|
+
if (result.payload.dryRun) {
|
|
2175
|
+
const firstQuery = result.payload.queries?.[0];
|
|
2176
|
+
writeWizardLine(`Saved preview to ${result.outPath}.`);
|
|
2177
|
+
writeWizardLine(`Saved logs to ${result.payload.logPath}.`);
|
|
2178
|
+
if (firstQuery) {
|
|
2179
|
+
writeWizardLine(`First Sales Navigator title search: ${firstQuery.title}.`);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
else {
|
|
2183
|
+
if (result.payload.uploaded) {
|
|
2184
|
+
writeWizardLine(`Uploaded ${result.payload.uploaded.upserted} LinkedIn product record${result.payload.uploaded.upserted === 1 ? "" : "s"} to Salesprompter.`);
|
|
2185
|
+
}
|
|
2186
|
+
writeWizardLine(`Finished ${result.payload.crawls?.length ?? 0} durable Sales Navigator crawl${result.payload.crawls?.length === 1 ? "" : "s"}.`);
|
|
2187
|
+
writeWizardLine(`Imported ${result.payload.summary.totalImportedPeople} people across ${result.payload.summary.totalExportedSlices} exported slice${result.payload.summary.totalExportedSlices === 1 ? "" : "s"}.`);
|
|
2188
|
+
if (result.payload.summary.workflowStatus !== "completed") {
|
|
2189
|
+
writeWizardLine(`Some title crawls still failed: ${result.payload.summary.completedWithFailuresTitles} completed with failures, ${result.payload.summary.runningTitles} still non-terminal, ${result.payload.summary.truncatedTitles} truncated.`);
|
|
2190
|
+
}
|
|
2191
|
+
writeWizardLine(`Saved crawl summary to ${result.outPath}.`);
|
|
2192
|
+
writeWizardLine(`Saved logs to ${result.payload.logPath}.`);
|
|
2193
|
+
}
|
|
2194
|
+
writeWizardLine();
|
|
2195
|
+
writeWizardLine("Equivalent raw command:");
|
|
2196
|
+
const commandArgs = [
|
|
2197
|
+
"salesprompter",
|
|
2198
|
+
"salesnav:from-product-category",
|
|
2199
|
+
"--input",
|
|
2200
|
+
input,
|
|
2201
|
+
"--product-limit",
|
|
2202
|
+
String(productLimit),
|
|
2203
|
+
"--title-limit",
|
|
2204
|
+
String(titleLimit)
|
|
2205
|
+
];
|
|
2206
|
+
if (dryRun) {
|
|
2207
|
+
commandArgs.push("--dry-run");
|
|
2208
|
+
}
|
|
2209
|
+
writeWizardLine(` ${buildCommandLine(commandArgs)}`);
|
|
2210
|
+
}
|
|
2211
|
+
async function runVendorShortcutWizard(rl) {
|
|
2212
|
+
writeWizardSection("Built-in Deel shortcut", "Use the built-in Deel ICP template and search your workspace lead data.");
|
|
2213
|
+
const reference = parseCompanyReference(await promptText(rl, "Which company shortcut should I use?", {
|
|
1229
2214
|
required: true
|
|
1230
2215
|
}));
|
|
1231
2216
|
writeWizardLine();
|
|
1232
2217
|
if (reference.vendorTemplate !== "deel") {
|
|
1233
|
-
throw new Error("
|
|
2218
|
+
throw new Error("The built-in shortcut only supports Deel right now. Use deel.com or the Deel LinkedIn company page.");
|
|
1234
2219
|
}
|
|
1235
|
-
writeWizardSection("Find matching leads", `Using the built-in ${reference.companyName} profile to search your workspace data.`);
|
|
1236
2220
|
const market = await promptChoice(rl, "Where do you want to search?", [
|
|
1237
2221
|
{ value: "dach", label: "DACH", description: "Germany, Austria, Switzerland" },
|
|
1238
2222
|
{ value: "europe", label: "Europe" },
|
|
@@ -1244,7 +2228,16 @@ async function runReferenceCompanyWizard(rl) {
|
|
|
1244
2228
|
const icpPath = `./data/${reference.slug}-icp-${market}.json`;
|
|
1245
2229
|
const leadPath = buildQualifiedLeadsPath(`${reference.slug}-${market}`);
|
|
1246
2230
|
await writeJsonFile(icpPath, icp);
|
|
1247
|
-
const leads =
|
|
2231
|
+
const leads = shouldBypassAuth()
|
|
2232
|
+
? (await leadProvider.generateLeads(icp, leadCount, {
|
|
2233
|
+
companyDomain: reference.domain ?? `${reference.slug}.com`,
|
|
2234
|
+
companyName: reference.companyName
|
|
2235
|
+
})).leads
|
|
2236
|
+
: await fetchWorkspaceLeadSearch(await requireAuthSession(), {
|
|
2237
|
+
mode: "reference-company",
|
|
2238
|
+
icp,
|
|
2239
|
+
limit: leadCount
|
|
2240
|
+
});
|
|
1248
2241
|
await writeJsonFile(leadPath, leads);
|
|
1249
2242
|
writeWizardLine(`Saved ICP to ${icpPath}.`);
|
|
1250
2243
|
if (leads.length === 0) {
|
|
@@ -1342,7 +2335,7 @@ async function runWizard(options) {
|
|
|
1342
2335
|
throw new Error("wizard does not support --json or --quiet.");
|
|
1343
2336
|
}
|
|
1344
2337
|
writeWizardLine("Salesprompter");
|
|
1345
|
-
writeWizardLine("Start with a company website or
|
|
2338
|
+
writeWizardLine("Start with a company website, LinkedIn product page, or category URL. I will guide you from there.");
|
|
1346
2339
|
writeWizardLine();
|
|
1347
2340
|
await ensureWizardSession(options);
|
|
1348
2341
|
const rl = createInterface({
|
|
@@ -1351,11 +2344,17 @@ async function runWizard(options) {
|
|
|
1351
2344
|
});
|
|
1352
2345
|
try {
|
|
1353
2346
|
const flow = await promptChoice(rl, "What do you want help with?", [
|
|
2347
|
+
{
|
|
2348
|
+
value: "product-market",
|
|
2349
|
+
label: "Find leads from a product market",
|
|
2350
|
+
description: "Start from a company, product, or LinkedIn category and crawl Sales Navigator",
|
|
2351
|
+
aliases: ["product market", "linkedin products", "category", "sales navigator", "crawl"]
|
|
2352
|
+
},
|
|
1354
2353
|
{
|
|
1355
2354
|
value: "reference-company",
|
|
1356
|
-
label: "
|
|
1357
|
-
description: "
|
|
1358
|
-
aliases: ["
|
|
2355
|
+
label: "Use the built-in Deel shortcut",
|
|
2356
|
+
description: "Generate the saved Deel ICP and search workspace leads",
|
|
2357
|
+
aliases: ["deel", "shortcut", "vendor template", "quick deel"]
|
|
1359
2358
|
},
|
|
1360
2359
|
{
|
|
1361
2360
|
value: "target-company",
|
|
@@ -1369,10 +2368,14 @@ async function runWizard(options) {
|
|
|
1369
2368
|
description: "Use a saved leads file to fill an Instantly campaign",
|
|
1370
2369
|
aliases: ["instantly", "outreach", "send leads", "campaign"]
|
|
1371
2370
|
}
|
|
1372
|
-
], "
|
|
2371
|
+
], "product-market");
|
|
1373
2372
|
writeWizardLine();
|
|
2373
|
+
if (flow === "product-market") {
|
|
2374
|
+
await runProductMarketWizard(rl);
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
1374
2377
|
if (flow === "reference-company") {
|
|
1375
|
-
await
|
|
2378
|
+
await runVendorShortcutWizard(rl);
|
|
1376
2379
|
return;
|
|
1377
2380
|
}
|
|
1378
2381
|
if (flow === "target-company") {
|
|
@@ -1521,7 +2524,7 @@ async function fetchHistoricalQueryRows(tables) {
|
|
|
1521
2524
|
}
|
|
1522
2525
|
program
|
|
1523
2526
|
.name("salesprompter")
|
|
1524
|
-
.description("Sales workflow CLI for
|
|
2527
|
+
.description("Sales workflow CLI for LinkedIn product discovery, Sales Navigator crawling, lead enrichment, scoring, and sync.")
|
|
1525
2528
|
.version(packageVersion)
|
|
1526
2529
|
.option("--json", "Emit compact machine-readable JSON output", false)
|
|
1527
2530
|
.option("--quiet", "Suppress successful stdout output", false);
|
|
@@ -1862,6 +2865,219 @@ program
|
|
|
1862
2865
|
uploaded
|
|
1863
2866
|
});
|
|
1864
2867
|
});
|
|
2868
|
+
program
|
|
2869
|
+
.command("salesnav:from-product-category")
|
|
2870
|
+
.description("Crawl a LinkedIn product category, derive intended-role title searches, then run durable Sales Navigator crawls that export through Phantombuster into Salesprompter.")
|
|
2871
|
+
.requiredOption("--input <value>", "Company domain, LinkedIn company URL, LinkedIn product URL, LinkedIn category URL, or LinkedIn product search URL")
|
|
2872
|
+
.option("--max-pages <number>", "Maximum LinkedIn category pages to fetch", "25")
|
|
2873
|
+
.option("--product-limit <number>", "Optional cap on the number of LinkedIn products to inspect")
|
|
2874
|
+
.option("--title-limit <number>", "Optional cap on the number of intended-role titles to crawl")
|
|
2875
|
+
.option("--max-results-per-search <number>", "Maximum Sales Navigator results allowed for one slice before splitting again. Current live export cap is 2500.", "2500")
|
|
2876
|
+
.option("--number-of-profiles <number>", "Profiles to request from Phantombuster per finished Sales Navigator slice. Current live export cap is 2500.", "2500")
|
|
2877
|
+
.option("--slice-preset <name>", "Slice preset label stored with every durable crawl job", "linkedin-product-category")
|
|
2878
|
+
.option("--max-split-depth <number>", "Maximum number of adaptive split dimensions to use", "6")
|
|
2879
|
+
.option("--max-slices-per-title <number>", "Safety cap for total claimed slices per intended-role title", "1000")
|
|
2880
|
+
.option("--max-retries <number>", "Retries for non-splitting export failures", "3")
|
|
2881
|
+
.option("--probe-profiles <number>", "Profiles to scrape while probing whether a slice is still too broad", "100")
|
|
2882
|
+
.option("--agent-busy-wait-seconds <number>", "Seconds to wait before retrying when the export agent is already busy", "30")
|
|
2883
|
+
.option("--agent-busy-max-waits <number>", "How many busy-agent waits to tolerate before failing the slice", "20")
|
|
2884
|
+
.option("--idle-poll-seconds <number>", "Seconds to wait before polling durable crawl status when remote slices are still running", "10")
|
|
2885
|
+
.option("--idle-max-polls <number>", "How many no-claim status polls to tolerate before the crawl is considered stalled", "180")
|
|
2886
|
+
.option("--parallel-exports <number>", "How many Sales Navigator slices to export concurrently per title crawl", "3")
|
|
2887
|
+
.option("--allow-partial-success", "Exit 0 even when one or more durable title crawls finish with failures", false)
|
|
2888
|
+
.option("--skip-product-upload", "Do not upload the crawled LinkedIn product catalog before starting Sales Navigator crawls", false)
|
|
2889
|
+
.option("--out <path>", "Optional local JSON output path")
|
|
2890
|
+
.option("--log-path <path>", "Optional JSONL log path with timestamps, trace id, and Sales Navigator query metadata")
|
|
2891
|
+
.option("--dry-run", "Preview the derived intended-role title queries without creating crawl jobs", false)
|
|
2892
|
+
.action(async (options) => {
|
|
2893
|
+
const maxPages = z.coerce.number().int().min(1).max(500).parse(options.maxPages);
|
|
2894
|
+
const productLimit = options.productLimit === undefined
|
|
2895
|
+
? undefined
|
|
2896
|
+
: z.coerce.number().int().min(1).max(5000).parse(options.productLimit);
|
|
2897
|
+
const titleLimit = options.titleLimit === undefined
|
|
2898
|
+
? undefined
|
|
2899
|
+
: z.coerce.number().int().min(1).max(1000).parse(options.titleLimit);
|
|
2900
|
+
const maxResultsPerSearch = z.coerce.number().int().min(1).max(2500).parse(options.maxResultsPerSearch);
|
|
2901
|
+
const numberOfProfiles = z.coerce.number().int().min(1).max(2500).parse(options.numberOfProfiles);
|
|
2902
|
+
const maxSplitDepth = z.coerce.number().int().min(1).max(6).parse(options.maxSplitDepth);
|
|
2903
|
+
const maxSlicesPerTitle = z.coerce.number().int().min(1).max(10000).parse(options.maxSlicesPerTitle);
|
|
2904
|
+
const maxRetries = z.coerce.number().int().min(0).max(5).parse(options.maxRetries);
|
|
2905
|
+
const probeProfiles = z.coerce.number().int().min(1).max(2500).parse(options.probeProfiles);
|
|
2906
|
+
const agentBusyWaitSeconds = z.coerce.number().int().min(1).max(300).parse(options.agentBusyWaitSeconds);
|
|
2907
|
+
const agentBusyMaxWaits = z.coerce.number().int().min(0).max(120).parse(options.agentBusyMaxWaits);
|
|
2908
|
+
const idlePollSeconds = z.coerce.number().int().min(0).max(300).parse(options.idlePollSeconds);
|
|
2909
|
+
const idleMaxPolls = z.coerce.number().int().min(0).max(10000).parse(options.idleMaxPolls);
|
|
2910
|
+
const parallelExports = z.coerce.number().int().min(1).max(10).parse(options.parallelExports);
|
|
2911
|
+
const result = await runSalesNavigatorFromProductCategoryWorkflow({
|
|
2912
|
+
input: options.input,
|
|
2913
|
+
maxPages,
|
|
2914
|
+
productLimit,
|
|
2915
|
+
titleLimit,
|
|
2916
|
+
maxResultsPerSearch,
|
|
2917
|
+
numberOfProfiles,
|
|
2918
|
+
slicePreset: options.slicePreset,
|
|
2919
|
+
maxSplitDepth,
|
|
2920
|
+
maxSlicesPerTitle,
|
|
2921
|
+
maxRetries,
|
|
2922
|
+
probeProfiles,
|
|
2923
|
+
agentBusyWaitSeconds,
|
|
2924
|
+
agentBusyMaxWaits,
|
|
2925
|
+
idlePollSeconds,
|
|
2926
|
+
idleMaxPolls,
|
|
2927
|
+
parallelExports,
|
|
2928
|
+
skipProductUpload: Boolean(options.skipProductUpload),
|
|
2929
|
+
outPath: options.out,
|
|
2930
|
+
logPath: options.logPath,
|
|
2931
|
+
dryRun: Boolean(options.dryRun || shouldBypassAuth())
|
|
2932
|
+
});
|
|
2933
|
+
printOutput({
|
|
2934
|
+
...result.payload,
|
|
2935
|
+
out: result.outPath
|
|
2936
|
+
});
|
|
2937
|
+
if (!result.payload.dryRun && result.payload.summary.workflowStatus !== "completed" && !options.allowPartialSuccess) {
|
|
2938
|
+
throw new Error(buildSalesNavigatorWorkflowFailureMessage(result.payload.summary));
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
program
|
|
2942
|
+
.command("salesnav:ensure-count")
|
|
2943
|
+
.description("Ensure the workspace has at least the target number of Sales Navigator people rows by importing historical BigQuery windows directly.")
|
|
2944
|
+
.option("--target-count <number>", "Minimum linkedin_sales_nav_people rows to guarantee", "200000")
|
|
2945
|
+
.option("--scope <scope>", "Historical scope: all-sales-people|hr-function-included", "all-sales-people")
|
|
2946
|
+
.option("--org-id <id>", "Workspace org id. Defaults to the active CLI org.")
|
|
2947
|
+
.option("--start-offset <number>", "BigQuery offset override. By default the CLI resumes from prior historical backfill runs.")
|
|
2948
|
+
.option("--window-size <number>", "How many historical contacts to request from BigQuery per window", String(salesNavigatorHistoricalBackfillDefaults.windowSize))
|
|
2949
|
+
.option("--max-windows <number>", "Maximum number of BigQuery windows to import in this invocation", "10")
|
|
2950
|
+
.option("--page-size <number>", "BigQuery page size per API read", String(salesNavigatorHistoricalBackfillDefaults.pageSize))
|
|
2951
|
+
.option("--upsert-batch-size <number>", "Supabase upsert batch size", String(salesNavigatorHistoricalBackfillDefaults.upsertBatchSize))
|
|
2952
|
+
.option("--min-upsert-batch-size <number>", "Smallest batch size allowed after timeout-driven splitting", String(salesNavigatorHistoricalBackfillDefaults.minUpsertBatchSize))
|
|
2953
|
+
.option("--max-upsert-retries <number>", "How many retry rounds to tolerate for timeout-prone writes", String(salesNavigatorHistoricalBackfillDefaults.maxUpsertRetries))
|
|
2954
|
+
.option("--retry-delay-ms <number>", "Base retry delay in milliseconds for write retries", String(salesNavigatorHistoricalBackfillDefaults.retryDelayMs))
|
|
2955
|
+
.option("--out <path>", "Optional local JSON output path")
|
|
2956
|
+
.option("--dry-run", "Preview the historical import plan without touching BigQuery or Supabase", false)
|
|
2957
|
+
.action(async (options) => {
|
|
2958
|
+
const targetCount = z.coerce.number().int().min(1).parse(options.targetCount);
|
|
2959
|
+
const scope = z.enum(["all-sales-people", "hr-function-included"]).parse(options.scope);
|
|
2960
|
+
const explicitStartOffset = typeof options.startOffset === "string" && options.startOffset.trim().length > 0
|
|
2961
|
+
? z.coerce.number().int().min(0).parse(options.startOffset)
|
|
2962
|
+
: null;
|
|
2963
|
+
const windowSize = z.coerce.number().int().min(1).parse(options.windowSize);
|
|
2964
|
+
const maxWindows = z.coerce.number().int().min(1).max(100).parse(options.maxWindows);
|
|
2965
|
+
const pageSize = z.coerce.number().int().min(1).parse(options.pageSize);
|
|
2966
|
+
const upsertBatchSize = z.coerce.number().int().min(1).parse(options.upsertBatchSize);
|
|
2967
|
+
const minUpsertBatchSize = z.coerce.number().int().min(1).parse(options.minUpsertBatchSize);
|
|
2968
|
+
const maxUpsertRetries = z.coerce.number().int().min(0).parse(options.maxUpsertRetries);
|
|
2969
|
+
const retryDelayMs = z.coerce.number().int().min(0).parse(options.retryDelayMs);
|
|
2970
|
+
if (minUpsertBatchSize > upsertBatchSize) {
|
|
2971
|
+
throw new Error("--min-upsert-batch-size must be less than or equal to --upsert-batch-size.");
|
|
2972
|
+
}
|
|
2973
|
+
if (Boolean(options.dryRun)) {
|
|
2974
|
+
const plan = buildSalesNavigatorHistoricalBackfillPlan({
|
|
2975
|
+
targetCount,
|
|
2976
|
+
currentCount: null,
|
|
2977
|
+
startOffset: explicitStartOffset ?? 0,
|
|
2978
|
+
windowSize,
|
|
2979
|
+
maxWindows
|
|
2980
|
+
});
|
|
2981
|
+
const payload = {
|
|
2982
|
+
status: "ok",
|
|
2983
|
+
dryRun: true,
|
|
2984
|
+
mode: "historical-bigquery-backfill",
|
|
2985
|
+
orgId: options.orgId ?? null,
|
|
2986
|
+
scope,
|
|
2987
|
+
targetCount,
|
|
2988
|
+
resumedFromHistory: false,
|
|
2989
|
+
plan
|
|
2990
|
+
};
|
|
2991
|
+
if (options.out) {
|
|
2992
|
+
await writeJsonFile(options.out, payload);
|
|
2993
|
+
}
|
|
2994
|
+
printOutput(payload);
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
let sessionOrgId = null;
|
|
2998
|
+
if (!shouldBypassAuth()) {
|
|
2999
|
+
const session = await requireAuthSession();
|
|
3000
|
+
sessionOrgId = session.user.orgId ?? null;
|
|
3001
|
+
}
|
|
3002
|
+
const orgId = resolveSalesNavigatorHistoricalBackfillOrgId({
|
|
3003
|
+
explicitOrgId: options.orgId,
|
|
3004
|
+
env: process.env,
|
|
3005
|
+
sessionOrgId
|
|
3006
|
+
});
|
|
3007
|
+
const config = resolveSalesNavigatorHistoricalBackfillConfig(process.env);
|
|
3008
|
+
const supabase = createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
|
|
3009
|
+
auth: { persistSession: false }
|
|
3010
|
+
});
|
|
3011
|
+
const resumeState = explicitStartOffset === null
|
|
3012
|
+
? await resolveSalesNavigatorHistoricalBackfillResumeState({
|
|
3013
|
+
supabase,
|
|
3014
|
+
orgId,
|
|
3015
|
+
scope,
|
|
3016
|
+
windowSize,
|
|
3017
|
+
fallbackOffset: 0
|
|
3018
|
+
})
|
|
3019
|
+
: {
|
|
3020
|
+
startOffset: explicitStartOffset,
|
|
3021
|
+
resumedFromHistory: false,
|
|
3022
|
+
matchedHistoryRows: 0,
|
|
3023
|
+
reason: "fallback"
|
|
3024
|
+
};
|
|
3025
|
+
if (resumeState.resumedFromHistory) {
|
|
3026
|
+
writeProgress(`Resuming historical Sales Navigator backfill from offset ${resumeState.startOffset} based on prior CLI runs.`);
|
|
3027
|
+
}
|
|
3028
|
+
else if (explicitStartOffset !== null) {
|
|
3029
|
+
writeProgress(`Using explicit historical Sales Navigator backfill offset ${explicitStartOffset}.`);
|
|
3030
|
+
}
|
|
3031
|
+
const summary = await ensureSalesNavigatorPeopleCount({
|
|
3032
|
+
config,
|
|
3033
|
+
orgId,
|
|
3034
|
+
targetCount,
|
|
3035
|
+
scope,
|
|
3036
|
+
startOffset: resumeState.startOffset,
|
|
3037
|
+
resumedFromHistory: resumeState.resumedFromHistory,
|
|
3038
|
+
windowSize,
|
|
3039
|
+
maxWindows,
|
|
3040
|
+
pageSize,
|
|
3041
|
+
upsertBatchSize,
|
|
3042
|
+
minUpsertBatchSize,
|
|
3043
|
+
maxUpsertRetries,
|
|
3044
|
+
retryDelayMs,
|
|
3045
|
+
onProgress: (event) => {
|
|
3046
|
+
if (event.type === "window-start") {
|
|
3047
|
+
writeProgress(`Starting historical Sales Navigator backfill window ${event.windowIndex + 1}: offset ${event.offset}, limit ${event.limit}.`);
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
if (event.type === "window-progress") {
|
|
3051
|
+
writeProgress(`Historical window ${event.windowIndex + 1}: ${event.processed}/${event.totalResults} rows imported (${event.percent}%).`);
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
writeProgress(`Historical window ${event.windowIndex + 1} complete: count ${event.countBefore} -> ${event.countAfter} (delta ${event.countDelta}).`);
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
const payload = {
|
|
3058
|
+
status: summary.status,
|
|
3059
|
+
dryRun: false,
|
|
3060
|
+
mode: "historical-bigquery-backfill",
|
|
3061
|
+
orgId: summary.orgId,
|
|
3062
|
+
scope: summary.scope,
|
|
3063
|
+
targetCount: summary.targetCount,
|
|
3064
|
+
initialCount: summary.initialCount,
|
|
3065
|
+
currentCount: summary.currentCount,
|
|
3066
|
+
resumedFromHistory: summary.resumedFromHistory,
|
|
3067
|
+
startOffset: summary.startOffset,
|
|
3068
|
+
nextOffset: summary.nextOffset,
|
|
3069
|
+
exhausted: summary.exhausted,
|
|
3070
|
+
completedWindows: summary.completedWindows,
|
|
3071
|
+
windows: summary.windows
|
|
3072
|
+
};
|
|
3073
|
+
if (options.out) {
|
|
3074
|
+
await writeJsonFile(options.out, payload);
|
|
3075
|
+
}
|
|
3076
|
+
if (summary.status !== "ok") {
|
|
3077
|
+
throw new Error(`Historical Sales Navigator backfill stopped at ${summary.currentCount} rows before reaching the target ${summary.targetCount}.`);
|
|
3078
|
+
}
|
|
3079
|
+
printOutput(payload);
|
|
3080
|
+
});
|
|
1865
3081
|
program
|
|
1866
3082
|
.command("salesnav:crawl")
|
|
1867
3083
|
.description("Adaptively split broad LinkedIn Sales Navigator people searches into exportable slices and store every finished slice through Salesprompter.")
|
|
@@ -1876,7 +3092,12 @@ program
|
|
|
1876
3092
|
.option("--probe-profiles <number>", "Profiles to scrape while probing whether a slice is still too broad", "100")
|
|
1877
3093
|
.option("--agent-busy-wait-seconds <number>", "Seconds to wait before retrying when the export agent is already busy", "30")
|
|
1878
3094
|
.option("--agent-busy-max-waits <number>", "How many busy-agent waits to tolerate before failing the slice", "20")
|
|
3095
|
+
.option("--idle-poll-seconds <number>", "Seconds to wait before polling durable crawl status when remote slices are still running", "10")
|
|
3096
|
+
.option("--idle-max-polls <number>", "How many no-claim status polls to tolerate before the crawl is considered stalled", "180")
|
|
3097
|
+
.option("--parallel-exports <number>", "How many Sales Navigator slices to export concurrently in this invocation", "3")
|
|
3098
|
+
.option("--allow-partial-success", "Exit 0 even when the durable crawl finishes with failures or remains non-terminal", false)
|
|
1879
3099
|
.option("--out <path>", "Optional local JSON output path")
|
|
3100
|
+
.option("--log-path <path>", "Optional JSONL log path with timestamps, trace id, and Sales Navigator slice metadata")
|
|
1880
3101
|
.option("--dry-run", "Preview the adaptive crawl plan without exporting anything", false)
|
|
1881
3102
|
.action(async (options) => {
|
|
1882
3103
|
const queryUrl = z.string().url().optional().parse(options.queryUrl);
|
|
@@ -1889,7 +3110,30 @@ program
|
|
|
1889
3110
|
const probeProfiles = z.coerce.number().int().min(1).max(2500).parse(options.probeProfiles);
|
|
1890
3111
|
const agentBusyWaitSeconds = z.coerce.number().int().min(1).max(300).parse(options.agentBusyWaitSeconds);
|
|
1891
3112
|
const agentBusyMaxWaits = z.coerce.number().int().min(0).max(120).parse(options.agentBusyMaxWaits);
|
|
3113
|
+
const idlePollSeconds = z.coerce.number().int().min(0).max(300).parse(options.idlePollSeconds);
|
|
3114
|
+
const idleMaxPolls = z.coerce.number().int().min(0).max(10000).parse(options.idleMaxPolls);
|
|
3115
|
+
const parallelExports = z.coerce.number().int().min(1).max(10).parse(options.parallelExports);
|
|
1892
3116
|
const effectiveDryRun = Boolean(options.dryRun || shouldBypassAuth());
|
|
3117
|
+
const logger = await createWorkflowLogger({
|
|
3118
|
+
logPath: options.logPath ?? buildSalesNavigatorCrawlLogPath(jobId ?? queryUrl ?? "salesnav-crawl")
|
|
3119
|
+
});
|
|
3120
|
+
await logger.log("salesnav.crawl.command.started", {
|
|
3121
|
+
queryUrl: queryUrl ?? null,
|
|
3122
|
+
jobId: jobId ?? null,
|
|
3123
|
+
maxResultsPerSearch,
|
|
3124
|
+
numberOfProfiles,
|
|
3125
|
+
slicePreset: options.slicePreset,
|
|
3126
|
+
maxSplitDepth,
|
|
3127
|
+
maxSlices,
|
|
3128
|
+
maxRetries,
|
|
3129
|
+
probeProfiles,
|
|
3130
|
+
agentBusyWaitSeconds,
|
|
3131
|
+
agentBusyMaxWaits,
|
|
3132
|
+
idlePollSeconds,
|
|
3133
|
+
idleMaxPolls,
|
|
3134
|
+
parallelExports,
|
|
3135
|
+
dryRun: effectiveDryRun
|
|
3136
|
+
});
|
|
1893
3137
|
if (effectiveDryRun) {
|
|
1894
3138
|
if (jobId) {
|
|
1895
3139
|
throw new Error("--dry-run does not support --job-id. Use --query-url instead.");
|
|
@@ -1901,6 +3145,8 @@ program
|
|
|
1901
3145
|
status: "ok",
|
|
1902
3146
|
dryRun: true,
|
|
1903
3147
|
mode: "adaptive",
|
|
3148
|
+
traceId: logger.traceId,
|
|
3149
|
+
logPath: logger.logPath,
|
|
1904
3150
|
dimensionPreset: "human-resources-adaptive",
|
|
1905
3151
|
query: (() => {
|
|
1906
3152
|
const preview = buildSalesNavigatorCrawlPreview({
|
|
@@ -1930,6 +3176,15 @@ program
|
|
|
1930
3176
|
};
|
|
1931
3177
|
})()
|
|
1932
3178
|
};
|
|
3179
|
+
await logger.log("salesnav.crawl.dry-run.preview", {
|
|
3180
|
+
sourceQueryUrl: payload.query.sourceQueryUrl,
|
|
3181
|
+
root: summarizeSalesNavigatorQuery(payload.query.rootQueryUrl, payload.query.rootAppliedFilters),
|
|
3182
|
+
dimensionOrder: payload.query.dimensionOrder,
|
|
3183
|
+
firstSplitQueries: payload.query.firstSplitQueries.map((attempt) => ({
|
|
3184
|
+
splitTrail: attempt.splitTrail,
|
|
3185
|
+
...summarizeSalesNavigatorQuery(attempt.slicedQueryUrl, attempt.appliedFilters)
|
|
3186
|
+
}))
|
|
3187
|
+
});
|
|
1933
3188
|
if (options.out) {
|
|
1934
3189
|
await writeJsonFile(options.out, payload);
|
|
1935
3190
|
}
|
|
@@ -1954,23 +3209,57 @@ program
|
|
|
1954
3209
|
slicePreset: options.slicePreset,
|
|
1955
3210
|
maxResultsPerSearch,
|
|
1956
3211
|
numberOfProfiles,
|
|
3212
|
+
rawPayload: {
|
|
3213
|
+
workflow: "salesnav:crawl",
|
|
3214
|
+
traceId: logger.traceId,
|
|
3215
|
+
command: {
|
|
3216
|
+
sourceQueryUrl: queryUrl,
|
|
3217
|
+
slicePreset: options.slicePreset,
|
|
3218
|
+
maxResultsPerSearch,
|
|
3219
|
+
numberOfProfiles,
|
|
3220
|
+
maxSplitDepth,
|
|
3221
|
+
maxSlices,
|
|
3222
|
+
maxRetries,
|
|
3223
|
+
probeProfiles,
|
|
3224
|
+
agentBusyWaitSeconds,
|
|
3225
|
+
agentBusyMaxWaits,
|
|
3226
|
+
idlePollSeconds,
|
|
3227
|
+
idleMaxPolls,
|
|
3228
|
+
parallelExports
|
|
3229
|
+
}
|
|
3230
|
+
},
|
|
1957
3231
|
rootSlice: {
|
|
1958
3232
|
slicedQueryUrl: seed.slicedQueryUrl,
|
|
1959
3233
|
appliedFilters: seed.appliedFilters,
|
|
1960
3234
|
depth: seed.depth,
|
|
1961
|
-
splitTrail: seed.splitTrail
|
|
3235
|
+
splitTrail: seed.splitTrail,
|
|
3236
|
+
rawPayload: {
|
|
3237
|
+
workflow: "salesnav:crawl",
|
|
3238
|
+
traceId: logger.traceId
|
|
3239
|
+
}
|
|
1962
3240
|
}
|
|
1963
|
-
});
|
|
3241
|
+
}, logger.traceId);
|
|
1964
3242
|
session = created.session;
|
|
1965
3243
|
createResult = {
|
|
1966
3244
|
resumed: created.value.resumed,
|
|
1967
3245
|
job: created.value.job
|
|
1968
3246
|
};
|
|
1969
3247
|
resolvedJobId = created.value.job.id;
|
|
3248
|
+
await logger.log("salesnav.crawl.job.ready", {
|
|
3249
|
+
jobId: resolvedJobId,
|
|
3250
|
+
resumed: created.value.resumed,
|
|
3251
|
+
sourceQueryUrl: queryUrl,
|
|
3252
|
+
rootSlice: summarizeSalesNavigatorQuery(seed.slicedQueryUrl, seed.appliedFilters)
|
|
3253
|
+
});
|
|
1970
3254
|
}
|
|
1971
3255
|
else {
|
|
1972
|
-
const status = await getSalesNavigatorCrawlStatus(session, resolvedJobId);
|
|
3256
|
+
const status = await getSalesNavigatorCrawlStatus(session, resolvedJobId, logger.traceId);
|
|
1973
3257
|
session = status.session;
|
|
3258
|
+
await logger.log("salesnav.crawl.job.resumed", {
|
|
3259
|
+
jobId: resolvedJobId,
|
|
3260
|
+
sourceQueryUrl: status.value.job.sourceQueryUrl,
|
|
3261
|
+
status: status.value.job.status
|
|
3262
|
+
});
|
|
1974
3263
|
}
|
|
1975
3264
|
if (!resolvedJobId) {
|
|
1976
3265
|
throw new Error("Failed to determine Sales Navigator crawl job id.");
|
|
@@ -1981,12 +3270,19 @@ program
|
|
|
1981
3270
|
maxRetries,
|
|
1982
3271
|
probeProfiles,
|
|
1983
3272
|
agentBusyWaitSeconds,
|
|
1984
|
-
agentBusyMaxWaits
|
|
3273
|
+
agentBusyMaxWaits,
|
|
3274
|
+
idlePollSeconds,
|
|
3275
|
+
idleMaxPolls,
|
|
3276
|
+
parallelExports,
|
|
3277
|
+
traceId: logger.traceId,
|
|
3278
|
+
logger
|
|
1985
3279
|
});
|
|
1986
3280
|
const payload = {
|
|
1987
3281
|
status: "ok",
|
|
1988
3282
|
dryRun: false,
|
|
1989
3283
|
mode: "durable",
|
|
3284
|
+
traceId: logger.traceId,
|
|
3285
|
+
logPath: logger.logPath,
|
|
1990
3286
|
jobId: resolvedJobId,
|
|
1991
3287
|
resumed: createResult?.resumed ?? true,
|
|
1992
3288
|
sourceQueryUrl: crawl.job.sourceQueryUrl,
|
|
@@ -2009,10 +3305,20 @@ program
|
|
|
2009
3305
|
: null,
|
|
2010
3306
|
lastOutcome: crawl.lastOutcome
|
|
2011
3307
|
};
|
|
3308
|
+
await logger.log("salesnav.crawl.command.completed", {
|
|
3309
|
+
jobId: resolvedJobId,
|
|
3310
|
+
status: crawl.job.status,
|
|
3311
|
+
claimedSlices: crawl.claimedSlices,
|
|
3312
|
+
truncated: crawl.truncated,
|
|
3313
|
+
lastOutcome: crawl.lastOutcome
|
|
3314
|
+
});
|
|
2012
3315
|
if (options.out) {
|
|
2013
3316
|
await writeJsonFile(options.out, payload);
|
|
2014
3317
|
}
|
|
2015
3318
|
printOutput(payload);
|
|
3319
|
+
if ((crawl.job.status !== "completed" || crawl.truncated) && !options.allowPartialSuccess) {
|
|
3320
|
+
throw new Error(`Sales Navigator crawl did not finish cleanly. status=${crawl.job.status} truncated=${crawl.truncated} failedSlices=${crawl.job.failedSlices} runningSlices=${crawl.job.runningSlices} queuedSlices=${crawl.job.queuedSlices}`);
|
|
3321
|
+
}
|
|
2016
3322
|
});
|
|
2017
3323
|
program
|
|
2018
3324
|
.command("salesnav:crawl:status")
|
|
@@ -2073,7 +3379,13 @@ program
|
|
|
2073
3379
|
appliedFilters: item.appliedFilters,
|
|
2074
3380
|
maxResultsPerSearch,
|
|
2075
3381
|
numberOfProfiles,
|
|
2076
|
-
slicePreset: options.slicePreset
|
|
3382
|
+
slicePreset: options.slicePreset,
|
|
3383
|
+
rawPayload: {
|
|
3384
|
+
workflow: "salesnav:export",
|
|
3385
|
+
sourceQueryUrl: item.sourceQueryUrl,
|
|
3386
|
+
slicedQueryUrl: item.slicedQueryUrl,
|
|
3387
|
+
appliedFilters: item.appliedFilters
|
|
3388
|
+
}
|
|
2077
3389
|
});
|
|
2078
3390
|
exported.push(result);
|
|
2079
3391
|
}
|