salesprompter-cli 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/cli.js +413 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,9 +19,16 @@ It is built for two users at the same time:
19
19
 
20
20
  ## Start Here
21
21
 
22
- If someone discovers Salesprompter from a vague prompt, the first thing they need is the shortest working path.
22
+ If someone discovers Salesprompter from a vague prompt, give them the shortest working path for their context.
23
23
 
24
24
  ```bash
25
+ ## Human-friendly guided path
26
+ npx -y salesprompter-cli@latest
27
+
28
+ ## Explicit guided path
29
+ npx -y salesprompter-cli@latest wizard
30
+
31
+ ## Raw command surface for agents and scripts
25
32
  npx salesprompter-cli@latest --help
26
33
  ```
27
34
 
@@ -29,9 +36,13 @@ Or install it globally:
29
36
 
30
37
  ```bash
31
38
  npm install -g salesprompter-cli
39
+ salesprompter
40
+ salesprompter wizard
32
41
  salesprompter --help
33
42
  ```
34
43
 
44
+ Bare `salesprompter` now opens a guided wizard in an interactive terminal. Keep using explicit subcommands for agents, CI, and copy-paste docs.
45
+
35
46
  ## Prompt To Command
36
47
 
37
48
  If the user says something like "I need to determine the ICP of deel.com", there are two different meanings.
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 { createInterface } from "node:readline/promises";
4
5
  import { Command } from "commander";
5
6
  import { z } from "zod";
6
7
  import { clearAuthSession, loginWithBrowserConnect, loginWithDeviceFlow, loginWithToken, requireAuthSession, shouldBypassAuth, verifySession } from "./auth.js";
@@ -89,6 +90,386 @@ function writeBrowserLoginInstructions(info) {
89
90
  process.stderr.write("Opened the browser for you.\n");
90
91
  }
91
92
  }
93
+ async function performLogin(options) {
94
+ const token = typeof options.token === "string" ? options.token.trim() : "";
95
+ if (token.length > 0) {
96
+ const session = await loginWithToken(token, options.apiUrl);
97
+ return {
98
+ method: "token",
99
+ session
100
+ };
101
+ }
102
+ const startedAt = new Date().toISOString();
103
+ try {
104
+ const session = await loginWithBrowserConnect({
105
+ apiBaseUrl: options.apiUrl,
106
+ timeoutSeconds: options.timeoutSeconds,
107
+ onConnectStart: writeBrowserLoginInstructions
108
+ });
109
+ return {
110
+ method: "browser",
111
+ startedAt,
112
+ session
113
+ };
114
+ }
115
+ catch (error) {
116
+ const message = error instanceof Error ? error.message : String(error);
117
+ if (!message.includes("timed out waiting for browser login") &&
118
+ !message.includes("invalid localhost callback response") &&
119
+ !message.includes("request failed")) {
120
+ throw error;
121
+ }
122
+ if (!/request failed \((401|403|404|405|500|501|502|503|504)\)/.test(message)) {
123
+ throw error;
124
+ }
125
+ }
126
+ const result = await loginWithDeviceFlow({
127
+ apiBaseUrl: options.apiUrl,
128
+ timeoutSeconds: options.timeoutSeconds,
129
+ onDeviceStart: writeDeviceLoginInstructions
130
+ });
131
+ return {
132
+ method: "device",
133
+ startedAt,
134
+ verificationUrl: result.verificationUrl,
135
+ userCode: result.userCode,
136
+ session: result.session
137
+ };
138
+ }
139
+ function shellQuote(value) {
140
+ if (/^[A-Za-z0-9_./:@=-]+$/.test(value)) {
141
+ return value;
142
+ }
143
+ return `'${value.replaceAll("'", "'\\''")}'`;
144
+ }
145
+ function buildCommandLine(args) {
146
+ return args.map((arg) => shellQuote(arg)).join(" ");
147
+ }
148
+ function slugify(value) {
149
+ return value
150
+ .trim()
151
+ .toLowerCase()
152
+ .replace(/^https?:\/\//, "")
153
+ .replace(/^www\./, "")
154
+ .split("/")[0]
155
+ .replace(/[^a-z0-9]+/g, "-")
156
+ .replace(/^-+|-+$/g, "")
157
+ .replace(/-{2,}/g, "-");
158
+ }
159
+ function normalizeDomainInput(value) {
160
+ return value
161
+ .trim()
162
+ .toLowerCase()
163
+ .replace(/^https?:\/\//, "")
164
+ .replace(/^www\./, "")
165
+ .split("/")[0] ?? "";
166
+ }
167
+ function deriveCompanyNameFromDomain(domain) {
168
+ const hostname = normalizeDomainInput(domain).split(".")[0] ?? domain;
169
+ return hostname
170
+ .split(/[-_]/)
171
+ .filter((part) => part.length > 0)
172
+ .map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
173
+ .join(" ");
174
+ }
175
+ function writeWizardLine(message = "") {
176
+ process.stdout.write(`${message}\n`);
177
+ }
178
+ async function promptChoice(rl, prompt, options, defaultValue) {
179
+ const defaultIndex = options.findIndex((option) => option.value === defaultValue);
180
+ if (defaultIndex === -1) {
181
+ throw new Error(`wizard default option is invalid for ${prompt}`);
182
+ }
183
+ while (true) {
184
+ writeWizardLine(prompt);
185
+ for (const [index, option] of options.entries()) {
186
+ const description = option.description ? ` - ${option.description}` : "";
187
+ writeWizardLine(` ${index + 1}. ${option.label}${description}`);
188
+ }
189
+ const answer = (await rl.question(`Choose [1-${options.length}] (default ${defaultIndex + 1}): `)).trim();
190
+ if (answer.length === 0) {
191
+ return defaultValue;
192
+ }
193
+ const numeric = Number(answer);
194
+ if (Number.isInteger(numeric) && numeric >= 1 && numeric <= options.length) {
195
+ const selected = options[numeric - 1];
196
+ if (!selected) {
197
+ throw new Error("wizard selection invariant violated");
198
+ }
199
+ return selected.value;
200
+ }
201
+ const matched = options.find((option) => option.value.toLowerCase() === answer.toLowerCase());
202
+ if (matched) {
203
+ return matched.value;
204
+ }
205
+ writeWizardLine("Please choose one of the numbered options.");
206
+ writeWizardLine();
207
+ }
208
+ }
209
+ async function promptText(rl, prompt, options = {}) {
210
+ while (true) {
211
+ const suffix = options.defaultValue !== undefined ? ` [${options.defaultValue}]` : "";
212
+ const answer = (await rl.question(`${prompt}${suffix}: `)).trim();
213
+ if (answer.length > 0) {
214
+ return answer;
215
+ }
216
+ if (options.defaultValue !== undefined) {
217
+ return options.defaultValue;
218
+ }
219
+ if (!options.required) {
220
+ return "";
221
+ }
222
+ writeWizardLine("This field is required.");
223
+ }
224
+ }
225
+ async function promptYesNo(rl, prompt, defaultValue) {
226
+ while (true) {
227
+ const answer = (await rl.question(`${prompt} [${defaultValue ? "Y/n" : "y/N"}]: `)).trim().toLowerCase();
228
+ if (answer.length === 0) {
229
+ return defaultValue;
230
+ }
231
+ if (["y", "yes"].includes(answer)) {
232
+ return true;
233
+ }
234
+ if (["n", "no"].includes(answer)) {
235
+ return false;
236
+ }
237
+ writeWizardLine("Please answer yes or no.");
238
+ }
239
+ }
240
+ async function ensureWizardSession(options) {
241
+ if (shouldBypassAuth()) {
242
+ return null;
243
+ }
244
+ try {
245
+ const session = await requireAuthSession();
246
+ writeWizardLine(`Signed in as ${session.user.email}.`);
247
+ writeWizardLine();
248
+ return session;
249
+ }
250
+ catch (error) {
251
+ const message = error instanceof Error ? error.message : String(error);
252
+ if (!message.includes("not logged in") && !message.includes("session expired")) {
253
+ throw error;
254
+ }
255
+ }
256
+ writeWizardLine("No active Salesprompter session found. Starting login...");
257
+ writeWizardLine();
258
+ const result = await performLogin({
259
+ apiUrl: options?.apiUrl,
260
+ timeoutSeconds: options?.timeoutSeconds ?? 180
261
+ });
262
+ writeWizardLine(`Signed in as ${result.session.user.email}.`);
263
+ writeWizardLine();
264
+ return result.session;
265
+ }
266
+ async function runVendorIcpWizard(rl) {
267
+ const vendor = await promptChoice(rl, "Which vendor template do you want?", [{ value: "deel", label: "Deel", description: "Global payroll, EOR, and contractor workflows" }], "deel");
268
+ writeWizardLine();
269
+ const market = await promptChoice(rl, "Which market should this ICP target?", [
270
+ { value: "dach", label: "DACH", description: "Germany, Austria, Switzerland" },
271
+ { value: "europe", label: "Europe" },
272
+ { value: "global", label: "Global" }
273
+ ], "dach");
274
+ writeWizardLine();
275
+ const outPath = await promptText(rl, "Where should I save the ICP JSON?", {
276
+ defaultValue: `./data/${slugify(vendor)}-icp-${market}.json`,
277
+ required: true
278
+ });
279
+ writeWizardLine();
280
+ const icp = buildVendorIcp(vendor, market);
281
+ await writeJsonFile(outPath, icp);
282
+ writeWizardLine(`Created ${icp.name}.`);
283
+ writeWizardLine(`Saved ICP to ${outPath}.`);
284
+ writeWizardLine();
285
+ writeWizardLine("Equivalent raw command:");
286
+ writeWizardLine(` ${buildCommandLine(["salesprompter", "icp:vendor", "--vendor", vendor, "--market", market, "--out", outPath])}`);
287
+ writeWizardLine();
288
+ writeWizardLine("Next suggested command:");
289
+ writeWizardLine(` ${buildCommandLine([
290
+ "salesprompter",
291
+ "leads:lookup:bq",
292
+ "--icp",
293
+ outPath,
294
+ "--limit",
295
+ "100",
296
+ "--lead-out",
297
+ `./data/${slugify(vendor)}-leads.json`
298
+ ])}`);
299
+ }
300
+ async function runTargetAccountWizard(rl) {
301
+ const domain = normalizeDomainInput(await promptText(rl, "Which company domain are you targeting?", { required: true }));
302
+ writeWizardLine();
303
+ const companyName = await promptText(rl, "Company name override (optional)");
304
+ const displayName = companyName || deriveCompanyNameFromDomain(domain);
305
+ const leadCount = z.coerce.number().int().min(1).max(1000).parse(await promptText(rl, "How many leads should I generate?", { defaultValue: "5", required: true }));
306
+ const region = await promptText(rl, "Primary region hint", { defaultValue: "Global", required: true });
307
+ const industries = await promptText(rl, "Industry hint (optional, comma-separated)");
308
+ const titles = await promptText(rl, "Target titles (optional, comma-separated)");
309
+ writeWizardLine();
310
+ const slug = slugify(domain);
311
+ const icpPath = await promptText(rl, "Where should I save the ad-hoc ICP JSON?", {
312
+ defaultValue: `./data/${slug}-target-icp.json`,
313
+ required: true
314
+ });
315
+ const leadsPath = await promptText(rl, "Where should I save the generated leads JSON?", {
316
+ defaultValue: `./data/${slug}-leads.json`,
317
+ required: true
318
+ });
319
+ writeWizardLine();
320
+ const icp = IcpSchema.parse({
321
+ name: `${displayName} target account`,
322
+ regions: region.length > 0 ? [region] : [],
323
+ industries: splitCsv(industries),
324
+ titles: splitCsv(titles)
325
+ });
326
+ await writeJsonFile(icpPath, icp);
327
+ const result = await leadProvider.generateLeads(icp, leadCount, {
328
+ companyDomain: domain,
329
+ companyName: companyName || undefined
330
+ });
331
+ await writeJsonFile(leadsPath, result.leads);
332
+ writeWizardLine(`Generated ${result.leads.length} leads for ${result.account.companyName} (${result.account.domain}).`);
333
+ writeWizardLine(`Saved ad-hoc ICP to ${icpPath}.`);
334
+ writeWizardLine(`Saved leads to ${leadsPath}.`);
335
+ if (result.warnings.length > 0) {
336
+ writeWizardLine();
337
+ writeWizardLine(`Warning: ${result.warnings.join(" ")}`);
338
+ }
339
+ writeWizardLine();
340
+ writeWizardLine("Equivalent raw commands:");
341
+ const defineArgs = ["salesprompter", "icp:define", "--name", icp.name];
342
+ if (region.length > 0) {
343
+ defineArgs.push("--regions", region);
344
+ }
345
+ if (industries.trim().length > 0) {
346
+ defineArgs.push("--industries", industries);
347
+ }
348
+ if (titles.trim().length > 0) {
349
+ defineArgs.push("--titles", titles);
350
+ }
351
+ defineArgs.push("--out", icpPath);
352
+ writeWizardLine(` ${buildCommandLine(defineArgs)}`);
353
+ const leadArgs = ["salesprompter", "leads:generate", "--icp", icpPath, "--count", String(leadCount), "--domain", domain];
354
+ if (companyName.trim().length > 0) {
355
+ leadArgs.push("--company-name", companyName);
356
+ }
357
+ leadArgs.push("--out", leadsPath);
358
+ writeWizardLine(` ${buildCommandLine(leadArgs)}`);
359
+ }
360
+ async function runVendorLookupWizard(rl) {
361
+ const vendor = await promptChoice(rl, "Which vendor template do you want to use?", [{ value: "deel", label: "Deel", description: "Global payroll, EOR, and contractor workflows" }], "deel");
362
+ writeWizardLine();
363
+ const market = await promptChoice(rl, "Which market should the BigQuery lookup target?", [
364
+ { value: "dach", label: "DACH", description: "Germany, Austria, Switzerland" },
365
+ { value: "europe", label: "Europe" },
366
+ { value: "global", label: "Global" }
367
+ ], "dach");
368
+ 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 }));
369
+ const execute = await promptYesNo(rl, "Execute the BigQuery lookup now?", false);
370
+ writeWizardLine();
371
+ const slug = slugify(vendor);
372
+ const icpPath = await promptText(rl, "Where should I save the ICP JSON?", {
373
+ defaultValue: `./data/${slug}-icp-${market}.json`,
374
+ required: true
375
+ });
376
+ const sqlPath = await promptText(rl, "Where should I save the generated SQL?", {
377
+ defaultValue: `./data/${slug}-lookup-${market}.sql`,
378
+ required: true
379
+ });
380
+ const rawPath = execute
381
+ ? await promptText(rl, "Where should I save raw BigQuery rows?", {
382
+ defaultValue: `./data/${slug}-leads-raw-${market}.json`,
383
+ required: true
384
+ })
385
+ : "";
386
+ const leadPath = execute
387
+ ? await promptText(rl, "Where should I save normalized leads?", {
388
+ defaultValue: `./data/${slug}-leads-${market}.json`,
389
+ required: true
390
+ })
391
+ : "";
392
+ writeWizardLine();
393
+ const icp = buildVendorIcp(vendor, market);
394
+ await writeJsonFile(icpPath, icp);
395
+ const sql = buildBigQueryLeadLookupSql(icp, {
396
+ table: "icpidentifier.SalesGPT.leadPool_new",
397
+ companyField: "companyName",
398
+ domainField: "domain",
399
+ regionField: undefined,
400
+ keywordFields: splitCsv("companyName,industry,description,tagline,specialties"),
401
+ titleField: "jobTitle",
402
+ industryField: "industry",
403
+ companySizeField: "companySize",
404
+ countryField: "company_countryCode",
405
+ firstNameField: "firstName",
406
+ lastNameField: "lastName",
407
+ emailField: "email",
408
+ limit,
409
+ additionalWhere: undefined,
410
+ useSalesprompterGuards: true
411
+ });
412
+ await writeTextFile(sqlPath, `${sql}\n`);
413
+ let executedRowCount = null;
414
+ if (execute) {
415
+ const rows = await runBigQueryQuery(sql, { maxRows: limit });
416
+ const parsedRows = z.array(z.record(z.string(), z.unknown())).parse(rows);
417
+ await writeJsonFile(rawPath, parsedRows);
418
+ const normalizedLeads = normalizeBigQueryLeadRows(parsedRows);
419
+ await writeJsonFile(leadPath, normalizedLeads);
420
+ executedRowCount = parsedRows.length;
421
+ }
422
+ writeWizardLine(`Saved vendor ICP to ${icpPath}.`);
423
+ writeWizardLine(`Saved lookup SQL to ${sqlPath}.`);
424
+ if (execute) {
425
+ writeWizardLine(`Saved ${executedRowCount ?? 0} raw rows to ${rawPath}.`);
426
+ writeWizardLine(`Saved normalized leads to ${leadPath}.`);
427
+ }
428
+ writeWizardLine();
429
+ writeWizardLine("Equivalent raw commands:");
430
+ writeWizardLine(` ${buildCommandLine(["salesprompter", "icp:vendor", "--vendor", vendor, "--market", market, "--out", icpPath])}`);
431
+ const lookupArgs = ["salesprompter", "leads:lookup:bq", "--icp", icpPath, "--limit", String(limit), "--sql-out", sqlPath];
432
+ if (execute) {
433
+ lookupArgs.push("--execute", "--out", rawPath, "--lead-out", leadPath);
434
+ }
435
+ writeWizardLine(` ${buildCommandLine(lookupArgs)}`);
436
+ }
437
+ async function runWizard(options) {
438
+ if (runtimeOutputOptions.json || runtimeOutputOptions.quiet) {
439
+ throw new Error("wizard does not support --json or --quiet.");
440
+ }
441
+ writeWizardLine("Salesprompter Wizard");
442
+ writeWizardLine("Choose a workflow and I will map it to the underlying CLI commands.");
443
+ writeWizardLine();
444
+ await ensureWizardSession(options);
445
+ const rl = createInterface({
446
+ input: process.stdin,
447
+ output: process.stdout
448
+ });
449
+ try {
450
+ const flow = await promptChoice(rl, "What do you want to do?", [
451
+ { value: "vendor-icp", label: "Create a vendor ICP template", description: "Best for prompts like \"determine Deel's ICP\"" },
452
+ { value: "target-account", label: "Generate leads for a target company", description: "Best for prompts like \"find contacts at deel.com\"" },
453
+ { value: "vendor-lookup", label: "Create a vendor ICP and prepare a BigQuery lookup", description: "Best for warehouse-backed lead discovery" }
454
+ ], "vendor-icp");
455
+ writeWizardLine();
456
+ if (flow === "vendor-icp") {
457
+ await runVendorIcpWizard(rl);
458
+ return;
459
+ }
460
+ if (flow === "target-account") {
461
+ await runTargetAccountWizard(rl);
462
+ return;
463
+ }
464
+ await runVendorLookupWizard(rl);
465
+ }
466
+ finally {
467
+ rl.close();
468
+ }
469
+ }
470
+ function shouldAutoRunWizard(argv) {
471
+ return argv.length <= 2 && Boolean(process.stdin.isTTY && process.stdout.isTTY);
472
+ }
92
473
  function buildCliError(error) {
93
474
  if (error instanceof z.ZodError) {
94
475
  return {
@@ -234,53 +615,15 @@ program
234
615
  .option("--timeout-seconds <number>", "Device flow wait timeout in seconds", "180")
235
616
  .action(async (options) => {
236
617
  const timeoutSeconds = z.coerce.number().int().min(30).max(1800).parse(options.timeoutSeconds);
237
- const token = typeof options.token === "string" ? options.token.trim() : "";
238
- if (token.length > 0) {
239
- const session = await loginWithToken(token, options.apiUrl);
240
- printOutput({
241
- status: "ok",
242
- method: "token",
243
- apiBaseUrl: session.apiBaseUrl,
244
- user: session.user,
245
- expiresAt: session.expiresAt ?? null
246
- });
247
- return;
248
- }
249
- const startedAt = new Date().toISOString();
250
- try {
251
- const session = await loginWithBrowserConnect({
252
- apiBaseUrl: options.apiUrl,
253
- timeoutSeconds,
254
- onConnectStart: writeBrowserLoginInstructions
255
- });
256
- printOutput({
257
- status: "ok",
258
- method: "browser",
259
- startedAt,
260
- apiBaseUrl: session.apiBaseUrl,
261
- user: session.user,
262
- expiresAt: session.expiresAt ?? null
263
- });
264
- return;
265
- }
266
- catch (error) {
267
- const message = error instanceof Error ? error.message : String(error);
268
- if (!message.includes("timed out waiting for browser login") && !message.includes("invalid localhost callback response") && !message.includes("request failed")) {
269
- throw error;
270
- }
271
- if (!/request failed \((401|403|404|405|500|501|502|503|504)\)/.test(message)) {
272
- throw error;
273
- }
274
- }
275
- const result = await loginWithDeviceFlow({
276
- apiBaseUrl: options.apiUrl,
277
- timeoutSeconds,
278
- onDeviceStart: writeDeviceLoginInstructions
618
+ const result = await performLogin({
619
+ token: options.token,
620
+ apiUrl: options.apiUrl,
621
+ timeoutSeconds
279
622
  });
280
623
  printOutput({
281
624
  status: "ok",
282
- method: "device",
283
- startedAt,
625
+ method: result.method,
626
+ startedAt: result.startedAt,
284
627
  verificationUrl: result.verificationUrl,
285
628
  userCode: result.userCode,
286
629
  apiBaseUrl: result.session.apiBaseUrl,
@@ -288,6 +631,19 @@ program
288
631
  expiresAt: result.session.expiresAt ?? null
289
632
  });
290
633
  });
634
+ program
635
+ .command("wizard")
636
+ .alias("start")
637
+ .description("Run an interactive guided workflow for common Salesprompter tasks.")
638
+ .option("--api-url <url>", "Salesprompter API base URL, defaults to SALESPROMPTER_API_BASE_URL or salesprompter.ai")
639
+ .option("--timeout-seconds <number>", "Auth login timeout in seconds when the wizard needs to sign in", "180")
640
+ .action(async (options) => {
641
+ const timeoutSeconds = z.coerce.number().int().min(30).max(1800).parse(options.timeoutSeconds);
642
+ await runWizard({
643
+ apiUrl: options.apiUrl,
644
+ timeoutSeconds
645
+ });
646
+ });
291
647
  program
292
648
  .command("auth:whoami")
293
649
  .description("Show current authenticated user and session status.")
@@ -313,7 +669,7 @@ program
313
669
  program.hook("preAction", async (_thisCommand, actionCommand) => {
314
670
  applyGlobalOutputOptions(actionCommand);
315
671
  const commandName = actionCommand.name();
316
- if (commandName.startsWith("auth:")) {
672
+ if (commandName.startsWith("auth:") || commandName === "wizard") {
317
673
  return;
318
674
  }
319
675
  if (shouldBypassAuth()) {
@@ -980,7 +1336,18 @@ program
980
1336
  execution
981
1337
  });
982
1338
  });
983
- program.parseAsync(process.argv).catch((error) => {
1339
+ async function main() {
1340
+ if (shouldAutoRunWizard(process.argv)) {
1341
+ await runWizard();
1342
+ return;
1343
+ }
1344
+ if (process.argv.length <= 2) {
1345
+ program.outputHelp();
1346
+ return;
1347
+ }
1348
+ await program.parseAsync(process.argv);
1349
+ }
1350
+ main().catch((error) => {
984
1351
  const cliError = buildCliError(error);
985
1352
  const space = runtimeOutputOptions.json ? undefined : 2;
986
1353
  if (runtimeOutputOptions.json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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": {