salesprompter-cli 0.1.68 → 0.1.70
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 +14 -1
- package/dist/cli.js +126 -4
- package/dist/linkedin-product-details.js +1203 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,7 +109,7 @@ salesprompter affiliate:grow \
|
|
|
109
109
|
--send-from 09:00 \
|
|
110
110
|
--send-to 16:00
|
|
111
111
|
|
|
112
|
-
#
|
|
112
|
+
# Verify live campaign state, lead safety, and contact activity, then activate explicitly
|
|
113
113
|
salesprompter affiliate:list
|
|
114
114
|
salesprompter affiliate:status "$AFFILIATE_RUN_ID"
|
|
115
115
|
salesprompter affiliate:analytics "$AFFILIATE_RUN_ID"
|
|
@@ -139,6 +139,19 @@ salesprompter products:collect \
|
|
|
139
139
|
|
|
140
140
|
Use a signed-in in-app-browser worker with the loopback `GET /task` and `POST /page` protocol. Interrupted, bounded, challenged, or rate-limited runs remain checkpointed and do not create the complete artifact.
|
|
141
141
|
|
|
142
|
+
Enrich that proven order with public product-page details, without changing its ranks or uploading it:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
salesprompter products:collect-details \
|
|
146
|
+
--catalog ./data/products.complete.json \
|
|
147
|
+
--checkpoint ./data/products.details.checkpoint.json \
|
|
148
|
+
--raw-jsonl ./data/products.details.raw.jsonl \
|
|
149
|
+
--out ./data/products.with-details.json \
|
|
150
|
+
--relay-port 43118
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The detail worker uses the same local `GET /task` lease protocol and posts only allowlisted product fields to `POST /detail`. Public URLs must be HTTPS; product images are capped at 2,048 characters, other URLs at 4,096, descriptions at 10,000, other strings at 200, roles/features at 100 entries, and customer companies at 50. The final artifact is created only after every rank has either a complete detail page or explicit unavailable-page proof.
|
|
154
|
+
|
|
142
155
|
Download stored workspace leads without starting a new Sales Navigator scrape:
|
|
143
156
|
|
|
144
157
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ import { buildHistoricalVendorIcp, buildVendorIcp } from "./icp-templates.js";
|
|
|
28
28
|
import { InstantlySyncProvider } from "./instantly.js";
|
|
29
29
|
import { backfillLinkedInCompanies } from "./linkedin-companies.js";
|
|
30
30
|
import { parseLinkedInCompanyPage } from "./linkedin-companies.js";
|
|
31
|
+
import { collectLinkedInProductDetailsViaBrowserRelay } from "./linkedin-product-details.js";
|
|
31
32
|
import { collectLinkedInProductsViaBrowserRelay } from "./linkedin-product-search.js";
|
|
32
33
|
import { crawlLinkedInProductCategory } from "./linkedin-products.js";
|
|
33
34
|
import { claimLinkedInSessionCookieForCli, claimValidatedSalesNavigatorSessionCookieForCli, createLinkedInSessionSupabaseClient, recordLinkedInSessionCookieAudit, resolveConfiguredEnvValue } from "./linkedin-session.js";
|
|
@@ -246,6 +247,57 @@ const AffiliateOutreachResponseSchema = z.object({
|
|
|
246
247
|
status: z.literal("ok"),
|
|
247
248
|
outreach: AffiliateOutreachRunSchema
|
|
248
249
|
});
|
|
250
|
+
const AffiliateOutreachHealthSchema = z.object({
|
|
251
|
+
checkedAt: DatabaseTimestampSchema,
|
|
252
|
+
liveChecked: z.boolean(),
|
|
253
|
+
done: z.boolean(),
|
|
254
|
+
verdict: z.enum([
|
|
255
|
+
"preparing",
|
|
256
|
+
"failed",
|
|
257
|
+
"unavailable",
|
|
258
|
+
"not_running",
|
|
259
|
+
"ready_to_activate",
|
|
260
|
+
"running",
|
|
261
|
+
"unsafe_running"
|
|
262
|
+
]),
|
|
263
|
+
running: z.boolean(),
|
|
264
|
+
activationReady: z.boolean(),
|
|
265
|
+
safeToSend: z.boolean(),
|
|
266
|
+
dataPreparationComplete: z.boolean(),
|
|
267
|
+
campaign: z
|
|
268
|
+
.object({
|
|
269
|
+
id: z.string().min(1),
|
|
270
|
+
name: z.string().min(1).nullable(),
|
|
271
|
+
status: z.number().int().nullable(),
|
|
272
|
+
statusLabel: z.string().min(1),
|
|
273
|
+
dailyLimit: z.number().nonnegative().nullable(),
|
|
274
|
+
sendingAccountCount: z.number().int().nonnegative()
|
|
275
|
+
})
|
|
276
|
+
.nullable(),
|
|
277
|
+
leads: z
|
|
278
|
+
.object({
|
|
279
|
+
total: z.number().int().nonnegative(),
|
|
280
|
+
active: z.number().int().nonnegative(),
|
|
281
|
+
inactive: z.number().int().nonnegative(),
|
|
282
|
+
verified: z.number().int().nonnegative(),
|
|
283
|
+
pendingVerification: z.number().int().nonnegative(),
|
|
284
|
+
invalid: z.number().int().nonnegative(),
|
|
285
|
+
unsafe: z.number().int().nonnegative(),
|
|
286
|
+
contacted: z.number().int().nonnegative(),
|
|
287
|
+
replied: z.number().int().nonnegative(),
|
|
288
|
+
statusCounts: z.record(z.string(), z.number().int().nonnegative()),
|
|
289
|
+
verificationStatusCounts: z.record(z.string(), z.number().int().nonnegative())
|
|
290
|
+
})
|
|
291
|
+
.nullable(),
|
|
292
|
+
blockers: z.array(z.object({
|
|
293
|
+
code: z.string().min(1),
|
|
294
|
+
message: z.string().min(1),
|
|
295
|
+
count: z.number().int().nonnegative().optional()
|
|
296
|
+
}))
|
|
297
|
+
});
|
|
298
|
+
const AffiliateOutreachLiveStatusResponseSchema = AffiliateOutreachResponseSchema.extend({
|
|
299
|
+
health: AffiliateOutreachHealthSchema
|
|
300
|
+
});
|
|
249
301
|
const AffiliateOutreachSummarySchema = AffiliateOutreachRunBaseSchema;
|
|
250
302
|
const AffiliateAudienceSummarySchema = z.object({
|
|
251
303
|
public_token: z.string().min(1),
|
|
@@ -626,7 +678,7 @@ const cliPacks = [
|
|
|
626
678
|
slug: "research",
|
|
627
679
|
title: "Research",
|
|
628
680
|
summary: "Scrape markets and enrich companies before outreach.",
|
|
629
|
-
commands: ["products:collect", "market:scrape", "companies:enrich"],
|
|
681
|
+
commands: ["products:collect", "products:collect-details", "market:scrape", "companies:enrich"],
|
|
630
682
|
installStatus: "included"
|
|
631
683
|
},
|
|
632
684
|
{
|
|
@@ -677,6 +729,7 @@ const helpAliasByCommandName = new Map([
|
|
|
677
729
|
["linkedin-companies:scrape-local", "companies:scrape-linkedin"],
|
|
678
730
|
["dealroom-companies:scrape-local", "companies:scrape-dealroom"],
|
|
679
731
|
["linkedin-products:collect", "products:collect"],
|
|
732
|
+
["linkedin-products:collect-details", "products:collect-details"],
|
|
680
733
|
["linkedin-products:scrape", "market:scrape"],
|
|
681
734
|
["salesnav:from-product-category", "leads:discover"],
|
|
682
735
|
["salesnav:people:collect", "leads:collect"],
|
|
@@ -728,6 +781,7 @@ const helpVisibleCommandNames = new Set([
|
|
|
728
781
|
"linkedin-companies:scrape-local",
|
|
729
782
|
"dealroom-companies:scrape-local",
|
|
730
783
|
"linkedin-products:collect",
|
|
784
|
+
"linkedin-products:collect-details",
|
|
731
785
|
"linkedin-products:scrape",
|
|
732
786
|
"salesnav:from-product-category",
|
|
733
787
|
"salesnav:people:collect",
|
|
@@ -6762,6 +6816,35 @@ async function getAffiliateOutreachViaApp(session, audienceRunId) {
|
|
|
6762
6816
|
}), AffiliateOutreachResponseSchema);
|
|
6763
6817
|
return value.outreach;
|
|
6764
6818
|
}
|
|
6819
|
+
async function getAffiliateOutreachLiveStatusViaApp(session, audienceRunId) {
|
|
6820
|
+
const { value } = await fetchCliJson(session, (currentSession) => {
|
|
6821
|
+
const url = new URL(`/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}`, currentSession.apiBaseUrl);
|
|
6822
|
+
url.searchParams.set("live", "1");
|
|
6823
|
+
return fetch(url, {
|
|
6824
|
+
headers: {
|
|
6825
|
+
Authorization: `Bearer ${currentSession.accessToken}`
|
|
6826
|
+
}
|
|
6827
|
+
});
|
|
6828
|
+
}, AffiliateOutreachLiveStatusResponseSchema);
|
|
6829
|
+
return value;
|
|
6830
|
+
}
|
|
6831
|
+
function describeAffiliateOutreachHealth(health) {
|
|
6832
|
+
const leads = health.leads;
|
|
6833
|
+
if (health.done && leads) {
|
|
6834
|
+
return `Done: the campaign is running with ${leads.verified}/${leads.total} verified leads.`;
|
|
6835
|
+
}
|
|
6836
|
+
if (health.verdict === "unsafe_running" && leads) {
|
|
6837
|
+
return `Attention: the campaign is running with ${leads.unsafe} unsafe leads.`;
|
|
6838
|
+
}
|
|
6839
|
+
if (health.verdict === "ready_to_activate" && leads) {
|
|
6840
|
+
return `Ready to activate: ${leads.verified}/${leads.total} leads are verified.`;
|
|
6841
|
+
}
|
|
6842
|
+
if (health.liveChecked && leads) {
|
|
6843
|
+
const campaignState = health.campaign?.statusLabel ?? "unknown";
|
|
6844
|
+
return `Not done: the campaign is ${campaignState}; ${leads.verified}/${leads.total} leads are verified; ${leads.contacted} contacted.`;
|
|
6845
|
+
}
|
|
6846
|
+
return health.blockers[0]?.message ?? "Live campaign status could not be verified.";
|
|
6847
|
+
}
|
|
6765
6848
|
async function getAffiliateOutreachAnalyticsViaApp(session, audienceRunId) {
|
|
6766
6849
|
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}/analytics`, {
|
|
6767
6850
|
headers: {
|
|
@@ -15105,11 +15188,18 @@ program
|
|
|
15105
15188
|
});
|
|
15106
15189
|
program
|
|
15107
15190
|
.command("affiliate:status <run-id>")
|
|
15108
|
-
.description("
|
|
15191
|
+
.description("Verify whether an affiliate campaign is prepared, safe, and actually running.")
|
|
15109
15192
|
.action(async (runId) => {
|
|
15110
15193
|
const session = await requireAuthSession();
|
|
15111
|
-
const
|
|
15112
|
-
printOutput({
|
|
15194
|
+
const result = await getAffiliateOutreachLiveStatusViaApp(session, z.string().uuid().parse(runId));
|
|
15195
|
+
printOutput({
|
|
15196
|
+
status: "ok",
|
|
15197
|
+
done: result.health.done,
|
|
15198
|
+
verdict: result.health.verdict,
|
|
15199
|
+
message: describeAffiliateOutreachHealth(result.health),
|
|
15200
|
+
health: result.health,
|
|
15201
|
+
outreach: result.outreach
|
|
15202
|
+
});
|
|
15113
15203
|
});
|
|
15114
15204
|
program
|
|
15115
15205
|
.command("affiliate:analytics <run-id>")
|
|
@@ -15349,6 +15439,38 @@ program
|
|
|
15349
15439
|
});
|
|
15350
15440
|
printOutput(result);
|
|
15351
15441
|
});
|
|
15442
|
+
program
|
|
15443
|
+
.command("linkedin-products:collect-details")
|
|
15444
|
+
.alias("products:collect-details")
|
|
15445
|
+
.description("Enrich a proven LinkedIn product ranking locally through a resumable loopback browser relay.")
|
|
15446
|
+
.requiredOption("--catalog <path>", "Complete products:collect ranking artifact path")
|
|
15447
|
+
.requiredOption("--checkpoint <path>", "Private resumable product-detail checkpoint JSON path")
|
|
15448
|
+
.requiredOption("--raw-jsonl <path>", "Private append-only normalized product-detail evidence path")
|
|
15449
|
+
.requiredOption("--out <path>", "Complete public-fields-only enriched artifact path")
|
|
15450
|
+
.requiredOption("--relay-port <number>", "Loopback relay port for GET /task and POST /detail")
|
|
15451
|
+
.option("--max-requests <number>", "Optional number of unique product-detail tasks to issue in this run")
|
|
15452
|
+
.action(async (options) => {
|
|
15453
|
+
const relayPort = z.coerce.number().int().min(1).max(65_535).parse(options.relayPort);
|
|
15454
|
+
const maxRequests = options.maxRequests === undefined
|
|
15455
|
+
? undefined
|
|
15456
|
+
: z.coerce.number().int().min(1).max(100_000).parse(options.maxRequests);
|
|
15457
|
+
const idleTimeoutMs = process.env.SALESPROMPTER_LINKEDIN_PRODUCT_DETAILS_RELAY_IDLE_TIMEOUT_MS
|
|
15458
|
+
? z.coerce.number().int().min(100).max(86_400_000).parse(process.env.SALESPROMPTER_LINKEDIN_PRODUCT_DETAILS_RELAY_IDLE_TIMEOUT_MS)
|
|
15459
|
+
: undefined;
|
|
15460
|
+
const result = await collectLinkedInProductDetailsViaBrowserRelay({
|
|
15461
|
+
catalogPath: path.resolve(String(options.catalog)),
|
|
15462
|
+
checkpointPath: path.resolve(String(options.checkpoint)),
|
|
15463
|
+
rawJsonlPath: path.resolve(String(options.rawJsonl)),
|
|
15464
|
+
outPath: path.resolve(String(options.out)),
|
|
15465
|
+
relayPort,
|
|
15466
|
+
maxRequests,
|
|
15467
|
+
idleTimeoutMs,
|
|
15468
|
+
onListening: ({ taskUrl, detailUrl }) => {
|
|
15469
|
+
writeProgress(`LinkedIn product detail relay ready: GET ${taskUrl} and POST ${detailUrl}`);
|
|
15470
|
+
}
|
|
15471
|
+
});
|
|
15472
|
+
printOutput(result);
|
|
15473
|
+
});
|
|
15352
15474
|
program
|
|
15353
15475
|
.command("linkedin-products:scrape")
|
|
15354
15476
|
.alias("market:scrape")
|