salesprompter-cli 0.1.60 → 0.1.62
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 +2 -0
- package/dist/cli.js +103 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ salesprompter contacts:resolve-emails --in ./contacts.tsv --out-dir ./email-run
|
|
|
68
68
|
# Collect a Sales Navigator people search into the active workspace
|
|
69
69
|
# Normal people searches use 2,500-result slices; Connections-of searches use 1,000.
|
|
70
70
|
# Oversized searches split by company headcount and deduplicate by canonical profile URL.
|
|
71
|
+
# LinkedIn headline drift is retained as metadata after the complete partition is proven.
|
|
72
|
+
# Exhaustive runs resume completed slices automatically after interruptions or rate limits.
|
|
71
73
|
salesprompter leads:collect \
|
|
72
74
|
--linkedin-url "$SALES_NAV_PEOPLE_URL"
|
|
73
75
|
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { access, appendFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
4
5
|
import { createServer } from "node:http";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import os from "node:os";
|
|
@@ -7662,7 +7663,8 @@ function collectPotentialSalesNavigatorRows(value, rows = []) {
|
|
|
7662
7663
|
function normalizeLocalSalesNavigatorPeople(responseBody, queryUrl) {
|
|
7663
7664
|
const seen = new Set();
|
|
7664
7665
|
const rows = [];
|
|
7665
|
-
|
|
7666
|
+
const resultRecords = extractLocalSalesNavigatorElements(responseBody).flatMap((element) => collectPotentialSalesNavigatorRows(element));
|
|
7667
|
+
for (const record of resultRecords) {
|
|
7666
7668
|
const currentPosition = firstLocalArrayRecord(record, "currentPositions");
|
|
7667
7669
|
const company = firstLocalNestedRecord(currentPosition, "companyUrnResolutionResult");
|
|
7668
7670
|
const profileUrl = deriveLocalSalesNavigatorProfileUrl(record);
|
|
@@ -8725,6 +8727,27 @@ async function fetchAllLocalSalesNavigatorPeople(parsedRequest, options) {
|
|
|
8725
8727
|
};
|
|
8726
8728
|
}
|
|
8727
8729
|
const LOCAL_SALES_NAVIGATOR_REPORTED_COUNT_DRIFT_LIMIT = 5;
|
|
8730
|
+
async function readLocalSalesNavigatorPeopleCheckpoint(checkpointPath) {
|
|
8731
|
+
try {
|
|
8732
|
+
await access(checkpointPath);
|
|
8733
|
+
}
|
|
8734
|
+
catch {
|
|
8735
|
+
return null;
|
|
8736
|
+
}
|
|
8737
|
+
const parsed = JSON.parse(await readFile(checkpointPath, "utf8"));
|
|
8738
|
+
if (parsed.version !== 1 ||
|
|
8739
|
+
typeof parsed.sourceQueryUrl !== "string" ||
|
|
8740
|
+
typeof parsed.maxResultsPerSearch !== "number" ||
|
|
8741
|
+
typeof parsed.pageSize !== "number" ||
|
|
8742
|
+
!Array.isArray(parsed.queue) ||
|
|
8743
|
+
!Array.isArray(parsed.queuedUrls) ||
|
|
8744
|
+
!Array.isArray(parsed.rootRecoveryDimensionKeys) ||
|
|
8745
|
+
!Array.isArray(parsed.people) ||
|
|
8746
|
+
!Array.isArray(parsed.slices)) {
|
|
8747
|
+
throw new Error(`Sales Navigator people checkpoint is invalid: ${checkpointPath}`);
|
|
8748
|
+
}
|
|
8749
|
+
return parsed;
|
|
8750
|
+
}
|
|
8728
8751
|
function canonicalLocalSalesNavigatorLeadKey(person) {
|
|
8729
8752
|
try {
|
|
8730
8753
|
const url = new URL(person.profileUrl);
|
|
@@ -8751,15 +8774,48 @@ async function fetchCompleteLocalSalesNavigatorPeople(sourceQueryUrl, requestHea
|
|
|
8751
8774
|
searchType: "people",
|
|
8752
8775
|
});
|
|
8753
8776
|
const dimensions = localPeopleSplitDimensions(root.sourceQueryUrl);
|
|
8754
|
-
const
|
|
8755
|
-
|
|
8756
|
-
|
|
8777
|
+
const checkpoint = options.checkpointPath
|
|
8778
|
+
? await readLocalSalesNavigatorPeopleCheckpoint(options.checkpointPath)
|
|
8779
|
+
: null;
|
|
8780
|
+
if (checkpoint &&
|
|
8781
|
+
(checkpoint.sourceQueryUrl !== sourceQueryUrl ||
|
|
8782
|
+
checkpoint.maxResultsPerSearch !== options.maxResultsPerSearch ||
|
|
8783
|
+
checkpoint.pageSize !== options.pageSize)) {
|
|
8784
|
+
throw new Error("Sales Navigator people checkpoint does not match this query or collection window.");
|
|
8785
|
+
}
|
|
8786
|
+
const queue = checkpoint?.queue ?? [root];
|
|
8787
|
+
const queuedUrls = new Set(checkpoint?.queuedUrls ?? [root.slicedQueryUrl]);
|
|
8788
|
+
const rootRecoveryDimensionKeys = new Set(checkpoint?.rootRecoveryDimensionKeys ?? []);
|
|
8757
8789
|
const peopleByKey = new Map();
|
|
8758
|
-
const
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
let
|
|
8790
|
+
for (const person of checkpoint?.people ?? []) {
|
|
8791
|
+
peopleByKey.set(canonicalLocalSalesNavigatorLeadKey(person), person);
|
|
8792
|
+
}
|
|
8793
|
+
const slices = checkpoint?.slices ?? [];
|
|
8794
|
+
let rootTotalResults = checkpoint?.rootTotalResults ?? null;
|
|
8795
|
+
let fetchedPages = checkpoint?.fetchedPages ?? 0;
|
|
8796
|
+
let totalDelayMs = checkpoint?.totalDelayMs ?? 0;
|
|
8797
|
+
let retryCount = checkpoint?.retryCount ?? 0;
|
|
8798
|
+
const persistCheckpoint = async () => {
|
|
8799
|
+
if (!options.checkpointPath)
|
|
8800
|
+
return;
|
|
8801
|
+
await writeJsonFile(options.checkpointPath, {
|
|
8802
|
+
version: 1,
|
|
8803
|
+
sourceQueryUrl,
|
|
8804
|
+
maxResultsPerSearch: options.maxResultsPerSearch,
|
|
8805
|
+
pageSize: options.pageSize,
|
|
8806
|
+
queue,
|
|
8807
|
+
queuedUrls: [...queuedUrls],
|
|
8808
|
+
rootRecoveryDimensionKeys: [...rootRecoveryDimensionKeys],
|
|
8809
|
+
people: [...peopleByKey.values()],
|
|
8810
|
+
slices,
|
|
8811
|
+
rootTotalResults,
|
|
8812
|
+
fetchedPages,
|
|
8813
|
+
totalDelayMs,
|
|
8814
|
+
retryCount,
|
|
8815
|
+
updatedAt: new Date().toISOString(),
|
|
8816
|
+
});
|
|
8817
|
+
};
|
|
8818
|
+
await persistCheckpoint();
|
|
8763
8819
|
while (true) {
|
|
8764
8820
|
while (queue.length > 0) {
|
|
8765
8821
|
const attempt = queue.shift();
|
|
@@ -8840,6 +8896,7 @@ async function fetchCompleteLocalSalesNavigatorPeople(sourceQueryUrl, requestHea
|
|
|
8840
8896
|
status: "split",
|
|
8841
8897
|
splitDimension: nextDimension.key,
|
|
8842
8898
|
});
|
|
8899
|
+
await persistCheckpoint();
|
|
8843
8900
|
continue;
|
|
8844
8901
|
}
|
|
8845
8902
|
if (!collected) {
|
|
@@ -8857,6 +8914,7 @@ async function fetchCompleteLocalSalesNavigatorPeople(sourceQueryUrl, requestHea
|
|
|
8857
8914
|
status: "collected",
|
|
8858
8915
|
splitDimension: null,
|
|
8859
8916
|
});
|
|
8917
|
+
await persistCheckpoint();
|
|
8860
8918
|
}
|
|
8861
8919
|
const currentReportedCountDrift = rootTotalResults == null
|
|
8862
8920
|
? 0
|
|
@@ -8865,9 +8923,11 @@ async function fetchCompleteLocalSalesNavigatorPeople(sourceQueryUrl, requestHea
|
|
|
8865
8923
|
rootTotalResults <= options.maxResultsPerSearch &&
|
|
8866
8924
|
currentReportedCountDrift <=
|
|
8867
8925
|
LOCAL_SALES_NAVIGATOR_REPORTED_COUNT_DRIFT_LIMIT;
|
|
8926
|
+
const partitionCoverageComplete = rootRecoveryDimensionKeys.size > 0;
|
|
8868
8927
|
if (rootTotalResults == null ||
|
|
8869
8928
|
peopleByKey.size >= rootTotalResults ||
|
|
8870
|
-
currentReportedCountDriftAccepted
|
|
8929
|
+
currentReportedCountDriftAccepted ||
|
|
8930
|
+
partitionCoverageComplete) {
|
|
8871
8931
|
break;
|
|
8872
8932
|
}
|
|
8873
8933
|
const nextRecoveryDimension = dimensions.find((dimension) => !rootRecoveryDimensionKeys.has(dimension.key));
|
|
@@ -8894,19 +8954,28 @@ async function fetchCompleteLocalSalesNavigatorPeople(sourceQueryUrl, requestHea
|
|
|
8894
8954
|
status: "split",
|
|
8895
8955
|
splitDimension: nextRecoveryDimension.key,
|
|
8896
8956
|
});
|
|
8957
|
+
await persistCheckpoint();
|
|
8897
8958
|
}
|
|
8898
8959
|
const people = [...peopleByKey.values()];
|
|
8899
8960
|
const reportedCountDrift = rootTotalResults == null ? 0 : Math.max(0, rootTotalResults - people.length);
|
|
8900
8961
|
const acceptedReportedCountDrift = rootTotalResults != null &&
|
|
8901
8962
|
rootTotalResults <= options.maxResultsPerSearch &&
|
|
8902
8963
|
reportedCountDrift <= LOCAL_SALES_NAVIGATOR_REPORTED_COUNT_DRIFT_LIMIT;
|
|
8903
|
-
|
|
8964
|
+
const partitionCoverageComplete = rootRecoveryDimensionKeys.size > 0;
|
|
8965
|
+
if (reportedCountDrift > 0 &&
|
|
8966
|
+
!acceptedReportedCountDrift &&
|
|
8967
|
+
!partitionCoverageComplete) {
|
|
8904
8968
|
throw new Error(`Adaptive collection found ${people.length} unique people but the root search reported ${rootTotalResults}. No import was written because coverage is incomplete.`);
|
|
8905
8969
|
}
|
|
8970
|
+
await persistCheckpoint();
|
|
8906
8971
|
return {
|
|
8907
8972
|
people,
|
|
8908
8973
|
totalResults: rootTotalResults,
|
|
8909
8974
|
rootTotalResults,
|
|
8975
|
+
coverageProof: partitionCoverageComplete
|
|
8976
|
+
? "partitioned-canonical-union"
|
|
8977
|
+
: "direct-pagination",
|
|
8978
|
+
resumedFromCheckpoint: checkpoint != null,
|
|
8910
8979
|
reportedCountDrift,
|
|
8911
8980
|
fetchedPages,
|
|
8912
8981
|
sourceQueryUrl,
|
|
@@ -13592,6 +13661,13 @@ program
|
|
|
13592
13661
|
}
|
|
13593
13662
|
});
|
|
13594
13663
|
});
|
|
13664
|
+
function getLocalSalesNavigatorPeopleCheckpointPath(sourceQueryUrl, maxResultsPerSearch, pageSize) {
|
|
13665
|
+
const key = createHash("sha256")
|
|
13666
|
+
.update(`${sourceQueryUrl}\n${maxResultsPerSearch}\n${pageSize}`)
|
|
13667
|
+
.digest("hex")
|
|
13668
|
+
.slice(0, 20);
|
|
13669
|
+
return path.join(getSalesprompterConfigDir(), "checkpoints", `salesnav-people-${key}.json`);
|
|
13670
|
+
}
|
|
13595
13671
|
async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
13596
13672
|
const linkedInUrl = z.string().url().parse(options.linkedinUrl);
|
|
13597
13673
|
const pageSize = z.coerce.number().int().min(1).max(100).parse(options.pageSize);
|
|
@@ -13613,6 +13689,11 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
|
13613
13689
|
.max(accessibleResultLimit)
|
|
13614
13690
|
.parse(options.maxResults);
|
|
13615
13691
|
const complete = Boolean(options.complete || options.maxResults == null);
|
|
13692
|
+
const checkpointPath = complete
|
|
13693
|
+
? options.checkpoint
|
|
13694
|
+
? path.resolve(options.checkpoint)
|
|
13695
|
+
: getLocalSalesNavigatorPeopleCheckpointPath(normalizedSourceUrl, maxResults, pageSize)
|
|
13696
|
+
: null;
|
|
13616
13697
|
if (options.dryRun) {
|
|
13617
13698
|
const payload = {
|
|
13618
13699
|
status: "ok",
|
|
@@ -13621,6 +13702,7 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
|
13621
13702
|
maxResults,
|
|
13622
13703
|
accessibleResultLimit,
|
|
13623
13704
|
complete,
|
|
13705
|
+
resumable: complete,
|
|
13624
13706
|
destination: "/leads/cli-imports"
|
|
13625
13707
|
};
|
|
13626
13708
|
if (options.out)
|
|
@@ -13668,6 +13750,7 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
|
13668
13750
|
collected = complete
|
|
13669
13751
|
? await fetchCompleteLocalSalesNavigatorPeople(normalizedSourceUrl, parsedRequest.headers, {
|
|
13670
13752
|
maxResultsPerSearch: maxResults,
|
|
13753
|
+
checkpointPath,
|
|
13671
13754
|
...collectorOptions,
|
|
13672
13755
|
})
|
|
13673
13756
|
: await fetchAllLocalSalesNavigatorPeople(parsedRequest, {
|
|
@@ -13695,6 +13778,8 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
|
13695
13778
|
rawPayload: adaptiveCollection
|
|
13696
13779
|
? {
|
|
13697
13780
|
collectionMode: adaptiveCollection.collectionMode,
|
|
13781
|
+
coverageProof: adaptiveCollection.coverageProof,
|
|
13782
|
+
resumedFromCheckpoint: adaptiveCollection.resumedFromCheckpoint,
|
|
13698
13783
|
accessibleResultLimit,
|
|
13699
13784
|
reportedCountDrift: adaptiveCollection.reportedCountDrift,
|
|
13700
13785
|
sliceCount: adaptiveCollection.slices.filter((slice) => slice.status === "collected").length,
|
|
@@ -13726,6 +13811,8 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
|
13726
13811
|
fetchedPages: collected.fetchedPages,
|
|
13727
13812
|
accessibleResultLimit,
|
|
13728
13813
|
collectionMode: adaptiveCollection ? "adaptive" : "bounded",
|
|
13814
|
+
coverageProof: adaptiveCollection?.coverageProof ?? "bounded-pagination",
|
|
13815
|
+
resumedFromCheckpoint: adaptiveCollection?.resumedFromCheckpoint ?? false,
|
|
13729
13816
|
sliceCount: adaptiveCollection?.slices.filter((slice) => slice.status === "collected")
|
|
13730
13817
|
.length ?? 1,
|
|
13731
13818
|
runId: imported.runId,
|
|
@@ -13734,6 +13821,9 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
|
13734
13821
|
workspaceUrl: `${session.apiBaseUrl}/leads/cli-imports?runId=` +
|
|
13735
13822
|
encodeURIComponent(imported.runId)
|
|
13736
13823
|
};
|
|
13824
|
+
if (checkpointPath) {
|
|
13825
|
+
await rm(checkpointPath, { force: true });
|
|
13826
|
+
}
|
|
13737
13827
|
if (options.out)
|
|
13738
13828
|
await writeJsonFile(options.out, payload);
|
|
13739
13829
|
return payload;
|
|
@@ -14592,6 +14682,7 @@ program
|
|
|
14592
14682
|
.requiredOption("--linkedin-url <url>", "Sales Navigator people search URL")
|
|
14593
14683
|
.option("--max-results <number>", "Bounded collection size; defaults to exhaustive collection with a 2500-person window")
|
|
14594
14684
|
.option("--complete", "Split oversized searches by company headcount and deduplicate the complete result", false)
|
|
14685
|
+
.option("--checkpoint <path>", "Optional resume checkpoint; exhaustive runs use a per-query checkpoint by default")
|
|
14595
14686
|
.option("--curl-file <path>", "Optional copied Sales Navigator Lead Search curl request")
|
|
14596
14687
|
.option("--browser-relay-port <number>", "Use the signed-in browser through a loopback relay")
|
|
14597
14688
|
.option("--page-size <number>", "Direct Sales Navigator page size", "100")
|
package/package.json
CHANGED