salesprompter-cli 0.1.13 → 0.1.15

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 -49
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
3
  import { createRequire } from "node:module";
4
+ import { emitKeypressEvents } from "node:readline";
4
5
  import { createInterface } from "node:readline/promises";
5
6
  import { Command } from "commander";
6
7
  import { z } from "zod";
@@ -175,30 +176,151 @@ function deriveCompanyNameFromDomain(domain) {
175
176
  function writeWizardLine(message = "") {
176
177
  process.stdout.write(`${message}\n`);
177
178
  }
179
+ function isOpaqueOrgId(value) {
180
+ return /^org_[A-Za-z0-9]+$/.test(value);
181
+ }
178
182
  function getOrgLabel(session) {
179
- return session.user.orgName ?? session.user.orgSlug ?? session.user.orgId ?? null;
183
+ const label = session.user.orgName ?? session.user.orgSlug ?? session.user.orgId ?? null;
184
+ if (label && isOpaqueOrgId(label)) {
185
+ return null;
186
+ }
187
+ return label;
180
188
  }
181
189
  function writeSessionSummary(session) {
190
+ const identity = session.user.name?.trim()
191
+ ? `${session.user.name} (${session.user.email})`
192
+ : session.user.email;
193
+ writeWizardLine(`Signed in as ${identity}.`);
182
194
  const orgLabel = getOrgLabel(session);
183
195
  if (orgLabel) {
184
- writeWizardLine(`Signed in as ${session.user.email} for ${orgLabel}.`);
185
- return;
196
+ writeWizardLine(`Workspace: ${orgLabel}`);
186
197
  }
187
- writeWizardLine(`Signed in as ${session.user.email}.`);
188
- writeWizardLine("No organization is attached to this CLI session.");
198
+ }
199
+ function normalizeChoiceText(value) {
200
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
201
+ }
202
+ function supportsInteractiveChoice() {
203
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && typeof process.stdin.setRawMode === "function");
204
+ }
205
+ function renderInteractiveChoiceLines(prompt, options, activeIndex) {
206
+ const lines = [prompt];
207
+ for (const [index, option] of options.entries()) {
208
+ const description = option.description ? ` - ${option.description}` : "";
209
+ const text = `${index + 1}. ${option.label}${description}`;
210
+ if (index === activeIndex) {
211
+ lines.push(`\x1b[7m> ${text}\x1b[0m`);
212
+ }
213
+ else {
214
+ lines.push(` ${text}`);
215
+ }
216
+ }
217
+ lines.push("\x1b[2mUse Up/Down or number keys. Press Enter to continue.\x1b[0m");
218
+ return lines;
219
+ }
220
+ function redrawInteractiveChoice(lines, previousLineCount) {
221
+ if (previousLineCount > 0) {
222
+ for (let index = 0; index < previousLineCount; index += 1) {
223
+ process.stdout.write("\x1b[1A\x1b[2K\r");
224
+ }
225
+ }
226
+ for (const line of lines) {
227
+ process.stdout.write(`${line}\n`);
228
+ }
229
+ }
230
+ async function promptChoiceInteractive(prompt, options, defaultIndex) {
231
+ const stdin = process.stdin;
232
+ let activeIndex = defaultIndex;
233
+ let renderedLineCount = 0;
234
+ const render = () => {
235
+ const lines = renderInteractiveChoiceLines(prompt, options, activeIndex);
236
+ redrawInteractiveChoice(lines, renderedLineCount);
237
+ renderedLineCount = lines.length;
238
+ };
239
+ const cleanup = () => {
240
+ stdin.removeListener("keypress", onKeypress);
241
+ process.removeListener("SIGINT", onSigint);
242
+ if (stdin.isTTY) {
243
+ stdin.setRawMode(false);
244
+ }
245
+ stdin.pause();
246
+ };
247
+ const finalize = (index, resolve) => {
248
+ const selected = options[index];
249
+ if (!selected) {
250
+ throw new Error("wizard selection invariant violated");
251
+ }
252
+ const summary = `${prompt} ${selected.label}`;
253
+ redrawInteractiveChoice([summary], renderedLineCount);
254
+ renderedLineCount = 1;
255
+ process.stdout.write("\n");
256
+ cleanup();
257
+ resolve(selected.value);
258
+ };
259
+ const cancel = (reject) => {
260
+ cleanup();
261
+ process.stdout.write("\n");
262
+ reject(new Error("prompt cancelled"));
263
+ };
264
+ const onSigint = () => {
265
+ cancel(rejectPromise);
266
+ };
267
+ const onKeypress = (_character, key) => {
268
+ if (key.ctrl && key.name === "c") {
269
+ return cancel(rejectPromise);
270
+ }
271
+ if (key.name === "up") {
272
+ activeIndex = activeIndex === 0 ? options.length - 1 : activeIndex - 1;
273
+ render();
274
+ return;
275
+ }
276
+ if (key.name === "down") {
277
+ activeIndex = activeIndex === options.length - 1 ? 0 : activeIndex + 1;
278
+ render();
279
+ return;
280
+ }
281
+ if (key.name === "return" || key.name === "enter") {
282
+ finalize(activeIndex, resolvePromise);
283
+ return;
284
+ }
285
+ const digit = Number(key.sequence ?? "");
286
+ if (Number.isInteger(digit) && digit >= 1 && digit <= options.length) {
287
+ finalize(digit - 1, resolvePromise);
288
+ }
289
+ };
290
+ let resolvePromise;
291
+ let rejectPromise;
292
+ return await new Promise((resolve, reject) => {
293
+ resolvePromise = resolve;
294
+ rejectPromise = reject;
295
+ emitKeypressEvents(stdin);
296
+ stdin.setRawMode(true);
297
+ stdin.resume();
298
+ stdin.on("keypress", onKeypress);
299
+ process.on("SIGINT", onSigint);
300
+ render();
301
+ });
189
302
  }
190
303
  async function promptChoice(rl, prompt, options, defaultValue) {
191
304
  const defaultIndex = options.findIndex((option) => option.value === defaultValue);
192
305
  if (defaultIndex === -1) {
193
306
  throw new Error(`wizard default option is invalid for ${prompt}`);
194
307
  }
308
+ if (supportsInteractiveChoice()) {
309
+ rl.pause?.();
310
+ try {
311
+ return await promptChoiceInteractive(prompt, options, defaultIndex);
312
+ }
313
+ finally {
314
+ rl.resume?.();
315
+ }
316
+ }
195
317
  while (true) {
196
318
  writeWizardLine(prompt);
197
319
  for (const [index, option] of options.entries()) {
198
320
  const description = option.description ? ` - ${option.description}` : "";
199
321
  writeWizardLine(` ${index + 1}. ${option.label}${description}`);
200
322
  }
201
- const answer = (await rl.question(`Choose [1-${options.length}] (default ${defaultIndex + 1}): `)).trim();
323
+ const answer = (await rl.question(`Select an option (press Enter for ${defaultIndex + 1}): `)).trim();
202
324
  if (answer.length === 0) {
203
325
  return defaultValue;
204
326
  }
@@ -210,11 +332,25 @@ async function promptChoice(rl, prompt, options, defaultValue) {
210
332
  }
211
333
  return selected.value;
212
334
  }
213
- const matched = options.find((option) => option.value.toLowerCase() === answer.toLowerCase());
335
+ const normalizedAnswer = normalizeChoiceText(answer);
336
+ const matched = options.find((option) => {
337
+ if (normalizeChoiceText(option.value) === normalizedAnswer) {
338
+ return true;
339
+ }
340
+ if (normalizeChoiceText(option.label) === normalizedAnswer) {
341
+ return true;
342
+ }
343
+ return (option.aliases ?? []).some((alias) => normalizeChoiceText(alias) === normalizedAnswer);
344
+ });
214
345
  if (matched) {
215
346
  return matched.value;
216
347
  }
217
- writeWizardLine("Please choose one of the numbered options.");
348
+ if (/^(npx|salesprompter)\b/i.test(answer)) {
349
+ writeWizardLine("You are already in the wizard. Pick an option here, or press Ctrl-C to exit.");
350
+ }
351
+ else {
352
+ writeWizardLine("Please choose one of the options above.");
353
+ }
218
354
  writeWizardLine();
219
355
  }
220
356
  }
@@ -265,7 +401,7 @@ async function ensureWizardSession(options) {
265
401
  throw error;
266
402
  }
267
403
  }
268
- writeWizardLine("No active Salesprompter session found. Starting login...");
404
+ writeWizardLine("First, sign in to Salesprompter.");
269
405
  writeWizardLine();
270
406
  const result = await performLogin({
271
407
  apiUrl: options?.apiUrl,
@@ -276,6 +412,90 @@ async function ensureWizardSession(options) {
276
412
  return result.session;
277
413
  }
278
414
  async function runVendorIcpWizard(rl) {
415
+ const startPoint = await promptChoice(rl, "How do you want to start?", [
416
+ {
417
+ value: "custom",
418
+ label: "Create a custom profile",
419
+ description: "Answer a few questions about who you want to sell to",
420
+ aliases: ["custom", "from scratch", "new profile"]
421
+ },
422
+ {
423
+ value: "template",
424
+ label: "Use the Deel template",
425
+ description: "Quick start from the current built-in template",
426
+ aliases: ["template", "deel", "use template"]
427
+ }
428
+ ], "custom");
429
+ writeWizardLine();
430
+ 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)");
438
+ const keywords = await promptText(rl, "Keywords or buying signals (optional, comma-separated)");
439
+ writeWizardLine();
440
+ 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();
446
+ const icp = IcpSchema.parse({
447
+ name: `${productName} ICP`,
448
+ description,
449
+ industries: splitCsv(industries),
450
+ companySizes: splitCsv(companySizes),
451
+ regions: splitCsv(regions),
452
+ countries: splitCsv(countries),
453
+ titles: splitCsv(titles),
454
+ keywords: splitCsv(keywords)
455
+ });
456
+ await writeJsonFile(outPath, icp);
457
+ writeWizardLine(`Created ${icp.name}.`);
458
+ writeWizardLine(`Saved ICP to ${outPath}.`);
459
+ writeWizardLine();
460
+ writeWizardLine("Equivalent raw command:");
461
+ const defineArgs = ["salesprompter", "icp:define", "--name", icp.name];
462
+ if (description.trim().length > 0) {
463
+ defineArgs.push("--description", description);
464
+ }
465
+ if (industries.trim().length > 0) {
466
+ defineArgs.push("--industries", industries);
467
+ }
468
+ if (companySizes.trim().length > 0) {
469
+ defineArgs.push("--company-sizes", companySizes);
470
+ }
471
+ if (regions.trim().length > 0) {
472
+ defineArgs.push("--regions", regions);
473
+ }
474
+ if (countries.trim().length > 0) {
475
+ defineArgs.push("--countries", countries);
476
+ }
477
+ if (titles.trim().length > 0) {
478
+ defineArgs.push("--titles", titles);
479
+ }
480
+ if (keywords.trim().length > 0) {
481
+ defineArgs.push("--keywords", keywords);
482
+ }
483
+ defineArgs.push("--out", outPath);
484
+ writeWizardLine(` ${buildCommandLine(defineArgs)}`);
485
+ 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
+ ])}`);
497
+ return;
498
+ }
279
499
  const vendor = "deel";
280
500
  writeWizardLine("Using the built-in Deel ICP template.");
281
501
  writeWizardLine();
@@ -374,13 +594,15 @@ async function runLeadGenerationWizard(rl) {
374
594
  const source = await promptChoice(rl, "How do you want to generate leads?", [
375
595
  {
376
596
  value: "target-account",
377
- label: "At a specific company",
378
- description: "Example: find people at deel.com"
597
+ label: "At one company",
598
+ description: "Example: find people at acme.com",
599
+ aliases: ["company", "one company", "specific company", "account"]
379
600
  },
380
601
  {
381
602
  value: "vendor-lookup",
382
- label: "From BigQuery",
383
- description: "Use an ICP to search lead data you already have in BigQuery"
603
+ label: "From my BigQuery data",
604
+ description: "Use a saved profile to search the lead data you already have",
605
+ aliases: ["bigquery", "warehouse", "my data", "from bigquery"]
384
606
  }
385
607
  ], "target-account");
386
608
  writeWizardLine();
@@ -391,41 +613,32 @@ async function runLeadGenerationWizard(rl) {
391
613
  await runVendorLookupWizard(rl);
392
614
  }
393
615
  async function runVendorLookupWizard(rl) {
394
- const vendor = "deel";
395
- writeWizardLine("Using the built-in Deel ICP template.");
396
- writeWizardLine();
397
- const market = await promptChoice(rl, "Which market should the BigQuery lookup target?", [
398
- { value: "dach", label: "DACH", description: "Germany, Austria, Switzerland" },
399
- { value: "europe", label: "Europe" },
400
- { value: "global", label: "Global" }
401
- ], "dach");
402
- const limit = z.coerce.number().int().min(1).max(5000).parse(await promptText(rl, "How many rows should the lookup return?", { defaultValue: "100", required: true }));
403
- const execute = await promptYesNo(rl, "Execute the BigQuery lookup now?", false);
404
- writeWizardLine();
405
- const slug = slugify(vendor);
406
- const icpPath = await promptText(rl, "Where should I save the ICP JSON?", {
407
- defaultValue: `./data/${slug}-icp-${market}.json`,
616
+ const icpPath = await promptText(rl, "Where is your ICP JSON file?", {
617
+ defaultValue: "./data/icp.json",
408
618
  required: true
409
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);
622
+ writeWizardLine();
623
+ const icp = await readJsonFile(icpPath, IcpSchema);
624
+ const slug = slugify(icp.name) || "icp";
410
625
  const sqlPath = await promptText(rl, "Where should I save the generated SQL?", {
411
- defaultValue: `./data/${slug}-lookup-${market}.sql`,
626
+ defaultValue: `./data/${slug}-lookup.sql`,
412
627
  required: true
413
628
  });
414
629
  const rawPath = execute
415
630
  ? await promptText(rl, "Where should I save raw BigQuery rows?", {
416
- defaultValue: `./data/${slug}-leads-raw-${market}.json`,
631
+ defaultValue: `./data/${slug}-leads-raw.json`,
417
632
  required: true
418
633
  })
419
634
  : "";
420
635
  const leadPath = execute
421
636
  ? await promptText(rl, "Where should I save normalized leads?", {
422
- defaultValue: `./data/${slug}-leads-${market}.json`,
637
+ defaultValue: `./data/${slug}-leads.json`,
423
638
  required: true
424
639
  })
425
640
  : "";
426
641
  writeWizardLine();
427
- const icp = buildVendorIcp(vendor, market);
428
- await writeJsonFile(icpPath, icp);
429
642
  const sql = buildBigQueryLeadLookupSql(icp, {
430
643
  table: "icpidentifier.SalesGPT.leadPool_new",
431
644
  companyField: "companyName",
@@ -453,15 +666,14 @@ async function runVendorLookupWizard(rl) {
453
666
  await writeJsonFile(leadPath, normalizedLeads);
454
667
  executedRowCount = parsedRows.length;
455
668
  }
456
- writeWizardLine(`Saved vendor ICP to ${icpPath}.`);
669
+ writeWizardLine(`Using ICP from ${icpPath}.`);
457
670
  writeWizardLine(`Saved lookup SQL to ${sqlPath}.`);
458
671
  if (execute) {
459
672
  writeWizardLine(`Saved ${executedRowCount ?? 0} raw rows to ${rawPath}.`);
460
673
  writeWizardLine(`Saved normalized leads to ${leadPath}.`);
461
674
  }
462
675
  writeWizardLine();
463
- writeWizardLine("Equivalent raw commands:");
464
- writeWizardLine(` ${buildCommandLine(["salesprompter", "icp:vendor", "--vendor", vendor, "--market", market, "--out", icpPath])}`);
676
+ writeWizardLine("Equivalent raw command:");
465
677
  const lookupArgs = ["salesprompter", "leads:lookup:bq", "--icp", icpPath, "--limit", String(limit), "--sql-out", sqlPath];
466
678
  if (execute) {
467
679
  lookupArgs.push("--execute", "--out", rawPath, "--lead-out", leadPath);
@@ -469,13 +681,8 @@ async function runVendorLookupWizard(rl) {
469
681
  writeWizardLine(` ${buildCommandLine(lookupArgs)}`);
470
682
  }
471
683
  async function runOutreachSyncWizard(rl) {
472
- const target = await promptChoice(rl, "Which outreach tool do you want to use?", [
473
- {
474
- value: "instantly",
475
- label: "Instantly",
476
- description: "Create campaign leads from a scored leads JSON file"
477
- }
478
- ], "instantly");
684
+ const target = "instantly";
685
+ writeWizardLine("Using Instantly.");
479
686
  writeWizardLine();
480
687
  if (!process.env.INSTANTLY_API_KEY || process.env.INSTANTLY_API_KEY.trim().length === 0) {
481
688
  throw new Error("INSTANTLY_API_KEY is required for the Instantly sync flow.");
@@ -530,8 +737,8 @@ async function runWizard(options) {
530
737
  if (runtimeOutputOptions.json || runtimeOutputOptions.quiet) {
531
738
  throw new Error("wizard does not support --json or --quiet.");
532
739
  }
533
- writeWizardLine("Salesprompter Wizard");
534
- writeWizardLine("Choose the outcome you want. I will ask a few questions and run the matching CLI workflow.");
740
+ writeWizardLine("Salesprompter");
741
+ writeWizardLine("Tell me what you want to do, and I will guide you through it.");
535
742
  writeWizardLine();
536
743
  await ensureWizardSession(options);
537
744
  const rl = createInterface({
@@ -542,18 +749,21 @@ async function runWizard(options) {
542
749
  const flow = await promptChoice(rl, "What do you want help with?", [
543
750
  {
544
751
  value: "vendor-icp",
545
- label: "Define my ICP",
546
- description: "Build the kind of company profile your product sells to"
752
+ label: "Figure out who to target",
753
+ description: "Create an ideal customer profile for your product",
754
+ aliases: ["icp", "ideal customer", "who to target", "targeting", "profile"]
547
755
  },
548
756
  {
549
757
  value: "lead-generation",
550
758
  label: "Generate leads",
551
- description: "Find people at a target company or from your BigQuery data"
759
+ description: "Find people at one company or from your BigQuery data",
760
+ aliases: ["leads", "find leads", "lead generation", "find people"]
552
761
  },
553
762
  {
554
763
  value: "outreach-sync",
555
- label: "Send leads to outreach",
556
- description: "Push scored leads into Instantly"
764
+ label: "Add leads to Instantly",
765
+ description: "Send a scored leads file to an Instantly campaign",
766
+ aliases: ["instantly", "outreach", "send leads", "campaign"]
557
767
  }
558
768
  ], "vendor-icp");
559
769
  writeWizardLine();
@@ -1452,6 +1662,11 @@ async function main() {
1452
1662
  await program.parseAsync(process.argv);
1453
1663
  }
1454
1664
  main().catch((error) => {
1665
+ if (error instanceof Error &&
1666
+ (error.message === "prompt cancelled" || error.message === "readline was closed")) {
1667
+ process.exitCode = 130;
1668
+ return;
1669
+ }
1455
1670
  const cliError = buildCliError(error);
1456
1671
  const space = runtimeOutputOptions.json ? undefined : 2;
1457
1672
  if (runtimeOutputOptions.json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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": {