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 CHANGED
@@ -109,7 +109,7 @@ salesprompter affiliate:grow \
109
109
  --send-from 09:00 \
110
110
  --send-to 16:00
111
111
 
112
- # Review the persisted sequence and enrichment summary, then activate explicitly
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("Show enrichment, sequence, Instantly, and activation status for an affiliate run.")
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 outreach = await getAffiliateOutreachViaApp(session, z.string().uuid().parse(runId));
15112
- printOutput({ status: "ok", outreach });
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")