@audienti/cli 0.1.75 → 0.1.77

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/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";
@@ -207,6 +213,21 @@ const TOOLS_LINKEDIN_REVIEW_USAGE = "Usage: audienti tools linkedin-review --url
207
213
  const TOOLS_LINKEDIN_REVIEW_REPORTS_USAGE = "Usage: audienti tools linkedin-review reports [--limit <n>] [--json] [--account <acct_id>]";
208
214
  const TOOLS_LINKEDIN_REVIEW_SHOW_USAGE = "Usage: audienti tools linkedin-review show <rprt_id> [--json] [--account <acct_id>]";
209
215
  const TOOLS_LINKEDIN_REVIEW_STATUS_USAGE = "Usage: audienti tools linkedin-review status <rprt_id> [--json] [--account <acct_id>]";
216
+ const TOOLS_LINKEDIN_STRATEGY_REVIEW_DELETE_USAGE = "Usage: audienti tools linkedin-strategy-review delete <rprt_id> --confirm <yes|true|Y|y> [--json] [--account <acct_id>]";
217
+ const LISTS_BULK_ADD_TAG_USAGE = "Usage: audienti lists bulk-add-tag --tag <tag> <list_id> [list_id...] [--json] [--account <acct_id>]";
218
+ const LISTS_MERGE_USAGE = "Usage: audienti lists merge <list_id> <list_id> [--json] [--account <acct_id>]";
219
+ const LISTS_EXPORT_USAGE = "Usage: audienti lists export <list_id> [--output <file.csv>] [--inactive-reason <reason>] [--json] [--account <acct_id>]";
220
+ const LIST_ROUTING_RULES_TOGGLE_USAGE = "Usage: audienti lists routing-rules <list_id> toggle <rule_id> [--json] [--account <acct_id>]";
221
+ const ICPS_BULK_ADD_TAG_USAGE = "Usage: audienti icps bulk-add-tag --tag <tag> <icp_id> [icp_id...] [--json] [--account <acct_id>]";
222
+ const ICPS_CLONE_USAGE = "Usage: audienti icps clone <icp_id> [--json] [--account <acct_id>]";
223
+ const ICPS_DELETE_USAGE = "Usage: audienti icps delete <icp_id> --confirm <yes|true|Y|y> [--json] [--account <acct_id>]";
224
+ const ICPS_PROSPECTS_USAGE = "Usage: audienti icps prospects <icp_id> [--query <text>] [--limit <n>] [--offset <n> | --page <n>] [--profiles] [--json] [--account <acct_id>]";
225
+ const OFFERS_REGENERATE_RESEARCH_USAGE = "Usage: audienti offers regenerate-research <offr_id> [--guidance <text>] [--json] [--account <acct_id>]";
226
+ const OFFERS_UPDATE_WRITEUP_USAGE = "Usage: audienti offers update-writeup <offr_id> --description <text> [--json] [--account <acct_id>]";
227
+ const OFFERS_ADD_ARTIFACTS_USAGE = "Usage: audienti offers add-artifacts <offr_id> <file> [file...] [--json] [--account <acct_id>]";
228
+ const OFFERS_REMOVE_ARTIFACT_USAGE = "Usage: audienti offers remove-artifact <offr_id> <artifact_id> [--json] [--account <acct_id>]";
229
+ const TASKS_UPDATE_USAGE = "Usage: audienti tasks update <ptsk_id> [--title <text>] [--due <time>] [--notes <text>] [--prospect <prsp_id|\"\">] [--list <list_id|\"\">] [--assigned-user <id|me>] [--json] [--account <acct_id>]";
230
+ const TASKS_BULK_UPDATE_USAGE = "Usage: audienti tasks bulk-update --action <complete|reassign> [--assigned-user <id|me>] <ptsk_id> [ptsk_id...] [--json] [--account <acct_id>]";
210
231
  const SOCIAL_COOKIES_SYNC_MESSAGES_USAGE = "Usage: audienti social-cookies sync-messages <scok_id> [--folder <folder>] [--retry] [--json] [--account <acct_id>]";
211
232
  const COHORT_STAGE_ORDER = [
212
233
  "identified",
@@ -272,6 +293,7 @@ export async function run(argv = process.argv.slice(2), deps = {}) {
272
293
 
273
294
  async function dispatch(argv, context) {
274
295
  const { args, accountOverride } = extractGlobalOptions(argv);
296
+ if (args.length === 0 && !(await readConfig({ env: context.env })).token) return start([], context);
275
297
  const helpTopic = helpTopicFromArgs(args);
276
298
 
277
299
  if (helpTopic) {
@@ -302,6 +324,8 @@ async function dispatch(argv, context) {
302
324
  if (normalizedResource === "users" && action === "select") return usersSelect(rest, context, { accountOverride });
303
325
  if (normalizedResource === "users" && action === "activity") return usersActivity(rest, context, { accountOverride });
304
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 });
305
329
  if (normalizedResource === "setup" && action === "play" && rest[0] === "preflight") return setupPlayPreflight(rest.slice(1), context, { accountOverride });
306
330
  if (normalizedResource === "social-cookies" && action === "sync-messages") return socialCookiesSyncMessages(rest, context, { accountOverride });
307
331
  if (normalizedResource === "offers" && action === "list") return offersList(rest, context, { accountOverride });
@@ -309,6 +333,10 @@ async function dispatch(argv, context) {
309
333
  if (normalizedResource === "offers" && action === "create") return offersCreate(rest, context, { accountOverride });
310
334
  if (normalizedResource === "offers" && action === "update") return offersUpdate(rest, context, { accountOverride });
311
335
  if (normalizedResource === "offers" && action === "delete") return offersDelete(rest, context, { accountOverride });
336
+ if (normalizedResource === "offers" && action === "regenerate-research") return offersRegenerateResearch(rest, context, { accountOverride });
337
+ if (normalizedResource === "offers" && action === "update-writeup") return offersUpdateWriteup(rest, context, { accountOverride });
338
+ if (normalizedResource === "offers" && action === "add-artifacts") return offersAddArtifacts(rest, context, { accountOverride });
339
+ if (normalizedResource === "offers" && action === "remove-artifact") return offersRemoveArtifact(rest, context, { accountOverride });
312
340
  if (normalizedResource === "icps" && action === "list") return icpsList(rest, context, { accountOverride });
313
341
  if (normalizedResource === "icps" && action === "show") return icpsShow(rest, context, { accountOverride });
314
342
  if (normalizedResource === "icps" && action === "analytics") return analyticsIcps(rest, context, { accountOverride });
@@ -318,6 +346,10 @@ async function dispatch(argv, context) {
318
346
  if (normalizedResource === "icps" && action === "restore") return icpsLifecycleMutation("restore", rest, context, { accountOverride });
319
347
  if (normalizedResource === "icps" && action === "add-tag") return icpsTagMutation("add", rest, context, { accountOverride });
320
348
  if (normalizedResource === "icps" && action === "remove-tag") return icpsTagMutation("remove", rest, context, { accountOverride });
349
+ if (normalizedResource === "icps" && action === "bulk-add-tag") return icpsBulkAddTag(rest, context, { accountOverride });
350
+ if (normalizedResource === "icps" && action === "clone") return icpsClone(rest, context, { accountOverride });
351
+ if (normalizedResource === "icps" && action === "delete") return icpsDelete(rest, context, { accountOverride });
352
+ if (normalizedResource === "icps" && action === "prospects") return icpsProspects(rest, context, { accountOverride });
321
353
  if (normalizedResource === "companies" && action === "search") return companiesSearch(rest, context, { accountOverride });
322
354
  if (normalizedResource === "dnc" && action === "list") return dncList(rest, context, { accountOverride });
323
355
  if (normalizedResource === "dnc" && action === "add") return dncAdd(rest, context, { accountOverride });
@@ -339,6 +371,8 @@ async function dispatch(argv, context) {
339
371
  if (normalizedResource === "tasks" && ["list", "manage"].includes(action)) return tasksList(rest, context, { accountOverride });
340
372
  if (normalizedResource === "tasks" && action === "add") return tasksAdd(rest, context, { accountOverride });
341
373
  if (normalizedResource === "tasks" && action === "complete") return tasksComplete(rest, context, { accountOverride });
374
+ if (normalizedResource === "tasks" && action === "update") return tasksUpdate(rest, context, { accountOverride });
375
+ if (normalizedResource === "tasks" && action === "bulk-update") return tasksBulkUpdate(rest, context, { accountOverride });
342
376
  if (normalizedResource === "lists" && action === "list") return listsList(rest, context, { accountOverride });
343
377
  if (normalizedResource === "lists" && action === "create") return listsCreate(rest, context, { accountOverride });
344
378
  if (normalizedResource === "lists" && action === "show") return listsShow(rest, context, { accountOverride });
@@ -346,6 +380,9 @@ async function dispatch(argv, context) {
346
380
  if (normalizedResource === "lists" && action === "add-tag") return listsTagMutation("add", rest, context, { accountOverride });
347
381
  if (normalizedResource === "lists" && action === "remove-tag") return listsTagMutation("remove", rest, context, { accountOverride });
348
382
  if (normalizedResource === "lists" && action === "delete") return listsDelete(rest, context, { accountOverride });
383
+ if (normalizedResource === "lists" && action === "bulk-add-tag") return listsBulkAddTag(rest, context, { accountOverride });
384
+ if (normalizedResource === "lists" && action === "merge") return listsMerge(rest, context, { accountOverride });
385
+ if (normalizedResource === "lists" && action === "export") return listsExport(rest, context, { accountOverride });
349
386
  if (normalizedResource === "lists" && action === "prospects") return listProspects(rest, context, { accountOverride });
350
387
  if (normalizedResource === "lists" && action === "add-prospects") return listsAddProspects(rest, context, { accountOverride });
351
388
  if (normalizedResource === "lists" && action === "remove-prospects") return listsRemoveProspects(rest, context, { accountOverride });
@@ -409,6 +446,7 @@ async function dispatch(argv, context) {
409
446
  if (normalizedResource === "tools" && action === "get") return toolsGet(rest, context, { accountOverride });
410
447
  if (normalizedResource === "tools" && action === "humanize") return toolsHumanize(rest, context, { accountOverride });
411
448
  if (normalizedResource === "tools" && action === "linkedin-review") return toolsLinkedinReview(rest, context, { accountOverride });
449
+ if (normalizedResource === "tools" && action === "linkedin-strategy-review") return toolsLinkedinStrategyReview(rest, context, { accountOverride });
412
450
  if (normalizedResource === "operator" && action === "failed-drafts") return operatorFailedDrafts(rest, context, { accountOverride });
413
451
  if (normalizedResource === "operator" && action === "queue") return operatorQueue(rest, context, { accountOverride });
414
452
  if (normalizedResource === "operator" && action === "next") return operatorNext(rest, context, { accountOverride });
@@ -567,25 +605,50 @@ async function authLogin(args, context) {
567
605
 
568
606
  const host = normalizeHost(values.host || DEFAULT_HOST);
569
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 }) {
570
644
  const state = randomBytes(24).toString("hex");
571
645
  const callback = await createAuthCallbackServer({ state, context });
572
646
  const authUrl = new URL("/cli/auth", host);
573
647
  authUrl.searchParams.set("redirect_uri", callback.redirectUri);
574
648
  authUrl.searchParams.set("state", state);
575
649
 
576
- if (values.json) {
577
- writeJson(context.stdout, {
578
- kind: "auth_login_start",
579
- auth_url: authUrl.toString(),
580
- callback_url: callback.redirectUri,
581
- timeout_seconds: timeoutSeconds
582
- });
583
- } else {
584
- writeLine(context.stdout, "Open this URL to authenticate Audienti CLI:");
585
- writeLine(context.stdout, authUrl.toString());
586
- }
587
-
588
- if (!values["no-open"]) openBrowser(authUrl.toString(), context);
650
+ announce(authUrl.toString(), callback.redirectUri);
651
+ if (open) openBrowser(authUrl.toString(), context);
589
652
 
590
653
  let payload;
591
654
  try {
@@ -603,19 +666,473 @@ async function authLogin(args, context) {
603
666
  accountName: payload.account_name || undefined
604
667
  }, { env: context.env });
605
668
 
606
- const result = {
607
- kind: "auth_login_complete",
608
- host: client.host,
609
- user: user?.name || payload.user_name || payload.user_email || user?.email || null,
610
- account_id: payload.account_id || null,
611
- account_name: payload.account_name || null
669
+ return { client, user, payload };
670
+ }
671
+
672
+ async function start(args, context) {
673
+ const { values, positionals } = parseCommandArgs(args, {
674
+ ...jsonOptions(),
675
+ host: { type: "string" },
676
+ "no-open": { type: "boolean" }
677
+ });
678
+ if (positionals.length > 0) throw new CommandError(START_USAGE);
679
+
680
+ const interactive = stdinIsInteractive(context) && !values.json;
681
+ const say = (line = "") => {
682
+ if (!values.json) writeLine(context.stdout, line);
612
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);
613
704
 
614
- if (values.json) return writeJson(context.stdout, result);
705
+ say("Sign in or create an account to continue.");
706
+ say(` New here? Create an account at ${payload.sign_up_url}`);
707
+ say(" Then run `audienti auth login` in a terminal (it opens your browser),");
708
+ say(" or save an API token with `audienti auth token <token>`.");
709
+ say("Next: run `audienti start` again.");
710
+ return 0;
711
+ }
615
712
 
616
- writeLine(context.stdout, `Authenticated to ${client.host} as ${result.user || "current user"}.`);
617
- if (result.account_id) writeLine(context.stdout, `Selected account ${result.account_name || result.account_id} (${result.account_id}).`);
618
- if (!result.account_id) writeLine(context.stdout, "Run `audienti accounts list` to choose an account.");
713
+ const prompter = createPrompter(context);
714
+ try {
715
+ if (session) {
716
+ say(`You're signed in as ${session.label}.`);
717
+ } else {
718
+ say("First, sign in or create an account.");
719
+ const { client, user, payload } = await browserSignIn({
720
+ host,
721
+ timeoutSeconds: DEFAULT_START_LOGIN_TIMEOUT_SECONDS,
722
+ open: !values["no-open"],
723
+ context,
724
+ announce(authUrl) {
725
+ say("We'll open Audienti in your browser. New here? Choose \"sign up for an account\" on that page.");
726
+ say("If the browser doesn't open, visit:");
727
+ say(` ${authUrl}`);
728
+ say("Waiting for you to finish in the browser (confirm your email if asked)...");
729
+ }
730
+ });
731
+ config = await readConfig({ env: context.env });
732
+ session = { client, label: user?.name || payload.user_name || user?.email || payload.user_email || "you" };
733
+ say(`Signed in as ${session.label}.`);
734
+ }
735
+
736
+ const account = await chooseStartAccount(session.client, config, { interactive, prompter, say });
737
+ const accountUser = account ? await chooseStartAccountUser(session.client, account, { interactive, prompter, say }) : null;
738
+ if (account) {
739
+ const sameAccount = account.prefix_id === config.accountId;
740
+ await writeConfig({
741
+ ...config,
742
+ accountId: account.prefix_id,
743
+ accountName: account.name,
744
+ accountUserId: accountUser ? String(accountUser.id) : sameAccount ? config.accountUserId : undefined,
745
+ accountUserName: accountUser ? accountUser.name : sameAccount ? config.accountUserName : undefined,
746
+ accountUserEmail: accountUser ? accountUser.email : sameAccount ? config.accountUserEmail : undefined
747
+ }, { env: context.env });
748
+ }
749
+
750
+ const status = !account ? "account_selection_required" : !accountUser ? "account_user_selection_required" : "ready";
751
+ const nextCommand = status === "account_selection_required"
752
+ ? "audienti accounts select <acct_id>"
753
+ : status === "account_user_selection_required"
754
+ ? "audienti users select <account_user_id|email|name|me>"
755
+ : interactive ? "audienti setup" : SETUP_FLAGS_EXAMPLE;
756
+
757
+ if (values.json) {
758
+ return writeJson(context.stdout, {
759
+ kind: "start",
760
+ status,
761
+ signed_in: true,
762
+ host: session.client.host,
763
+ user: session.label,
764
+ account: account ? { id: account.prefix_id, name: account.name } : null,
765
+ account_user: accountUser ? { id: accountUser.id, name: accountUser.name || null, email: accountUser.email || null } : null,
766
+ next_command: nextCommand
767
+ });
768
+ }
769
+
770
+ if (status !== "ready" || !interactive) {
771
+ say(`Next: ${nextCommand}`);
772
+ return 0;
773
+ }
774
+
775
+ say();
776
+ if (await askYesNo(prompter, "Set up your first motion now? [Y/n] ", { defaultYes: true })) {
777
+ say();
778
+ return setupWizard([], context, { prompter });
779
+ }
780
+
781
+ say("No problem. When you're ready, run: audienti setup");
782
+ return 0;
783
+ } finally {
784
+ prompter.close();
785
+ }
786
+ }
787
+
788
+ async function savedSession(config, context) {
789
+ if (!config.token) return null;
790
+
791
+ const client = clientFromConfig(config, context);
792
+ try {
793
+ const user = await client.me();
794
+ return { client, label: user?.name || user?.email || "you" };
795
+ } catch (error) {
796
+ if (error instanceof ApiError && error.status === 401) return null;
797
+ throw error;
798
+ }
799
+ }
800
+
801
+ async function chooseStartAccount(client, config, { interactive, prompter, say }) {
802
+ const accounts = await client.accounts();
803
+ if (!Array.isArray(accounts) || accounts.length === 0) {
804
+ throw new CommandError("This login has no Audienti account yet. Finish creating one in the browser, then run `audienti start` again.");
805
+ }
806
+
807
+ const saved = accounts.find((account) => account.prefix_id === config.accountId);
808
+ if (accounts.length === 1) {
809
+ say(`Using account ${accounts[0].name} (${accounts[0].prefix_id}).`);
810
+ return accounts[0];
811
+ }
812
+
813
+ if (!interactive) {
814
+ if (saved) {
815
+ say(`Using account ${saved.name} (${saved.prefix_id}).`);
816
+ return saved;
817
+ }
818
+ say("You have several accounts:");
819
+ for (const account of accounts) say(` ${account.prefix_id}\t${account.name}`);
820
+ return null;
821
+ }
822
+
823
+ say("Which account should the CLI use?");
824
+ accounts.forEach((account, index) => {
825
+ say(` ${index + 1}. ${account.name} (${account.prefix_id})${account === saved ? " - current" : ""}`);
826
+ });
827
+ const defaultIndex = saved ? accounts.indexOf(saved) + 1 : null;
828
+ const account = await askChoice(prompter, accounts, {
829
+ question: defaultIndex ? `Account [${defaultIndex}]: ` : "Account: ",
830
+ defaultChoice: saved,
831
+ resolve: (answer) => resolveAccountSelection(accounts, answer)
832
+ });
833
+ say(`Using account ${account.name} (${account.prefix_id}).`);
834
+ return account;
835
+ }
836
+
837
+ async function chooseStartAccountUser(client, account, { interactive, prompter, say }) {
838
+ const users = await client.users(account.prefix_id);
839
+ const rows = Array.isArray(users) ? users : [];
840
+ const currentUsers = rows.filter((candidate) => candidate.current);
841
+ const automatic = currentUsers.length === 1 ? currentUsers[0] : rows.length === 1 ? rows[0] : null;
842
+ if (automatic) {
843
+ say(`Working as ${accountUserLabel(automatic)}.`);
844
+ return automatic;
845
+ }
846
+
847
+ if (!interactive || rows.length === 0) {
848
+ say("Pick which account user you are:");
849
+ for (const accountUser of rows) say(` ${accountUserLabel(accountUser)}`);
850
+ return null;
851
+ }
852
+
853
+ say("Which account user are you?");
854
+ rows.forEach((accountUser, index) => say(` ${index + 1}. ${accountUserLabel(accountUser)}`));
855
+ const accountUser = await askChoice(prompter, rows, {
856
+ question: "Account user: ",
857
+ resolve: (answer) => resolveAccountUserSelection(rows, answer)
858
+ });
859
+ say(`Working as ${accountUserLabel(accountUser)}.`);
860
+ return accountUser;
861
+ }
862
+
863
+ async function askChoice(prompter, choices, { question, defaultChoice = null, resolve }) {
864
+ for (;;) {
865
+ const answer = await prompter.ask(question);
866
+ if (!answer && defaultChoice) return defaultChoice;
867
+
868
+ const number = Number(answer);
869
+ if (Number.isInteger(number) && number >= 1 && number <= choices.length) return choices[number - 1];
870
+
871
+ try {
872
+ const match = answer ? resolve(answer) : null;
873
+ if (match) return match;
874
+ } catch (error) {
875
+ if (!(error instanceof CommandError)) throw error;
876
+ }
877
+ prompter.say(`Type a number from 1 to ${choices.length}.`);
878
+ }
879
+ }
880
+
881
+ async function askYesNo(prompter, question, { defaultYes = false } = {}) {
882
+ for (;;) {
883
+ const answer = (await prompter.ask(question)).toLowerCase();
884
+ if (!answer) return defaultYes;
885
+ if (/^y(es)?$/.test(answer)) return true;
886
+ if (/^no?$/.test(answer)) return false;
887
+ prompter.say("Please answer y or n.");
888
+ }
889
+ }
890
+
891
+ function stdinIsInteractive(context) {
892
+ return Boolean(context.stdin && context.stdin.isTTY);
893
+ }
894
+
895
+ // One line reader per command run. The async iterator buffers lines typed
896
+ // ahead of a question, so several questions can share one input stream.
897
+ function createPrompter(context) {
898
+ let reader;
899
+ let lines;
900
+
901
+ return {
902
+ async ask(question) {
903
+ if (!lines) {
904
+ reader = createInterface({ input: context.stdin, terminal: false });
905
+ lines = reader[Symbol.asyncIterator]();
906
+ }
907
+ context.stdout.write(question);
908
+ const { value, done } = await lines.next();
909
+ if (done) throw new CommandError("Input ended before the questions were answered. Run the command again when you're ready.");
910
+ return String(value).trim();
911
+ },
912
+ say(line = "") {
913
+ writeLine(context.stdout, line);
914
+ },
915
+ close() {
916
+ reader?.close();
917
+ }
918
+ };
919
+ }
920
+
921
+ const SETUP_QUESTIONS = [
922
+ {
923
+ key: "url",
924
+ title: "What's your company website?",
925
+ good: "https://acme.com",
926
+ bad: "acme (not a full web address)",
927
+ skip: "Press Enter to use the website saved on your account."
928
+ },
929
+ {
930
+ key: "sell-to",
931
+ title: "Who do you sell to? One sentence.",
932
+ good: "Heads of finance at US software companies with 50-500 people.",
933
+ bad: "Everyone who could use our product.",
934
+ skip: "Press Enter to skip."
935
+ },
936
+ {
937
+ key: "ask",
938
+ title: "What do you want the prospect to say yes to?",
939
+ good: "A 20-minute call to see if our audit fits their month-end close.",
940
+ bad: "Buy now.",
941
+ skip: "Press Enter to skip."
942
+ }
943
+ ];
944
+
945
+ async function setupWizard(args, context, { accountOverride, prompter: sharedPrompter } = {}) {
946
+ const { values, positionals } = parseCommandArgs(args, {
947
+ ...jsonOptions(),
948
+ url: { type: "string" },
949
+ "sell-to": { type: "string" },
950
+ ask: { type: "string" },
951
+ yes: { type: "boolean" }
952
+ });
953
+ if (positionals.length > 0) throw new CommandError(SETUP_USAGE);
954
+
955
+ const interactive = stdinIsInteractive(context) && !values.json;
956
+ const say = (line = "") => {
957
+ if (!values.json) writeLine(context.stdout, line);
958
+ };
959
+
960
+ if (!interactive && !values.url) {
961
+ throw new CommandError([
962
+ "No terminal is attached, so `audienti setup` can't ask questions.",
963
+ "Pass the answers as flags instead:",
964
+ ` ${SETUP_FLAGS_EXAMPLE}`,
965
+ SETUP_USAGE
966
+ ].join("\n"));
967
+ }
968
+
969
+ const config = await readConfig({ env: context.env });
970
+ if (!config.token) {
971
+ throw new CommandError("You're not signed in yet. Run `audienti start` to sign in or create an account, then run `audienti setup`.");
972
+ }
973
+ const accountId = accountOverride || config.accountId;
974
+ if (!accountId) {
975
+ throw new CommandError("No account is picked yet. Run `audienti start` to choose one, then run `audienti setup`.");
976
+ }
977
+ const client = clientFromConfig(config, context);
978
+ const prompter = sharedPrompter || createPrompter(context);
979
+
980
+ try {
981
+ const answers = { url: values.url, "sell-to": values["sell-to"], ask: values.ask };
982
+ if (interactive) {
983
+ say("Let's set up your first motion. Three quick questions.");
984
+ for (const [index, question] of SETUP_QUESTIONS.entries()) {
985
+ if (answers[question.key] !== undefined) continue;
986
+
987
+ say();
988
+ say(`${index + 1}/${SETUP_QUESTIONS.length} ${question.title}`);
989
+ say(` Good: ${question.good}`);
990
+ say(` Bad: ${question.bad}`);
991
+ say(` ${question.skip}`);
992
+ answers[question.key] = await prompter.ask("> ");
993
+ }
994
+ say();
995
+ }
996
+
997
+ const sellTo = cleanSentence(answers["sell-to"]);
998
+ const ask = cleanSentence(answers.ask);
999
+ const requestBody = {
1000
+ quick_start: compactObject({
1001
+ company_url: String(answers.url || "").trim() || undefined,
1002
+ feedback: setupFeedback({ sellTo, ask })
1003
+ })
1004
+ };
1005
+
1006
+ say("Reading your website and drafting your first motion. This usually takes a minute or two.");
1007
+ let draft;
1008
+ try {
1009
+ draft = await client.createQuickStart(accountId, requestBody);
1010
+ } catch (error) {
1011
+ throw setupCreateError(error);
1012
+ }
1013
+
1014
+ let polls = 0;
1015
+ draft = await waitForQuickStartDraft(client, accountId, draft, context, {
1016
+ timeoutSeconds: DEFAULT_SETUP_TIMEOUT_SECONDS,
1017
+ pollIntervalSeconds: DEFAULT_LOOKUP_POLL_INTERVAL_SECONDS,
1018
+ onPoll() {
1019
+ polls += 1;
1020
+ if (polls % 5 === 1) say(SETUP_PROGRESS[Math.min(Math.floor(polls / 5), SETUP_PROGRESS.length - 1)]);
1021
+ }
1022
+ });
1023
+
1024
+ if (draft?.status === "failed") throw setupDraftFailedError(draft?.error);
1025
+ if (!quickStartReady(draft)) {
1026
+ throw new CommandError("The draft is taking longer than usual. Nothing was created. Run `audienti setup` again in a few minutes.");
1027
+ }
1028
+
1029
+ renderSetupDraft(draft, { ask }, say);
1030
+
1031
+ let confirmed = Boolean(values.yes);
1032
+ if (!confirmed && interactive) confirmed = await askYesNo(prompter, "Create this? [y/N] ");
1033
+
1034
+ if (!confirmed) {
1035
+ const payload = {
1036
+ kind: "setup",
1037
+ status: "draft_ready",
1038
+ created: false,
1039
+ draft,
1040
+ next_command: interactive ? "audienti setup" : "Re-run with --yes to create it."
1041
+ };
1042
+ if (values.json) return writeJson(context.stdout, payload);
1043
+
1044
+ say("Nothing was created.");
1045
+ say(interactive
1046
+ ? "Run `audienti setup` again whenever you're ready. Changing your answers changes the draft."
1047
+ : "Re-run the same command with --yes to create it.");
1048
+ return 0;
1049
+ }
1050
+
1051
+ const confirmation = await client.confirmQuickStart(accountId, draft.id, {});
1052
+ const connectUrl = new URL("/user/social_cookies", client.host).toString();
1053
+ if (values.json) {
1054
+ return writeJson(context.stdout, {
1055
+ kind: "setup",
1056
+ status: "created",
1057
+ created: true,
1058
+ draft,
1059
+ motion: confirmation?.motion || null,
1060
+ reservation: confirmation?.reservation || null,
1061
+ next_step: {
1062
+ connect_linkedin_url: connectUrl,
1063
+ check_command: "audienti setup play preflight"
1064
+ }
1065
+ });
1066
+ }
1067
+
1068
+ const motion = confirmation?.motion || {};
1069
+ say(`Created your first motion: ${display(motion.name)} (${display(motion.prefix_id)}).`);
1070
+ say();
1071
+ say("Next: connect your LinkedIn account so Audienti can reach people for you.");
1072
+ say(` Connect it here: ${connectUrl}`);
1073
+ say(" Then check it with: audienti setup play preflight");
1074
+ say("Nothing is sent until LinkedIn is connected, and nothing sends without your approval.");
1075
+ return 0;
1076
+ } finally {
1077
+ if (!sharedPrompter) prompter.close();
1078
+ }
1079
+ }
1080
+
1081
+ const SETUP_PROGRESS = [
1082
+ "Reading your website...",
1083
+ "Working out who buys from you...",
1084
+ "Drafting your offer and the first buying signals...",
1085
+ "Still working. Thanks for waiting..."
1086
+ ];
1087
+
1088
+ function cleanSentence(value) {
1089
+ return String(value || "").trim().replace(/[\s.]+$/, "");
1090
+ }
1091
+
1092
+ function setupFeedback({ sellTo, ask }) {
1093
+ const parts = [];
1094
+ if (sellTo) parts.push(`We sell to: ${sellTo}.`);
1095
+ if (ask) parts.push(`What we want the prospect to say yes to: ${ask}.`);
1096
+ return parts.join(" ") || undefined;
1097
+ }
1098
+
1099
+ function setupCreateError(error) {
1100
+ if (!(error instanceof ApiError)) return error;
1101
+
1102
+ const reason = typeof error.body?.error === "string" ? cleanSentence(error.body.error) : null;
1103
+ if (error.status === 401) {
1104
+ return new CommandError("Your saved login no longer works. Run `audienti start` to sign in again.");
1105
+ }
1106
+ if (error.status === 403) {
1107
+ return new CommandError("Only an account admin can replace the website already saved on this account. Run `audienti setup` again and press Enter at the website question to use the saved one.");
1108
+ }
1109
+ if (error.status === 422 && error.body?.status === "failed") return setupDraftFailedError(reason);
1110
+ if (error.status === 422) {
1111
+ return new CommandError(`That website didn't work: ${reason || "it was rejected"}. Use a full public address like https://acme.com, then run \`audienti setup\` again.`);
1112
+ }
1113
+ return error;
1114
+ }
1115
+
1116
+ function setupDraftFailedError(reason) {
1117
+ const detail = reason ? ` (${cleanSentence(reason)})` : "";
1118
+ return new CommandError(`We couldn't draft a motion from that website${detail}. Nothing was created. Check the address or add a sentence about who you sell to, then run \`audienti setup\` again.`);
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();
619
1136
  }
620
1137
 
621
1138
  async function createAuthCallbackServer({ state, context }) {
@@ -1250,6 +1767,65 @@ async function offersDelete(args, context, { accountOverride } = {}) {
1250
1767
  writeLine(context.stdout, `Deleted offer ${display(payload?.name)} (${display(payload?.prefix_id)}).`);
1251
1768
  }
1252
1769
 
1770
+ async function offersRegenerateResearch(args, context, { accountOverride } = {}) {
1771
+ const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), guidance: { type: "string" } });
1772
+ if (positionals.length !== 1) throw new CommandError(OFFERS_REGENERATE_RESEARCH_USAGE);
1773
+
1774
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1775
+ const offer = await client.regenerateOfferResearch(accountId, positionals[0], compactObject({ guidance: values.guidance }));
1776
+ if (values.json) return writeJson(context.stdout, offer);
1777
+
1778
+ writeLine(context.stdout, `Queued a new research write-up for offer ${display(offer?.name)} (${display(offer?.prefix_id)}).`);
1779
+ }
1780
+
1781
+ async function offersUpdateWriteup(args, context, { accountOverride } = {}) {
1782
+ const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), description: { type: "string" } });
1783
+ if (positionals.length !== 1 || values.description === undefined) throw new CommandError(OFFERS_UPDATE_WRITEUP_USAGE);
1784
+
1785
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1786
+ const offer = await client.updateOfferWriteup(accountId, positionals[0], { offer: { description: values.description } });
1787
+ if (values.json) return writeJson(context.stdout, offer);
1788
+
1789
+ writeLine(context.stdout, `Updated the write-up for offer ${display(offer?.name)} (${display(offer?.prefix_id)}).`);
1790
+ }
1791
+
1792
+ async function offersAddArtifacts(args, context, { accountOverride } = {}) {
1793
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1794
+ if (positionals.length < 2) throw new CommandError(OFFERS_ADD_ARTIFACTS_USAGE);
1795
+
1796
+ const [offerId, ...paths] = positionals;
1797
+ const artifacts = [];
1798
+ for (const path of paths) {
1799
+ let data;
1800
+ try {
1801
+ data = await readFile(path);
1802
+ } catch (error) {
1803
+ throw new CommandError(`Cannot read ${path}: ${error.message}`);
1804
+ }
1805
+ artifacts.push({ filename: basename(path), data: data.toString("base64") });
1806
+ }
1807
+
1808
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1809
+ const payload = await client.addOfferArtifacts(accountId, offerId, { artifacts });
1810
+ if (values.json) return writeJson(context.stdout, payload);
1811
+
1812
+ const count = payload?.attached_count ?? artifacts.length;
1813
+ writeLine(context.stdout, `Attached ${count} ${count === 1 ? "artifact" : "artifacts"} to offer ${display(payload?.offer_id || offerId)}.`);
1814
+ renderOfferArtifacts(payload?.artifacts, context);
1815
+ }
1816
+
1817
+ async function offersRemoveArtifact(args, context, { accountOverride } = {}) {
1818
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1819
+ if (positionals.length !== 2) throw new CommandError(OFFERS_REMOVE_ARTIFACT_USAGE);
1820
+
1821
+ const [offerId, artifactId] = positionals;
1822
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1823
+ const payload = await client.removeOfferArtifact(accountId, offerId, artifactId);
1824
+ if (values.json) return writeJson(context.stdout, payload);
1825
+
1826
+ writeLine(context.stdout, `Removed artifact ${display(artifactId)} from offer ${display(payload?.offer_id || offerId)}.`);
1827
+ }
1828
+
1253
1829
  async function icpsList(args, context, { accountOverride } = {}) {
1254
1830
  const { values, positionals } = parseCommandArgs(args, {
1255
1831
  ...jsonOptions(),
@@ -1359,6 +1935,68 @@ async function icpsTagMutation(action, args, context, { accountOverride } = {})
1359
1935
  if (Array.isArray(icp?.tags)) writeLine(context.stdout, `Tags: ${display(icp.tags.join(", "), "-")}`);
1360
1936
  }
1361
1937
 
1938
+ async function icpsBulkAddTag(args, context, { accountOverride } = {}) {
1939
+ const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), tag: { type: "string" } });
1940
+ if (positionals.length === 0 || !String(values.tag || "").trim()) throw new CommandError(ICPS_BULK_ADD_TAG_USAGE);
1941
+
1942
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1943
+ const icps = await client.bulkAddIcpTag(accountId, { icp_ids: positionals, tag: values.tag });
1944
+ if (values.json) return writeJson(context.stdout, icps);
1945
+
1946
+ const count = Array.isArray(icps) ? icps.length : 0;
1947
+ writeLine(context.stdout, `Added tag ${display(values.tag)} to ${count} ${count === 1 ? "ICP" : "ICPs"}.`);
1948
+ }
1949
+
1950
+ async function icpsClone(args, context, { accountOverride } = {}) {
1951
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1952
+ if (positionals.length !== 1) throw new CommandError(ICPS_CLONE_USAGE);
1953
+
1954
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1955
+ const icp = await client.cloneIcp(accountId, positionals[0]);
1956
+ if (values.json) return writeJson(context.stdout, icp);
1957
+
1958
+ writeLine(context.stdout, `Cloned ICP as ${display(icp?.name)} (${display(icp?.prefix_id)}).`);
1959
+ }
1960
+
1961
+ async function icpsDelete(args, context, { accountOverride } = {}) {
1962
+ const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), confirm: { type: "string" } });
1963
+ const normalizedConfirm = String(values.confirm || "").trim().toLowerCase();
1964
+ if (positionals.length !== 1 || !DELETE_CONFIRMATION_VALUES.has(normalizedConfirm)) throw new CommandError(ICPS_DELETE_USAGE);
1965
+
1966
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1967
+ const payload = await client.deleteIcp(accountId, positionals[0]);
1968
+ if (values.json) return writeJson(context.stdout, payload);
1969
+
1970
+ writeLine(context.stdout, `Deleted ICP ${display(payload?.name)} (${display(payload?.prefix_id)}).`);
1971
+ }
1972
+
1973
+ async function icpsProspects(args, context, { accountOverride } = {}) {
1974
+ const { values, positionals } = parseCommandArgs(args, {
1975
+ ...jsonOptions(),
1976
+ query: { type: "string" },
1977
+ limit: { type: "string" },
1978
+ offset: { type: "string" },
1979
+ page: { type: "string" },
1980
+ profiles: { type: "boolean" }
1981
+ });
1982
+ if (positionals.length !== 1) throw new CommandError(ICPS_PROSPECTS_USAGE);
1983
+ if (values.page && values.offset) throw new CommandError("Choose one pagination mode: use either --page or --offset.");
1984
+
1985
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1986
+ const payload = await client.icpProspects(accountId, positionals[0], compactObject({
1987
+ query: values.query,
1988
+ limit: values.limit,
1989
+ offset: values.offset,
1990
+ page: values.page,
1991
+ include_profiles: values.profiles
1992
+ }));
1993
+ if (values.json) return writeJson(context.stdout, payload);
1994
+
1995
+ renderProspects(payload, context, { profiles: values.profiles });
1996
+ const meta = payload?.meta || {};
1997
+ writeLine(context.stdout, `Showing ${display(meta.returned_count, 0)} of ${display(meta.total_count, 0)} matched prospects.`);
1998
+ }
1999
+
1362
2000
  async function companiesSearch(args, context, { accountOverride } = {}) {
1363
2001
  const { values, positionals } = parseCommandArgs(args, {
1364
2002
  ...jsonOptions(),
@@ -1952,21 +2590,71 @@ async function tasksAdd(args, context, { accountOverride } = {}) {
1952
2590
  const payload = await client.createTask(accountId, { task });
1953
2591
  if (values.json) return writeJson(context.stdout, payload);
1954
2592
 
1955
- writeLine(context.stdout, `Created task ${display(payload?.prefix_id || payload?.id)}.`);
2593
+ writeLine(context.stdout, `Created task ${display(payload?.prefix_id || payload?.id)}.`);
2594
+ writeLine(context.stdout, `Title: ${display(payload?.title)}`);
2595
+ if (payload?.notes) writeLine(context.stdout, `Description: ${payload.notes}`);
2596
+ writeLine(context.stdout, `Due: ${display(payload?.due_at, "-")} | Association: ${taskAssociationLabel(payload)}`);
2597
+ }
2598
+
2599
+ async function tasksComplete(args, context, { accountOverride } = {}) {
2600
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
2601
+ if (positionals.length !== 1) throw new CommandError(TASKS_COMPLETE_USAGE);
2602
+
2603
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
2604
+ const payload = await client.completeTask(accountId, positionals[0]);
2605
+ if (values.json) return writeJson(context.stdout, payload);
2606
+
2607
+ writeLine(context.stdout, `Completed task ${display(payload?.prefix_id || payload?.id || positionals[0])}.`);
2608
+ }
2609
+
2610
+ async function tasksUpdate(args, context, { accountOverride } = {}) {
2611
+ const { values, positionals } = parseCommandArgs(args, taskAddOptions());
2612
+ const fields = {
2613
+ title: values.title,
2614
+ due_at: values.due,
2615
+ notes: values.notes,
2616
+ prospect_id: values.prospect,
2617
+ list_id: values.list,
2618
+ assignee_account_user_id: values["assigned-user"]
2619
+ };
2620
+ const sentFields = Object.entries(fields).filter(([, value]) => value !== undefined);
2621
+ if (positionals.length !== 1 || sentFields.length === 0) throw new CommandError(TASKS_UPDATE_USAGE);
2622
+ if (values.prospect && values.list) throw new CommandError("Use either --prospect or --list, not both.");
2623
+
2624
+ const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
2625
+ const task = Object.fromEntries(sentFields);
2626
+ if (task.assignee_account_user_id !== undefined) {
2627
+ task.assignee_account_user_id = resolveAccountUserId(task.assignee_account_user_id, config, { accountOverride }) ?? "";
2628
+ }
2629
+ const payload = await client.updateTask(accountId, positionals[0], { task });
2630
+ if (values.json) return writeJson(context.stdout, payload);
2631
+
2632
+ writeLine(context.stdout, `Updated task ${display(payload?.prefix_id || payload?.id || positionals[0])}.`);
1956
2633
  writeLine(context.stdout, `Title: ${display(payload?.title)}`);
1957
- if (payload?.notes) writeLine(context.stdout, `Description: ${payload.notes}`);
1958
2634
  writeLine(context.stdout, `Due: ${display(payload?.due_at, "-")} | Association: ${taskAssociationLabel(payload)}`);
1959
2635
  }
1960
2636
 
1961
- async function tasksComplete(args, context, { accountOverride } = {}) {
1962
- const { values, positionals } = parseCommandArgs(args, jsonOptions());
1963
- if (positionals.length !== 1) throw new CommandError(TASKS_COMPLETE_USAGE);
2637
+ async function tasksBulkUpdate(args, context, { accountOverride } = {}) {
2638
+ const { values, positionals } = parseCommandArgs(args, {
2639
+ ...jsonOptions(),
2640
+ action: { type: "string" },
2641
+ "assigned-user": { type: "string" }
2642
+ });
2643
+ const bulkAction = String(values.action || "").trim();
2644
+ if (positionals.length === 0 || !["complete", "reassign"].includes(bulkAction)) throw new CommandError(TASKS_BULK_UPDATE_USAGE);
2645
+ if (bulkAction === "reassign" && !values["assigned-user"]) throw new CommandError("--assigned-user is required with --action reassign.");
1964
2646
 
1965
- const { client, accountId } = await requireAccountContext(context, { accountOverride });
1966
- const payload = await client.completeTask(accountId, positionals[0]);
2647
+ const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
2648
+ const payload = await client.bulkUpdateTasks(accountId, compactObject({
2649
+ task_ids: positionals,
2650
+ bulk_action: bulkAction,
2651
+ assignee_account_user_id: bulkAction === "reassign" ? resolveAccountUserId(values["assigned-user"], config, { accountOverride }) : undefined
2652
+ }));
1967
2653
  if (values.json) return writeJson(context.stdout, payload);
1968
2654
 
1969
- writeLine(context.stdout, `Completed task ${display(payload?.prefix_id || payload?.id || positionals[0])}.`);
2655
+ const count = payload?.count ?? 0;
2656
+ const noun = count === 1 ? "task" : "tasks";
2657
+ writeLine(context.stdout, bulkAction === "complete" ? `Completed ${count} ${noun}.` : `Queued reassignment of ${count} ${noun}.`);
1970
2658
  }
1971
2659
 
1972
2660
  async function listsCreate(args, context, { accountOverride } = {}) {
@@ -2086,6 +2774,52 @@ async function listsDelete(args, context, { accountOverride } = {}) {
2086
2774
  }
2087
2775
  }
2088
2776
 
2777
+ async function listsBulkAddTag(args, context, { accountOverride } = {}) {
2778
+ const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), tag: { type: "string" } });
2779
+ if (positionals.length === 0 || !String(values.tag || "").trim()) throw new CommandError(LISTS_BULK_ADD_TAG_USAGE);
2780
+
2781
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
2782
+ const lists = await client.bulkAddListTag(accountId, { list_ids: positionals, tag: values.tag });
2783
+ if (values.json) return writeJson(context.stdout, lists);
2784
+
2785
+ const count = Array.isArray(lists) ? lists.length : 0;
2786
+ writeLine(context.stdout, `Added tag ${display(values.tag)} to ${count} ${count === 1 ? "list" : "lists"}.`);
2787
+ }
2788
+
2789
+ async function listsMerge(args, context, { accountOverride } = {}) {
2790
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
2791
+ if (positionals.length !== 2) throw new CommandError(LISTS_MERGE_USAGE);
2792
+
2793
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
2794
+ const list = await client.mergeLists(accountId, { list_ids: positionals });
2795
+ if (values.json) return writeJson(context.stdout, list);
2796
+
2797
+ writeLine(context.stdout, `Merged into list ${display(list?.name)} (${display(list?.prefix_id)}); the newer list was deleted.`);
2798
+ writeLine(context.stdout, `Prospects: ${display(list?.prospect_count, 0)}`);
2799
+ }
2800
+
2801
+ async function listsExport(args, context, { accountOverride } = {}) {
2802
+ const { values, positionals } = parseCommandArgs(args, {
2803
+ ...jsonOptions(),
2804
+ output: { type: "string" },
2805
+ "inactive-reason": { type: "string" }
2806
+ });
2807
+ if (positionals.length !== 1) throw new CommandError(LISTS_EXPORT_USAGE);
2808
+
2809
+ const listId = positionals[0];
2810
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
2811
+ const csv = String(await client.exportList(accountId, listId, compactObject({ inactive_reason: values["inactive-reason"] })) ?? "");
2812
+
2813
+ if (values.output) {
2814
+ await writeFile(values.output, csv);
2815
+ if (values.json) return writeJson(context.stdout, { list_id: listId, output: values.output, bytes: Buffer.byteLength(csv) });
2816
+ return writeLine(context.stdout, `Wrote list ${display(listId)} CSV to ${values.output}.`);
2817
+ }
2818
+ if (values.json) return writeJson(context.stdout, { list_id: listId, csv });
2819
+
2820
+ context.stdout.write(csv);
2821
+ }
2822
+
2089
2823
  async function listProspects(args, context, { accountOverride } = {}) {
2090
2824
  const { values, positionals } = parseCommandArgs(args, {
2091
2825
  ...jsonOptions(),
@@ -2167,7 +2901,7 @@ async function listsRemoveProspects(args, context, { accountOverride } = {}) {
2167
2901
  async function listsRoutingRules(args, context, { accountOverride } = {}) {
2168
2902
  const [listId, subaction, ...rest] = args;
2169
2903
  if (!listId || !subaction) {
2170
- throw new CommandError("Usage: audienti lists routing-rules <list_id> <list|create|update|remove|move|apply> [args] [--json] [--account <acct_id>]");
2904
+ throw new CommandError("Usage: audienti lists routing-rules <list_id> <list|create|update|remove|move|toggle|apply> [args] [--json] [--account <acct_id>]");
2171
2905
  }
2172
2906
 
2173
2907
  if (subaction === "list") return listRoutingRulesList(listId, rest, context, { accountOverride });
@@ -2175,9 +2909,10 @@ async function listsRoutingRules(args, context, { accountOverride } = {}) {
2175
2909
  if (subaction === "update") return listRoutingRulesUpdate(listId, rest, context, { accountOverride });
2176
2910
  if (["remove", "delete"].includes(subaction)) return listRoutingRulesRemove(listId, rest, context, { accountOverride });
2177
2911
  if (subaction === "move") return listRoutingRulesMove(listId, rest, context, { accountOverride });
2912
+ if (subaction === "toggle") return listRoutingRulesToggle(listId, rest, context, { accountOverride });
2178
2913
  if (subaction === "apply") return listRoutingRulesApply(listId, rest, context, { accountOverride });
2179
2914
 
2180
- throw new CommandError("Usage: audienti lists routing-rules <list_id> <list|create|update|remove|move|apply> [args] [--json] [--account <acct_id>]");
2915
+ throw new CommandError("Usage: audienti lists routing-rules <list_id> <list|create|update|remove|move|toggle|apply> [args] [--json] [--account <acct_id>]");
2181
2916
  }
2182
2917
 
2183
2918
  async function listRoutingRulesList(listId, args, context, { accountOverride } = {}) {
@@ -2251,6 +2986,19 @@ async function listRoutingRulesMove(listId, args, context, { accountOverride } =
2251
2986
  renderListRoutingRules(payload, context);
2252
2987
  }
2253
2988
 
2989
+ async function listRoutingRulesToggle(listId, args, context, { accountOverride } = {}) {
2990
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
2991
+ if (positionals.length !== 1) throw new CommandError(LIST_ROUTING_RULES_TOGGLE_USAGE);
2992
+
2993
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
2994
+ const payload = await client.toggleListRoutingRule(accountId, listId, positionals[0]);
2995
+ if (values.json) return writeJson(context.stdout, payload);
2996
+
2997
+ const rule = payload?.routing_rule;
2998
+ const state = rule?.enabled ? "Enabled" : "Disabled";
2999
+ writeLine(context.stdout, `${state} routing rule ${display(rule?.name)} (${display(rule?.id || positionals[0])}) on list ${display(payload?.list_id || listId)}.`);
3000
+ }
3001
+
2254
3002
  async function listRoutingRulesApply(listId, args, context, { accountOverride } = {}) {
2255
3003
  const { values, positionals } = parseCommandArgs(args, jsonOptions());
2256
3004
  if (positionals.length > 0) throw new CommandError(LIST_ROUTING_RULES_APPLY_USAGE);
@@ -3803,6 +4551,23 @@ async function toolsLinkedinReviewStatus(args, context, { accountOverride } = {}
3803
4551
  renderLinkedinReviewStatus(payload, context);
3804
4552
  }
3805
4553
 
4554
+ async function toolsLinkedinStrategyReview(args, context, { accountOverride } = {}) {
4555
+ const [subaction, ...rest] = args;
4556
+ if (subaction !== "delete") throw new CommandError(TOOLS_LINKEDIN_STRATEGY_REVIEW_DELETE_USAGE);
4557
+
4558
+ const { values, positionals } = parseCommandArgs(rest, { ...jsonOptions(), confirm: { type: "string" } });
4559
+ const normalizedConfirm = String(values.confirm || "").trim().toLowerCase();
4560
+ if (positionals.length !== 1 || !DELETE_CONFIRMATION_VALUES.has(normalizedConfirm)) {
4561
+ throw new CommandError(TOOLS_LINKEDIN_STRATEGY_REVIEW_DELETE_USAGE);
4562
+ }
4563
+
4564
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
4565
+ const payload = await client.deleteLinkedinStrategyReview(accountId, positionals[0]);
4566
+ if (values.json) return writeJson(context.stdout, payload);
4567
+
4568
+ writeLine(context.stdout, `Deleted LinkedIn strategy review report ${display(payload?.id || positionals[0])}.`);
4569
+ }
4570
+
3806
4571
  async function operatorQueue(args, context, { accountOverride } = {}) {
3807
4572
  const { values, positionals } = parseCommandArgs(args, operatorFilterOptions(operatorPaginationOptions()));
3808
4573
  if (positionals.length > 0) throw new CommandError("Usage: audienti operator queue [--json] [filters] [--account <acct_id>]");
@@ -4140,9 +4905,8 @@ async function inboxOpsActionsInBatches(client, accountId, { operation, rows, dr
4140
4905
  }
4141
4906
 
4142
4907
  async function confirmInboxOpsAction(context) {
4143
- const input = context.stdin;
4144
- if (!input || !input.isTTY) return false;
4145
- const prompt = createInterface({ input, output: context.stdout });
4908
+ if (!stdinIsInteractive(context)) return false;
4909
+ const prompt = createInterface({ input: context.stdin, output: context.stdout });
4146
4910
  try {
4147
4911
  const answer = await prompt.question("Apply? [y/N] ");
4148
4912
  return /^y(es)?$/i.test(String(answer).trim());
@@ -6128,6 +6892,16 @@ function renderOffer(offer, context) {
6128
6892
  writeLine(context.stdout, `Offer: ${display(offer?.name)} (${display(offer?.prefix_id)})`);
6129
6893
  if (offer?.description) writeLine(context.stdout, `Description: ${offer.description}`);
6130
6894
  if (offer?.url) writeLine(context.stdout, `URL: ${offer.url}`);
6895
+ if (Array.isArray(offer?.artifacts) && offer.artifacts.length > 0) renderOfferArtifacts(offer.artifacts, context);
6896
+ }
6897
+
6898
+ function renderOfferArtifacts(artifacts, context) {
6899
+ if (!Array.isArray(artifacts) || artifacts.length === 0) return writeLine(context.stdout, "No artifacts attached.");
6900
+
6901
+ writeLine(context.stdout, "ARTIFACT ID\tFILENAME\tTYPE\tBYTES");
6902
+ for (const artifact of artifacts) {
6903
+ writeLine(context.stdout, [display(artifact.id), display(artifact.filename), display(artifact.content_type, "-"), display(artifact.byte_size, 0)].join("\t"));
6904
+ }
6131
6905
  }
6132
6906
 
6133
6907
  function renderIcps(icps, context) {
@@ -7825,13 +8599,14 @@ function quickStartTerminal(draft) {
7825
8599
  return quickStartReady(draft) || draft?.status === "failed";
7826
8600
  }
7827
8601
 
7828
- async function waitForQuickStartDraft(client, accountId, initialDraft, context, { timeoutSeconds, pollIntervalSeconds }) {
8602
+ async function waitForQuickStartDraft(client, accountId, initialDraft, context, { timeoutSeconds, pollIntervalSeconds, onPoll }) {
7829
8603
  let draft = initialDraft;
7830
8604
  const deadline = context.now().getTime() + timeoutSeconds * 1000;
7831
8605
 
7832
8606
  while (!quickStartTerminal(draft)) {
7833
8607
  if (context.now().getTime() >= deadline) return draft;
7834
8608
 
8609
+ onPoll?.();
7835
8610
  await context.sleep(pollIntervalSeconds * 1000);
7836
8611
  draft = await client.quickStart(accountId, draft.id);
7837
8612
  }
@@ -8889,6 +9664,8 @@ const HELP_TOPICS = new Map([
8889
9664
  " audienti <command> [options]",
8890
9665
  "",
8891
9666
  "Start:",
9667
+ " audienti start Guided first run: sign in, pick your account",
9668
+ " audienti setup Guided setup of your first motion",
8892
9669
  " audienti auth login Sign in through the browser",
8893
9670
  " audienti auth token <token> Save an API token",
8894
9671
  " audienti accounts list See accounts available to this token",
@@ -8971,15 +9748,23 @@ const HELP_TOPICS = new Map([
8971
9748
  " audienti lists routing-rules <list_id> list",
8972
9749
  " audienti lists routing-rules <list_id> create --payload <file.json>",
8973
9750
  " audienti lists routing-rules <list_id> apply",
9751
+ " audienti lists bulk-add-tag --tag <tag> <list_id> [list_id...]",
9752
+ " audienti lists merge <list_id> <list_id>",
9753
+ " audienti lists export <list_id> [--output <file.csv>]",
8974
9754
  " audienti tags list",
8975
9755
  " audienti tags show <tag>",
8976
9756
  " audienti tasks list [--status open]",
8977
9757
  " audienti tasks add --title <text> --due <time>",
8978
9758
  " audienti tasks complete <ptsk_id>",
9759
+ " audienti tasks update <ptsk_id> [--title <text>]",
9760
+ " audienti tasks bulk-update --action <complete|reassign> <ptsk_id> [ptsk_id...]",
8979
9761
  " audienti offers list",
8980
9762
  " audienti offers show <offr_id>",
8981
9763
  " audienti offers update <offr_id> [--name <text>]",
8982
9764
  " audienti offers delete <offr_id> --confirm <yes|true|Y|y>",
9765
+ " audienti offers regenerate-research <offr_id> [--guidance <text>]",
9766
+ " audienti offers update-writeup <offr_id> --description <text>",
9767
+ " audienti offers add-artifacts <offr_id> <file> [file...]",
8983
9768
  " audienti icps list [--status <active|archived|all>] [--tag <tag>]",
8984
9769
  " audienti icps show <icp_id>",
8985
9770
  " audienti icps update <icp_id> [--tags <tag[,tag...]> | --payload <file.json>]",
@@ -8987,6 +9772,10 @@ const HELP_TOPICS = new Map([
8987
9772
  " audienti icps restore <icp_id>",
8988
9773
  " audienti icps add-tag <icp_id> <tag>",
8989
9774
  " audienti icps remove-tag <icp_id> <tag>",
9775
+ " audienti icps bulk-add-tag --tag <tag> <icp_id> [icp_id...]",
9776
+ " audienti icps clone <icp_id>",
9777
+ " audienti icps delete <icp_id> --confirm <yes|true|Y|y>",
9778
+ " audienti icps prospects <icp_id> [--query <text>]",
8990
9779
  " audienti companies search --query <text>",
8991
9780
  " audienti linkedin-lookups <kind> [--query <text>]",
8992
9781
  " audienti dnc list",
@@ -9338,16 +10127,47 @@ const HELP_TOPICS = new Map([
9338
10127
  " Saves accountId and accountName in local CLI config."
9339
10128
  ].join("\n")],
9340
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
+
9341
10148
  ["setup", [
9342
10149
  "Usage:",
10150
+ ` ${SETUP_USAGE.slice("Usage: ".length)}`,
9343
10151
  " audienti setup play preflight [--principal <account_user_id|email|name|me>] [--platform linkedin] [--json]",
9344
10152
  "",
9345
10153
  "Status: implemented",
9346
10154
  "",
9347
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.",
9348
10159
  " Preflight account setup before an agent creates or activates an Audienti play.",
9349
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
+ "",
9350
10169
  "Commands:",
10170
+ " audienti setup Guided setup of your first motion",
9351
10171
  " audienti setup play preflight Check connected-account readiness and direct setup URLs"
9352
10172
  ].join("\n")],
9353
10173
 
@@ -9528,6 +10348,10 @@ const HELP_TOPICS = new Map([
9528
10348
  " audienti offers create --name <text> [--json]",
9529
10349
  " audienti offers update <offr_id> [--name <text>] [--json]",
9530
10350
  " audienti offers delete <offr_id> --confirm <yes|true|Y|y> [--json]",
10351
+ " audienti offers regenerate-research <offr_id> [--guidance <text>] [--json]",
10352
+ " audienti offers update-writeup <offr_id> --description <text> [--json]",
10353
+ " audienti offers add-artifacts <offr_id> <file> [file...] [--json]",
10354
+ " audienti offers remove-artifact <offr_id> <artifact_id> [--json]",
9531
10355
  "",
9532
10356
  "Status: implemented",
9533
10357
  "",
@@ -9639,6 +10463,10 @@ const HELP_TOPICS = new Map([
9639
10463
  " audienti icps remove-tag <icp_id> <tag> [--json]",
9640
10464
  " audienti icps archive <icp_id> [--json]",
9641
10465
  " audienti icps restore <icp_id> [--json]",
10466
+ " audienti icps bulk-add-tag --tag <tag> <icp_id> [icp_id...] [--json]",
10467
+ " audienti icps clone <icp_id> [--json]",
10468
+ " audienti icps delete <icp_id> --confirm <yes|true|Y|y> [--json]",
10469
+ " audienti icps prospects <icp_id> [--query <text>] [--json]",
9642
10470
  "",
9643
10471
  "Status: implemented",
9644
10472
  "",
@@ -10337,6 +11165,8 @@ const HELP_TOPICS = new Map([
10337
11165
  ` ${TASKS_LIST_USAGE.slice("Usage: ".length)}`,
10338
11166
  ` ${TASKS_ADD_USAGE.slice("Usage: ".length)}`,
10339
11167
  ` ${TASKS_COMPLETE_USAGE.slice("Usage: ".length)}`,
11168
+ ` ${TASKS_UPDATE_USAGE.slice("Usage: ".length)}`,
11169
+ ` ${TASKS_BULK_UPDATE_USAGE.slice("Usage: ".length)}`,
10340
11170
  "",
10341
11171
  "Status: implemented",
10342
11172
  "",
@@ -10417,10 +11247,13 @@ const HELP_TOPICS = new Map([
10417
11247
  " audienti lists add-tag <list_id> <tag> [--json]",
10418
11248
  " audienti lists remove-tag <list_id> <tag> [--json]",
10419
11249
  " audienti lists delete <list_id> --confirm <yes|true|Y|y> [--json]",
11250
+ " audienti lists bulk-add-tag --tag <tag> <list_id> [list_id...] [--json]",
11251
+ " audienti lists merge <list_id> <list_id> [--json]",
11252
+ " audienti lists export <list_id> [--output <file.csv>] [--json]",
10420
11253
  " audienti lists prospects <list_id> [--json]",
10421
11254
  " audienti lists add-prospects <list_id> <prsp_id> [prsp_id...] [--json]",
10422
11255
  " audienti lists remove-prospects <list_id> <prsp_id> [prsp_id...] [--json]",
10423
- " audienti lists routing-rules <list_id> <list|create|update|remove|move|apply> [args] [--json]",
11256
+ " audienti lists routing-rules <list_id> <list|create|update|remove|move|toggle|apply> [args] [--json]",
10424
11257
  "",
10425
11258
  "Status: read, create, update, delete, membership, and routing-rule commands implemented",
10426
11259
  "",
@@ -10435,6 +11268,7 @@ const HELP_TOPICS = new Map([
10435
11268
  " audienti lists routing-rules <list_id> update <rule_id> --payload <file.json> [--json] [--account <acct_id>]",
10436
11269
  " audienti lists routing-rules <list_id> remove <rule_id> [--json] [--account <acct_id>]",
10437
11270
  " audienti lists routing-rules <list_id> move <rule_id> <up|down> [--json] [--account <acct_id>]",
11271
+ " audienti lists routing-rules <list_id> toggle <rule_id> [--json] [--account <acct_id>]",
10438
11272
  " audienti lists routing-rules <list_id> apply [--json] [--account <acct_id>]",
10439
11273
  "",
10440
11274
  "Status: implemented",
@@ -10576,6 +11410,259 @@ const HELP_TOPICS = new Map([
10576
11410
  " PATCH /api/v1/accounts/:account_id/lists/:id.json"
10577
11411
  ].join("\n")],
10578
11412
 
11413
+ ["lists bulk-add-tag", [
11414
+ "Usage:",
11415
+ ` ${LISTS_BULK_ADD_TAG_USAGE.slice("Usage: ".length)}`,
11416
+ "",
11417
+ "Status: implemented",
11418
+ "",
11419
+ "Purpose:",
11420
+ " Add one normalized tag to several lists at once. All lists change or none do.",
11421
+ "",
11422
+ "Input shape:",
11423
+ " list_id: list_ prefix id or integer id; every id must belong to the account",
11424
+ " tag: string",
11425
+ "",
11426
+ "API:",
11427
+ " POST /api/v1/accounts/:account_id/lists/bulk_add_tag.json"
11428
+ ].join("\n")],
11429
+
11430
+ ["lists merge", [
11431
+ "Usage:",
11432
+ ` ${LISTS_MERGE_USAGE.slice("Usage: ".length)}`,
11433
+ "",
11434
+ "Status: implemented",
11435
+ "",
11436
+ "Purpose:",
11437
+ " Merge two lists. The older list keeps its name and gains the newer list's tags, prospects, agents, and routing targets; the newer list is deleted.",
11438
+ " System and HubSpot-synced lists cannot be merged.",
11439
+ "",
11440
+ "Input shape:",
11441
+ " list_id: exactly two different list ids in the account",
11442
+ "",
11443
+ "API:",
11444
+ " POST /api/v1/accounts/:account_id/lists/merge_selected.json"
11445
+ ].join("\n")],
11446
+
11447
+ ["lists export", [
11448
+ "Usage:",
11449
+ ` ${LISTS_EXPORT_USAGE.slice("Usage: ".length)}`,
11450
+ "",
11451
+ "Status: implemented",
11452
+ "",
11453
+ "Purpose:",
11454
+ " Download the list's prospects as CSV with the same columns as the web export.",
11455
+ " Prints to stdout unless --output is given.",
11456
+ "",
11457
+ "Input shape:",
11458
+ " list_id: list_ prefix id",
11459
+ " inactive-reason: optional filter for the inactive system list",
11460
+ "",
11461
+ "API:",
11462
+ " GET /api/v1/accounts/:account_id/lists/:id/export.json (returns text/csv)"
11463
+ ].join("\n")],
11464
+
11465
+ ["lists routing-rules toggle", [
11466
+ "Usage:",
11467
+ ` ${LIST_ROUTING_RULES_TOGGLE_USAGE.slice("Usage: ".length)}`,
11468
+ "",
11469
+ "Status: implemented",
11470
+ "",
11471
+ "Purpose:",
11472
+ " Turn one routing rule on or off.",
11473
+ "",
11474
+ "Input shape:",
11475
+ " rule_id: routing rule id on the list",
11476
+ "",
11477
+ "API:",
11478
+ " PATCH /api/v1/accounts/:account_id/lists/:list_id/routing_rules/:id/toggle.json"
11479
+ ].join("\n")],
11480
+
11481
+ ["icps bulk-add-tag", [
11482
+ "Usage:",
11483
+ ` ${ICPS_BULK_ADD_TAG_USAGE.slice("Usage: ".length)}`,
11484
+ "",
11485
+ "Status: implemented",
11486
+ "",
11487
+ "Purpose:",
11488
+ " Add one normalized tag to several ICPs at once. All ICPs change or none do.",
11489
+ "",
11490
+ "Input shape:",
11491
+ " icp_id: icpp_ prefix id or integer id; every id must belong to the account",
11492
+ " tag: string",
11493
+ "",
11494
+ "API:",
11495
+ " POST /api/v1/accounts/:account_id/icps/bulk_add_tag.json"
11496
+ ].join("\n")],
11497
+
11498
+ ["icps clone", [
11499
+ "Usage:",
11500
+ ` ${ICPS_CLONE_USAGE.slice("Usage: ".length)}`,
11501
+ "",
11502
+ "Status: implemented",
11503
+ "",
11504
+ "Purpose:",
11505
+ " Copy an ICP and its audience rules under a new \"(Copy)\" name.",
11506
+ "",
11507
+ "Input shape:",
11508
+ " icp_id: icpp_ prefix id or integer id",
11509
+ "",
11510
+ "API:",
11511
+ " POST /api/v1/accounts/:account_id/icps/:id/clone.json"
11512
+ ].join("\n")],
11513
+
11514
+ ["icps delete", [
11515
+ "Usage:",
11516
+ ` ${ICPS_DELETE_USAGE.slice("Usage: ".length)}`,
11517
+ "",
11518
+ "Status: implemented",
11519
+ "",
11520
+ "Purpose:",
11521
+ " Delete an ICP. ICPs still used by motions or agents are refused; archive them instead.",
11522
+ "",
11523
+ "Input shape:",
11524
+ " icp_id: icpp_ prefix id or integer id",
11525
+ " confirm: one of yes, true, Y, y",
11526
+ "",
11527
+ "API:",
11528
+ " DELETE /api/v1/accounts/:account_id/icps/:id.json"
11529
+ ].join("\n")],
11530
+
11531
+ ["icps prospects", [
11532
+ "Usage:",
11533
+ ` ${ICPS_PROSPECTS_USAGE.slice("Usage: ".length)}`,
11534
+ "",
11535
+ "Status: implemented",
11536
+ "",
11537
+ "Purpose:",
11538
+ " List prospects matched to an ICP, newest first, optionally filtered by a text query.",
11539
+ "",
11540
+ "Input shape:",
11541
+ " icp_id: icpp_ prefix id or integer id",
11542
+ " limit: 1-100 (default 25)",
11543
+ "",
11544
+ "API:",
11545
+ " GET /api/v1/accounts/:account_id/icps/:id/prospects.json"
11546
+ ].join("\n")],
11547
+
11548
+ ["offers regenerate-research", [
11549
+ "Usage:",
11550
+ ` ${OFFERS_REGENERATE_RESEARCH_USAGE.slice("Usage: ".length)}`,
11551
+ "",
11552
+ "Status: implemented",
11553
+ "",
11554
+ "Purpose:",
11555
+ " Queue a new research write-up for an offer, optionally steered by guidance. Returns once queued.",
11556
+ "",
11557
+ "Input shape:",
11558
+ " offr_id: offr_ prefixed id or integer id",
11559
+ " guidance: optional text",
11560
+ "",
11561
+ "API:",
11562
+ " POST /api/v1/accounts/:account_id/offers/:id/regenerate_research.json"
11563
+ ].join("\n")],
11564
+
11565
+ ["offers update-writeup", [
11566
+ "Usage:",
11567
+ ` ${OFFERS_UPDATE_WRITEUP_USAGE.slice("Usage: ".length)}`,
11568
+ "",
11569
+ "Status: implemented",
11570
+ "",
11571
+ "Purpose:",
11572
+ " Replace an offer's write-up without queueing new research.",
11573
+ "",
11574
+ "Input shape:",
11575
+ " offr_id: offr_ prefixed id or integer id",
11576
+ " description: text",
11577
+ "",
11578
+ "API:",
11579
+ " PATCH /api/v1/accounts/:account_id/offers/:id/writeup.json"
11580
+ ].join("\n")],
11581
+
11582
+ ["offers add-artifacts", [
11583
+ "Usage:",
11584
+ ` ${OFFERS_ADD_ARTIFACTS_USAGE.slice("Usage: ".length)}`,
11585
+ "",
11586
+ "Status: implemented",
11587
+ "",
11588
+ "Purpose:",
11589
+ " Attach local files to an offer as artifacts. `audienti offers show` lists attached artifacts.",
11590
+ "",
11591
+ "Input shape:",
11592
+ " offr_id: offr_ prefixed id or integer id",
11593
+ " file: local file path; sent as base64",
11594
+ "",
11595
+ "API:",
11596
+ " POST /api/v1/accounts/:account_id/offers/:offer_id/artifacts.json"
11597
+ ].join("\n")],
11598
+
11599
+ ["offers remove-artifact", [
11600
+ "Usage:",
11601
+ ` ${OFFERS_REMOVE_ARTIFACT_USAGE.slice("Usage: ".length)}`,
11602
+ "",
11603
+ "Status: implemented",
11604
+ "",
11605
+ "Purpose:",
11606
+ " Remove one artifact from an offer.",
11607
+ "",
11608
+ "Input shape:",
11609
+ " artifact_id: id from `audienti offers show <offr_id>`",
11610
+ "",
11611
+ "API:",
11612
+ " DELETE /api/v1/accounts/:account_id/offers/:offer_id/artifacts/:id.json"
11613
+ ].join("\n")],
11614
+
11615
+ ["tasks update", [
11616
+ "Usage:",
11617
+ ` ${TASKS_UPDATE_USAGE.slice("Usage: ".length)}`,
11618
+ "",
11619
+ "Status: implemented",
11620
+ "",
11621
+ "Purpose:",
11622
+ " Edit any task in the account. Only the options you pass change; pass an empty --prospect or --list to clear it.",
11623
+ "",
11624
+ "Input shape:",
11625
+ " task_id: ptsk_ prefix id or numeric id",
11626
+ " assigned-user: account user id or me",
11627
+ "",
11628
+ "API:",
11629
+ " PATCH /api/v1/accounts/:account_id/tasks/:id.json"
11630
+ ].join("\n")],
11631
+
11632
+ ["tasks bulk-update", [
11633
+ "Usage:",
11634
+ ` ${TASKS_BULK_UPDATE_USAGE.slice("Usage: ".length)}`,
11635
+ "",
11636
+ "Status: implemented",
11637
+ "",
11638
+ "Purpose:",
11639
+ " Complete several tasks now, or queue their reassignment to one account user.",
11640
+ "",
11641
+ "Input shape:",
11642
+ " task_id: ptsk_ prefix ids; every id must belong to the account",
11643
+ " action: complete or reassign (reassign needs --assigned-user)",
11644
+ "",
11645
+ "API:",
11646
+ " PATCH /api/v1/accounts/:account_id/tasks/bulk_update.json"
11647
+ ].join("\n")],
11648
+
11649
+ ["tools linkedin-strategy-review delete", [
11650
+ "Usage:",
11651
+ ` ${TOOLS_LINKEDIN_STRATEGY_REVIEW_DELETE_USAGE.slice("Usage: ".length)}`,
11652
+ "",
11653
+ "Status: implemented",
11654
+ "",
11655
+ "Purpose:",
11656
+ " Delete one LinkedIn strategy review report.",
11657
+ "",
11658
+ "Input shape:",
11659
+ " rprt_id: rprt_ report id of a strategy review",
11660
+ " confirm: one of yes, true, Y, y",
11661
+ "",
11662
+ "API:",
11663
+ " DELETE /api/v1/accounts/:account_id/tools/linkedin-strategy-review/reports/:id.json"
11664
+ ].join("\n")],
11665
+
10579
11666
  ["lists add-tag", [
10580
11667
  "Usage:",
10581
11668
  ` ${LISTS_ADD_TAG_USAGE.slice("Usage: ".length)}`,
@@ -12037,6 +13124,7 @@ const HELP_TOPICS = new Map([
12037
13124
  " audienti tools linkedin-review reports [--limit <n>] [--json]",
12038
13125
  " audienti tools linkedin-review show <rprt_id> [--json]",
12039
13126
  " audienti tools linkedin-review status <rprt_id> [--json]",
13127
+ " audienti tools linkedin-strategy-review delete <rprt_id> --confirm <yes|true|Y|y> [--json]",
12040
13128
  "",
12041
13129
  "Status: implemented",
12042
13130
  "",
@@ -12047,7 +13135,8 @@ const HELP_TOPICS = new Map([
12047
13135
  " audienti tools linkedin-review Queue a LinkedIn personal profile authority review and ICP-fit positioning blueprint.",
12048
13136
  " audienti tools linkedin-review reports List recent LinkedIn Review reports for the active account.",
12049
13137
  " audienti tools linkedin-review show View the completed report content in the terminal.",
12050
- " audienti tools linkedin-review status Show the current report stage and run status."
13138
+ " audienti tools linkedin-review status Show the current report stage and run status.",
13139
+ " audienti tools linkedin-strategy-review delete Delete one LinkedIn strategy review report."
12051
13140
  ].join("\n")],
12052
13141
 
12053
13142
  ["tools list", [