salesprompter-cli 0.1.70 → 0.1.72

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 (3) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +123 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -112,6 +112,7 @@ salesprompter affiliate:grow \
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
+ salesprompter affiliate:finish "$AFFILIATE_RUN_ID"
115
116
  salesprompter affiliate:analytics "$AFFILIATE_RUN_ID"
116
117
  salesprompter affiliate:regenerate-sequence "$AFFILIATE_RUN_ID"
117
118
  salesprompter affiliate:enrich "$AFFILIATE_RUN_ID"
@@ -174,11 +175,12 @@ the app's CLI imports view.
174
175
  - Respect provider terms and customer data boundaries.
175
176
  - `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.
176
177
  - 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.
177
- - Affiliate outreach uses direct email enrichment first, then Phantombuster Email Finder for unresolved people before creating a draft Instantly campaign.
178
+ - Affiliate outreach uses Hunter first, requires Hunter's `valid` verdict before import, and sends recovered addresses through Hunter Email Verifier before creating a draft Instantly campaign. Catch-all, disposable, webmail, invalid, unknown, claimed, and unresolved addresses are excluded. Instantly verification remains enabled as a second gate.
178
179
  - Affiliate preparation uses exactly three emails with three observable variants per step; `affiliate:regenerate-sequence` rebuilds and syncs them through the Salesprompter app.
179
180
  - `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`.
180
181
  - `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.
181
182
  - `affiliate:enrich` starts durable email recovery and adds only newly found addresses to an existing campaign. It returns a background-processing status for large audiences instead of blocking the terminal. Pass `--source-audience-run-id` to merge a newer compatible audience into that campaign without creating a duplicate campaign.
183
+ - `affiliate:finish` closes the loop: it requires persisted Hunter-valid evidence for new campaigns, repairs stalled Instantly verification in 1,000-lead batches, quarantines stale pending or invalid leads outside the sending campaign, accepts an explicitly audited recovery fallback, activates only a fully verified campaign, and reads the live state back. Use `--requeue-pending` only after restoring Instantly verification capacity.
182
184
  - A draft campaign does not send until `affiliate:activate` is run.
183
185
  - The CLI is designed for interactive users and agent-assisted workflows.
184
186
  - A repo-local Chrome extension compatibility copy is available in `chrome-extension/` for local LinkedIn session sync and popup copy/debug flows.
package/dist/cli.js CHANGED
@@ -283,6 +283,8 @@ const AffiliateOutreachHealthSchema = z.object({
283
283
  pendingVerification: z.number().int().nonnegative(),
284
284
  invalid: z.number().int().nonnegative(),
285
285
  unsafe: z.number().int().nonnegative(),
286
+ hunterVerified: z.number().int().nonnegative().default(0),
287
+ hunterEvidenceMissing: z.number().int().nonnegative().default(0),
286
288
  contacted: z.number().int().nonnegative(),
287
289
  replied: z.number().int().nonnegative(),
288
290
  statusCounts: z.record(z.string(), z.number().int().nonnegative()),
@@ -298,6 +300,21 @@ const AffiliateOutreachHealthSchema = z.object({
298
300
  const AffiliateOutreachLiveStatusResponseSchema = AffiliateOutreachResponseSchema.extend({
299
301
  health: AffiliateOutreachHealthSchema
300
302
  });
303
+ const AffiliateVerificationRepairResponseSchema = AffiliateOutreachResponseSchema.extend({
304
+ repair: z.object({
305
+ checkedAt: DatabaseTimestampSchema,
306
+ beforeCount: z.number().int().nonnegative(),
307
+ afterCount: z.number().int().nonnegative(),
308
+ restoredMissing: z.number().int().nonnegative(),
309
+ requeuedPending: z.number().int().nonnegative(),
310
+ quarantinedPending: z.number().int().nonnegative(),
311
+ prunedInvalid: z.number().int().nonnegative(),
312
+ pendingAfter: z.number().int().nonnegative(),
313
+ invalidAfter: z.number().int().nonnegative(),
314
+ verifiedAfter: z.number().int().nonnegative(),
315
+ dataPreparationAccepted: z.boolean()
316
+ })
317
+ });
301
318
  const AffiliateOutreachSummarySchema = AffiliateOutreachRunBaseSchema;
302
319
  const AffiliateAudienceSummarySchema = z.object({
303
320
  public_token: z.string().min(1),
@@ -710,6 +727,7 @@ const cliPacks = [
710
727
  "affiliate:run",
711
728
  "affiliate:list",
712
729
  "affiliate:status",
730
+ "affiliate:finish",
713
731
  "affiliate:analytics",
714
732
  "affiliate:regenerate-sequence",
715
733
  "affiliate:enrich",
@@ -770,6 +788,7 @@ const helpVisibleCommandNames = new Set([
770
788
  "affiliate:run",
771
789
  "affiliate:list",
772
790
  "affiliate:status",
791
+ "affiliate:finish",
773
792
  "affiliate:analytics",
774
793
  "affiliate:regenerate-sequence",
775
794
  "affiliate:enrich",
@@ -6828,20 +6847,81 @@ async function getAffiliateOutreachLiveStatusViaApp(session, audienceRunId) {
6828
6847
  }, AffiliateOutreachLiveStatusResponseSchema);
6829
6848
  return value;
6830
6849
  }
6850
+ async function repairAffiliateOutreachVerificationViaApp(session, audienceRunId, options) {
6851
+ const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}/repair-verification`, {
6852
+ method: "POST",
6853
+ headers: {
6854
+ "Content-Type": "application/json",
6855
+ Authorization: `Bearer ${currentSession.accessToken}`
6856
+ },
6857
+ body: JSON.stringify({
6858
+ ...options,
6859
+ pruneInvalid: true
6860
+ })
6861
+ }), AffiliateVerificationRepairResponseSchema);
6862
+ return value;
6863
+ }
6864
+ async function finishAffiliateOutreachViaApp(session, audienceRunId, options) {
6865
+ let live = await getAffiliateOutreachLiveStatusViaApp(session, audienceRunId);
6866
+ const repairs = [];
6867
+ for (let batch = 0; batch < options.maxRepairBatches; batch += 1) {
6868
+ if (live.outreach.status === "active" || live.health.activationReady) {
6869
+ break;
6870
+ }
6871
+ if (live.outreach.status !== "ready" || !live.health.liveChecked || !live.health.leads) {
6872
+ throw new Error(describeAffiliateOutreachHealth(live.health));
6873
+ }
6874
+ const beforeUnsafe = live.health.leads.unsafe;
6875
+ const needsRepair = beforeUnsafe > 0 || !live.health.dataPreparationComplete;
6876
+ if (!needsRepair) {
6877
+ break;
6878
+ }
6879
+ const repaired = await repairAffiliateOutreachVerificationViaApp(session, audienceRunId, {
6880
+ staleAfterMinutes: options.staleAfterMinutes,
6881
+ maxPendingRequeues: 1_000,
6882
+ quarantinePending: !options.requeuePending,
6883
+ acceptIncompleteRecovery: true
6884
+ });
6885
+ repairs.push(repaired.repair);
6886
+ live = await getAffiliateOutreachLiveStatusViaApp(session, audienceRunId);
6887
+ const afterUnsafe = live.health.leads?.unsafe ?? beforeUnsafe;
6888
+ if (!live.health.activationReady &&
6889
+ afterUnsafe >= beforeUnsafe &&
6890
+ live.health.dataPreparationComplete) {
6891
+ throw new Error(`${describeAffiliateOutreachHealth(live.health)} Pending leads may not be old enough for safe automatic repair yet.`);
6892
+ }
6893
+ }
6894
+ if (live.outreach.status !== "active") {
6895
+ if (!live.health.activationReady) {
6896
+ throw new Error(`${describeAffiliateOutreachHealth(live.health)} The safe activation gate remains closed.`);
6897
+ }
6898
+ await activateAffiliateOutreachViaApp(session, audienceRunId);
6899
+ live = await getAffiliateOutreachLiveStatusViaApp(session, audienceRunId);
6900
+ }
6901
+ if (!live.health.running || !live.health.safeToSend) {
6902
+ throw new Error(`Activation could not be verified safely. ${describeAffiliateOutreachHealth(live.health)}`);
6903
+ }
6904
+ return { live, repairs };
6905
+ }
6831
6906
  function describeAffiliateOutreachHealth(health) {
6832
6907
  const leads = health.leads;
6908
+ const hunterSummary = leads
6909
+ ? leads.hunterEvidenceMissing > 0
6910
+ ? `Hunter evidence recorded for ${leads.hunterVerified}/${leads.total} leads (${leads.hunterEvidenceMissing} legacy or missing)`
6911
+ : `${leads.hunterVerified}/${leads.total} Hunter-valid`
6912
+ : null;
6833
6913
  if (health.done && leads) {
6834
- return `Done: the campaign is running with ${leads.verified}/${leads.total} verified leads.`;
6914
+ return `Done: the campaign is running with ${leads.verified}/${leads.total} Instantly-verified leads; ${hunterSummary}.`;
6835
6915
  }
6836
6916
  if (health.verdict === "unsafe_running" && leads) {
6837
6917
  return `Attention: the campaign is running with ${leads.unsafe} unsafe leads.`;
6838
6918
  }
6839
6919
  if (health.verdict === "ready_to_activate" && leads) {
6840
- return `Ready to activate: ${leads.verified}/${leads.total} leads are verified.`;
6920
+ return `Ready to activate: ${leads.verified}/${leads.total} Instantly-verified; ${hunterSummary}.`;
6841
6921
  }
6842
6922
  if (health.liveChecked && leads) {
6843
6923
  const campaignState = health.campaign?.statusLabel ?? "unknown";
6844
- return `Not done: the campaign is ${campaignState}; ${leads.verified}/${leads.total} leads are verified; ${leads.contacted} contacted.`;
6924
+ return `Not done: the campaign is ${campaignState}; ${leads.verified}/${leads.total} Instantly-verified; ${hunterSummary}; ${leads.contacted} contacted.`;
6845
6925
  }
6846
6926
  return health.blockers[0]?.message ?? "Live campaign status could not be verified.";
6847
6927
  }
@@ -13912,6 +13992,9 @@ function parseAffiliateOutreachCommandOptions(options) {
13912
13992
  if (!parsedTimingMode.success) {
13913
13993
  throw new Error("--timing-mode must be either auto or custom.");
13914
13994
  }
13995
+ if (options.allowAcceptAll) {
13996
+ throw new Error("--allow-accept-all is no longer supported. Affiliate outreach requires Hunter status valid.");
13997
+ }
13915
13998
  const trimmedCampaignName = options.campaignName?.trim();
13916
13999
  const campaignName = trimmedCampaignName
13917
14000
  ? z.string().max(120, "--campaign-name must be 120 characters or fewer.").parse(trimmedCampaignName)
@@ -13921,7 +14004,7 @@ function parseAffiliateOutreachCommandOptions(options) {
13921
14004
  language: z.string().trim().min(1).max(40).parse(options.language),
13922
14005
  numberOfSteps: 3,
13923
14006
  minEmailScore: z.coerce.number().int().min(0).max(100).parse(options.minEmailScore),
13924
- allowAcceptAll: Boolean(options.allowAcceptAll),
14007
+ allowAcceptAll: false,
13925
14008
  timingMode: parsedTimingMode.data,
13926
14009
  dailyLimit: z.coerce.number().int().min(1).max(1000).parse(options.dailyLimit),
13927
14010
  timezone: z.string().trim().min(1).max(100).parse(options.timezone),
@@ -15096,7 +15179,7 @@ addAffiliateAudienceOptions(program
15096
15179
  .option("--language <language>", "Sequence language", "English")
15097
15180
  .option("--steps <number>", "Exactly 3 sequence emails (fixed)", "3")
15098
15181
  .option("--min-email-score <number>", "Minimum Hunter confidence score", "80")
15099
- .option("--allow-accept-all", "Include catch-all domains that meet the score threshold", false)
15182
+ .option("--allow-accept-all", "Deprecated: catch-all emails are rejected", false)
15100
15183
  .option("--timing-mode <mode>", "Schedule mode: custom honors the daily limit and sending window; auto derives them from capacity", "custom")
15101
15184
  .option("--daily-limit <number>", "Maximum new leads contacted per day", "25")
15102
15185
  .option("--timezone <timezone>", "Instantly sending timezone", "America/Detroit")
@@ -15170,6 +15253,13 @@ addAffiliateAudienceOptions(program
15170
15253
  ? "complete"
15171
15254
  : "not_reported",
15172
15255
  persistence: "verified",
15256
+ emailVerification: {
15257
+ provider: "hunter",
15258
+ policy: "valid_only",
15259
+ verified: Number(finalOutreach.stats.hunterVerifiedEmails ?? 0),
15260
+ rejected: Number(finalOutreach.stats.hunterRejectedEmails ?? 0),
15261
+ secondaryProvider: "instantly"
15262
+ },
15173
15263
  outreach: finalOutreach.status,
15174
15264
  activation: finalOutreach.status === "active" ? "complete" : "not_requested",
15175
15265
  },
@@ -15201,6 +15291,34 @@ program
15201
15291
  outreach: result.outreach
15202
15292
  });
15203
15293
  });
15294
+ program
15295
+ .command("affiliate:finish <run-id>")
15296
+ .description("Repair stalled verification, activate safely, and verify the live campaign.")
15297
+ .option("--stale-after-minutes <number>", "Age before a pending verification can be repaired or quarantined", "15")
15298
+ .option("--max-repair-batches <number>", "Maximum 1,000-lead repair batches", "125")
15299
+ .option("--requeue-pending", "Retry stalled verification instead of quarantining it outside the sending campaign", false)
15300
+ .action(async (runId, options) => {
15301
+ const session = await requireAuthSession();
15302
+ const audienceRunId = z.string().uuid().parse(runId);
15303
+ const result = await finishAffiliateOutreachViaApp(session, audienceRunId, {
15304
+ staleAfterMinutes: z.coerce.number().int().min(15).max(30 * 24 * 60).parse(options.staleAfterMinutes),
15305
+ maxRepairBatches: z.coerce.number().int().min(1).max(250).parse(options.maxRepairBatches),
15306
+ requeuePending: Boolean(options.requeuePending)
15307
+ });
15308
+ const leads = result.live.health.leads;
15309
+ printOutput({
15310
+ status: "ok",
15311
+ running: result.live.health.running,
15312
+ safeToSend: result.live.health.safeToSend,
15313
+ done: result.live.health.done,
15314
+ repairs: result.repairs,
15315
+ health: result.live.health,
15316
+ outreach: result.live.outreach,
15317
+ message: result.live.health.done
15318
+ ? describeAffiliateOutreachHealth(result.live.health)
15319
+ : `The campaign is active and safe with ${leads?.verified ?? 0}/${leads?.total ?? 0} verified leads; sending will begin in its configured schedule.`
15320
+ });
15321
+ });
15204
15322
  program
15205
15323
  .command("affiliate:analytics <run-id>")
15206
15324
  .description("Show safe campaign and step/variant performance metrics for an affiliate run.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
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",