salesprompter-cli 0.1.6 → 0.1.8

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/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,14 @@ 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
+ If your Salesprompter user belongs to multiple organizations, browser login asks which organization the CLI session should use.
46
+
35
47
  ## Prompt To Command
36
48
 
37
49
  If the user says something like "I need to determine the ICP of deel.com", there are two different meanings.
@@ -68,7 +80,9 @@ salesprompter --json leads:lookup:bq --icp ./data/deel-icp.json --limit 100 --le
68
80
 
69
81
  ## Documentation
70
82
 
71
- This repository now includes a Mintlify docs site for the wider Salesprompter universe, including the app contract, CLI surface, Chrome extension contract, and the main warehouse-backed workflows.
83
+ This repository now includes the public Salesprompter docs site for the wider Salesprompter universe, including the app contract, CLI surface, Chrome extension contract, and the main warehouse-backed workflows.
84
+
85
+ - Live docs: `https://salesprompter-cli.vercel.app`
72
86
 
73
87
  - Docs home: `./index.mdx`
74
88
  - Quickstart: `./quickstart.mdx`
@@ -87,6 +101,12 @@ Run the docs locally with:
87
101
  npm run docs:dev
88
102
  ```
89
103
 
104
+ Build the deployable static docs site with:
105
+
106
+ ```bash
107
+ npm run build:docs:site
108
+ ```
109
+
90
110
  ## Integration Contract
91
111
 
92
112
  This CLI is not a standalone toy. It is a production integration surface for the Salesprompter app.
@@ -127,6 +147,8 @@ salesprompter auth:whoami --verify
127
147
  salesprompter auth:logout
128
148
  ```
129
149
 
150
+ If your user belongs to multiple organizations, the browser flow asks you to choose the organization for that CLI session before returning to the terminal.
151
+
130
152
  Environment variables:
131
153
 
132
154
  - `SALESPROMPTER_API_BASE_URL`: override backend URL (default `https://salesprompter.ai`)
package/dist/auth.js CHANGED
@@ -11,7 +11,10 @@ const DEFAULT_DEVICE_TIMEOUT_SECONDS = 180;
11
11
  const UserSchema = z.object({
12
12
  id: z.string().min(1),
13
13
  email: z.string().email(),
14
- name: z.string().min(1).optional()
14
+ name: z.string().min(1).optional(),
15
+ orgId: z.string().min(1).optional(),
16
+ orgName: z.string().min(1).optional(),
17
+ orgSlug: z.string().min(1).optional()
15
18
  });
16
19
  const AuthSessionSchema = z.object({
17
20
  accessToken: z.string().min(1),
@@ -57,6 +60,9 @@ const WhoAmIResponseSchema = z
57
60
  id: z.string().min(1),
58
61
  email: z.string().email(),
59
62
  name: z.string().min(1).optional(),
63
+ orgId: z.string().min(1).optional(),
64
+ orgName: z.string().min(1).optional(),
65
+ orgSlug: z.string().min(1).optional(),
60
66
  expiresAt: z.string().datetime().optional()
61
67
  }),
62
68
  z.object({
@@ -77,7 +83,10 @@ const WhoAmIResponseSchema = z
77
83
  user: {
78
84
  id: value.id,
79
85
  email: value.email,
80
- name: value.name
86
+ name: value.name,
87
+ orgId: value.orgId,
88
+ orgName: value.orgName,
89
+ orgSlug: value.orgSlug
81
90
  },
82
91
  expiresAt: value.expiresAt
83
92
  };
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,398 @@ 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
+ function getOrgLabel(session) {
179
+ return session.user.orgName ?? session.user.orgSlug ?? session.user.orgId ?? null;
180
+ }
181
+ function writeSessionSummary(session) {
182
+ const orgLabel = getOrgLabel(session);
183
+ if (orgLabel) {
184
+ writeWizardLine(`Signed in as ${session.user.email} for ${orgLabel}.`);
185
+ return;
186
+ }
187
+ writeWizardLine(`Signed in as ${session.user.email}.`);
188
+ writeWizardLine("No organization is attached to this CLI session.");
189
+ }
190
+ async function promptChoice(rl, prompt, options, defaultValue) {
191
+ const defaultIndex = options.findIndex((option) => option.value === defaultValue);
192
+ if (defaultIndex === -1) {
193
+ throw new Error(`wizard default option is invalid for ${prompt}`);
194
+ }
195
+ while (true) {
196
+ writeWizardLine(prompt);
197
+ for (const [index, option] of options.entries()) {
198
+ const description = option.description ? ` - ${option.description}` : "";
199
+ writeWizardLine(` ${index + 1}. ${option.label}${description}`);
200
+ }
201
+ const answer = (await rl.question(`Choose [1-${options.length}] (default ${defaultIndex + 1}): `)).trim();
202
+ if (answer.length === 0) {
203
+ return defaultValue;
204
+ }
205
+ const numeric = Number(answer);
206
+ if (Number.isInteger(numeric) && numeric >= 1 && numeric <= options.length) {
207
+ const selected = options[numeric - 1];
208
+ if (!selected) {
209
+ throw new Error("wizard selection invariant violated");
210
+ }
211
+ return selected.value;
212
+ }
213
+ const matched = options.find((option) => option.value.toLowerCase() === answer.toLowerCase());
214
+ if (matched) {
215
+ return matched.value;
216
+ }
217
+ writeWizardLine("Please choose one of the numbered options.");
218
+ writeWizardLine();
219
+ }
220
+ }
221
+ async function promptText(rl, prompt, options = {}) {
222
+ while (true) {
223
+ const suffix = options.defaultValue !== undefined ? ` [${options.defaultValue}]` : "";
224
+ const answer = (await rl.question(`${prompt}${suffix}: `)).trim();
225
+ if (answer.length > 0) {
226
+ return answer;
227
+ }
228
+ if (options.defaultValue !== undefined) {
229
+ return options.defaultValue;
230
+ }
231
+ if (!options.required) {
232
+ return "";
233
+ }
234
+ writeWizardLine("This field is required.");
235
+ }
236
+ }
237
+ async function promptYesNo(rl, prompt, defaultValue) {
238
+ while (true) {
239
+ const answer = (await rl.question(`${prompt} [${defaultValue ? "Y/n" : "y/N"}]: `)).trim().toLowerCase();
240
+ if (answer.length === 0) {
241
+ return defaultValue;
242
+ }
243
+ if (["y", "yes"].includes(answer)) {
244
+ return true;
245
+ }
246
+ if (["n", "no"].includes(answer)) {
247
+ return false;
248
+ }
249
+ writeWizardLine("Please answer yes or no.");
250
+ }
251
+ }
252
+ async function ensureWizardSession(options) {
253
+ if (shouldBypassAuth()) {
254
+ return null;
255
+ }
256
+ try {
257
+ const session = await requireAuthSession();
258
+ writeSessionSummary(session);
259
+ writeWizardLine();
260
+ return session;
261
+ }
262
+ catch (error) {
263
+ const message = error instanceof Error ? error.message : String(error);
264
+ if (!message.includes("not logged in") && !message.includes("session expired")) {
265
+ throw error;
266
+ }
267
+ }
268
+ writeWizardLine("No active Salesprompter session found. Starting login...");
269
+ writeWizardLine();
270
+ const result = await performLogin({
271
+ apiUrl: options?.apiUrl,
272
+ timeoutSeconds: options?.timeoutSeconds ?? 180
273
+ });
274
+ writeSessionSummary(result.session);
275
+ writeWizardLine();
276
+ return result.session;
277
+ }
278
+ async function runVendorIcpWizard(rl) {
279
+ const vendor = await promptChoice(rl, "Which product are you selling?", [{ value: "deel", label: "Deel", description: "Use Deel's built-in ICP template" }], "deel");
280
+ writeWizardLine();
281
+ const market = await promptChoice(rl, "Which market do you want to target?", [
282
+ { value: "dach", label: "DACH", description: "Germany, Austria, Switzerland" },
283
+ { value: "europe", label: "Europe" },
284
+ { value: "global", label: "Global" }
285
+ ], "dach");
286
+ writeWizardLine();
287
+ const outPath = await promptText(rl, "Where should I save the ICP JSON?", {
288
+ defaultValue: `./data/${slugify(vendor)}-icp-${market}.json`,
289
+ required: true
290
+ });
291
+ writeWizardLine();
292
+ const icp = buildVendorIcp(vendor, market);
293
+ await writeJsonFile(outPath, icp);
294
+ writeWizardLine(`Created ${icp.name}.`);
295
+ writeWizardLine(`Saved ICP to ${outPath}.`);
296
+ writeWizardLine();
297
+ writeWizardLine("Equivalent raw command:");
298
+ writeWizardLine(` ${buildCommandLine(["salesprompter", "icp:vendor", "--vendor", vendor, "--market", market, "--out", outPath])}`);
299
+ writeWizardLine();
300
+ writeWizardLine("Next suggested command:");
301
+ writeWizardLine(` ${buildCommandLine([
302
+ "salesprompter",
303
+ "leads:lookup:bq",
304
+ "--icp",
305
+ outPath,
306
+ "--limit",
307
+ "100",
308
+ "--lead-out",
309
+ `./data/${slugify(vendor)}-leads.json`
310
+ ])}`);
311
+ }
312
+ async function runTargetAccountWizard(rl) {
313
+ const domain = normalizeDomainInput(await promptText(rl, "Which company are you targeting? Enter the domain", { required: true }));
314
+ writeWizardLine();
315
+ const companyName = await promptText(rl, "Company name override (optional)");
316
+ const displayName = companyName || deriveCompanyNameFromDomain(domain);
317
+ const leadCount = z.coerce.number().int().min(1).max(1000).parse(await promptText(rl, "How many leads should I generate?", { defaultValue: "5", required: true }));
318
+ const region = await promptText(rl, "Primary region hint", { defaultValue: "Global", required: true });
319
+ const industries = await promptText(rl, "Industry hint (optional, comma-separated)");
320
+ const titles = await promptText(rl, "Target titles (optional, comma-separated)");
321
+ writeWizardLine();
322
+ const slug = slugify(domain);
323
+ const icpPath = await promptText(rl, "Where should I save the ad-hoc ICP JSON?", {
324
+ defaultValue: `./data/${slug}-target-icp.json`,
325
+ required: true
326
+ });
327
+ const leadsPath = await promptText(rl, "Where should I save the generated leads JSON?", {
328
+ defaultValue: `./data/${slug}-leads.json`,
329
+ required: true
330
+ });
331
+ writeWizardLine();
332
+ const icp = IcpSchema.parse({
333
+ name: `${displayName} target account`,
334
+ regions: region.length > 0 ? [region] : [],
335
+ industries: splitCsv(industries),
336
+ titles: splitCsv(titles)
337
+ });
338
+ await writeJsonFile(icpPath, icp);
339
+ const result = await leadProvider.generateLeads(icp, leadCount, {
340
+ companyDomain: domain,
341
+ companyName: companyName || undefined
342
+ });
343
+ await writeJsonFile(leadsPath, result.leads);
344
+ writeWizardLine(`Generated ${result.leads.length} leads for ${result.account.companyName} (${result.account.domain}).`);
345
+ writeWizardLine(`Saved ad-hoc ICP to ${icpPath}.`);
346
+ writeWizardLine(`Saved leads to ${leadsPath}.`);
347
+ if (result.warnings.length > 0) {
348
+ writeWizardLine();
349
+ writeWizardLine(`Warning: ${result.warnings.join(" ")}`);
350
+ }
351
+ writeWizardLine();
352
+ writeWizardLine("Equivalent raw commands:");
353
+ const defineArgs = ["salesprompter", "icp:define", "--name", icp.name];
354
+ if (region.length > 0) {
355
+ defineArgs.push("--regions", region);
356
+ }
357
+ if (industries.trim().length > 0) {
358
+ defineArgs.push("--industries", industries);
359
+ }
360
+ if (titles.trim().length > 0) {
361
+ defineArgs.push("--titles", titles);
362
+ }
363
+ defineArgs.push("--out", icpPath);
364
+ writeWizardLine(` ${buildCommandLine(defineArgs)}`);
365
+ const leadArgs = ["salesprompter", "leads:generate", "--icp", icpPath, "--count", String(leadCount), "--domain", domain];
366
+ if (companyName.trim().length > 0) {
367
+ leadArgs.push("--company-name", companyName);
368
+ }
369
+ leadArgs.push("--out", leadsPath);
370
+ writeWizardLine(` ${buildCommandLine(leadArgs)}`);
371
+ }
372
+ async function runVendorLookupWizard(rl) {
373
+ const vendor = await promptChoice(rl, "Which product are you selling?", [{ value: "deel", label: "Deel", description: "Use Deel's built-in ICP template" }], "deel");
374
+ writeWizardLine();
375
+ const market = await promptChoice(rl, "Which market should the BigQuery lookup target?", [
376
+ { value: "dach", label: "DACH", description: "Germany, Austria, Switzerland" },
377
+ { value: "europe", label: "Europe" },
378
+ { value: "global", label: "Global" }
379
+ ], "dach");
380
+ 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 }));
381
+ const execute = await promptYesNo(rl, "Execute the BigQuery lookup now?", false);
382
+ writeWizardLine();
383
+ const slug = slugify(vendor);
384
+ const icpPath = await promptText(rl, "Where should I save the ICP JSON?", {
385
+ defaultValue: `./data/${slug}-icp-${market}.json`,
386
+ required: true
387
+ });
388
+ const sqlPath = await promptText(rl, "Where should I save the generated SQL?", {
389
+ defaultValue: `./data/${slug}-lookup-${market}.sql`,
390
+ required: true
391
+ });
392
+ const rawPath = execute
393
+ ? await promptText(rl, "Where should I save raw BigQuery rows?", {
394
+ defaultValue: `./data/${slug}-leads-raw-${market}.json`,
395
+ required: true
396
+ })
397
+ : "";
398
+ const leadPath = execute
399
+ ? await promptText(rl, "Where should I save normalized leads?", {
400
+ defaultValue: `./data/${slug}-leads-${market}.json`,
401
+ required: true
402
+ })
403
+ : "";
404
+ writeWizardLine();
405
+ const icp = buildVendorIcp(vendor, market);
406
+ await writeJsonFile(icpPath, icp);
407
+ const sql = buildBigQueryLeadLookupSql(icp, {
408
+ table: "icpidentifier.SalesGPT.leadPool_new",
409
+ companyField: "companyName",
410
+ domainField: "domain",
411
+ regionField: undefined,
412
+ keywordFields: splitCsv("companyName,industry,description,tagline,specialties"),
413
+ titleField: "jobTitle",
414
+ industryField: "industry",
415
+ companySizeField: "companySize",
416
+ countryField: "company_countryCode",
417
+ firstNameField: "firstName",
418
+ lastNameField: "lastName",
419
+ emailField: "email",
420
+ limit,
421
+ additionalWhere: undefined,
422
+ useSalesprompterGuards: true
423
+ });
424
+ await writeTextFile(sqlPath, `${sql}\n`);
425
+ let executedRowCount = null;
426
+ if (execute) {
427
+ const rows = await runBigQueryQuery(sql, { maxRows: limit });
428
+ const parsedRows = z.array(z.record(z.string(), z.unknown())).parse(rows);
429
+ await writeJsonFile(rawPath, parsedRows);
430
+ const normalizedLeads = normalizeBigQueryLeadRows(parsedRows);
431
+ await writeJsonFile(leadPath, normalizedLeads);
432
+ executedRowCount = parsedRows.length;
433
+ }
434
+ writeWizardLine(`Saved vendor ICP to ${icpPath}.`);
435
+ writeWizardLine(`Saved lookup SQL to ${sqlPath}.`);
436
+ if (execute) {
437
+ writeWizardLine(`Saved ${executedRowCount ?? 0} raw rows to ${rawPath}.`);
438
+ writeWizardLine(`Saved normalized leads to ${leadPath}.`);
439
+ }
440
+ writeWizardLine();
441
+ writeWizardLine("Equivalent raw commands:");
442
+ writeWizardLine(` ${buildCommandLine(["salesprompter", "icp:vendor", "--vendor", vendor, "--market", market, "--out", icpPath])}`);
443
+ const lookupArgs = ["salesprompter", "leads:lookup:bq", "--icp", icpPath, "--limit", String(limit), "--sql-out", sqlPath];
444
+ if (execute) {
445
+ lookupArgs.push("--execute", "--out", rawPath, "--lead-out", leadPath);
446
+ }
447
+ writeWizardLine(` ${buildCommandLine(lookupArgs)}`);
448
+ }
449
+ async function runWizard(options) {
450
+ if (runtimeOutputOptions.json || runtimeOutputOptions.quiet) {
451
+ throw new Error("wizard does not support --json or --quiet.");
452
+ }
453
+ writeWizardLine("Salesprompter Wizard");
454
+ writeWizardLine("Choose the outcome you want. I will ask a few questions and run the matching CLI workflow.");
455
+ writeWizardLine();
456
+ await ensureWizardSession(options);
457
+ const rl = createInterface({
458
+ input: process.stdin,
459
+ output: process.stdout
460
+ });
461
+ try {
462
+ const flow = await promptChoice(rl, "What do you want to do?", [
463
+ { value: "vendor-icp", label: "Build ICP for the product I sell", description: "Example: I sell for Deel and want Deel's ideal customer profile" },
464
+ { value: "target-account", label: "Find contacts at one company", description: "Example: find people at deel.com" },
465
+ { value: "vendor-lookup", label: "Prepare a warehouse lead lookup", description: "Build a product ICP, then generate BigQuery SQL or leads" }
466
+ ], "vendor-icp");
467
+ writeWizardLine();
468
+ if (flow === "vendor-icp") {
469
+ await runVendorIcpWizard(rl);
470
+ return;
471
+ }
472
+ if (flow === "target-account") {
473
+ await runTargetAccountWizard(rl);
474
+ return;
475
+ }
476
+ await runVendorLookupWizard(rl);
477
+ }
478
+ finally {
479
+ rl.close();
480
+ }
481
+ }
482
+ function shouldAutoRunWizard(argv) {
483
+ return argv.length <= 2 && Boolean(process.stdin.isTTY && process.stdout.isTTY);
484
+ }
92
485
  function buildCliError(error) {
93
486
  if (error instanceof z.ZodError) {
94
487
  return {
@@ -234,53 +627,15 @@ program
234
627
  .option("--timeout-seconds <number>", "Device flow wait timeout in seconds", "180")
235
628
  .action(async (options) => {
236
629
  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
630
+ const result = await performLogin({
631
+ token: options.token,
632
+ apiUrl: options.apiUrl,
633
+ timeoutSeconds
279
634
  });
280
635
  printOutput({
281
636
  status: "ok",
282
- method: "device",
283
- startedAt,
637
+ method: result.method,
638
+ startedAt: result.startedAt,
284
639
  verificationUrl: result.verificationUrl,
285
640
  userCode: result.userCode,
286
641
  apiBaseUrl: result.session.apiBaseUrl,
@@ -288,6 +643,19 @@ program
288
643
  expiresAt: result.session.expiresAt ?? null
289
644
  });
290
645
  });
646
+ program
647
+ .command("wizard")
648
+ .alias("start")
649
+ .description("Run an interactive guided workflow for common Salesprompter tasks.")
650
+ .option("--api-url <url>", "Salesprompter API base URL, defaults to SALESPROMPTER_API_BASE_URL or salesprompter.ai")
651
+ .option("--timeout-seconds <number>", "Auth login timeout in seconds when the wizard needs to sign in", "180")
652
+ .action(async (options) => {
653
+ const timeoutSeconds = z.coerce.number().int().min(30).max(1800).parse(options.timeoutSeconds);
654
+ await runWizard({
655
+ apiUrl: options.apiUrl,
656
+ timeoutSeconds
657
+ });
658
+ });
291
659
  program
292
660
  .command("auth:whoami")
293
661
  .description("Show current authenticated user and session status.")
@@ -313,7 +681,7 @@ program
313
681
  program.hook("preAction", async (_thisCommand, actionCommand) => {
314
682
  applyGlobalOutputOptions(actionCommand);
315
683
  const commandName = actionCommand.name();
316
- if (commandName.startsWith("auth:")) {
684
+ if (commandName.startsWith("auth:") || commandName === "wizard") {
317
685
  return;
318
686
  }
319
687
  if (shouldBypassAuth()) {
@@ -980,7 +1348,18 @@ program
980
1348
  execution
981
1349
  });
982
1350
  });
983
- program.parseAsync(process.argv).catch((error) => {
1351
+ async function main() {
1352
+ if (shouldAutoRunWizard(process.argv)) {
1353
+ await runWizard();
1354
+ return;
1355
+ }
1356
+ if (process.argv.length <= 2) {
1357
+ program.outputHelp();
1358
+ return;
1359
+ }
1360
+ await program.parseAsync(process.argv);
1361
+ }
1362
+ main().catch((error) => {
984
1363
  const cliError = buildCliError(error);
985
1364
  const space = runtimeOutputOptions.json ? undefined : 2;
986
1365
  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.8",
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": {
@@ -12,12 +12,14 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "build": "tsc -p tsconfig.json",
15
+ "build:docs:site": "node ./scripts/build-docs-site.mjs",
15
16
  "check": "tsc --noEmit -p tsconfig.json",
16
17
  "docs:dev": "mint dev",
17
18
  "docs:broken-links": "mint broken-links",
18
19
  "docs:a11y": "mint a11y",
19
20
  "start": "node ./dist/cli.js",
20
- "test": "npm run build && tsc -p tsconfig.test.json && node --test dist-tests/tests/**/*.test.js"
21
+ "test": "npm run build && tsc -p tsconfig.test.json && node --test dist-tests/tests/**/*.test.js",
22
+ "vercel-build": "npm run build:docs:site"
21
23
  },
22
24
  "keywords": [
23
25
  "sales",
@@ -38,7 +40,7 @@
38
40
  "ai-agent",
39
41
  "codex"
40
42
  ],
41
- "homepage": "https://salesprompter.ai/docs",
43
+ "homepage": "https://salesprompter-cli.vercel.app",
42
44
  "repository": {
43
45
  "type": "git",
44
46
  "url": "git+https://github.com/danielsinewe/salesprompter-cli.git"
@@ -53,6 +55,8 @@
53
55
  },
54
56
  "devDependencies": {
55
57
  "@types/node": "^24.3.0",
58
+ "gray-matter": "^4.0.3",
59
+ "marked": "^16.3.0",
56
60
  "mint": "^4.2.420",
57
61
  "typescript": "^5.9.2"
58
62
  }