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.
- package/dist/cli.js +264 -107
- 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
|
|
470
|
+
const startPoint = await promptChoice(rl, "How do you want to build your ICP?", [
|
|
305
471
|
{
|
|
306
472
|
value: "custom",
|
|
307
|
-
label: "
|
|
308
|
-
description: "Answer a few questions about
|
|
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
|
|
321
|
-
const description = await promptText(rl, "
|
|
322
|
-
const industries = await promptText(rl, "
|
|
323
|
-
const companySizes = await promptText(rl, "
|
|
324
|
-
const regions = await promptText(rl, "
|
|
325
|
-
const countries = await promptText(rl, "
|
|
326
|
-
const titles = await promptText(rl, "
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
428
|
-
const region = await promptText(rl, "
|
|
429
|
-
const industries = await promptText(rl, "
|
|
430
|
-
const titles = await promptText(rl, "
|
|
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 =
|
|
434
|
-
|
|
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
|
|
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
|
|
493
|
-
description: "
|
|
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 =
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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 =
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
685
|
+
normalizedLeads = normalizeBigQueryLeadRows(parsedRows);
|
|
555
686
|
await writeJsonFile(leadPath, normalizedLeads);
|
|
556
687
|
executedRowCount = parsedRows.length;
|
|
557
688
|
}
|
|
558
|
-
writeWizardLine(`
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
|
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, "
|
|
589
|
-
const allowDuplicates = await promptYesNo(rl, "
|
|
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 ${
|
|
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("
|
|
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: "
|
|
642
|
-
description: "
|
|
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
|
|
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: "
|
|
654
|
-
description: "
|
|
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