salesprompter-cli 0.1.67 → 0.1.68
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/README.md +6 -3
- package/dist/cli.js +136 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,11 +97,12 @@ salesprompter affiliate:launch \
|
|
|
97
97
|
--linkedin-url "https://www.linkedin.com/sales/search/people?query=..." \
|
|
98
98
|
--max-results 100
|
|
99
99
|
|
|
100
|
-
# End to end: collect,
|
|
101
|
-
salesprompter affiliate:
|
|
100
|
+
# End to end: collect, exclude customers, deduplicate, verify, enrich, and create a draft
|
|
101
|
+
salesprompter affiliate:grow \
|
|
102
102
|
--affiliate-link "https://example.com/?ref=you" \
|
|
103
103
|
--linkedin-url "https://www.linkedin.com/sales/search/people?query=..." \
|
|
104
104
|
--max-results 100 \
|
|
105
|
+
--seed-profile "https://www.linkedin.com/in/existing-customer" \
|
|
105
106
|
--steps 3 \
|
|
106
107
|
--timing-mode custom \
|
|
107
108
|
--daily-limit 25 \
|
|
@@ -158,7 +159,9 @@ the app's CLI imports view.
|
|
|
158
159
|
|
|
159
160
|
- Use your own authorized data access and workspace credentials.
|
|
160
161
|
- Respect provider terms and customer data boundaries.
|
|
161
|
-
- `affiliate:
|
|
162
|
+
- `affiliate:grow` is an alias for `affiliate:run`. It excludes repeated `--seed-profile` customers, removes profiles already stored in prior workspace affiliate audiences, and verifies the final persisted count before preparing Instantly.
|
|
163
|
+
- If every collected profile is known or excluded, no audience or Instantly campaign is created. A stale Sales Navigator session also fails before persistence with a safe retry instruction.
|
|
164
|
+
- Affiliate outreach uses direct email enrichment first, then Phantombuster Email Finder for unresolved people before creating a draft Instantly campaign.
|
|
162
165
|
- Affiliate preparation uses exactly three emails with three observable variants per step; `affiliate:regenerate-sequence` rebuilds and syncs them through the Salesprompter app.
|
|
163
166
|
- `affiliate:list` shows safe workspace summaries. `affiliate:analytics` shows aggregate and step/variant outcomes without exposing lead data; unsupported per-step meeting and won fields render as `n/a`.
|
|
164
167
|
- `affiliate:run` defaults to `--timing-mode custom`, so its daily limit and weekday sending window are honored. Use `--timing-mode auto` only when capacity-derived scheduling is intended.
|
package/dist/cli.js
CHANGED
|
@@ -144,6 +144,18 @@ const AffiliateCampaignLaunchResponseSchema = z.object({
|
|
|
144
144
|
extraction: z
|
|
145
145
|
.object({
|
|
146
146
|
provider: z.string().min(1),
|
|
147
|
+
runId: z.string().min(1).optional(),
|
|
148
|
+
resultCount: z.number().int().nonnegative().optional(),
|
|
149
|
+
deduplication: z
|
|
150
|
+
.object({
|
|
151
|
+
collectedCount: z.number().int().nonnegative(),
|
|
152
|
+
duplicateCount: z.number().int().nonnegative(),
|
|
153
|
+
withinBatchDuplicateCount: z.number().int().nonnegative(),
|
|
154
|
+
workspaceDuplicateCount: z.number().int().nonnegative(),
|
|
155
|
+
excludedSeedProfileCount: z.number().int().nonnegative(),
|
|
156
|
+
netNewCount: z.number().int().nonnegative(),
|
|
157
|
+
})
|
|
158
|
+
.optional(),
|
|
147
159
|
monitorId: z.string().min(1).optional(),
|
|
148
160
|
agentId: z.string().min(1).optional(),
|
|
149
161
|
containerId: z.string().min(1).optional()
|
|
@@ -161,6 +173,21 @@ const AffiliateCampaignLaunchResponseSchema = z.object({
|
|
|
161
173
|
previewUrl: z.string().nullable().optional(),
|
|
162
174
|
next: z.string().optional()
|
|
163
175
|
});
|
|
176
|
+
const AffiliateAudienceVerificationSchema = z.object({
|
|
177
|
+
status: z.literal("ok"),
|
|
178
|
+
audience: z.object({
|
|
179
|
+
runId: z.string().uuid(),
|
|
180
|
+
status: z.enum(["launching", "running", "finished", "failed"]),
|
|
181
|
+
productDomain: z.string().min(1),
|
|
182
|
+
sourceType: z.string().min(1),
|
|
183
|
+
linkedInUrl: z.string().url(),
|
|
184
|
+
maxResults: z.number().int().positive(),
|
|
185
|
+
resultCount: z.number().int().nonnegative(),
|
|
186
|
+
provider: z.string().min(1).nullable(),
|
|
187
|
+
updatedAt: z.string().min(1),
|
|
188
|
+
}),
|
|
189
|
+
previewUrl: z.string().min(1),
|
|
190
|
+
});
|
|
164
191
|
const AffiliateCampaignLocalResultSchema = z
|
|
165
192
|
.object({
|
|
166
193
|
profileUrl: z.string().min(1),
|
|
@@ -6700,6 +6727,14 @@ async function launchAffiliateCampaignViaApp(session, payload) {
|
|
|
6700
6727
|
}), AffiliateCampaignLaunchResponseSchema);
|
|
6701
6728
|
return value;
|
|
6702
6729
|
}
|
|
6730
|
+
async function verifyAffiliateCampaignViaApp(session, audienceRunId) {
|
|
6731
|
+
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-campaigns/${encodeURIComponent(audienceRunId)}`, {
|
|
6732
|
+
headers: {
|
|
6733
|
+
Authorization: `Bearer ${currentSession.accessToken}`,
|
|
6734
|
+
},
|
|
6735
|
+
}), AffiliateAudienceVerificationSchema);
|
|
6736
|
+
return value;
|
|
6737
|
+
}
|
|
6703
6738
|
async function prepareAffiliateOutreachViaApp(session, payload) {
|
|
6704
6739
|
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach`, {
|
|
6705
6740
|
method: "POST",
|
|
@@ -7935,7 +7970,7 @@ async function fetchLocalSalesNavigatorRequest(parsedRequest, retryOptions) {
|
|
|
7935
7970
|
retryDelayMs += await waitWithFullJitter(retryOptions.retryBaseDelayMs, retryOptions.retryMaxDelayMs, attempt);
|
|
7936
7971
|
continue;
|
|
7937
7972
|
}
|
|
7938
|
-
throw new
|
|
7973
|
+
throw new LocalSalesNavigatorHttpError(response.status, text, `Sales Navigator request failed with HTTP ${response.status}: ${preview}`);
|
|
7939
7974
|
}
|
|
7940
7975
|
try {
|
|
7941
7976
|
return { body: JSON.parse(text), retryCount, retryDelayMs };
|
|
@@ -7945,7 +7980,8 @@ async function fetchLocalSalesNavigatorRequest(parsedRequest, retryOptions) {
|
|
|
7945
7980
|
}
|
|
7946
7981
|
}
|
|
7947
7982
|
catch (error) {
|
|
7948
|
-
if (error instanceof LocalSalesNavigatorRateLimitError
|
|
7983
|
+
if (error instanceof LocalSalesNavigatorRateLimitError ||
|
|
7984
|
+
error instanceof LocalSalesNavigatorHttpError) {
|
|
7949
7985
|
throw error;
|
|
7950
7986
|
}
|
|
7951
7987
|
if (attempt < retryOptions.maxRetries) {
|
|
@@ -14776,6 +14812,10 @@ async function runAffiliateLaunchCommand(options) {
|
|
|
14776
14812
|
.max(accessibleResultLimit)
|
|
14777
14813
|
.parse(options.maxResults);
|
|
14778
14814
|
const session = await requireAuthSession();
|
|
14815
|
+
const excludedProfileUrls = z
|
|
14816
|
+
.array(z.string().url())
|
|
14817
|
+
.max(100)
|
|
14818
|
+
.parse(options.seedProfile ?? []);
|
|
14779
14819
|
const isSalesNavigatorPeopleSearch = new URL(linkedInUrl).pathname.includes("/sales/search/people");
|
|
14780
14820
|
let localResults;
|
|
14781
14821
|
let localCollection;
|
|
@@ -14789,7 +14829,7 @@ async function runAffiliateLaunchCommand(options) {
|
|
|
14789
14829
|
if (options.curlFile && browserRelayPort != null) {
|
|
14790
14830
|
throw new Error("Use either --curl-file or --browser-relay-port, not both.");
|
|
14791
14831
|
}
|
|
14792
|
-
|
|
14832
|
+
let parsedRequest = options.curlFile
|
|
14793
14833
|
? parseSalesNavigatorCurlRequest(await readFile(path.resolve(String(options.curlFile)), "utf8"))
|
|
14794
14834
|
: browserRelayPort != null
|
|
14795
14835
|
? {
|
|
@@ -14805,7 +14845,7 @@ async function runAffiliateLaunchCommand(options) {
|
|
|
14805
14845
|
: await createLocalAccountSearchBrowserRelay(browserRelayPort);
|
|
14806
14846
|
let collected;
|
|
14807
14847
|
try {
|
|
14808
|
-
|
|
14848
|
+
const collect = async () => await fetchAllLocalSalesNavigatorPeople(parsedRequest, {
|
|
14809
14849
|
requestedProfiles: maxResults,
|
|
14810
14850
|
pageSize,
|
|
14811
14851
|
pageDelayMinMs,
|
|
@@ -14819,6 +14859,37 @@ async function runAffiliateLaunchCommand(options) {
|
|
|
14819
14859
|
? (request) => browserRelay.request(request)
|
|
14820
14860
|
: undefined,
|
|
14821
14861
|
});
|
|
14862
|
+
try {
|
|
14863
|
+
collected = await collect();
|
|
14864
|
+
}
|
|
14865
|
+
catch (error) {
|
|
14866
|
+
const rejectedSession = error instanceof LocalSalesNavigatorHttpError &&
|
|
14867
|
+
(error.status === 401 || error.status === 403);
|
|
14868
|
+
if (!rejectedSession ||
|
|
14869
|
+
options.curlFile ||
|
|
14870
|
+
browserRelay ||
|
|
14871
|
+
shouldDisableLinkedInDirectLookupAutodiscovery()) {
|
|
14872
|
+
throw error;
|
|
14873
|
+
}
|
|
14874
|
+
const extensionConfig = await readLocalLinkedInExtensionDirectLookupConfig();
|
|
14875
|
+
if (!extensionConfig)
|
|
14876
|
+
throw error;
|
|
14877
|
+
const extensionRequest = buildSalesNavigatorApiRequestFromSearchUrl(linkedInUrl, extensionConfig, pageSize);
|
|
14878
|
+
if (extensionRequest.headers.cookie === parsedRequest.headers.cookie &&
|
|
14879
|
+
extensionRequest.headers["csrf-token"] === parsedRequest.headers["csrf-token"]) {
|
|
14880
|
+
throw error;
|
|
14881
|
+
}
|
|
14882
|
+
writeProgress("Stored Sales Navigator session was rejected; retrying with the latest extension-synced session.");
|
|
14883
|
+
parsedRequest = extensionRequest;
|
|
14884
|
+
collected = await collect();
|
|
14885
|
+
}
|
|
14886
|
+
}
|
|
14887
|
+
catch (error) {
|
|
14888
|
+
if (error instanceof LocalSalesNavigatorHttpError &&
|
|
14889
|
+
(error.status === 401 || error.status === 403)) {
|
|
14890
|
+
throw new Error("Sales Navigator rejected the available session. No audience was persisted. Refresh the Sales Navigator search in the signed-in browser, then rerun this command with a current --curl-file or --browser-relay-port. Workspace deduplication makes the retry safe.");
|
|
14891
|
+
}
|
|
14892
|
+
throw error;
|
|
14822
14893
|
}
|
|
14823
14894
|
finally {
|
|
14824
14895
|
await browserRelay?.close();
|
|
@@ -14837,11 +14908,26 @@ async function runAffiliateLaunchCommand(options) {
|
|
|
14837
14908
|
dryRun: Boolean(options.dryRun),
|
|
14838
14909
|
localResults,
|
|
14839
14910
|
localCollection,
|
|
14911
|
+
...(excludedProfileUrls.length > 0 ? { excludedProfileUrls } : {}),
|
|
14840
14912
|
});
|
|
14913
|
+
const runId = payload.extraction?.runId;
|
|
14914
|
+
const verification = !options.dryRun && runId && z.string().uuid().safeParse(runId).success
|
|
14915
|
+
? await verifyAffiliateCampaignViaApp(session, runId)
|
|
14916
|
+
: null;
|
|
14917
|
+
if (verification) {
|
|
14918
|
+
const expectedResultCount = payload.extraction?.resultCount;
|
|
14919
|
+
if (verification.audience.status !== "finished") {
|
|
14920
|
+
throw new Error(`Affiliate audience ${runId} was persisted but canonical status is ${verification.audience.status}, not finished.`);
|
|
14921
|
+
}
|
|
14922
|
+
if (expectedResultCount != null &&
|
|
14923
|
+
verification.audience.resultCount !== expectedResultCount) {
|
|
14924
|
+
throw new Error(`Affiliate audience ${runId} readback mismatch: created ${expectedResultCount}, stored ${verification.audience.resultCount}.`);
|
|
14925
|
+
}
|
|
14926
|
+
}
|
|
14841
14927
|
if (options.out) {
|
|
14842
|
-
await writeJsonFile(options.out, payload);
|
|
14928
|
+
await writeJsonFile(options.out, { ...payload, verification });
|
|
14843
14929
|
}
|
|
14844
|
-
return { session, payload };
|
|
14930
|
+
return { session, payload, verification };
|
|
14845
14931
|
}
|
|
14846
14932
|
function addAffiliateAudienceOptions(command) {
|
|
14847
14933
|
return command
|
|
@@ -14850,6 +14936,7 @@ function addAffiliateAudienceOptions(command) {
|
|
|
14850
14936
|
.option("--max-results <number>", "Maximum audience results (people searches: 2500; Connections of: 1000)")
|
|
14851
14937
|
.option("--curl-file <path>", "Optional copied Sales Navigator Lead Search curl request")
|
|
14852
14938
|
.option("--browser-relay-port <number>", "Use the signed-in browser through a loopback relay")
|
|
14939
|
+
.option("--seed-profile <url>", "Exclude a known customer or seed profile; repeat for multiple profiles", collectStringOptionValue, [])
|
|
14853
14940
|
.option("--page-size <number>", "Direct Sales Navigator page size", "100")
|
|
14854
14941
|
.option("--page-delay-min-ms <number>", "Minimum delay between direct pages", "5000")
|
|
14855
14942
|
.option("--page-delay-max-ms <number>", "Maximum delay between direct pages", "8000")
|
|
@@ -14915,12 +15002,13 @@ addAffiliateAudienceOptions(program
|
|
|
14915
15002
|
.command("affiliate:launch")
|
|
14916
15003
|
.description("Build an affiliate audience from an affiliate link and LinkedIn search URL."))
|
|
14917
15004
|
.action(async (options) => {
|
|
14918
|
-
const { payload } = await runAffiliateLaunchCommand(options);
|
|
14919
|
-
printOutput(payload);
|
|
15005
|
+
const { payload, verification } = await runAffiliateLaunchCommand(options);
|
|
15006
|
+
printOutput({ ...payload, verification });
|
|
14920
15007
|
});
|
|
14921
15008
|
addAffiliateAudienceOptions(program
|
|
14922
15009
|
.command("affiliate:run")
|
|
14923
|
-
.
|
|
15010
|
+
.alias("affiliate:grow")
|
|
15011
|
+
.description("Collect, deduplicate, verify, and prepare a paused Instantly campaign end to end."))
|
|
14924
15012
|
.option("--campaign-name <name>", "Optional Instantly campaign name")
|
|
14925
15013
|
.option("--language <language>", "Sequence language", "English")
|
|
14926
15014
|
.option("--steps <number>", "Exactly 3 sequence emails (fixed)", "3")
|
|
@@ -14938,6 +15026,13 @@ addAffiliateAudienceOptions(program
|
|
|
14938
15026
|
if (options.dryRun) {
|
|
14939
15027
|
printOutput({
|
|
14940
15028
|
...launched.payload,
|
|
15029
|
+
verification: null,
|
|
15030
|
+
journey: {
|
|
15031
|
+
collection: "validated",
|
|
15032
|
+
persistence: "not_requested",
|
|
15033
|
+
outreach: "not_requested",
|
|
15034
|
+
activation: "not_requested",
|
|
15035
|
+
},
|
|
14941
15036
|
outreach: {
|
|
14942
15037
|
status: "dry-run",
|
|
14943
15038
|
provider: "instantly",
|
|
@@ -14952,7 +15047,28 @@ addAffiliateAudienceOptions(program
|
|
|
14952
15047
|
? launched.payload.extraction.runId
|
|
14953
15048
|
: null;
|
|
14954
15049
|
if (!runId) {
|
|
14955
|
-
|
|
15050
|
+
const deduplication = launched.payload.extraction?.deduplication;
|
|
15051
|
+
if (deduplication?.netNewCount === 0) {
|
|
15052
|
+
printOutput({
|
|
15053
|
+
status: "ok",
|
|
15054
|
+
audience: launched.payload,
|
|
15055
|
+
verification: null,
|
|
15056
|
+
journey: {
|
|
15057
|
+
collection: "complete",
|
|
15058
|
+
deduplication: "complete",
|
|
15059
|
+
persistence: "not_needed",
|
|
15060
|
+
outreach: "not_created",
|
|
15061
|
+
activation: "not_requested",
|
|
15062
|
+
},
|
|
15063
|
+
outreach: { status: "not_created", provider: "instantly" },
|
|
15064
|
+
next: "Change the Sales Navigator cohort; every collected profile was already known or excluded.",
|
|
15065
|
+
});
|
|
15066
|
+
return;
|
|
15067
|
+
}
|
|
15068
|
+
throw new Error("The audience provider did not return a durable run id. End-to-end outreach requires a finished Sales Navigator people audience.");
|
|
15069
|
+
}
|
|
15070
|
+
if (!launched.verification) {
|
|
15071
|
+
throw new Error(`Affiliate audience ${runId} was created but could not be independently verified. Outreach was not prepared.`);
|
|
14956
15072
|
}
|
|
14957
15073
|
const outreach = await prepareAffiliateOutreachViaApp(launched.session, {
|
|
14958
15074
|
audienceRunId: z.string().uuid().parse(runId),
|
|
@@ -14964,6 +15080,16 @@ addAffiliateAudienceOptions(program
|
|
|
14964
15080
|
printOutput({
|
|
14965
15081
|
status: "ok",
|
|
14966
15082
|
audience: launched.payload,
|
|
15083
|
+
verification: launched.verification,
|
|
15084
|
+
journey: {
|
|
15085
|
+
collection: "complete",
|
|
15086
|
+
deduplication: launched.payload.extraction?.deduplication
|
|
15087
|
+
? "complete"
|
|
15088
|
+
: "not_reported",
|
|
15089
|
+
persistence: "verified",
|
|
15090
|
+
outreach: finalOutreach.status,
|
|
15091
|
+
activation: finalOutreach.status === "active" ? "complete" : "not_requested",
|
|
15092
|
+
},
|
|
14967
15093
|
outreach: finalOutreach,
|
|
14968
15094
|
next: finalOutreach.status === "ready"
|
|
14969
15095
|
? `Review the sequence, then run: salesprompter affiliate:activate ${runId}`
|
package/package.json
CHANGED