salesprompter-cli 0.1.15 → 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 +148 -107
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
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";
4
5
  import { emitKeypressEvents } from "node:readline";
5
6
  import { createInterface } from "node:readline/promises";
@@ -196,6 +197,22 @@ function writeSessionSummary(session) {
196
197
  writeWizardLine(`Workspace: ${orgLabel}`);
197
198
  }
198
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
+ }
199
216
  function normalizeChoiceText(value) {
200
217
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
201
218
  }
@@ -385,6 +402,44 @@ async function promptYesNo(rl, prompt, defaultValue) {
385
402
  writeWizardLine("Please answer yes or no.");
386
403
  }
387
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
+ }
388
443
  async function ensureWizardSession(options) {
389
444
  if (shouldBypassAuth()) {
390
445
  return null;
@@ -412,11 +467,11 @@ async function ensureWizardSession(options) {
412
467
  return result.session;
413
468
  }
414
469
  async function runVendorIcpWizard(rl) {
415
- 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?", [
416
471
  {
417
472
  value: "custom",
418
- label: "Create a custom profile",
419
- 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",
420
475
  aliases: ["custom", "from scratch", "new profile"]
421
476
  },
422
477
  {
@@ -428,21 +483,17 @@ async function runVendorIcpWizard(rl) {
428
483
  ], "custom");
429
484
  writeWizardLine();
430
485
  if (startPoint === "custom") {
431
- const productName = await promptText(rl, "What are you selling?", { required: true });
432
- const description = await promptText(rl, "Who is this for? (optional)");
433
- const industries = await promptText(rl, "Target industries (optional, comma-separated)");
434
- const companySizes = await promptText(rl, "Target company sizes (optional, comma-separated)");
435
- const regions = await promptText(rl, "Target regions (optional, comma-separated)");
436
- const countries = await promptText(rl, "Target countries (optional, comma-separated)");
437
- 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)");
438
493
  const keywords = await promptText(rl, "Keywords or buying signals (optional, comma-separated)");
439
494
  writeWizardLine();
440
495
  const slug = slugify(productName) || "icp";
441
- const outPath = await promptText(rl, "Where should I save the ICP JSON?", {
442
- defaultValue: `./data/${slug}-icp.json`,
443
- required: true
444
- });
445
- writeWizardLine();
496
+ const outPath = `./data/${slug}-icp.json`;
446
497
  const icp = IcpSchema.parse({
447
498
  name: `${productName} ICP`,
448
499
  description,
@@ -455,7 +506,7 @@ async function runVendorIcpWizard(rl) {
455
506
  });
456
507
  await writeJsonFile(outPath, icp);
457
508
  writeWizardLine(`Created ${icp.name}.`);
458
- writeWizardLine(`Saved ICP to ${outPath}.`);
509
+ writeWizardLine(`Saved profile to ${outPath}.`);
459
510
  writeWizardLine();
460
511
  writeWizardLine("Equivalent raw command:");
461
512
  const defineArgs = ["salesprompter", "icp:define", "--name", icp.name];
@@ -483,73 +534,42 @@ async function runVendorIcpWizard(rl) {
483
534
  defineArgs.push("--out", outPath);
484
535
  writeWizardLine(` ${buildCommandLine(defineArgs)}`);
485
536
  writeWizardLine();
486
- writeWizardLine("Next suggested command:");
487
- writeWizardLine(` ${buildCommandLine([
488
- "salesprompter",
489
- "leads:generate",
490
- "--icp",
491
- outPath,
492
- "--count",
493
- "5",
494
- "--out",
495
- `./data/${slug}-leads.json`
496
- ])}`);
537
+ await maybeSearchLeadDataNow(rl, { icpPath: outPath });
497
538
  return;
498
539
  }
499
540
  const vendor = "deel";
500
541
  writeWizardLine("Using the built-in Deel ICP template.");
501
542
  writeWizardLine();
502
- 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?", [
503
544
  { value: "dach", label: "DACH", description: "Germany, Austria, Switzerland" },
504
545
  { value: "europe", label: "Europe" },
505
546
  { value: "global", label: "Global" }
506
547
  ], "dach");
507
548
  writeWizardLine();
508
- const outPath = await promptText(rl, "Where should I save the ICP JSON?", {
509
- defaultValue: `./data/${slugify(vendor)}-icp-${market}.json`,
510
- required: true
511
- });
512
- writeWizardLine();
549
+ const outPath = `./data/${slugify(vendor)}-icp-${market}.json`;
513
550
  const icp = buildVendorIcp(vendor, market);
514
551
  await writeJsonFile(outPath, icp);
515
552
  writeWizardLine(`Created ${icp.name}.`);
516
- writeWizardLine(`Saved ICP to ${outPath}.`);
553
+ writeWizardLine(`Saved profile to ${outPath}.`);
517
554
  writeWizardLine();
518
555
  writeWizardLine("Equivalent raw command:");
519
556
  writeWizardLine(` ${buildCommandLine(["salesprompter", "icp:vendor", "--vendor", vendor, "--market", market, "--out", outPath])}`);
520
557
  writeWizardLine();
521
- writeWizardLine("Next suggested command:");
522
- writeWizardLine(` ${buildCommandLine([
523
- "salesprompter",
524
- "leads:lookup:bq",
525
- "--icp",
526
- outPath,
527
- "--limit",
528
- "100",
529
- "--lead-out",
530
- `./data/${slugify(vendor)}-leads.json`
531
- ])}`);
558
+ await maybeSearchLeadDataNow(rl, { icpPath: outPath });
532
559
  }
533
560
  async function runTargetAccountWizard(rl) {
534
- 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 }));
535
562
  writeWizardLine();
536
- const companyName = await promptText(rl, "Company name override (optional)");
563
+ const companyName = await promptText(rl, "Company name (optional)");
537
564
  const displayName = companyName || deriveCompanyNameFromDomain(domain);
538
- const leadCount = z.coerce.number().int().min(1).max(1000).parse(await promptText(rl, "How many leads should I generate?", { defaultValue: "5", required: true }));
539
- const region = await promptText(rl, "Primary region hint", { defaultValue: "Global", required: true });
540
- const industries = await promptText(rl, "Industry hint (optional, comma-separated)");
541
- 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)");
542
569
  writeWizardLine();
543
570
  const slug = slugify(domain);
544
- const icpPath = await promptText(rl, "Where should I save the ad-hoc ICP JSON?", {
545
- defaultValue: `./data/${slug}-target-icp.json`,
546
- required: true
547
- });
548
- const leadsPath = await promptText(rl, "Where should I save the generated leads JSON?", {
549
- defaultValue: `./data/${slug}-leads.json`,
550
- required: true
551
- });
552
- writeWizardLine();
571
+ const icpPath = `./data/${slug}-target-icp.json`;
572
+ const { leadsPath } = buildLeadOutputPaths(slug);
553
573
  const icp = IcpSchema.parse({
554
574
  name: `${displayName} target account`,
555
575
  regions: region.length > 0 ? [region] : [],
@@ -563,7 +583,7 @@ async function runTargetAccountWizard(rl) {
563
583
  });
564
584
  await writeJsonFile(leadsPath, result.leads);
565
585
  writeWizardLine(`Generated ${result.leads.length} leads for ${result.account.companyName} (${result.account.domain}).`);
566
- writeWizardLine(`Saved ad-hoc ICP to ${icpPath}.`);
586
+ writeWizardLine(`Saved profile to ${icpPath}.`);
567
587
  writeWizardLine(`Saved leads to ${leadsPath}.`);
568
588
  if (result.warnings.length > 0) {
569
589
  writeWizardLine();
@@ -589,6 +609,14 @@ async function runTargetAccountWizard(rl) {
589
609
  }
590
610
  leadArgs.push("--out", leadsPath);
591
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
+ });
592
620
  }
593
621
  async function runLeadGenerationWizard(rl) {
594
622
  const source = await promptChoice(rl, "How do you want to generate leads?", [
@@ -600,8 +628,8 @@ async function runLeadGenerationWizard(rl) {
600
628
  },
601
629
  {
602
630
  value: "vendor-lookup",
603
- label: "From my BigQuery data",
604
- 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",
605
633
  aliases: ["bigquery", "warehouse", "my data", "from bigquery"]
606
634
  }
607
635
  ], "target-account");
@@ -612,33 +640,24 @@ async function runLeadGenerationWizard(rl) {
612
640
  }
613
641
  await runVendorLookupWizard(rl);
614
642
  }
615
- async function runVendorLookupWizard(rl) {
616
- const icpPath = await promptText(rl, "Where is your ICP JSON file?", {
617
- defaultValue: "./data/icp.json",
618
- required: true
619
- });
620
- 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 }));
621
- 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);
622
655
  writeWizardLine();
623
656
  const icp = await readJsonFile(icpPath, IcpSchema);
624
657
  const slug = slugify(icp.name) || "icp";
625
- const sqlPath = await promptText(rl, "Where should I save the generated SQL?", {
626
- defaultValue: `./data/${slug}-lookup.sql`,
627
- required: true
628
- });
629
- const rawPath = execute
630
- ? await promptText(rl, "Where should I save raw BigQuery rows?", {
631
- defaultValue: `./data/${slug}-leads-raw.json`,
632
- required: true
633
- })
634
- : "";
635
- const leadPath = execute
636
- ? await promptText(rl, "Where should I save normalized leads?", {
637
- defaultValue: `./data/${slug}-leads.json`,
638
- required: true
639
- })
640
- : "";
641
- writeWizardLine();
658
+ const sqlPath = `./data/${slug}-lookup.sql`;
659
+ const rawPath = `./data/${slug}-leads-raw.json`;
660
+ const { leadsPath: leadPath } = buildLeadOutputPaths(slug);
642
661
  const sql = buildBigQueryLeadLookupSql(icp, {
643
662
  table: "icpidentifier.SalesGPT.leadPool_new",
644
663
  companyField: "companyName",
@@ -658,19 +677,19 @@ async function runVendorLookupWizard(rl) {
658
677
  });
659
678
  await writeTextFile(sqlPath, `${sql}\n`);
660
679
  let executedRowCount = null;
680
+ let normalizedLeads = [];
661
681
  if (execute) {
662
682
  const rows = await runBigQueryQuery(sql, { maxRows: limit });
663
683
  const parsedRows = z.array(z.record(z.string(), z.unknown())).parse(rows);
664
684
  await writeJsonFile(rawPath, parsedRows);
665
- const normalizedLeads = normalizeBigQueryLeadRows(parsedRows);
685
+ normalizedLeads = normalizeBigQueryLeadRows(parsedRows);
666
686
  await writeJsonFile(leadPath, normalizedLeads);
667
687
  executedRowCount = parsedRows.length;
668
688
  }
669
- writeWizardLine(`Using ICP from ${icpPath}.`);
670
- writeWizardLine(`Saved lookup SQL to ${sqlPath}.`);
689
+ writeWizardLine(`Saved search SQL to ${sqlPath}.`);
671
690
  if (execute) {
672
691
  writeWizardLine(`Saved ${executedRowCount ?? 0} raw rows to ${rawPath}.`);
673
- writeWizardLine(`Saved normalized leads to ${leadPath}.`);
692
+ writeWizardLine(`Saved leads to ${leadPath}.`);
674
693
  }
675
694
  writeWizardLine();
676
695
  writeWizardLine("Equivalent raw command:");
@@ -679,25 +698,47 @@ async function runVendorLookupWizard(rl) {
679
698
  lookupArgs.push("--execute", "--out", rawPath, "--lead-out", leadPath);
680
699
  }
681
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
+ });
682
712
  }
683
- async function runOutreachSyncWizard(rl) {
713
+ async function runOutreachSyncWizard(rl, options) {
684
714
  const target = "instantly";
685
- writeWizardLine("Using Instantly.");
715
+ const targetLabel = "Instantly";
716
+ writeWizardLine(`Using ${targetLabel}.`);
686
717
  writeWizardLine();
687
718
  if (!process.env.INSTANTLY_API_KEY || process.env.INSTANTLY_API_KEY.trim().length === 0) {
688
719
  throw new Error("INSTANTLY_API_KEY is required for the Instantly sync flow.");
689
720
  }
690
- const inputPath = await promptText(rl, "Where is your scored leads JSON file?", {
691
- defaultValue: "./data/scored.json",
692
- required: true
693
- });
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
+ }
694
735
  const defaultCampaignId = process.env.INSTANTLY_CAMPAIGN_ID?.trim();
695
- const campaignId = await promptText(rl, "Instantly campaign ID", {
736
+ const campaignId = await promptText(rl, "Which Instantly campaign should I use?", {
696
737
  defaultValue: defaultCampaignId && defaultCampaignId.length > 0 ? defaultCampaignId : undefined,
697
738
  required: defaultCampaignId === undefined || defaultCampaignId.length === 0
698
739
  });
699
- const apply = await promptYesNo(rl, "Create the leads in Instantly now?", false);
700
- 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);
701
742
  writeWizardLine();
702
743
  const leads = await readJsonFile(inputPath, z.array(ScoredLeadSchema));
703
744
  const result = await syncProvider.sync(target, leads, {
@@ -706,12 +747,12 @@ async function runOutreachSyncWizard(rl) {
706
747
  allowDuplicates
707
748
  });
708
749
  const skipped = result.skipped ?? 0;
709
- 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}.`);
710
751
  if (skipped > 0) {
711
752
  writeWizardLine(`Skipped ${skipped} duplicate lead${skipped === 1 ? "" : "s"}.`);
712
753
  }
713
754
  if (result.dryRun) {
714
- 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.");
715
756
  }
716
757
  writeWizardLine();
717
758
  writeWizardLine("Equivalent raw command:");
@@ -749,20 +790,20 @@ async function runWizard(options) {
749
790
  const flow = await promptChoice(rl, "What do you want help with?", [
750
791
  {
751
792
  value: "vendor-icp",
752
- label: "Figure out who to target",
753
- 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",
754
795
  aliases: ["icp", "ideal customer", "who to target", "targeting", "profile"]
755
796
  },
756
797
  {
757
798
  value: "lead-generation",
758
799
  label: "Generate leads",
759
- description: "Find people at one company or from your BigQuery data",
800
+ description: "Find people at one company or from your own lead data",
760
801
  aliases: ["leads", "find leads", "lead generation", "find people"]
761
802
  },
762
803
  {
763
804
  value: "outreach-sync",
764
- label: "Add leads to Instantly",
765
- 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",
766
807
  aliases: ["instantly", "outreach", "send leads", "campaign"]
767
808
  }
768
809
  ], "vendor-icp");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.15",
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": {