salesprompter-cli 0.1.47 → 0.1.49

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 +155 -23
  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
@@ -8,6 +8,7 @@ import path from "node:path";
8
8
  import { emitKeypressEvents } from "node:readline";
9
9
  import { createInterface } from "node:readline/promises";
10
10
  import { setTimeout as delay } from "node:timers/promises";
11
+ import { gzipSync } from "node:zlib";
11
12
  import { createClient } from "@supabase/supabase-js";
12
13
  import pg from "pg";
13
14
  import { Command } from "commander";
@@ -147,6 +148,14 @@ const AffiliateCampaignLaunchResponseSchema = z.object({
147
148
  })
148
149
  .passthrough()
149
150
  .optional(),
151
+ workspaceImport: z
152
+ .object({
153
+ runId: z.string().uuid(),
154
+ imported: z.number().int().nonnegative(),
155
+ upserted: z.number().int().nonnegative(),
156
+ previewUrl: z.string().min(1)
157
+ })
158
+ .optional(),
150
159
  previewUrl: z.string().nullable().optional(),
151
160
  next: z.string().optional()
152
161
  });
@@ -469,6 +478,7 @@ const cliPacks = [
469
478
  summary: "Find leads from product and market inputs.",
470
479
  commands: [
471
480
  "leads:discover",
481
+ "leads:collect",
472
482
  "search:run",
473
483
  "search:status",
474
484
  "search:export",
@@ -506,6 +516,7 @@ const helpAliasByCommandName = new Map([
506
516
  ["dealroom-companies:scrape-local", "companies:scrape-dealroom"],
507
517
  ["linkedin-products:scrape", "market:scrape"],
508
518
  ["salesnav:from-product-category", "leads:discover"],
519
+ ["salesnav:people:collect", "leads:collect"],
509
520
  ["salesnav:crawl", "search:run"],
510
521
  ["salesnav:crawl:status", "search:status"],
511
522
  ["salesnav:export", "search:export"],
@@ -550,6 +561,7 @@ const helpVisibleCommandNames = new Set([
550
561
  "dealroom-companies:scrape-local",
551
562
  "linkedin-products:scrape",
552
563
  "salesnav:from-product-category",
564
+ "salesnav:people:collect",
553
565
  "salesnav:crawl",
554
566
  "salesnav:crawl:status",
555
567
  "salesnav:export",
@@ -6793,6 +6805,10 @@ function buildSalesNavigatorApiRequestFromSearchUrl(searchUrl, config, count) {
6793
6805
  }
6794
6806
  function buildSalesNavigatorLeadApiUrlFromSearchUrl(searchUrl, count) {
6795
6807
  const source = new URL(searchUrl);
6808
+ if (!source.searchParams.has("query") && source.hash.startsWith("#query=")) {
6809
+ source.search = `?${source.hash.slice(1)}`;
6810
+ source.hash = "";
6811
+ }
6796
6812
  if (source.pathname.includes("/sales-api/salesApiLeadSearch")) {
6797
6813
  return source.toString();
6798
6814
  }
@@ -8724,34 +8740,41 @@ function describeLocalSalesNavigatorAccountPartialError(fetchResult) {
8724
8740
  async function importLocalSalesNavigatorPeopleViaApp(session, payload) {
8725
8741
  const { value } = await fetchCliJson(session, (currentSession) => {
8726
8742
  const workspaceClientId = currentSession.user.workspaceClientId?.trim() || null;
8743
+ const serializedPayload = JSON.stringify({
8744
+ sourceQueryUrl: payload.sourceQueryUrl,
8745
+ slicedQueryUrl: payload.sourceQueryUrl,
8746
+ appliedFilters: [],
8747
+ maxResultsPerSearch: payload.maxResultsPerSearch,
8748
+ numberOfProfiles: payload.numberOfProfiles,
8749
+ slicePreset: payload.slicePreset,
8750
+ totalResults: payload.totalResults,
8751
+ fetchedPages: payload.fetchedPages,
8752
+ people: payload.people,
8753
+ rawPayload: {
8754
+ clientId: workspaceClientId ?? undefined,
8755
+ client_id: workspaceClientId ?? undefined,
8756
+ leadList: workspaceClientId
8757
+ ? { clientId: workspaceClientId }
8758
+ : undefined,
8759
+ workflow: "salesnav:local-import",
8760
+ localPeopleCount: payload.people.length,
8761
+ fetchedPages: payload.fetchedPages,
8762
+ pacing: payload.pacing,
8763
+ },
8764
+ });
8765
+ const shouldCompress = Buffer.byteLength(serializedPayload) >= 512 * 1024;
8727
8766
  return fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/local-import`, {
8728
8767
  method: "POST",
8729
8768
  headers: {
8730
- "Content-Type": "application/json",
8769
+ "Content-Type": shouldCompress
8770
+ ? "application/octet-stream"
8771
+ : "application/json",
8731
8772
  Authorization: `Bearer ${currentSession.accessToken}`,
8773
+ ...(shouldCompress
8774
+ ? { "X-Salesprompter-Content-Encoding": "gzip" }
8775
+ : {}),
8732
8776
  },
8733
- body: JSON.stringify({
8734
- sourceQueryUrl: payload.sourceQueryUrl,
8735
- slicedQueryUrl: payload.sourceQueryUrl,
8736
- appliedFilters: [],
8737
- maxResultsPerSearch: payload.maxResultsPerSearch,
8738
- numberOfProfiles: payload.numberOfProfiles,
8739
- slicePreset: payload.slicePreset,
8740
- totalResults: payload.totalResults,
8741
- fetchedPages: payload.fetchedPages,
8742
- people: payload.people,
8743
- rawPayload: {
8744
- clientId: workspaceClientId ?? undefined,
8745
- client_id: workspaceClientId ?? undefined,
8746
- leadList: workspaceClientId
8747
- ? { clientId: workspaceClientId }
8748
- : undefined,
8749
- workflow: "salesnav:local-import",
8750
- localPeopleCount: payload.people.length,
8751
- fetchedPages: payload.fetchedPages,
8752
- pacing: payload.pacing,
8753
- },
8754
- }),
8777
+ body: shouldCompress ? gzipSync(serializedPayload) : serializedPayload,
8755
8778
  });
8756
8779
  }, SalesNavigatorLocalImportResponseSchema);
8757
8780
  return value;
@@ -13198,6 +13221,99 @@ program
13198
13221
  }
13199
13222
  });
13200
13223
  });
13224
+ async function runSalesNavigatorPeopleCollectCommand(options) {
13225
+ const linkedInUrl = z.string().url().parse(options.linkedinUrl);
13226
+ const maxResults = z.coerce.number().int().min(1).max(100000).parse(options.maxResults);
13227
+ const pageSize = z.coerce.number().int().min(1).max(100).parse(options.pageSize);
13228
+ const normalizedSourceUrl = (() => {
13229
+ const source = new URL(linkedInUrl);
13230
+ if (!source.searchParams.has("query") && source.hash.startsWith("#query=")) {
13231
+ source.search = `?${source.hash.slice(1)}`;
13232
+ source.hash = "";
13233
+ }
13234
+ return source.toString();
13235
+ })();
13236
+ if (options.dryRun) {
13237
+ const payload = {
13238
+ status: "ok",
13239
+ dryRun: true,
13240
+ linkedInUrl: normalizedSourceUrl,
13241
+ maxResults,
13242
+ destination: "/leads/cli-imports"
13243
+ };
13244
+ if (options.out)
13245
+ await writeJsonFile(options.out, payload);
13246
+ return payload;
13247
+ }
13248
+ const session = await requireAuthSession();
13249
+ const pageDelayMinMs = z.coerce.number().int().min(0).parse(options.pageDelayMinMs);
13250
+ const pageDelayMaxMs = z.coerce.number().int().min(pageDelayMinMs).parse(options.pageDelayMaxMs);
13251
+ const browserRelayPort = options.browserRelayPort == null
13252
+ ? null
13253
+ : z.coerce.number().int().min(0).max(65535).parse(options.browserRelayPort);
13254
+ if (options.curlFile && browserRelayPort != null) {
13255
+ throw new Error("Use either --curl-file or --browser-relay-port, not both.");
13256
+ }
13257
+ const parsedRequest = options.curlFile
13258
+ ? parseSalesNavigatorCurlRequest(await readFile(path.resolve(String(options.curlFile)), "utf8"))
13259
+ : browserRelayPort != null
13260
+ ? {
13261
+ url: buildSalesNavigatorLeadApiUrlFromSearchUrl(normalizedSourceUrl, pageSize),
13262
+ headers: {}
13263
+ }
13264
+ : buildSalesNavigatorApiRequestFromSearchUrl(normalizedSourceUrl, await readLinkedInDirectLookupConfig(), pageSize);
13265
+ if (!new URL(parsedRequest.url).pathname.includes("/sales-api/salesApiLeadSearch")) {
13266
+ throw new Error("--curl-file must contain a /sales-api/salesApiLeadSearch request.");
13267
+ }
13268
+ const browserRelay = browserRelayPort == null
13269
+ ? null
13270
+ : await createLocalAccountSearchBrowserRelay(browserRelayPort);
13271
+ let collected;
13272
+ try {
13273
+ collected = await fetchAllLocalSalesNavigatorPeople(parsedRequest, {
13274
+ requestedProfiles: maxResults,
13275
+ pageSize,
13276
+ pageDelayMinMs,
13277
+ pageDelayMaxMs,
13278
+ retry: {
13279
+ maxRetries: 2,
13280
+ retryBaseDelayMs: 2_000,
13281
+ retryMaxDelayMs: 10_000
13282
+ },
13283
+ executeRequest: browserRelay
13284
+ ? (request) => browserRelay.request(request.url)
13285
+ : undefined
13286
+ });
13287
+ }
13288
+ finally {
13289
+ await browserRelay?.close();
13290
+ }
13291
+ const imported = await importLocalSalesNavigatorPeopleViaApp(session, {
13292
+ sourceQueryUrl: normalizedSourceUrl,
13293
+ people: collected.people,
13294
+ totalResults: collected.totalResults,
13295
+ fetchedPages: collected.fetchedPages,
13296
+ pacing: collected.pacing,
13297
+ maxResultsPerSearch: maxResults,
13298
+ numberOfProfiles: maxResults,
13299
+ slicePreset: "local-salesnav-people"
13300
+ });
13301
+ const payload = {
13302
+ status: "ok",
13303
+ dryRun: false,
13304
+ collected: collected.people.length,
13305
+ totalResults: collected.totalResults,
13306
+ fetchedPages: collected.fetchedPages,
13307
+ runId: imported.runId,
13308
+ imported: imported.imported,
13309
+ upserted: imported.upserted,
13310
+ workspaceUrl: `${session.apiBaseUrl}/leads/cli-imports?runId=` +
13311
+ encodeURIComponent(imported.runId)
13312
+ };
13313
+ if (options.out)
13314
+ await writeJsonFile(options.out, payload);
13315
+ return payload;
13316
+ }
13201
13317
  async function runAffiliateLaunchCommand(options) {
13202
13318
  const affiliateLink = z.string().url().parse(options.affiliateLink);
13203
13319
  const linkedInUrl = z.string().url().parse(options.linkedinUrl);
@@ -13283,6 +13399,22 @@ function addAffiliateAudienceOptions(command) {
13283
13399
  .option("--dry-run", "Validate and preview without creating remote resources", false)
13284
13400
  .option("--out <path>", "Optional local JSON output path");
13285
13401
  }
13402
+ program
13403
+ .command("salesnav:people:collect")
13404
+ .alias("leads:collect")
13405
+ .description("Collect a Sales Navigator people search into the active workspace.")
13406
+ .requiredOption("--linkedin-url <url>", "Sales Navigator people search URL")
13407
+ .option("--max-results <number>", "Maximum people to collect", "1000")
13408
+ .option("--curl-file <path>", "Optional copied Sales Navigator Lead Search curl request")
13409
+ .option("--browser-relay-port <number>", "Use the signed-in browser through a loopback relay")
13410
+ .option("--page-size <number>", "Direct Sales Navigator page size", "100")
13411
+ .option("--page-delay-min-ms <number>", "Minimum delay between direct pages", "5000")
13412
+ .option("--page-delay-max-ms <number>", "Maximum delay between direct pages", "8000")
13413
+ .option("--dry-run", "Validate without collecting or importing", false)
13414
+ .option("--out <path>", "Optional local JSON output path")
13415
+ .action(async (options) => {
13416
+ printOutput(await runSalesNavigatorPeopleCollectCommand(options));
13417
+ });
13286
13418
  addAffiliateAudienceOptions(program
13287
13419
  .command("affiliate:launch")
13288
13420
  .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.49",
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",