salesprompter-cli 0.1.63 → 0.1.65

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 +178 -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,27 @@ 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 AffiliateOutreachRunBaseSchema = z.object({
170
190
  id: z.string().uuid(),
171
191
  audience_run_id: z.string().uuid(),
172
192
  organization_id: z.string().min(1),
@@ -174,36 +194,7 @@ const AffiliateOutreachRunSchema = z.object({
174
194
  provider: z.literal("instantly"),
175
195
  instantly_campaign_id: z.string().min(1).nullable(),
176
196
  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
- })),
197
+ sequence: z.array(AffiliateHistoricalSequenceStepSchema),
207
198
  stats: z.record(z.string(), z.unknown()),
208
199
  settings: z.record(z.string(), z.unknown()),
209
200
  error_message: z.string().nullable(),
@@ -211,10 +202,83 @@ const AffiliateOutreachRunSchema = z.object({
211
202
  created_at: DatabaseTimestampSchema,
212
203
  updated_at: DatabaseTimestampSchema
213
204
  });
205
+ const AffiliateOutreachRunSchema = z.discriminatedUnion("status", [
206
+ AffiliateOutreachRunBaseSchema.extend({ status: z.literal("preparing") }),
207
+ AffiliateOutreachRunBaseSchema.extend({ status: z.literal("failed") }),
208
+ AffiliateOutreachRunBaseSchema.extend({
209
+ status: z.literal("ready"),
210
+ sequence: AffiliateStrictSequenceSchema
211
+ }),
212
+ AffiliateOutreachRunBaseSchema.extend({
213
+ status: z.literal("active"),
214
+ sequence: AffiliateStrictSequenceSchema
215
+ })
216
+ ]);
214
217
  const AffiliateOutreachResponseSchema = z.object({
215
218
  status: z.literal("ok"),
216
219
  outreach: AffiliateOutreachRunSchema
217
220
  });
221
+ const AffiliateOutreachSummarySchema = AffiliateOutreachRunBaseSchema;
222
+ const AffiliateAudienceSummarySchema = z.object({
223
+ public_token: z.string().min(1),
224
+ status: z.enum(["launching", "running", "finished", "failed"]),
225
+ affiliate_link: z.string().url(),
226
+ product_domain: z.string().min(1),
227
+ source_type: z.string().min(1),
228
+ linkedin_url: z.string().url(),
229
+ max_results: z.number().int().positive(),
230
+ error_message: z.string().nullable(),
231
+ organization_id: z.string().nullable(),
232
+ created_at: DatabaseTimestampSchema,
233
+ updated_at: DatabaseTimestampSchema
234
+ });
235
+ const AffiliateOutreachListResponseSchema = z.object({
236
+ status: z.literal("ok"),
237
+ audiences: z.array(AffiliateAudienceSummarySchema),
238
+ outreach: z.array(AffiliateOutreachSummarySchema)
239
+ });
240
+ const AffiliateAnalyticsAggregateSchema = z.object({
241
+ leadsCount: z.number().int().nonnegative(),
242
+ contactedCount: z.number().int().nonnegative(),
243
+ emailsSentCount: z.number().int().nonnegative(),
244
+ replyCount: z.number().int().nonnegative(),
245
+ automaticReplyCount: z.number().int().nonnegative(),
246
+ humanReplyCount: z.number().int().nonnegative(),
247
+ humanReplyRate: z.number().nonnegative(),
248
+ bouncedCount: z.number().int().nonnegative(),
249
+ unsubscribedCount: z.number().int().nonnegative(),
250
+ opportunityCount: z.number().int().nonnegative(),
251
+ meetingCount: z.number().int().nonnegative(),
252
+ meetingCompletedCount: z.number().int().nonnegative(),
253
+ wonCount: z.number().int().nonnegative()
254
+ });
255
+ const AffiliateAnalyticsStepSchema = z.object({
256
+ step: z.number().int().positive(),
257
+ variant: z.number().int().nonnegative(),
258
+ variantLabel: z.string().trim().min(1).max(20),
259
+ sentCount: z.number().int().nonnegative(),
260
+ replyCount: z.number().int().nonnegative(),
261
+ automaticReplyCount: z.number().int().nonnegative(),
262
+ humanReplyCount: z.number().int().nonnegative(),
263
+ humanReplyRate: z.number().nonnegative(),
264
+ opportunityCount: z.number().int().nonnegative(),
265
+ meetingCount: z.number().int().nonnegative().nullable(),
266
+ wonCount: z.number().int().nonnegative().nullable(),
267
+ learning: z.object({
268
+ ready: z.boolean(),
269
+ reason: z.string().min(1)
270
+ })
271
+ });
272
+ const AffiliateOutreachAnalyticsResponseSchema = z.object({
273
+ status: z.literal("ok"),
274
+ runId: z.string().uuid(),
275
+ runStatus: z.string().min(1),
276
+ campaignId: z.string().min(1),
277
+ campaignName: z.string().min(1).nullable(),
278
+ campaignStatus: z.number().nullable(),
279
+ aggregate: AffiliateAnalyticsAggregateSchema,
280
+ steps: z.array(AffiliateAnalyticsStepSchema)
281
+ });
218
282
  const AffiliateOutreachEnrichStartResponseSchema = z.object({
219
283
  status: z.literal("accepted"),
220
284
  audienceRunId: z.string().uuid(),
@@ -547,7 +611,10 @@ const cliPacks = [
547
611
  summary: "Prepare and sync qualified leads into downstream systems.",
548
612
  commands: [
549
613
  "affiliate:run",
614
+ "affiliate:list",
550
615
  "affiliate:status",
616
+ "affiliate:analytics",
617
+ "affiliate:regenerate-sequence",
551
618
  "affiliate:enrich",
552
619
  "affiliate:activate",
553
620
  "affiliate:launch",
@@ -602,7 +669,10 @@ const helpVisibleCommandNames = new Set([
602
669
  "leads:score",
603
670
  "leads:pipeline",
604
671
  "affiliate:run",
672
+ "affiliate:list",
605
673
  "affiliate:status",
674
+ "affiliate:analytics",
675
+ "affiliate:regenerate-sequence",
606
676
  "affiliate:enrich",
607
677
  "affiliate:activate",
608
678
  "affiliate:launch",
@@ -6621,6 +6691,14 @@ async function prepareAffiliateOutreachViaApp(session, payload) {
6621
6691
  }), AffiliateOutreachResponseSchema);
6622
6692
  return value.outreach;
6623
6693
  }
6694
+ async function listAffiliateOutreachViaApp(session) {
6695
+ const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach`, {
6696
+ headers: {
6697
+ Authorization: `Bearer ${currentSession.accessToken}`
6698
+ }
6699
+ }), AffiliateOutreachListResponseSchema);
6700
+ return value;
6701
+ }
6624
6702
  async function getAffiliateOutreachViaApp(session, audienceRunId) {
6625
6703
  const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}`, {
6626
6704
  headers: {
@@ -6629,6 +6707,14 @@ async function getAffiliateOutreachViaApp(session, audienceRunId) {
6629
6707
  }), AffiliateOutreachResponseSchema);
6630
6708
  return value.outreach;
6631
6709
  }
6710
+ async function getAffiliateOutreachAnalyticsViaApp(session, audienceRunId) {
6711
+ const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}/analytics`, {
6712
+ headers: {
6713
+ Authorization: `Bearer ${currentSession.accessToken}`
6714
+ }
6715
+ }), AffiliateOutreachAnalyticsResponseSchema);
6716
+ return value;
6717
+ }
6632
6718
  async function activateAffiliateOutreachViaApp(session, audienceRunId) {
6633
6719
  const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-outreach/${encodeURIComponent(audienceRunId)}/activate`, {
6634
6720
  method: "POST",
@@ -13661,6 +13747,44 @@ program
13661
13747
  }
13662
13748
  });
13663
13749
  });
13750
+ function parseAffiliateOutreachCommandOptions(options) {
13751
+ const parsedSteps = z.coerce.number().int().safeParse(options.steps);
13752
+ if (!parsedSteps.success || parsedSteps.data !== 3) {
13753
+ throw new Error("Affiliate outreach currently requires exactly 3 sequence emails. Use --steps 3.");
13754
+ }
13755
+ const parsedTimingMode = z.enum(["auto", "custom"]).safeParse(options.timingMode);
13756
+ if (!parsedTimingMode.success) {
13757
+ throw new Error("--timing-mode must be either auto or custom.");
13758
+ }
13759
+ const trimmedCampaignName = options.campaignName?.trim();
13760
+ const campaignName = trimmedCampaignName
13761
+ ? z.string().max(120, "--campaign-name must be 120 characters or fewer.").parse(trimmedCampaignName)
13762
+ : undefined;
13763
+ return {
13764
+ campaignName,
13765
+ language: z.string().trim().min(1).max(40).parse(options.language),
13766
+ numberOfSteps: 3,
13767
+ minEmailScore: z.coerce.number().int().min(0).max(100).parse(options.minEmailScore),
13768
+ allowAcceptAll: Boolean(options.allowAcceptAll),
13769
+ timingMode: parsedTimingMode.data,
13770
+ dailyLimit: z.coerce.number().int().min(1).max(1000).parse(options.dailyLimit),
13771
+ timezone: z.string().trim().min(1).max(100).parse(options.timezone),
13772
+ sendFrom: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).parse(options.sendFrom),
13773
+ sendTo: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).parse(options.sendTo)
13774
+ };
13775
+ }
13776
+ function formatAffiliateAnalyticsForOutput(analytics) {
13777
+ if (runtimeOutputOptions.json)
13778
+ return analytics;
13779
+ return {
13780
+ ...analytics,
13781
+ steps: analytics.steps.map((step) => ({
13782
+ ...step,
13783
+ meetingCount: step.meetingCount ?? "n/a",
13784
+ wonCount: step.wonCount ?? "n/a"
13785
+ }))
13786
+ };
13787
+ }
13664
13788
  function getLocalSalesNavigatorPeopleCheckpointPath(sourceQueryUrl, maxResultsPerSearch, pageSize) {
13665
13789
  const key = createHash("sha256")
13666
13790
  .update(`${sourceQueryUrl}\n${maxResultsPerSearch}\n${pageSize}`)
@@ -14741,15 +14865,17 @@ addAffiliateAudienceOptions(program
14741
14865
  .description("Build an affiliate audience and prepare a reviewed Instantly campaign end to end."))
14742
14866
  .option("--campaign-name <name>", "Optional Instantly campaign name")
14743
14867
  .option("--language <language>", "Sequence language", "English")
14744
- .option("--steps <number>", "Number of sequence emails", "3")
14868
+ .option("--steps <number>", "Exactly 3 sequence emails (fixed)", "3")
14745
14869
  .option("--min-email-score <number>", "Minimum Hunter confidence score", "80")
14746
14870
  .option("--allow-accept-all", "Include catch-all domains that meet the score threshold", false)
14871
+ .option("--timing-mode <mode>", "Schedule mode: custom honors the daily limit and sending window; auto derives them from capacity", "custom")
14747
14872
  .option("--daily-limit <number>", "Maximum new leads contacted per day", "25")
14748
14873
  .option("--timezone <timezone>", "Instantly sending timezone", "America/Detroit")
14749
14874
  .option("--send-from <time>", "Weekday sending window start (HH:MM)", "09:00")
14750
14875
  .option("--send-to <time>", "Weekday sending window end (HH:MM)", "16:00")
14751
14876
  .option("--activate", "Activate the prepared Instantly campaign after review data is returned", false)
14752
14877
  .action(async (options) => {
14878
+ const outreachOptions = parseAffiliateOutreachCommandOptions(options);
14753
14879
  const launched = await runAffiliateLaunchCommand(options);
14754
14880
  if (options.dryRun) {
14755
14881
  printOutput({
@@ -14757,7 +14883,8 @@ addAffiliateAudienceOptions(program
14757
14883
  outreach: {
14758
14884
  status: "dry-run",
14759
14885
  provider: "instantly",
14760
- activation: "not_requested"
14886
+ activation: "not_requested",
14887
+ settings: outreachOptions
14761
14888
  }
14762
14889
  });
14763
14890
  return;
@@ -14771,15 +14898,7 @@ addAffiliateAudienceOptions(program
14771
14898
  }
14772
14899
  const outreach = await prepareAffiliateOutreachViaApp(launched.session, {
14773
14900
  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)
14901
+ ...outreachOptions
14783
14902
  });
14784
14903
  const finalOutreach = options.activate
14785
14904
  ? await activateAffiliateOutreachViaApp(launched.session, runId)
@@ -14793,6 +14912,13 @@ addAffiliateAudienceOptions(program
14793
14912
  : "The Instantly campaign is active."
14794
14913
  });
14795
14914
  });
14915
+ program
14916
+ .command("affiliate:list")
14917
+ .description("List affiliate audiences and Instantly outreach runs in the active workspace.")
14918
+ .action(async () => {
14919
+ const session = await requireAuthSession();
14920
+ printOutput(await listAffiliateOutreachViaApp(session));
14921
+ });
14796
14922
  program
14797
14923
  .command("affiliate:status <run-id>")
14798
14924
  .description("Show enrichment, sequence, Instantly, and activation status for an affiliate run.")
@@ -14801,16 +14927,24 @@ program
14801
14927
  const outreach = await getAffiliateOutreachViaApp(session, z.string().uuid().parse(runId));
14802
14928
  printOutput({ status: "ok", outreach });
14803
14929
  });
14930
+ program
14931
+ .command("affiliate:analytics <run-id>")
14932
+ .description("Show safe campaign and step/variant performance metrics for an affiliate run.")
14933
+ .action(async (runId) => {
14934
+ const session = await requireAuthSession();
14935
+ const analytics = await getAffiliateOutreachAnalyticsViaApp(session, z.string().uuid().parse(runId));
14936
+ printOutput(formatAffiliateAnalyticsForOutput(analytics));
14937
+ });
14804
14938
  program
14805
14939
  .command("affiliate:regenerate-sequence <run-id>")
14806
- .description("Regenerate sequence variants and spintax, then sync them to Instantly.")
14940
+ .description("Regenerate observable sequence variants, then sync them to Instantly.")
14807
14941
  .action(async (runId) => {
14808
14942
  const session = await requireAuthSession();
14809
14943
  const outreach = await regenerateAffiliateOutreachSequenceViaApp(session, z.string().uuid().parse(runId));
14810
14944
  printOutput({
14811
14945
  status: "ok",
14812
14946
  outreach,
14813
- message: "Sequence variants and spintax were regenerated and synced to Instantly."
14947
+ message: "Observable sequence variants were regenerated and synced to Instantly."
14814
14948
  });
14815
14949
  });
14816
14950
  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.65",
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",