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.
- package/README.md +3 -1
- package/dist/cli.js +191 -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
|
-
#
|
|
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("
|
|
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
|
|
15115
|
-
printOutput({
|
|
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