salesprompter-cli 0.1.37 → 0.1.39

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 (2) hide show
  1. package/dist/cli.js +189 -11
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
+ import { createHash } from "node:crypto";
3
4
  import { access, appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
4
5
  import { createRequire } from "node:module";
5
6
  import os from "node:os";
@@ -25,7 +26,8 @@ import { InstantlySyncProvider } from "./instantly.js";
25
26
  import { backfillLinkedInCompanies } from "./linkedin-companies.js";
26
27
  import { parseLinkedInCompanyPage } from "./linkedin-companies.js";
27
28
  import { crawlLinkedInProductCategory } from "./linkedin-products.js";
28
- import { claimValidatedSalesNavigatorSessionCookieForCli, createLinkedInSessionSupabaseClient, resolveConfiguredEnvValue } from "./linkedin-session.js";
29
+ import { claimValidatedSalesNavigatorSessionCookieForCli, createLinkedInSessionSupabaseClient, probeSalesNavigatorSearchSession, resolveConfiguredEnvValue } from "./linkedin-session.js";
30
+ import { extractLiAtCookieValue } from "./linkedin-session-contracts.js";
29
31
  import { buildLeadlistsFunnelQueries } from "./leadlists-funnel.js";
30
32
  import { readJsonFile, splitCsv, writeJsonFile, writeTextFile } from "./io.js";
31
33
  import { buildSalesNavigatorCrawlPreview, createSalesNavigatorCrawlSeed, DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS, buildSalesNavigatorPeopleSlice, deriveSalesNavigatorTitleQuerySeeds, expandSalesNavigatorCrawlAttempt, SalesNavigatorSliceTooBroadError } from "./sales-navigator.js";
@@ -122,6 +124,20 @@ const LinkedInCompanyBackfillStatusResponseSchema = z.object({
122
124
  failureCode: z.string().nullable().optional(),
123
125
  failureMessage: z.string().nullable().optional()
124
126
  });
127
+ const LeadPoolEmailFinderProcessResponseSchema = z.object({
128
+ success: z.boolean(),
129
+ provider: z.string().optional(),
130
+ selected: z.number().int().nonnegative(),
131
+ found: z.number().int().nonnegative(),
132
+ notFound: z.number().int().nonnegative(),
133
+ failed: z.number().int().nonnegative(),
134
+ durationMs: z.number().nonnegative(),
135
+ launched: z.boolean().optional(),
136
+ agentId: z.string().nullable().optional(),
137
+ containerId: z.string().nullable().optional(),
138
+ csvInputUrl: z.string().nullable().optional(),
139
+ processedAt: z.string()
140
+ });
125
141
  const PhantombusterContainersSyncResponseSchema = z.object({
126
142
  status: z.literal("ok"),
127
143
  agentIds: z.array(z.string().min(1)),
@@ -5717,6 +5733,24 @@ async function syncPhantombusterContainersViaApp(session, payload) {
5717
5733
  }), PhantombusterContainersSyncResponseSchema);
5718
5734
  return value;
5719
5735
  }
5736
+ async function runLeadPoolEmailFinderViaApp(session, payload) {
5737
+ const url = new URL("/api/leadpool/process-hunter-emails", session.apiBaseUrl);
5738
+ url.searchParams.set("provider", payload.provider);
5739
+ url.searchParams.set("limit", String(payload.limit));
5740
+ if (payload.concurrency != null) {
5741
+ url.searchParams.set("concurrency", String(payload.concurrency));
5742
+ }
5743
+ if (payload.retryNotFoundAfterDays != null) {
5744
+ url.searchParams.set("retryNotFoundAfterDays", String(payload.retryNotFoundAfterDays));
5745
+ }
5746
+ const { value } = await fetchCliJson(session, (currentSession) => fetch(url.toString(), {
5747
+ method: "POST",
5748
+ headers: {
5749
+ Authorization: `Bearer ${currentSession.accessToken}`
5750
+ }
5751
+ }), LeadPoolEmailFinderProcessResponseSchema);
5752
+ return value;
5753
+ }
5720
5754
  function serializeSalesNavigatorFiltersForApi(filters) {
5721
5755
  return filters.map((filter) => ({
5722
5756
  type: filter.type,
@@ -5869,6 +5903,15 @@ async function runSalesNavigatorExport(session, payload, traceId, logOptions = {
5869
5903
  }
5870
5904
  }
5871
5905
  async function startSalesNavigatorExport(session, payload, traceId) {
5906
+ const exportSession = await resolveSalesNavigatorExportSessionOverride(payload.slicedQueryUrl, {
5907
+ source: "cli_salesnav_export_start"
5908
+ });
5909
+ const sessionOverridePayload = exportSession
5910
+ ? {
5911
+ sessionCookie: exportSession.sessionCookie,
5912
+ selectedSessionCookieSha256: exportSession.selectedSessionCookieSha256
5913
+ }
5914
+ : {};
5872
5915
  return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/export`, {
5873
5916
  method: "POST",
5874
5917
  signal: AbortSignal.timeout(SALES_NAVIGATOR_EXPORT_START_TIMEOUT_MS),
@@ -5879,7 +5922,8 @@ async function startSalesNavigatorExport(session, payload, traceId) {
5879
5922
  },
5880
5923
  body: JSON.stringify({
5881
5924
  ...payload,
5882
- appliedFilters: serializeSalesNavigatorFiltersForApi(payload.appliedFilters)
5925
+ appliedFilters: serializeSalesNavigatorFiltersForApi(payload.appliedFilters),
5926
+ ...sessionOverridePayload
5883
5927
  })
5884
5928
  }), SalesNavigatorExportStartResponseSchema);
5885
5929
  }
@@ -6686,25 +6730,95 @@ function buildSalesNavigatorSliceFailureReport(slice, error, options) {
6686
6730
  function formatSalesNavigatorSplitTrail(splitTrail) {
6687
6731
  return splitTrail.map((entry) => `${entry.key}:${entry.value.text}`);
6688
6732
  }
6733
+ let cachedSalesNavigatorExportSessionOverride = null;
6734
+ function hashSalesNavigatorSessionCookieForPhantombuster(sessionCookie) {
6735
+ const liAt = extractLiAtCookieValue(sessionCookie);
6736
+ if (!liAt) {
6737
+ return null;
6738
+ }
6739
+ return createHash("sha256").update(liAt).digest("hex");
6740
+ }
6741
+ function normalizeSalesNavigatorSessionCookieForPhantombuster(sessionCookie) {
6742
+ return extractLiAtCookieValue(sessionCookie) ?? sessionCookie;
6743
+ }
6744
+ function cacheSalesNavigatorExportSessionOverride(queryUrl, value) {
6745
+ cachedSalesNavigatorExportSessionOverride = {
6746
+ queryUrl,
6747
+ expiresAt: Date.now() + 120_000,
6748
+ value
6749
+ };
6750
+ return value;
6751
+ }
6752
+ async function resolveSalesNavigatorExportSessionOverride(queryUrl, options) {
6753
+ if (cachedSalesNavigatorExportSessionOverride &&
6754
+ cachedSalesNavigatorExportSessionOverride.queryUrl === queryUrl &&
6755
+ cachedSalesNavigatorExportSessionOverride.expiresAt > Date.now()) {
6756
+ return cachedSalesNavigatorExportSessionOverride.value;
6757
+ }
6758
+ const localExtensionConfig = await readLocalLinkedInExtensionDirectLookupConfig();
6759
+ if (localExtensionConfig?.cookie) {
6760
+ const selectedSessionCookieSha256 = hashSalesNavigatorSessionCookieForPhantombuster(localExtensionConfig.cookie);
6761
+ if (process.env.SALESPROMPTER_SALESNAV_EXPORT_SKIP_EXTENSION_PREFLIGHT === "1") {
6762
+ return cacheSalesNavigatorExportSessionOverride(queryUrl, {
6763
+ sessionCookie: normalizeSalesNavigatorSessionCookieForPhantombuster(localExtensionConfig.cookie),
6764
+ selectedSessionCookieSha256,
6765
+ source: "chrome_extension"
6766
+ });
6767
+ }
6768
+ const probe = await probeSalesNavigatorSearchSession(localExtensionConfig.cookie, queryUrl, {
6769
+ timeoutMs: 8000
6770
+ });
6771
+ await options.logger?.log("salesnav.export.session.chrome_extension.preflight", {
6772
+ source: options.source,
6773
+ queryUrl,
6774
+ status: probe.status,
6775
+ selectedSessionCookieSha256,
6776
+ finalUrl: probe.finalUrl,
6777
+ validationError: probe.validationError
6778
+ });
6779
+ if (probe.status === "ok") {
6780
+ return cacheSalesNavigatorExportSessionOverride(queryUrl, {
6781
+ sessionCookie: normalizeSalesNavigatorSessionCookieForPhantombuster(localExtensionConfig.cookie),
6782
+ selectedSessionCookieSha256,
6783
+ source: "chrome_extension"
6784
+ });
6785
+ }
6786
+ }
6787
+ const claimed = await claimValidatedSalesNavigatorSessionCookieForCli({
6788
+ queryUrl,
6789
+ source: options.source,
6790
+ env: process.env
6791
+ });
6792
+ if (claimed?.sessionCookie) {
6793
+ return cacheSalesNavigatorExportSessionOverride(queryUrl, {
6794
+ sessionCookie: normalizeSalesNavigatorSessionCookieForPhantombuster(claimed.sessionCookie),
6795
+ selectedSessionCookieSha256: claimed.sessionCookieSha256 ??
6796
+ hashSalesNavigatorSessionCookieForPhantombuster(claimed.sessionCookie),
6797
+ source: "session_vault"
6798
+ });
6799
+ }
6800
+ if (localExtensionConfig?.cookie &&
6801
+ process.env.SALESPROMPTER_SALESNAV_EXPORT_REQUIRE_FRESH_SESSION === "1") {
6802
+ throw new Error("The local Salesprompter Chrome extension session cookie is not valid for Sales Navigator. Reconnect the extension, then retry the CLI command.");
6803
+ }
6804
+ if (process.env.SALESPROMPTER_CLI_MANAGE_LINKEDIN_SESSIONS === "1") {
6805
+ throw new Error("No validated LinkedIn Sales Navigator session cookie is available from the CLI-managed session pool.");
6806
+ }
6807
+ return null;
6808
+ }
6689
6809
  async function ensureSalesNavigatorSessionPoolReady(queryUrl, options) {
6690
6810
  try {
6691
6811
  await options.logger?.log("salesnav.session_pool.preflight.started", {
6692
6812
  source: options.source,
6693
6813
  queryUrl
6694
6814
  });
6695
- const claimed = await claimValidatedSalesNavigatorSessionCookieForCli({
6696
- queryUrl,
6697
- source: options.source,
6698
- env: process.env
6699
- });
6815
+ const claimed = await resolveSalesNavigatorExportSessionOverride(queryUrl, options);
6700
6816
  await options.logger?.log("salesnav.session_pool.preflight.completed", {
6701
6817
  source: options.source,
6702
6818
  queryUrl,
6703
6819
  status: claimed ? "ok" : "skipped",
6704
- selectedSessionUserEmail: claimed?.userEmail ?? null,
6705
- selectedSessionUserHandle: claimed?.userHandle ?? null,
6706
- selectedSessionCookieSha256: claimed?.sessionCookieSha256 ?? null,
6707
- selectedSessionLastIngestedSource: claimed?.lastIngestedSource ?? null
6820
+ selectedSessionSource: claimed?.source ?? "app_managed",
6821
+ selectedSessionCookieSha256: claimed?.selectedSessionCookieSha256 ?? null
6708
6822
  });
6709
6823
  return {
6710
6824
  ready: true
@@ -10681,6 +10795,8 @@ program
10681
10795
  .requiredOption("--out-dir <path>", "Output directory for artifacts")
10682
10796
  .option("--trace-id <traceId>", "Trace id for this batch")
10683
10797
  .option("--endpoint-url <url>", "Override the enrichment workflow endpoint")
10798
+ .option("--provider <provider>", "Email finder provider for workspace queue: hunter or phantombuster", "hunter")
10799
+ .option("--retry-not-found-after-days <number>", "Retry stale Email not found rows after this many days", "1")
10684
10800
  .option("--timeout-ms <number>", "Workflow trigger timeout in milliseconds", "30000")
10685
10801
  .option("--company-cleaning <mode>", "Company cleaning mode for direct input: off, basic, or ai", "basic")
10686
10802
  .option("--dry-run", "Preview the next batch without starting background processing", false)
@@ -10689,7 +10805,69 @@ program
10689
10805
  const outDir = z.string().min(1).parse(options.outDir);
10690
10806
  const timeoutMs = z.coerce.number().int().min(1000).max(300000).parse(options.timeoutMs);
10691
10807
  const usingDirectInput = typeof options.in === "string" && options.in.trim().length > 0;
10808
+ const provider = z.enum(["hunter", "phantombuster"]).parse(String(options.provider ?? "hunter").trim().toLowerCase());
10809
+ const retryNotFoundAfterDays = z.coerce.number().int().min(0).max(30).parse(options.retryNotFoundAfterDays);
10692
10810
  const companyCleaningMode = resolveCompanyCleaningMode(String(options.companyCleaning ?? process.env.SALESPROMPTER_COMPANY_CLEANING_MODE ?? "basic"));
10811
+ if (provider === "phantombuster" && !usingDirectInput) {
10812
+ await mkdir(outDir, { recursive: true });
10813
+ const traceId = z.string().min(1).parse(options.traceId ?? `salesprompter-cli-email-phantombuster-${Date.now()}`);
10814
+ const requestPath = `${outDir}/email-enrichment-request-phantombuster.json`;
10815
+ const responsePath = `${outDir}/email-enrichment-response-phantombuster.json`;
10816
+ const requestPayload = {
10817
+ traceId,
10818
+ provider,
10819
+ limit,
10820
+ retryNotFoundAfterDays
10821
+ };
10822
+ await writeJsonFile(requestPath, requestPayload);
10823
+ if (options.dryRun) {
10824
+ printOutput({
10825
+ status: "ok",
10826
+ provider,
10827
+ limit,
10828
+ traceId,
10829
+ dryRun: true,
10830
+ mode: "workspace-queue",
10831
+ contactsQueued: null,
10832
+ started: false,
10833
+ artifacts: {
10834
+ inputSqlPath: null,
10835
+ inputRowsPath: null,
10836
+ directReportPath: null,
10837
+ requestPath,
10838
+ responsePath: null
10839
+ }
10840
+ });
10841
+ return;
10842
+ }
10843
+ const session = await requireAuthSession();
10844
+ const result = await runLeadPoolEmailFinderViaApp(session, {
10845
+ provider,
10846
+ limit,
10847
+ retryNotFoundAfterDays
10848
+ });
10849
+ await writeJsonFile(responsePath, result);
10850
+ printOutput({
10851
+ status: result.success ? "ok" : "partial",
10852
+ provider,
10853
+ limit,
10854
+ traceId,
10855
+ dryRun: false,
10856
+ mode: "workspace-queue",
10857
+ contactsQueued: result.selected,
10858
+ started: Boolean(result.launched),
10859
+ agentId: result.agentId ?? null,
10860
+ containerId: result.containerId ?? null,
10861
+ artifacts: {
10862
+ inputSqlPath: null,
10863
+ inputRowsPath: null,
10864
+ directReportPath: null,
10865
+ requestPath,
10866
+ responsePath
10867
+ }
10868
+ });
10869
+ return;
10870
+ }
10693
10871
  let clientId;
10694
10872
  let queueRows;
10695
10873
  let directReportRows = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
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",