salesprompter-cli 0.1.0 → 0.1.2
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 +159 -6
- package/dist/auth.js +242 -0
- package/dist/bigquery.js +194 -0
- package/dist/cli.js +814 -19
- package/dist/domain.js +14 -0
- package/dist/domainfinder.js +632 -0
- package/dist/engine.js +84 -15
- package/dist/historical-queries.js +189 -0
- package/dist/icp-templates.js +171 -0
- package/dist/instantly.js +135 -0
- package/dist/io.js +4 -0
- package/dist/leadlists-funnel.js +131 -0
- package/package.json +6 -2
- package/.codex/environments/environment.toml +0 -19
- package/data/deel-icp.json +0 -17
- package/data/deel-leads.json +0 -77
- package/data/enriched.json +0 -106
- package/data/icp.json +0 -24
- package/data/leads.json +0 -62
- package/data/scored.json +0 -142
- package/dist-tests/src/cli.js +0 -114
- package/dist-tests/src/domain.js +0 -36
- package/dist-tests/src/engine.js +0 -147
- package/dist-tests/src/io.js +0 -17
- package/dist-tests/src/providers.js +0 -1
- package/dist-tests/src/sample-data.js +0 -34
- package/dist-tests/tests/cli.test.js +0 -149
- package/src/cli.ts +0 -136
- package/src/domain.ts +0 -50
- package/src/engine.ts +0 -170
- package/src/io.ts +0 -21
- package/src/providers.ts +0 -22
- package/src/sample-data.ts +0 -37
- package/tests/cli.test.ts +0 -184
- package/tsconfig.json +0 -16
- package/tsconfig.test.json +0 -8
package/dist/cli.js
CHANGED
|
@@ -1,65 +1,386 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { clearAuthSession, loginWithDeviceFlow, loginWithToken, requireAuthSession, shouldBypassAuth, verifySession } from "./auth.js";
|
|
5
|
+
import { buildBigQueryLeadLookupSql, executeBigQuerySql, normalizeBigQueryLeadRows, runBigQueryQuery, runBigQueryRows } from "./bigquery.js";
|
|
6
|
+
import { AccountProfileSchema, EnrichedLeadSchema, IcpSchema, LeadSchema, ScoredLeadSchema, SyncTargetSchema } from "./domain.js";
|
|
7
|
+
import { auditDomainDecisions, buildDomainfinderBacklogQueries, buildDomainfinderCandidatesSql, buildDomainfinderInputSql, buildDomainfinderWritebackSql, buildExistingDomainRepairSql, buildExistingDomainAuditQueries, compareDomainSelectionStrategies, selectBestDomains } from "./domainfinder.js";
|
|
8
|
+
import { AccountLeadProvider, DryRunSyncProvider, HeuristicCompanyProvider, HeuristicEnrichmentProvider, HeuristicPeopleSearchProvider, HeuristicScoringProvider, RoutedSyncProvider } from "./engine.js";
|
|
9
|
+
import { analyzeHistoricalQueries } from "./historical-queries.js";
|
|
10
|
+
import { buildHistoricalVendorIcp, buildVendorIcp } from "./icp-templates.js";
|
|
11
|
+
import { InstantlySyncProvider } from "./instantly.js";
|
|
12
|
+
import { buildLeadlistsFunnelQueries } from "./leadlists-funnel.js";
|
|
13
|
+
import { readJsonFile, splitCsv, writeJsonFile, writeTextFile } from "./io.js";
|
|
7
14
|
const program = new Command();
|
|
8
|
-
const leadProvider = new
|
|
15
|
+
const leadProvider = new AccountLeadProvider(new HeuristicCompanyProvider(), new HeuristicPeopleSearchProvider());
|
|
9
16
|
const enrichmentProvider = new HeuristicEnrichmentProvider();
|
|
10
17
|
const scoringProvider = new HeuristicScoringProvider();
|
|
11
|
-
const syncProvider = new DryRunSyncProvider();
|
|
18
|
+
const syncProvider = new RoutedSyncProvider(new DryRunSyncProvider(), new InstantlySyncProvider());
|
|
19
|
+
const runtimeOutputOptions = {
|
|
20
|
+
json: false,
|
|
21
|
+
quiet: false
|
|
22
|
+
};
|
|
12
23
|
function printOutput(value) {
|
|
13
|
-
|
|
24
|
+
if (runtimeOutputOptions.quiet) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const space = runtimeOutputOptions.json ? undefined : 2;
|
|
28
|
+
process.stdout.write(`${JSON.stringify(value, null, space)}\n`);
|
|
29
|
+
}
|
|
30
|
+
function applyGlobalOutputOptions(actionCommand) {
|
|
31
|
+
const globalOptions = actionCommand.optsWithGlobals();
|
|
32
|
+
runtimeOutputOptions.json = Boolean(globalOptions.json);
|
|
33
|
+
runtimeOutputOptions.quiet = Boolean(globalOptions.quiet);
|
|
34
|
+
}
|
|
35
|
+
function buildCliError(error) {
|
|
36
|
+
if (error instanceof z.ZodError) {
|
|
37
|
+
return {
|
|
38
|
+
status: "error",
|
|
39
|
+
code: "invalid_arguments",
|
|
40
|
+
message: "Invalid arguments",
|
|
41
|
+
details: error.issues.map((issue) => ({
|
|
42
|
+
path: issue.path.join("."),
|
|
43
|
+
message: issue.message
|
|
44
|
+
}))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
48
|
+
if (message.includes("not logged in")) {
|
|
49
|
+
return {
|
|
50
|
+
status: "error",
|
|
51
|
+
code: "auth_required",
|
|
52
|
+
message
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (message.includes("session expired")) {
|
|
56
|
+
return {
|
|
57
|
+
status: "error",
|
|
58
|
+
code: "session_expired",
|
|
59
|
+
message
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
status: "error",
|
|
64
|
+
code: "runtime_error",
|
|
65
|
+
message
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function exitCodeForError(code) {
|
|
69
|
+
switch (code) {
|
|
70
|
+
case "invalid_arguments":
|
|
71
|
+
return 2;
|
|
72
|
+
case "auth_required":
|
|
73
|
+
case "session_expired":
|
|
74
|
+
return 3;
|
|
75
|
+
case "runtime_error":
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const nullableInt = z.union([z.number().int(), z.string(), z.null()]).transform((value, ctx) => {
|
|
80
|
+
if (value === null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === "number") {
|
|
84
|
+
if (!Number.isInteger(value)) {
|
|
85
|
+
ctx.addIssue({
|
|
86
|
+
code: z.ZodIssueCode.custom,
|
|
87
|
+
message: "Expected an integer-compatible value"
|
|
88
|
+
});
|
|
89
|
+
return z.NEVER;
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
const trimmed = value.trim();
|
|
94
|
+
if (trimmed.length === 0) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const parsed = Number(trimmed);
|
|
98
|
+
if (!Number.isInteger(parsed)) {
|
|
99
|
+
ctx.addIssue({
|
|
100
|
+
code: z.ZodIssueCode.custom,
|
|
101
|
+
message: "Expected an integer-compatible value"
|
|
102
|
+
});
|
|
103
|
+
return z.NEVER;
|
|
104
|
+
}
|
|
105
|
+
return parsed;
|
|
106
|
+
});
|
|
107
|
+
const domainCandidateArraySchema = z.array(z.object({
|
|
108
|
+
companyId: nullableInt,
|
|
109
|
+
crmCompanyId: nullableInt,
|
|
110
|
+
companyName: z.string(),
|
|
111
|
+
domain: z.string().nullable(),
|
|
112
|
+
source: z.string(),
|
|
113
|
+
type: z.string().nullable(),
|
|
114
|
+
hunterEmailCount: nullableInt,
|
|
115
|
+
linkedinDomain: z.string().nullable(),
|
|
116
|
+
linkedinWebsite: z.string().nullable()
|
|
117
|
+
}));
|
|
118
|
+
const domainDecisionArraySchema = z.array(z.object({
|
|
119
|
+
companyKey: z.string(),
|
|
120
|
+
selected: z
|
|
121
|
+
.object({
|
|
122
|
+
companyId: nullableInt,
|
|
123
|
+
crmCompanyId: nullableInt,
|
|
124
|
+
companyName: z.string(),
|
|
125
|
+
domain: z.string().nullable(),
|
|
126
|
+
source: z.string(),
|
|
127
|
+
type: z.string().nullable(),
|
|
128
|
+
hunterEmailCount: nullableInt,
|
|
129
|
+
linkedinDomain: z.string().nullable(),
|
|
130
|
+
linkedinWebsite: z.string().nullable()
|
|
131
|
+
})
|
|
132
|
+
.nullable(),
|
|
133
|
+
reason: z.enum([
|
|
134
|
+
"linkedin-domain",
|
|
135
|
+
"linkedin-website",
|
|
136
|
+
"highest-hunter-count",
|
|
137
|
+
"fallback-first-non-null",
|
|
138
|
+
"no-domain"
|
|
139
|
+
]),
|
|
140
|
+
candidates: domainCandidateArraySchema
|
|
141
|
+
}));
|
|
142
|
+
const existingAuditReportSchema = z.object({
|
|
143
|
+
market: z.string(),
|
|
144
|
+
countries: z.array(z.string()),
|
|
145
|
+
stages: z.array(z.object({
|
|
146
|
+
key: z.string(),
|
|
147
|
+
description: z.string(),
|
|
148
|
+
rows: z.array(z.record(z.string(), z.unknown()))
|
|
149
|
+
}))
|
|
150
|
+
});
|
|
151
|
+
async function fetchHistoricalQueryRows(tables) {
|
|
152
|
+
const rows = [];
|
|
153
|
+
for (const table of tables) {
|
|
154
|
+
const sql = `SELECT CAST(query AS STRING) AS query, COUNT(*) AS freq FROM \`icpidentifier.SalesPrompter.${table}\` WHERE query IS NOT NULL GROUP BY CAST(query AS STRING)`;
|
|
155
|
+
const resultRows = await runBigQueryRows(sql);
|
|
156
|
+
for (const row of resultRows) {
|
|
157
|
+
rows.push({
|
|
158
|
+
sourceTable: table,
|
|
159
|
+
query: String(row.query ?? ""),
|
|
160
|
+
freq: Number(row.freq ?? 0)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return rows;
|
|
14
165
|
}
|
|
15
166
|
program
|
|
16
167
|
.name("salesprompter")
|
|
17
168
|
.description("Sales workflow CLI for ICP definition, lead generation, enrichment, scoring, and sync.")
|
|
18
|
-
.version("0.1.
|
|
169
|
+
.version("0.1.1")
|
|
170
|
+
.option("--json", "Emit compact machine-readable JSON output", false)
|
|
171
|
+
.option("--quiet", "Suppress successful stdout output", false);
|
|
172
|
+
program
|
|
173
|
+
.command("auth:login")
|
|
174
|
+
.description("Authenticate CLI with Salesprompter app credentials.")
|
|
175
|
+
.option("--token <token>", "App-issued bearer token for direct login")
|
|
176
|
+
.option("--api-url <url>", "Salesprompter API base URL, defaults to SALESPROMPTER_API_BASE_URL or app.salesprompter.com")
|
|
177
|
+
.option("--timeout-seconds <number>", "Device flow wait timeout in seconds", "180")
|
|
178
|
+
.action(async (options) => {
|
|
179
|
+
const timeoutSeconds = z.coerce.number().int().min(30).max(1800).parse(options.timeoutSeconds);
|
|
180
|
+
const token = typeof options.token === "string" ? options.token.trim() : "";
|
|
181
|
+
if (token.length > 0) {
|
|
182
|
+
const session = await loginWithToken(token, options.apiUrl);
|
|
183
|
+
printOutput({
|
|
184
|
+
status: "ok",
|
|
185
|
+
method: "token",
|
|
186
|
+
apiBaseUrl: session.apiBaseUrl,
|
|
187
|
+
user: session.user,
|
|
188
|
+
expiresAt: session.expiresAt ?? null
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const startedAt = new Date().toISOString();
|
|
193
|
+
process.stderr.write("Starting device login flow. Complete login in the browser.\n");
|
|
194
|
+
const result = await loginWithDeviceFlow({
|
|
195
|
+
apiBaseUrl: options.apiUrl,
|
|
196
|
+
timeoutSeconds
|
|
197
|
+
});
|
|
198
|
+
printOutput({
|
|
199
|
+
status: "ok",
|
|
200
|
+
method: "device",
|
|
201
|
+
startedAt,
|
|
202
|
+
verificationUrl: result.verificationUrl,
|
|
203
|
+
userCode: result.userCode,
|
|
204
|
+
apiBaseUrl: result.session.apiBaseUrl,
|
|
205
|
+
user: result.session.user,
|
|
206
|
+
expiresAt: result.session.expiresAt ?? null
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
program
|
|
210
|
+
.command("auth:whoami")
|
|
211
|
+
.description("Show current authenticated user and session status.")
|
|
212
|
+
.option("--verify", "Verify token with API before printing", false)
|
|
213
|
+
.action(async (options) => {
|
|
214
|
+
const session = await requireAuthSession();
|
|
215
|
+
const verifiedSession = options.verify ? await verifySession(session) : session;
|
|
216
|
+
printOutput({
|
|
217
|
+
status: "ok",
|
|
218
|
+
apiBaseUrl: verifiedSession.apiBaseUrl,
|
|
219
|
+
user: verifiedSession.user,
|
|
220
|
+
expiresAt: verifiedSession.expiresAt ?? null,
|
|
221
|
+
createdAt: verifiedSession.createdAt
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
program
|
|
225
|
+
.command("auth:logout")
|
|
226
|
+
.description("Remove local CLI auth session.")
|
|
227
|
+
.action(async () => {
|
|
228
|
+
await clearAuthSession();
|
|
229
|
+
printOutput({ status: "ok", loggedOut: true });
|
|
230
|
+
});
|
|
231
|
+
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
232
|
+
applyGlobalOutputOptions(actionCommand);
|
|
233
|
+
const commandName = actionCommand.name();
|
|
234
|
+
if (commandName.startsWith("auth:")) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (shouldBypassAuth()) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const session = await requireAuthSession();
|
|
241
|
+
if (session.expiresAt !== undefined && Date.now() >= Date.parse(session.expiresAt)) {
|
|
242
|
+
throw new Error("session expired. Run `salesprompter auth:login`.");
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
program
|
|
246
|
+
.command("account:resolve")
|
|
247
|
+
.description("Resolve a target company into a normalized account profile.")
|
|
248
|
+
.requiredOption("--domain <domain>", "Company domain like deel.com")
|
|
249
|
+
.option("--company-name <name>", "Optional company name override")
|
|
250
|
+
.option("--icp <path>", "Optional path to ICP JSON for industry/region/title hints")
|
|
251
|
+
.requiredOption("--out <path>", "Output file path")
|
|
252
|
+
.action(async (options) => {
|
|
253
|
+
const icp = options.icp
|
|
254
|
+
? await readJsonFile(options.icp, IcpSchema)
|
|
255
|
+
: IcpSchema.parse({ name: "ad-hoc account resolve" });
|
|
256
|
+
const result = await leadProvider.generateLeads(icp, 1, {
|
|
257
|
+
companyDomain: options.domain,
|
|
258
|
+
companyName: options.companyName
|
|
259
|
+
});
|
|
260
|
+
await writeJsonFile(options.out, result.account);
|
|
261
|
+
printOutput({
|
|
262
|
+
status: "ok",
|
|
263
|
+
provider: result.provider,
|
|
264
|
+
mode: result.mode,
|
|
265
|
+
account: AccountProfileSchema.parse(result.account),
|
|
266
|
+
out: options.out,
|
|
267
|
+
warnings: result.warnings
|
|
268
|
+
});
|
|
269
|
+
});
|
|
19
270
|
program
|
|
20
271
|
.command("icp:define")
|
|
21
272
|
.description("Define an ideal customer profile and write it to a JSON file.")
|
|
22
273
|
.requiredOption("--name <name>", "Human-readable ICP name")
|
|
274
|
+
.option("--description <text>", "Plain-language ICP description", "")
|
|
23
275
|
.option("--industries <items>", "Comma-separated industries", "")
|
|
24
276
|
.option("--company-sizes <items>", "Comma-separated buckets like 1-49,50-199,200-499,500+", "")
|
|
25
277
|
.option("--regions <items>", "Comma-separated regions", "")
|
|
278
|
+
.option("--countries <items>", "Comma-separated country codes like DE,AT,CH", "")
|
|
26
279
|
.option("--titles <items>", "Comma-separated titles", "")
|
|
27
280
|
.option("--pains <items>", "Comma-separated pain points", "")
|
|
28
281
|
.option("--required-signals <items>", "Comma-separated required signals", "")
|
|
29
282
|
.option("--excluded-signals <items>", "Comma-separated excluded signals", "")
|
|
283
|
+
.option("--keywords <items>", "Comma-separated keywords for search matching", "")
|
|
284
|
+
.option("--excluded-keywords <items>", "Comma-separated keywords to filter out", "")
|
|
30
285
|
.requiredOption("--out <path>", "Output file path")
|
|
31
286
|
.action(async (options) => {
|
|
32
287
|
const icp = IcpSchema.parse({
|
|
33
288
|
name: options.name,
|
|
289
|
+
description: options.description,
|
|
34
290
|
industries: splitCsv(options.industries),
|
|
35
291
|
companySizes: splitCsv(options.companySizes),
|
|
36
292
|
regions: splitCsv(options.regions),
|
|
293
|
+
countries: splitCsv(options.countries),
|
|
37
294
|
titles: splitCsv(options.titles),
|
|
38
295
|
pains: splitCsv(options.pains),
|
|
39
296
|
requiredSignals: splitCsv(options.requiredSignals),
|
|
40
|
-
excludedSignals: splitCsv(options.excludedSignals)
|
|
297
|
+
excludedSignals: splitCsv(options.excludedSignals),
|
|
298
|
+
keywords: splitCsv(options.keywords),
|
|
299
|
+
excludedKeywords: splitCsv(options.excludedKeywords)
|
|
41
300
|
});
|
|
42
301
|
await writeJsonFile(options.out, icp);
|
|
43
302
|
printOutput({ status: "ok", icp });
|
|
44
303
|
});
|
|
304
|
+
program
|
|
305
|
+
.command("icp:vendor")
|
|
306
|
+
.description("Create a vendor-specific ICP template.")
|
|
307
|
+
.requiredOption("--vendor <vendor>", "Vendor template name, currently: deel")
|
|
308
|
+
.option("--market <market>", "global|europe|dach", "dach")
|
|
309
|
+
.requiredOption("--out <path>", "Output file path")
|
|
310
|
+
.action(async (options) => {
|
|
311
|
+
const vendor = z.enum(["deel"]).parse(options.vendor);
|
|
312
|
+
const market = z.enum(["global", "europe", "dach"]).parse(options.market);
|
|
313
|
+
const icp = buildVendorIcp(vendor, market);
|
|
314
|
+
await writeJsonFile(options.out, icp);
|
|
315
|
+
printOutput({ status: "ok", icp });
|
|
316
|
+
});
|
|
317
|
+
program
|
|
318
|
+
.command("icp:from-historical-queries:bq")
|
|
319
|
+
.description("Build a vendor ICP from historical BigQuery query patterns.")
|
|
320
|
+
.requiredOption("--vendor <vendor>", "Vendor template name, currently: deel")
|
|
321
|
+
.option("--market <market>", "global|europe|dach", "dach")
|
|
322
|
+
.option("--tables <items>", "Comma-separated BigQuery tables with a query column", "leadLists_raw,leadLists_unique,linkedinSearchExport_people_unique,salesNavigatorSearchExport_companies_unique,snse_containers_input")
|
|
323
|
+
.option("--search-kind <kind>", "all|people|sales-people|sales-company", "sales-people")
|
|
324
|
+
.option("--include-function <items>", "Comma-separated included function texts to keep", "Human Resources")
|
|
325
|
+
.requiredOption("--out <path>", "Output ICP file path")
|
|
326
|
+
.option("--report-out <path>", "Optional output path for the historical report")
|
|
327
|
+
.action(async (options) => {
|
|
328
|
+
const vendor = z.enum(["deel"]).parse(options.vendor);
|
|
329
|
+
const market = z.enum(["global", "europe", "dach"]).parse(options.market);
|
|
330
|
+
const searchKind = z.enum(["all", "people", "sales-people", "sales-company"]).parse(options.searchKind);
|
|
331
|
+
const tables = splitCsv(options.tables);
|
|
332
|
+
if (tables.length === 0) {
|
|
333
|
+
throw new Error("at least one table is required");
|
|
334
|
+
}
|
|
335
|
+
const rows = await fetchHistoricalQueryRows(tables);
|
|
336
|
+
const report = analyzeHistoricalQueries(rows, {
|
|
337
|
+
searchKind,
|
|
338
|
+
includeFunctionText: splitCsv(options.includeFunction)
|
|
339
|
+
});
|
|
340
|
+
const icp = buildHistoricalVendorIcp(vendor, market, report);
|
|
341
|
+
await writeJsonFile(options.out, icp);
|
|
342
|
+
if (options.reportOut) {
|
|
343
|
+
await writeJsonFile(options.reportOut, report);
|
|
344
|
+
}
|
|
345
|
+
printOutput({
|
|
346
|
+
status: "ok",
|
|
347
|
+
icp,
|
|
348
|
+
out: options.out,
|
|
349
|
+
reportOut: options.reportOut ?? null,
|
|
350
|
+
distinctQueryRowsAnalyzed: report.distinctQueryRowsAnalyzed,
|
|
351
|
+
weightedQueryVolume: report.weightedQueryVolume,
|
|
352
|
+
sourceTables: report.sourceTables
|
|
353
|
+
});
|
|
354
|
+
});
|
|
45
355
|
program
|
|
46
356
|
.command("leads:generate")
|
|
47
|
-
.description("Generate
|
|
357
|
+
.description("Generate leads for a target account or from fallback seeds.")
|
|
48
358
|
.requiredOption("--icp <path>", "Path to ICP JSON")
|
|
49
359
|
.option("--count <number>", "Number of leads to generate", "10")
|
|
50
|
-
.option("--
|
|
360
|
+
.option("--domain <domain>", "Target a specific company domain like deel.com")
|
|
361
|
+
.option("--company-domain <domain>", "Deprecated alias for --domain")
|
|
51
362
|
.option("--company-name <name>", "Optional company name override for a targeted domain")
|
|
52
363
|
.requiredOption("--out <path>", "Output file path")
|
|
53
364
|
.action(async (options) => {
|
|
54
365
|
const icp = await readJsonFile(options.icp, IcpSchema);
|
|
55
366
|
const count = z.coerce.number().int().min(1).max(1000).parse(options.count);
|
|
367
|
+
const domain = options.domain ?? options.companyDomain;
|
|
56
368
|
const target = {
|
|
57
|
-
companyDomain:
|
|
369
|
+
companyDomain: domain,
|
|
58
370
|
companyName: options.companyName
|
|
59
371
|
};
|
|
60
|
-
const
|
|
61
|
-
await writeJsonFile(options.out, leads);
|
|
62
|
-
printOutput({
|
|
372
|
+
const result = await leadProvider.generateLeads(icp, count, target);
|
|
373
|
+
await writeJsonFile(options.out, result.leads);
|
|
374
|
+
printOutput({
|
|
375
|
+
status: "ok",
|
|
376
|
+
generated: result.leads.length,
|
|
377
|
+
out: options.out,
|
|
378
|
+
target: result.account.domain,
|
|
379
|
+
provider: result.provider,
|
|
380
|
+
mode: result.mode,
|
|
381
|
+
account: result.account,
|
|
382
|
+
warnings: result.warnings
|
|
383
|
+
});
|
|
63
384
|
});
|
|
64
385
|
program
|
|
65
386
|
.command("leads:enrich")
|
|
@@ -98,17 +419,491 @@ program
|
|
|
98
419
|
});
|
|
99
420
|
program
|
|
100
421
|
.command("sync:outreach")
|
|
101
|
-
.description("
|
|
422
|
+
.description("Sync scored leads into an outreach platform. Instantly supports real writes and defaults to dry-run.")
|
|
102
423
|
.requiredOption("--target <target>", "apollo|instantly|outreach")
|
|
103
424
|
.requiredOption("--in <path>", "Path to scored lead JSON array")
|
|
425
|
+
.option("--campaign-id <id>", "Instantly campaign id, or set INSTANTLY_CAMPAIGN_ID")
|
|
426
|
+
.option("--apply", "Create leads in Instantly instead of dry-run", false)
|
|
427
|
+
.option("--allow-duplicates", "Do not skip emails already present in the Instantly campaign", false)
|
|
104
428
|
.action(async (options) => {
|
|
105
429
|
const target = SyncTargetSchema.parse(options.target);
|
|
106
430
|
const leads = await readJsonFile(options.in, z.array(ScoredLeadSchema));
|
|
107
|
-
const result = await syncProvider.sync(target, leads
|
|
431
|
+
const result = await syncProvider.sync(target, leads, {
|
|
432
|
+
apply: Boolean(options.apply),
|
|
433
|
+
instantlyCampaignId: options.campaignId,
|
|
434
|
+
allowDuplicates: Boolean(options.allowDuplicates)
|
|
435
|
+
});
|
|
108
436
|
printOutput({ status: "ok", ...result });
|
|
109
437
|
});
|
|
438
|
+
program
|
|
439
|
+
.command("leads:lookup:bq")
|
|
440
|
+
.description("Build a BigQuery lead lookup from an ICP and optionally execute it with the bq CLI.")
|
|
441
|
+
.requiredOption("--icp <path>", "Path to ICP JSON")
|
|
442
|
+
.option("--table <table>", "Fully qualified BigQuery table or view", "icpidentifier.SalesGPT.leadPool_new")
|
|
443
|
+
.option("--company-field <field>", "Company name field", "companyName")
|
|
444
|
+
.option("--domain-field <field>", "Company domain field", "domain")
|
|
445
|
+
.option("--region-field <field>", "Region field if your table has one")
|
|
446
|
+
.option("--keyword-fields <items>", "Comma-separated text fields used for keyword matching", "companyName,industry,description,tagline,specialties")
|
|
447
|
+
.option("--title-field <field>", "Contact title field", "jobTitle")
|
|
448
|
+
.option("--industry-field <field>", "Industry field", "industry")
|
|
449
|
+
.option("--company-size-field <field>", "Company size field", "companySize")
|
|
450
|
+
.option("--country-field <field>", "Country or region field", "company_countryCode")
|
|
451
|
+
.option("--first-name-field <field>", "First name field", "firstName")
|
|
452
|
+
.option("--last-name-field <field>", "Last name field", "lastName")
|
|
453
|
+
.option("--email-field <field>", "Email field", "email")
|
|
454
|
+
.option("--additional-where <sql>", "Extra WHERE predicate to append")
|
|
455
|
+
.option("--limit <number>", "Max rows to return", "200")
|
|
456
|
+
.option("--sql-out <path>", "Optional file path for generated SQL")
|
|
457
|
+
.option("--out <path>", "Optional file path for query results JSON")
|
|
458
|
+
.option("--lead-out <path>", "Optional file path for normalized lead JSON")
|
|
459
|
+
.option("--execute", "Run the generated query via bq")
|
|
460
|
+
.option("--no-salesprompter-guards", "Disable default leadPool_new quality guards")
|
|
461
|
+
.action(async (options) => {
|
|
462
|
+
const icp = await readJsonFile(options.icp, IcpSchema);
|
|
463
|
+
const limit = z.coerce.number().int().min(1).max(5000).parse(options.limit);
|
|
464
|
+
const sql = buildBigQueryLeadLookupSql(icp, {
|
|
465
|
+
table: options.table,
|
|
466
|
+
companyField: options.companyField,
|
|
467
|
+
domainField: options.domainField,
|
|
468
|
+
regionField: options.regionField,
|
|
469
|
+
keywordFields: splitCsv(options.keywordFields),
|
|
470
|
+
titleField: options.titleField,
|
|
471
|
+
industryField: options.industryField,
|
|
472
|
+
companySizeField: options.companySizeField,
|
|
473
|
+
countryField: options.countryField,
|
|
474
|
+
firstNameField: options.firstNameField,
|
|
475
|
+
lastNameField: options.lastNameField,
|
|
476
|
+
emailField: options.emailField,
|
|
477
|
+
limit,
|
|
478
|
+
additionalWhere: options.additionalWhere,
|
|
479
|
+
useSalesprompterGuards: Boolean(options.salesprompterGuards)
|
|
480
|
+
});
|
|
481
|
+
if (options.sqlOut) {
|
|
482
|
+
await writeTextFile(options.sqlOut, `${sql}\n`);
|
|
483
|
+
}
|
|
484
|
+
if (!options.execute) {
|
|
485
|
+
printOutput({
|
|
486
|
+
status: "ok",
|
|
487
|
+
mode: "sql-only",
|
|
488
|
+
table: options.table,
|
|
489
|
+
sql,
|
|
490
|
+
sqlOut: options.sqlOut ?? null
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const rows = await runBigQueryQuery(sql, { maxRows: limit });
|
|
495
|
+
if (options.out) {
|
|
496
|
+
await writeJsonFile(options.out, rows);
|
|
497
|
+
}
|
|
498
|
+
if (options.leadOut) {
|
|
499
|
+
const normalizedLeads = normalizeBigQueryLeadRows(z.array(z.record(z.string(), z.unknown())).parse(rows));
|
|
500
|
+
await writeJsonFile(options.leadOut, normalizedLeads);
|
|
501
|
+
}
|
|
502
|
+
printOutput({
|
|
503
|
+
status: "ok",
|
|
504
|
+
mode: "executed",
|
|
505
|
+
table: options.table,
|
|
506
|
+
rowCount: Array.isArray(rows) ? rows.length : null,
|
|
507
|
+
out: options.out ?? null,
|
|
508
|
+
leadOut: options.leadOut ?? null,
|
|
509
|
+
sqlOut: options.sqlOut ?? null
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
program
|
|
513
|
+
.command("queries:analyze:bq")
|
|
514
|
+
.description("Analyze historical query parameters from BigQuery tables.")
|
|
515
|
+
.option("--tables <items>", "Comma-separated BigQuery tables with a query column", "leadLists_raw,leadLists_unique,linkedinSearchExport_people_unique,salesNavigatorSearchExport_companies_unique,snse_containers_input")
|
|
516
|
+
.option("--search-kind <kind>", "all|people|sales-people|sales-company", "all")
|
|
517
|
+
.option("--include-function <items>", "Comma-separated included function texts to keep, e.g. Human Resources", "")
|
|
518
|
+
.requiredOption("--out <path>", "Output report path")
|
|
519
|
+
.action(async (options) => {
|
|
520
|
+
const searchKind = z.enum(["all", "people", "sales-people", "sales-company"]).parse(options.searchKind);
|
|
521
|
+
const tables = splitCsv(options.tables);
|
|
522
|
+
if (tables.length === 0) {
|
|
523
|
+
throw new Error("at least one table is required");
|
|
524
|
+
}
|
|
525
|
+
const rows = await fetchHistoricalQueryRows(tables);
|
|
526
|
+
const report = analyzeHistoricalQueries(rows, {
|
|
527
|
+
searchKind,
|
|
528
|
+
includeFunctionText: splitCsv(options.includeFunction)
|
|
529
|
+
});
|
|
530
|
+
await writeJsonFile(options.out, report);
|
|
531
|
+
printOutput({
|
|
532
|
+
status: "ok",
|
|
533
|
+
distinctQueryRowsAnalyzed: report.distinctQueryRowsAnalyzed,
|
|
534
|
+
weightedQueryVolume: report.weightedQueryVolume,
|
|
535
|
+
out: options.out,
|
|
536
|
+
sourceTables: report.sourceTables
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
program
|
|
540
|
+
.command("leadlists:funnel:bq")
|
|
541
|
+
.description("Build an upstream lead-list funnel report for a vendor/market.")
|
|
542
|
+
.requiredOption("--vendor <vendor>", "Vendor template name, currently: deel")
|
|
543
|
+
.option("--market <market>", "global|europe|dach", "dach")
|
|
544
|
+
.requiredOption("--out <path>", "Output report path")
|
|
545
|
+
.action(async (options) => {
|
|
546
|
+
const vendor = z.enum(["deel"]).parse(options.vendor);
|
|
547
|
+
const market = z.enum(["global", "europe", "dach"]).parse(options.market);
|
|
548
|
+
const definition = buildLeadlistsFunnelQueries(vendor, market);
|
|
549
|
+
const stages = [];
|
|
550
|
+
for (const stage of definition.stages) {
|
|
551
|
+
const rows = await runBigQueryRows(stage.sql, { maxRows: 1000 });
|
|
552
|
+
const result = rows[0] ?? {};
|
|
553
|
+
stages.push({
|
|
554
|
+
key: stage.key,
|
|
555
|
+
description: stage.description,
|
|
556
|
+
result
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
const report = {
|
|
560
|
+
vendor,
|
|
561
|
+
market,
|
|
562
|
+
queryTerms: definition.queryTerms,
|
|
563
|
+
stages,
|
|
564
|
+
notes: definition.notes,
|
|
565
|
+
requiredFields: {
|
|
566
|
+
upstreamPeople: ["profileUrl", "companyUrl", "firstName", "lastName", "jobTitle"],
|
|
567
|
+
upstreamCompany: ["companyUrl", "handle", "domain", "countryCode", "companySize"],
|
|
568
|
+
downstreamContact: ["firstName_cleaned", "lastName_cleaned", "domain", "email", "email_invalid"]
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
await writeJsonFile(options.out, report);
|
|
572
|
+
printOutput({
|
|
573
|
+
status: "ok",
|
|
574
|
+
vendor,
|
|
575
|
+
market,
|
|
576
|
+
out: options.out,
|
|
577
|
+
stages: stages.map((stage) => ({ key: stage.key, result: stage.result }))
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
program
|
|
581
|
+
.command("domainfinder:backlog:bq")
|
|
582
|
+
.description("Analyze the domain finder backlog and its input bottleneck.")
|
|
583
|
+
.option("--market <market>", "global|europe|dach", "dach")
|
|
584
|
+
.requiredOption("--out <path>", "Output report path")
|
|
585
|
+
.action(async (options) => {
|
|
586
|
+
const market = z.enum(["global", "europe", "dach"]).parse(options.market);
|
|
587
|
+
const definition = buildDomainfinderBacklogQueries(market);
|
|
588
|
+
const stages = [];
|
|
589
|
+
for (const stage of definition.stages) {
|
|
590
|
+
const rows = await runBigQueryRows(stage.sql, { maxRows: 1000 });
|
|
591
|
+
stages.push({
|
|
592
|
+
key: stage.key,
|
|
593
|
+
description: stage.description,
|
|
594
|
+
result: rows[0] ?? {}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
const report = {
|
|
598
|
+
market,
|
|
599
|
+
countries: definition.countries,
|
|
600
|
+
stages,
|
|
601
|
+
notes: definition.notes
|
|
602
|
+
};
|
|
603
|
+
await writeJsonFile(options.out, report);
|
|
604
|
+
printOutput({
|
|
605
|
+
status: "ok",
|
|
606
|
+
market,
|
|
607
|
+
out: options.out,
|
|
608
|
+
stages: stages.map((stage) => ({ key: stage.key, result: stage.result }))
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
program
|
|
612
|
+
.command("domainfinder:candidates:bq")
|
|
613
|
+
.description("Fetch real domain candidates from BigQuery for backlog companies.")
|
|
614
|
+
.option("--market <market>", "global|europe|dach", "dach")
|
|
615
|
+
.option("--limit <number>", "Max candidate rows to fetch", "500")
|
|
616
|
+
.requiredOption("--out <path>", "Output candidate JSON path")
|
|
617
|
+
.option("--sql-out <path>", "Optional SQL output path")
|
|
618
|
+
.action(async (options) => {
|
|
619
|
+
const market = z.enum(["global", "europe", "dach"]).parse(options.market);
|
|
620
|
+
const limit = z.coerce.number().int().min(1).max(50000).parse(options.limit);
|
|
621
|
+
const sql = buildDomainfinderCandidatesSql(market, limit);
|
|
622
|
+
if (options.sqlOut) {
|
|
623
|
+
await writeTextFile(options.sqlOut, `${sql}\n`);
|
|
624
|
+
}
|
|
625
|
+
const rows = await runBigQueryRows(sql, { maxRows: limit });
|
|
626
|
+
await writeJsonFile(options.out, rows);
|
|
627
|
+
printOutput({
|
|
628
|
+
status: "ok",
|
|
629
|
+
market,
|
|
630
|
+
candidates: rows.length,
|
|
631
|
+
out: options.out,
|
|
632
|
+
sqlOut: options.sqlOut ?? null
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
program
|
|
636
|
+
.command("domainfinder:input-sql")
|
|
637
|
+
.description("Generate improved SQL for the domainFinder input view.")
|
|
638
|
+
.option("--market <market>", "global|europe|dach", "dach")
|
|
639
|
+
.requiredOption("--out <path>", "Output SQL file path")
|
|
640
|
+
.action(async (options) => {
|
|
641
|
+
const market = z.enum(["global", "europe", "dach"]).parse(options.market);
|
|
642
|
+
const sql = buildDomainfinderInputSql(market);
|
|
643
|
+
await writeTextFile(options.out, `${sql}\n`);
|
|
644
|
+
printOutput({ status: "ok", market, out: options.out });
|
|
645
|
+
});
|
|
646
|
+
program
|
|
647
|
+
.command("domainfinder:select")
|
|
648
|
+
.description("Select the best domain per company from candidate rows using improved logic.")
|
|
649
|
+
.requiredOption("--in <path>", "Path to candidate JSON array")
|
|
650
|
+
.requiredOption("--out <path>", "Output file path")
|
|
651
|
+
.action(async (options) => {
|
|
652
|
+
const candidates = await readJsonFile(options.in, domainCandidateArraySchema);
|
|
653
|
+
const decisions = selectBestDomains(candidates);
|
|
654
|
+
await writeJsonFile(options.out, decisions);
|
|
655
|
+
printOutput({
|
|
656
|
+
status: "ok",
|
|
657
|
+
companies: decisions.length,
|
|
658
|
+
out: options.out,
|
|
659
|
+
reasons: decisions.reduce((acc, decision) => {
|
|
660
|
+
acc[decision.reason] = (acc[decision.reason] ?? 0) + 1;
|
|
661
|
+
return acc;
|
|
662
|
+
}, {})
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
program
|
|
666
|
+
.command("domainfinder:audit")
|
|
667
|
+
.description("Audit selected domain decisions and surface risky writeback cases.")
|
|
668
|
+
.requiredOption("--in <path>", "Path to decision JSON array")
|
|
669
|
+
.requiredOption("--out <path>", "Output audit JSON path")
|
|
670
|
+
.action(async (options) => {
|
|
671
|
+
const decisions = await readJsonFile(options.in, domainDecisionArraySchema);
|
|
672
|
+
const report = auditDomainDecisions(decisions);
|
|
673
|
+
await writeJsonFile(options.out, report);
|
|
674
|
+
printOutput({
|
|
675
|
+
status: "ok",
|
|
676
|
+
out: options.out,
|
|
677
|
+
summary: report.summary
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
program
|
|
681
|
+
.command("domainfinder:compare-pipedream")
|
|
682
|
+
.description("Compare the legacy Pipedream selector against the improved CLI selector.")
|
|
683
|
+
.requiredOption("--in <path>", "Path to candidate JSON array")
|
|
684
|
+
.requiredOption("--out <path>", "Output comparison JSON path")
|
|
685
|
+
.action(async (options) => {
|
|
686
|
+
const candidates = await readJsonFile(options.in, domainCandidateArraySchema);
|
|
687
|
+
const report = compareDomainSelectionStrategies(candidates);
|
|
688
|
+
await writeJsonFile(options.out, report);
|
|
689
|
+
printOutput({
|
|
690
|
+
status: "ok",
|
|
691
|
+
out: options.out,
|
|
692
|
+
summary: report.summary
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
program
|
|
696
|
+
.command("domainfinder:audit-existing:bq")
|
|
697
|
+
.description("Audit currently exposed linkedin_companies domains against LinkedIn references in BigQuery.")
|
|
698
|
+
.option("--market <market>", "global|europe|dach", "dach")
|
|
699
|
+
.requiredOption("--out <path>", "Output audit JSON path")
|
|
700
|
+
.action(async (options) => {
|
|
701
|
+
const market = z.enum(["global", "europe", "dach"]).parse(options.market);
|
|
702
|
+
const definition = buildExistingDomainAuditQueries(market);
|
|
703
|
+
const stages = [];
|
|
704
|
+
for (const stage of definition.stages) {
|
|
705
|
+
const rows = await runBigQueryRows(stage.sql, { maxRows: stage.key === "samples" ? 100 : 1000 });
|
|
706
|
+
stages.push({
|
|
707
|
+
key: stage.key,
|
|
708
|
+
description: stage.description,
|
|
709
|
+
rows
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
const report = {
|
|
713
|
+
market,
|
|
714
|
+
countries: definition.countries,
|
|
715
|
+
stages
|
|
716
|
+
};
|
|
717
|
+
await writeJsonFile(options.out, report);
|
|
718
|
+
printOutput({
|
|
719
|
+
status: "ok",
|
|
720
|
+
out: options.out,
|
|
721
|
+
market,
|
|
722
|
+
summary: stages.find((stage) => stage.key === "summary")?.rows[0] ?? {}
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
program
|
|
726
|
+
.command("domainfinder:audit-delta")
|
|
727
|
+
.description("Compare two domainfinder audit-existing reports and compute metric deltas.")
|
|
728
|
+
.requiredOption("--before <path>", "Path to previous audit-existing report")
|
|
729
|
+
.requiredOption("--after <path>", "Path to newer audit-existing report")
|
|
730
|
+
.requiredOption("--out <path>", "Output delta JSON path")
|
|
731
|
+
.action(async (options) => {
|
|
732
|
+
const before = await readJsonFile(options.before, existingAuditReportSchema);
|
|
733
|
+
const after = await readJsonFile(options.after, existingAuditReportSchema);
|
|
734
|
+
const beforeSummary = before.stages.find((stage) => stage.key === "summary")?.rows[0] ?? {};
|
|
735
|
+
const afterSummary = after.stages.find((stage) => stage.key === "summary")?.rows[0] ?? {};
|
|
736
|
+
const keys = [
|
|
737
|
+
"companies",
|
|
738
|
+
"with_chosen_domain",
|
|
739
|
+
"with_linkedin_domain",
|
|
740
|
+
"with_linkedin_website",
|
|
741
|
+
"chosen_blacklisted",
|
|
742
|
+
"chosen_starts_with_www",
|
|
743
|
+
"chosen_differs_from_linkedin_domain",
|
|
744
|
+
"chosen_differs_from_linkedin_website"
|
|
745
|
+
];
|
|
746
|
+
const delta = {};
|
|
747
|
+
for (const key of keys) {
|
|
748
|
+
const beforeValue = Number(beforeSummary[key] ?? 0);
|
|
749
|
+
const afterValue = Number(afterSummary[key] ?? 0);
|
|
750
|
+
delta[key] = {
|
|
751
|
+
before: beforeValue,
|
|
752
|
+
after: afterValue,
|
|
753
|
+
delta: afterValue - beforeValue
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
const report = {
|
|
757
|
+
before: {
|
|
758
|
+
path: options.before,
|
|
759
|
+
market: before.market
|
|
760
|
+
},
|
|
761
|
+
after: {
|
|
762
|
+
path: options.after,
|
|
763
|
+
market: after.market
|
|
764
|
+
},
|
|
765
|
+
delta
|
|
766
|
+
};
|
|
767
|
+
await writeJsonFile(options.out, report);
|
|
768
|
+
printOutput({
|
|
769
|
+
status: "ok",
|
|
770
|
+
out: options.out,
|
|
771
|
+
delta
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
program
|
|
775
|
+
.command("domainfinder:repair-existing:bq")
|
|
776
|
+
.description("Generate targeted repair SQL for existing chosen domains and optionally execute it in BigQuery.")
|
|
777
|
+
.option("--market <market>", "global|europe|dach", "dach")
|
|
778
|
+
.option("--limit <number>", "Max companies to repair in this batch", "5000")
|
|
779
|
+
.option("--mode <mode>", "conservative|aggressive|mismatch-only", "conservative")
|
|
780
|
+
.requiredOption("--out <path>", "Output SQL file path")
|
|
781
|
+
.option("--trace-id <traceId>", "Trace id for this repair batch")
|
|
782
|
+
.option("--execute", "Execute the generated SQL in BigQuery", false)
|
|
783
|
+
.action(async (options) => {
|
|
784
|
+
const market = z.enum(["global", "europe", "dach"]).parse(options.market);
|
|
785
|
+
const limit = z.coerce.number().int().min(1).max(500000).parse(options.limit);
|
|
786
|
+
const mode = z.enum(["conservative", "aggressive", "mismatch-only"]).parse(options.mode);
|
|
787
|
+
const traceId = z.string().min(1).parse(options.traceId ?? `salesprompter-cli-repair-${market}-${Date.now()}`);
|
|
788
|
+
const sql = buildExistingDomainRepairSql(market, traceId, limit, mode);
|
|
789
|
+
await writeTextFile(options.out, `${sql}\n`);
|
|
790
|
+
let execution = null;
|
|
791
|
+
if (options.execute) {
|
|
792
|
+
const stdout = await executeBigQuerySql(sql);
|
|
793
|
+
execution = { executed: true, stdout };
|
|
794
|
+
}
|
|
795
|
+
printOutput({
|
|
796
|
+
status: "ok",
|
|
797
|
+
market,
|
|
798
|
+
limit,
|
|
799
|
+
mode,
|
|
800
|
+
traceId,
|
|
801
|
+
out: options.out,
|
|
802
|
+
execution
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
program
|
|
806
|
+
.command("domainfinder:writeback-sql")
|
|
807
|
+
.description("Generate conservative writeback SQL for domainFinder_output from audited decisions.")
|
|
808
|
+
.requiredOption("--in <path>", "Path to decision JSON array")
|
|
809
|
+
.requiredOption("--out <path>", "Output SQL file path")
|
|
810
|
+
.option("--trace-id <traceId>", "Trace id for this writeback batch")
|
|
811
|
+
.action(async (options) => {
|
|
812
|
+
const decisions = await readJsonFile(options.in, domainDecisionArraySchema);
|
|
813
|
+
const traceId = z.string().min(1).parse(options.traceId ?? `salesprompter-cli-${Date.now()}`);
|
|
814
|
+
const sql = buildDomainfinderWritebackSql(decisions, traceId);
|
|
815
|
+
await writeTextFile(options.out, `${sql}\n`);
|
|
816
|
+
const report = auditDomainDecisions(decisions);
|
|
817
|
+
printOutput({
|
|
818
|
+
status: "ok",
|
|
819
|
+
out: options.out,
|
|
820
|
+
traceId,
|
|
821
|
+
acceptedForWriteback: report.summary.acceptedForWriteback,
|
|
822
|
+
rejectedForWriteback: report.summary.rejectedForWriteback
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
program
|
|
826
|
+
.command("domainfinder:writeback:bq")
|
|
827
|
+
.description("Generate writeback SQL and optionally execute it against domainFinder_output.")
|
|
828
|
+
.requiredOption("--in <path>", "Path to decision JSON array")
|
|
829
|
+
.requiredOption("--out <path>", "Output SQL file path")
|
|
830
|
+
.option("--trace-id <traceId>", "Trace id for this writeback batch")
|
|
831
|
+
.option("--execute", "Execute the generated SQL in BigQuery", false)
|
|
832
|
+
.action(async (options) => {
|
|
833
|
+
const decisions = await readJsonFile(options.in, domainDecisionArraySchema);
|
|
834
|
+
const traceId = z.string().min(1).parse(options.traceId ?? `salesprompter-cli-${Date.now()}`);
|
|
835
|
+
const sql = buildDomainfinderWritebackSql(decisions, traceId);
|
|
836
|
+
await writeTextFile(options.out, `${sql}\n`);
|
|
837
|
+
const report = auditDomainDecisions(decisions);
|
|
838
|
+
let execution = null;
|
|
839
|
+
if (options.execute) {
|
|
840
|
+
const stdout = await executeBigQuerySql(sql);
|
|
841
|
+
execution = { executed: true, stdout };
|
|
842
|
+
}
|
|
843
|
+
printOutput({
|
|
844
|
+
status: "ok",
|
|
845
|
+
out: options.out,
|
|
846
|
+
traceId,
|
|
847
|
+
acceptedForWriteback: report.summary.acceptedForWriteback,
|
|
848
|
+
rejectedForWriteback: report.summary.rejectedForWriteback,
|
|
849
|
+
execution
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
program
|
|
853
|
+
.command("domainfinder:run:bq")
|
|
854
|
+
.description("Run the full domainfinder pipeline: candidates, select, audit, writeback SQL, optional BigQuery execution.")
|
|
855
|
+
.option("--market <market>", "global|europe|dach", "dach")
|
|
856
|
+
.option("--limit <number>", "Max candidate rows to fetch", "500")
|
|
857
|
+
.requiredOption("--out-dir <path>", "Output directory for artifacts")
|
|
858
|
+
.option("--trace-id <traceId>", "Trace id for this pipeline batch")
|
|
859
|
+
.option("--execute-writeback", "Execute the generated writeback SQL in BigQuery", false)
|
|
860
|
+
.action(async (options) => {
|
|
861
|
+
const market = z.enum(["global", "europe", "dach"]).parse(options.market);
|
|
862
|
+
const limit = z.coerce.number().int().min(1).max(50000).parse(options.limit);
|
|
863
|
+
const outDir = z.string().min(1).parse(options.outDir);
|
|
864
|
+
const traceId = z.string().min(1).parse(options.traceId ?? `salesprompter-cli-${market}-${Date.now()}`);
|
|
865
|
+
const candidatesPath = `${outDir}/domain-candidates-${market}.json`;
|
|
866
|
+
const candidatesSqlPath = `${outDir}/domain-candidates-${market}.sql`;
|
|
867
|
+
const decisionsPath = `${outDir}/domain-decisions-${market}.json`;
|
|
868
|
+
const auditPath = `${outDir}/domain-audit-${market}.json`;
|
|
869
|
+
const writebackSqlPath = `${outDir}/domain-writeback-${market}.sql`;
|
|
870
|
+
const candidateSql = buildDomainfinderCandidatesSql(market, limit);
|
|
871
|
+
await writeTextFile(candidatesSqlPath, `${candidateSql}\n`);
|
|
872
|
+
const candidateRows = await runBigQueryRows(candidateSql, { maxRows: limit });
|
|
873
|
+
await writeJsonFile(candidatesPath, candidateRows);
|
|
874
|
+
const candidates = domainCandidateArraySchema.parse(candidateRows);
|
|
875
|
+
const decisions = selectBestDomains(candidates);
|
|
876
|
+
await writeJsonFile(decisionsPath, decisions);
|
|
877
|
+
const audit = auditDomainDecisions(decisions);
|
|
878
|
+
await writeJsonFile(auditPath, audit);
|
|
879
|
+
const writebackSql = buildDomainfinderWritebackSql(decisions, traceId);
|
|
880
|
+
await writeTextFile(writebackSqlPath, `${writebackSql}\n`);
|
|
881
|
+
let execution = null;
|
|
882
|
+
if (options.executeWriteback) {
|
|
883
|
+
const stdout = await executeBigQuerySql(writebackSql);
|
|
884
|
+
execution = { executed: true, stdout };
|
|
885
|
+
}
|
|
886
|
+
printOutput({
|
|
887
|
+
status: "ok",
|
|
888
|
+
market,
|
|
889
|
+
limit,
|
|
890
|
+
traceId,
|
|
891
|
+
candidatesPath,
|
|
892
|
+
decisionsPath,
|
|
893
|
+
auditPath,
|
|
894
|
+
writebackSqlPath,
|
|
895
|
+
summary: audit.summary,
|
|
896
|
+
execution
|
|
897
|
+
});
|
|
898
|
+
});
|
|
110
899
|
program.parseAsync(process.argv).catch((error) => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
900
|
+
const cliError = buildCliError(error);
|
|
901
|
+
const space = runtimeOutputOptions.json ? undefined : 2;
|
|
902
|
+
if (runtimeOutputOptions.json) {
|
|
903
|
+
process.stderr.write(`${JSON.stringify(cliError, null, space)}\n`);
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
process.stderr.write(`${cliError.message}\n`);
|
|
907
|
+
}
|
|
908
|
+
process.exitCode = exitCodeForError(cliError.code);
|
|
114
909
|
});
|