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 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));
@@ -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 publish = async () => { const report = shortlistCompanyLeads(brief, 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)); return report; };
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
- for (const job of planCompanySearches(brief)) {
224
- if (state.results[job.key])
225
- continue;
226
- if (performed >= input.maxSearches)
227
- break;
228
- state.results[job.key] = await input.search(job);
229
- performed++;
230
- await save(checkpoint, JSON.stringify(state));
231
- await publish();
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(brief).length, output: outDir, resumable: true };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "description": "Sales workflow CLI for guided lead generation, enrichment, scoring, and sync.",
5
5
  "author": "Daniel Sinewe <hello@danielsinewe.com>",
6
6
  "type": "module",