salesprompter-cli 0.1.46 → 0.1.48
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 +6 -2
- package/dist/cli.js +149 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,11 @@ salesprompter companies:resolve-linkedin-urls --in ./companies.txt --out ./compa
|
|
|
65
65
|
# When authenticated, Salesprompter first enriches company domains in the app, then the CLI builds the email batch.
|
|
66
66
|
salesprompter contacts:resolve-emails --in ./contacts.tsv --out-dir ./email-run --dry-run
|
|
67
67
|
|
|
68
|
-
#
|
|
68
|
+
# Collect a Sales Navigator people search into the active workspace
|
|
69
|
+
salesprompter leads:collect \
|
|
70
|
+
--linkedin-url "$SALES_NAV_PEOPLE_URL" \
|
|
71
|
+
--max-results 1000
|
|
72
|
+
|
|
69
73
|
salesprompter affiliate:launch \
|
|
70
74
|
--affiliate-link "https://example.com/?ref=you" \
|
|
71
75
|
--linkedin-url "https://www.linkedin.com/sales/search/people?query=..." \
|
|
@@ -110,7 +114,7 @@ salesprompter --help
|
|
|
110
114
|
- Respect provider terms and customer data boundaries.
|
|
111
115
|
- `affiliate:run` uses direct email enrichment first, then Phantombuster Email Finder for unresolved people before creating a draft Instantly campaign.
|
|
112
116
|
- Affiliate preparation automatically creates three variants per step with spintax; `affiliate:regenerate-sequence` rebuilds and syncs them through the Salesprompter app.
|
|
113
|
-
- `affiliate:enrich`
|
|
117
|
+
- `affiliate:enrich` starts durable email recovery and adds only newly found addresses to an existing campaign. It returns a background-processing status for large audiences instead of blocking the terminal. Pass `--source-audience-run-id` to merge a newer compatible audience into that campaign without creating a duplicate campaign.
|
|
114
118
|
- A draft campaign does not send until `affiliate:activate` is run.
|
|
115
119
|
- The CLI is designed for interactive users and agent-assisted workflows.
|
|
116
120
|
- A repo-local Chrome extension compatibility copy is available in `chrome-extension/` for local LinkedIn session sync and popup copy/debug flows.
|
package/dist/cli.js
CHANGED
|
@@ -147,6 +147,14 @@ const AffiliateCampaignLaunchResponseSchema = z.object({
|
|
|
147
147
|
})
|
|
148
148
|
.passthrough()
|
|
149
149
|
.optional(),
|
|
150
|
+
workspaceImport: z
|
|
151
|
+
.object({
|
|
152
|
+
runId: z.string().uuid(),
|
|
153
|
+
imported: z.number().int().nonnegative(),
|
|
154
|
+
upserted: z.number().int().nonnegative(),
|
|
155
|
+
previewUrl: z.string().min(1)
|
|
156
|
+
})
|
|
157
|
+
.optional(),
|
|
150
158
|
previewUrl: z.string().nullable().optional(),
|
|
151
159
|
next: z.string().optional()
|
|
152
160
|
});
|
|
@@ -469,6 +477,7 @@ const cliPacks = [
|
|
|
469
477
|
summary: "Find leads from product and market inputs.",
|
|
470
478
|
commands: [
|
|
471
479
|
"leads:discover",
|
|
480
|
+
"leads:collect",
|
|
472
481
|
"search:run",
|
|
473
482
|
"search:status",
|
|
474
483
|
"search:export",
|
|
@@ -506,6 +515,7 @@ const helpAliasByCommandName = new Map([
|
|
|
506
515
|
["dealroom-companies:scrape-local", "companies:scrape-dealroom"],
|
|
507
516
|
["linkedin-products:scrape", "market:scrape"],
|
|
508
517
|
["salesnav:from-product-category", "leads:discover"],
|
|
518
|
+
["salesnav:people:collect", "leads:collect"],
|
|
509
519
|
["salesnav:crawl", "search:run"],
|
|
510
520
|
["salesnav:crawl:status", "search:status"],
|
|
511
521
|
["salesnav:export", "search:export"],
|
|
@@ -550,6 +560,7 @@ const helpVisibleCommandNames = new Set([
|
|
|
550
560
|
"dealroom-companies:scrape-local",
|
|
551
561
|
"linkedin-products:scrape",
|
|
552
562
|
"salesnav:from-product-category",
|
|
563
|
+
"salesnav:people:collect",
|
|
553
564
|
"salesnav:crawl",
|
|
554
565
|
"salesnav:crawl:status",
|
|
555
566
|
"salesnav:export",
|
|
@@ -6591,7 +6602,7 @@ async function enrichAffiliateOutreachViaApp(session, audienceRunId, sourceAudie
|
|
|
6591
6602
|
const previousAttemptedAt = typeof before.stats.latestTopUp === "object" && before.stats.latestTopUp !== null
|
|
6592
6603
|
? String(before.stats.latestTopUp.attemptedAt ?? "")
|
|
6593
6604
|
: "";
|
|
6594
|
-
await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}/enrich`, {
|
|
6605
|
+
const { value: started } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}/enrich`, {
|
|
6595
6606
|
method: "POST",
|
|
6596
6607
|
headers: {
|
|
6597
6608
|
"Content-Type": "application/json",
|
|
@@ -6599,7 +6610,11 @@ async function enrichAffiliateOutreachViaApp(session, audienceRunId, sourceAudie
|
|
|
6599
6610
|
},
|
|
6600
6611
|
body: JSON.stringify({ sourceAudienceRunId })
|
|
6601
6612
|
}), AffiliateOutreachEnrichStartResponseSchema);
|
|
6602
|
-
const
|
|
6613
|
+
const configuredWaitMs = Number(process.env.SALESPROMPTER_AFFILIATE_ENRICH_WAIT_MS);
|
|
6614
|
+
const waitMs = Number.isFinite(configuredWaitMs) && configuredWaitMs >= 0
|
|
6615
|
+
? Math.min(10 * 60 * 1000, Math.trunc(configuredWaitMs))
|
|
6616
|
+
: 15_000;
|
|
6617
|
+
const deadline = Date.now() + waitMs;
|
|
6603
6618
|
const configuredPollMs = Number(process.env.SALESPROMPTER_AFFILIATE_ENRICH_POLL_MS);
|
|
6604
6619
|
const pollMs = Number.isFinite(configuredPollMs) && configuredPollMs > 0
|
|
6605
6620
|
? Math.max(10, Math.trunc(configuredPollMs))
|
|
@@ -6612,11 +6627,20 @@ async function enrichAffiliateOutreachViaApp(session, audienceRunId, sourceAudie
|
|
|
6612
6627
|
? current.stats.latestTopUp
|
|
6613
6628
|
: null;
|
|
6614
6629
|
const attemptedAt = String(latestTopUp?.attemptedAt ?? "");
|
|
6615
|
-
|
|
6616
|
-
|
|
6630
|
+
const complete = latestTopUp?.complete !== false;
|
|
6631
|
+
if (attemptedAt && attemptedAt !== previousAttemptedAt && complete) {
|
|
6632
|
+
return {
|
|
6633
|
+
outreach: current,
|
|
6634
|
+
processing: false,
|
|
6635
|
+
workflowRunId: started.workflowRunId
|
|
6636
|
+
};
|
|
6617
6637
|
}
|
|
6618
6638
|
}
|
|
6619
|
-
|
|
6639
|
+
return {
|
|
6640
|
+
outreach: before,
|
|
6641
|
+
processing: true,
|
|
6642
|
+
workflowRunId: started.workflowRunId
|
|
6643
|
+
};
|
|
6620
6644
|
}
|
|
6621
6645
|
async function uploadLinkedInProductsCatalog(session, payload, batchSize = 100, traceId) {
|
|
6622
6646
|
let imported = 0;
|
|
@@ -6780,6 +6804,10 @@ function buildSalesNavigatorApiRequestFromSearchUrl(searchUrl, config, count) {
|
|
|
6780
6804
|
}
|
|
6781
6805
|
function buildSalesNavigatorLeadApiUrlFromSearchUrl(searchUrl, count) {
|
|
6782
6806
|
const source = new URL(searchUrl);
|
|
6807
|
+
if (!source.searchParams.has("query") && source.hash.startsWith("#query=")) {
|
|
6808
|
+
source.search = `?${source.hash.slice(1)}`;
|
|
6809
|
+
source.hash = "";
|
|
6810
|
+
}
|
|
6783
6811
|
if (source.pathname.includes("/sales-api/salesApiLeadSearch")) {
|
|
6784
6812
|
return source.toString();
|
|
6785
6813
|
}
|
|
@@ -13185,6 +13213,99 @@ program
|
|
|
13185
13213
|
}
|
|
13186
13214
|
});
|
|
13187
13215
|
});
|
|
13216
|
+
async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
13217
|
+
const linkedInUrl = z.string().url().parse(options.linkedinUrl);
|
|
13218
|
+
const maxResults = z.coerce.number().int().min(1).max(100000).parse(options.maxResults);
|
|
13219
|
+
const pageSize = z.coerce.number().int().min(1).max(100).parse(options.pageSize);
|
|
13220
|
+
const normalizedSourceUrl = (() => {
|
|
13221
|
+
const source = new URL(linkedInUrl);
|
|
13222
|
+
if (!source.searchParams.has("query") && source.hash.startsWith("#query=")) {
|
|
13223
|
+
source.search = `?${source.hash.slice(1)}`;
|
|
13224
|
+
source.hash = "";
|
|
13225
|
+
}
|
|
13226
|
+
return source.toString();
|
|
13227
|
+
})();
|
|
13228
|
+
if (options.dryRun) {
|
|
13229
|
+
const payload = {
|
|
13230
|
+
status: "ok",
|
|
13231
|
+
dryRun: true,
|
|
13232
|
+
linkedInUrl: normalizedSourceUrl,
|
|
13233
|
+
maxResults,
|
|
13234
|
+
destination: "/leads/cli-imports"
|
|
13235
|
+
};
|
|
13236
|
+
if (options.out)
|
|
13237
|
+
await writeJsonFile(options.out, payload);
|
|
13238
|
+
return payload;
|
|
13239
|
+
}
|
|
13240
|
+
const session = await requireAuthSession();
|
|
13241
|
+
const pageDelayMinMs = z.coerce.number().int().min(0).parse(options.pageDelayMinMs);
|
|
13242
|
+
const pageDelayMaxMs = z.coerce.number().int().min(pageDelayMinMs).parse(options.pageDelayMaxMs);
|
|
13243
|
+
const browserRelayPort = options.browserRelayPort == null
|
|
13244
|
+
? null
|
|
13245
|
+
: z.coerce.number().int().min(0).max(65535).parse(options.browserRelayPort);
|
|
13246
|
+
if (options.curlFile && browserRelayPort != null) {
|
|
13247
|
+
throw new Error("Use either --curl-file or --browser-relay-port, not both.");
|
|
13248
|
+
}
|
|
13249
|
+
const parsedRequest = options.curlFile
|
|
13250
|
+
? parseSalesNavigatorCurlRequest(await readFile(path.resolve(String(options.curlFile)), "utf8"))
|
|
13251
|
+
: browserRelayPort != null
|
|
13252
|
+
? {
|
|
13253
|
+
url: buildSalesNavigatorLeadApiUrlFromSearchUrl(normalizedSourceUrl, pageSize),
|
|
13254
|
+
headers: {}
|
|
13255
|
+
}
|
|
13256
|
+
: buildSalesNavigatorApiRequestFromSearchUrl(normalizedSourceUrl, await readLinkedInDirectLookupConfig(), pageSize);
|
|
13257
|
+
if (!new URL(parsedRequest.url).pathname.includes("/sales-api/salesApiLeadSearch")) {
|
|
13258
|
+
throw new Error("--curl-file must contain a /sales-api/salesApiLeadSearch request.");
|
|
13259
|
+
}
|
|
13260
|
+
const browserRelay = browserRelayPort == null
|
|
13261
|
+
? null
|
|
13262
|
+
: await createLocalAccountSearchBrowserRelay(browserRelayPort);
|
|
13263
|
+
let collected;
|
|
13264
|
+
try {
|
|
13265
|
+
collected = await fetchAllLocalSalesNavigatorPeople(parsedRequest, {
|
|
13266
|
+
requestedProfiles: maxResults,
|
|
13267
|
+
pageSize,
|
|
13268
|
+
pageDelayMinMs,
|
|
13269
|
+
pageDelayMaxMs,
|
|
13270
|
+
retry: {
|
|
13271
|
+
maxRetries: 2,
|
|
13272
|
+
retryBaseDelayMs: 2_000,
|
|
13273
|
+
retryMaxDelayMs: 10_000
|
|
13274
|
+
},
|
|
13275
|
+
executeRequest: browserRelay
|
|
13276
|
+
? (request) => browserRelay.request(request.url)
|
|
13277
|
+
: undefined
|
|
13278
|
+
});
|
|
13279
|
+
}
|
|
13280
|
+
finally {
|
|
13281
|
+
await browserRelay?.close();
|
|
13282
|
+
}
|
|
13283
|
+
const imported = await importLocalSalesNavigatorPeopleViaApp(session, {
|
|
13284
|
+
sourceQueryUrl: normalizedSourceUrl,
|
|
13285
|
+
people: collected.people,
|
|
13286
|
+
totalResults: collected.totalResults,
|
|
13287
|
+
fetchedPages: collected.fetchedPages,
|
|
13288
|
+
pacing: collected.pacing,
|
|
13289
|
+
maxResultsPerSearch: maxResults,
|
|
13290
|
+
numberOfProfiles: maxResults,
|
|
13291
|
+
slicePreset: "local-salesnav-people"
|
|
13292
|
+
});
|
|
13293
|
+
const payload = {
|
|
13294
|
+
status: "ok",
|
|
13295
|
+
dryRun: false,
|
|
13296
|
+
collected: collected.people.length,
|
|
13297
|
+
totalResults: collected.totalResults,
|
|
13298
|
+
fetchedPages: collected.fetchedPages,
|
|
13299
|
+
runId: imported.runId,
|
|
13300
|
+
imported: imported.imported,
|
|
13301
|
+
upserted: imported.upserted,
|
|
13302
|
+
workspaceUrl: `${session.apiBaseUrl}/leads/cli-imports?runId=` +
|
|
13303
|
+
encodeURIComponent(imported.runId)
|
|
13304
|
+
};
|
|
13305
|
+
if (options.out)
|
|
13306
|
+
await writeJsonFile(options.out, payload);
|
|
13307
|
+
return payload;
|
|
13308
|
+
}
|
|
13188
13309
|
async function runAffiliateLaunchCommand(options) {
|
|
13189
13310
|
const affiliateLink = z.string().url().parse(options.affiliateLink);
|
|
13190
13311
|
const linkedInUrl = z.string().url().parse(options.linkedinUrl);
|
|
@@ -13270,6 +13391,22 @@ function addAffiliateAudienceOptions(command) {
|
|
|
13270
13391
|
.option("--dry-run", "Validate and preview without creating remote resources", false)
|
|
13271
13392
|
.option("--out <path>", "Optional local JSON output path");
|
|
13272
13393
|
}
|
|
13394
|
+
program
|
|
13395
|
+
.command("salesnav:people:collect")
|
|
13396
|
+
.alias("leads:collect")
|
|
13397
|
+
.description("Collect a Sales Navigator people search into the active workspace.")
|
|
13398
|
+
.requiredOption("--linkedin-url <url>", "Sales Navigator people search URL")
|
|
13399
|
+
.option("--max-results <number>", "Maximum people to collect", "1000")
|
|
13400
|
+
.option("--curl-file <path>", "Optional copied Sales Navigator Lead Search curl request")
|
|
13401
|
+
.option("--browser-relay-port <number>", "Use the signed-in browser through a loopback relay")
|
|
13402
|
+
.option("--page-size <number>", "Direct Sales Navigator page size", "100")
|
|
13403
|
+
.option("--page-delay-min-ms <number>", "Minimum delay between direct pages", "5000")
|
|
13404
|
+
.option("--page-delay-max-ms <number>", "Maximum delay between direct pages", "8000")
|
|
13405
|
+
.option("--dry-run", "Validate without collecting or importing", false)
|
|
13406
|
+
.option("--out <path>", "Optional local JSON output path")
|
|
13407
|
+
.action(async (options) => {
|
|
13408
|
+
printOutput(await runSalesNavigatorPeopleCollectCommand(options));
|
|
13409
|
+
});
|
|
13273
13410
|
addAffiliateAudienceOptions(program
|
|
13274
13411
|
.command("affiliate:launch")
|
|
13275
13412
|
.description("Build an affiliate audience from an affiliate link and LinkedIn search URL."))
|
|
@@ -13360,13 +13497,17 @@ program
|
|
|
13360
13497
|
.option("--source-audience-run-id <run-id>", "Use a newer compatible audience to top up this campaign")
|
|
13361
13498
|
.action(async (runId, options) => {
|
|
13362
13499
|
const session = await requireAuthSession();
|
|
13363
|
-
const
|
|
13500
|
+
const result = await enrichAffiliateOutreachViaApp(session, z.string().uuid().parse(runId), options.sourceAudienceRunId
|
|
13364
13501
|
? z.string().uuid().parse(options.sourceAudienceRunId)
|
|
13365
13502
|
: undefined);
|
|
13366
13503
|
printOutput({
|
|
13367
13504
|
status: "ok",
|
|
13368
|
-
|
|
13369
|
-
|
|
13505
|
+
processing: result.processing,
|
|
13506
|
+
workflowRunId: result.workflowRunId,
|
|
13507
|
+
outreach: result.outreach,
|
|
13508
|
+
message: result.processing
|
|
13509
|
+
? `Email recovery is running in the background. Check: salesprompter affiliate:status ${runId}`
|
|
13510
|
+
: "Email recovery finished and new verified leads were added."
|
|
13370
13511
|
});
|
|
13371
13512
|
});
|
|
13372
13513
|
program
|
package/package.json
CHANGED