salesprompter-cli 0.1.69 → 0.1.71

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 +191 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -109,9 +109,10 @@ 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
+ 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"
@@ -179,6 +180,7 @@ the app's CLI imports view.
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 repairs stalled 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
@@ -247,6 +247,72 @@ const AffiliateOutreachResponseSchema = z.object({
247
247
  status: z.literal("ok"),
248
248
  outreach: AffiliateOutreachRunSchema
249
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
+ });
301
+ const AffiliateVerificationRepairResponseSchema = AffiliateOutreachResponseSchema.extend({
302
+ repair: z.object({
303
+ checkedAt: DatabaseTimestampSchema,
304
+ beforeCount: z.number().int().nonnegative(),
305
+ afterCount: z.number().int().nonnegative(),
306
+ restoredMissing: z.number().int().nonnegative(),
307
+ requeuedPending: z.number().int().nonnegative(),
308
+ quarantinedPending: z.number().int().nonnegative(),
309
+ prunedInvalid: z.number().int().nonnegative(),
310
+ pendingAfter: z.number().int().nonnegative(),
311
+ invalidAfter: z.number().int().nonnegative(),
312
+ verifiedAfter: z.number().int().nonnegative(),
313
+ dataPreparationAccepted: z.boolean()
314
+ })
315
+ });
250
316
  const AffiliateOutreachSummarySchema = AffiliateOutreachRunBaseSchema;
251
317
  const AffiliateAudienceSummarySchema = z.object({
252
318
  public_token: z.string().min(1),
@@ -659,6 +725,7 @@ const cliPacks = [
659
725
  "affiliate:run",
660
726
  "affiliate:list",
661
727
  "affiliate:status",
728
+ "affiliate:finish",
662
729
  "affiliate:analytics",
663
730
  "affiliate:regenerate-sequence",
664
731
  "affiliate:enrich",
@@ -719,6 +786,7 @@ const helpVisibleCommandNames = new Set([
719
786
  "affiliate:run",
720
787
  "affiliate:list",
721
788
  "affiliate:status",
789
+ "affiliate:finish",
722
790
  "affiliate:analytics",
723
791
  "affiliate:regenerate-sequence",
724
792
  "affiliate:enrich",
@@ -6765,6 +6833,91 @@ async function getAffiliateOutreachViaApp(session, audienceRunId) {
6765
6833
  }), AffiliateOutreachResponseSchema);
6766
6834
  return value.outreach;
6767
6835
  }
6836
+ async function getAffiliateOutreachLiveStatusViaApp(session, audienceRunId) {
6837
+ const { value } = await fetchCliJson(session, (currentSession) => {
6838
+ const url = new URL(`/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}`, currentSession.apiBaseUrl);
6839
+ url.searchParams.set("live", "1");
6840
+ return fetch(url, {
6841
+ headers: {
6842
+ Authorization: `Bearer ${currentSession.accessToken}`
6843
+ }
6844
+ });
6845
+ }, AffiliateOutreachLiveStatusResponseSchema);
6846
+ return value;
6847
+ }
6848
+ async function repairAffiliateOutreachVerificationViaApp(session, audienceRunId, options) {
6849
+ const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}/repair-verification`, {
6850
+ method: "POST",
6851
+ headers: {
6852
+ "Content-Type": "application/json",
6853
+ Authorization: `Bearer ${currentSession.accessToken}`
6854
+ },
6855
+ body: JSON.stringify({
6856
+ ...options,
6857
+ pruneInvalid: true
6858
+ })
6859
+ }), AffiliateVerificationRepairResponseSchema);
6860
+ return value;
6861
+ }
6862
+ async function finishAffiliateOutreachViaApp(session, audienceRunId, options) {
6863
+ let live = await getAffiliateOutreachLiveStatusViaApp(session, audienceRunId);
6864
+ const repairs = [];
6865
+ for (let batch = 0; batch < options.maxRepairBatches; batch += 1) {
6866
+ if (live.outreach.status === "active" || live.health.activationReady) {
6867
+ break;
6868
+ }
6869
+ if (live.outreach.status !== "ready" || !live.health.liveChecked || !live.health.leads) {
6870
+ throw new Error(describeAffiliateOutreachHealth(live.health));
6871
+ }
6872
+ const beforeUnsafe = live.health.leads.unsafe;
6873
+ const needsRepair = beforeUnsafe > 0 || !live.health.dataPreparationComplete;
6874
+ if (!needsRepair) {
6875
+ break;
6876
+ }
6877
+ const repaired = await repairAffiliateOutreachVerificationViaApp(session, audienceRunId, {
6878
+ staleAfterMinutes: options.staleAfterMinutes,
6879
+ maxPendingRequeues: 1_000,
6880
+ quarantinePending: !options.requeuePending,
6881
+ acceptIncompleteRecovery: true
6882
+ });
6883
+ repairs.push(repaired.repair);
6884
+ live = await getAffiliateOutreachLiveStatusViaApp(session, audienceRunId);
6885
+ const afterUnsafe = live.health.leads?.unsafe ?? beforeUnsafe;
6886
+ if (!live.health.activationReady &&
6887
+ afterUnsafe >= beforeUnsafe &&
6888
+ live.health.dataPreparationComplete) {
6889
+ throw new Error(`${describeAffiliateOutreachHealth(live.health)} Pending leads may not be old enough for safe automatic repair yet.`);
6890
+ }
6891
+ }
6892
+ if (live.outreach.status !== "active") {
6893
+ if (!live.health.activationReady) {
6894
+ throw new Error(`${describeAffiliateOutreachHealth(live.health)} The safe activation gate remains closed.`);
6895
+ }
6896
+ await activateAffiliateOutreachViaApp(session, audienceRunId);
6897
+ live = await getAffiliateOutreachLiveStatusViaApp(session, audienceRunId);
6898
+ }
6899
+ if (!live.health.running || !live.health.safeToSend) {
6900
+ throw new Error(`Activation could not be verified safely. ${describeAffiliateOutreachHealth(live.health)}`);
6901
+ }
6902
+ return { live, repairs };
6903
+ }
6904
+ function describeAffiliateOutreachHealth(health) {
6905
+ const leads = health.leads;
6906
+ if (health.done && leads) {
6907
+ return `Done: the campaign is running with ${leads.verified}/${leads.total} verified leads.`;
6908
+ }
6909
+ if (health.verdict === "unsafe_running" && leads) {
6910
+ return `Attention: the campaign is running with ${leads.unsafe} unsafe leads.`;
6911
+ }
6912
+ if (health.verdict === "ready_to_activate" && leads) {
6913
+ return `Ready to activate: ${leads.verified}/${leads.total} leads are verified.`;
6914
+ }
6915
+ if (health.liveChecked && leads) {
6916
+ const campaignState = health.campaign?.statusLabel ?? "unknown";
6917
+ return `Not done: the campaign is ${campaignState}; ${leads.verified}/${leads.total} leads are verified; ${leads.contacted} contacted.`;
6918
+ }
6919
+ return health.blockers[0]?.message ?? "Live campaign status could not be verified.";
6920
+ }
6768
6921
  async function getAffiliateOutreachAnalyticsViaApp(session, audienceRunId) {
6769
6922
  const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}/analytics`, {
6770
6923
  headers: {
@@ -15108,11 +15261,46 @@ program
15108
15261
  });
15109
15262
  program
15110
15263
  .command("affiliate:status <run-id>")
15111
- .description("Show enrichment, sequence, Instantly, and activation status for an affiliate run.")
15264
+ .description("Verify whether an affiliate campaign is prepared, safe, and actually running.")
15112
15265
  .action(async (runId) => {
15113
15266
  const session = await requireAuthSession();
15114
- const outreach = await getAffiliateOutreachViaApp(session, z.string().uuid().parse(runId));
15115
- printOutput({ status: "ok", outreach });
15267
+ const result = await getAffiliateOutreachLiveStatusViaApp(session, z.string().uuid().parse(runId));
15268
+ printOutput({
15269
+ status: "ok",
15270
+ done: result.health.done,
15271
+ verdict: result.health.verdict,
15272
+ message: describeAffiliateOutreachHealth(result.health),
15273
+ health: result.health,
15274
+ outreach: result.outreach
15275
+ });
15276
+ });
15277
+ program
15278
+ .command("affiliate:finish <run-id>")
15279
+ .description("Repair stalled verification, activate safely, and verify the live campaign.")
15280
+ .option("--stale-after-minutes <number>", "Age before a pending verification can be repaired or quarantined", "15")
15281
+ .option("--max-repair-batches <number>", "Maximum 1,000-lead repair batches", "125")
15282
+ .option("--requeue-pending", "Retry stalled verification instead of quarantining it outside the sending campaign", false)
15283
+ .action(async (runId, options) => {
15284
+ const session = await requireAuthSession();
15285
+ const audienceRunId = z.string().uuid().parse(runId);
15286
+ const result = await finishAffiliateOutreachViaApp(session, audienceRunId, {
15287
+ staleAfterMinutes: z.coerce.number().int().min(15).max(30 * 24 * 60).parse(options.staleAfterMinutes),
15288
+ maxRepairBatches: z.coerce.number().int().min(1).max(250).parse(options.maxRepairBatches),
15289
+ requeuePending: Boolean(options.requeuePending)
15290
+ });
15291
+ const leads = result.live.health.leads;
15292
+ printOutput({
15293
+ status: "ok",
15294
+ running: result.live.health.running,
15295
+ safeToSend: result.live.health.safeToSend,
15296
+ done: result.live.health.done,
15297
+ repairs: result.repairs,
15298
+ health: result.live.health,
15299
+ outreach: result.live.outreach,
15300
+ message: result.live.health.done
15301
+ ? describeAffiliateOutreachHealth(result.live.health)
15302
+ : `The campaign is active and safe with ${leads?.verified ?? 0}/${leads?.total ?? 0} verified leads; sending will begin in its configured schedule.`
15303
+ });
15116
15304
  });
15117
15305
  program
15118
15306
  .command("affiliate:analytics <run-id>")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.69",
3
+ "version": "0.1.71",
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",