salesprompter-cli 0.1.36 → 0.1.38

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 +194 -17
  2. package/package.json +1 -1
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 { access, appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
+ import { createHash } from "node:crypto";
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";
6
7
  import path from "node:path";
@@ -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";
@@ -136,7 +138,13 @@ const PhantombusterContainersSyncResponseSchema = z.object({
136
138
  resultsSynced: z.number().int().nonnegative(),
137
139
  outputsStored: z.number().int().nonnegative(),
138
140
  resultObjectsStored: z.number().int().nonnegative(),
139
- resultRowsStored: z.number().int().nonnegative()
141
+ resultRowsStored: z.number().int().nonnegative(),
142
+ leadListsProjected: z.number().int().nonnegative().optional(),
143
+ leadListContactsProjected: z.number().int().nonnegative().optional(),
144
+ contactsProjected: z.number().int().nonnegative().optional(),
145
+ leadPoolRows: z.number().int().nonnegative().nullable().optional(),
146
+ qualifiedContacts: z.number().int().nonnegative().nullable().optional(),
147
+ qualifiedCompanies: z.number().int().nonnegative().nullable().optional()
140
148
  });
141
149
  const CliEmailEnrichmentCompaniesResponseSchema = z.object({
142
150
  clientId: z.number().int().positive(),
@@ -4834,19 +4842,29 @@ function buildLinkedInProductCategorySalesNavigatorOutputPath(categorySlug) {
4834
4842
  return `./data/salesnav-product-category-${categorySlug}.json`;
4835
4843
  }
4836
4844
  const SALES_NAVIGATOR_TERMINAL_JOB_STATUSES = new Set(["completed", "completed_with_failures"]);
4845
+ const WORKFLOW_LOCAL_LOG_MAX_FILE_BYTES = 10 * 1024 * 1024;
4846
+ const WORKFLOW_LOCAL_LOG_MAX_LINE_BYTES = 32 * 1024;
4847
+ const WORKFLOW_LOCAL_LOG_MAX_STRING_CHARS = 4000;
4848
+ const WORKFLOW_LOCAL_LOG_MAX_ARRAY_ITEMS = 50;
4849
+ const WORKFLOW_LOCAL_LOG_MAX_OBJECT_KEYS = 50;
4850
+ const WORKFLOW_LOCAL_LOG_MAX_DEPTH = 5;
4837
4851
  function isSalesNavigatorCrawlJobTerminal(status) {
4838
4852
  return SALES_NAVIGATOR_TERMINAL_JOB_STATUSES.has(status);
4839
4853
  }
4840
4854
  function buildWorkflowTraceId(prefix) {
4841
4855
  return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
4842
4856
  }
4857
+ function buildWorkflowLogRunSuffix() {
4858
+ const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
4859
+ return `${timestamp}-${Math.random().toString(36).slice(2, 8)}`;
4860
+ }
4843
4861
  function buildSalesNavigatorWorkflowLogPath(input) {
4844
4862
  const slug = slugify(input) || "salesnav-product-category";
4845
- return `./data/${slug}-salesnav.log.jsonl`;
4863
+ return `./data/${slug}-${buildWorkflowLogRunSuffix()}-salesnav.log.jsonl`;
4846
4864
  }
4847
4865
  function buildSalesNavigatorCrawlLogPath(input) {
4848
4866
  const slug = slugify(input) || "salesnav-crawl";
4849
- return `./data/${slug}-crawl.log.jsonl`;
4867
+ return `./data/${slug}-${buildWorkflowLogRunSuffix()}-crawl.log.jsonl`;
4850
4868
  }
4851
4869
  function buildSalesNavigatorCrawlOutputPath(input) {
4852
4870
  const slug = slugify(input) || "salesnav-crawl";
@@ -4878,6 +4896,83 @@ function decodeSalesNavigatorQueryParam(url) {
4878
4896
  return null;
4879
4897
  }
4880
4898
  }
4899
+ function sanitizeWorkflowLogValue(value, depth = 0, seen = new WeakSet()) {
4900
+ if (typeof value === "string") {
4901
+ if (value.length <= WORKFLOW_LOCAL_LOG_MAX_STRING_CHARS) {
4902
+ return value;
4903
+ }
4904
+ return `${value.slice(0, WORKFLOW_LOCAL_LOG_MAX_STRING_CHARS)}... [truncated ${value.length - WORKFLOW_LOCAL_LOG_MAX_STRING_CHARS} chars]`;
4905
+ }
4906
+ if (typeof value !== "object" || value === null) {
4907
+ return value;
4908
+ }
4909
+ if (seen.has(value)) {
4910
+ return "[Circular]";
4911
+ }
4912
+ if (depth >= WORKFLOW_LOCAL_LOG_MAX_DEPTH) {
4913
+ return "[MaxDepth]";
4914
+ }
4915
+ seen.add(value);
4916
+ if (Array.isArray(value)) {
4917
+ const items = value
4918
+ .slice(0, WORKFLOW_LOCAL_LOG_MAX_ARRAY_ITEMS)
4919
+ .map((item) => sanitizeWorkflowLogValue(item, depth + 1, seen));
4920
+ if (value.length > WORKFLOW_LOCAL_LOG_MAX_ARRAY_ITEMS) {
4921
+ items.push({ truncatedItems: value.length - WORKFLOW_LOCAL_LOG_MAX_ARRAY_ITEMS });
4922
+ }
4923
+ return items;
4924
+ }
4925
+ const entries = Object.entries(value);
4926
+ const sanitized = {};
4927
+ for (const [key, entryValue] of entries.slice(0, WORKFLOW_LOCAL_LOG_MAX_OBJECT_KEYS)) {
4928
+ sanitized[key] = sanitizeWorkflowLogValue(entryValue, depth + 1, seen);
4929
+ }
4930
+ if (entries.length > WORKFLOW_LOCAL_LOG_MAX_OBJECT_KEYS) {
4931
+ sanitized.truncatedKeys = entries.length - WORKFLOW_LOCAL_LOG_MAX_OBJECT_KEYS;
4932
+ }
4933
+ return sanitized;
4934
+ }
4935
+ function serializeWorkflowLogEntry(entry) {
4936
+ const sanitizedEntry = {
4937
+ ...entry,
4938
+ metadata: sanitizeWorkflowLogValue(entry.metadata)
4939
+ };
4940
+ let line = JSON.stringify(sanitizedEntry);
4941
+ if (Buffer.byteLength(line, "utf8") <= WORKFLOW_LOCAL_LOG_MAX_LINE_BYTES) {
4942
+ return `${line}\n`;
4943
+ }
4944
+ const originalMetadata = entry.metadata ?? {};
4945
+ const metadataKeys = Object.keys(originalMetadata);
4946
+ line = JSON.stringify({
4947
+ ...entry,
4948
+ metadata: {
4949
+ localLogTruncated: true,
4950
+ originalMetadataKeys: metadataKeys.slice(0, WORKFLOW_LOCAL_LOG_MAX_OBJECT_KEYS),
4951
+ truncatedKeys: Math.max(0, metadataKeys.length - WORKFLOW_LOCAL_LOG_MAX_OBJECT_KEYS),
4952
+ originalMetadataBytes: Buffer.byteLength(JSON.stringify(sanitizeWorkflowLogValue(originalMetadata)), "utf8")
4953
+ }
4954
+ });
4955
+ return `${line}\n`;
4956
+ }
4957
+ async function appendWorkflowLocalLog(logPath, entry) {
4958
+ try {
4959
+ const current = await stat(logPath).catch((error) => {
4960
+ if (error.code === "ENOENT") {
4961
+ return null;
4962
+ }
4963
+ throw error;
4964
+ });
4965
+ if (current && current.size >= WORKFLOW_LOCAL_LOG_MAX_FILE_BYTES) {
4966
+ writeProgress(`[${entry.timestamp}] ${entry.event} (local log skipped because ${logPath} is already over 10 MB; durable event storage still runs when configured)`);
4967
+ return;
4968
+ }
4969
+ await appendFile(logPath, serializeWorkflowLogEntry(entry), "utf8");
4970
+ }
4971
+ catch (error) {
4972
+ const message = error instanceof Error ? error.message : String(error);
4973
+ writeProgress(`[${entry.timestamp}] workflow.local_log.write_failed: ${message}`);
4974
+ }
4975
+ }
4881
4976
  async function createWorkflowLogger(options) {
4882
4977
  const traceId = options.traceId ?? buildWorkflowTraceId("salesprompter-cli");
4883
4978
  const logPath = options.logPath;
@@ -4896,7 +4991,7 @@ async function createWorkflowLogger(options) {
4896
4991
  event,
4897
4992
  metadata
4898
4993
  };
4899
- await appendFile(logPath, `${JSON.stringify(entry)}\n`, "utf8");
4994
+ await appendWorkflowLocalLog(logPath, entry);
4900
4995
  if (eventStore) {
4901
4996
  try {
4902
4997
  await eventStore.append({
@@ -5776,6 +5871,15 @@ async function runSalesNavigatorExport(session, payload, traceId, logOptions = {
5776
5871
  }
5777
5872
  }
5778
5873
  async function startSalesNavigatorExport(session, payload, traceId) {
5874
+ const exportSession = await resolveSalesNavigatorExportSessionOverride(payload.slicedQueryUrl, {
5875
+ source: "cli_salesnav_export_start"
5876
+ });
5877
+ const sessionOverridePayload = exportSession
5878
+ ? {
5879
+ sessionCookie: exportSession.sessionCookie,
5880
+ selectedSessionCookieSha256: exportSession.selectedSessionCookieSha256
5881
+ }
5882
+ : {};
5779
5883
  return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/export`, {
5780
5884
  method: "POST",
5781
5885
  signal: AbortSignal.timeout(SALES_NAVIGATOR_EXPORT_START_TIMEOUT_MS),
@@ -5786,7 +5890,8 @@ async function startSalesNavigatorExport(session, payload, traceId) {
5786
5890
  },
5787
5891
  body: JSON.stringify({
5788
5892
  ...payload,
5789
- appliedFilters: serializeSalesNavigatorFiltersForApi(payload.appliedFilters)
5893
+ appliedFilters: serializeSalesNavigatorFiltersForApi(payload.appliedFilters),
5894
+ ...sessionOverridePayload
5790
5895
  })
5791
5896
  }), SalesNavigatorExportStartResponseSchema);
5792
5897
  }
@@ -6593,25 +6698,95 @@ function buildSalesNavigatorSliceFailureReport(slice, error, options) {
6593
6698
  function formatSalesNavigatorSplitTrail(splitTrail) {
6594
6699
  return splitTrail.map((entry) => `${entry.key}:${entry.value.text}`);
6595
6700
  }
6701
+ let cachedSalesNavigatorExportSessionOverride = null;
6702
+ function hashSalesNavigatorSessionCookieForPhantombuster(sessionCookie) {
6703
+ const liAt = extractLiAtCookieValue(sessionCookie);
6704
+ if (!liAt) {
6705
+ return null;
6706
+ }
6707
+ return createHash("sha256").update(liAt).digest("hex");
6708
+ }
6709
+ function normalizeSalesNavigatorSessionCookieForPhantombuster(sessionCookie) {
6710
+ return extractLiAtCookieValue(sessionCookie) ?? sessionCookie;
6711
+ }
6712
+ function cacheSalesNavigatorExportSessionOverride(queryUrl, value) {
6713
+ cachedSalesNavigatorExportSessionOverride = {
6714
+ queryUrl,
6715
+ expiresAt: Date.now() + 120_000,
6716
+ value
6717
+ };
6718
+ return value;
6719
+ }
6720
+ async function resolveSalesNavigatorExportSessionOverride(queryUrl, options) {
6721
+ if (cachedSalesNavigatorExportSessionOverride &&
6722
+ cachedSalesNavigatorExportSessionOverride.queryUrl === queryUrl &&
6723
+ cachedSalesNavigatorExportSessionOverride.expiresAt > Date.now()) {
6724
+ return cachedSalesNavigatorExportSessionOverride.value;
6725
+ }
6726
+ const localExtensionConfig = await readLocalLinkedInExtensionDirectLookupConfig();
6727
+ if (localExtensionConfig?.cookie) {
6728
+ const selectedSessionCookieSha256 = hashSalesNavigatorSessionCookieForPhantombuster(localExtensionConfig.cookie);
6729
+ if (process.env.SALESPROMPTER_SALESNAV_EXPORT_SKIP_EXTENSION_PREFLIGHT === "1") {
6730
+ return cacheSalesNavigatorExportSessionOverride(queryUrl, {
6731
+ sessionCookie: normalizeSalesNavigatorSessionCookieForPhantombuster(localExtensionConfig.cookie),
6732
+ selectedSessionCookieSha256,
6733
+ source: "chrome_extension"
6734
+ });
6735
+ }
6736
+ const probe = await probeSalesNavigatorSearchSession(localExtensionConfig.cookie, queryUrl, {
6737
+ timeoutMs: 8000
6738
+ });
6739
+ await options.logger?.log("salesnav.export.session.chrome_extension.preflight", {
6740
+ source: options.source,
6741
+ queryUrl,
6742
+ status: probe.status,
6743
+ selectedSessionCookieSha256,
6744
+ finalUrl: probe.finalUrl,
6745
+ validationError: probe.validationError
6746
+ });
6747
+ if (probe.status === "ok") {
6748
+ return cacheSalesNavigatorExportSessionOverride(queryUrl, {
6749
+ sessionCookie: normalizeSalesNavigatorSessionCookieForPhantombuster(localExtensionConfig.cookie),
6750
+ selectedSessionCookieSha256,
6751
+ source: "chrome_extension"
6752
+ });
6753
+ }
6754
+ }
6755
+ const claimed = await claimValidatedSalesNavigatorSessionCookieForCli({
6756
+ queryUrl,
6757
+ source: options.source,
6758
+ env: process.env
6759
+ });
6760
+ if (claimed?.sessionCookie) {
6761
+ return cacheSalesNavigatorExportSessionOverride(queryUrl, {
6762
+ sessionCookie: normalizeSalesNavigatorSessionCookieForPhantombuster(claimed.sessionCookie),
6763
+ selectedSessionCookieSha256: claimed.sessionCookieSha256 ??
6764
+ hashSalesNavigatorSessionCookieForPhantombuster(claimed.sessionCookie),
6765
+ source: "session_vault"
6766
+ });
6767
+ }
6768
+ if (localExtensionConfig?.cookie &&
6769
+ process.env.SALESPROMPTER_SALESNAV_EXPORT_REQUIRE_FRESH_SESSION === "1") {
6770
+ throw new Error("The local Salesprompter Chrome extension session cookie is not valid for Sales Navigator. Reconnect the extension, then retry the CLI command.");
6771
+ }
6772
+ if (process.env.SALESPROMPTER_CLI_MANAGE_LINKEDIN_SESSIONS === "1") {
6773
+ throw new Error("No validated LinkedIn Sales Navigator session cookie is available from the CLI-managed session pool.");
6774
+ }
6775
+ return null;
6776
+ }
6596
6777
  async function ensureSalesNavigatorSessionPoolReady(queryUrl, options) {
6597
6778
  try {
6598
6779
  await options.logger?.log("salesnav.session_pool.preflight.started", {
6599
6780
  source: options.source,
6600
6781
  queryUrl
6601
6782
  });
6602
- const claimed = await claimValidatedSalesNavigatorSessionCookieForCli({
6603
- queryUrl,
6604
- source: options.source,
6605
- env: process.env
6606
- });
6783
+ const claimed = await resolveSalesNavigatorExportSessionOverride(queryUrl, options);
6607
6784
  await options.logger?.log("salesnav.session_pool.preflight.completed", {
6608
6785
  source: options.source,
6609
6786
  queryUrl,
6610
6787
  status: claimed ? "ok" : "skipped",
6611
- selectedSessionUserEmail: claimed?.userEmail ?? null,
6612
- selectedSessionUserHandle: claimed?.userHandle ?? null,
6613
- selectedSessionCookieSha256: claimed?.sessionCookieSha256 ?? null,
6614
- selectedSessionLastIngestedSource: claimed?.lastIngestedSource ?? null
6788
+ selectedSessionSource: claimed?.source ?? "app_managed",
6789
+ selectedSessionCookieSha256: claimed?.selectedSessionCookieSha256 ?? null
6615
6790
  });
6616
6791
  return {
6617
6792
  ready: true
@@ -9811,6 +9986,7 @@ program
9811
9986
  .option("--mode <mode>", "Phantombuster container mode: all or finalized", "all")
9812
9987
  .option("--before-ended-at <iso>", "Only fetch containers that ended before this ISO timestamp")
9813
9988
  .option("--metadata-only", "Store container metadata without fetching output and result objects", false)
9989
+ .option("--refresh-lead-pool", "After syncing results, rebuild the Neon lead_pool_new reporting table. This can take several minutes.", false)
9814
9990
  .option("--out <path>", "Optional local JSON output path")
9815
9991
  .action(async (options) => {
9816
9992
  const agentIds = z.array(z.string().min(1)).parse(options.agentId);
@@ -9827,7 +10003,8 @@ program
9827
10003
  maxPages,
9828
10004
  mode,
9829
10005
  beforeEndedAt,
9830
- includeResults: !options.metadataOnly
10006
+ includeResults: !options.metadataOnly,
10007
+ refreshLeadPool: Boolean(options.refreshLeadPool)
9831
10008
  });
9832
10009
  const payload = {
9833
10010
  ...result,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
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",