salesprompter-cli 0.1.78 → 0.1.79
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 +8 -0
- package/dist/cli.js +38 -2
- package/dist/company-leads.js +38 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,14 @@ If LinkedIn rejects the saved session, the command tries a different local exten
|
|
|
52
52
|
|
|
53
53
|
Create a JSON brief with verified numeric LinkedIn company IDs. Use each subsidiary's own ID; names alone are not an employer match. Missing IDs stay in the coverage report as unresolved.
|
|
54
54
|
|
|
55
|
+
For a name-only list, the executable end-to-end research mode is:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
salesprompter leads:at-companies --brief companies.json --out-dir ./research --resolve-companies --all
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The CLI first collects prepared IDs, then resolves missing IDs through paginated Account Search and immediately collects their contacts. It accepts only one exact-name identity from a fully read result set of at most 1,000 companies; fuzzy, shared-ID, oversized and ambiguous matches remain unresolved. Resolutions and misses are saved in the workspace-bound checkpoint and `company-resolutions.json`, without editing the input brief. Repeat the command to resume; use a new output directory for intentionally retrying saved identity misses. `--all` removes the invocation's job-count limit, not LinkedIn pacing or per-function candidate limits. A working LinkedIn session is required; the Codex browser relay still requires a connected browser worker when used.
|
|
62
|
+
|
|
55
63
|
```json
|
|
56
64
|
{
|
|
57
65
|
"companies": [
|
package/dist/cli.js
CHANGED
|
@@ -15142,12 +15142,14 @@ program
|
|
|
15142
15142
|
.option("--out-dir <path>", "Private output directory and resume checkpoint", "./company-leads")
|
|
15143
15143
|
.option("--max-searches <number>", "Maximum company/function searches this invocation", "12")
|
|
15144
15144
|
.option("--report-only", "Refresh saved research reports without contacting LinkedIn", false)
|
|
15145
|
+
.option("--all", "Process all pending company/function searches in this invocation", false)
|
|
15146
|
+
.option("--resolve-companies", "Resolve unique exact company names and collect their contacts in the same run", false)
|
|
15145
15147
|
.option("--browser-relay-port <number>", "Use the signed-in Codex browser through a loopback relay")
|
|
15146
15148
|
.option("--dry-run", "Preview targeting and unresolved companies without network calls", false)
|
|
15147
15149
|
.action(async (options) => {
|
|
15148
15150
|
const brief = await readJsonFile(path.resolve(options.brief), CompanyBriefSchema);
|
|
15149
15151
|
const jobs = planCompanySearches(brief);
|
|
15150
|
-
const maxSearches = z.coerce.number().int().min(1).max(10000).parse(options.maxSearches);
|
|
15152
|
+
const maxSearches = options.all ? Number.MAX_SAFE_INTEGER : z.coerce.number().int().min(1).max(10000).parse(options.maxSearches);
|
|
15151
15153
|
if (options.dryRun) {
|
|
15152
15154
|
printOutput({ status: "ok", dryRun: true, searches: jobs, unresolvedCompanies: brief.companies.filter(c => !c.companyId), maxSearches, outreachStarted: false, emailEnrichmentStarted: false });
|
|
15153
15155
|
return;
|
|
@@ -15166,7 +15168,7 @@ program
|
|
|
15166
15168
|
const config = relay ? null : await readLinkedInDirectLookupConfig();
|
|
15167
15169
|
const recoverSession = createSessionRecovery({
|
|
15168
15170
|
initial: config,
|
|
15169
|
-
isAuthError: error => error instanceof LocalSalesNavigatorHttpError && (error.status === 401 || error.status === 403),
|
|
15171
|
+
isAuthError: error => error instanceof CliImportCompanySessionError || error instanceof LocalSalesNavigatorHttpError && (error.status === 401 || error.status === 403),
|
|
15170
15172
|
refresh: async () => relay || shouldDisableLinkedInDirectLookupAutodiscovery() ? null : readLocalLinkedInExtensionDirectLookupConfig(),
|
|
15171
15173
|
sameSession: (a, b) => a?.cookie === b?.cookie && a?.csrfToken === b?.csrfToken,
|
|
15172
15174
|
beforeRetry: async () => {
|
|
@@ -15177,6 +15179,40 @@ program
|
|
|
15177
15179
|
let startedSearches = 0;
|
|
15178
15180
|
try {
|
|
15179
15181
|
const report = await runCompanyResearch({ brief, outDir: path.resolve(options.outDir), scope: `${session.apiBaseUrl}:${orgId}`, maxSearches,
|
|
15182
|
+
resolveCompany: !options.resolveCompanies ? undefined : async (company) => {
|
|
15183
|
+
if (startedSearches++ > 0)
|
|
15184
|
+
await delay(randomIntegerBetween(5000, 8000));
|
|
15185
|
+
writeProgress(`Resolving ${company.name}`);
|
|
15186
|
+
const queryUrl = buildCliImportAccountSearchUrl(company.name);
|
|
15187
|
+
const response = await recoverSession(activeConfig => fetchCliImportSalesNavigatorJson({ url: queryUrl, config: activeConfig, browserRelay: relay, timeoutMs: 30000, label: `Company identity for ${company.name}` }));
|
|
15188
|
+
if (response.status !== 200 || !response.body)
|
|
15189
|
+
throw new Error(`Company identity search failed (${response.status}).`);
|
|
15190
|
+
const elements = extractLocalSalesNavigatorElements(response.body);
|
|
15191
|
+
const total = extractLocalSalesNavigatorTotalResults(response.body);
|
|
15192
|
+
// A truncated or unknown result set cannot establish a unique identity.
|
|
15193
|
+
if (total == null || total > 1000)
|
|
15194
|
+
return null;
|
|
15195
|
+
for (let start = 25; start < total; start += 25) {
|
|
15196
|
+
await delay(randomIntegerBetween(5000, 8000));
|
|
15197
|
+
const pageUrl = queryUrl.replace(/([?&])start=\d+/, `$1start=${start}`);
|
|
15198
|
+
const page = await recoverSession(activeConfig => fetchCliImportSalesNavigatorJson({ url: pageUrl, config: activeConfig, browserRelay: relay, timeoutMs: 30000, label: `Company identity for ${company.name}` }));
|
|
15199
|
+
if (page.status !== 200 || !page.body)
|
|
15200
|
+
throw new Error(`Company identity page failed (${page.status}).`);
|
|
15201
|
+
const rows = extractLocalSalesNavigatorElements(page.body);
|
|
15202
|
+
if (!rows.length)
|
|
15203
|
+
return null;
|
|
15204
|
+
elements.push(...rows);
|
|
15205
|
+
}
|
|
15206
|
+
if (elements.length < total)
|
|
15207
|
+
return null;
|
|
15208
|
+
const accounts = elements.map(element => normalizeLocalSalesNavigatorAccount(element, queryUrl)).filter(Boolean);
|
|
15209
|
+
const exact = accounts.filter(account => normalizeLooseMatchText(String(account.companyName ?? "")) === normalizeLooseMatchText(company.name));
|
|
15210
|
+
const ids = new Set(exact.map(account => String(account.companyId ?? "")).filter(id => /^[1-9]\d*$/.test(id)));
|
|
15211
|
+
if (ids.size !== 1)
|
|
15212
|
+
return null;
|
|
15213
|
+
const companyId = [...ids][0];
|
|
15214
|
+
return { companyId, companyName: company.name, evidenceUrl: `https://www.linkedin.com/sales/company/${companyId}` };
|
|
15215
|
+
},
|
|
15180
15216
|
search: async (job) => {
|
|
15181
15217
|
if (startedSearches++ > 0)
|
|
15182
15218
|
await delay(randomIntegerBetween(5000, 8000));
|
package/dist/company-leads.js
CHANGED
|
@@ -217,18 +217,45 @@ async function runCompanyResearchLocked(input) {
|
|
|
217
217
|
throw e;
|
|
218
218
|
}
|
|
219
219
|
const save = async (file, value) => { await writeFile(file + ".tmp", value, { mode: 0o600 }); await chmod(file + ".tmp", 0o600); await rename(file + ".tmp", file); };
|
|
220
|
-
const
|
|
220
|
+
const effectiveBrief = () => CompanyBriefSchema.parse({ ...brief, companies: brief.companies.map((company, index) => {
|
|
221
|
+
const resolution = state.resolutions?.[String(index)];
|
|
222
|
+
return !company.companyId && resolution ? { ...company, companyId: resolution.companyId } : company;
|
|
223
|
+
}) });
|
|
224
|
+
const publish = async () => { const report = shortlistCompanyLeads(effectiveBrief(), state.results); await save(path.join(outDir, "contacts.csv"), companyLeadCsv(report.selected)); await save(path.join(outDir, "review.csv"), companyLeadCsv(report.review)); await save(path.join(outDir, "coverage.json"), JSON.stringify(report, null, 2)); await save(path.join(outDir, "company-resolutions.json"), JSON.stringify(state.resolutions ?? {}, null, 2)); return report; };
|
|
221
225
|
let performed = 0;
|
|
222
226
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
227
|
+
// Known IDs run first; an unresolved identity cannot block already prepared work.
|
|
228
|
+
const collect = async () => {
|
|
229
|
+
for (const job of planCompanySearches(effectiveBrief())) {
|
|
230
|
+
if (state.results[job.key])
|
|
231
|
+
continue;
|
|
232
|
+
if (performed >= input.maxSearches)
|
|
233
|
+
break;
|
|
234
|
+
state.results[job.key] = await input.search(job);
|
|
235
|
+
performed++;
|
|
236
|
+
await save(checkpoint, JSON.stringify(state));
|
|
237
|
+
await publish();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
await collect();
|
|
241
|
+
if (input.resolveCompany) {
|
|
242
|
+
let resolutionsPerformed = 0;
|
|
243
|
+
for (const [index, company] of brief.companies.entries()) {
|
|
244
|
+
if (performed >= input.maxSearches || resolutionsPerformed >= input.maxSearches)
|
|
245
|
+
break;
|
|
246
|
+
if (company.companyId || Object.hasOwn(state.resolutions ?? {}, String(index)))
|
|
247
|
+
continue;
|
|
248
|
+
const resolution = await input.resolveCompany(company);
|
|
249
|
+
resolutionsPerformed++;
|
|
250
|
+
if (resolution && (!/^[1-9]\d*$/.test(resolution.companyId) || words(resolution.companyName) !== words(company.name)))
|
|
251
|
+
throw new Error("Company resolver returned a non-exact identity.");
|
|
252
|
+
const occupied = new Set(effectiveBrief().companies.map(c => c.companyId).filter(Boolean));
|
|
253
|
+
state.resolutions ??= {};
|
|
254
|
+
state.resolutions[String(index)] = resolution && !occupied.has(resolution.companyId) ? resolution : null;
|
|
255
|
+
await save(checkpoint, JSON.stringify(state));
|
|
256
|
+
await publish();
|
|
257
|
+
await collect();
|
|
258
|
+
}
|
|
232
259
|
}
|
|
233
260
|
}
|
|
234
261
|
catch (e) {
|
|
@@ -236,5 +263,5 @@ async function runCompanyResearchLocked(input) {
|
|
|
236
263
|
throw e;
|
|
237
264
|
}
|
|
238
265
|
const report = await publish();
|
|
239
|
-
return { status: "ok", ...report, completedSearches: Object.keys(state.results).length, totalSearches: planCompanySearches(
|
|
266
|
+
return { status: "ok", ...report, completedSearches: Object.keys(state.results).length, totalSearches: planCompanySearches(effectiveBrief()).length, output: outDir, resumable: true };
|
|
240
267
|
}
|
package/package.json
CHANGED