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 +24 -2
- package/dist/auth.js +11 -2
- package/dist/cli.js +425 -46
- package/package.json +7 -3
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,
|
|
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
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|