salesprompter-cli 0.1.14 → 0.1.16

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 (2) hide show
  1. package/dist/cli.js +264 -107
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
+ import { access } from "node:fs/promises";
3
4
  import { createRequire } from "node:module";
5
+ import { emitKeypressEvents } from "node:readline";
4
6
  import { createInterface } from "node:readline/promises";
5
7
  import { Command } from "commander";
6
8
  import { z } from "zod";
@@ -195,14 +197,140 @@ function writeSessionSummary(session) {
195
197
  writeWizardLine(`Workspace: ${orgLabel}`);
196
198
  }
197
199
  }
200
+ async function fileExists(filePath) {
201
+ try {
202
+ await access(filePath);
203
+ return true;
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ }
209
+ function buildLeadOutputPaths(baseSlug) {
210
+ return {
211
+ leadsPath: `./data/${baseSlug}-leads.json`,
212
+ enrichedPath: `./data/${baseSlug}-enriched.json`,
213
+ scoredPath: `./data/${baseSlug}-scored.json`
214
+ };
215
+ }
198
216
  function normalizeChoiceText(value) {
199
217
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
200
218
  }
219
+ function supportsInteractiveChoice() {
220
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && typeof process.stdin.setRawMode === "function");
221
+ }
222
+ function renderInteractiveChoiceLines(prompt, options, activeIndex) {
223
+ const lines = [prompt];
224
+ for (const [index, option] of options.entries()) {
225
+ const description = option.description ? ` - ${option.description}` : "";
226
+ const text = `${index + 1}. ${option.label}${description}`;
227
+ if (index === activeIndex) {
228
+ lines.push(`\x1b[7m> ${text}\x1b[0m`);
229
+ }
230
+ else {
231
+ lines.push(` ${text}`);
232
+ }
233
+ }
234
+ lines.push("\x1b[2mUse Up/Down or number keys. Press Enter to continue.\x1b[0m");
235
+ return lines;
236
+ }
237
+ function redrawInteractiveChoice(lines, previousLineCount) {
238
+ if (previousLineCount > 0) {
239
+ for (let index = 0; index < previousLineCount; index += 1) {
240
+ process.stdout.write("\x1b[1A\x1b[2K\r");
241
+ }
242
+ }
243
+ for (const line of lines) {
244
+ process.stdout.write(`${line}\n`);
245
+ }
246
+ }
247
+ async function promptChoiceInteractive(prompt, options, defaultIndex) {
248
+ const stdin = process.stdin;
249
+ let activeIndex = defaultIndex;
250
+ let renderedLineCount = 0;
251
+ const render = () => {
252
+ const lines = renderInteractiveChoiceLines(prompt, options, activeIndex);
253
+ redrawInteractiveChoice(lines, renderedLineCount);
254
+ renderedLineCount = lines.length;
255
+ };
256
+ const cleanup = () => {
257
+ stdin.removeListener("keypress", onKeypress);
258
+ process.removeListener("SIGINT", onSigint);
259
+ if (stdin.isTTY) {
260
+ stdin.setRawMode(false);
261
+ }
262
+ stdin.pause();
263
+ };
264
+ const finalize = (index, resolve) => {
265
+ const selected = options[index];
266
+ if (!selected) {
267
+ throw new Error("wizard selection invariant violated");
268
+ }
269
+ const summary = `${prompt} ${selected.label}`;
270
+ redrawInteractiveChoice([summary], renderedLineCount);
271
+ renderedLineCount = 1;
272
+ process.stdout.write("\n");
273
+ cleanup();
274
+ resolve(selected.value);
275
+ };
276
+ const cancel = (reject) => {
277
+ cleanup();
278
+ process.stdout.write("\n");
279
+ reject(new Error("prompt cancelled"));
280
+ };
281
+ const onSigint = () => {
282
+ cancel(rejectPromise);
283
+ };
284
+ const onKeypress = (_character, key) => {
285
+ if (key.ctrl && key.name === "c") {
286
+ return cancel(rejectPromise);
287
+ }
288
+ if (key.name === "up") {
289
+ activeIndex = activeIndex === 0 ? options.length - 1 : activeIndex - 1;
290
+ render();
291
+ return;
292
+ }
293
+ if (key.name === "down") {
294
+ activeIndex = activeIndex === options.length - 1 ? 0 : activeIndex + 1;
295
+ render();
296
+ return;
297
+ }
298
+ if (key.name === "return" || key.name === "enter") {
299
+ finalize(activeIndex, resolvePromise);
300
+ return;
301
+ }
302
+ const digit = Number(key.sequence ?? "");
303
+ if (Number.isInteger(digit) && digit >= 1 && digit <= options.length) {
304
+ finalize(digit - 1, resolvePromise);
305
+ }
306
+ };
307
+ let resolvePromise;
308
+ let rejectPromise;
309
+ return await new Promise((resolve, reject) => {
310
+ resolvePromise = resolve;
311
+ rejectPromise = reject;
312
+ emitKeypressEvents(stdin);
313
+ stdin.setRawMode(true);
314
+ stdin.resume();
315
+ stdin.on("keypress", onKeypress);
316
+ process.on("SIGINT", onSigint);
317
+ render();
318
+ });
319
+ }
201
320
  async function promptChoice(rl, prompt, options, defaultValue) {
202
321
  const defaultIndex = options.findIndex((option) => option.value === defaultValue);
203
322
  if (defaultIndex === -1) {
204
323
  throw new Error(`wizard default option is invalid for ${prompt}`);
205
324
  }
325
+ if (supportsInteractiveChoice()) {
326
+ rl.pause?.();
327
+ try {
328
+ return await promptChoiceInteractive(prompt, options, defaultIndex);
329
+ }
330
+ finally {
331
+ rl.resume?.();
332
+ }
333
+ }
206
334
  while (true) {
207
335
  writeWizardLine(prompt);
208
336
  for (const [index, option] of options.entries()) {
@@ -274,6 +402,44 @@ async function promptYesNo(rl, prompt, defaultValue) {
274
402
  writeWizardLine("Please answer yes or no.");
275
403
  }
276
404
  }
405
+ async function maybeSearchLeadDataNow(rl, options) {
406
+ const shouldSearch = await promptYesNo(rl, "Do you want to search your lead data for matches now?", false);
407
+ writeWizardLine();
408
+ if (!shouldSearch) {
409
+ return;
410
+ }
411
+ await runVendorLookupWizard(rl, { icpPath: options.icpPath });
412
+ }
413
+ async function maybePrepareLeadsForOutreach(rl, options) {
414
+ const shouldScore = await promptYesNo(rl, "Do you want me to score these leads for outreach?", false);
415
+ writeWizardLine();
416
+ if (!shouldScore) {
417
+ return;
418
+ }
419
+ const { enrichedPath, scoredPath } = buildLeadOutputPaths(options.baseSlug);
420
+ const enriched = await enrichmentProvider.enrichLeads(options.leads);
421
+ const scored = await scoringProvider.scoreLeads(options.icp, enriched);
422
+ await writeJsonFile(enrichedPath, enriched);
423
+ await writeJsonFile(scoredPath, scored);
424
+ writeWizardLine(`Saved enriched leads to ${enrichedPath}.`);
425
+ writeWizardLine(`Saved scored leads to ${scoredPath}.`);
426
+ writeWizardLine();
427
+ writeWizardLine("Equivalent raw commands:");
428
+ writeWizardLine(` ${buildCommandLine(["salesprompter", "leads:enrich", "--in", options.leadPath, "--out", enrichedPath])}`);
429
+ writeWizardLine(` ${buildCommandLine(["salesprompter", "leads:score", "--icp", options.icpPath, "--in", enrichedPath, "--out", scoredPath])}`);
430
+ if (!process.env.INSTANTLY_API_KEY || process.env.INSTANTLY_API_KEY.trim().length === 0) {
431
+ writeWizardLine();
432
+ writeWizardLine("You can send the scored leads to Instantly later from the main menu.");
433
+ return;
434
+ }
435
+ writeWizardLine();
436
+ const shouldSync = await promptYesNo(rl, "Do you want to send these leads to Instantly now?", false);
437
+ writeWizardLine();
438
+ if (!shouldSync) {
439
+ return;
440
+ }
441
+ await runOutreachSyncWizard(rl, { inputPath: scoredPath });
442
+ }
277
443
  async function ensureWizardSession(options) {
278
444
  if (shouldBypassAuth()) {
279
445
  return null;
@@ -301,11 +467,11 @@ async function ensureWizardSession(options) {
301
467
  return result.session;
302
468
  }
303
469
  async function runVendorIcpWizard(rl) {
304
- const startPoint = await promptChoice(rl, "How do you want to start?", [
470
+ const startPoint = await promptChoice(rl, "How do you want to build your ICP?", [
305
471
  {
306
472
  value: "custom",
307
- label: "Create a custom profile",
308
- description: "Answer a few questions about who you want to sell to",
473
+ label: "Start from scratch",
474
+ description: "Answer a few questions about the companies you want to sell to",
309
475
  aliases: ["custom", "from scratch", "new profile"]
310
476
  },
311
477
  {
@@ -317,21 +483,17 @@ async function runVendorIcpWizard(rl) {
317
483
  ], "custom");
318
484
  writeWizardLine();
319
485
  if (startPoint === "custom") {
320
- const productName = await promptText(rl, "What are you selling?", { required: true });
321
- const description = await promptText(rl, "Who is this for? (optional)");
322
- const industries = await promptText(rl, "Target industries (optional, comma-separated)");
323
- const companySizes = await promptText(rl, "Target company sizes (optional, comma-separated)");
324
- const regions = await promptText(rl, "Target regions (optional, comma-separated)");
325
- const countries = await promptText(rl, "Target countries (optional, comma-separated)");
326
- const titles = await promptText(rl, "Target job titles (optional, comma-separated)");
486
+ const productName = await promptText(rl, "What do you sell?", { required: true });
487
+ const description = await promptText(rl, "Short description (optional)");
488
+ const industries = await promptText(rl, "Industries to target (optional, comma-separated)");
489
+ const companySizes = await promptText(rl, "Company sizes to target (optional, comma-separated)");
490
+ const regions = await promptText(rl, "Regions to target (optional, comma-separated)");
491
+ const countries = await promptText(rl, "Countries to target (optional, comma-separated)");
492
+ const titles = await promptText(rl, "Job titles to target (optional, comma-separated)");
327
493
  const keywords = await promptText(rl, "Keywords or buying signals (optional, comma-separated)");
328
494
  writeWizardLine();
329
495
  const slug = slugify(productName) || "icp";
330
- const outPath = await promptText(rl, "Where should I save the ICP JSON?", {
331
- defaultValue: `./data/${slug}-icp.json`,
332
- required: true
333
- });
334
- writeWizardLine();
496
+ const outPath = `./data/${slug}-icp.json`;
335
497
  const icp = IcpSchema.parse({
336
498
  name: `${productName} ICP`,
337
499
  description,
@@ -344,7 +506,7 @@ async function runVendorIcpWizard(rl) {
344
506
  });
345
507
  await writeJsonFile(outPath, icp);
346
508
  writeWizardLine(`Created ${icp.name}.`);
347
- writeWizardLine(`Saved ICP to ${outPath}.`);
509
+ writeWizardLine(`Saved profile to ${outPath}.`);
348
510
  writeWizardLine();
349
511
  writeWizardLine("Equivalent raw command:");
350
512
  const defineArgs = ["salesprompter", "icp:define", "--name", icp.name];
@@ -372,73 +534,42 @@ async function runVendorIcpWizard(rl) {
372
534
  defineArgs.push("--out", outPath);
373
535
  writeWizardLine(` ${buildCommandLine(defineArgs)}`);
374
536
  writeWizardLine();
375
- writeWizardLine("Next suggested command:");
376
- writeWizardLine(` ${buildCommandLine([
377
- "salesprompter",
378
- "leads:generate",
379
- "--icp",
380
- outPath,
381
- "--count",
382
- "5",
383
- "--out",
384
- `./data/${slug}-leads.json`
385
- ])}`);
537
+ await maybeSearchLeadDataNow(rl, { icpPath: outPath });
386
538
  return;
387
539
  }
388
540
  const vendor = "deel";
389
541
  writeWizardLine("Using the built-in Deel ICP template.");
390
542
  writeWizardLine();
391
- const market = await promptChoice(rl, "Which market do you want to target?", [
543
+ const market = await promptChoice(rl, "Which market do you want to focus on?", [
392
544
  { value: "dach", label: "DACH", description: "Germany, Austria, Switzerland" },
393
545
  { value: "europe", label: "Europe" },
394
546
  { value: "global", label: "Global" }
395
547
  ], "dach");
396
548
  writeWizardLine();
397
- const outPath = await promptText(rl, "Where should I save the ICP JSON?", {
398
- defaultValue: `./data/${slugify(vendor)}-icp-${market}.json`,
399
- required: true
400
- });
401
- writeWizardLine();
549
+ const outPath = `./data/${slugify(vendor)}-icp-${market}.json`;
402
550
  const icp = buildVendorIcp(vendor, market);
403
551
  await writeJsonFile(outPath, icp);
404
552
  writeWizardLine(`Created ${icp.name}.`);
405
- writeWizardLine(`Saved ICP to ${outPath}.`);
553
+ writeWizardLine(`Saved profile to ${outPath}.`);
406
554
  writeWizardLine();
407
555
  writeWizardLine("Equivalent raw command:");
408
556
  writeWizardLine(` ${buildCommandLine(["salesprompter", "icp:vendor", "--vendor", vendor, "--market", market, "--out", outPath])}`);
409
557
  writeWizardLine();
410
- writeWizardLine("Next suggested command:");
411
- writeWizardLine(` ${buildCommandLine([
412
- "salesprompter",
413
- "leads:lookup:bq",
414
- "--icp",
415
- outPath,
416
- "--limit",
417
- "100",
418
- "--lead-out",
419
- `./data/${slugify(vendor)}-leads.json`
420
- ])}`);
558
+ await maybeSearchLeadDataNow(rl, { icpPath: outPath });
421
559
  }
422
560
  async function runTargetAccountWizard(rl) {
423
- const domain = normalizeDomainInput(await promptText(rl, "Which company are you targeting? Enter the domain", { required: true }));
561
+ const domain = normalizeDomainInput(await promptText(rl, "Which company do you want leads from? Enter the domain", { required: true }));
424
562
  writeWizardLine();
425
- const companyName = await promptText(rl, "Company name override (optional)");
563
+ const companyName = await promptText(rl, "Company name (optional)");
426
564
  const displayName = companyName || deriveCompanyNameFromDomain(domain);
427
- const leadCount = z.coerce.number().int().min(1).max(1000).parse(await promptText(rl, "How many leads should I generate?", { defaultValue: "5", required: true }));
428
- const region = await promptText(rl, "Primary region hint", { defaultValue: "Global", required: true });
429
- const industries = await promptText(rl, "Industry hint (optional, comma-separated)");
430
- const titles = await promptText(rl, "Target titles (optional, comma-separated)");
565
+ const leadCount = z.coerce.number().int().min(1).max(1000).parse(await promptText(rl, "How many people do you want?", { defaultValue: "5", required: true }));
566
+ const region = await promptText(rl, "Region", { defaultValue: "Global", required: true });
567
+ const industries = await promptText(rl, "Industries (optional, comma-separated)");
568
+ const titles = await promptText(rl, "Job titles (optional, comma-separated)");
431
569
  writeWizardLine();
432
570
  const slug = slugify(domain);
433
- const icpPath = await promptText(rl, "Where should I save the ad-hoc ICP JSON?", {
434
- defaultValue: `./data/${slug}-target-icp.json`,
435
- required: true
436
- });
437
- const leadsPath = await promptText(rl, "Where should I save the generated leads JSON?", {
438
- defaultValue: `./data/${slug}-leads.json`,
439
- required: true
440
- });
441
- writeWizardLine();
571
+ const icpPath = `./data/${slug}-target-icp.json`;
572
+ const { leadsPath } = buildLeadOutputPaths(slug);
442
573
  const icp = IcpSchema.parse({
443
574
  name: `${displayName} target account`,
444
575
  regions: region.length > 0 ? [region] : [],
@@ -452,7 +583,7 @@ async function runTargetAccountWizard(rl) {
452
583
  });
453
584
  await writeJsonFile(leadsPath, result.leads);
454
585
  writeWizardLine(`Generated ${result.leads.length} leads for ${result.account.companyName} (${result.account.domain}).`);
455
- writeWizardLine(`Saved ad-hoc ICP to ${icpPath}.`);
586
+ writeWizardLine(`Saved profile to ${icpPath}.`);
456
587
  writeWizardLine(`Saved leads to ${leadsPath}.`);
457
588
  if (result.warnings.length > 0) {
458
589
  writeWizardLine();
@@ -478,6 +609,14 @@ async function runTargetAccountWizard(rl) {
478
609
  }
479
610
  leadArgs.push("--out", leadsPath);
480
611
  writeWizardLine(` ${buildCommandLine(leadArgs)}`);
612
+ writeWizardLine();
613
+ await maybePrepareLeadsForOutreach(rl, {
614
+ baseSlug: slug,
615
+ icp,
616
+ icpPath,
617
+ leadPath: leadsPath,
618
+ leads: result.leads
619
+ });
481
620
  }
482
621
  async function runLeadGenerationWizard(rl) {
483
622
  const source = await promptChoice(rl, "How do you want to generate leads?", [
@@ -489,8 +628,8 @@ async function runLeadGenerationWizard(rl) {
489
628
  },
490
629
  {
491
630
  value: "vendor-lookup",
492
- label: "From my BigQuery data",
493
- description: "Use a saved profile to search the lead data you already have",
631
+ label: "From my own lead data",
632
+ description: "Search leads you already have in BigQuery",
494
633
  aliases: ["bigquery", "warehouse", "my data", "from bigquery"]
495
634
  }
496
635
  ], "target-account");
@@ -501,33 +640,24 @@ async function runLeadGenerationWizard(rl) {
501
640
  }
502
641
  await runVendorLookupWizard(rl);
503
642
  }
504
- async function runVendorLookupWizard(rl) {
505
- const icpPath = await promptText(rl, "Where is your ICP JSON file?", {
506
- defaultValue: "./data/icp.json",
507
- required: true
508
- });
509
- const limit = z.coerce.number().int().min(1).max(5000).parse(await promptText(rl, "How many leads should I look up?", { defaultValue: "100", required: true }));
510
- const execute = await promptYesNo(rl, "Execute the BigQuery lookup now?", false);
643
+ async function runVendorLookupWizard(rl, options) {
644
+ const icpPath = options?.icpPath
645
+ ? options.icpPath
646
+ : await promptText(rl, "Which saved ICP should I use?", {
647
+ defaultValue: "./data/icp.json",
648
+ required: true
649
+ });
650
+ if (options?.icpPath) {
651
+ writeWizardLine(`Using profile from ${icpPath}.`);
652
+ }
653
+ const limit = z.coerce.number().int().min(1).max(5000).parse(await promptText(rl, "How many leads do you want?", { defaultValue: "100", required: true }));
654
+ const execute = await promptYesNo(rl, "Do you want me to run the BigQuery search now?", false);
511
655
  writeWizardLine();
512
656
  const icp = await readJsonFile(icpPath, IcpSchema);
513
657
  const slug = slugify(icp.name) || "icp";
514
- const sqlPath = await promptText(rl, "Where should I save the generated SQL?", {
515
- defaultValue: `./data/${slug}-lookup.sql`,
516
- required: true
517
- });
518
- const rawPath = execute
519
- ? await promptText(rl, "Where should I save raw BigQuery rows?", {
520
- defaultValue: `./data/${slug}-leads-raw.json`,
521
- required: true
522
- })
523
- : "";
524
- const leadPath = execute
525
- ? await promptText(rl, "Where should I save normalized leads?", {
526
- defaultValue: `./data/${slug}-leads.json`,
527
- required: true
528
- })
529
- : "";
530
- writeWizardLine();
658
+ const sqlPath = `./data/${slug}-lookup.sql`;
659
+ const rawPath = `./data/${slug}-leads-raw.json`;
660
+ const { leadsPath: leadPath } = buildLeadOutputPaths(slug);
531
661
  const sql = buildBigQueryLeadLookupSql(icp, {
532
662
  table: "icpidentifier.SalesGPT.leadPool_new",
533
663
  companyField: "companyName",
@@ -547,19 +677,19 @@ async function runVendorLookupWizard(rl) {
547
677
  });
548
678
  await writeTextFile(sqlPath, `${sql}\n`);
549
679
  let executedRowCount = null;
680
+ let normalizedLeads = [];
550
681
  if (execute) {
551
682
  const rows = await runBigQueryQuery(sql, { maxRows: limit });
552
683
  const parsedRows = z.array(z.record(z.string(), z.unknown())).parse(rows);
553
684
  await writeJsonFile(rawPath, parsedRows);
554
- const normalizedLeads = normalizeBigQueryLeadRows(parsedRows);
685
+ normalizedLeads = normalizeBigQueryLeadRows(parsedRows);
555
686
  await writeJsonFile(leadPath, normalizedLeads);
556
687
  executedRowCount = parsedRows.length;
557
688
  }
558
- writeWizardLine(`Using ICP from ${icpPath}.`);
559
- writeWizardLine(`Saved lookup SQL to ${sqlPath}.`);
689
+ writeWizardLine(`Saved search SQL to ${sqlPath}.`);
560
690
  if (execute) {
561
691
  writeWizardLine(`Saved ${executedRowCount ?? 0} raw rows to ${rawPath}.`);
562
- writeWizardLine(`Saved normalized leads to ${leadPath}.`);
692
+ writeWizardLine(`Saved leads to ${leadPath}.`);
563
693
  }
564
694
  writeWizardLine();
565
695
  writeWizardLine("Equivalent raw command:");
@@ -568,25 +698,47 @@ async function runVendorLookupWizard(rl) {
568
698
  lookupArgs.push("--execute", "--out", rawPath, "--lead-out", leadPath);
569
699
  }
570
700
  writeWizardLine(` ${buildCommandLine(lookupArgs)}`);
701
+ if (!execute) {
702
+ return;
703
+ }
704
+ writeWizardLine();
705
+ await maybePrepareLeadsForOutreach(rl, {
706
+ baseSlug: slug,
707
+ icp,
708
+ icpPath,
709
+ leadPath,
710
+ leads: normalizedLeads
711
+ });
571
712
  }
572
- async function runOutreachSyncWizard(rl) {
713
+ async function runOutreachSyncWizard(rl, options) {
573
714
  const target = "instantly";
574
- writeWizardLine("Using Instantly.");
715
+ const targetLabel = "Instantly";
716
+ writeWizardLine(`Using ${targetLabel}.`);
575
717
  writeWizardLine();
576
718
  if (!process.env.INSTANTLY_API_KEY || process.env.INSTANTLY_API_KEY.trim().length === 0) {
577
719
  throw new Error("INSTANTLY_API_KEY is required for the Instantly sync flow.");
578
720
  }
579
- const inputPath = await promptText(rl, "Where is your scored leads JSON file?", {
580
- defaultValue: "./data/scored.json",
581
- required: true
582
- });
721
+ let inputPath = options?.inputPath?.trim() ?? "";
722
+ if (inputPath.length === 0 && (await fileExists("./data/scored.json"))) {
723
+ inputPath = "./data/scored.json";
724
+ }
725
+ if (inputPath.length === 0) {
726
+ inputPath = await promptText(rl, "Where is your scored leads file?", {
727
+ defaultValue: "./data/scored.json",
728
+ required: true
729
+ });
730
+ }
731
+ else {
732
+ writeWizardLine(`Using scored leads from ${inputPath}.`);
733
+ writeWizardLine();
734
+ }
583
735
  const defaultCampaignId = process.env.INSTANTLY_CAMPAIGN_ID?.trim();
584
- const campaignId = await promptText(rl, "Instantly campaign ID", {
736
+ const campaignId = await promptText(rl, "Which Instantly campaign should I use?", {
585
737
  defaultValue: defaultCampaignId && defaultCampaignId.length > 0 ? defaultCampaignId : undefined,
586
738
  required: defaultCampaignId === undefined || defaultCampaignId.length === 0
587
739
  });
588
- const apply = await promptYesNo(rl, "Create the leads in Instantly now?", false);
589
- const allowDuplicates = await promptYesNo(rl, "Allow duplicate emails in the campaign?", false);
740
+ const apply = await promptYesNo(rl, "Add these leads to Instantly now?", false);
741
+ const allowDuplicates = await promptYesNo(rl, "Keep leads that are already in the campaign?", false);
590
742
  writeWizardLine();
591
743
  const leads = await readJsonFile(inputPath, z.array(ScoredLeadSchema));
592
744
  const result = await syncProvider.sync(target, leads, {
@@ -595,12 +747,12 @@ async function runOutreachSyncWizard(rl) {
595
747
  allowDuplicates
596
748
  });
597
749
  const skipped = result.skipped ?? 0;
598
- writeWizardLine(`${apply ? "Sent" : "Prepared"} ${result.synced} lead${result.synced === 1 ? "" : "s"} for ${target}.`);
750
+ writeWizardLine(`${apply ? "Sent" : "Prepared"} ${result.synced} lead${result.synced === 1 ? "" : "s"} for ${targetLabel}.`);
599
751
  if (skipped > 0) {
600
752
  writeWizardLine(`Skipped ${skipped} duplicate lead${skipped === 1 ? "" : "s"}.`);
601
753
  }
602
754
  if (result.dryRun) {
603
- writeWizardLine("This was a dry run. Re-run and confirm the write when you are ready.");
755
+ writeWizardLine("Nothing was sent yet. Re-run and confirm when you are ready.");
604
756
  }
605
757
  writeWizardLine();
606
758
  writeWizardLine("Equivalent raw command:");
@@ -638,20 +790,20 @@ async function runWizard(options) {
638
790
  const flow = await promptChoice(rl, "What do you want help with?", [
639
791
  {
640
792
  value: "vendor-icp",
641
- label: "Figure out who to target",
642
- description: "Create an ideal customer profile for your product",
793
+ label: "Define my ICP",
794
+ description: "Build the company profile you want to sell to",
643
795
  aliases: ["icp", "ideal customer", "who to target", "targeting", "profile"]
644
796
  },
645
797
  {
646
798
  value: "lead-generation",
647
799
  label: "Generate leads",
648
- description: "Find people at one company or from your BigQuery data",
800
+ description: "Find people at one company or from your own lead data",
649
801
  aliases: ["leads", "find leads", "lead generation", "find people"]
650
802
  },
651
803
  {
652
804
  value: "outreach-sync",
653
- label: "Add leads to Instantly",
654
- description: "Send a scored leads file to an Instantly campaign",
805
+ label: "Send leads to Instantly",
806
+ description: "Use a scored leads file to fill an Instantly campaign",
655
807
  aliases: ["instantly", "outreach", "send leads", "campaign"]
656
808
  }
657
809
  ], "vendor-icp");
@@ -1551,6 +1703,11 @@ async function main() {
1551
1703
  await program.parseAsync(process.argv);
1552
1704
  }
1553
1705
  main().catch((error) => {
1706
+ if (error instanceof Error &&
1707
+ (error.message === "prompt cancelled" || error.message === "readline was closed")) {
1708
+ process.exitCode = 130;
1709
+ return;
1710
+ }
1554
1711
  const cliError = buildCliError(error);
1555
1712
  const space = runtimeOutputOptions.json ? undefined : 2;
1556
1713
  if (runtimeOutputOptions.json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "JSON-first sales prospecting CLI for ICP definition, lead generation, enrichment, scoring, and CRM/outreach sync.",
5
5
  "type": "module",
6
6
  "bin": {