@audienti/cli 0.1.76 → 0.1.78

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/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to the Audienti CLI are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.78] - 2026-09-27
8
+
9
+ ### Fixed
10
+
11
+ - Fix `audienti setup` and `audienti motions quick-start --confirm` failing with HTTP 400 when creating the drafted motion.
12
+ - When drafting fails because the drafting service is down, `audienti setup` now says to try again in a few minutes instead of only suggesting a different website.
13
+
14
+ ## [0.1.77] - 2026-09-27
15
+
16
+ ### Added
17
+
18
+ - Add `audienti start`, a friendly first run: it opens the browser so you can sign in or create an account, picks your account and account user (automatically when there is only one), and offers to run setup. Running `audienti` with no saved login now starts it; `audienti --help` is unchanged.
19
+ - Add `audienti setup`, a guided wizard that asks for your company website, who you sell to, and what you want prospects to say yes to, drafts your first motion, shows the draft, and asks before creating it. Scripts and agents can pass `--url`, `--sell-to`, `--ask`, `--yes`, and `--json`; without a terminal it never prompts. `setup play preflight` is unchanged.
20
+ - The install script now runs `audienti start` for new users at a terminal, only reports the new version when you are already signed in, and prints next steps when there is no terminal.
21
+ - `motions quick-start --json` includes a `preview` summary of a ready draft.
22
+
7
23
  ## [0.1.76] - 2026-09-27
8
24
 
9
25
  ### Added
package/README.md CHANGED
@@ -17,18 +17,45 @@ Requires Node.js 20 or newer.
17
17
 
18
18
  ```bash
19
19
  curl -fsSL https://cli.audienti.com/install | bash
20
- audienti --help
21
20
  ```
22
21
 
23
- Or install directly through npm:
22
+ At a terminal, the installer runs `audienti start` for you. Or install directly
23
+ through npm, then start:
24
24
 
25
25
  ```bash
26
26
  npm install --global @audienti/cli
27
- audienti --help
27
+ audienti start
28
28
  ```
29
29
 
30
30
  For one-off use, run `npx @audienti/cli --help`.
31
31
 
32
+ ## First run
33
+
34
+ ```bash
35
+ audienti start
36
+ audienti setup
37
+ ```
38
+
39
+ `audienti start` opens Audienti in your browser so you can sign in or create an
40
+ account, then picks your account and account user (automatically when there is
41
+ only one) and offers to run setup.
42
+
43
+ `audienti setup` asks for your company website, who you sell to, and what you
44
+ want prospects to say yes to. It drafts your first motion, shows the draft, and
45
+ asks `Create this?` before saving anything. Next, connect LinkedIn. Nothing is
46
+ sent without your approval.
47
+
48
+ Without a terminal, `audienti start` never prompts and reports what to do next,
49
+ and `setup` takes flags:
50
+
51
+ ```bash
52
+ audienti setup --url https://acme.com --sell-to "<who you sell to>" --ask "<what they say yes to>" --yes --json
53
+ ```
54
+
55
+ Without `--yes`, a non-interactive setup drafts but creates nothing. Running
56
+ `audienti` with no saved login starts `audienti start`; `audienti --help` still
57
+ prints help.
58
+
32
59
  ## Authenticate
33
60
 
34
61
  Authenticate through the browser, then configure the default account context:
package/install CHANGED
@@ -52,11 +52,32 @@ fi
52
52
 
53
53
  "$binary" --help >/dev/null
54
54
 
55
+ installed_version="$(node -e 'try { process.stdout.write(require(process.argv[1]).version || "") } catch {}' "$(npm root --global 2>/dev/null || true)/${package}/package.json" 2>/dev/null || true)"
56
+
57
+ # A saved login means this is an upgrade: stay quiet.
58
+ if "$binary" config list --json 2>/dev/null | node -e 'let s="";process.stdin.on("data",(c)=>s+=c).on("end",()=>{try{process.exit(JSON.parse(s).token?0:1)}catch{process.exit(1)}})'; then
59
+ say "Audienti CLI updated to ${installed_version:-the latest version}."
60
+ exit 0
61
+ fi
62
+
63
+ # `curl ... | bash` feeds this script on stdin, so the guided start must read
64
+ # the keyboard from the terminal device instead.
65
+ tty_device="${AUDIENTI_INSTALL_TTY:-/dev/tty}"
66
+ if (: <"$tty_device") 2>/dev/null; then
67
+ say "Audienti CLI${installed_version:+ ${installed_version}} installed."
68
+ say ""
69
+ "$binary" start <"$tty_device" || say "You can pick up where you left off with: audienti start"
70
+ exit 0
71
+ fi
72
+
55
73
  cat <<'NEXT'
56
74
  Audienti CLI installed.
57
75
 
58
76
  Next:
77
+ audienti start Sign in or create an account (run it in a terminal)
78
+
79
+ Without a terminal (CI or a coding agent):
59
80
  audienti auth token <token>
60
- audienti accounts list --json
61
81
  audienti accounts select <acct_id>
82
+ audienti setup --url <company_url> --yes
62
83
  NEXT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
4
4
  "description": "Agent-first command-line client for Audienti.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,13 +31,26 @@ curl -fsSL https://cli.audienti.com/install | bash
31
31
  ```
32
32
 
33
33
  3. Authentication is explicit and per machine. Do not ask a user to paste a
34
- production token into chat, a repository file, an issue, or a CI secret. Prefer
35
- browser login:
34
+ production token into chat, a repository file, an issue, or a CI secret. For a
35
+ person at a terminal, the friendly first run is:
36
+
37
+ ```bash
38
+ audienti start
39
+ audienti setup
40
+ ```
41
+
42
+ `audienti start` signs in through the browser (or creates an account) and picks
43
+ the account. `audienti start --json` never prompts; it reports
44
+ `sign_in_required` or `ready`. Browser login alone is:
36
45
 
37
46
  ```bash
38
47
  audienti auth login
39
48
  ```
40
49
 
50
+ `audienti setup --url <company_url> --sell-to "<who they sell to>" --ask "<what
51
+ prospects say yes to>" --json` drafts a first motion without creating it. Show
52
+ the draft to the user and add `--yes` only after they approve creating it.
53
+
41
54
  Use `audienti auth token` only after the user supplies a token through an
42
55
  approved secure channel.
43
56
 
package/src/cli.js CHANGED
@@ -22,6 +22,12 @@ const API_MAX_LIST_LIMIT = 100;
22
22
  const DEFAULT_LOOKUP_TIMEOUT_SECONDS = 60;
23
23
  const DEFAULT_LOOKUP_POLL_INTERVAL_SECONDS = 2;
24
24
  const DEFAULT_AUTH_LOGIN_TIMEOUT_SECONDS = 180;
25
+ // Start covers signup plus email confirmation, which takes longer than a sign-in.
26
+ const DEFAULT_START_LOGIN_TIMEOUT_SECONDS = 900;
27
+ const DEFAULT_SETUP_TIMEOUT_SECONDS = 300;
28
+ const START_USAGE = "Usage: audienti start [--host https://app.audienti.com] [--no-open] [--json]";
29
+ const SETUP_USAGE = "Usage: audienti setup [--url <company_url>] [--sell-to <who you sell to>] [--ask <what the prospect says yes to>] [--yes] [--json]";
30
+ const SETUP_FLAGS_EXAMPLE = "audienti setup --url https://acme.com --sell-to \"<who you sell to>\" --ask \"<what they say yes to>\" --yes";
25
31
  const DEFAULT_WRITER_TEST_RUN_TIMEOUT_SECONDS = 180;
26
32
  const DEFAULT_WRITER_TEST_RUN_POLL_INTERVAL_SECONDS = 2;
27
33
  const PACKAGE_NAME = "@audienti/cli";
@@ -287,6 +293,7 @@ export async function run(argv = process.argv.slice(2), deps = {}) {
287
293
 
288
294
  async function dispatch(argv, context) {
289
295
  const { args, accountOverride } = extractGlobalOptions(argv);
296
+ if (args.length === 0 && !(await readConfig({ env: context.env })).token) return start([], context);
290
297
  const helpTopic = helpTopicFromArgs(args);
291
298
 
292
299
  if (helpTopic) {
@@ -317,6 +324,8 @@ async function dispatch(argv, context) {
317
324
  if (normalizedResource === "users" && action === "select") return usersSelect(rest, context, { accountOverride });
318
325
  if (normalizedResource === "users" && action === "activity") return usersActivity(rest, context, { accountOverride });
319
326
  if (normalizedResource === "users" && action === "automation") return usersAutomation(rest, context, { accountOverride });
327
+ if (normalizedResource === "start") return start(args.slice(1), context);
328
+ if (normalizedResource === "setup" && (action === undefined || action.startsWith("--"))) return setupWizard(args.slice(1), context, { accountOverride });
320
329
  if (normalizedResource === "setup" && action === "play" && rest[0] === "preflight") return setupPlayPreflight(rest.slice(1), context, { accountOverride });
321
330
  if (normalizedResource === "social-cookies" && action === "sync-messages") return socialCookiesSyncMessages(rest, context, { accountOverride });
322
331
  if (normalizedResource === "offers" && action === "list") return offersList(rest, context, { accountOverride });
@@ -596,25 +605,50 @@ async function authLogin(args, context) {
596
605
 
597
606
  const host = normalizeHost(values.host || DEFAULT_HOST);
598
607
  const timeoutSeconds = normalizeOptionalPositiveInteger(values["timeout-seconds"], "--timeout-seconds") || DEFAULT_AUTH_LOGIN_TIMEOUT_SECONDS;
608
+ const { client, user, payload } = await browserSignIn({
609
+ host,
610
+ timeoutSeconds,
611
+ open: !values["no-open"],
612
+ context,
613
+ announce(authUrl, callbackUrl) {
614
+ if (values.json) {
615
+ writeJson(context.stdout, {
616
+ kind: "auth_login_start",
617
+ auth_url: authUrl,
618
+ callback_url: callbackUrl,
619
+ timeout_seconds: timeoutSeconds
620
+ });
621
+ } else {
622
+ writeLine(context.stdout, "Open this URL to authenticate Audienti CLI:");
623
+ writeLine(context.stdout, authUrl);
624
+ }
625
+ }
626
+ });
627
+
628
+ const result = {
629
+ kind: "auth_login_complete",
630
+ host: client.host,
631
+ user: user?.name || payload.user_name || payload.user_email || user?.email || null,
632
+ account_id: payload.account_id || null,
633
+ account_name: payload.account_name || null
634
+ };
635
+
636
+ if (values.json) return writeJson(context.stdout, result);
637
+
638
+ writeLine(context.stdout, `Authenticated to ${client.host} as ${result.user || "current user"}.`);
639
+ if (result.account_id) writeLine(context.stdout, `Selected account ${result.account_name || result.account_id} (${result.account_id}).`);
640
+ if (!result.account_id) writeLine(context.stdout, "Run `audienti accounts list` to choose an account.");
641
+ }
642
+
643
+ async function browserSignIn({ host, timeoutSeconds, open, context, announce }) {
599
644
  const state = randomBytes(24).toString("hex");
600
645
  const callback = await createAuthCallbackServer({ state, context });
601
646
  const authUrl = new URL("/cli/auth", host);
602
647
  authUrl.searchParams.set("redirect_uri", callback.redirectUri);
603
648
  authUrl.searchParams.set("state", state);
604
649
 
605
- if (values.json) {
606
- writeJson(context.stdout, {
607
- kind: "auth_login_start",
608
- auth_url: authUrl.toString(),
609
- callback_url: callback.redirectUri,
610
- timeout_seconds: timeoutSeconds
611
- });
612
- } else {
613
- writeLine(context.stdout, "Open this URL to authenticate Audienti CLI:");
614
- writeLine(context.stdout, authUrl.toString());
615
- }
616
-
617
- if (!values["no-open"]) openBrowser(authUrl.toString(), context);
650
+ announce(authUrl.toString(), callback.redirectUri);
651
+ if (open) openBrowser(authUrl.toString(), context);
618
652
 
619
653
  let payload;
620
654
  try {
@@ -632,19 +666,473 @@ async function authLogin(args, context) {
632
666
  accountName: payload.account_name || undefined
633
667
  }, { env: context.env });
634
668
 
635
- const result = {
636
- kind: "auth_login_complete",
637
- host: client.host,
638
- user: user?.name || payload.user_name || payload.user_email || user?.email || null,
639
- account_id: payload.account_id || null,
640
- account_name: payload.account_name || null
669
+ return { client, user, payload };
670
+ }
671
+
672
+ async function start(args, context) {
673
+ const { values, positionals } = parseCommandArgs(args, {
674
+ ...jsonOptions(),
675
+ host: { type: "string" },
676
+ "no-open": { type: "boolean" }
677
+ });
678
+ if (positionals.length > 0) throw new CommandError(START_USAGE);
679
+
680
+ const interactive = stdinIsInteractive(context) && !values.json;
681
+ const say = (line = "") => {
682
+ if (!values.json) writeLine(context.stdout, line);
641
683
  };
684
+ let config = await readConfig({ env: context.env });
685
+ const host = normalizeHost(values.host || config.host || DEFAULT_HOST);
686
+
687
+ say("Welcome to Audienti.");
688
+ say("Audienti finds the people who fit your business, spots when they're ready to talk, and drafts the outreach.");
689
+ say("Nothing sends without your approval.");
690
+ say();
691
+
692
+ let session = await savedSession(config, context);
693
+ if (!session && !interactive) {
694
+ const payload = {
695
+ kind: "start",
696
+ status: "sign_in_required",
697
+ signed_in: false,
698
+ host,
699
+ sign_up_url: new URL("/users/sign_up", host).toString(),
700
+ sign_in_command: "audienti auth login",
701
+ next_command: "audienti start"
702
+ };
703
+ if (values.json) return writeJson(context.stdout, payload);
642
704
 
643
- if (values.json) return writeJson(context.stdout, result);
705
+ say("Sign in or create an account to continue.");
706
+ say(` New here? Create an account at ${payload.sign_up_url}`);
707
+ say(" Then run `audienti auth login` in a terminal (it opens your browser),");
708
+ say(" or save an API token with `audienti auth token <token>`.");
709
+ say("Next: run `audienti start` again.");
710
+ return 0;
711
+ }
644
712
 
645
- writeLine(context.stdout, `Authenticated to ${client.host} as ${result.user || "current user"}.`);
646
- if (result.account_id) writeLine(context.stdout, `Selected account ${result.account_name || result.account_id} (${result.account_id}).`);
647
- if (!result.account_id) writeLine(context.stdout, "Run `audienti accounts list` to choose an account.");
713
+ const prompter = createPrompter(context);
714
+ try {
715
+ if (session) {
716
+ say(`You're signed in as ${session.label}.`);
717
+ } else {
718
+ say("First, sign in or create an account.");
719
+ const { client, user, payload } = await browserSignIn({
720
+ host,
721
+ timeoutSeconds: DEFAULT_START_LOGIN_TIMEOUT_SECONDS,
722
+ open: !values["no-open"],
723
+ context,
724
+ announce(authUrl) {
725
+ say("We'll open Audienti in your browser. New here? Choose \"sign up for an account\" on that page.");
726
+ say("If the browser doesn't open, visit:");
727
+ say(` ${authUrl}`);
728
+ say("Waiting for you to finish in the browser (confirm your email if asked)...");
729
+ }
730
+ });
731
+ config = await readConfig({ env: context.env });
732
+ session = { client, label: user?.name || payload.user_name || user?.email || payload.user_email || "you" };
733
+ say(`Signed in as ${session.label}.`);
734
+ }
735
+
736
+ const account = await chooseStartAccount(session.client, config, { interactive, prompter, say });
737
+ const accountUser = account ? await chooseStartAccountUser(session.client, account, { interactive, prompter, say }) : null;
738
+ if (account) {
739
+ const sameAccount = account.prefix_id === config.accountId;
740
+ await writeConfig({
741
+ ...config,
742
+ accountId: account.prefix_id,
743
+ accountName: account.name,
744
+ accountUserId: accountUser ? String(accountUser.id) : sameAccount ? config.accountUserId : undefined,
745
+ accountUserName: accountUser ? accountUser.name : sameAccount ? config.accountUserName : undefined,
746
+ accountUserEmail: accountUser ? accountUser.email : sameAccount ? config.accountUserEmail : undefined
747
+ }, { env: context.env });
748
+ }
749
+
750
+ const status = !account ? "account_selection_required" : !accountUser ? "account_user_selection_required" : "ready";
751
+ const nextCommand = status === "account_selection_required"
752
+ ? "audienti accounts select <acct_id>"
753
+ : status === "account_user_selection_required"
754
+ ? "audienti users select <account_user_id|email|name|me>"
755
+ : interactive ? "audienti setup" : SETUP_FLAGS_EXAMPLE;
756
+
757
+ if (values.json) {
758
+ return writeJson(context.stdout, {
759
+ kind: "start",
760
+ status,
761
+ signed_in: true,
762
+ host: session.client.host,
763
+ user: session.label,
764
+ account: account ? { id: account.prefix_id, name: account.name } : null,
765
+ account_user: accountUser ? { id: accountUser.id, name: accountUser.name || null, email: accountUser.email || null } : null,
766
+ next_command: nextCommand
767
+ });
768
+ }
769
+
770
+ if (status !== "ready" || !interactive) {
771
+ say(`Next: ${nextCommand}`);
772
+ return 0;
773
+ }
774
+
775
+ say();
776
+ if (await askYesNo(prompter, "Set up your first motion now? [Y/n] ", { defaultYes: true })) {
777
+ say();
778
+ return setupWizard([], context, { prompter });
779
+ }
780
+
781
+ say("No problem. When you're ready, run: audienti setup");
782
+ return 0;
783
+ } finally {
784
+ prompter.close();
785
+ }
786
+ }
787
+
788
+ async function savedSession(config, context) {
789
+ if (!config.token) return null;
790
+
791
+ const client = clientFromConfig(config, context);
792
+ try {
793
+ const user = await client.me();
794
+ return { client, label: user?.name || user?.email || "you" };
795
+ } catch (error) {
796
+ if (error instanceof ApiError && error.status === 401) return null;
797
+ throw error;
798
+ }
799
+ }
800
+
801
+ async function chooseStartAccount(client, config, { interactive, prompter, say }) {
802
+ const accounts = await client.accounts();
803
+ if (!Array.isArray(accounts) || accounts.length === 0) {
804
+ throw new CommandError("This login has no Audienti account yet. Finish creating one in the browser, then run `audienti start` again.");
805
+ }
806
+
807
+ const saved = accounts.find((account) => account.prefix_id === config.accountId);
808
+ if (accounts.length === 1) {
809
+ say(`Using account ${accounts[0].name} (${accounts[0].prefix_id}).`);
810
+ return accounts[0];
811
+ }
812
+
813
+ if (!interactive) {
814
+ if (saved) {
815
+ say(`Using account ${saved.name} (${saved.prefix_id}).`);
816
+ return saved;
817
+ }
818
+ say("You have several accounts:");
819
+ for (const account of accounts) say(` ${account.prefix_id}\t${account.name}`);
820
+ return null;
821
+ }
822
+
823
+ say("Which account should the CLI use?");
824
+ accounts.forEach((account, index) => {
825
+ say(` ${index + 1}. ${account.name} (${account.prefix_id})${account === saved ? " - current" : ""}`);
826
+ });
827
+ const defaultIndex = saved ? accounts.indexOf(saved) + 1 : null;
828
+ const account = await askChoice(prompter, accounts, {
829
+ question: defaultIndex ? `Account [${defaultIndex}]: ` : "Account: ",
830
+ defaultChoice: saved,
831
+ resolve: (answer) => resolveAccountSelection(accounts, answer)
832
+ });
833
+ say(`Using account ${account.name} (${account.prefix_id}).`);
834
+ return account;
835
+ }
836
+
837
+ async function chooseStartAccountUser(client, account, { interactive, prompter, say }) {
838
+ const users = await client.users(account.prefix_id);
839
+ const rows = Array.isArray(users) ? users : [];
840
+ const currentUsers = rows.filter((candidate) => candidate.current);
841
+ const automatic = currentUsers.length === 1 ? currentUsers[0] : rows.length === 1 ? rows[0] : null;
842
+ if (automatic) {
843
+ say(`Working as ${accountUserLabel(automatic)}.`);
844
+ return automatic;
845
+ }
846
+
847
+ if (!interactive || rows.length === 0) {
848
+ say("Pick which account user you are:");
849
+ for (const accountUser of rows) say(` ${accountUserLabel(accountUser)}`);
850
+ return null;
851
+ }
852
+
853
+ say("Which account user are you?");
854
+ rows.forEach((accountUser, index) => say(` ${index + 1}. ${accountUserLabel(accountUser)}`));
855
+ const accountUser = await askChoice(prompter, rows, {
856
+ question: "Account user: ",
857
+ resolve: (answer) => resolveAccountUserSelection(rows, answer)
858
+ });
859
+ say(`Working as ${accountUserLabel(accountUser)}.`);
860
+ return accountUser;
861
+ }
862
+
863
+ async function askChoice(prompter, choices, { question, defaultChoice = null, resolve }) {
864
+ for (;;) {
865
+ const answer = await prompter.ask(question);
866
+ if (!answer && defaultChoice) return defaultChoice;
867
+
868
+ const number = Number(answer);
869
+ if (Number.isInteger(number) && number >= 1 && number <= choices.length) return choices[number - 1];
870
+
871
+ try {
872
+ const match = answer ? resolve(answer) : null;
873
+ if (match) return match;
874
+ } catch (error) {
875
+ if (!(error instanceof CommandError)) throw error;
876
+ }
877
+ prompter.say(`Type a number from 1 to ${choices.length}.`);
878
+ }
879
+ }
880
+
881
+ async function askYesNo(prompter, question, { defaultYes = false } = {}) {
882
+ for (;;) {
883
+ const answer = (await prompter.ask(question)).toLowerCase();
884
+ if (!answer) return defaultYes;
885
+ if (/^y(es)?$/.test(answer)) return true;
886
+ if (/^no?$/.test(answer)) return false;
887
+ prompter.say("Please answer y or n.");
888
+ }
889
+ }
890
+
891
+ function stdinIsInteractive(context) {
892
+ return Boolean(context.stdin && context.stdin.isTTY);
893
+ }
894
+
895
+ // One line reader per command run. The async iterator buffers lines typed
896
+ // ahead of a question, so several questions can share one input stream.
897
+ function createPrompter(context) {
898
+ let reader;
899
+ let lines;
900
+
901
+ return {
902
+ async ask(question) {
903
+ if (!lines) {
904
+ reader = createInterface({ input: context.stdin, terminal: false });
905
+ lines = reader[Symbol.asyncIterator]();
906
+ }
907
+ context.stdout.write(question);
908
+ const { value, done } = await lines.next();
909
+ if (done) throw new CommandError("Input ended before the questions were answered. Run the command again when you're ready.");
910
+ return String(value).trim();
911
+ },
912
+ say(line = "") {
913
+ writeLine(context.stdout, line);
914
+ },
915
+ close() {
916
+ reader?.close();
917
+ }
918
+ };
919
+ }
920
+
921
+ const SETUP_QUESTIONS = [
922
+ {
923
+ key: "url",
924
+ title: "What's your company website?",
925
+ good: "https://acme.com",
926
+ bad: "acme (not a full web address)",
927
+ skip: "Press Enter to use the website saved on your account."
928
+ },
929
+ {
930
+ key: "sell-to",
931
+ title: "Who do you sell to? One sentence.",
932
+ good: "Heads of finance at US software companies with 50-500 people.",
933
+ bad: "Everyone who could use our product.",
934
+ skip: "Press Enter to skip."
935
+ },
936
+ {
937
+ key: "ask",
938
+ title: "What do you want the prospect to say yes to?",
939
+ good: "A 20-minute call to see if our audit fits their month-end close.",
940
+ bad: "Buy now.",
941
+ skip: "Press Enter to skip."
942
+ }
943
+ ];
944
+
945
+ async function setupWizard(args, context, { accountOverride, prompter: sharedPrompter } = {}) {
946
+ const { values, positionals } = parseCommandArgs(args, {
947
+ ...jsonOptions(),
948
+ url: { type: "string" },
949
+ "sell-to": { type: "string" },
950
+ ask: { type: "string" },
951
+ yes: { type: "boolean" }
952
+ });
953
+ if (positionals.length > 0) throw new CommandError(SETUP_USAGE);
954
+
955
+ const interactive = stdinIsInteractive(context) && !values.json;
956
+ const say = (line = "") => {
957
+ if (!values.json) writeLine(context.stdout, line);
958
+ };
959
+
960
+ if (!interactive && !values.url) {
961
+ throw new CommandError([
962
+ "No terminal is attached, so `audienti setup` can't ask questions.",
963
+ "Pass the answers as flags instead:",
964
+ ` ${SETUP_FLAGS_EXAMPLE}`,
965
+ SETUP_USAGE
966
+ ].join("\n"));
967
+ }
968
+
969
+ const config = await readConfig({ env: context.env });
970
+ if (!config.token) {
971
+ throw new CommandError("You're not signed in yet. Run `audienti start` to sign in or create an account, then run `audienti setup`.");
972
+ }
973
+ const accountId = accountOverride || config.accountId;
974
+ if (!accountId) {
975
+ throw new CommandError("No account is picked yet. Run `audienti start` to choose one, then run `audienti setup`.");
976
+ }
977
+ const client = clientFromConfig(config, context);
978
+ const prompter = sharedPrompter || createPrompter(context);
979
+
980
+ try {
981
+ const answers = { url: values.url, "sell-to": values["sell-to"], ask: values.ask };
982
+ if (interactive) {
983
+ say("Let's set up your first motion. Three quick questions.");
984
+ for (const [index, question] of SETUP_QUESTIONS.entries()) {
985
+ if (answers[question.key] !== undefined) continue;
986
+
987
+ say();
988
+ say(`${index + 1}/${SETUP_QUESTIONS.length} ${question.title}`);
989
+ say(` Good: ${question.good}`);
990
+ say(` Bad: ${question.bad}`);
991
+ say(` ${question.skip}`);
992
+ answers[question.key] = await prompter.ask("> ");
993
+ }
994
+ say();
995
+ }
996
+
997
+ const sellTo = cleanSentence(answers["sell-to"]);
998
+ const ask = cleanSentence(answers.ask);
999
+ const requestBody = {
1000
+ quick_start: compactObject({
1001
+ company_url: String(answers.url || "").trim() || undefined,
1002
+ feedback: setupFeedback({ sellTo, ask })
1003
+ })
1004
+ };
1005
+
1006
+ say("Reading your website and drafting your first motion. This usually takes a minute or two.");
1007
+ let draft;
1008
+ try {
1009
+ draft = await client.createQuickStart(accountId, requestBody);
1010
+ } catch (error) {
1011
+ throw setupCreateError(error);
1012
+ }
1013
+
1014
+ let polls = 0;
1015
+ draft = await waitForQuickStartDraft(client, accountId, draft, context, {
1016
+ timeoutSeconds: DEFAULT_SETUP_TIMEOUT_SECONDS,
1017
+ pollIntervalSeconds: DEFAULT_LOOKUP_POLL_INTERVAL_SECONDS,
1018
+ onPoll() {
1019
+ polls += 1;
1020
+ if (polls % 5 === 1) say(SETUP_PROGRESS[Math.min(Math.floor(polls / 5), SETUP_PROGRESS.length - 1)]);
1021
+ }
1022
+ });
1023
+
1024
+ if (draft?.status === "failed") throw setupDraftFailedError(draft?.error);
1025
+ if (!quickStartReady(draft)) {
1026
+ throw new CommandError("The draft is taking longer than usual. Nothing was created. Run `audienti setup` again in a few minutes.");
1027
+ }
1028
+
1029
+ renderSetupDraft(draft, { ask }, say);
1030
+
1031
+ let confirmed = Boolean(values.yes);
1032
+ if (!confirmed && interactive) confirmed = await askYesNo(prompter, "Create this? [y/N] ");
1033
+
1034
+ if (!confirmed) {
1035
+ const payload = {
1036
+ kind: "setup",
1037
+ status: "draft_ready",
1038
+ created: false,
1039
+ draft,
1040
+ next_command: interactive ? "audienti setup" : "Re-run with --yes to create it."
1041
+ };
1042
+ if (values.json) return writeJson(context.stdout, payload);
1043
+
1044
+ say("Nothing was created.");
1045
+ say(interactive
1046
+ ? "Run `audienti setup` again whenever you're ready. Changing your answers changes the draft."
1047
+ : "Re-run the same command with --yes to create it.");
1048
+ return 0;
1049
+ }
1050
+
1051
+ const confirmation = await client.confirmQuickStart(accountId, draft.id, {});
1052
+ const connectUrl = new URL("/user/social_cookies", client.host).toString();
1053
+ if (values.json) {
1054
+ return writeJson(context.stdout, {
1055
+ kind: "setup",
1056
+ status: "created",
1057
+ created: true,
1058
+ draft,
1059
+ motion: confirmation?.motion || null,
1060
+ reservation: confirmation?.reservation || null,
1061
+ next_step: {
1062
+ connect_linkedin_url: connectUrl,
1063
+ check_command: "audienti setup play preflight"
1064
+ }
1065
+ });
1066
+ }
1067
+
1068
+ const motion = confirmation?.motion || {};
1069
+ say(`Created your first motion: ${display(motion.name)} (${display(motion.prefix_id)}).`);
1070
+ say();
1071
+ say("Next: connect your LinkedIn account so Audienti can reach people for you.");
1072
+ say(` Connect it here: ${connectUrl}`);
1073
+ say(" Then check it with: audienti setup play preflight");
1074
+ say("Nothing is sent until LinkedIn is connected, and nothing sends without your approval.");
1075
+ return 0;
1076
+ } finally {
1077
+ if (!sharedPrompter) prompter.close();
1078
+ }
1079
+ }
1080
+
1081
+ const SETUP_PROGRESS = [
1082
+ "Reading your website...",
1083
+ "Working out who buys from you...",
1084
+ "Drafting your offer and the first buying signals...",
1085
+ "Still working. Thanks for waiting..."
1086
+ ];
1087
+
1088
+ function cleanSentence(value) {
1089
+ return String(value || "").trim().replace(/[\s.]+$/, "");
1090
+ }
1091
+
1092
+ function setupFeedback({ sellTo, ask }) {
1093
+ const parts = [];
1094
+ if (sellTo) parts.push(`We sell to: ${sellTo}.`);
1095
+ if (ask) parts.push(`What we want the prospect to say yes to: ${ask}.`);
1096
+ return parts.join(" ") || undefined;
1097
+ }
1098
+
1099
+ function setupCreateError(error) {
1100
+ if (!(error instanceof ApiError)) return error;
1101
+
1102
+ const reason = typeof error.body?.error === "string" ? cleanSentence(error.body.error) : null;
1103
+ if (error.status === 401) {
1104
+ return new CommandError("Your saved login no longer works. Run `audienti start` to sign in again.");
1105
+ }
1106
+ if (error.status === 403) {
1107
+ return new CommandError("Only an account admin can replace the website already saved on this account. Run `audienti setup` again and press Enter at the website question to use the saved one.");
1108
+ }
1109
+ if (error.status === 422 && error.body?.status === "failed") return setupDraftFailedError(reason);
1110
+ if (error.status === 422) {
1111
+ return new CommandError(`That website didn't work: ${reason || "it was rejected"}. Use a full public address like https://acme.com, then run \`audienti setup\` again.`);
1112
+ }
1113
+ return error;
1114
+ }
1115
+
1116
+ function setupDraftFailedError(reason) {
1117
+ const detail = reason ? ` (${cleanSentence(reason)})` : "";
1118
+ return new CommandError(`We couldn't draft a motion from that website${detail}. Nothing was created. Run \`audienti setup\` again in a few minutes. If it keeps failing, check the address or add a sentence about who you sell to.`);
1119
+ }
1120
+
1121
+ function renderSetupDraft(draft, { ask }, say) {
1122
+ const preview = draft?.preview || {};
1123
+ const icp = preview.icp || {};
1124
+ const offer = preview.offer || {};
1125
+ const titles = Array.isArray(icp.job_titles) && icp.job_titles.length ? ` (${icp.job_titles.join(", ")})` : "";
1126
+
1127
+ say("Here's the draft:");
1128
+ if (preview.company_name || preview.product_or_service) {
1129
+ say(` Your business: ${[preview.company_name, preview.product_or_service].filter(Boolean).join(" - ")}`);
1130
+ }
1131
+ say(` Who you'll reach: ${display(icp.name, "people who fit your website")}${titles}`);
1132
+ say(` Your offer: ${[offer.title, offer.description].filter(Boolean).join(" - ") || "drafted from your website"}`);
1133
+ say(` The ask: ${ask || "not set; the writer will suggest one"}`);
1134
+ if (preview.premise) say(` Why now: ${preview.premise}`);
1135
+ say();
648
1136
  }
649
1137
 
650
1138
  async function createAuthCallbackServer({ state, context }) {
@@ -4417,9 +4905,8 @@ async function inboxOpsActionsInBatches(client, accountId, { operation, rows, dr
4417
4905
  }
4418
4906
 
4419
4907
  async function confirmInboxOpsAction(context) {
4420
- const input = context.stdin;
4421
- if (!input || !input.isTTY) return false;
4422
- const prompt = createInterface({ input, output: context.stdout });
4908
+ if (!stdinIsInteractive(context)) return false;
4909
+ const prompt = createInterface({ input: context.stdin, output: context.stdout });
4423
4910
  try {
4424
4911
  const answer = await prompt.question("Apply? [y/N] ");
4425
4912
  return /^y(es)?$/i.test(String(answer).trim());
@@ -8112,13 +8599,14 @@ function quickStartTerminal(draft) {
8112
8599
  return quickStartReady(draft) || draft?.status === "failed";
8113
8600
  }
8114
8601
 
8115
- async function waitForQuickStartDraft(client, accountId, initialDraft, context, { timeoutSeconds, pollIntervalSeconds }) {
8602
+ async function waitForQuickStartDraft(client, accountId, initialDraft, context, { timeoutSeconds, pollIntervalSeconds, onPoll }) {
8116
8603
  let draft = initialDraft;
8117
8604
  const deadline = context.now().getTime() + timeoutSeconds * 1000;
8118
8605
 
8119
8606
  while (!quickStartTerminal(draft)) {
8120
8607
  if (context.now().getTime() >= deadline) return draft;
8121
8608
 
8609
+ onPoll?.();
8122
8610
  await context.sleep(pollIntervalSeconds * 1000);
8123
8611
  draft = await client.quickStart(accountId, draft.id);
8124
8612
  }
@@ -9176,6 +9664,8 @@ const HELP_TOPICS = new Map([
9176
9664
  " audienti <command> [options]",
9177
9665
  "",
9178
9666
  "Start:",
9667
+ " audienti start Guided first run: sign in, pick your account",
9668
+ " audienti setup Guided setup of your first motion",
9179
9669
  " audienti auth login Sign in through the browser",
9180
9670
  " audienti auth token <token> Save an API token",
9181
9671
  " audienti accounts list See accounts available to this token",
@@ -9637,16 +10127,47 @@ const HELP_TOPICS = new Map([
9637
10127
  " Saves accountId and accountName in local CLI config."
9638
10128
  ].join("\n")],
9639
10129
 
10130
+ ["start", [
10131
+ "Usage:",
10132
+ ` ${START_USAGE.slice("Usage: ".length)}`,
10133
+ "",
10134
+ "Status: implemented",
10135
+ "",
10136
+ "Purpose:",
10137
+ " Guided first run. Signs you in (or sends you to create an account) through the browser,",
10138
+ " picks your account and account user, then offers to run `audienti setup`.",
10139
+ " Running `audienti` with no arguments and no saved login starts here.",
10140
+ "",
10141
+ "Without a terminal:",
10142
+ " Never asks questions. Prints the sign-in command and the next step; `--json` returns them.",
10143
+ "",
10144
+ "Effect:",
10145
+ " Saves the login, accountId and accountUserId in local CLI config. Sends nothing."
10146
+ ].join("\n")],
10147
+
9640
10148
  ["setup", [
9641
10149
  "Usage:",
10150
+ ` ${SETUP_USAGE.slice("Usage: ".length)}`,
9642
10151
  " audienti setup play preflight [--principal <account_user_id|email|name|me>] [--platform linkedin] [--json]",
9643
10152
  "",
9644
10153
  "Status: implemented",
9645
10154
  "",
9646
10155
  "Purpose:",
10156
+ " `audienti setup` asks three questions (company website, who you sell to, what the prospect",
10157
+ " should say yes to), drafts an ICP, offer and motion with Quick Start, reads the draft back,",
10158
+ " and creates it only when you say yes. Nothing sends until LinkedIn is connected and approved.",
9647
10159
  " Preflight account setup before an agent creates or activates an Audienti play.",
9648
10160
  "",
10161
+ "Without a terminal:",
10162
+ " Pass --url (required) and optionally --sell-to and --ask. Add --yes to create the draft.",
10163
+ "",
10164
+ "API:",
10165
+ " POST /api/v1/accounts/:account_id/quick_start.json",
10166
+ " GET /api/v1/accounts/:account_id/quick_start/:draft_id.json",
10167
+ " POST /api/v1/accounts/:account_id/quick_start/:draft_id/confirm.json",
10168
+ "",
9649
10169
  "Commands:",
10170
+ " audienti setup Guided setup of your first motion",
9650
10171
  " audienti setup play preflight Check connected-account readiness and direct setup URLs"
9651
10172
  ].join("\n")],
9652
10173