@audienti/cli 0.1.102 → 0.1.103
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 +9 -0
- package/README.md +26 -11
- package/package.json +1 -1
- package/skills/audienti/SKILL.md +9 -4
- package/src/api-client.js +7 -0
- package/src/cli.js +302 -41
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to the Audienti CLI are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.103] - 2026-10-06
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `audienti start` now walks a new person through the same path as the web (#2551). It prints the intro, shows the price and opens the pay page for an unpaid account (the terminal waits until the payment shows), or takes a signup code. Then it runs setup from the step the person is on; a ready draft is read back as saved, with its edits, and never drafted again. Connecting LinkedIn is finished on the web; the command names the account to switch the browser to, then prints the page.
|
|
12
|
+
- `audienti setup` shows the audience, offer and signals cards. Type `a`, `o` or `s` to change one card through the same editor as the web setup page, or press Enter to create the experiment. It asks for the city, state or province, and country when the account needs them; without a terminal pass `--city`, `--state` and `--country`.
|
|
13
|
+
- `audienti start --json` reports `payment_required` with `payment_url` for an unpaid account.
|
|
14
|
+
- Setup now says "experiment" and points to the web go-live page instead of the social accounts list.
|
|
15
|
+
|
|
7
16
|
## [0.1.102] - 2026-10-06
|
|
8
17
|
|
|
9
18
|
### Removed
|
package/README.md
CHANGED
|
@@ -36,22 +36,37 @@ audienti start
|
|
|
36
36
|
audienti setup
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`audienti start`
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
`audienti start` walks you through the same path as the web:
|
|
40
|
+
|
|
41
|
+
1. It prints the intro: what Audienti does and how it warms people up.
|
|
42
|
+
2. It opens Audienti in your browser so you can sign in or create an account,
|
|
43
|
+
then picks your account and account user (automatically when there is only
|
|
44
|
+
one).
|
|
45
|
+
3. If the account has not paid, it shows the price and credits. Press Enter to
|
|
46
|
+
pay by card on the pay page (it opens in your browser and the address is
|
|
47
|
+
printed; the terminal waits until the payment shows), or type a signup code.
|
|
48
|
+
4. It runs setup in the terminal from the step you are on. Step 1, your
|
|
49
|
+
company: website, who you sell to, what they should say yes to, and your
|
|
50
|
+
city, state or province, and country when the account needs them. Step 2,
|
|
51
|
+
verify your targeting: the audience, offer and signals cards. Type `a`, `o`
|
|
52
|
+
or `s` to change one card, or press Enter to create the experiment.
|
|
53
|
+
5. Step 3, go live: connecting LinkedIn is not in the terminal yet. The command
|
|
54
|
+
prints the web page where you finish it.
|
|
55
|
+
|
|
56
|
+
`audienti setup` runs steps 1 and 2 on their own. Nothing is sent without your
|
|
57
|
+
approval.
|
|
58
|
+
|
|
59
|
+
Without a terminal, `audienti start` never prompts and reports what to do next
|
|
60
|
+
(`payment_required` with the pay page when the account has not paid), and
|
|
61
|
+
`setup` takes flags:
|
|
50
62
|
|
|
51
63
|
```bash
|
|
52
64
|
audienti setup --url https://acme.com --sell-to "<who you sell to>" --ask "<what they say yes to>" --yes --json
|
|
53
65
|
```
|
|
54
66
|
|
|
67
|
+
Add `--city <city> --state <code> --country <code>` when setup says it needs
|
|
68
|
+
your place.
|
|
69
|
+
|
|
55
70
|
Without `--yes`, a non-interactive setup drafts but creates nothing. Running
|
|
56
71
|
`audienti` with no saved login starts `audienti start`; `audienti --help` still
|
|
57
72
|
prints help.
|
package/package.json
CHANGED
package/skills/audienti/SKILL.md
CHANGED
|
@@ -39,16 +39,21 @@ audienti start
|
|
|
39
39
|
audienti setup
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
`audienti start` signs in through the browser (or creates an account)
|
|
43
|
-
the account
|
|
44
|
-
`
|
|
42
|
+
`audienti start` signs in through the browser (or creates an account), picks
|
|
43
|
+
the account, hands an unpaid account to the web pay page (or takes a signup
|
|
44
|
+
code), and runs setup in the terminal. `audienti start --json` never prompts; it
|
|
45
|
+
reports `sign_in_required`, `payment_required` (with `payment_url`) or `ready`.
|
|
46
|
+
Never pay or enter a signup code for the user. LinkedIn connect is finished on
|
|
47
|
+
the web setup page (`/quick_start/go_live`); that page acts on the browser's current
|
|
48
|
+
account, so tell the user to switch the browser to the CLI account first. Browser login alone is:
|
|
45
49
|
|
|
46
50
|
```bash
|
|
47
51
|
audienti auth login
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
`audienti setup --url <company_url> --sell-to "<who they sell to>" --ask "<what
|
|
51
|
-
prospects say yes to>" --json` drafts a first
|
|
55
|
+
prospects say yes to>" --json` drafts a first experiment without creating it.
|
|
56
|
+
Add `--city`, `--state` and `--country` when setup says it needs the place. Show
|
|
52
57
|
the draft to the user and add `--yes` only after they approve creating it.
|
|
53
58
|
|
|
54
59
|
Use `audienti auth token` only after the user supplies a token through an
|
package/src/api-client.js
CHANGED
|
@@ -880,6 +880,13 @@ export class AudientiClient {
|
|
|
880
880
|
return this.requestJson(accountPath(accountId, ["quick_start", draftId]));
|
|
881
881
|
}
|
|
882
882
|
|
|
883
|
+
updateQuickStart(accountId, draftId, body) {
|
|
884
|
+
return this.requestJson(accountPath(accountId, ["quick_start", draftId]), {
|
|
885
|
+
method: "PATCH",
|
|
886
|
+
body
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
883
890
|
setupState(accountId, query = {}) {
|
|
884
891
|
return this.requestJson(accountPath(accountId, ["quick_start", "setup_state"], query));
|
|
885
892
|
}
|
package/src/cli.js
CHANGED
|
@@ -25,8 +25,9 @@ const DEFAULT_AUTH_LOGIN_TIMEOUT_SECONDS = 180;
|
|
|
25
25
|
// Start covers signup plus email confirmation, which takes longer than a sign-in.
|
|
26
26
|
const DEFAULT_START_LOGIN_TIMEOUT_SECONDS = 900;
|
|
27
27
|
const DEFAULT_SETUP_TIMEOUT_SECONDS = 300;
|
|
28
|
+
const DEFAULT_PAYMENT_POLL_INTERVAL_SECONDS = 3;
|
|
28
29
|
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_USAGE = "Usage: audienti setup [--url <company_url>] [--city <city> --state <code> --country <code>] [--sell-to <who you sell to>] [--ask <what the prospect says yes to>] [--yes] [--json]";
|
|
30
31
|
const SETUP_FLAGS_EXAMPLE = "audienti setup --url https://acme.com --sell-to \"<who you sell to>\" --ask \"<what they say yes to>\" --yes";
|
|
31
32
|
const DEFAULT_WRITER_TEST_RUN_TIMEOUT_SECONDS = 180;
|
|
32
33
|
const DEFAULT_WRITER_TEST_RUN_POLL_INTERVAL_SECONDS = 2;
|
|
@@ -744,9 +745,10 @@ async function start(args, context) {
|
|
|
744
745
|
const host = normalizeHost(values.host || config.host || DEFAULT_HOST);
|
|
745
746
|
|
|
746
747
|
say("Welcome to Audienti.");
|
|
747
|
-
say("Audienti finds the people who fit your business, spots when they're ready to talk, and drafts the outreach.");
|
|
748
|
-
say("Nothing sends without your approval.");
|
|
749
748
|
say();
|
|
749
|
+
// The first two intro screens of the web signup. The third names the
|
|
750
|
+
// price, so it is shown at the pay step, where the price is known.
|
|
751
|
+
START_INTRO.slice(0, 2).forEach((screen, index) => writeIntroPoint(say, index + 1, screen));
|
|
750
752
|
|
|
751
753
|
let session = await savedSession(config, context);
|
|
752
754
|
if (!session && !interactive) {
|
|
@@ -813,37 +815,172 @@ async function start(args, context) {
|
|
|
813
815
|
? "audienti users select <account_user_id|email|name|me>"
|
|
814
816
|
: interactive ? "audienti setup" : SETUP_FLAGS_EXAMPLE;
|
|
815
817
|
|
|
818
|
+
// Payment comes before setup, as on the web: unpaid accounts run nothing.
|
|
819
|
+
const payment = status === "ready" ? await session.client.accountPayment(account.prefix_id) : null;
|
|
820
|
+
const paymentNeeded = payment?.state === "payment_needed";
|
|
821
|
+
const startStatus = paymentNeeded ? "payment_required" : status;
|
|
822
|
+
const startNext = paymentNeeded ? "audienti payment code <signup_code>" : nextCommand;
|
|
823
|
+
|
|
816
824
|
if (values.json) {
|
|
817
825
|
return writeJson(context.stdout, {
|
|
818
826
|
kind: "start",
|
|
819
|
-
status,
|
|
827
|
+
status: startStatus,
|
|
820
828
|
signed_in: true,
|
|
821
829
|
host: session.client.host,
|
|
822
830
|
user: session.label,
|
|
823
831
|
account: account ? { id: account.prefix_id, name: account.name } : null,
|
|
824
832
|
account_user: accountUser ? { id: accountUser.id, name: accountUser.name || null, email: accountUser.email || null } : null,
|
|
825
|
-
|
|
833
|
+
...(paymentNeeded ? { payment_url: payment.payment_url || null } : {}),
|
|
834
|
+
next_command: startNext
|
|
826
835
|
});
|
|
827
836
|
}
|
|
828
837
|
|
|
838
|
+
if (payment?.state === "payment_stopped") throw new CommandError(PAYMENT_STOPPED_MESSAGE);
|
|
839
|
+
|
|
829
840
|
if (status !== "ready" || !interactive) {
|
|
830
|
-
|
|
841
|
+
if (paymentNeeded) writeAccountPayment(payment, context);
|
|
842
|
+
say(`Next: ${startNext}`);
|
|
831
843
|
return 0;
|
|
832
844
|
}
|
|
833
845
|
|
|
834
|
-
|
|
835
|
-
if (await askYesNo(prompter, "Set up your first motion now? [Y/n] ", { defaultYes: true })) {
|
|
846
|
+
if (paymentNeeded) {
|
|
836
847
|
say();
|
|
837
|
-
|
|
848
|
+
await startPayStep(session.client, account.prefix_id, payment, { prompter, say, context, open: !values["no-open"] });
|
|
838
849
|
}
|
|
839
850
|
|
|
840
|
-
|
|
841
|
-
return 0;
|
|
851
|
+
return startSetupStep(session.client, account, context, { prompter, say });
|
|
842
852
|
} finally {
|
|
843
853
|
prompter.close();
|
|
844
854
|
}
|
|
845
855
|
}
|
|
846
856
|
|
|
857
|
+
// The three intro screens of the web signup (Users::SignupIntro), word for word.
|
|
858
|
+
const START_INTRO = [
|
|
859
|
+
{
|
|
860
|
+
title: "See who's ready to buy from you this week.",
|
|
861
|
+
body: "Give us your website. In a few minutes you'll see your ideal buyer, your offer in plain words, and real people showing signs they're ready right now."
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
title: "We warm them up in your name, like a person would.",
|
|
865
|
+
body: "We view, follow and like their posts on LinkedIn, weekdays 9 to 5 your time, at a normal human pace. By the time you reach out, they've already seen your name."
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
title: "Get {credits} of credits for {price}.",
|
|
869
|
+
body: "A free trial would only show you a demo. Your {price} gets you {credits} of credits that do real work: real buyers found and warmed up on your profile, with messages written and waiting for your OK. Nothing goes out unless you approve it."
|
|
870
|
+
}
|
|
871
|
+
];
|
|
872
|
+
|
|
873
|
+
const PAYMENT_STOPPED_MESSAGE = "The payment for this account was returned, so nothing runs. Contact Audienti support to start again.";
|
|
874
|
+
|
|
875
|
+
function writeIntroPoint(say, number, screen, values = {}) {
|
|
876
|
+
const fill = (text) => text.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? "");
|
|
877
|
+
say(`${number}. ${fill(screen.title)}`);
|
|
878
|
+
say(` ${fill(screen.body)}`);
|
|
879
|
+
say();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Same as Accounts::Admission.usage_label for dollars: $5, or $5.50.
|
|
883
|
+
function dollarLabel(cents) {
|
|
884
|
+
const amount = Number(cents) || 0;
|
|
885
|
+
return amount % 100 === 0 ? `$${amount / 100}` : `$${(amount / 100).toFixed(2)}`;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Pay by card in the browser, or type a signup code here. The card form needs
|
|
889
|
+
// a browser; the terminal waits until the account shows as paid.
|
|
890
|
+
async function startPayStep(client, accountId, payment, { prompter, say, context, open }) {
|
|
891
|
+
const usage = payment?.card_usage?.unit === "usd_cents" ? dollarLabel(payment.card_usage.amount) : "starting";
|
|
892
|
+
writeIntroPoint(say, 3, START_INTRO[2], { price: dollarLabel(payment?.price?.amount), credits: usage });
|
|
893
|
+
|
|
894
|
+
for (;;) {
|
|
895
|
+
const answer = await prompter.ask("Press Enter to pay by card in your browser, or type a signup code: ");
|
|
896
|
+
if (!answer) break;
|
|
897
|
+
|
|
898
|
+
try {
|
|
899
|
+
await client.redeemSignupCode(accountId, answer);
|
|
900
|
+
say("Code accepted. No card needed.");
|
|
901
|
+
say();
|
|
902
|
+
return;
|
|
903
|
+
} catch (error) {
|
|
904
|
+
if (!(error instanceof ApiError) || ![403, 422, 429].includes(error.status)) throw error;
|
|
905
|
+
say(typeof error.body?.error === "string" ? error.body.error : "That code didn't work.");
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
say("Opening the pay page in your browser. If it doesn't open, visit:");
|
|
910
|
+
say(` ${payment.payment_url}`);
|
|
911
|
+
if (open) openBrowser(payment.payment_url, context);
|
|
912
|
+
say("Waiting for your payment...");
|
|
913
|
+
|
|
914
|
+
const deadline = context.now().getTime() + DEFAULT_START_LOGIN_TIMEOUT_SECONDS * 1000;
|
|
915
|
+
let current = payment;
|
|
916
|
+
while (current?.state === "payment_needed") {
|
|
917
|
+
if (context.now().getTime() >= deadline) {
|
|
918
|
+
throw new CommandError("We haven't seen your payment yet. Finish on the pay page, then run `audienti start` again.");
|
|
919
|
+
}
|
|
920
|
+
await context.sleep(DEFAULT_PAYMENT_POLL_INTERVAL_SECONDS * 1000);
|
|
921
|
+
current = await client.accountPayment(accountId);
|
|
922
|
+
}
|
|
923
|
+
if (current?.state === "payment_stopped") throw new CommandError(PAYMENT_STOPPED_MESSAGE);
|
|
924
|
+
|
|
925
|
+
say("Payment received. Thank you.");
|
|
926
|
+
say();
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Lands on the setup step the person is on, as the web does after a return.
|
|
930
|
+
async function startSetupStep(client, account, context, { prompter, say }) {
|
|
931
|
+
const accountId = account.prefix_id;
|
|
932
|
+
const goLiveAccount = { id: accountId, name: account.name };
|
|
933
|
+
const state = await client.setupState(accountId);
|
|
934
|
+
if (state?.step === "live") {
|
|
935
|
+
say("Setup is done. LinkedIn is connected and your experiment is live.");
|
|
936
|
+
say("Next: audienti operator next");
|
|
937
|
+
return 0;
|
|
938
|
+
}
|
|
939
|
+
if (state?.step === "go_live") {
|
|
940
|
+
writeGoLiveStep(client.host, say, { state, account: goLiveAccount });
|
|
941
|
+
return 0;
|
|
942
|
+
}
|
|
943
|
+
if (state?.step === "targeting" && state.draft_id) {
|
|
944
|
+
// Read the saved draft as it is. Generating again could replace a draft
|
|
945
|
+
// the person already edited.
|
|
946
|
+
say("Your draft is ready. Let's check it.");
|
|
947
|
+
say();
|
|
948
|
+
return setupWizard([], context, { prompter, resumeDraftId: state.draft_id });
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
say();
|
|
952
|
+
if (await askYesNo(prompter, "Set up your first experiment now? [Y/n] ", { defaultYes: true })) {
|
|
953
|
+
say();
|
|
954
|
+
return setupWizard([], context, { prompter });
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
say("No problem. When you're ready, run: audienti setup");
|
|
958
|
+
return 0;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function goLiveUrl(host) {
|
|
962
|
+
return new URL("/quick_start/go_live", host).toString();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Step 3. LinkedIn sign-in has no terminal prompt yet, so the person
|
|
966
|
+
// finishes it on the setup page, which grants the LinkedIn account to the
|
|
967
|
+
// browser's current account only. The page has no account in its URL, so
|
|
968
|
+
// name the account the terminal set up and send the person to switch first.
|
|
969
|
+
function writeGoLiveStep(host, say, { state = null, account = null } = {}) {
|
|
970
|
+
if (state?.motion) say(`Your experiment: ${display(state.motion.name)} (${display(state.motion.prefix_id)}).`);
|
|
971
|
+
say("Last step: connect LinkedIn to go live.");
|
|
972
|
+
say("Connecting starts outreach: we view, follow and like the people we find, weekdays, 9 to 5 your time. Connection requests and messages wait for your OK.");
|
|
973
|
+
say("The terminal can't connect LinkedIn yet. Finish this step on the web.");
|
|
974
|
+
if (account?.id) {
|
|
975
|
+
const label = account.name ? `${account.name} (${account.id})` : account.id;
|
|
976
|
+
say(`First make sure the browser is on the account ${label}. If it shows another account, switch here:`);
|
|
977
|
+
say(` ${new URL("/accounts", host).toString()}`);
|
|
978
|
+
say("Then open:");
|
|
979
|
+
}
|
|
980
|
+
say(` ${goLiveUrl(host)}`);
|
|
981
|
+
say("Audienti keeps finding people in the meantime. Check where setup stands with: audienti motions setup-state");
|
|
982
|
+
}
|
|
983
|
+
|
|
847
984
|
async function savedSession(config, context) {
|
|
848
985
|
if (!config.token) return null;
|
|
849
986
|
|
|
@@ -1001,10 +1138,14 @@ const SETUP_QUESTIONS = [
|
|
|
1001
1138
|
}
|
|
1002
1139
|
];
|
|
1003
1140
|
|
|
1004
|
-
async function setupWizard(args, context, { accountOverride, prompter: sharedPrompter } = {}) {
|
|
1141
|
+
async function setupWizard(args, context, { accountOverride, prompter: sharedPrompter, resumeDraftId = null } = {}) {
|
|
1142
|
+
const resume = Boolean(resumeDraftId);
|
|
1005
1143
|
const { values, positionals } = parseCommandArgs(args, {
|
|
1006
1144
|
...jsonOptions(),
|
|
1007
1145
|
url: { type: "string" },
|
|
1146
|
+
city: { type: "string" },
|
|
1147
|
+
state: { type: "string" },
|
|
1148
|
+
country: { type: "string" },
|
|
1008
1149
|
"sell-to": { type: "string" },
|
|
1009
1150
|
ask: { type: "string" },
|
|
1010
1151
|
yes: { type: "boolean" }
|
|
@@ -1038,8 +1179,8 @@ async function setupWizard(args, context, { accountOverride, prompter: sharedPro
|
|
|
1038
1179
|
|
|
1039
1180
|
try {
|
|
1040
1181
|
const answers = { url: values.url, "sell-to": values["sell-to"], ask: values.ask };
|
|
1041
|
-
if (interactive) {
|
|
1042
|
-
say("
|
|
1182
|
+
if (interactive && !resume) {
|
|
1183
|
+
say("Step 1 of 3: Your company. Three quick questions.");
|
|
1043
1184
|
for (const [index, question] of SETUP_QUESTIONS.entries()) {
|
|
1044
1185
|
if (answers[question.key] !== undefined) continue;
|
|
1045
1186
|
|
|
@@ -1055,19 +1196,33 @@ async function setupWizard(args, context, { accountOverride, prompter: sharedPro
|
|
|
1055
1196
|
|
|
1056
1197
|
const sellTo = cleanSentence(answers["sell-to"]);
|
|
1057
1198
|
const ask = cleanSentence(answers.ask);
|
|
1058
|
-
|
|
1199
|
+
let place = { city: values.city, state_code: values.state, country_code: values.country };
|
|
1200
|
+
const requestBody = () => ({
|
|
1059
1201
|
quick_start: compactObject({
|
|
1060
1202
|
company_url: String(answers.url || "").trim() || undefined,
|
|
1203
|
+
...place,
|
|
1061
1204
|
feedback: setupFeedback({ sellTo, ask })
|
|
1062
1205
|
})
|
|
1063
|
-
};
|
|
1206
|
+
});
|
|
1064
1207
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1208
|
+
let draft = resume ? await client.quickStart(accountId, resumeDraftId) : null;
|
|
1209
|
+
if (!resume) say("Reading your website and drafting your first experiment. This usually takes a minute or two.");
|
|
1210
|
+
while (!draft) {
|
|
1211
|
+
try {
|
|
1212
|
+
draft = await client.createQuickStart(accountId, requestBody());
|
|
1213
|
+
break;
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
// Step 1 needs the person's place before the first draft. The API
|
|
1216
|
+
// names the field; ask for it here and send again.
|
|
1217
|
+
if (!(error instanceof ApiError && error.body?.field === "location")) throw setupCreateError(error);
|
|
1218
|
+
const reason = cleanSentence(typeof error.body?.error === "string" ? error.body.error : "We need your city, state or province, and country");
|
|
1219
|
+
if (!interactive) {
|
|
1220
|
+
throw new CommandError(`${reason}. Pass --city, --state and --country, for example --city Denver --state CO --country US.`);
|
|
1221
|
+
}
|
|
1222
|
+
say(`${reason}.`);
|
|
1223
|
+
place = await askSetupPlace(prompter, place);
|
|
1224
|
+
say();
|
|
1225
|
+
}
|
|
1071
1226
|
}
|
|
1072
1227
|
|
|
1073
1228
|
let polls = 0;
|
|
@@ -1085,10 +1240,13 @@ async function setupWizard(args, context, { accountOverride, prompter: sharedPro
|
|
|
1085
1240
|
throw new CommandError("The draft is taking longer than usual. Nothing was created. Run `audienti setup` again in a few minutes.");
|
|
1086
1241
|
}
|
|
1087
1242
|
|
|
1088
|
-
renderSetupDraft(draft, { ask }, say);
|
|
1089
|
-
|
|
1090
1243
|
let confirmed = Boolean(values.yes);
|
|
1091
|
-
if (!confirmed && interactive)
|
|
1244
|
+
if (!confirmed && interactive) {
|
|
1245
|
+
say("Step 2 of 3: Verify your targeting.");
|
|
1246
|
+
({ draft, confirmed } = await reviewSetupDraft(client, accountId, draft, { ask, prompter, say }));
|
|
1247
|
+
} else {
|
|
1248
|
+
renderSetupDraft(draft, { ask }, say);
|
|
1249
|
+
}
|
|
1092
1250
|
|
|
1093
1251
|
if (!confirmed) {
|
|
1094
1252
|
const payload = {
|
|
@@ -1108,7 +1266,7 @@ async function setupWizard(args, context, { accountOverride, prompter: sharedPro
|
|
|
1108
1266
|
}
|
|
1109
1267
|
|
|
1110
1268
|
const confirmation = await client.confirmQuickStart(accountId, draft.id, {});
|
|
1111
|
-
const connectUrl =
|
|
1269
|
+
const connectUrl = goLiveUrl(client.host);
|
|
1112
1270
|
if (values.json) {
|
|
1113
1271
|
return writeJson(context.stdout, {
|
|
1114
1272
|
kind: "setup",
|
|
@@ -1125,11 +1283,14 @@ async function setupWizard(args, context, { accountOverride, prompter: sharedPro
|
|
|
1125
1283
|
}
|
|
1126
1284
|
|
|
1127
1285
|
const motion = confirmation?.motion || {};
|
|
1128
|
-
say(`Created your first
|
|
1286
|
+
say(`Created your first experiment: ${display(motion.name)} (${display(motion.prefix_id)}). Finding people now.`);
|
|
1129
1287
|
say();
|
|
1130
|
-
say("
|
|
1131
|
-
|
|
1132
|
-
|
|
1288
|
+
say("Step 3 of 3: Go live.");
|
|
1289
|
+
const goLiveAccount = {
|
|
1290
|
+
id: accountId,
|
|
1291
|
+
name: accountId === config.accountId ? config.accountName : undefined
|
|
1292
|
+
};
|
|
1293
|
+
writeGoLiveStep(client.host, say, { account: goLiveAccount });
|
|
1133
1294
|
say("Nothing is sent until LinkedIn is connected, and nothing sends without your approval.");
|
|
1134
1295
|
return 0;
|
|
1135
1296
|
} finally {
|
|
@@ -1174,7 +1335,7 @@ function setupCreateError(error) {
|
|
|
1174
1335
|
|
|
1175
1336
|
function setupDraftFailedError(reason) {
|
|
1176
1337
|
const detail = reason ? ` (${cleanSentence(reason)})` : "";
|
|
1177
|
-
return new CommandError(`We couldn't draft
|
|
1338
|
+
return new CommandError(`We couldn't draft an experiment 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.`);
|
|
1178
1339
|
}
|
|
1179
1340
|
|
|
1180
1341
|
function renderSetupDraft(draft, { ask }, say) {
|
|
@@ -1189,6 +1350,8 @@ function renderSetupDraft(draft, { ask }, say) {
|
|
|
1189
1350
|
}
|
|
1190
1351
|
say(` Who you'll reach: ${display(icp.name, "people who fit your website")}${titles}`);
|
|
1191
1352
|
say(` Your offer: ${[offer.title, offer.description].filter(Boolean).join(" - ") || "drafted from your website"}`);
|
|
1353
|
+
const signals = Array.isArray(preview.signals) ? preview.signals : [];
|
|
1354
|
+
if (signals.length) say(` Signals: ${signals.map((signal) => signal.signal_text || signal.name).filter(Boolean).join(" · ")}`);
|
|
1192
1355
|
say(` The ask: ${ask || "not set; the writer will suggest one"}`);
|
|
1193
1356
|
if (preview.premise) say(` Why now: ${preview.premise}`);
|
|
1194
1357
|
const examples = Array.isArray(preview.example_people) ? preview.example_people : [];
|
|
@@ -1201,6 +1364,88 @@ function renderSetupDraft(draft, { ask }, say) {
|
|
|
1201
1364
|
say();
|
|
1202
1365
|
}
|
|
1203
1366
|
|
|
1367
|
+
async function askSetupPlace(prompter, place) {
|
|
1368
|
+
const city = await askKeeping(prompter, "City", place.city);
|
|
1369
|
+
const stateCode = await askKeeping(prompter, "State or province code, like CO or ON", place.state_code);
|
|
1370
|
+
const country = await askKeeping(prompter, "Country code", place.country_code || "US");
|
|
1371
|
+
return { city, state_code: stateCode, country_code: country };
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// Asks one question; Enter keeps the value shown in brackets.
|
|
1375
|
+
async function askKeeping(prompter, label, current) {
|
|
1376
|
+
const shown = current ? ` [${current}]` : "";
|
|
1377
|
+
const answer = await prompter.ask(`${label}${shown}: `);
|
|
1378
|
+
return answer || current || undefined;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const SETUP_DRAFT_CHOICES = "Press Enter to create this experiment, or type a to change the audience, o the offer, s the signals, or n to stop: ";
|
|
1382
|
+
|
|
1383
|
+
// Step 2: show the cards, let the person change one card at a time through
|
|
1384
|
+
// the same editor as the setup page, then create on Enter.
|
|
1385
|
+
async function reviewSetupDraft(client, accountId, initialDraft, { ask, prompter, say }) {
|
|
1386
|
+
let draft = initialDraft;
|
|
1387
|
+
for (;;) {
|
|
1388
|
+
renderSetupDraft(draft, { ask }, say);
|
|
1389
|
+
const answer = (await prompter.ask(SETUP_DRAFT_CHOICES)).toLowerCase();
|
|
1390
|
+
if (!answer || /^y(es)?$/.test(answer)) return { draft, confirmed: true };
|
|
1391
|
+
if (/^no?$/.test(answer)) return { draft, confirmed: false };
|
|
1392
|
+
|
|
1393
|
+
const section = { a: "icp", audience: "icp", o: "offer", offer: "offer", s: "signals", signals: "signals" }[answer];
|
|
1394
|
+
if (!section) {
|
|
1395
|
+
say("Press Enter, or type a, o, s or n.");
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
draft = await editSetupDraftCard(client, accountId, draft, section, { prompter, say });
|
|
1399
|
+
say();
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
async function editSetupDraftCard(client, accountId, draft, section, { prompter, say }) {
|
|
1404
|
+
const preview = draft?.preview || {};
|
|
1405
|
+
let values;
|
|
1406
|
+
if (section === "icp") {
|
|
1407
|
+
const icp = preview.icp || {};
|
|
1408
|
+
const name = await askKeeping(prompter, "Who you'll reach, in one line", icp.name);
|
|
1409
|
+
const titles = await askKeeping(prompter, "Job titles, separated by commas", (icp.job_titles || []).join(", "));
|
|
1410
|
+
values = { name, job_titles: String(titles || "").split(",").map((title) => title.trim()).filter(Boolean) };
|
|
1411
|
+
} else if (section === "offer") {
|
|
1412
|
+
const offer = preview.offer || {};
|
|
1413
|
+
values = {
|
|
1414
|
+
title: await askKeeping(prompter, "Offer name", offer.title),
|
|
1415
|
+
description: await askKeeping(prompter, "Offer in one or two sentences", offer.description)
|
|
1416
|
+
};
|
|
1417
|
+
} else {
|
|
1418
|
+
const signals = Array.isArray(preview.signals) ? preview.signals : [];
|
|
1419
|
+
if (!signals.length) {
|
|
1420
|
+
say("This draft has no signals to change.");
|
|
1421
|
+
return draft;
|
|
1422
|
+
}
|
|
1423
|
+
const rows = [];
|
|
1424
|
+
for (const [index, signal] of signals.entries()) {
|
|
1425
|
+
const before = signal.signal_text || signal.name;
|
|
1426
|
+
const text = await askKeeping(prompter, `Signal ${index + 1}`, before);
|
|
1427
|
+
// The name stays only while the text is the same; a new text names itself.
|
|
1428
|
+
rows.push(compactObject({
|
|
1429
|
+
index,
|
|
1430
|
+
name: text === before ? signal.name : undefined,
|
|
1431
|
+
signal_text: text,
|
|
1432
|
+
company_signal_category: signal.company_signal_category
|
|
1433
|
+
}));
|
|
1434
|
+
}
|
|
1435
|
+
values = { signals: rows };
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
try {
|
|
1439
|
+
const updated = await client.updateQuickStart(accountId, draft.id, { section, draft: values });
|
|
1440
|
+
say("Saved.");
|
|
1441
|
+
return updated;
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
if (!(error instanceof ApiError) || error.status !== 422) throw error;
|
|
1444
|
+
say(`That change wasn't saved: ${cleanSentence(error.body?.error || "it was rejected")}.`);
|
|
1445
|
+
return draft;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1204
1449
|
async function createAuthCallbackServer({ state, context }) {
|
|
1205
1450
|
let resolvePayload;
|
|
1206
1451
|
let rejectPayload;
|
|
@@ -10268,8 +10513,8 @@ const HELP_TOPICS = new Map([
|
|
|
10268
10513
|
" audienti <command> [options]",
|
|
10269
10514
|
"",
|
|
10270
10515
|
"Start:",
|
|
10271
|
-
" audienti start Guided first run: sign in,
|
|
10272
|
-
" audienti setup Guided setup of your first
|
|
10516
|
+
" audienti start Guided first run: sign in, pay, set up your first experiment",
|
|
10517
|
+
" audienti setup Guided setup of your first experiment",
|
|
10273
10518
|
" audienti auth login Sign in through the browser",
|
|
10274
10519
|
" audienti auth token <token> Save an API token",
|
|
10275
10520
|
" audienti accounts list See accounts available to this token",
|
|
@@ -10744,15 +10989,26 @@ const HELP_TOPICS = new Map([
|
|
|
10744
10989
|
"Status: implemented",
|
|
10745
10990
|
"",
|
|
10746
10991
|
"Purpose:",
|
|
10747
|
-
" Guided first run.
|
|
10748
|
-
" picks your account and account user
|
|
10992
|
+
" Guided first run, the same path as the web. Prints the intro, signs you in (or sends you to",
|
|
10993
|
+
" create an account) through the browser, and picks your account and account user. An unpaid",
|
|
10994
|
+
" account pays by card on the web pay page (opened for you; the terminal waits) or types a",
|
|
10995
|
+
" signup code here. Then setup runs in the terminal from the step you are on: your company,",
|
|
10996
|
+
" verify your targeting (change the audience, offer or signals card), and create the experiment.",
|
|
10997
|
+
" Connecting LinkedIn is finished on the web; the command prints the page address.",
|
|
10749
10998
|
" Running `audienti` with no arguments and no saved login starts here.",
|
|
10750
10999
|
"",
|
|
10751
11000
|
"Without a terminal:",
|
|
10752
|
-
" Never asks questions. Prints the sign-in command
|
|
11001
|
+
" Never asks questions. Prints the sign-in command, the pay page when payment is needed, and",
|
|
11002
|
+
" the next step; `--json` returns them (status payment_required with payment_url when unpaid).",
|
|
11003
|
+
"",
|
|
11004
|
+
"API:",
|
|
11005
|
+
" GET /api/v1/accounts/:account_id/admission.json",
|
|
11006
|
+
" POST /api/v1/accounts/:account_id/admission.json",
|
|
11007
|
+
" GET /api/v1/accounts/:account_id/quick_start/setup_state.json",
|
|
10753
11008
|
"",
|
|
10754
11009
|
"Effect:",
|
|
10755
|
-
" Saves the login, accountId and accountUserId in local CLI config.
|
|
11010
|
+
" Saves the login, accountId and accountUserId in local CLI config. A signup code is used only",
|
|
11011
|
+
" when you type one. Creates an experiment only when you say so. Sends nothing."
|
|
10756
11012
|
].join("\n")],
|
|
10757
11013
|
|
|
10758
11014
|
["setup", [
|
|
@@ -10764,20 +11020,25 @@ const HELP_TOPICS = new Map([
|
|
|
10764
11020
|
"",
|
|
10765
11021
|
"Purpose:",
|
|
10766
11022
|
" `audienti setup` asks three questions (company website, who you sell to, what the prospect",
|
|
10767
|
-
" should say yes to),
|
|
10768
|
-
"
|
|
11023
|
+
" should say yes to), and your city, state or province, and country when the account needs them.",
|
|
11024
|
+
" It drafts an audience, offer and signals with Quick Start and shows the cards. Type a, o or s",
|
|
11025
|
+
" to change one card (the same editor as the web setup page), or press Enter to create the",
|
|
11026
|
+
" experiment. Then it prints the web page where you connect LinkedIn to go live.",
|
|
11027
|
+
" Nothing sends until LinkedIn is connected and approved.",
|
|
10769
11028
|
" Preflight account setup before an agent creates or activates an Audienti play.",
|
|
10770
11029
|
"",
|
|
10771
11030
|
"Without a terminal:",
|
|
10772
|
-
" Pass --url (required) and optionally --sell-to
|
|
11031
|
+
" Pass --url (required) and optionally --sell-to, --ask, and --city/--state/--country.",
|
|
11032
|
+
" Add --yes to create the draft.",
|
|
10773
11033
|
"",
|
|
10774
11034
|
"API:",
|
|
10775
11035
|
" POST /api/v1/accounts/:account_id/quick_start.json",
|
|
10776
11036
|
" GET /api/v1/accounts/:account_id/quick_start/:draft_id.json",
|
|
11037
|
+
" PATCH /api/v1/accounts/:account_id/quick_start/:draft_id.json",
|
|
10777
11038
|
" POST /api/v1/accounts/:account_id/quick_start/:draft_id/confirm.json",
|
|
10778
11039
|
"",
|
|
10779
11040
|
"Commands:",
|
|
10780
|
-
" audienti setup Guided setup of your first
|
|
11041
|
+
" audienti setup Guided setup of your first experiment",
|
|
10781
11042
|
" audienti setup play preflight Check connected-account readiness and direct setup URLs"
|
|
10782
11043
|
].join("\n")],
|
|
10783
11044
|
|