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.
- package/dist/cli.js +194 -17
- 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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
6612
|
-
|
|
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