@audienti/cli 0.1.101 → 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 CHANGED
@@ -4,6 +4,21 @@ 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
+
16
+ ## [0.1.102] - 2026-10-06
17
+
18
+ ### Removed
19
+
20
+ - The `unsupported_plan_version` planner status. Every prospect plan now uses the current planner version, so the API no longer returns that status and the CLI no longer labels it.
21
+
7
22
  ## [0.1.101] - 2026-10-05
8
23
 
9
24
  ### Changed
package/README.md CHANGED
@@ -36,22 +36,37 @@ audienti start
36
36
  audienti setup
37
37
  ```
38
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:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.1.101",
3
+ "version": "0.1.103",
4
4
  "description": "Agent-first command-line client for Audienti.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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) and picks
43
- the account. `audienti start --json` never prompts; it reports
44
- `sign_in_required` or `ready`. Browser login alone is:
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 motion without creating it. Show
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
- next_command: nextCommand
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
- say(`Next: ${nextCommand}`);
841
+ if (paymentNeeded) writeAccountPayment(payment, context);
842
+ say(`Next: ${startNext}`);
831
843
  return 0;
832
844
  }
833
845
 
834
- say();
835
- if (await askYesNo(prompter, "Set up your first motion now? [Y/n] ", { defaultYes: true })) {
846
+ if (paymentNeeded) {
836
847
  say();
837
- return setupWizard([], context, { prompter });
848
+ await startPayStep(session.client, account.prefix_id, payment, { prompter, say, context, open: !values["no-open"] });
838
849
  }
839
850
 
840
- say("No problem. When you're ready, run: audienti setup");
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("Let's set up your first motion. Three quick questions.");
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
- const requestBody = {
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
- say("Reading your website and drafting your first motion. This usually takes a minute or two.");
1066
- let draft;
1067
- try {
1068
- draft = await client.createQuickStart(accountId, requestBody);
1069
- } catch (error) {
1070
- throw setupCreateError(error);
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) confirmed = await askYesNo(prompter, "Create this? [y/N] ");
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 = new URL("/user/social_cookies", client.host).toString();
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 motion: ${display(motion.name)} (${display(motion.prefix_id)}).`);
1286
+ say(`Created your first experiment: ${display(motion.name)} (${display(motion.prefix_id)}). Finding people now.`);
1129
1287
  say();
1130
- say("Next: connect your LinkedIn account so Audienti can reach people for you.");
1131
- say(` Connect it here: ${connectUrl}`);
1132
- say(" Then check it with: audienti setup play preflight");
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 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.`);
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;
@@ -7996,8 +8241,6 @@ function renderProspectReplan(payload, context) {
7996
8241
  writeLine(context.stdout, "The request was recorded, but the Planner could not apply it. Review the reason before retrying.");
7997
8242
  } else if (payload?.status === "evaluation_error") {
7998
8243
  writeLine(context.stdout, "Planner evaluation failed. Fix the evaluation error and rerun with --apply.");
7999
- } else if (payload?.status === "unsupported_plan_version") {
8000
- writeLine(context.stdout, "This membership uses an unsupported Planner version. Migrate it before retrying.");
8001
8244
  } else if (payload?.status === "not_applied") {
8002
8245
  writeLine(context.stdout, "The plan was not persisted. Review the reason before retrying.");
8003
8246
  }
@@ -8065,8 +8308,7 @@ const REPLAN_STATUS_LABELS = Object.freeze({
8065
8308
  pending: "pending",
8066
8309
  busy: "busy",
8067
8310
  failed: "failed",
8068
- evaluation_error: "evaluation error",
8069
- unsupported_plan_version: "unsupported plan version"
8311
+ evaluation_error: "evaluation error"
8070
8312
  });
8071
8313
 
8072
8314
  function replanStatusLabel(payload) {
@@ -10271,8 +10513,8 @@ const HELP_TOPICS = new Map([
10271
10513
  " audienti <command> [options]",
10272
10514
  "",
10273
10515
  "Start:",
10274
- " audienti start Guided first run: sign in, pick your account",
10275
- " audienti setup Guided setup of your first motion",
10516
+ " audienti start Guided first run: sign in, pay, set up your first experiment",
10517
+ " audienti setup Guided setup of your first experiment",
10276
10518
  " audienti auth login Sign in through the browser",
10277
10519
  " audienti auth token <token> Save an API token",
10278
10520
  " audienti accounts list See accounts available to this token",
@@ -10747,15 +10989,26 @@ const HELP_TOPICS = new Map([
10747
10989
  "Status: implemented",
10748
10990
  "",
10749
10991
  "Purpose:",
10750
- " Guided first run. Signs you in (or sends you to create an account) through the browser,",
10751
- " picks your account and account user, then offers to run `audienti setup`.",
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.",
10752
10998
  " Running `audienti` with no arguments and no saved login starts here.",
10753
10999
  "",
10754
11000
  "Without a terminal:",
10755
- " Never asks questions. Prints the sign-in command and the next step; `--json` returns them.",
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",
10756
11008
  "",
10757
11009
  "Effect:",
10758
- " Saves the login, accountId and accountUserId in local CLI config. Sends nothing."
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."
10759
11012
  ].join("\n")],
10760
11013
 
10761
11014
  ["setup", [
@@ -10767,20 +11020,25 @@ const HELP_TOPICS = new Map([
10767
11020
  "",
10768
11021
  "Purpose:",
10769
11022
  " `audienti setup` asks three questions (company website, who you sell to, what the prospect",
10770
- " should say yes to), drafts an ICP, offer and motion with Quick Start, reads the draft back,",
10771
- " and creates it only when you say yes. Nothing sends until LinkedIn is connected and approved.",
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.",
10772
11028
  " Preflight account setup before an agent creates or activates an Audienti play.",
10773
11029
  "",
10774
11030
  "Without a terminal:",
10775
- " Pass --url (required) and optionally --sell-to and --ask. Add --yes to create the draft.",
11031
+ " Pass --url (required) and optionally --sell-to, --ask, and --city/--state/--country.",
11032
+ " Add --yes to create the draft.",
10776
11033
  "",
10777
11034
  "API:",
10778
11035
  " POST /api/v1/accounts/:account_id/quick_start.json",
10779
11036
  " GET /api/v1/accounts/:account_id/quick_start/:draft_id.json",
11037
+ " PATCH /api/v1/accounts/:account_id/quick_start/:draft_id.json",
10780
11038
  " POST /api/v1/accounts/:account_id/quick_start/:draft_id/confirm.json",
10781
11039
  "",
10782
11040
  "Commands:",
10783
- " audienti setup Guided setup of your first motion",
11041
+ " audienti setup Guided setup of your first experiment",
10784
11042
  " audienti setup play preflight Check connected-account readiness and direct setup URLs"
10785
11043
  ].join("\n")],
10786
11044