@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 +16 -0
- package/README.md +30 -3
- package/install +22 -1
- package/package.json +1 -1
- package/skills/audienti/SKILL.md +15 -2
- package/src/cli.js +548 -27
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
|
|
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
|
|
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
package/skills/audienti/SKILL.md
CHANGED
|
@@ -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.
|
|
35
|
-
|
|
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
|
-
|
|
606
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
4421
|
-
|
|
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
|
|