salesprompter-cli 0.1.32 → 0.1.33

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.
Files changed (2) hide show
  1. package/dist/cli.js +277 -38
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -311,6 +311,7 @@ const helpVisibleCommandNames = new Set([
311
311
  "packs:add",
312
312
  "upgrade",
313
313
  "auth:login",
314
+ "auth:workspace",
314
315
  "wizard",
315
316
  "auth:whoami",
316
317
  "llm:ready",
@@ -2858,6 +2859,13 @@ async function confirmWizardWorkspace(rl, session, options) {
2858
2859
  writeWizardLine();
2859
2860
  return result.session;
2860
2861
  }
2862
+ async function switchWorkspaceWithBrowser(options) {
2863
+ await clearAuthSession();
2864
+ return (await performLogin({
2865
+ apiUrl: options?.apiUrl,
2866
+ timeoutSeconds: options?.timeoutSeconds ?? 180
2867
+ })).session;
2868
+ }
2861
2869
  async function resolveLlmAuthReadiness() {
2862
2870
  const apiBaseUrl = process.env.SALESPROMPTER_API_BASE_URL?.trim() || "https://salesprompter.ai";
2863
2871
  const envToken = resolveNonInteractiveAuthToken(process.env);
@@ -2942,6 +2950,19 @@ function buildSalesNavigatorCrawlLogPath(input) {
2942
2950
  const slug = slugify(input) || "salesnav-crawl";
2943
2951
  return `./data/${slug}-crawl.log.jsonl`;
2944
2952
  }
2953
+ function buildSalesNavigatorCrawlOutputPath(input) {
2954
+ const slug = slugify(input) || "salesnav-crawl";
2955
+ return `./data/${slug}-crawl.json`;
2956
+ }
2957
+ function isSalesNavigatorPeopleSearchUrl(input) {
2958
+ try {
2959
+ const parsed = new URL(input);
2960
+ return /(^|\.)linkedin\.com$/i.test(parsed.hostname) && parsed.pathname.replace(/\/+$/, "") === "/sales/search/people";
2961
+ }
2962
+ catch {
2963
+ return false;
2964
+ }
2965
+ }
2945
2966
  function decodeSalesNavigatorQueryParam(url) {
2946
2967
  try {
2947
2968
  const encoded = new URL(url).searchParams.get("query");
@@ -5035,11 +5056,192 @@ async function searchTargetCompanyLeads(reference, limit) {
5035
5056
  limit
5036
5057
  });
5037
5058
  }
5059
+ async function runDirectSalesNavigatorSearchWizard(input) {
5060
+ const maxResultsPerSearch = 2500;
5061
+ const numberOfProfiles = 2500;
5062
+ const slicePreset = "wizard-salesnav-search";
5063
+ const maxSplitDepth = DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS.length;
5064
+ const maxSlices = 1000;
5065
+ const maxRetries = 3;
5066
+ const probeProfiles = 100;
5067
+ const agentBusyWaitSeconds = 30;
5068
+ const agentBusyMaxWaits = 20;
5069
+ const idlePollSeconds = 10;
5070
+ const idleMaxPolls = 180;
5071
+ const parallelExports = 3;
5072
+ const outPath = buildSalesNavigatorCrawlOutputPath(input);
5073
+ const dryRun = shouldBypassAuth();
5074
+ const logger = await createWorkflowLogger({
5075
+ logPath: buildSalesNavigatorCrawlLogPath(input)
5076
+ });
5077
+ writeWizardLine("Detected a Sales Navigator people search. I will process this search directly.");
5078
+ if (dryRun) {
5079
+ writeWizardLine("Auth bypass is enabled, so I will preview the crawl plan instead of launching it.");
5080
+ }
5081
+ writeWizardLine();
5082
+ await logger.log("salesnav.crawl.command.started", {
5083
+ queryUrl: input,
5084
+ jobId: null,
5085
+ maxResultsPerSearch,
5086
+ numberOfProfiles,
5087
+ slicePreset,
5088
+ maxSplitDepth,
5089
+ maxSlices,
5090
+ maxRetries,
5091
+ probeProfiles,
5092
+ agentBusyWaitSeconds,
5093
+ agentBusyMaxWaits,
5094
+ idlePollSeconds,
5095
+ idleMaxPolls,
5096
+ parallelExports,
5097
+ dryRun
5098
+ });
5099
+ if (dryRun) {
5100
+ const preview = buildSalesNavigatorCrawlPreview({
5101
+ sourceQueryUrl: input,
5102
+ maxResultsPerSearch,
5103
+ numberOfProfiles,
5104
+ slicePreset
5105
+ });
5106
+ const payload = {
5107
+ status: "ok",
5108
+ dryRun: true,
5109
+ mode: "adaptive",
5110
+ traceId: logger.traceId,
5111
+ logPath: logger.logPath,
5112
+ sourceQueryUrl: input,
5113
+ rootQueryUrl: preview.root.slicedQueryUrl,
5114
+ rootAppliedFilters: preview.root.appliedFilters,
5115
+ dimensionOrder: preview.dimensions.map((dimension) => ({
5116
+ key: dimension.key,
5117
+ filterType: dimension.filterType,
5118
+ valueCount: dimension.values.length
5119
+ })),
5120
+ firstSplitQueries: preview.firstSplit.map((attempt) => ({
5121
+ slicedQueryUrl: attempt.slicedQueryUrl,
5122
+ appliedFilters: attempt.appliedFilters,
5123
+ splitTrail: attempt.splitTrail.map((entry) => ({
5124
+ key: entry.key,
5125
+ filterType: entry.filterType,
5126
+ valueText: entry.value.text
5127
+ }))
5128
+ }))
5129
+ };
5130
+ await logger.log("salesnav.crawl.dry-run.preview", {
5131
+ sourceQueryUrl: input,
5132
+ root: summarizeSalesNavigatorQuery(payload.rootQueryUrl, payload.rootAppliedFilters),
5133
+ dimensionOrder: payload.dimensionOrder,
5134
+ firstSplitQueries: payload.firstSplitQueries.map((attempt) => ({
5135
+ splitTrail: attempt.splitTrail,
5136
+ ...summarizeSalesNavigatorQuery(attempt.slicedQueryUrl, attempt.appliedFilters)
5137
+ }))
5138
+ });
5139
+ await writeJsonFile(outPath, payload);
5140
+ writeWizardLine(`Saved Sales Navigator crawl preview to ${outPath}.`);
5141
+ writeWizardLine(`Saved logs to ${logger.logPath}.`);
5142
+ return;
5143
+ }
5144
+ let session = await requireAuthSession();
5145
+ const sessionOrgId = resolveSessionOrgId(session);
5146
+ if (sessionOrgId) {
5147
+ logger.setEventStore(await createSalesNavigatorCrawlEventStore({ orgId: sessionOrgId }));
5148
+ }
5149
+ const seed = createSalesNavigatorCrawlSeed({
5150
+ sourceQueryUrl: input,
5151
+ maxResultsPerSearch,
5152
+ numberOfProfiles,
5153
+ slicePreset
5154
+ });
5155
+ const created = await createOrResumeSalesNavigatorCrawlJob(session, {
5156
+ sourceQueryUrl: input,
5157
+ slicePreset,
5158
+ maxResultsPerSearch,
5159
+ numberOfProfiles,
5160
+ rawPayload: {
5161
+ workflow: "wizard:salesnav-search",
5162
+ traceId: logger.traceId,
5163
+ command: {
5164
+ sourceQueryUrl: input,
5165
+ slicePreset,
5166
+ maxResultsPerSearch,
5167
+ numberOfProfiles,
5168
+ maxSplitDepth,
5169
+ maxSlices,
5170
+ maxRetries,
5171
+ probeProfiles,
5172
+ agentBusyWaitSeconds,
5173
+ agentBusyMaxWaits,
5174
+ idlePollSeconds,
5175
+ idleMaxPolls,
5176
+ parallelExports
5177
+ }
5178
+ },
5179
+ rootSlice: {
5180
+ slicedQueryUrl: seed.slicedQueryUrl,
5181
+ appliedFilters: seed.appliedFilters,
5182
+ depth: seed.depth,
5183
+ splitTrail: seed.splitTrail,
5184
+ rawPayload: {
5185
+ workflow: "wizard:salesnav-search",
5186
+ traceId: logger.traceId
5187
+ }
5188
+ }
5189
+ }, logger.traceId);
5190
+ session = created.session;
5191
+ const jobId = created.value.job.id;
5192
+ writeWizardLine(`${created.value.resumed ? "Resumed" : "Started"} Sales Navigator crawl ${jobId}. Processing the search now...`);
5193
+ const crawl = await executeSalesNavigatorCrawlJob(session, jobId, {
5194
+ maxSplitDepth,
5195
+ maxSlices,
5196
+ maxRetries,
5197
+ probeProfiles,
5198
+ agentBusyWaitSeconds,
5199
+ agentBusyMaxWaits,
5200
+ idlePollSeconds,
5201
+ idleMaxPolls,
5202
+ parallelExports,
5203
+ traceId: logger.traceId,
5204
+ logger
5205
+ });
5206
+ const payload = {
5207
+ status: "ok",
5208
+ dryRun: false,
5209
+ mode: "durable",
5210
+ traceId: logger.traceId,
5211
+ logPath: logger.logPath,
5212
+ jobId,
5213
+ resumed: created.value.resumed,
5214
+ sourceQueryUrl: crawl.job.sourceQueryUrl,
5215
+ slicePreset: crawl.job.slicePreset,
5216
+ maxResultsPerSearch: crawl.job.maxResultsPerSearch,
5217
+ numberOfProfiles: crawl.job.numberOfProfiles,
5218
+ claimedSlices: crawl.claimedSlices,
5219
+ truncated: crawl.truncated,
5220
+ job: crawl.job,
5221
+ lastOutcome: crawl.lastOutcome
5222
+ };
5223
+ await writeJsonFile(outPath, payload);
5224
+ writeWizardLine(`Sales Navigator crawl status: ${crawl.job.status}.`);
5225
+ writeWizardLine(`Imported ${crawl.job.importedPeople} people across ${crawl.job.exportedSlices} exported slice${crawl.job.exportedSlices === 1 ? "" : "s"}.`);
5226
+ if (crawl.job.failedSlices > 0 || crawl.truncated) {
5227
+ writeWizardLine(`Some work still needs attention: ${crawl.job.failedSlices} failed slice${crawl.job.failedSlices === 1 ? "" : "s"}, truncated=${crawl.truncated}.`);
5228
+ }
5229
+ writeWizardLine(`Saved crawl summary to ${outPath}.`);
5230
+ writeWizardLine(`Saved logs to ${logger.logPath}.`);
5231
+ writeWizardLine();
5232
+ writeWizardLine("Equivalent raw command:");
5233
+ writeWizardLine(` ${buildCommandLine(["salesprompter", "salesnav:crawl", "--query-url", input])}`);
5234
+ }
5038
5235
  async function runProductMarketWizard(rl) {
5039
5236
  writeWizardSection("Find leads from a product market", "Start from a company website, LinkedIn company page, product page, or category page. I will turn that into intended job titles and durable Sales Navigator crawls.");
5040
5237
  const input = await promptText(rl, "What company website or LinkedIn page should I start from?", {
5041
5238
  required: true
5042
5239
  });
5240
+ if (isSalesNavigatorPeopleSearchUrl(input)) {
5241
+ writeWizardLine();
5242
+ await runDirectSalesNavigatorSearchWizard(input);
5243
+ return;
5244
+ }
5043
5245
  const productLimit = z.coerce.number().int().min(1).max(5000).parse(await promptText(rl, "How many products should I inspect?", { defaultValue: "25", required: true }));
5044
5246
  const titleLimit = z.coerce.number().int().min(1).max(1000).parse(await promptText(rl, "How many job titles should I turn into Sales Navigator crawls?", {
5045
5247
  defaultValue: "5",
@@ -5237,7 +5439,7 @@ async function runWizard(options) {
5237
5439
  throw new Error("wizard does not support --json or --quiet.");
5238
5440
  }
5239
5441
  writeWizardLine("Salesprompter");
5240
- writeWizardLine("Start with a company website, LinkedIn product page, or category URL. I will guide you from there.");
5442
+ writeWizardLine("Start with a company website, LinkedIn product page, category URL, or Sales Navigator search. I will guide you from there.");
5241
5443
  writeWizardLine();
5242
5444
  const rl = createInterface({
5243
5445
  input: process.stdin,
@@ -5248,46 +5450,63 @@ async function runWizard(options) {
5248
5450
  if (wizardSession.session && wizardSession.restoredFromCache) {
5249
5451
  await confirmWizardWorkspace(rl, wizardSession.session, options);
5250
5452
  }
5251
- const flow = await promptChoice(rl, "What do you want help with?", [
5252
- {
5253
- value: "product-market",
5254
- label: "Find leads from a product market",
5255
- description: "Start from a company, product, or LinkedIn category and crawl Sales Navigator",
5256
- aliases: ["product market", "linkedin products", "category", "sales navigator", "crawl"]
5257
- },
5258
- {
5259
- value: "reference-company",
5260
- label: "Use a built-in vendor shortcut",
5261
- description: "Generate the saved vendor ICP and search workspace leads",
5262
- aliases: ["vendor", "shortcut", "vendor template", "quick template"]
5263
- },
5264
- {
5265
- value: "target-company",
5266
- label: "Find people at a specific company",
5267
- description: "Example: find people at company.com",
5268
- aliases: ["target company", "company", "find people", "people at a company", "lead generation"]
5269
- },
5270
- {
5271
- value: "outreach-sync",
5272
- label: "Push qualified leads to Instantly",
5273
- description: "Use a saved leads file to fill an Instantly campaign",
5274
- aliases: ["instantly", "outreach", "send leads", "campaign"]
5453
+ for (;;) {
5454
+ const flow = await promptChoice(rl, "What do you want help with?", [
5455
+ {
5456
+ value: "product-market",
5457
+ label: "Find leads from a product market",
5458
+ description: "Start from a company, product, or LinkedIn category and crawl Sales Navigator",
5459
+ aliases: ["product market", "linkedin products", "category", "sales navigator", "crawl"]
5460
+ },
5461
+ {
5462
+ value: "reference-company",
5463
+ label: "Use a built-in vendor shortcut",
5464
+ description: "Generate the saved vendor ICP and search workspace leads",
5465
+ aliases: ["vendor", "shortcut", "vendor template", "quick template"]
5466
+ },
5467
+ {
5468
+ value: "target-company",
5469
+ label: "Find people at a specific company",
5470
+ description: "Example: find people at company.com",
5471
+ aliases: ["target company", "company", "find people", "people at a company", "lead generation"]
5472
+ },
5473
+ {
5474
+ value: "switch-workspace",
5475
+ label: "Switch workspace",
5476
+ description: "Choose Gojiberry, SelectLine, or another Salesprompter workspace",
5477
+ aliases: ["switch workspace", "change workspace", "gojiberry", "organization", "org"]
5478
+ },
5479
+ {
5480
+ value: "outreach-sync",
5481
+ label: "Push qualified leads to Instantly",
5482
+ description: "Use a saved leads file to fill an Instantly campaign",
5483
+ aliases: ["instantly", "outreach", "send leads", "campaign"]
5484
+ }
5485
+ ], "product-market");
5486
+ writeWizardLine();
5487
+ if (flow === "switch-workspace") {
5488
+ writeWizardLine("Choose the workspace for this CLI session in the browser.");
5489
+ writeWizardLine();
5490
+ const session = await switchWorkspaceWithBrowser(options);
5491
+ writeSessionSummary(session);
5492
+ writeWizardLine();
5493
+ continue;
5275
5494
  }
5276
- ], "product-market");
5277
- writeWizardLine();
5278
- if (flow === "product-market") {
5279
- await runProductMarketWizard(rl);
5280
- return;
5281
- }
5282
- if (flow === "reference-company") {
5283
- await runVendorShortcutWizard(rl);
5284
- return;
5285
- }
5286
- if (flow === "target-company") {
5287
- await runTargetCompanyWizard(rl);
5495
+ if (flow === "product-market") {
5496
+ await runProductMarketWizard(rl);
5497
+ return;
5498
+ }
5499
+ if (flow === "reference-company") {
5500
+ await runVendorShortcutWizard(rl);
5501
+ return;
5502
+ }
5503
+ if (flow === "target-company") {
5504
+ await runTargetCompanyWizard(rl);
5505
+ return;
5506
+ }
5507
+ await runOutreachSyncWizard(rl);
5288
5508
  return;
5289
5509
  }
5290
- await runOutreachSyncWizard(rl);
5291
5510
  }
5292
5511
  finally {
5293
5512
  rl.close();
@@ -5605,6 +5824,26 @@ program
5605
5824
  expiresAt: result.session.expiresAt ?? null
5606
5825
  });
5607
5826
  });
5827
+ program
5828
+ .command("auth:workspace")
5829
+ .alias("auth:switch")
5830
+ .description("Switch the active Salesprompter workspace for this CLI session.")
5831
+ .option("--api-url <url>", "Salesprompter API base URL, defaults to SALESPROMPTER_API_BASE_URL or salesprompter.ai")
5832
+ .option("--timeout-seconds <number>", "Browser login timeout in seconds", "180")
5833
+ .action(async (options) => {
5834
+ const timeoutSeconds = z.coerce.number().int().min(30).max(1800).parse(options.timeoutSeconds);
5835
+ const session = await switchWorkspaceWithBrowser({
5836
+ apiUrl: options.apiUrl,
5837
+ timeoutSeconds
5838
+ });
5839
+ printOutput({
5840
+ status: "ok",
5841
+ method: "browser",
5842
+ apiBaseUrl: session.apiBaseUrl,
5843
+ user: session.user,
5844
+ expiresAt: session.expiresAt ?? null
5845
+ });
5846
+ });
5608
5847
  program
5609
5848
  .command("wizard")
5610
5849
  .alias("start")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
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",