salesprompter-cli 0.1.47 → 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.
Files changed (3) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +124 -0
  3. 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
- # Preview an affiliate audience without touching LinkedIn
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=..." \
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",
@@ -6793,6 +6804,10 @@ function buildSalesNavigatorApiRequestFromSearchUrl(searchUrl, config, count) {
6793
6804
  }
6794
6805
  function buildSalesNavigatorLeadApiUrlFromSearchUrl(searchUrl, count) {
6795
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
+ }
6796
6811
  if (source.pathname.includes("/sales-api/salesApiLeadSearch")) {
6797
6812
  return source.toString();
6798
6813
  }
@@ -13198,6 +13213,99 @@ program
13198
13213
  }
13199
13214
  });
13200
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
+ }
13201
13309
  async function runAffiliateLaunchCommand(options) {
13202
13310
  const affiliateLink = z.string().url().parse(options.affiliateLink);
13203
13311
  const linkedInUrl = z.string().url().parse(options.linkedinUrl);
@@ -13283,6 +13391,22 @@ function addAffiliateAudienceOptions(command) {
13283
13391
  .option("--dry-run", "Validate and preview without creating remote resources", false)
13284
13392
  .option("--out <path>", "Optional local JSON output path");
13285
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
+ });
13286
13410
  addAffiliateAudienceOptions(program
13287
13411
  .command("affiliate:launch")
13288
13412
  .description("Build an affiliate audience from an affiliate link and LinkedIn search URL."))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "description": "Sales workflow CLI for guided lead generation, enrichment, scoring, and sync.",
5
5
  "author": "Daniel Sinewe <hello@danielsinewe.com>",
6
6
  "type": "module",