salesprompter-cli 0.1.31 → 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 (3) hide show
  1. package/dist/auth.js +8 -2
  2. package/dist/cli.js +285 -42
  3. package/package.json +1 -1
package/dist/auth.js CHANGED
@@ -15,7 +15,9 @@ const UserSchema = z.object({
15
15
  name: nullableOptionalString,
16
16
  orgId: nullableOptionalString,
17
17
  orgName: nullableOptionalString,
18
- orgSlug: nullableOptionalString
18
+ orgSlug: nullableOptionalString,
19
+ workspaceClientId: nullableOptionalString,
20
+ workspaceClientName: nullableOptionalString
19
21
  });
20
22
  const AuthSessionSchema = z.object({
21
23
  accessToken: z.string().min(1),
@@ -84,6 +86,8 @@ const WhoAmIResponseSchema = z
84
86
  orgId: nullableOptionalString,
85
87
  orgName: nullableOptionalString,
86
88
  orgSlug: nullableOptionalString,
89
+ workspaceClientId: nullableOptionalString,
90
+ workspaceClientName: nullableOptionalString,
87
91
  expiresAt: z.string().datetime().optional()
88
92
  }),
89
93
  z.object({
@@ -107,7 +111,9 @@ const WhoAmIResponseSchema = z
107
111
  name: value.name,
108
112
  orgId: value.orgId,
109
113
  orgName: value.orgName,
110
- orgSlug: value.orgSlug
114
+ orgSlug: value.orgSlug,
115
+ workspaceClientId: value.workspaceClientId,
116
+ workspaceClientName: value.workspaceClientName
111
117
  },
112
118
  expiresAt: value.expiresAt
113
119
  };
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import { setTimeout as delay } from "node:timers/promises";
10
10
  import { createClient } from "@supabase/supabase-js";
11
11
  import { Command } from "commander";
12
12
  import { z } from "zod";
13
- import { clearAuthSession, loginWithBrowserConnect, loginWithDeviceFlow, loginWithToken, requireAuthSession, shouldBypassAuth, verifySession } from "./auth.js";
13
+ import { clearAuthSession, loginWithBrowserConnect, loginWithDeviceFlow, loginWithToken, requireAuthSession, shouldBypassAuth, verifySession, writeAuthSession } from "./auth.js";
14
14
  import { buildBigQueryLeadLookupSql, executeBigQuerySql, normalizeBigQueryLeadRows, runBigQueryQuery, runBigQueryRows } from "./bigquery.js";
15
15
  import { AccountProfileSchema, EnrichedLeadSchema, IcpSchema, LeadSchema, ScoredLeadSchema, SyncTargetSchema } from "./domain.js";
16
16
  import { auditDomainDecisions, buildDomainfinderBacklogQueries, buildDomainfinderCandidatesSql, buildDomainfinderInputSql, buildDomainfinderWritebackSql, buildExistingDomainRepairSql, buildExistingDomainAuditQueries, compareDomainSelectionStrategies, selectBestDomains } from "./domainfinder.js";
@@ -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",
@@ -2545,11 +2546,13 @@ function compactOptionalText(value) {
2545
2546
  return compacted && compacted.length > 0 ? compacted : null;
2546
2547
  }
2547
2548
  function getOrgLabel(session) {
2548
- const orgName = compactOptionalText(session.user.orgName);
2549
+ const orgName = compactOptionalText(session.user.orgName) ?? compactOptionalText(session.user.workspaceClientName);
2549
2550
  const orgSlug = compactOptionalText(session.user.orgSlug);
2550
2551
  const orgId = compactOptionalText(session.user.orgId);
2552
+ const workspaceClientId = compactOptionalText(session.user.workspaceClientId);
2551
2553
  if (orgName) {
2552
- const details = [orgSlug, orgId].filter((value) => Boolean(value));
2554
+ const clientLabel = workspaceClientId ? `client ${workspaceClientId}` : null;
2555
+ const details = [orgSlug, clientLabel, orgId].filter((value) => Boolean(value));
2553
2556
  return details.length > 0 ? `${orgName} (${details.join(", ")})` : orgName;
2554
2557
  }
2555
2558
  if (orgSlug) {
@@ -2784,7 +2787,9 @@ async function ensureWizardSession(options) {
2784
2787
  };
2785
2788
  }
2786
2789
  try {
2787
- const session = await requireAuthSession();
2790
+ const cachedSession = await requireAuthSession();
2791
+ const session = await verifySession(cachedSession);
2792
+ await writeAuthSession(session);
2788
2793
  writeSessionSummary(session);
2789
2794
  writeWizardLine();
2790
2795
  return {
@@ -2854,6 +2859,13 @@ async function confirmWizardWorkspace(rl, session, options) {
2854
2859
  writeWizardLine();
2855
2860
  return result.session;
2856
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
+ }
2857
2869
  async function resolveLlmAuthReadiness() {
2858
2870
  const apiBaseUrl = process.env.SALESPROMPTER_API_BASE_URL?.trim() || "https://salesprompter.ai";
2859
2871
  const envToken = resolveNonInteractiveAuthToken(process.env);
@@ -2938,6 +2950,19 @@ function buildSalesNavigatorCrawlLogPath(input) {
2938
2950
  const slug = slugify(input) || "salesnav-crawl";
2939
2951
  return `./data/${slug}-crawl.log.jsonl`;
2940
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
+ }
2941
2966
  function decodeSalesNavigatorQueryParam(url) {
2942
2967
  try {
2943
2968
  const encoded = new URL(url).searchParams.get("query");
@@ -5031,11 +5056,192 @@ async function searchTargetCompanyLeads(reference, limit) {
5031
5056
  limit
5032
5057
  });
5033
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
+ }
5034
5235
  async function runProductMarketWizard(rl) {
5035
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.");
5036
5237
  const input = await promptText(rl, "What company website or LinkedIn page should I start from?", {
5037
5238
  required: true
5038
5239
  });
5240
+ if (isSalesNavigatorPeopleSearchUrl(input)) {
5241
+ writeWizardLine();
5242
+ await runDirectSalesNavigatorSearchWizard(input);
5243
+ return;
5244
+ }
5039
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 }));
5040
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?", {
5041
5247
  defaultValue: "5",
@@ -5233,7 +5439,7 @@ async function runWizard(options) {
5233
5439
  throw new Error("wizard does not support --json or --quiet.");
5234
5440
  }
5235
5441
  writeWizardLine("Salesprompter");
5236
- 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.");
5237
5443
  writeWizardLine();
5238
5444
  const rl = createInterface({
5239
5445
  input: process.stdin,
@@ -5244,46 +5450,63 @@ async function runWizard(options) {
5244
5450
  if (wizardSession.session && wizardSession.restoredFromCache) {
5245
5451
  await confirmWizardWorkspace(rl, wizardSession.session, options);
5246
5452
  }
5247
- const flow = await promptChoice(rl, "What do you want help with?", [
5248
- {
5249
- value: "product-market",
5250
- label: "Find leads from a product market",
5251
- description: "Start from a company, product, or LinkedIn category and crawl Sales Navigator",
5252
- aliases: ["product market", "linkedin products", "category", "sales navigator", "crawl"]
5253
- },
5254
- {
5255
- value: "reference-company",
5256
- label: "Use a built-in vendor shortcut",
5257
- description: "Generate the saved vendor ICP and search workspace leads",
5258
- aliases: ["vendor", "shortcut", "vendor template", "quick template"]
5259
- },
5260
- {
5261
- value: "target-company",
5262
- label: "Find people at a specific company",
5263
- description: "Example: find people at company.com",
5264
- aliases: ["target company", "company", "find people", "people at a company", "lead generation"]
5265
- },
5266
- {
5267
- value: "outreach-sync",
5268
- label: "Push qualified leads to Instantly",
5269
- description: "Use a saved leads file to fill an Instantly campaign",
5270
- 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;
5271
5494
  }
5272
- ], "product-market");
5273
- writeWizardLine();
5274
- if (flow === "product-market") {
5275
- await runProductMarketWizard(rl);
5276
- return;
5277
- }
5278
- if (flow === "reference-company") {
5279
- await runVendorShortcutWizard(rl);
5280
- return;
5281
- }
5282
- if (flow === "target-company") {
5283
- 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);
5284
5508
  return;
5285
5509
  }
5286
- await runOutreachSyncWizard(rl);
5287
5510
  }
5288
5511
  finally {
5289
5512
  rl.close();
@@ -5601,6 +5824,26 @@ program
5601
5824
  expiresAt: result.session.expiresAt ?? null
5602
5825
  });
5603
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
+ });
5604
5847
  program
5605
5848
  .command("wizard")
5606
5849
  .alias("start")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.31",
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",