salesprompter-cli 0.1.63 → 0.1.64

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 +11 -2
  2. package/dist/cli.js +196 -44
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -96,10 +96,17 @@ salesprompter affiliate:launch \
96
96
  salesprompter affiliate:run \
97
97
  --affiliate-link "https://example.com/?ref=you" \
98
98
  --linkedin-url "https://www.linkedin.com/sales/search/people?query=..." \
99
- --max-results 100
99
+ --max-results 100 \
100
+ --steps 3 \
101
+ --timing-mode custom \
102
+ --daily-limit 25 \
103
+ --send-from 09:00 \
104
+ --send-to 16:00
100
105
 
101
106
  # Review the persisted sequence and enrichment summary, then activate explicitly
107
+ salesprompter affiliate:list
102
108
  salesprompter affiliate:status "$AFFILIATE_RUN_ID"
109
+ salesprompter affiliate:analytics "$AFFILIATE_RUN_ID"
103
110
  salesprompter affiliate:regenerate-sequence "$AFFILIATE_RUN_ID"
104
111
  salesprompter affiliate:enrich "$AFFILIATE_RUN_ID"
105
112
  salesprompter affiliate:enrich "$AFFILIATE_RUN_ID" \
@@ -134,7 +141,9 @@ the app's CLI imports view.
134
141
  - Use your own authorized data access and workspace credentials.
135
142
  - Respect provider terms and customer data boundaries.
136
143
  - `affiliate:run` uses direct email enrichment first, then Phantombuster Email Finder for unresolved people before creating a draft Instantly campaign.
137
- - Affiliate preparation automatically creates three variants per step with spintax; `affiliate:regenerate-sequence` rebuilds and syncs them through the Salesprompter app.
144
+ - Affiliate preparation uses exactly three emails with three observable variants per step; `affiliate:regenerate-sequence` rebuilds and syncs them through the Salesprompter app.
145
+ - `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`.
146
+ - `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.
138
147
  - `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.
139
148
  - A draft campaign does not send until `affiliate:activate` is run.
140
149
  - The CLI is designed for interactive users and agent-assisted workflows.
package/dist/cli.js CHANGED
@@ -166,7 +166,42 @@ const AffiliateCampaignLocalResultSchema = z
166
166
  })
167
167
  .passthrough();
168
168
  const DatabaseTimestampSchema = z.string().min(1);
169
- const AffiliateOutreachRunSchema = z.object({
169
+ const AffiliateSequenceVariantSchema = z.object({
170
+ subject: z.string(),
171
+ body: z.string()
172
+ });
173
+ const AffiliateHistoricalSequenceStepSchema = z.object({
174
+ step: z.number().int().positive(),
175
+ subject: z.string(),
176
+ body: z.string(),
177
+ rationale: z.string(),
178
+ delayDays: z.number().int().nonnegative(),
179
+ variants: z.array(AffiliateSequenceVariantSchema).min(1).max(3).optional()
180
+ });
181
+ const AffiliateStrictSequenceStepSchema = AffiliateHistoricalSequenceStepSchema.extend({
182
+ variants: z.array(AffiliateSequenceVariantSchema).length(3)
183
+ });
184
+ const AffiliateStrictSequenceSchema = z.tuple([
185
+ AffiliateStrictSequenceStepSchema.extend({ step: z.literal(1) }),
186
+ AffiliateStrictSequenceStepSchema.extend({ step: z.literal(2) }),
187
+ AffiliateStrictSequenceStepSchema.extend({ step: z.literal(3) })
188
+ ]);
189
+ const AffiliateOutreachLeadSchema = z.object({
190
+ rowIndex: z.number().int().nonnegative(),
191
+ fullName: z.string().min(1),
192
+ firstName: z.string().min(1),
193
+ lastName: z.string().min(1),
194
+ jobTitle: z.string().nullable(),
195
+ companyName: z.string().min(1),
196
+ companyDomain: z.string().min(1),
197
+ location: z.string().nullable(),
198
+ profileUrl: z.string().nullable(),
199
+ email: z.string().email(),
200
+ emailScore: z.number().min(0).max(100),
201
+ acceptAll: z.boolean(),
202
+ emailSource: z.enum(["hunter", "phantombuster"]).optional()
203
+ });
204
+ const AffiliateOutreachRunBaseSchema = z.object({
170
205
  id: z.string().uuid(),
171
206
  audience_run_id: z.string().uuid(),
172
207
  organization_id: z.string().min(1),
@@ -174,36 +209,8 @@ const AffiliateOutreachRunSchema = z.object({
174
209
  provider: z.literal("instantly"),
175
210
  instantly_campaign_id: z.string().min(1).nullable(),
176
211
  instantly_campaign_name: z.string().min(1).nullable(),
177
- sequence: z.array(z.object({
178
- step: z.number().int().positive(),
179
- subject: z.string(),
180
- body: z.string(),
181
- rationale: z.string(),
182
- delayDays: z.number().int().nonnegative(),
183
- variants: z
184
- .array(z.object({
185
- subject: z.string(),
186
- body: z.string()
187
- }))
188
- .min(1)
189
- .max(3)
190
- .optional()
191
- })),
192
- enriched_leads: z.array(z.object({
193
- rowIndex: z.number().int().nonnegative(),
194
- fullName: z.string().min(1),
195
- firstName: z.string().min(1),
196
- lastName: z.string().min(1),
197
- jobTitle: z.string().nullable(),
198
- companyName: z.string().min(1),
199
- companyDomain: z.string().min(1),
200
- location: z.string().nullable(),
201
- profileUrl: z.string().nullable(),
202
- email: z.string().email(),
203
- emailScore: z.number().min(0).max(100),
204
- acceptAll: z.boolean(),
205
- emailSource: z.enum(["hunter", "phantombuster"]).optional()
206
- })),
212
+ sequence: z.array(AffiliateHistoricalSequenceStepSchema),
213
+ enriched_leads: z.array(AffiliateOutreachLeadSchema),
207
214
  stats: z.record(z.string(), z.unknown()),
208
215
  settings: z.record(z.string(), z.unknown()),
209
216
  error_message: z.string().nullable(),
@@ -211,10 +218,85 @@ const AffiliateOutreachRunSchema = z.object({
211
218
  created_at: DatabaseTimestampSchema,
212
219
  updated_at: DatabaseTimestampSchema
213
220
  });
221
+ const AffiliateOutreachRunSchema = z.discriminatedUnion("status", [
222
+ AffiliateOutreachRunBaseSchema.extend({ status: z.literal("preparing") }),
223
+ AffiliateOutreachRunBaseSchema.extend({ status: z.literal("failed") }),
224
+ AffiliateOutreachRunBaseSchema.extend({
225
+ status: z.literal("ready"),
226
+ sequence: AffiliateStrictSequenceSchema
227
+ }),
228
+ AffiliateOutreachRunBaseSchema.extend({
229
+ status: z.literal("active"),
230
+ sequence: AffiliateStrictSequenceSchema
231
+ })
232
+ ]);
214
233
  const AffiliateOutreachResponseSchema = z.object({
215
234
  status: z.literal("ok"),
216
235
  outreach: AffiliateOutreachRunSchema
217
236
  });
237
+ const AffiliateOutreachSummarySchema = AffiliateOutreachRunBaseSchema.omit({
238
+ enriched_leads: true
239
+ });
240
+ const AffiliateAudienceSummarySchema = z.object({
241
+ public_token: z.string().min(1),
242
+ status: z.enum(["launching", "running", "finished", "failed"]),
243
+ affiliate_link: z.string().url(),
244
+ product_domain: z.string().min(1),
245
+ source_type: z.string().min(1),
246
+ linkedin_url: z.string().url(),
247
+ max_results: z.number().int().positive(),
248
+ error_message: z.string().nullable(),
249
+ organization_id: z.string().nullable(),
250
+ created_at: DatabaseTimestampSchema,
251
+ updated_at: DatabaseTimestampSchema
252
+ });
253
+ const AffiliateOutreachListResponseSchema = z.object({
254
+ status: z.literal("ok"),
255
+ audiences: z.array(AffiliateAudienceSummarySchema),
256
+ outreach: z.array(AffiliateOutreachSummarySchema)
257
+ });
258
+ const AffiliateAnalyticsAggregateSchema = z.object({
259
+ leadsCount: z.number().int().nonnegative(),
260
+ contactedCount: z.number().int().nonnegative(),
261
+ emailsSentCount: z.number().int().nonnegative(),
262
+ replyCount: z.number().int().nonnegative(),
263
+ automaticReplyCount: z.number().int().nonnegative(),
264
+ humanReplyCount: z.number().int().nonnegative(),
265
+ humanReplyRate: z.number().nonnegative(),
266
+ bouncedCount: z.number().int().nonnegative(),
267
+ unsubscribedCount: z.number().int().nonnegative(),
268
+ opportunityCount: z.number().int().nonnegative(),
269
+ meetingCount: z.number().int().nonnegative(),
270
+ meetingCompletedCount: z.number().int().nonnegative(),
271
+ wonCount: z.number().int().nonnegative()
272
+ });
273
+ const AffiliateAnalyticsStepSchema = z.object({
274
+ step: z.number().int().positive(),
275
+ variant: z.number().int().nonnegative(),
276
+ variantLabel: z.string().trim().min(1).max(20),
277
+ sentCount: z.number().int().nonnegative(),
278
+ replyCount: z.number().int().nonnegative(),
279
+ automaticReplyCount: z.number().int().nonnegative(),
280
+ humanReplyCount: z.number().int().nonnegative(),
281
+ humanReplyRate: z.number().nonnegative(),
282
+ opportunityCount: z.number().int().nonnegative(),
283
+ meetingCount: z.number().int().nonnegative().nullable(),
284
+ wonCount: z.number().int().nonnegative().nullable(),
285
+ learning: z.object({
286
+ ready: z.boolean(),
287
+ reason: z.string().min(1)
288
+ })
289
+ });
290
+ const AffiliateOutreachAnalyticsResponseSchema = z.object({
291
+ status: z.literal("ok"),
292
+ runId: z.string().uuid(),
293
+ runStatus: z.string().min(1),
294
+ campaignId: z.string().min(1),
295
+ campaignName: z.string().min(1).nullable(),
296
+ campaignStatus: z.number().nullable(),
297
+ aggregate: AffiliateAnalyticsAggregateSchema,
298
+ steps: z.array(AffiliateAnalyticsStepSchema)
299
+ });
218
300
  const AffiliateOutreachEnrichStartResponseSchema = z.object({
219
301
  status: z.literal("accepted"),
220
302
  audienceRunId: z.string().uuid(),
@@ -547,7 +629,10 @@ const cliPacks = [
547
629
  summary: "Prepare and sync qualified leads into downstream systems.",
548
630
  commands: [
549
631
  "affiliate:run",
632
+ "affiliate:list",
550
633
  "affiliate:status",
634
+ "affiliate:analytics",
635
+ "affiliate:regenerate-sequence",
551
636
  "affiliate:enrich",
552
637
  "affiliate:activate",
553
638
  "affiliate:launch",
@@ -602,7 +687,10 @@ const helpVisibleCommandNames = new Set([
602
687
  "leads:score",
603
688
  "leads:pipeline",
604
689
  "affiliate:run",
690
+ "affiliate:list",
605
691
  "affiliate:status",
692
+ "affiliate:analytics",
693
+ "affiliate:regenerate-sequence",
606
694
  "affiliate:enrich",
607
695
  "affiliate:activate",
608
696
  "affiliate:launch",
@@ -6621,6 +6709,14 @@ async function prepareAffiliateOutreachViaApp(session, payload) {
6621
6709
  }), AffiliateOutreachResponseSchema);
6622
6710
  return value.outreach;
6623
6711
  }
6712
+ async function listAffiliateOutreachViaApp(session) {
6713
+ const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach`, {
6714
+ headers: {
6715
+ Authorization: `Bearer ${currentSession.accessToken}`
6716
+ }
6717
+ }), AffiliateOutreachListResponseSchema);
6718
+ return value;
6719
+ }
6624
6720
  async function getAffiliateOutreachViaApp(session, audienceRunId) {
6625
6721
  const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}`, {
6626
6722
  headers: {
@@ -6629,6 +6725,14 @@ async function getAffiliateOutreachViaApp(session, audienceRunId) {
6629
6725
  }), AffiliateOutreachResponseSchema);
6630
6726
  return value.outreach;
6631
6727
  }
6728
+ async function getAffiliateOutreachAnalyticsViaApp(session, audienceRunId) {
6729
+ const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}/analytics`, {
6730
+ headers: {
6731
+ Authorization: `Bearer ${currentSession.accessToken}`
6732
+ }
6733
+ }), AffiliateOutreachAnalyticsResponseSchema);
6734
+ return value;
6735
+ }
6632
6736
  async function activateAffiliateOutreachViaApp(session, audienceRunId) {
6633
6737
  const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}/activate`, {
6634
6738
  method: "POST",
@@ -13661,6 +13765,44 @@ program
13661
13765
  }
13662
13766
  });
13663
13767
  });
13768
+ function parseAffiliateOutreachCommandOptions(options) {
13769
+ const parsedSteps = z.coerce.number().int().safeParse(options.steps);
13770
+ if (!parsedSteps.success || parsedSteps.data !== 3) {
13771
+ throw new Error("Affiliate outreach currently requires exactly 3 sequence emails. Use --steps 3.");
13772
+ }
13773
+ const parsedTimingMode = z.enum(["auto", "custom"]).safeParse(options.timingMode);
13774
+ if (!parsedTimingMode.success) {
13775
+ throw new Error("--timing-mode must be either auto or custom.");
13776
+ }
13777
+ const trimmedCampaignName = options.campaignName?.trim();
13778
+ const campaignName = trimmedCampaignName
13779
+ ? z.string().max(120, "--campaign-name must be 120 characters or fewer.").parse(trimmedCampaignName)
13780
+ : undefined;
13781
+ return {
13782
+ campaignName,
13783
+ language: z.string().trim().min(1).max(40).parse(options.language),
13784
+ numberOfSteps: 3,
13785
+ minEmailScore: z.coerce.number().int().min(0).max(100).parse(options.minEmailScore),
13786
+ allowAcceptAll: Boolean(options.allowAcceptAll),
13787
+ timingMode: parsedTimingMode.data,
13788
+ dailyLimit: z.coerce.number().int().min(1).max(1000).parse(options.dailyLimit),
13789
+ timezone: z.string().trim().min(1).max(100).parse(options.timezone),
13790
+ sendFrom: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).parse(options.sendFrom),
13791
+ sendTo: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).parse(options.sendTo)
13792
+ };
13793
+ }
13794
+ function formatAffiliateAnalyticsForOutput(analytics) {
13795
+ if (runtimeOutputOptions.json)
13796
+ return analytics;
13797
+ return {
13798
+ ...analytics,
13799
+ steps: analytics.steps.map((step) => ({
13800
+ ...step,
13801
+ meetingCount: step.meetingCount ?? "n/a",
13802
+ wonCount: step.wonCount ?? "n/a"
13803
+ }))
13804
+ };
13805
+ }
13664
13806
  function getLocalSalesNavigatorPeopleCheckpointPath(sourceQueryUrl, maxResultsPerSearch, pageSize) {
13665
13807
  const key = createHash("sha256")
13666
13808
  .update(`${sourceQueryUrl}\n${maxResultsPerSearch}\n${pageSize}`)
@@ -14741,15 +14883,17 @@ addAffiliateAudienceOptions(program
14741
14883
  .description("Build an affiliate audience and prepare a reviewed Instantly campaign end to end."))
14742
14884
  .option("--campaign-name <name>", "Optional Instantly campaign name")
14743
14885
  .option("--language <language>", "Sequence language", "English")
14744
- .option("--steps <number>", "Number of sequence emails", "3")
14886
+ .option("--steps <number>", "Exactly 3 sequence emails (fixed)", "3")
14745
14887
  .option("--min-email-score <number>", "Minimum Hunter confidence score", "80")
14746
14888
  .option("--allow-accept-all", "Include catch-all domains that meet the score threshold", false)
14889
+ .option("--timing-mode <mode>", "Schedule mode: custom honors the daily limit and sending window; auto derives them from capacity", "custom")
14747
14890
  .option("--daily-limit <number>", "Maximum new leads contacted per day", "25")
14748
14891
  .option("--timezone <timezone>", "Instantly sending timezone", "America/Detroit")
14749
14892
  .option("--send-from <time>", "Weekday sending window start (HH:MM)", "09:00")
14750
14893
  .option("--send-to <time>", "Weekday sending window end (HH:MM)", "16:00")
14751
14894
  .option("--activate", "Activate the prepared Instantly campaign after review data is returned", false)
14752
14895
  .action(async (options) => {
14896
+ const outreachOptions = parseAffiliateOutreachCommandOptions(options);
14753
14897
  const launched = await runAffiliateLaunchCommand(options);
14754
14898
  if (options.dryRun) {
14755
14899
  printOutput({
@@ -14757,7 +14901,8 @@ addAffiliateAudienceOptions(program
14757
14901
  outreach: {
14758
14902
  status: "dry-run",
14759
14903
  provider: "instantly",
14760
- activation: "not_requested"
14904
+ activation: "not_requested",
14905
+ settings: outreachOptions
14761
14906
  }
14762
14907
  });
14763
14908
  return;
@@ -14771,15 +14916,7 @@ addAffiliateAudienceOptions(program
14771
14916
  }
14772
14917
  const outreach = await prepareAffiliateOutreachViaApp(launched.session, {
14773
14918
  audienceRunId: z.string().uuid().parse(runId),
14774
- campaignName: options.campaignName?.trim() || undefined,
14775
- language: z.string().trim().min(1).max(40).parse(options.language),
14776
- numberOfSteps: z.coerce.number().int().min(1).max(6).parse(options.steps),
14777
- minEmailScore: z.coerce.number().int().min(0).max(100).parse(options.minEmailScore),
14778
- allowAcceptAll: Boolean(options.allowAcceptAll),
14779
- dailyLimit: z.coerce.number().int().min(1).max(1000).parse(options.dailyLimit),
14780
- timezone: z.string().trim().min(1).max(100).parse(options.timezone),
14781
- sendFrom: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).parse(options.sendFrom),
14782
- sendTo: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).parse(options.sendTo)
14919
+ ...outreachOptions
14783
14920
  });
14784
14921
  const finalOutreach = options.activate
14785
14922
  ? await activateAffiliateOutreachViaApp(launched.session, runId)
@@ -14793,6 +14930,13 @@ addAffiliateAudienceOptions(program
14793
14930
  : "The Instantly campaign is active."
14794
14931
  });
14795
14932
  });
14933
+ program
14934
+ .command("affiliate:list")
14935
+ .description("List affiliate audiences and Instantly outreach runs in the active workspace.")
14936
+ .action(async () => {
14937
+ const session = await requireAuthSession();
14938
+ printOutput(await listAffiliateOutreachViaApp(session));
14939
+ });
14796
14940
  program
14797
14941
  .command("affiliate:status <run-id>")
14798
14942
  .description("Show enrichment, sequence, Instantly, and activation status for an affiliate run.")
@@ -14801,16 +14945,24 @@ program
14801
14945
  const outreach = await getAffiliateOutreachViaApp(session, z.string().uuid().parse(runId));
14802
14946
  printOutput({ status: "ok", outreach });
14803
14947
  });
14948
+ program
14949
+ .command("affiliate:analytics <run-id>")
14950
+ .description("Show safe campaign and step/variant performance metrics for an affiliate run.")
14951
+ .action(async (runId) => {
14952
+ const session = await requireAuthSession();
14953
+ const analytics = await getAffiliateOutreachAnalyticsViaApp(session, z.string().uuid().parse(runId));
14954
+ printOutput(formatAffiliateAnalyticsForOutput(analytics));
14955
+ });
14804
14956
  program
14805
14957
  .command("affiliate:regenerate-sequence <run-id>")
14806
- .description("Regenerate sequence variants and spintax, then sync them to Instantly.")
14958
+ .description("Regenerate observable sequence variants, then sync them to Instantly.")
14807
14959
  .action(async (runId) => {
14808
14960
  const session = await requireAuthSession();
14809
14961
  const outreach = await regenerateAffiliateOutreachSequenceViaApp(session, z.string().uuid().parse(runId));
14810
14962
  printOutput({
14811
14963
  status: "ok",
14812
14964
  outreach,
14813
- message: "Sequence variants and spintax were regenerated and synced to Instantly."
14965
+ message: "Observable sequence variants were regenerated and synced to Instantly."
14814
14966
  });
14815
14967
  });
14816
14968
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
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",