salesprompter-cli 0.1.33 → 0.1.35

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 +306 -20
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -42,6 +42,32 @@ const runtimeOutputOptions = {
42
42
  quiet: false
43
43
  };
44
44
  const nullableOptionalString = z.string().min(1).nullish().transform((value) => value ?? undefined);
45
+ const CliWorkspaceSchema = z.object({
46
+ id: z.string().min(1),
47
+ name: nullableOptionalString,
48
+ slug: nullableOptionalString,
49
+ workspaceClientId: nullableOptionalString,
50
+ workspaceClientName: nullableOptionalString
51
+ });
52
+ const CliWorkspaceListResponseSchema = z.object({
53
+ workspaces: z.array(CliWorkspaceSchema),
54
+ currentOrgId: nullableOptionalString
55
+ });
56
+ const CliAuthUserSchema = z.object({
57
+ id: z.string().min(1),
58
+ email: z.string().email(),
59
+ name: nullableOptionalString,
60
+ orgId: nullableOptionalString,
61
+ orgName: nullableOptionalString,
62
+ orgSlug: nullableOptionalString,
63
+ workspaceClientId: nullableOptionalString,
64
+ workspaceClientName: nullableOptionalString
65
+ });
66
+ const CliWorkspaceSwitchResponseSchema = z.object({
67
+ token: z.string().min(1),
68
+ expiresAt: z.string().datetime().optional(),
69
+ user: CliAuthUserSchema
70
+ });
45
71
  const LinkedInCompanyBackfillClientIdStateSchema = z
46
72
  .object({
47
73
  clientId: z.number().int().positive(),
@@ -2564,6 +2590,37 @@ function resolveSessionOrgId(session) {
2564
2590
  const orgId = session.user.orgId?.trim();
2565
2591
  return orgId && orgId.length > 0 ? orgId : null;
2566
2592
  }
2593
+ function normalizeCliApiBaseUrl(value) {
2594
+ return value.trim().replace(/\/+$/, "");
2595
+ }
2596
+ function getWorkspaceDisplayName(workspace) {
2597
+ return (compactOptionalText(workspace.name) ??
2598
+ compactOptionalText(workspace.workspaceClientName) ??
2599
+ compactOptionalText(workspace.slug) ??
2600
+ workspace.id);
2601
+ }
2602
+ function formatCliWorkspaceLabel(workspace) {
2603
+ const name = getWorkspaceDisplayName(workspace);
2604
+ const details = [
2605
+ compactOptionalText(workspace.slug),
2606
+ compactOptionalText(workspace.workspaceClientId),
2607
+ workspace.id
2608
+ ].filter((value) => Boolean(value));
2609
+ return details.length > 0 ? `${name} (${details.join(", ")})` : name;
2610
+ }
2611
+ function workspaceMatchesInput(workspace, value) {
2612
+ const normalized = normalizeChoiceText(value);
2613
+ return [
2614
+ workspace.id,
2615
+ workspace.name,
2616
+ workspace.slug,
2617
+ workspace.workspaceClientId,
2618
+ workspace.workspaceClientName
2619
+ ].some((candidate) => {
2620
+ const compacted = compactOptionalText(candidate);
2621
+ return compacted ? normalizeChoiceText(compacted) === normalized : false;
2622
+ });
2623
+ }
2567
2624
  function writeSessionSummary(session) {
2568
2625
  const identity = session.user.name?.trim()
2569
2626
  ? `${session.user.name} (${session.user.email})`
@@ -2748,6 +2805,21 @@ async function promptChoice(rl, prompt, options, defaultValue) {
2748
2805
  writeWizardLine();
2749
2806
  }
2750
2807
  }
2808
+ async function withPromptReader(existingReader, run) {
2809
+ if (existingReader) {
2810
+ return await run(existingReader);
2811
+ }
2812
+ const rl = createInterface({
2813
+ input: process.stdin,
2814
+ output: process.stdout
2815
+ });
2816
+ try {
2817
+ return await run(rl);
2818
+ }
2819
+ finally {
2820
+ rl.close();
2821
+ }
2822
+ }
2751
2823
  async function promptText(rl, prompt, options = {}) {
2752
2824
  while (true) {
2753
2825
  const suffix = options.defaultValue !== undefined ? ` [${options.defaultValue}]` : "";
@@ -2764,6 +2836,87 @@ async function promptText(rl, prompt, options = {}) {
2764
2836
  writeWizardLine("This field is required.");
2765
2837
  }
2766
2838
  }
2839
+ async function promptLongPastedText(rl, prompt, options = {}) {
2840
+ if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
2841
+ return await promptText(rl, prompt, options);
2842
+ }
2843
+ const suffix = options.defaultValue !== undefined ? ` [${options.defaultValue}]` : "";
2844
+ while (true) {
2845
+ const answer = await new Promise((resolve, reject) => {
2846
+ const stdin = process.stdin;
2847
+ let value = "";
2848
+ let finished = false;
2849
+ const cleanup = () => {
2850
+ if (finished) {
2851
+ return;
2852
+ }
2853
+ finished = true;
2854
+ stdin.off("data", onData);
2855
+ process.off("SIGINT", onSigint);
2856
+ if (stdin.isTTY) {
2857
+ stdin.setRawMode(false);
2858
+ }
2859
+ stdin.pause();
2860
+ rl.resume?.();
2861
+ };
2862
+ const finish = () => {
2863
+ cleanup();
2864
+ process.stdout.write("\n");
2865
+ resolve(value.trim());
2866
+ };
2867
+ const cancel = () => {
2868
+ cleanup();
2869
+ process.stdout.write("\n");
2870
+ reject(new Error("prompt cancelled"));
2871
+ };
2872
+ const onSigint = () => {
2873
+ cancel();
2874
+ };
2875
+ const onData = (chunk) => {
2876
+ for (const byte of chunk) {
2877
+ if (byte === 3) {
2878
+ cancel();
2879
+ return;
2880
+ }
2881
+ if (byte === 13 || byte === 10) {
2882
+ finish();
2883
+ return;
2884
+ }
2885
+ if (byte === 127 || byte === 8) {
2886
+ if (value.length > 0) {
2887
+ value = value.slice(0, -1);
2888
+ process.stdout.write("\b \b");
2889
+ }
2890
+ continue;
2891
+ }
2892
+ // Ignore escape sequences for arrows and other terminal controls.
2893
+ if (byte === 27) {
2894
+ continue;
2895
+ }
2896
+ const character = Buffer.from([byte]).toString("utf8");
2897
+ value += character;
2898
+ process.stdout.write(character);
2899
+ }
2900
+ };
2901
+ rl.pause?.();
2902
+ process.stdout.write(`${prompt}${suffix}: `);
2903
+ stdin.setRawMode(true);
2904
+ stdin.resume();
2905
+ stdin.on("data", onData);
2906
+ process.on("SIGINT", onSigint);
2907
+ });
2908
+ if (answer.length > 0) {
2909
+ return answer;
2910
+ }
2911
+ if (options.defaultValue !== undefined) {
2912
+ return options.defaultValue;
2913
+ }
2914
+ if (!options.required) {
2915
+ return "";
2916
+ }
2917
+ writeWizardLine("This field is required.");
2918
+ }
2919
+ }
2767
2920
  async function promptYesNo(rl, prompt, defaultValue) {
2768
2921
  while (true) {
2769
2922
  const answer = (await rl.question(`${prompt} [${defaultValue ? "Y/n" : "y/N"}]: `)).trim().toLowerCase();
@@ -2828,7 +2981,7 @@ async function confirmWizardWorkspace(rl, session, options) {
2828
2981
  ? hasNamedOrg
2829
2982
  ? "Current cached CLI workspace"
2830
2983
  : "Workspace name is not available in this cached token"
2831
- : "Choose another workspace in the browser if this account belongs to more than one";
2984
+ : "Select from your Salesprompter organizations in this terminal";
2832
2985
  const workspaceChoice = await promptChoice(rl, "Which workspace should I use?", [
2833
2986
  {
2834
2987
  value: "current",
@@ -2838,8 +2991,8 @@ async function confirmWizardWorkspace(rl, session, options) {
2838
2991
  },
2839
2992
  {
2840
2993
  value: "browser",
2841
- label: "Choose another workspace in the browser",
2842
- description: "Opens Salesprompter so you can pick from your organizations",
2994
+ label: "Choose another workspace",
2995
+ description: "Select from your Salesprompter organizations in this terminal",
2843
2996
  aliases: ["browser", "choose another", "switch workspace", "select organization"]
2844
2997
  }
2845
2998
  ], "current");
@@ -2848,12 +3001,10 @@ async function confirmWizardWorkspace(rl, session, options) {
2848
3001
  return session;
2849
3002
  }
2850
3003
  writeWizardLine();
2851
- writeWizardLine("Choose the workspace for this CLI session in the browser.");
2852
- writeWizardLine();
2853
- await clearAuthSession();
2854
- const result = await performLogin({
3004
+ const result = await switchWorkspaceInCli({
2855
3005
  apiUrl: options?.apiUrl ?? session.apiBaseUrl,
2856
- timeoutSeconds: options?.timeoutSeconds ?? 180
3006
+ timeoutSeconds: options?.timeoutSeconds,
3007
+ rl
2857
3008
  });
2858
3009
  writeSessionSummary(result.session);
2859
3010
  writeWizardLine();
@@ -2866,6 +3017,134 @@ async function switchWorkspaceWithBrowser(options) {
2866
3017
  timeoutSeconds: options?.timeoutSeconds ?? 180
2867
3018
  })).session;
2868
3019
  }
3020
+ async function listCliWorkspaces(session) {
3021
+ const { session: refreshedSession, value } = await fetchCliJson(session, async (currentSession) => await fetch(`${normalizeCliApiBaseUrl(currentSession.apiBaseUrl)}/api/cli/auth/workspaces`, {
3022
+ method: "GET",
3023
+ headers: {
3024
+ Authorization: `Bearer ${currentSession.accessToken}`,
3025
+ "X-Salesprompter-Client": "salesprompter-cli/0.2"
3026
+ }
3027
+ }), CliWorkspaceListResponseSchema);
3028
+ return {
3029
+ session: refreshedSession,
3030
+ workspaces: value.workspaces,
3031
+ currentOrgId: value.currentOrgId ?? null
3032
+ };
3033
+ }
3034
+ async function selectCliWorkspace(session, orgId) {
3035
+ const { value } = await fetchCliJson(session, async (currentSession) => await fetch(`${normalizeCliApiBaseUrl(currentSession.apiBaseUrl)}/api/cli/auth/workspaces`, {
3036
+ method: "POST",
3037
+ headers: {
3038
+ Authorization: `Bearer ${currentSession.accessToken}`,
3039
+ "Content-Type": "application/json",
3040
+ "X-Salesprompter-Client": "salesprompter-cli/0.2"
3041
+ },
3042
+ body: JSON.stringify({ orgId })
3043
+ }), CliWorkspaceSwitchResponseSchema);
3044
+ const nextSession = {
3045
+ accessToken: value.token,
3046
+ refreshToken: session.refreshToken,
3047
+ apiBaseUrl: session.apiBaseUrl,
3048
+ user: value.user,
3049
+ expiresAt: value.expiresAt,
3050
+ createdAt: new Date().toISOString()
3051
+ };
3052
+ await writeAuthSession(nextSession);
3053
+ return nextSession;
3054
+ }
3055
+ async function promptForCliWorkspace(rl, workspaces, currentOrgId, requestedWorkspace) {
3056
+ const requested = compactOptionalText(requestedWorkspace);
3057
+ if (requested) {
3058
+ const matched = workspaces.find((workspace) => workspaceMatchesInput(workspace, requested));
3059
+ if (!matched) {
3060
+ throw new Error(`workspace not found for this account: ${requested}`);
3061
+ }
3062
+ return matched.id;
3063
+ }
3064
+ const current = currentOrgId ? workspaces.find((workspace) => workspace.id === currentOrgId) : undefined;
3065
+ const options = workspaces.map((workspace) => ({
3066
+ value: workspace.id,
3067
+ label: formatCliWorkspaceLabel(workspace),
3068
+ description: workspace.id === currentOrgId ? "Current cached CLI workspace" : undefined,
3069
+ aliases: [workspace.name, workspace.slug, workspace.workspaceClientId, workspace.workspaceClientName].filter((value) => Boolean(compactOptionalText(value)))
3070
+ }));
3071
+ options.push({
3072
+ value: "browser",
3073
+ label: "Use browser chooser",
3074
+ description: "Fallback if this terminal list is missing a workspace",
3075
+ aliases: ["browser", "open browser", "web"]
3076
+ });
3077
+ return await promptChoice(rl, "Which workspace should I use?", options, current?.id ?? options[0]?.value ?? "browser");
3078
+ }
3079
+ async function switchWorkspaceInCli(options = {}) {
3080
+ if (options.browser) {
3081
+ return {
3082
+ session: await switchWorkspaceWithBrowser(options),
3083
+ method: "browser"
3084
+ };
3085
+ }
3086
+ let session = null;
3087
+ try {
3088
+ session = await requireAuthSession();
3089
+ if (options.apiUrl) {
3090
+ session = {
3091
+ ...session,
3092
+ apiBaseUrl: normalizeCliApiBaseUrl(options.apiUrl)
3093
+ };
3094
+ }
3095
+ }
3096
+ catch {
3097
+ if (!runtimeOutputOptions.quiet) {
3098
+ writeWizardLine("No cached CLI session found. Starting browser login flow.");
3099
+ writeWizardLine();
3100
+ }
3101
+ return {
3102
+ session: await switchWorkspaceWithBrowser(options),
3103
+ method: "browser"
3104
+ };
3105
+ }
3106
+ let workspaces;
3107
+ let currentOrgId;
3108
+ try {
3109
+ const listed = await listCliWorkspaces(session);
3110
+ session = listed.session;
3111
+ workspaces = listed.workspaces;
3112
+ currentOrgId = listed.currentOrgId;
3113
+ }
3114
+ catch (error) {
3115
+ if (!runtimeOutputOptions.quiet) {
3116
+ const message = error instanceof Error ? error.message : String(error);
3117
+ writeWizardLine(`Terminal workspace selection is unavailable (${message}). Starting browser chooser.`);
3118
+ writeWizardLine();
3119
+ }
3120
+ return {
3121
+ session: await switchWorkspaceWithBrowser(options),
3122
+ method: "browser"
3123
+ };
3124
+ }
3125
+ if (workspaces.length === 0) {
3126
+ if (!runtimeOutputOptions.quiet) {
3127
+ writeWizardLine("No Salesprompter workspaces were returned for this account. Starting browser chooser.");
3128
+ writeWizardLine();
3129
+ }
3130
+ return {
3131
+ session: await switchWorkspaceWithBrowser(options),
3132
+ method: "browser"
3133
+ };
3134
+ }
3135
+ const selectedOrgId = options.orgId ?? (await withPromptReader(options.rl, async (rl) => await promptForCliWorkspace(rl, workspaces, currentOrgId, options.workspace)));
3136
+ if (selectedOrgId === "browser") {
3137
+ return {
3138
+ session: await switchWorkspaceWithBrowser(options),
3139
+ method: "browser"
3140
+ };
3141
+ }
3142
+ const nextSession = await selectCliWorkspace(session, selectedOrgId);
3143
+ return {
3144
+ session: nextSession,
3145
+ method: "terminal"
3146
+ };
3147
+ }
2869
3148
  async function resolveLlmAuthReadiness() {
2870
3149
  const apiBaseUrl = process.env.SALESPROMPTER_API_BASE_URL?.trim() || "https://salesprompter.ai";
2871
3150
  const envToken = resolveNonInteractiveAuthToken(process.env);
@@ -5234,7 +5513,7 @@ async function runDirectSalesNavigatorSearchWizard(input) {
5234
5513
  }
5235
5514
  async function runProductMarketWizard(rl) {
5236
5515
  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.");
5237
- const input = await promptText(rl, "What company website or LinkedIn page should I start from?", {
5516
+ const input = await promptLongPastedText(rl, "What company website or LinkedIn page should I start from?", {
5238
5517
  required: true
5239
5518
  });
5240
5519
  if (isSalesNavigatorPeopleSearchUrl(input)) {
@@ -5485,10 +5764,11 @@ async function runWizard(options) {
5485
5764
  ], "product-market");
5486
5765
  writeWizardLine();
5487
5766
  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);
5767
+ const result = await switchWorkspaceInCli({
5768
+ ...options,
5769
+ rl
5770
+ });
5771
+ writeSessionSummary(result.session);
5492
5772
  writeWizardLine();
5493
5773
  continue;
5494
5774
  }
@@ -5829,19 +6109,25 @@ program
5829
6109
  .alias("auth:switch")
5830
6110
  .description("Switch the active Salesprompter workspace for this CLI session.")
5831
6111
  .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")
6112
+ .option("--timeout-seconds <number>", "Browser fallback login timeout in seconds", "180")
6113
+ .option("--org-id <id>", "Switch directly to a Clerk organization id")
6114
+ .option("--workspace <nameOrSlug>", "Switch directly to a workspace by name, slug, client id, or org id")
6115
+ .option("--browser", "Use the browser chooser instead of terminal workspace selection")
5833
6116
  .action(async (options) => {
5834
6117
  const timeoutSeconds = z.coerce.number().int().min(30).max(1800).parse(options.timeoutSeconds);
5835
- const session = await switchWorkspaceWithBrowser({
6118
+ const result = await switchWorkspaceInCli({
5836
6119
  apiUrl: options.apiUrl,
5837
- timeoutSeconds
6120
+ timeoutSeconds,
6121
+ orgId: options.orgId,
6122
+ workspace: options.workspace,
6123
+ browser: Boolean(options.browser)
5838
6124
  });
5839
6125
  printOutput({
5840
6126
  status: "ok",
5841
- method: "browser",
5842
- apiBaseUrl: session.apiBaseUrl,
5843
- user: session.user,
5844
- expiresAt: session.expiresAt ?? null
6127
+ method: result.method,
6128
+ apiBaseUrl: result.session.apiBaseUrl,
6129
+ user: result.session.user,
6130
+ expiresAt: result.session.expiresAt ?? null
5845
6131
  });
5846
6132
  });
5847
6133
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
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",