@audienti/cli 0.1.11 → 0.1.15

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
@@ -17,6 +17,8 @@ const DEFAULT_LIST_LIMIT = 20;
17
17
  const API_MAX_LIST_LIMIT = 100;
18
18
  const DEFAULT_LOOKUP_TIMEOUT_SECONDS = 60;
19
19
  const DEFAULT_LOOKUP_POLL_INTERVAL_SECONDS = 2;
20
+ const PACKAGE_NAME = "@audienti/cli";
21
+ const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
20
22
  const DEFAULT_PROFILE_IDENTIFIERS = [
21
23
  "linkedin/profile",
22
24
  "linkedin/company",
@@ -31,17 +33,34 @@ const PROSPECTS_ADD_STEER_USAGE = "Usage: audienti prospects add-steer <prsp_id>
31
33
  const PROSPECTS_ADD_PROFILE_USAGE = "Usage: audienti prospects add-profile <prsp_id> --url <profile_url|email|phone> [--json] [--account <acct_id>]";
32
34
  const PROSPECTS_REPORT_BAD_PROFILE_USAGE = "Usage: audienti prospects report-bad-profile <prsp_id> <prof_id|citation_id> [--json] [--account <acct_id>]";
33
35
  const PROSPECTS_ASSIGN_USAGE = "Usage: audienti prospects assign <prsp_id> [prsp_id...] --assigned-user <id|me|unassign> [--json] [--account <acct_id>]";
36
+ const PROSPECTS_REJECT_USAGE = "Usage: audienti prospects reject <prsp_id> [--json] [--account <acct_id>]";
37
+ const PROSPECTS_NURTURE_USAGE = "Usage: audienti prospects nurture <prsp_id> [--reason <nurture|non_responsive|not_fit>] [--json] [--account <acct_id>]";
38
+ const PROSPECTS_RESTORE_USAGE = "Usage: audienti prospects restore <prsp_id> [--json] [--account <acct_id>]";
39
+ const PROSPECTS_CHECK_USAGE = "Usage: audienti prospects check [--json|--csv] [filters] [--account <acct_id>]";
34
40
  const PROSPECTS_IMPORT_BATCH_USAGE = "Usage: audienti prospects import-batch --file <csv|jsonl|json> [--list <list_id>] [--motion <motn_id>] [--assigned-user <id|me>] [--json] [--account <acct_id>]";
35
41
  const USERS_ACTIVITY_USAGE = "Usage: audienti users activity [account_user_id|me] [--mode <actor|account_usage>] [--window <24h|7d|30d>] [--platform <linkedin|email|gmail>] [--query <text>] [--limit <n>] [--page <n>] [--json] [--account <acct_id>]";
42
+ const OFFERS_SHOW_USAGE = "Usage: audienti offers show <offr_id> [--json] [--account <acct_id>]";
43
+ const OFFERS_UPDATE_USAGE = "Usage: audienti offers update <offr_id> [--name <text>] [--description <text>] [--url <url>] [--json] [--account <acct_id>]";
44
+ const OFFERS_DELETE_USAGE = "Usage: audienti offers delete <offr_id> --confirm <yes|true|Y|y> [--json] [--account <acct_id>]";
36
45
  const WRITER_TEST_RUN_USAGE = "Usage: audienti writer test-run <prsp_id> [--json] [--mode <report|plan|step>] [--branch <both|no-accept|accepted>] [--step <step_key|row_number>] [--no-cache] [--clear-cache] [--account <acct_id>]";
37
46
  const MOTIONS_ANALYTICS_USAGE = "Usage: audienti motions analytics <motn_id> [--window 30d] [--json] [--account <acct_id>]";
38
- const MOTIONS_UPDATE_USAGE = "Usage: audienti motions update <motn_id> --status <draft|preparing|active|paused|archived> [--json] [--account <acct_id>]";
47
+ const MOTIONS_UPDATE_USAGE = "Usage: audienti motions update <motn_id> [--status <draft|preparing|active|paused|archived>] [--tags <tag[,tag...]>] [--json] [--account <acct_id>]";
48
+ const MOTIONS_ADD_TAG_USAGE = "Usage: audienti motions add-tag <motn_id> <tag> [--json] [--account <acct_id>]";
49
+ const MOTIONS_REMOVE_TAG_USAGE = "Usage: audienti motions remove-tag <motn_id> <tag> [--json] [--account <acct_id>]";
39
50
  const MOTIONS_DELETE_USAGE = "Usage: audienti motions delete <motn_id> --confirm <yes|true|Y|y> [--json] [--account <acct_id>]";
40
51
  const MOTIONS_CLONE_USAGE = "Usage: audienti motions clone <motn_id> --name <text> [--json] [--account <acct_id>]";
41
52
  const MOTIONS_MOVE_PROSPECTS_USAGE = "Usage: audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...] [--json] [--account <acct_id>]";
53
+ const MOTIONS_RUN_DISCOVERY_USAGE = "Usage: audienti motions run-discovery <motn_id> [--target-count <n>] [--json] [--account <acct_id>]";
54
+ const ICPS_SHOW_USAGE = "Usage: audienti icps show <icp_id> [--json] [--account <acct_id>]";
55
+ const ICPS_UPDATE_USAGE = "Usage: audienti icps update <icp_id> [--name <text>] [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] [--json] [--account <acct_id>]";
56
+ const ICPS_ADD_TAG_USAGE = "Usage: audienti icps add-tag <icp_id> <tag> [--json] [--account <acct_id>]";
57
+ const ICPS_REMOVE_TAG_USAGE = "Usage: audienti icps remove-tag <icp_id> <tag> [--json] [--account <acct_id>]";
58
+ const LISTS_ADD_TAG_USAGE = "Usage: audienti lists add-tag <list_id> <tag> [--json] [--account <acct_id>]";
59
+ const LISTS_REMOVE_TAG_USAGE = "Usage: audienti lists remove-tag <list_id> <tag> [--json] [--account <acct_id>]";
42
60
  const ANALYTICS_PROSPECTS_USAGE = "Usage: audienti analytics prospects [--window 24h] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
43
61
  const ANALYTICS_PROSPECTS_COHORT_ANALYSIS_USAGE = "Usage: audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
44
62
  const ANALYTICS_USERS_USAGE = "Usage: audienti analytics users [--user <account_user_id|email|name|me>] [--window 30d | --start YYYY-MM-DD --end YYYY-MM-DD] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--provenance <source>] [--platform <linkedin|email|gmail>] [--json] [--account <acct_id>]";
63
+ const ANALYTICS_DASHBOARD_USAGE = "Usage: audienti analytics dashboard [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--play-tag <tag>] [--motion <motn_id>] [--offer <offr_id>] [--icp <icp_id>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
45
64
  const COHORT_STAGE_ORDER = [
46
65
  "identified",
47
66
  "pre_connect",
@@ -120,20 +139,32 @@ async function dispatch(argv, context) {
120
139
  if (normalizedResource === "auth" && action === "status") return authStatus(rest, context, { accountOverride });
121
140
  if (normalizedResource === "auth" && action === "logout") return authLogout(rest, context);
122
141
  if (normalizedResource === "config" && action === "list") return configList(rest, context);
142
+ if (normalizedResource === "update" && action === "check") return updateCheck(rest, context);
123
143
  if (normalizedResource === "accounts" && action === "list") return accountsList(rest, context, { accountOverride });
124
144
  if (normalizedResource === "accounts" && action === "select") return accountsSelect(rest, context);
125
145
  if (normalizedResource === "users" && action === "list") return usersList(rest, context, { accountOverride });
126
146
  if (normalizedResource === "users" && action === "select") return usersSelect(rest, context, { accountOverride });
127
147
  if (normalizedResource === "users" && action === "activity") return usersActivity(rest, context, { accountOverride });
128
148
  if (normalizedResource === "offers" && action === "list") return offersList(rest, context, { accountOverride });
149
+ if (normalizedResource === "offers" && action === "show") return offersShow(rest, context, { accountOverride });
129
150
  if (normalizedResource === "offers" && action === "create") return offersCreate(rest, context, { accountOverride });
151
+ if (normalizedResource === "offers" && action === "update") return offersUpdate(rest, context, { accountOverride });
152
+ if (normalizedResource === "offers" && action === "delete") return offersDelete(rest, context, { accountOverride });
130
153
  if (normalizedResource === "icps" && action === "list") return icpsList(rest, context, { accountOverride });
154
+ if (normalizedResource === "icps" && action === "show") return icpsShow(rest, context, { accountOverride });
131
155
  if (normalizedResource === "icps" && action === "create") return icpsCreate(rest, context, { accountOverride });
156
+ if (normalizedResource === "icps" && action === "update") return icpsUpdate(rest, context, { accountOverride });
157
+ if (normalizedResource === "icps" && action === "add-tag") return icpsTagMutation("add", rest, context, { accountOverride });
158
+ if (normalizedResource === "icps" && action === "remove-tag") return icpsTagMutation("remove", rest, context, { accountOverride });
132
159
  if (normalizedResource === "companies" && action === "search") return companiesSearch(rest, context, { accountOverride });
160
+ if (normalizedResource === "tags" && action === "list") return tagsList(rest, context, { accountOverride });
161
+ if (normalizedResource === "tags" && action === "show") return tagsShow(rest, context, { accountOverride });
133
162
  if (normalizedResource === "lists" && action === "list") return listsList(rest, context, { accountOverride });
134
163
  if (normalizedResource === "lists" && action === "create") return listsCreate(rest, context, { accountOverride });
135
164
  if (normalizedResource === "lists" && action === "show") return listsShow(rest, context, { accountOverride });
136
165
  if (normalizedResource === "lists" && action === "update") return listsUpdate(rest, context, { accountOverride });
166
+ if (normalizedResource === "lists" && action === "add-tag") return listsTagMutation("add", rest, context, { accountOverride });
167
+ if (normalizedResource === "lists" && action === "remove-tag") return listsTagMutation("remove", rest, context, { accountOverride });
137
168
  if (normalizedResource === "lists" && action === "delete") return listsDelete(rest, context, { accountOverride });
138
169
  if (normalizedResource === "lists" && action === "prospects") return listProspects(rest, context, { accountOverride });
139
170
  if (normalizedResource === "lists" && action === "add-prospects") return listsAddProspects(rest, context, { accountOverride });
@@ -146,13 +177,20 @@ async function dispatch(argv, context) {
146
177
  if (normalizedResource === "motions" && action === "add-prospects") return motionsAddProspects(rest, context, { accountOverride });
147
178
  if (normalizedResource === "motions" && action === "create") return motionsCreate(rest, context, { accountOverride });
148
179
  if (normalizedResource === "motions" && action === "update") return motionsUpdate(rest, context, { accountOverride });
180
+ if (normalizedResource === "motions" && action === "add-tag") return motionsTagMutation("add", rest, context, { accountOverride });
181
+ if (normalizedResource === "motions" && action === "remove-tag") return motionsTagMutation("remove", rest, context, { accountOverride });
149
182
  if (normalizedResource === "motions" && ["activate", "pause", "archive"].includes(action)) return motionsStatusShortcut(action, rest, context, { accountOverride });
150
183
  if (normalizedResource === "motions" && action === "delete") return motionsDelete(rest, context, { accountOverride });
151
184
  if (normalizedResource === "motions" && action === "clone") return motionsClone(rest, context, { accountOverride });
152
185
  if (normalizedResource === "motions" && action === "move-prospects") return motionsMoveProspects(rest, context, { accountOverride });
186
+ if (normalizedResource === "motions" && action === "run-discovery") return motionsRunDiscovery(rest, context, { accountOverride });
153
187
  if (normalizedResource === "prospects" && action === "list") return prospectsList(rest, context, { accountOverride });
188
+ if (normalizedResource === "prospects" && action === "check") return prospectsCheck(rest, context, { accountOverride });
154
189
  if (normalizedResource === "prospects" && action === "show") return prospectsShow(rest, context, { accountOverride });
155
190
  if (normalizedResource === "prospects" && action === "assign") return prospectsAssign(rest, context, { accountOverride });
191
+ if (normalizedResource === "prospects" && action === "reject") return prospectsDisposition("reject", rest, context, { accountOverride });
192
+ if (normalizedResource === "prospects" && action === "nurture") return prospectsDisposition("nurture", rest, context, { accountOverride });
193
+ if (normalizedResource === "prospects" && action === "restore") return prospectsDisposition("restore", rest, context, { accountOverride });
156
194
  if (normalizedResource === "prospects" && action === "timeline") return prospectsTimeline(rest, context, { accountOverride });
157
195
  if (normalizedResource === "prospects" && action === "message-types") return prospectsMessageTypes(rest, context, { accountOverride });
158
196
  if (normalizedResource === "prospects" && action === "write") return prospectsWrite(rest, context, { accountOverride });
@@ -174,6 +212,7 @@ async function dispatch(argv, context) {
174
212
  if (normalizedResource === "analytics" && ["users", "user"].includes(action)) return analyticsUsers(rest, context, { accountOverride });
175
213
  if (normalizedResource === "analytics" && ["visibility", "visops"].includes(action)) return analyticsVisibility(rest, context, { accountOverride });
176
214
  if (normalizedResource === "analytics" && action === "content") return analyticsContent(rest, context, { accountOverride });
215
+ if (normalizedResource === "analytics" && ["dashboard", "campaign", "campaigns"].includes(action)) return analyticsDashboard(rest, context, { accountOverride });
177
216
 
178
217
  throw new CommandError(usage(), { exitCode: resource ? 1 : 0 });
179
218
  }
@@ -332,6 +371,51 @@ async function configList(args, context) {
332
371
  }
333
372
  }
334
373
 
374
+ async function updateCheck(args, context) {
375
+ const { values, positionals } = parseCommandArgs(args, {
376
+ ...jsonOptions(),
377
+ registry: { type: "string" }
378
+ });
379
+ if (positionals.length > 0) throw new CommandError("Usage: audienti update check [--json] [--registry <url>]");
380
+
381
+ const localPackage = await readLocalPackageMetadata();
382
+ let payload;
383
+
384
+ try {
385
+ const latestVersion = await fetchLatestPackageVersion({
386
+ fetchImpl: context.fetchImpl,
387
+ registry: values.registry
388
+ });
389
+ const updateAvailable = compareVersions(localPackage.version, latestVersion) < 0;
390
+ payload = updateCheckPayload({
391
+ currentVersion: localPackage.version,
392
+ latestVersion,
393
+ status: updateAvailable ? "update_available" : "current",
394
+ updateAvailable,
395
+ registry: values.registry || DEFAULT_NPM_REGISTRY,
396
+ now: context.now()
397
+ });
398
+ } catch (error) {
399
+ payload = updateCheckPayload({
400
+ currentVersion: localPackage.version,
401
+ latestVersion: null,
402
+ status: "unknown",
403
+ updateAvailable: null,
404
+ registry: values.registry || DEFAULT_NPM_REGISTRY,
405
+ error: error.message,
406
+ now: context.now()
407
+ });
408
+ }
409
+
410
+ if (values.json) {
411
+ writeJson(context.stdout, payload);
412
+ return payload.status === "unknown" ? 1 : 0;
413
+ }
414
+
415
+ renderUpdateCheck(payload, context);
416
+ return payload.status === "unknown" ? 1 : 0;
417
+ }
418
+
335
419
  async function accountsList(args, context, { accountOverride } = {}) {
336
420
  const { values, positionals } = parseCommandArgs(args, {
337
421
  json: { type: "boolean" }
@@ -452,11 +536,14 @@ function accountUserLabel(accountUser) {
452
536
  }
453
537
 
454
538
  async function listsList(args, context, { accountOverride } = {}) {
455
- const { values, positionals } = parseCommandArgs(args, jsonOptions());
456
- if (positionals.length > 0) throw new CommandError("Usage: audienti lists list [--json] [--account <acct_id>]");
539
+ const { values, positionals } = parseCommandArgs(args, {
540
+ ...jsonOptions(),
541
+ tag: { type: "string" }
542
+ });
543
+ if (positionals.length > 0) throw new CommandError("Usage: audienti lists list [--tag <tag>] [--json] [--account <acct_id>]");
457
544
 
458
545
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
459
- const lists = await client.lists(accountId);
546
+ const lists = filterRecordsByTag(await client.lists(accountId), values.tag, "tags");
460
547
  if (values.json) return writeJson(context.stdout, lists);
461
548
 
462
549
  renderLists(lists, context);
@@ -533,6 +620,17 @@ async function offersList(args, context, { accountOverride } = {}) {
533
620
  renderOffers(offers, context);
534
621
  }
535
622
 
623
+ async function offersShow(args, context, { accountOverride } = {}) {
624
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
625
+ if (positionals.length !== 1) throw new CommandError(OFFERS_SHOW_USAGE);
626
+
627
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
628
+ const offer = await client.offer(accountId, positionals[0]);
629
+ if (values.json) return writeJson(context.stdout, offer);
630
+
631
+ renderOffer(offer, context);
632
+ }
633
+
536
634
  async function offersCreate(args, context, { accountOverride } = {}) {
537
635
  const { values, positionals } = parseCommandArgs(args, {
538
636
  ...jsonOptions(),
@@ -559,27 +657,85 @@ async function offersCreate(args, context, { accountOverride } = {}) {
559
657
  if (offer?.url) writeLine(context.stdout, `URL: ${offer.url}`);
560
658
  }
561
659
 
660
+ async function offersUpdate(args, context, { accountOverride } = {}) {
661
+ const { values, positionals } = parseCommandArgs(args, {
662
+ ...jsonOptions(),
663
+ name: { type: "string" },
664
+ description: { type: "string" },
665
+ url: { type: "string" }
666
+ });
667
+ const hasUpdateField = values.name || values.description !== undefined || values.url !== undefined;
668
+ if (positionals.length !== 1 || !hasUpdateField) {
669
+ throw new CommandError(OFFERS_UPDATE_USAGE);
670
+ }
671
+
672
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
673
+ const offer = await client.updateOffer(accountId, positionals[0], {
674
+ offer: compactObject({
675
+ name: values.name,
676
+ description: values.description,
677
+ url: values.url
678
+ })
679
+ });
680
+ if (values.json) return writeJson(context.stdout, offer);
681
+
682
+ writeLine(context.stdout, `Updated offer ${display(offer?.name)} (${display(offer?.prefix_id)}).`);
683
+ renderOffer(offer, context);
684
+ }
685
+
686
+ async function offersDelete(args, context, { accountOverride } = {}) {
687
+ const { values, positionals } = parseCommandArgs(args, {
688
+ ...jsonOptions(),
689
+ confirm: { type: "string" }
690
+ });
691
+ const normalizedConfirm = String(values.confirm || "").trim().toLowerCase();
692
+ if (positionals.length !== 1 || !DELETE_CONFIRMATION_VALUES.has(normalizedConfirm)) {
693
+ throw new CommandError(OFFERS_DELETE_USAGE);
694
+ }
695
+
696
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
697
+ const payload = await client.deleteOffer(accountId, positionals[0]);
698
+ if (values.json) return writeJson(context.stdout, payload);
699
+
700
+ writeLine(context.stdout, `Deleted offer ${display(payload?.name)} (${display(payload?.prefix_id)}).`);
701
+ }
702
+
562
703
  async function icpsList(args, context, { accountOverride } = {}) {
563
- const { values, positionals } = parseCommandArgs(args, jsonOptions());
564
- if (positionals.length > 0) throw new CommandError("Usage: audienti icps list [--json] [--account <acct_id>]");
704
+ const { values, positionals } = parseCommandArgs(args, {
705
+ ...jsonOptions(),
706
+ tag: { type: "string" }
707
+ });
708
+ if (positionals.length > 0) throw new CommandError("Usage: audienti icps list [--tag <tag>] [--json] [--account <acct_id>]");
565
709
 
566
710
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
567
- const icps = await client.icps(accountId);
711
+ const icps = filterRecordsByTag(await client.icps(accountId), values.tag, "tags");
568
712
  if (values.json) return writeJson(context.stdout, icps);
569
713
 
570
714
  renderIcps(icps, context);
571
715
  }
572
716
 
717
+ async function icpsShow(args, context, { accountOverride } = {}) {
718
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
719
+ if (positionals.length !== 1) throw new CommandError(ICPS_SHOW_USAGE);
720
+
721
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
722
+ const icp = await client.icp(accountId, positionals[0]);
723
+ if (values.json) return writeJson(context.stdout, icp);
724
+
725
+ renderIcp(icp, context);
726
+ }
727
+
573
728
  async function icpsCreate(args, context, { accountOverride } = {}) {
574
729
  const { values, positionals } = parseCommandArgs(args, {
575
730
  ...jsonOptions(),
576
731
  payload: { type: "string" },
577
732
  name: { type: "string" },
578
733
  notes: { type: "string" },
734
+ tags: { type: "string" },
579
735
  "discovery-keyword": { type: "string" }
580
736
  });
581
737
  if (positionals.length > 0) {
582
- throw new CommandError("Usage: audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] | --payload <file.json>) [--json] [--account <acct_id>]");
738
+ throw new CommandError("Usage: audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] | --payload <file.json>) [--json] [--account <acct_id>]");
583
739
  }
584
740
 
585
741
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
@@ -589,9 +745,58 @@ async function icpsCreate(args, context, { accountOverride } = {}) {
589
745
 
590
746
  writeLine(context.stdout, `Created ICP ${display(icp?.name)} (${display(icp?.prefix_id)}).`);
591
747
  if (icp?.notes) writeLine(context.stdout, `Notes: ${icp.notes}`);
748
+ if (Array.isArray(icp?.tags)) writeLine(context.stdout, `Tags: ${display(icp.tags.join(", "), "-")}`);
592
749
  if (icp?.discovery_keyword) writeLine(context.stdout, `Discovery keyword: ${icp.discovery_keyword}`);
593
750
  }
594
751
 
752
+ async function icpsUpdate(args, context, { accountOverride } = {}) {
753
+ const { values, positionals } = parseCommandArgs(args, {
754
+ ...jsonOptions(),
755
+ name: { type: "string" },
756
+ notes: { type: "string" },
757
+ tags: { type: "string" },
758
+ "discovery-keyword": { type: "string" }
759
+ });
760
+ const hasUpdateField = values.name || values.notes !== undefined || values.tags !== undefined || values["discovery-keyword"] !== undefined;
761
+ if (positionals.length !== 1 || !hasUpdateField) {
762
+ throw new CommandError(ICPS_UPDATE_USAGE);
763
+ }
764
+
765
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
766
+ const icp = await client.updateIcp(accountId, positionals[0], {
767
+ icp: compactObject({
768
+ name: values.name,
769
+ notes: values.notes,
770
+ discovery_keyword: values["discovery-keyword"],
771
+ tags: values.tags !== undefined ? tagList(values.tags) : undefined
772
+ })
773
+ });
774
+ if (values.json) return writeJson(context.stdout, icp);
775
+
776
+ writeLine(context.stdout, `Updated ICP ${display(icp?.name)} (${display(icp?.prefix_id)}).`);
777
+ renderIcp(icp, context);
778
+ }
779
+
780
+ async function icpsTagMutation(action, args, context, { accountOverride } = {}) {
781
+ const usageText = action === "add" ? ICPS_ADD_TAG_USAGE : ICPS_REMOVE_TAG_USAGE;
782
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
783
+ if (positionals.length !== 2) {
784
+ throw new CommandError(usageText);
785
+ }
786
+
787
+ const [icpId, tag] = positionals;
788
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
789
+ const icp = action === "add" ?
790
+ await client.addIcpTag(accountId, icpId, { tag }) :
791
+ await client.removeIcpTag(accountId, icpId, { tag });
792
+ if (values.json) return writeJson(context.stdout, icp);
793
+
794
+ const verb = action === "add" ? "Added" : "Removed";
795
+ const preposition = action === "add" ? "to" : "from";
796
+ writeLine(context.stdout, `${verb} tag ${display(tag)} ${preposition} ICP ${display(icp?.name)} (${display(icp?.prefix_id)}).`);
797
+ if (Array.isArray(icp?.tags)) writeLine(context.stdout, `Tags: ${display(icp.tags.join(", "), "-")}`);
798
+ }
799
+
595
800
  async function companiesSearch(args, context, { accountOverride } = {}) {
596
801
  const { values, positionals } = parseCommandArgs(args, {
597
802
  ...jsonOptions(),
@@ -608,16 +813,47 @@ async function companiesSearch(args, context, { accountOverride } = {}) {
608
813
  renderCompanies(payload, context);
609
814
  }
610
815
 
816
+ async function tagsList(args, context, { accountOverride } = {}) {
817
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
818
+ if (positionals.length > 0) throw new CommandError("Usage: audienti tags list [--json] [--account <acct_id>]");
819
+
820
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
821
+ const payload = await client.tags(accountId);
822
+ if (values.json) return writeJson(context.stdout, payload);
823
+
824
+ renderTags(payload, context);
825
+ }
826
+
827
+ async function tagsShow(args, context, { accountOverride } = {}) {
828
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
829
+ if (positionals.length !== 1) throw new CommandError("Usage: audienti tags show <tag> [--json] [--account <acct_id>]");
830
+
831
+ const tag = tagList(positionals[0])[0];
832
+ if (!tag) throw new CommandError("Usage: audienti tags show <tag> [--json] [--account <acct_id>]");
833
+
834
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
835
+ const payload = {
836
+ tag,
837
+ icps: filterRecordsByTag(await client.icps(accountId), tag, "tags"),
838
+ lists: filterRecordsByTag(await client.lists(accountId), tag, "tags"),
839
+ motions: filterRecordsByTag(await client.motions(accountId), tag, "play_tags")
840
+ };
841
+ if (values.json) return writeJson(context.stdout, payload);
842
+
843
+ renderTagDetails(payload, context);
844
+ }
845
+
611
846
  async function listsCreate(args, context, { accountOverride } = {}) {
612
847
  const { values, positionals } = parseCommandArgs(args, {
613
848
  ...jsonOptions(),
614
849
  name: { type: "string" },
615
850
  description: { type: "string" },
851
+ tags: { type: "string" },
616
852
  "campaign-hook": { type: "string" },
617
853
  "audience-note": { type: "string" }
618
854
  });
619
855
  if (positionals.length > 0 || !values.name) {
620
- throw new CommandError("Usage: audienti lists create --name <text> [--description <text>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]");
856
+ throw new CommandError("Usage: audienti lists create --name <text> [--description <text>] [--tags <tag[,tag...]>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]");
621
857
  }
622
858
 
623
859
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
@@ -629,6 +865,7 @@ async function listsCreate(args, context, { accountOverride } = {}) {
629
865
  list: compactObject({
630
866
  name: values.name,
631
867
  description: values.description,
868
+ tags: tagList(values.tags),
632
869
  campaign_brief: Object.keys(campaignBrief).length > 0 ? campaignBrief : undefined
633
870
  })
634
871
  });
@@ -654,13 +891,14 @@ async function listsUpdate(args, context, { accountOverride } = {}) {
654
891
  ...jsonOptions(),
655
892
  name: { type: "string" },
656
893
  description: { type: "string" },
894
+ tags: { type: "string" },
657
895
  "campaign-hook": { type: "string" },
658
896
  "audience-note": { type: "string" }
659
897
  });
660
898
  const hasCampaignUpdate = values["campaign-hook"] || values["audience-note"];
661
- const hasUpdateField = values.name || values.description || hasCampaignUpdate;
899
+ const hasUpdateField = values.name || values.description || values.tags !== undefined || hasCampaignUpdate;
662
900
  if (positionals.length !== 1 || !hasUpdateField) {
663
- throw new CommandError("Usage: audienti lists update <list_id> [--name <text>] [--description <text>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]");
901
+ throw new CommandError("Usage: audienti lists update <list_id> [--name <text>] [--description <text>] [--tags <tag[,tag...]>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]");
664
902
  }
665
903
 
666
904
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
@@ -672,6 +910,7 @@ async function listsUpdate(args, context, { accountOverride } = {}) {
672
910
  list: compactObject({
673
911
  name: values.name,
674
912
  description: values.description,
913
+ tags: values.tags !== undefined ? tagList(values.tags) : undefined,
675
914
  campaign_brief: Object.keys(campaignBrief).length > 0 ? campaignBrief : undefined
676
915
  })
677
916
  });
@@ -681,6 +920,26 @@ async function listsUpdate(args, context, { accountOverride } = {}) {
681
920
  if (payload?.description) writeLine(context.stdout, `Description: ${payload.description}`);
682
921
  }
683
922
 
923
+ async function listsTagMutation(action, args, context, { accountOverride } = {}) {
924
+ const usageText = action === "add" ? LISTS_ADD_TAG_USAGE : LISTS_REMOVE_TAG_USAGE;
925
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
926
+ if (positionals.length !== 2) {
927
+ throw new CommandError(usageText);
928
+ }
929
+
930
+ const [listId, tag] = positionals;
931
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
932
+ const payload = action === "add" ?
933
+ await client.addListTag(accountId, listId, { tag }) :
934
+ await client.removeListTag(accountId, listId, { tag });
935
+ if (values.json) return writeJson(context.stdout, payload);
936
+
937
+ const verb = action === "add" ? "Added" : "Removed";
938
+ const preposition = action === "add" ? "to" : "from";
939
+ writeLine(context.stdout, `${verb} tag ${display(tag)} ${preposition} list ${display(payload?.name)} (${display(payload?.prefix_id)}).`);
940
+ if (Array.isArray(payload?.tags)) writeLine(context.stdout, `Tags: ${display(payload.tags.join(", "), "-")}`);
941
+ }
942
+
684
943
  async function listsDelete(args, context, { accountOverride } = {}) {
685
944
  const { values, positionals } = parseCommandArgs(args, {
686
945
  ...jsonOptions(),
@@ -780,11 +1039,14 @@ async function listsRemoveProspects(args, context, { accountOverride } = {}) {
780
1039
  }
781
1040
 
782
1041
  async function motionsList(args, context, { accountOverride } = {}) {
783
- const { values, positionals } = parseCommandArgs(args, jsonOptions());
784
- if (positionals.length > 0) throw new CommandError("Usage: audienti motions list [--json] [--account <acct_id>]");
1042
+ const { values, positionals } = parseCommandArgs(args, {
1043
+ ...jsonOptions(),
1044
+ tag: { type: "string" }
1045
+ });
1046
+ if (positionals.length > 0) throw new CommandError("Usage: audienti motions list [--tag <tag>] [--json] [--account <acct_id>]");
785
1047
 
786
1048
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
787
- const motions = await client.motions(accountId);
1049
+ const motions = filterRecordsByTag(await client.motions(accountId), values.tag, "play_tags");
788
1050
  if (values.json) return writeJson(context.stdout, motions);
789
1051
 
790
1052
  renderMotions(motions, context);
@@ -812,6 +1074,22 @@ async function motionsStatus(args, context, { accountOverride } = {}) {
812
1074
  renderMotionStatus(status, context);
813
1075
  }
814
1076
 
1077
+ async function motionsRunDiscovery(args, context, { accountOverride } = {}) {
1078
+ const { values, positionals } = parseCommandArgs(args, {
1079
+ ...jsonOptions(),
1080
+ "target-count": { type: "string" }
1081
+ });
1082
+ if (positionals.length !== 1) throw new CommandError(MOTIONS_RUN_DISCOVERY_USAGE);
1083
+
1084
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1085
+ const payload = await client.runMotionDiscovery(accountId, positionals[0], compactObject({
1086
+ target_count: normalizeOptionalPositiveInteger(values["target-count"], "--target-count")
1087
+ }));
1088
+ if (values.json) return writeJson(context.stdout, payload);
1089
+
1090
+ renderMotionDiscoveryRun(payload, context);
1091
+ }
1092
+
815
1093
  async function motionsAnalytics(args, context, { accountOverride } = {}) {
816
1094
  const { values, positionals } = parseCommandArgs(args, {
817
1095
  ...jsonOptions(),
@@ -929,13 +1207,26 @@ async function motionsDelete(args, context, { accountOverride } = {}) {
929
1207
  async function motionsUpdate(args, context, { accountOverride } = {}) {
930
1208
  const { values, positionals } = parseCommandArgs(args, {
931
1209
  ...jsonOptions(),
932
- status: { type: "string" }
1210
+ status: { type: "string" },
1211
+ tags: { type: "string" }
933
1212
  });
934
- if (positionals.length !== 1 || !values.status) {
1213
+ const hasUpdateField = values.status || values.tags !== undefined;
1214
+ if (positionals.length !== 1 || !hasUpdateField) {
935
1215
  throw new CommandError(MOTIONS_UPDATE_USAGE);
936
1216
  }
937
1217
 
938
- return updateMotionStatus(positionals[0], values.status, context, { accountOverride, json: values.json });
1218
+ const normalizedStatus = normalizeMotionStatus(values.status);
1219
+ const motionAttributes = compactObject({
1220
+ status: normalizedStatus,
1221
+ play_tags: values.tags !== undefined ? tagList(values.tags) : undefined
1222
+ });
1223
+
1224
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1225
+ const motion = await client.updateMotion(accountId, positionals[0], { motion: motionAttributes });
1226
+ if (values.json) return writeJson(context.stdout, motion);
1227
+
1228
+ writeLine(context.stdout, `Updated motion ${display(motion?.name)} (${display(motion?.prefix_id)}).`);
1229
+ renderMotion(motion, context);
939
1230
  }
940
1231
 
941
1232
  async function motionsStatusShortcut(action, args, context, { accountOverride } = {}) {
@@ -954,10 +1245,7 @@ async function motionsStatusShortcut(action, args, context, { accountOverride }
954
1245
  }
955
1246
 
956
1247
  async function updateMotionStatus(motionId, status, context, { accountOverride, json } = {}) {
957
- const normalizedStatus = String(status || "").trim().toLowerCase();
958
- if (!MOTION_STATUS_VALUES.has(normalizedStatus)) {
959
- throw new CommandError(MOTIONS_UPDATE_USAGE);
960
- }
1248
+ const normalizedStatus = normalizeMotionStatus(status);
961
1249
 
962
1250
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
963
1251
  const motion = await client.updateMotion(accountId, motionId, {
@@ -971,6 +1259,37 @@ async function updateMotionStatus(motionId, status, context, { accountOverride,
971
1259
  renderMotion(motion, context);
972
1260
  }
973
1261
 
1262
+ function normalizeMotionStatus(status) {
1263
+ if (status === undefined) return undefined;
1264
+
1265
+ const normalizedStatus = String(status || "").trim().toLowerCase();
1266
+ if (!MOTION_STATUS_VALUES.has(normalizedStatus)) {
1267
+ throw new CommandError(MOTIONS_UPDATE_USAGE);
1268
+ }
1269
+
1270
+ return normalizedStatus;
1271
+ }
1272
+
1273
+ async function motionsTagMutation(action, args, context, { accountOverride } = {}) {
1274
+ const usageText = action === "add" ? MOTIONS_ADD_TAG_USAGE : MOTIONS_REMOVE_TAG_USAGE;
1275
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1276
+ if (positionals.length !== 2) {
1277
+ throw new CommandError(usageText);
1278
+ }
1279
+
1280
+ const [motionId, tag] = positionals;
1281
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1282
+ const motion = action === "add" ?
1283
+ await client.addMotionTag(accountId, motionId, { tag }) :
1284
+ await client.removeMotionTag(accountId, motionId, { tag });
1285
+ if (values.json) return writeJson(context.stdout, motion);
1286
+
1287
+ const verb = action === "add" ? "Added" : "Removed";
1288
+ const preposition = action === "add" ? "to" : "from";
1289
+ writeLine(context.stdout, `${verb} tag ${display(tag)} ${preposition} motion ${display(motion?.name)} (${display(motion?.prefix_id)}).`);
1290
+ if (Array.isArray(motion?.play_tags)) writeLine(context.stdout, `Tags: ${display(motion.play_tags.join(", "), "-")}`);
1291
+ }
1292
+
974
1293
  async function motionsClone(args, context, { accountOverride } = {}) {
975
1294
  const { values, positionals } = parseCommandArgs(args, {
976
1295
  ...jsonOptions(),
@@ -1018,7 +1337,47 @@ async function motionsMoveProspects(args, context, { accountOverride } = {}) {
1018
1337
  }
1019
1338
 
1020
1339
  async function prospectsList(args, context, { accountOverride } = {}) {
1021
- const { values, positionals } = parseCommandArgs(args, {
1340
+ const { values, positionals } = parseCommandArgs(args, prospectFilterOptions());
1341
+ if (positionals.length > 0) throw new CommandError("Usage: audienti prospects list [--json] [filters] [--account <acct_id>]");
1342
+ if (values.csv && values.json) throw new CommandError("Choose one output format: use either --csv or --json.");
1343
+ validateProspectFilterValues(values);
1344
+
1345
+ const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
1346
+ const query = prospectQueryFromValues(values, config, { accountOverride });
1347
+ const payload = values.all ?
1348
+ await fetchAllProspects(client, accountId, query, { totalLimit: parseProspectTotalLimit(values.limit) }) :
1349
+ await client.prospects(accountId, query);
1350
+ if (values.json) return writeJson(context.stdout, payload);
1351
+ if (values.csv) return writeLine(context.stdout, prospectsToCsv(payload?.prospects || []));
1352
+
1353
+ renderProspects(payload, context, { wide: values.wide || values.all, profiles: values.profiles });
1354
+ }
1355
+
1356
+ async function prospectsCheck(args, context, { accountOverride } = {}) {
1357
+ const { values, positionals } = parseCommandArgs(args, prospectFilterOptions());
1358
+ if (positionals.length > 0) throw new CommandError(PROSPECTS_CHECK_USAGE);
1359
+ if (values.csv && values.json) throw new CommandError("Choose one output format: use either --csv or --json.");
1360
+ if (values.company || values["company-profile"]) throw new CommandError("Company filters are not supported for `prospects check`; it already finds prospects missing a certified company.");
1361
+ validateProspectFilterValues(values);
1362
+
1363
+ const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
1364
+ const query = {
1365
+ ...prospectQueryFromValues(values, config, { accountOverride }),
1366
+ data_quality: "missing_certified_company"
1367
+ };
1368
+ const rawPayload = values.all ?
1369
+ await fetchAllProspects(client, accountId, query, { totalLimit: parseProspectTotalLimit(values.limit) }) :
1370
+ await client.prospects(accountId, query);
1371
+ const payload = withProspectAppUrls(rawPayload, client.host);
1372
+
1373
+ if (values.json) return writeJson(context.stdout, payload);
1374
+ if (values.csv) return writeLine(context.stdout, prospectCheckToCsv(payload?.prospects || []));
1375
+
1376
+ renderProspectCheck(payload, context);
1377
+ }
1378
+
1379
+ function prospectFilterOptions() {
1380
+ return {
1022
1381
  ...jsonOptions(),
1023
1382
  all: { type: "boolean" },
1024
1383
  csv: { type: "boolean" },
@@ -1035,16 +1394,18 @@ async function prospectsList(args, context, { accountOverride } = {}) {
1035
1394
  page: { type: "string" },
1036
1395
  profiles: { type: "boolean" },
1037
1396
  wide: { type: "boolean" }
1038
- });
1039
- if (positionals.length > 0) throw new CommandError("Usage: audienti prospects list [--json] [filters] [--account <acct_id>]");
1040
- if (values.csv && values.json) throw new CommandError("Choose one output format: use either --csv or --json.");
1397
+ };
1398
+ }
1399
+
1400
+ function validateProspectFilterValues(values) {
1041
1401
  if (values.page && values.offset) throw new CommandError("Choose one pagination mode: use either --page or --offset.");
1042
1402
  if (values.all && (values.page || values.offset)) throw new CommandError("--all cannot be combined with --page or --offset.");
1043
1403
  if (values.motion && values.play) throw new CommandError("Choose one motion filter: use either --motion or --play.");
1044
1404
  if (values.company && values["company-profile"]) throw new CommandError("Choose one company filter: use either --company or --company-profile.");
1405
+ }
1045
1406
 
1046
- const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
1047
- const query = compactObject({
1407
+ function prospectQueryFromValues(values, config, { accountOverride } = {}) {
1408
+ return compactObject({
1048
1409
  query: values.query,
1049
1410
  company: values.company,
1050
1411
  company_profile_id: values["company-profile"],
@@ -1058,13 +1419,6 @@ async function prospectsList(args, context, { accountOverride } = {}) {
1058
1419
  page: values.page,
1059
1420
  include_profiles: values.profiles
1060
1421
  });
1061
- const payload = values.all ?
1062
- await fetchAllProspects(client, accountId, query, { totalLimit: parseProspectTotalLimit(values.limit) }) :
1063
- await client.prospects(accountId, query);
1064
- if (values.json) return writeJson(context.stdout, payload);
1065
- if (values.csv) return writeLine(context.stdout, prospectsToCsv(payload?.prospects || []));
1066
-
1067
- renderProspects(payload, context, { wide: values.wide || values.all, profiles: values.profiles });
1068
1422
  }
1069
1423
 
1070
1424
  async function prospectsAssign(args, context, { accountOverride } = {}) {
@@ -1109,6 +1463,32 @@ async function prospectsShow(args, context, { accountOverride } = {}) {
1109
1463
  renderProspect(prospect, context);
1110
1464
  }
1111
1465
 
1466
+ async function prospectsDisposition(action, args, context, { accountOverride } = {}) {
1467
+ const usageText = {
1468
+ reject: PROSPECTS_REJECT_USAGE,
1469
+ nurture: PROSPECTS_NURTURE_USAGE,
1470
+ restore: PROSPECTS_RESTORE_USAGE
1471
+ }[action];
1472
+ const { values, positionals } = parseCommandArgs(args, {
1473
+ ...jsonOptions(),
1474
+ reason: { type: "string" }
1475
+ });
1476
+ if (positionals.length !== 1 || (action !== "nurture" && values.reason)) {
1477
+ throw new CommandError(usageText);
1478
+ }
1479
+
1480
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1481
+ const prospectId = positionals[0];
1482
+ const payload = action === "reject" ?
1483
+ await client.rejectProspect(accountId, prospectId) :
1484
+ action === "restore" ?
1485
+ await client.restoreProspect(accountId, prospectId) :
1486
+ await client.nurtureProspect(accountId, prospectId, compactObject({ inactive_reason: values.reason }));
1487
+ if (values.json) return writeJson(context.stdout, payload);
1488
+
1489
+ renderProspectDisposition(payload, context, { action });
1490
+ }
1491
+
1112
1492
  async function prospectsTimeline(args, context, { accountOverride } = {}) {
1113
1493
  const { values, positionals } = parseCommandArgs(args, {
1114
1494
  ...jsonOptions(),
@@ -1784,6 +2164,18 @@ async function analyticsContent(args, context, { accountOverride } = {}) {
1784
2164
  renderAnalyticsContent(payload, context);
1785
2165
  }
1786
2166
 
2167
+ async function analyticsDashboard(args, context, { accountOverride } = {}) {
2168
+ const { values, positionals } = parseCommandArgs(args, analyticsDashboardOptions());
2169
+ if (positionals.length > 0) throw new CommandError(ANALYTICS_DASHBOARD_USAGE);
2170
+ validateDatePair(values["cohort-start"], values["cohort-end"], "--cohort-start", "--cohort-end");
2171
+
2172
+ const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
2173
+ const payload = await client.analyticsDashboard(accountId, analyticsDashboardQuery(values, config, { accountOverride }));
2174
+ if (values.json) return writeJson(context.stdout, payload);
2175
+
2176
+ renderAnalyticsDashboard(payload, context);
2177
+ }
2178
+
1787
2179
  function parseCommandArgs(args, options) {
1788
2180
  try {
1789
2181
  return parseArgs({
@@ -1955,6 +2347,20 @@ function analyticsUsersOptions() {
1955
2347
  };
1956
2348
  }
1957
2349
 
2350
+ function analyticsDashboardOptions() {
2351
+ return {
2352
+ ...jsonOptions(),
2353
+ "cohort-start": { type: "string" },
2354
+ "cohort-end": { type: "string" },
2355
+ "play-tag": { type: "string" },
2356
+ tag: { type: "string" },
2357
+ motion: { type: "string" },
2358
+ offer: { type: "string" },
2359
+ icp: { type: "string" },
2360
+ user: { type: "string" }
2361
+ };
2362
+ }
2363
+
1958
2364
  function analyticsQuery(values, config = {}, { accountOverride } = {}) {
1959
2365
  return compactObject({
1960
2366
  window: values.window,
@@ -1981,6 +2387,18 @@ function analyticsUsersQuery(values, config = {}, { accountOverride } = {}) {
1981
2387
  });
1982
2388
  }
1983
2389
 
2390
+ function analyticsDashboardQuery(values, config = {}, { accountOverride } = {}) {
2391
+ return compactObject({
2392
+ cohort_start_date: values["cohort-start"],
2393
+ cohort_end_date: values["cohort-end"],
2394
+ play_tag: values["play-tag"] || values.tag,
2395
+ motion_id: values.motion,
2396
+ offer_id: values.offer,
2397
+ icp_id: values.icp,
2398
+ account_user_id: resolveAccountUserId(values.user, config, { accountOverride })
2399
+ });
2400
+ }
2401
+
1984
2402
  function validateDatePair(start, end, startFlag, endFlag) {
1985
2403
  if ((start && !end) || (!start && end)) {
1986
2404
  throw new CommandError(`${startFlag} and ${endFlag} must be provided together.`);
@@ -2086,26 +2504,125 @@ function provenancePayload(provenance) {
2086
2504
 
2087
2505
  function compactObject(object) {
2088
2506
  return Object.fromEntries(
2089
- Object.entries(object).filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== "")
2507
+ Object.entries(object).filter(([, value]) => {
2508
+ if (value === undefined || value === null) return false;
2509
+ if (Array.isArray(value)) return true;
2510
+
2511
+ return String(value).trim() !== "";
2512
+ })
2090
2513
  );
2091
2514
  }
2092
2515
 
2093
- async function icpCreatePayload(values) {
2094
- if (values.payload) {
2095
- if (values.name || values.notes || values["discovery-keyword"]) {
2096
- throw new CommandError("Choose one ICP input mode: either --payload <file.json> or the simple --name/--notes/--discovery-keyword flags.");
2516
+ async function readLocalPackageMetadata() {
2517
+ const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
2518
+ return {
2519
+ name: packageJson.name || PACKAGE_NAME,
2520
+ version: packageJson.version
2521
+ };
2522
+ }
2523
+
2524
+ async function fetchLatestPackageVersion({ fetchImpl, registry }) {
2525
+ if (!fetchImpl) throw new Error("This Node runtime does not provide fetch.");
2526
+
2527
+ const response = await fetchImpl(registryLatestPackageUrl(registry), {
2528
+ headers: {
2529
+ accept: "application/vnd.npm.install-v1+json, application/json"
2097
2530
  }
2531
+ });
2532
+ const bodyText = await response.text();
2533
+ const body = bodyText ? JSON.parse(bodyText) : {};
2098
2534
 
2099
- return readJsonPayload(values.payload);
2535
+ if (!response.ok) {
2536
+ throw new Error(body?.error || body?.message || `Registry returned HTTP ${response.status}.`);
2100
2537
  }
2101
2538
 
2102
- if (!values.name) {
2103
- throw new CommandError("Usage: audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] | --payload <file.json>) [--json] [--account <acct_id>]");
2539
+ const version = body?.version?.toString().trim();
2540
+ if (!version) throw new Error("Registry response did not include a version.");
2541
+
2542
+ return version;
2543
+ }
2544
+
2545
+ function registryLatestPackageUrl(registry) {
2546
+ const base = new URL(registry || DEFAULT_NPM_REGISTRY);
2547
+ if (!base.pathname.endsWith("/")) base.pathname = `${base.pathname}/`;
2548
+
2549
+ return new URL(`${encodeURIComponent(PACKAGE_NAME)}/latest`, base).toString();
2550
+ }
2551
+
2552
+ function updateCheckPayload({ currentVersion, latestVersion, status, updateAvailable, registry, error, now }) {
2553
+ return {
2554
+ kind: "update_check",
2555
+ package_name: PACKAGE_NAME,
2556
+ current_version: currentVersion,
2557
+ latest_version: latestVersion,
2558
+ update_available: updateAvailable,
2559
+ status,
2560
+ install_command: `npm install --global ${PACKAGE_NAME}`,
2561
+ registry,
2562
+ checked_at: now.toISOString(),
2563
+ error: error || null
2564
+ };
2565
+ }
2566
+
2567
+ function compareVersions(currentVersion, latestVersion) {
2568
+ const current = parseVersion(currentVersion);
2569
+ const latest = parseVersion(latestVersion);
2570
+
2571
+ for (let index = 0; index < 3; index += 1) {
2572
+ if (current.parts[index] !== latest.parts[index]) return current.parts[index] - latest.parts[index];
2573
+ }
2574
+
2575
+ if (current.prerelease === latest.prerelease) return 0;
2576
+ if (!current.prerelease) return 1;
2577
+ if (!latest.prerelease) return -1;
2578
+
2579
+ return current.prerelease.localeCompare(latest.prerelease);
2580
+ }
2581
+
2582
+ function parseVersion(version) {
2583
+ const [core, prerelease = ""] = String(version || "").split("-", 2);
2584
+ const parts = core.split(".").map((part) => Number.parseInt(part, 10));
2585
+
2586
+ return {
2587
+ parts: [parts[0] || 0, parts[1] || 0, parts[2] || 0],
2588
+ prerelease
2589
+ };
2590
+ }
2591
+
2592
+ function tagList(value) {
2593
+ if (value === undefined) return undefined;
2594
+
2595
+ return String(value)
2596
+ .split(/[\r\n,;]+/)
2597
+ .map((tag) => tag.trim().toLowerCase())
2598
+ .filter(Boolean)
2599
+ .filter((tag, index, tags) => tags.indexOf(tag) === index);
2600
+ }
2601
+
2602
+ function filterRecordsByTag(records, value, field) {
2603
+ const tag = tagList(value)?.[0];
2604
+ if (!tag) return records;
2605
+
2606
+ return Array.isArray(records) ? records.filter((record) => (tagList(record?.[field]) || []).includes(tag)) : [];
2607
+ }
2608
+
2609
+ async function icpCreatePayload(values) {
2610
+ if (values.payload) {
2611
+ if (values.name || values.notes || values.tags !== undefined || values["discovery-keyword"]) {
2612
+ throw new CommandError("Choose one ICP input mode: either --payload <file.json> or the simple --name/--notes/--discovery-keyword/tags flags.");
2613
+ }
2614
+
2615
+ return readJsonPayload(values.payload);
2616
+ }
2617
+
2618
+ if (!values.name) {
2619
+ throw new CommandError("Usage: audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] | --payload <file.json>) [--json] [--account <acct_id>]");
2104
2620
  }
2105
2621
 
2106
2622
  return compactObject({
2107
2623
  name: values.name,
2108
2624
  notes: values.notes,
2625
+ tags: values.tags !== undefined ? tagList(values.tags) : undefined,
2109
2626
  discovery_keyword: values["discovery-keyword"]
2110
2627
  });
2111
2628
  }
@@ -2246,6 +2763,17 @@ function normalizePositiveInteger(value) {
2246
2763
  return parsed;
2247
2764
  }
2248
2765
 
2766
+ function normalizeOptionalPositiveInteger(value, flagName) {
2767
+ if (value === undefined || value === null || value === "") return undefined;
2768
+
2769
+ const parsed = normalizePositiveInteger(value);
2770
+ if (parsed === null || String(parsed) !== String(value).trim()) {
2771
+ throw new CommandError(`${flagName} must be a positive integer.`);
2772
+ }
2773
+
2774
+ return parsed;
2775
+ }
2776
+
2249
2777
  function normalizeLookupType(value) {
2250
2778
  const normalized = String(value || "").trim().toLowerCase();
2251
2779
  if (normalized === "email" || normalized === "phone") return normalized;
@@ -2394,6 +2922,22 @@ function writeJson(stream, value) {
2394
2922
  writeLine(stream, JSON.stringify(value, null, 2));
2395
2923
  }
2396
2924
 
2925
+ function renderUpdateCheck(payload, context) {
2926
+ writeLine(context.stdout, `Package: ${payload.package_name}`);
2927
+ writeLine(context.stdout, `Current version: ${display(payload.current_version, "-")}`);
2928
+ writeLine(context.stdout, `Latest version: ${display(payload.latest_version, "unknown")}`);
2929
+
2930
+ if (payload.status === "update_available") {
2931
+ writeLine(context.stdout, "Status: update available");
2932
+ writeLine(context.stdout, `Update: ${payload.install_command}`);
2933
+ } else if (payload.status === "current") {
2934
+ writeLine(context.stdout, "Status: current");
2935
+ } else {
2936
+ writeLine(context.stdout, "Status: unknown");
2937
+ if (payload.error) writeLine(context.stdout, `Error: ${payload.error}`);
2938
+ }
2939
+ }
2940
+
2397
2941
  async function sleep(milliseconds) {
2398
2942
  await new Promise((resolve) => setTimeout(resolve, milliseconds));
2399
2943
  }
@@ -2410,9 +2954,48 @@ function renderLists(lists, context) {
2410
2954
  function renderList(list, context) {
2411
2955
  writeLine(context.stdout, `List: ${display(list?.name)} (${display(list?.prefix_id)})`);
2412
2956
  writeLine(context.stdout, `Prospects: ${display(list?.prospect_count, 0)}`);
2957
+ if (Array.isArray(list?.tags)) writeLine(context.stdout, `Tags: ${display(list.tags.join(", "), "-")}`);
2413
2958
  if (list?.description) writeLine(context.stdout, `Description: ${list.description}`);
2414
2959
  }
2415
2960
 
2961
+ function renderTags(tags, context) {
2962
+ if (!Array.isArray(tags) || tags.length === 0) return writeLine(context.stdout, "No tags found.");
2963
+
2964
+ writeLine(context.stdout, "TAG\tICPS\tLISTS\tMOTIONS\tTOTAL");
2965
+ for (const tag of tags) {
2966
+ writeLine(
2967
+ context.stdout,
2968
+ [
2969
+ display(tag.name),
2970
+ display(tag.icp_count, 0),
2971
+ display(tag.list_count, 0),
2972
+ display(tag.motion_count, 0),
2973
+ display(tag.total_count, 0)
2974
+ ].join("\t")
2975
+ );
2976
+ }
2977
+ }
2978
+
2979
+ function renderTagDetails(payload, context) {
2980
+ const icps = Array.isArray(payload?.icps) ? payload.icps : [];
2981
+ const lists = Array.isArray(payload?.lists) ? payload.lists : [];
2982
+ const motions = Array.isArray(payload?.motions) ? payload.motions : [];
2983
+
2984
+ writeLine(context.stdout, `Tag: ${display(payload?.tag)}`);
2985
+ writeLine(context.stdout, `ICPs: ${icps.length}`);
2986
+ writeLine(context.stdout, `Lists: ${lists.length}`);
2987
+ writeLine(context.stdout, `Motions: ${motions.length}`);
2988
+ writeLine(context.stdout);
2989
+ writeLine(context.stdout, "ICPs");
2990
+ renderIcps(icps, context);
2991
+ writeLine(context.stdout);
2992
+ writeLine(context.stdout, "Lists");
2993
+ renderLists(lists, context);
2994
+ writeLine(context.stdout);
2995
+ writeLine(context.stdout, "Motions");
2996
+ renderMotions(motions, context);
2997
+ }
2998
+
2416
2999
  function renderUsers(users, context) {
2417
3000
  if (!Array.isArray(users) || users.length === 0) return writeLine(context.stdout, "No account users found.");
2418
3001
 
@@ -2482,16 +3065,23 @@ function renderOffers(offers, context) {
2482
3065
  }
2483
3066
  }
2484
3067
 
3068
+ function renderOffer(offer, context) {
3069
+ writeLine(context.stdout, `Offer: ${display(offer?.name)} (${display(offer?.prefix_id)})`);
3070
+ if (offer?.description) writeLine(context.stdout, `Description: ${offer.description}`);
3071
+ if (offer?.url) writeLine(context.stdout, `URL: ${offer.url}`);
3072
+ }
3073
+
2485
3074
  function renderIcps(icps, context) {
2486
3075
  if (!Array.isArray(icps) || icps.length === 0) return writeLine(context.stdout, "No ICPs found.");
2487
3076
 
2488
- writeLine(context.stdout, "ICP ID\tNAME\tDISCOVERY KEYWORD\tAGENT");
3077
+ writeLine(context.stdout, "ICP ID\tNAME\tTAGS\tDISCOVERY KEYWORD\tAGENT");
2489
3078
  for (const icp of icps) {
2490
3079
  writeLine(
2491
3080
  context.stdout,
2492
3081
  [
2493
3082
  display(icp.prefix_id),
2494
3083
  display(icp.name),
3084
+ display(Array.isArray(icp.tags) && icp.tags.length > 0 ? icp.tags.join(",") : "-"),
2495
3085
  display(icp.discovery_keyword),
2496
3086
  display(icp.agent?.name)
2497
3087
  ].join("\t")
@@ -2499,6 +3089,14 @@ function renderIcps(icps, context) {
2499
3089
  }
2500
3090
  }
2501
3091
 
3092
+ function renderIcp(icp, context) {
3093
+ writeLine(context.stdout, `ICP: ${display(icp?.name)} (${display(icp?.prefix_id)})`);
3094
+ if (Array.isArray(icp?.tags)) writeLine(context.stdout, `Tags: ${display(icp.tags.join(", "), "-")}`);
3095
+ if (icp?.notes) writeLine(context.stdout, `Notes: ${icp.notes}`);
3096
+ if (icp?.discovery_keyword) writeLine(context.stdout, `Discovery keyword: ${icp.discovery_keyword}`);
3097
+ if (icp?.agent?.name) writeLine(context.stdout, `Agent: ${icp.agent.name}`);
3098
+ }
3099
+
2502
3100
  function renderCompanies(payload, context) {
2503
3101
  const companies = Array.isArray(payload?.companies) ? payload.companies : [];
2504
3102
  if (companies.length === 0) return writeLine(context.stdout, "No companies found.");
@@ -2563,6 +3161,7 @@ function renderMotion(motion, context) {
2563
3161
  if (motion?.offer?.name) writeLine(context.stdout, `Offer: ${motion.offer.name} (${display(motion.offer.prefix_id)})`);
2564
3162
  if (motion?.icp?.name) writeLine(context.stdout, `ICP: ${motion.icp.name} (${display(motion.icp.prefix_id)})`);
2565
3163
  if (motion?.list?.name) writeLine(context.stdout, `List: ${motion.list.name} (${display(motion.list.prefix_id)})`);
3164
+ if (Array.isArray(motion?.play_tags)) writeLine(context.stdout, `Tags: ${display(motion.play_tags.join(", "), "-")}`);
2566
3165
  if (motion?.principal_account_user?.id) {
2567
3166
  writeLine(
2568
3167
  context.stdout,
@@ -2630,6 +3229,27 @@ function renderProspects(payload, context, { wide = false, profiles = false } =
2630
3229
  }
2631
3230
  }
2632
3231
 
3232
+ function renderProspectCheck(payload, context) {
3233
+ const prospects = Array.isArray(payload?.prospects) ? payload.prospects : [];
3234
+ const totalCount = display(payload?.meta?.total_count, prospects.length);
3235
+ if (prospects.length === 0) return writeLine(context.stdout, "No suspect prospects found.");
3236
+
3237
+ writeLine(context.stdout, `Suspect prospects: ${totalCount}`);
3238
+ writeLine(context.stdout, "PROSPECT ID\tSTAGE\tNAME\tREPORTED COMPANY\tCERTIFIED\tREASON\tURL");
3239
+ for (const prospect of prospects) {
3240
+ const certification = prospect.company_certification || {};
3241
+ writeLine(context.stdout, [
3242
+ display(prospect.prefix_id),
3243
+ display(prospect.account_prospect?.pipeline_stage),
3244
+ display(prospect.display_name || prospect.name),
3245
+ display(certification.reported_company || prospect.company),
3246
+ certification.status === "certified" ? "yes" : "no",
3247
+ display(certification.reason, "missing_employment_citation"),
3248
+ display(prospect.app_url)
3249
+ ].join("\t"));
3250
+ }
3251
+ }
3252
+
2633
3253
  function renderProspect(prospect, context) {
2634
3254
  writeLine(context.stdout, `Prospect: ${display(prospect?.display_name || prospect?.name)} (${display(prospect?.prefix_id)})`);
2635
3255
  if (prospect?.company) writeLine(context.stdout, `Company: ${prospect.company}`);
@@ -2639,6 +3259,21 @@ function renderProspect(prospect, context) {
2639
3259
  if (nextActionLabel(prospect?.queue)) writeLine(context.stdout, `Next action: ${nextActionLabel(prospect.queue)}`);
2640
3260
  }
2641
3261
 
3262
+ function renderProspectDisposition(payload, context, { action }) {
3263
+ const prospect = payload?.prospect || {};
3264
+ const accountProspect = payload?.account_prospect || {};
3265
+ const actionLabel = {
3266
+ reject: "Rejected",
3267
+ nurture: "Moved to nurture",
3268
+ restore: "Restored"
3269
+ }[action] || display(payload?.status, "Updated");
3270
+
3271
+ writeLine(context.stdout, `${actionLabel} prospect ${display(prospect.display_name || prospect.name)} (${display(prospect.prefix_id)}).`);
3272
+ if (accountProspect.status) writeLine(context.stdout, `Status: ${accountProspect.status}`);
3273
+ if (accountProspect.inactive_reason) writeLine(context.stdout, `Inactive reason: ${accountProspect.inactive_reason}`);
3274
+ if (payload?.system_list?.name) writeLine(context.stdout, `List: ${payload.system_list.name} (${display(payload.system_list.prefix_id)})`);
3275
+ }
3276
+
2642
3277
  function renderProspectTimeline(payload, context) {
2643
3278
  const prospect = payload?.prospect || {};
2644
3279
  const timeline = Array.isArray(payload?.timeline) ? payload.timeline : [];
@@ -3161,6 +3796,32 @@ function renderMotionAnalytics(payload, context) {
3161
3796
  writeCountTable(context, "Prospect cohorts by produced day", payload?.prospects_by_day, ["DATE", "PRODUCED", "ACTIVE", "ACTIVE %", "INACTIVE", "STAGES"], dailyProspectRow);
3162
3797
  }
3163
3798
 
3799
+ function renderMotionDiscoveryRun(payload, context) {
3800
+ const motion = payload?.motion || {};
3801
+ const label = entityLabel(motion) || payload?.motion_id;
3802
+ const prefix = payload?.enqueued ? "Discovery queued" : "Discovery not queued";
3803
+ writeLine(context.stdout, `${prefix} for ${display(label)}.`);
3804
+ writeLine(context.stdout, `Reason: ${display(payload?.reason)}`);
3805
+ writeLine(context.stdout, `Target count: ${display(payload?.target_count)}`);
3806
+ }
3807
+
3808
+ function renderAnalyticsDashboard(payload, context) {
3809
+ writeLine(context.stdout, `Dashboard analytics (${display(payload?.cohort?.label, "selected cohort")})`);
3810
+ if (payload?.cohort) {
3811
+ writeLine(context.stdout, `Cohort: ${payload.cohort.start_date} to ${payload.cohort.end_date} (${display(payload.cohort.field, "account_prospects.created_at")})`);
3812
+ }
3813
+ if (payload?.activity) {
3814
+ writeLine(context.stdout, `Activity: ${payload.activity.start_date} to ${payload.activity.end_date} (${display(payload.activity.field, "events.created_at")})`);
3815
+ }
3816
+ writeDashboardFilters(payload, context);
3817
+ writeLine(context.stdout, `Prospects: ${display(payload?.cohort_size, 0)}`);
3818
+ writeLine(context.stdout, `Companies: ${display(payload?.cohort_company_target_count, 0)}`);
3819
+ writeLine(context.stdout, `People/company: ${display(payload?.cohort_people_per_company_average, "0.0")}`);
3820
+ writeLine(context.stdout, `Active: ${display(payload?.active_cohort_count, 0)} (${display(payload?.active_cohort_company_target_count, 0)} companies, ${percentageLabel(payload?.active_cohort_percentage)})`);
3821
+ writeLine(context.stdout, `Inactive: ${display(payload?.inactive_cohort_count, 0)}`);
3822
+ writeCountTable(context, "Current pipeline stages", payload?.pipeline_stage_counts, ["STAGE", "COUNT"], countRow);
3823
+ }
3824
+
3164
3825
  function renderAnalyticsProspectCohortAnalysis(payload, context) {
3165
3826
  const cohorts = Array.isArray(payload?.cohorts) ? payload.cohorts : [];
3166
3827
  writeLine(context.stdout, `Prospect cohort analysis (${display(payload?.weeks, cohorts.length)} weeks)`);
@@ -3235,6 +3896,19 @@ function writeAnalyticsScope(payload, context) {
3235
3896
  }
3236
3897
  }
3237
3898
 
3899
+ function writeDashboardFilters(payload, context) {
3900
+ const filters = payload?.filters || {};
3901
+ if (filters.motion) writeLine(context.stdout, `Motion: ${entityLabel(filters.motion)}`);
3902
+ if (filters.play_tag) writeLine(context.stdout, `Tag: ${filters.play_tag}`);
3903
+ if (filters.offer) writeLine(context.stdout, `Offer: ${entityLabel(filters.offer)}`);
3904
+ if (filters.icp) writeLine(context.stdout, `ICP: ${entityLabel(filters.icp)}`);
3905
+ if (filters.account_user) {
3906
+ writeLine(context.stdout, `User: ${entityLabel(filters.account_user)}`);
3907
+ } else {
3908
+ writeLine(context.stdout, "User: all account users");
3909
+ }
3910
+ }
3911
+
3238
3912
  function writeAnalyticsPlatform(payload, context) {
3239
3913
  if (!payload?.platform) return;
3240
3914
 
@@ -3674,6 +4348,77 @@ function prospectsToCsv(prospects) {
3674
4348
  ].join("\n");
3675
4349
  }
3676
4350
 
4351
+ function prospectCheckToCsv(prospects) {
4352
+ const headers = [
4353
+ "prefix_id",
4354
+ "display_name",
4355
+ "name",
4356
+ "title",
4357
+ "reported_company",
4358
+ "company_certification_status",
4359
+ "company_certification_reason",
4360
+ "email",
4361
+ "linkedin_url",
4362
+ "app_url",
4363
+ "account_prospect_status",
4364
+ "pipeline_stage",
4365
+ "assigned_to_account_user_id",
4366
+ "motion_prefix_id",
4367
+ "motion_name",
4368
+ "updated_at"
4369
+ ];
4370
+
4371
+ const rows = prospects.map((prospect) => {
4372
+ const certification = prospect.company_certification || {};
4373
+ return {
4374
+ prefix_id: prospect.prefix_id,
4375
+ display_name: prospect.display_name,
4376
+ name: prospect.name,
4377
+ title: prospect.title,
4378
+ reported_company: certification.reported_company || prospect.company,
4379
+ company_certification_status: certification.status,
4380
+ company_certification_reason: certification.reason,
4381
+ email: prospect.email,
4382
+ linkedin_url: prospect.linkedin_url,
4383
+ app_url: prospect.app_url,
4384
+ account_prospect_status: prospect.account_prospect?.status,
4385
+ pipeline_stage: prospect.account_prospect?.pipeline_stage,
4386
+ assigned_to_account_user_id: prospect.account_prospect?.assigned_to_account_user_id,
4387
+ motion_prefix_id: prospect.account_prospect?.motion?.prefix_id,
4388
+ motion_name: prospect.account_prospect?.motion?.name,
4389
+ updated_at: prospect.updated_at
4390
+ };
4391
+ });
4392
+
4393
+ return [
4394
+ headers.join(","),
4395
+ ...rows.map((row) => headers.map((header) => csvField(row[header])).join(","))
4396
+ ].join("\n");
4397
+ }
4398
+
4399
+ function withProspectAppUrls(payload, host) {
4400
+ if (!payload || !Array.isArray(payload.prospects)) return payload;
4401
+
4402
+ return {
4403
+ ...payload,
4404
+ prospects: payload.prospects.map((prospect) => ({
4405
+ ...prospect,
4406
+ app_url: prospectAppUrl(prospect, host)
4407
+ }))
4408
+ };
4409
+ }
4410
+
4411
+ function prospectAppUrl(prospect, host) {
4412
+ const prospectId = String(prospect?.prefix_id || "").trim();
4413
+ if (!prospectId) return undefined;
4414
+
4415
+ try {
4416
+ return new URL(`/prospects/${encodeURIComponent(prospectId)}`, normalizeHost(host)).toString();
4417
+ } catch {
4418
+ return undefined;
4419
+ }
4420
+ }
4421
+
3677
4422
  function sequenceExportRowsToCsv(rows) {
3678
4423
  return [
3679
4424
  SEQUENCE_EXPORT_CSV_COLUMNS.join(","),
@@ -3774,6 +4519,7 @@ const HELP_TOPICS = new Map([
3774
4519
  " Setup & identity",
3775
4520
  " audienti auth status",
3776
4521
  " audienti config list",
4522
+ " audienti update check",
3777
4523
  " audienti users list",
3778
4524
  " audienti users select <account_user_id|email|name|me>",
3779
4525
  " audienti users activity [account_user_id|me]",
@@ -3782,9 +4528,12 @@ const HELP_TOPICS = new Map([
3782
4528
  " audienti motions list",
3783
4529
  " audienti motions show <motn_id>",
3784
4530
  " audienti motions analytics <motn_id>",
4531
+ " audienti motions run-discovery <motn_id>",
3785
4532
  " audienti motions prospects <motn_id>",
3786
4533
  " audienti motions create --payload <file.json>",
3787
- " audienti motions update <motn_id> --status <state>",
4534
+ " audienti motions update <motn_id> [--status <state>] [--tags <tag[,tag...]>]",
4535
+ " audienti motions add-tag <motn_id> <tag>",
4536
+ " audienti motions remove-tag <motn_id> <tag>",
3788
4537
  " audienti motions activate <motn_id>",
3789
4538
  " audienti motions pause <motn_id>",
3790
4539
  " audienti motions delete <motn_id> --confirm <yes|true|Y|y>",
@@ -3794,8 +4543,12 @@ const HELP_TOPICS = new Map([
3794
4543
  "",
3795
4544
  " Prospects",
3796
4545
  " audienti prospects list [filters]",
4546
+ " audienti prospects check [filters]",
3797
4547
  " audienti prospects show <prsp_id>",
3798
4548
  " audienti prospects assign <prsp_id> --assigned-user <id|me|unassign>",
4549
+ " audienti prospects reject <prsp_id>",
4550
+ " audienti prospects nurture <prsp_id> [--reason <reason>]",
4551
+ " audienti prospects restore <prsp_id>",
3799
4552
  " audienti prospects timeline <prsp_id>",
3800
4553
  " audienti prospects import <linkedin_url> [--motion <motn_id>]",
3801
4554
  " audienti prospects import-batch --file <csv|jsonl|json>",
@@ -3803,10 +4556,21 @@ const HELP_TOPICS = new Map([
3803
4556
  " audienti prospects add-profile <prsp_id> --url <profile_url|email|phone>",
3804
4557
  "",
3805
4558
  " Lists & targeting inputs",
3806
- " audienti lists list",
4559
+ " audienti lists list [--tag <tag>]",
3807
4560
  " audienti lists prospects <list_id>",
4561
+ " audienti lists add-tag <list_id> <tag>",
4562
+ " audienti lists remove-tag <list_id> <tag>",
4563
+ " audienti tags list",
4564
+ " audienti tags show <tag>",
3808
4565
  " audienti offers list",
3809
- " audienti icps list",
4566
+ " audienti offers show <offr_id>",
4567
+ " audienti offers update <offr_id> [--name <text>]",
4568
+ " audienti offers delete <offr_id> --confirm <yes|true|Y|y>",
4569
+ " audienti icps list [--tag <tag>]",
4570
+ " audienti icps show <icp_id>",
4571
+ " audienti icps update <icp_id> [--tags <tag[,tag...]>]",
4572
+ " audienti icps add-tag <icp_id> <tag>",
4573
+ " audienti icps remove-tag <icp_id> <tag>",
3810
4574
  " audienti companies search --query <text>",
3811
4575
  "",
3812
4576
  " Writer",
@@ -3821,6 +4585,7 @@ const HELP_TOPICS = new Map([
3821
4585
  "",
3822
4586
  " Analytics",
3823
4587
  " audienti analytics prospects --window 24h",
4588
+ " audienti analytics dashboard --play-tag <tag>",
3824
4589
  " audienti analytics prospects cohort-analysis --weeks 4 --motion <motn_id>",
3825
4590
  " audienti analytics users --user me --window 30d",
3826
4591
  " audienti analytics visibility --window 24h --user me",
@@ -3835,6 +4600,7 @@ const HELP_TOPICS = new Map([
3835
4600
  " Inspect a prospect: audienti prospects show <prsp_id> --json",
3836
4601
  " Preview a campaign: audienti writer test-run <prsp_id>",
3837
4602
  " Analyze one motion: audienti motions analytics <motn_id>",
4603
+ " Count one campaign: audienti analytics dashboard --play-tag <tag>",
3838
4604
  " Audit your work: audienti analytics users --user me --window 30d",
3839
4605
  "",
3840
4606
  "Global options:",
@@ -3935,6 +4701,46 @@ const HELP_TOPICS = new Map([
3935
4701
  " Default account user: account user name and id, or none selected"
3936
4702
  ].join("\n")],
3937
4703
 
4704
+ ["update", [
4705
+ "Usage:",
4706
+ " audienti update check [--json] [--registry <url>]",
4707
+ "",
4708
+ "Status: implemented",
4709
+ "",
4710
+ "Purpose:",
4711
+ " Check whether this local Audienti CLI install is behind the latest published package.",
4712
+ "",
4713
+ "Commands:",
4714
+ " audienti update check Compare the local package version to the npm registry"
4715
+ ].join("\n")],
4716
+
4717
+ ["update check", [
4718
+ "Usage:",
4719
+ " audienti update check [--json] [--registry <url>]",
4720
+ "",
4721
+ "Status: implemented",
4722
+ "",
4723
+ "Purpose:",
4724
+ " Report whether this local CLI should be updated.",
4725
+ "",
4726
+ "Output shape:",
4727
+ " package_name: @audienti/cli",
4728
+ " current_version: local package version",
4729
+ " latest_version: latest registry version, or null when unknown",
4730
+ " update_available: true | false | null",
4731
+ " status: current | update_available | unknown",
4732
+ " install_command: npm install --global @audienti/cli",
4733
+ " registry: registry URL used for the check",
4734
+ " checked_at: ISO timestamp",
4735
+ " error: string | null",
4736
+ "",
4737
+ "Options:",
4738
+ " --registry <url> Alternate npm-compatible registry. Default: https://registry.npmjs.org",
4739
+ "",
4740
+ "Example:",
4741
+ " audienti update check --json"
4742
+ ].join("\n")],
4743
+
3938
4744
  ["accounts", [
3939
4745
  "Usage:",
3940
4746
  " audienti accounts list [--json]",
@@ -4047,12 +4853,15 @@ const HELP_TOPICS = new Map([
4047
4853
  ["offers", [
4048
4854
  "Usage:",
4049
4855
  " audienti offers list [--json]",
4856
+ " audienti offers show <offr_id> [--json]",
4050
4857
  " audienti offers create --name <text> [--json]",
4858
+ " audienti offers update <offr_id> [--name <text>] [--json]",
4859
+ " audienti offers delete <offr_id> --confirm <yes|true|Y|y> [--json]",
4051
4860
  "",
4052
4861
  "Status: implemented",
4053
4862
  "",
4054
4863
  "Purpose:",
4055
- " List the offers available to the current account so an agent can choose offer_id for motion creation."
4864
+ " Manage the offers available to the current account so an agent can choose offer_id for motion creation."
4056
4865
  ].join("\n")],
4057
4866
 
4058
4867
  ["offers list", [
@@ -4102,10 +4911,60 @@ const HELP_TOPICS = new Map([
4102
4911
  " }"
4103
4912
  ].join("\n")],
4104
4913
 
4914
+ ["offers show", [
4915
+ "Usage:",
4916
+ ` ${OFFERS_SHOW_USAGE.slice("Usage: ".length)}`,
4917
+ "",
4918
+ "Status: implemented",
4919
+ "",
4920
+ "Input shape:",
4921
+ " offr_id: offr_ prefixed id or integer id",
4922
+ "",
4923
+ "API:",
4924
+ " GET /api/v1/accounts/:account_id/offers/:id.json"
4925
+ ].join("\n")],
4926
+
4927
+ ["offers update", [
4928
+ "Usage:",
4929
+ ` ${OFFERS_UPDATE_USAGE.slice("Usage: ".length)}`,
4930
+ "",
4931
+ "Status: implemented",
4932
+ "",
4933
+ "Purpose:",
4934
+ " Update simple offer fields without changing linked motions.",
4935
+ "",
4936
+ "Input shape:",
4937
+ " offr_id: offr_ prefixed id or integer id",
4938
+ " name: string | optional",
4939
+ " description: string | optional",
4940
+ " url: string | optional",
4941
+ "",
4942
+ "API:",
4943
+ " PATCH /api/v1/accounts/:account_id/offers/:id.json"
4944
+ ].join("\n")],
4945
+
4946
+ ["offers delete", [
4947
+ "Usage:",
4948
+ ` ${OFFERS_DELETE_USAGE.slice("Usage: ".length)}`,
4949
+ "",
4950
+ "Status: implemented",
4951
+ "",
4952
+ "Input shape:",
4953
+ " offr_id: offr_ prefixed id or integer id",
4954
+ " confirm: one of yes, true, Y, y",
4955
+ "",
4956
+ "API:",
4957
+ " DELETE /api/v1/accounts/:account_id/offers/:id.json"
4958
+ ].join("\n")],
4959
+
4105
4960
  ["icps", [
4106
4961
  "Usage:",
4107
- " audienti icps list [--json]",
4962
+ " audienti icps list [--tag <tag>] [--json]",
4963
+ " audienti icps show <icp_id> [--json]",
4108
4964
  " audienti icps create (--name <text> | --payload <file.json>) [--json]",
4965
+ " audienti icps update <icp_id> [--tags <tag[,tag...]>] [--json]",
4966
+ " audienti icps add-tag <icp_id> <tag> [--json]",
4967
+ " audienti icps remove-tag <icp_id> <tag> [--json]",
4109
4968
  "",
4110
4969
  "Status: implemented",
4111
4970
  "",
@@ -4115,10 +4974,13 @@ const HELP_TOPICS = new Map([
4115
4974
 
4116
4975
  ["icps list", [
4117
4976
  "Usage:",
4118
- " audienti icps list [--json] [--account <acct_id>]",
4977
+ " audienti icps list [--tag <tag>] [--json] [--account <acct_id>]",
4119
4978
  "",
4120
4979
  "Status: implemented",
4121
4980
  "",
4981
+ "Options:",
4982
+ " --tag <tag> Filter locally to ICPs whose tags include the normalized tag",
4983
+ "",
4122
4984
  "API:",
4123
4985
  " GET /api/v1/accounts/:account_id/icps.json",
4124
4986
  "",
@@ -4127,13 +4989,14 @@ const HELP_TOPICS = new Map([
4127
4989
  " prefix_id: icpp_",
4128
4990
  " name: string",
4129
4991
  " notes: string | null",
4992
+ " tags: [string]",
4130
4993
  " discovery_keyword: string | null",
4131
4994
  " agent: { id, name } | null"
4132
4995
  ].join("\n")],
4133
4996
 
4134
4997
  ["icps create", [
4135
4998
  "Usage:",
4136
- " audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] | --payload <file.json>) [--json] [--account <acct_id>]",
4999
+ " audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] | --payload <file.json>) [--json] [--account <acct_id>]",
4137
5000
  "",
4138
5001
  "Status: implemented",
4139
5002
  "",
@@ -4143,6 +5006,7 @@ const HELP_TOPICS = new Map([
4143
5006
  "Input shape:",
4144
5007
  " name: string Required ICP name",
4145
5008
  " notes: string | optional",
5009
+ " tags: comma-separated tag list | optional",
4146
5010
  " discovery_keyword: string | optional",
4147
5011
  " payload: file.json | optional full ICP object using the account API create shape",
4148
5012
  "",
@@ -4154,7 +5018,8 @@ const HELP_TOPICS = new Map([
4154
5018
  " \"icp\": {",
4155
5019
  " \"name\": \"Renewal-stage IT leaders\",",
4156
5020
  " \"notes\": \"IT leaders reviewing vendors before renewal or QBR.\",",
4157
- " \"discovery_keyword\": \"renewal\"",
5021
+ " \"discovery_keyword\": \"renewal\",",
5022
+ " \"tags\": [\"enterprise\", \"renewal\"]",
4158
5023
  " }",
4159
5024
  " }",
4160
5025
  "",
@@ -4175,6 +5040,90 @@ const HELP_TOPICS = new Map([
4175
5040
  " }"
4176
5041
  ].join("\n")],
4177
5042
 
5043
+ ["icps show", [
5044
+ "Usage:",
5045
+ ` ${ICPS_SHOW_USAGE.slice("Usage: ".length)}`,
5046
+ "",
5047
+ "Status: implemented",
5048
+ "",
5049
+ "Input shape:",
5050
+ " icp_id: icpp_ prefixed id or integer id",
5051
+ "",
5052
+ "API:",
5053
+ " GET /api/v1/accounts/:account_id/icps/:id.json"
5054
+ ].join("\n")],
5055
+
5056
+ ["icps update", [
5057
+ "Usage:",
5058
+ " audienti icps update <icp_id> [--name <text>] [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] [--json] [--account <acct_id>]",
5059
+ "",
5060
+ "Status: implemented",
5061
+ "",
5062
+ "Purpose:",
5063
+ " Update simple ICP fields or replace the ICP's full tag set.",
5064
+ "",
5065
+ "Input shape:",
5066
+ " icp_id: icpp_ prefixed id or integer id",
5067
+ " name: string | optional",
5068
+ " notes: string | optional",
5069
+ " discovery_keyword: string | optional",
5070
+ " tags: comma-separated tag list | optional",
5071
+ "",
5072
+ "API:",
5073
+ " PATCH /api/v1/accounts/:account_id/icps/:id.json",
5074
+ "",
5075
+ "JSON body:",
5076
+ " {",
5077
+ " \"icp\": {",
5078
+ " \"tags\": [\"enterprise\", \"renewal\"]",
5079
+ " }",
5080
+ " }"
5081
+ ].join("\n")],
5082
+
5083
+ ["icps add-tag", [
5084
+ "Usage:",
5085
+ " audienti icps add-tag <icp_id> <tag> [--json] [--account <acct_id>]",
5086
+ "",
5087
+ "Status: implemented",
5088
+ "",
5089
+ "Purpose:",
5090
+ " Add one normalized tag to an ICP.",
5091
+ "",
5092
+ "Input shape:",
5093
+ " icp_id: icpp_ prefixed id or integer id",
5094
+ " tag: string",
5095
+ "",
5096
+ "API:",
5097
+ " POST /api/v1/accounts/:account_id/icps/:id/add_tag.json",
5098
+ "",
5099
+ "JSON body:",
5100
+ " {",
5101
+ " \"tag\": \"enterprise\"",
5102
+ " }"
5103
+ ].join("\n")],
5104
+
5105
+ ["icps remove-tag", [
5106
+ "Usage:",
5107
+ " audienti icps remove-tag <icp_id> <tag> [--json] [--account <acct_id>]",
5108
+ "",
5109
+ "Status: implemented",
5110
+ "",
5111
+ "Purpose:",
5112
+ " Remove one normalized tag from an ICP.",
5113
+ "",
5114
+ "Input shape:",
5115
+ " icp_id: icpp_ prefixed id or integer id",
5116
+ " tag: string",
5117
+ "",
5118
+ "API:",
5119
+ " DELETE /api/v1/accounts/:account_id/icps/:id/remove_tag.json",
5120
+ "",
5121
+ "JSON body:",
5122
+ " {",
5123
+ " \"tag\": \"enterprise\"",
5124
+ " }"
5125
+ ].join("\n")],
5126
+
4178
5127
  ["companies", [
4179
5128
  "Usage:",
4180
5129
  " audienti companies search --query <text> [--json]",
@@ -4206,12 +5155,68 @@ const HELP_TOPICS = new Map([
4206
5155
  " companies[].location: string | null"
4207
5156
  ].join("\n")],
4208
5157
 
5158
+ ["tags", [
5159
+ "Usage:",
5160
+ " audienti tags list [--json]",
5161
+ " audienti tags show <tag> [--json]",
5162
+ "",
5163
+ "Status: implemented",
5164
+ "",
5165
+ "Purpose:",
5166
+ " Show the shared vocabulary from ICP tags, list tags, and motion play_tags currently in use.",
5167
+ "",
5168
+ "Run `audienti tags list help` or `audienti tags show help` for output shape."
5169
+ ].join("\n")],
5170
+
5171
+ ["tags list", [
5172
+ "Usage:",
5173
+ " audienti tags list [--json] [--account <acct_id>]",
5174
+ "",
5175
+ "Status: implemented",
5176
+ "",
5177
+ "Purpose:",
5178
+ " List normalized tags currently used by account ICPs, lists, and motions so new records can match existing labels.",
5179
+ "",
5180
+ "API:",
5181
+ " GET /api/v1/accounts/:account_id/tags.json",
5182
+ "",
5183
+ "Output shape:",
5184
+ " tags[].name: normalized tag string",
5185
+ " tags[].icp_count: number of ICPs using the tag",
5186
+ " tags[].list_count: number of lists using the tag",
5187
+ " tags[].motion_count: number of motions using the tag",
5188
+ " tags[].total_count: icp_count + list_count + motion_count"
5189
+ ].join("\n")],
5190
+
5191
+ ["tags show", [
5192
+ "Usage:",
5193
+ " audienti tags show <tag> [--json] [--account <acct_id>]",
5194
+ "",
5195
+ "Status: implemented",
5196
+ "",
5197
+ "Purpose:",
5198
+ " Show the ICPs, lists, and motions currently using one normalized tag.",
5199
+ "",
5200
+ "API:",
5201
+ " GET /api/v1/accounts/:account_id/icps.json",
5202
+ " GET /api/v1/accounts/:account_id/lists.json",
5203
+ " GET /api/v1/accounts/:account_id/motions.json",
5204
+ "",
5205
+ "Output shape:",
5206
+ " tag: normalized tag string",
5207
+ " icps[]: ICP rows whose tags include tag",
5208
+ " lists[]: list rows whose tags include tag",
5209
+ " motions[]: motion rows whose play_tags include tag"
5210
+ ].join("\n")],
5211
+
4209
5212
  ["lists", [
4210
5213
  "Usage:",
4211
- " audienti lists list [--json]",
4212
- " audienti lists create --name <text> [--json]",
5214
+ " audienti lists list [--tag <tag>] [--json]",
5215
+ " audienti lists create --name <text> [--tags <tag[,tag...]>] [--json]",
4213
5216
  " audienti lists show <list_id> [--json]",
4214
- " audienti lists update <list_id> [--json]",
5217
+ " audienti lists update <list_id> [--tags <tag[,tag...]>] [--json]",
5218
+ " audienti lists add-tag <list_id> <tag> [--json]",
5219
+ " audienti lists remove-tag <list_id> <tag> [--json]",
4215
5220
  " audienti lists delete <list_id> --confirm <yes|true|Y|y> [--json]",
4216
5221
  " audienti lists prospects <list_id> [--json]",
4217
5222
  " audienti lists add-prospects <list_id> <prsp_id> [prsp_id...] [--json]",
@@ -4225,10 +5230,13 @@ const HELP_TOPICS = new Map([
4225
5230
 
4226
5231
  ["lists list", [
4227
5232
  "Usage:",
4228
- " audienti lists list [--json] [--account <acct_id>]",
5233
+ " audienti lists list [--tag <tag>] [--json] [--account <acct_id>]",
4229
5234
  "",
4230
5235
  "Status: implemented",
4231
5236
  "",
5237
+ "Options:",
5238
+ " --tag <tag> Filter locally to lists whose tags include the normalized tag",
5239
+ "",
4232
5240
  "API:",
4233
5241
  " GET /api/v1/accounts/:account_id/lists.json",
4234
5242
  "",
@@ -4239,12 +5247,13 @@ const HELP_TOPICS = new Map([
4239
5247
  " description: string | null",
4240
5248
  " prospect_count: integer",
4241
5249
  " protected_system_list: boolean",
4242
- " hubspot_synced: boolean"
5250
+ " hubspot_synced: boolean",
5251
+ " tags: [string]"
4243
5252
  ].join("\n")],
4244
5253
 
4245
5254
  ["lists create", [
4246
5255
  "Usage:",
4247
- " audienti lists create --name <text> [--description <text>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]",
5256
+ " audienti lists create --name <text> [--description <text>] [--tags <tag[,tag...]>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]",
4248
5257
  "",
4249
5258
  "Status: implemented",
4250
5259
  "",
@@ -4254,6 +5263,7 @@ const HELP_TOPICS = new Map([
4254
5263
  "Input shape:",
4255
5264
  " name: string Required list name",
4256
5265
  " description: string | optional",
5266
+ " tags: comma-separated tag list | optional",
4257
5267
  " campaign_hook: string | optional",
4258
5268
  " audience_note: string | optional",
4259
5269
  "",
@@ -4262,10 +5272,11 @@ const HELP_TOPICS = new Map([
4262
5272
  "",
4263
5273
  "JSON body:",
4264
5274
  " {",
4265
- " \"list\": {",
4266
- " \"name\": \"CIO renewal targets\",",
4267
- " \"description\": \"Accounts to review before QBR outreach.\",",
4268
- " \"campaign_brief\": {",
5275
+ " \"list\": {",
5276
+ " \"name\": \"CIO renewal targets\",",
5277
+ " \"description\": \"Accounts to review before QBR outreach.\",",
5278
+ " \"tags\": [\"sarit\", \"pj\"],",
5279
+ " \"campaign_brief\": {",
4269
5280
  " \"hook\": \"Vendor accountability before renewal\",",
4270
5281
  " \"audience_note\": \"IT leaders running QBRs and renewals\"",
4271
5282
  " }",
@@ -4288,7 +5299,7 @@ const HELP_TOPICS = new Map([
4288
5299
 
4289
5300
  ["lists update", [
4290
5301
  "Usage:",
4291
- " audienti lists update <list_id> [--name <text>] [--description <text>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]",
5302
+ " audienti lists update <list_id> [--name <text>] [--description <text>] [--tags <tag[,tag...]>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]",
4292
5303
  "",
4293
5304
  "Status: implemented",
4294
5305
  "",
@@ -4299,6 +5310,7 @@ const HELP_TOPICS = new Map([
4299
5310
  " list_id: list_ prefix id",
4300
5311
  " list.name: string | optional",
4301
5312
  " list.description: string | optional",
5313
+ " list.tags: comma-separated tag list | optional",
4302
5314
  " list.campaign_brief.hook: string | optional",
4303
5315
  " list.campaign_brief.audience_note: string | optional",
4304
5316
  "",
@@ -4306,6 +5318,50 @@ const HELP_TOPICS = new Map([
4306
5318
  " PATCH /api/v1/accounts/:account_id/lists/:id.json"
4307
5319
  ].join("\n")],
4308
5320
 
5321
+ ["lists add-tag", [
5322
+ "Usage:",
5323
+ ` ${LISTS_ADD_TAG_USAGE.slice("Usage: ".length)}`,
5324
+ "",
5325
+ "Status: implemented",
5326
+ "",
5327
+ "Purpose:",
5328
+ " Add one normalized tag to a list without changing prospect membership.",
5329
+ "",
5330
+ "Input shape:",
5331
+ " list_id: list_ prefix id",
5332
+ " tag: string",
5333
+ "",
5334
+ "API:",
5335
+ " POST /api/v1/accounts/:account_id/lists/:id/add_tag.json",
5336
+ "",
5337
+ "JSON body:",
5338
+ " {",
5339
+ " \"tag\": \"sarit\"",
5340
+ " }"
5341
+ ].join("\n")],
5342
+
5343
+ ["lists remove-tag", [
5344
+ "Usage:",
5345
+ ` ${LISTS_REMOVE_TAG_USAGE.slice("Usage: ".length)}`,
5346
+ "",
5347
+ "Status: implemented",
5348
+ "",
5349
+ "Purpose:",
5350
+ " Remove one normalized tag from a list.",
5351
+ "",
5352
+ "Input shape:",
5353
+ " list_id: list_ prefix id",
5354
+ " tag: string",
5355
+ "",
5356
+ "API:",
5357
+ " DELETE /api/v1/accounts/:account_id/lists/:id/remove_tag.json",
5358
+ "",
5359
+ "JSON body:",
5360
+ " {",
5361
+ " \"tag\": \"sarit\"",
5362
+ " }"
5363
+ ].join("\n")],
5364
+
4309
5365
  ["lists delete", [
4310
5366
  "Usage:",
4311
5367
  " audienti lists delete <list_id> --confirm <yes|true|Y|y> [--json] [--account <acct_id>]",
@@ -4398,14 +5454,17 @@ const HELP_TOPICS = new Map([
4398
5454
 
4399
5455
  ["motions", [
4400
5456
  "Usage:",
4401
- " audienti motions list [--json]",
5457
+ " audienti motions list [--tag <tag>] [--json]",
4402
5458
  " audienti motions show <motn_id> [--json]",
4403
5459
  " audienti motions status <motn_id> [--json]",
5460
+ " audienti motions run-discovery <motn_id> [--target-count <n>] [--json]",
4404
5461
  " audienti motions analytics <motn_id> [--window 30d] [--json]",
4405
5462
  " audienti motions prospects <motn_id> [--json]",
4406
5463
  " audienti motions add-prospects <motn_id> <prsp_id> [prsp_id...] [--json]",
4407
5464
  " audienti motions create --payload <file.json> [--json]",
4408
- " audienti motions update <motn_id> --status <draft|preparing|active|paused|archived> [--json]",
5465
+ " audienti motions update <motn_id> [--status <draft|preparing|active|paused|archived>] [--tags <tag[,tag...]>] [--json]",
5466
+ " audienti motions add-tag <motn_id> <tag> [--json]",
5467
+ " audienti motions remove-tag <motn_id> <tag> [--json]",
4409
5468
  " audienti motions activate <motn_id> [--json]",
4410
5469
  " audienti motions pause <motn_id> [--json]",
4411
5470
  " audienti motions archive <motn_id> [--json]",
@@ -4413,7 +5472,7 @@ const HELP_TOPICS = new Map([
4413
5472
  " audienti motions clone <motn_id> --name <text> [--json]",
4414
5473
  " audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...] [--json]",
4415
5474
  "",
4416
- "Status: read, create, status update, delete, clone, status, and prospect attachment commands implemented",
5475
+ "Status: read, create, status update, delete, clone, status, discovery launch, and prospect attachment commands implemented",
4417
5476
  "",
4418
5477
  "CLI synonym:",
4419
5478
  " `plays` is accepted anywhere `motions` is accepted",
@@ -4424,10 +5483,13 @@ const HELP_TOPICS = new Map([
4424
5483
 
4425
5484
  ["motions list", [
4426
5485
  "Usage:",
4427
- " audienti motions list [--json] [--account <acct_id>]",
5486
+ " audienti motions list [--tag <tag>] [--json] [--account <acct_id>]",
4428
5487
  "",
4429
5488
  "Status: implemented",
4430
5489
  "",
5490
+ "Options:",
5491
+ " --tag <tag> Filter locally to motions whose play_tags include the normalized tag",
5492
+ "",
4431
5493
  "API:",
4432
5494
  " GET /api/v1/accounts/:account_id/motions.json",
4433
5495
  "",
@@ -4440,7 +5502,8 @@ const HELP_TOPICS = new Map([
4440
5502
  " offer.prefix_id: offr_",
4441
5503
  " icp.prefix_id: icpp_",
4442
5504
  " list.prefix_id: list_ | null",
4443
- " principal_account_user.id: integer"
5505
+ " principal_account_user.id: integer",
5506
+ " play_tags: [string]"
4444
5507
  ].join("\n")],
4445
5508
 
4446
5509
  ["motions show", [
@@ -4498,6 +5561,27 @@ const HELP_TOPICS = new Map([
4498
5561
  " GET /api/v1/accounts/:account_id/analytics/prospects.json?motion_id=:motion_id"
4499
5562
  ].join("\n")],
4500
5563
 
5564
+ ["motions run-discovery", [
5565
+ "Usage:",
5566
+ ` ${MOTIONS_RUN_DISCOVERY_USAGE.slice("Usage: ".length)}`,
5567
+ "",
5568
+ "Status: implemented",
5569
+ "",
5570
+ "Purpose:",
5571
+ " Queue an immediate discovery run for one discovery-capable motion or play.",
5572
+ "",
5573
+ "Options:",
5574
+ " --target-count <n> Override the manual replenishment target count for this launch.",
5575
+ "",
5576
+ "Output shape:",
5577
+ " enqueued: true when Motions::DiscoverJob was queued",
5578
+ " reason: launched | run_in_progress | lock_contention | target_met | enqueue_failed",
5579
+ " target_count: requested target count",
5580
+ "",
5581
+ "API:",
5582
+ " POST /api/v1/accounts/:account_id/motions/:id/run_discovery.json"
5583
+ ].join("\n")],
5584
+
4501
5585
  ["motions prospects", [
4502
5586
  "Usage:",
4503
5587
  " audienti motions prospects <motn_id> [--json] [--account <acct_id>]",
@@ -4568,7 +5652,8 @@ const HELP_TOPICS = new Map([
4568
5652
  " principal_account_user_id: integer | me | optional",
4569
5653
  " icp_id: icpp_ prefix id | optional",
4570
5654
  " list_id: list_ prefix id | optional",
4571
- " inbound_channels: [linkedin | reddit | x | tiktok | instagram | facebook] | optional",
5655
+ " play_tags: [string] | optional",
5656
+ " inbound_channels: [linkedin | reddit] | optional",
4572
5657
  " lopa_profiles: [{ url: string, source_type: creator | competitor | partner | customer | other }] | optional",
4573
5658
  "",
4574
5659
  "JSON example:",
@@ -4579,7 +5664,8 @@ const HELP_TOPICS = new Map([
4579
5664
  " \"status\": \"draft\",",
4580
5665
  " \"offer_id\": \"offr_abc123\",",
4581
5666
  " \"principal_account_user_id\": 42,",
4582
- " \"list_id\": \"list_abc123\"",
5667
+ " \"list_id\": \"list_abc123\",",
5668
+ " \"play_tags\": [\"sarit\", \"pj\"]",
4583
5669
  " }",
4584
5670
  "",
4585
5671
  "Behavior:",
@@ -4622,23 +5708,69 @@ const HELP_TOPICS = new Map([
4622
5708
  "Status: implemented",
4623
5709
  "",
4624
5710
  "Purpose:",
4625
- " Change one motion or play's lifecycle status.",
5711
+ " Change one motion or play's lifecycle status or replace its tag set.",
4626
5712
  "",
4627
5713
  "Input shape:",
4628
5714
  " motn_id: motn_ prefix id",
4629
- " status: draft | preparing | active | paused | archived",
5715
+ " status: draft | preparing | active | paused | archived | optional",
5716
+ " tags: comma-separated tag list | optional",
4630
5717
  "",
4631
5718
  "Behavior:",
4632
- " Updates only the motion status. Other motion configuration stays unchanged.",
5719
+ " Updates only the provided fields. Sending --tags replaces the motion's full tag set.",
4633
5720
  "",
4634
5721
  "API:",
4635
5722
  " PATCH /api/v1/accounts/:account_id/motions/:id.json",
4636
5723
  "",
4637
5724
  "JSON body:",
4638
5725
  " {",
4639
- " \"motion\": {",
4640
- " \"status\": \"paused\"",
4641
- " }",
5726
+ " \"motion\": {",
5727
+ " \"status\": \"paused\",",
5728
+ " \"play_tags\": [\"sarit\", \"pj\"]",
5729
+ " }",
5730
+ " }"
5731
+ ].join("\n")],
5732
+
5733
+ ["motions add-tag", [
5734
+ "Usage:",
5735
+ ` ${MOTIONS_ADD_TAG_USAGE.slice("Usage: ".length)}`,
5736
+ "",
5737
+ "Status: implemented",
5738
+ "",
5739
+ "Purpose:",
5740
+ " Add one normalized tag to a motion or play.",
5741
+ "",
5742
+ "Input shape:",
5743
+ " motn_id: motn_ prefix id",
5744
+ " tag: string",
5745
+ "",
5746
+ "API:",
5747
+ " POST /api/v1/accounts/:account_id/motions/:id/add_tag.json",
5748
+ "",
5749
+ "JSON body:",
5750
+ " {",
5751
+ " \"tag\": \"sarit\"",
5752
+ " }"
5753
+ ].join("\n")],
5754
+
5755
+ ["motions remove-tag", [
5756
+ "Usage:",
5757
+ ` ${MOTIONS_REMOVE_TAG_USAGE.slice("Usage: ".length)}`,
5758
+ "",
5759
+ "Status: implemented",
5760
+ "",
5761
+ "Purpose:",
5762
+ " Remove one normalized tag from a motion or play.",
5763
+ "",
5764
+ "Input shape:",
5765
+ " motn_id: motn_ prefix id",
5766
+ " tag: string",
5767
+ "",
5768
+ "API:",
5769
+ " DELETE /api/v1/accounts/:account_id/motions/:id/remove_tag.json",
5770
+ "",
5771
+ "JSON body:",
5772
+ " {",
5773
+ " \"tag\": \"sarit\"",
4642
5774
  " }"
4643
5775
  ].join("\n")],
4644
5776
 
@@ -4732,8 +5864,12 @@ const HELP_TOPICS = new Map([
4732
5864
  ["prospects", [
4733
5865
  "Usage:",
4734
5866
  " audienti prospects list [--json] [filters]",
5867
+ " audienti prospects check [--json|--csv] [filters]",
4735
5868
  " audienti prospects show <prsp_id> [--json]",
4736
5869
  " audienti prospects assign <prsp_id> [prsp_id...] --assigned-user <id|me|unassign> [--json]",
5870
+ " audienti prospects reject <prsp_id> [--json]",
5871
+ " audienti prospects nurture <prsp_id> [--reason <reason>] [--json]",
5872
+ " audienti prospects restore <prsp_id> [--json]",
4737
5873
  " audienti prospects timeline <prsp_id> [--json]",
4738
5874
  " audienti prospects message-types <prsp_id> [--json]",
4739
5875
  " audienti prospects write <prsp_id> --type <surface_key> [--json]",
@@ -4747,7 +5883,7 @@ const HELP_TOPICS = new Map([
4747
5883
  " audienti prospects import-batch --file <csv|jsonl|json> [--list <list_id>] [--motion <motn_id>] [--json]",
4748
5884
  " audienti prospects import-status <primp_id> [--json]",
4749
5885
  "",
4750
- "Status: read commands, assignment, per-prospect draft preview, sequence preview, and import implemented; disposition planned",
5886
+ "Status: read commands, assignment, disposition, per-prospect draft preview, sequence preview, and import implemented",
4751
5887
  "",
4752
5888
  "Filters:",
4753
5889
  " --query <text>",
@@ -4816,6 +5952,43 @@ const HELP_TOPICS = new Map([
4816
5952
  " audienti prospects list --all --csv"
4817
5953
  ].join("\n")],
4818
5954
 
5955
+ ["prospects check", [
5956
+ "Usage:",
5957
+ " audienti prospects check [--json|--csv] [filters] [--account <acct_id>]",
5958
+ "",
5959
+ "Status: implemented",
5960
+ "",
5961
+ "Description:",
5962
+ " Lists suspect people prospects that do not have a certified company employment citation.",
5963
+ " Imported or reported company text is shown for investigation but does not count as certification.",
5964
+ "",
5965
+ "Filters:",
5966
+ " --query <text> Search name, title, company, email, profile URL",
5967
+ " --motion <motn_id> Filter to a motion",
5968
+ " --play <motn_id> Filter to a play using the same motion relationship",
5969
+ " --list <list_id> Filter to a prospect list",
5970
+ " --stage <stage> Filter to a pipeline stage",
5971
+ " --assigned-user <id|me|unassigned> Filter by assigned account user",
5972
+ " --limit <n> Max rows for one page; with --all it caps total rows up to 1000",
5973
+ " --page <n> 1-based page number",
5974
+ " --offset <n> Row offset for manual pagination",
5975
+ " --all Fetch every matching suspect prospect up to 1000 rows",
5976
+ " --csv Export CSV with app_url for operator review",
5977
+ "",
5978
+ "Output shape:",
5979
+ " prospects[].company_certification.status: missing",
5980
+ " prospects[].company_certification.reason: missing_employment_citation",
5981
+ " prospects[].app_url: product URL for operator inspection",
5982
+ " meta.total_count: total matching suspect prospects",
5983
+ "",
5984
+ "API:",
5985
+ " GET /api/v1/accounts/:account_id/prospects.json?data_quality=missing_certified_company",
5986
+ "",
5987
+ "Examples:",
5988
+ " audienti prospects check --motion <motn_id> --all --csv",
5989
+ " audienti prospects check --assigned-user me --json"
5990
+ ].join("\n")],
5991
+
4819
5992
  ["prospects assign", [
4820
5993
  "Usage:",
4821
5994
  ` ${PROSPECTS_ASSIGN_USAGE.slice("Usage: ".length)}`,
@@ -4852,6 +6025,48 @@ const HELP_TOPICS = new Map([
4852
6025
  " GET /api/v1/accounts/:account_id/prospects/:id.json"
4853
6026
  ].join("\n")],
4854
6027
 
6028
+ ["prospects reject", [
6029
+ "Usage:",
6030
+ ` ${PROSPECTS_REJECT_USAGE.slice("Usage: ".length)}`,
6031
+ "",
6032
+ "Status: implemented",
6033
+ "",
6034
+ "Purpose:",
6035
+ " Reject one prospect through the shared disposition and account DNC path.",
6036
+ "",
6037
+ "API:",
6038
+ " POST /api/v1/accounts/:account_id/prospects/:id/reject.json"
6039
+ ].join("\n")],
6040
+
6041
+ ["prospects nurture", [
6042
+ "Usage:",
6043
+ ` ${PROSPECTS_NURTURE_USAGE.slice("Usage: ".length)}`,
6044
+ "",
6045
+ "Status: implemented",
6046
+ "",
6047
+ "Purpose:",
6048
+ " Move one prospect to an inactive nurture disposition through the shared disposition path.",
6049
+ "",
6050
+ "Input shape:",
6051
+ " reason: nurture | non_responsive | not_fit. Defaults to nurture.",
6052
+ "",
6053
+ "API:",
6054
+ " POST /api/v1/accounts/:account_id/prospects/:id/nurture.json"
6055
+ ].join("\n")],
6056
+
6057
+ ["prospects restore", [
6058
+ "Usage:",
6059
+ ` ${PROSPECTS_RESTORE_USAGE.slice("Usage: ".length)}`,
6060
+ "",
6061
+ "Status: implemented",
6062
+ "",
6063
+ "Purpose:",
6064
+ " Restore one rejected or inactive prospect through the shared disposition path.",
6065
+ "",
6066
+ "API:",
6067
+ " POST /api/v1/accounts/:account_id/prospects/:id/restore.json"
6068
+ ].join("\n")],
6069
+
4855
6070
  ["prospects timeline", [
4856
6071
  "Usage:",
4857
6072
  " audienti prospects timeline <prsp_id> [--json] [--types <post,comment,reaction>] [--limit <n>] [--account <acct_id>]",
@@ -5209,26 +6424,6 @@ const HELP_TOPICS = new Map([
5209
6424
  " GET /api/v1/accounts/:account_id/prospect_imports/:id.json"
5210
6425
  ].join("\n")],
5211
6426
 
5212
- ["prospects disposition", [
5213
- "Usage:",
5214
- " audienti prospects disposition <prsp_id> --payload <file.json> [--account <acct_id>]",
5215
- "",
5216
- "Status: planned",
5217
- "",
5218
- "Input shape:",
5219
- " action: defer | nurture | reject | restore",
5220
- " reason: string | optional",
5221
- " delay_until: ISO8601 datetime | optional",
5222
- " note: string | optional",
5223
- "",
5224
- "JSON example:",
5225
- " {",
5226
- " \"action\": \"nurture\",",
5227
- " \"reason\": \"not_a_fit\",",
5228
- " \"note\": \"Not a fit for this motion right now.\"",
5229
- " }"
5230
- ].join("\n")],
5231
-
5232
6427
  ["tools", [
5233
6428
  "Usage:",
5234
6429
  " audienti tools get <email|phone> --url <linkedin_url> [--json]",
@@ -5358,6 +6553,7 @@ const HELP_TOPICS = new Map([
5358
6553
  ["analytics", [
5359
6554
  "Usage:",
5360
6555
  " audienti analytics prospects [--window 24h] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--user <account_user_id|email|name|me>] [--json]",
6556
+ " audienti analytics dashboard [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--play-tag <tag>] [--motion <motn_id>] [--json]",
5361
6557
  " audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--user <account_user_id|email|name|me>] [--json]",
5362
6558
  " audienti analytics users [--user <account_user_id|email|name|me>] [--window 30d | --start YYYY-MM-DD --end YYYY-MM-DD] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--platform <linkedin|email|gmail>] [--json]",
5363
6559
  " audienti analytics visibility [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
@@ -5371,13 +6567,42 @@ const HELP_TOPICS = new Map([
5371
6567
  " --start <YYYY-MM-DD> --end <YYYY-MM-DD> For user analytics, select the events.created_at activity range instead of --window.",
5372
6568
  " --cohort-start <YYYY-MM-DD> --cohort-end <YYYY-MM-DD> Select the AccountProspect.created_at cohort while --window or --start/--end selects the activity period.",
5373
6569
  " --motion <motn_id> For prospect and user analytics, filter AccountProspect.motion_id to one motion/play.",
6570
+ " --play-tag <tag> For dashboard analytics, filter to motions/lists tagged with a campaign tag.",
5374
6571
  " --provenance <source> Optional lower-level AccountProspect.intake_source filter.",
5375
6572
  " --platform <linkedin|email|gmail> For user analytics, filter events.platform. --channel is accepted as an alias.",
5376
6573
  " cohort-analysis loops over recent weekly AccountProspect.created_at cohorts and compares their current stages.",
5377
6574
  " --user <account_user_id|email|name|me> Narrow analytics to one account user. For prospect analytics, this means prospects assigned to that account user. Email/name partials are accepted when they match exactly one account user.",
5378
6575
  "",
5379
6576
  "Output:",
5380
- " Account-scoped analytics for prospects, users, visibility engagement, and ContentOps publishing."
6577
+ " Account-scoped analytics for prospects, campaign dashboard counts, users, visibility engagement, and ContentOps publishing."
6578
+ ].join("\n")],
6579
+
6580
+ ["analytics dashboard", [
6581
+ "Usage:",
6582
+ ` ${ANALYTICS_DASHBOARD_USAGE.slice("Usage: ".length)}`,
6583
+ "",
6584
+ "Status: implemented",
6585
+ "",
6586
+ "Purpose:",
6587
+ " Return the dashboard outreach read model through the CLI, including distinct company target counts for a motion, tag, offer, ICP, or user-filtered cohort.",
6588
+ "",
6589
+ "Options:",
6590
+ " --cohort-start <YYYY-MM-DD> --cohort-end <YYYY-MM-DD> Select the AccountProspect.created_at cohort.",
6591
+ " --play-tag <tag> Filter to motions/lists tagged with a campaign tag. --tag is accepted as an alias.",
6592
+ " --motion <motn_id> Filter to one motion/play.",
6593
+ " --offer <offr_id> Filter to one offer.",
6594
+ " --icp <icp_id> Filter to one ICP.",
6595
+ " --user <account_user_id|email|name|me> Filter to prospects assigned to one account user.",
6596
+ "",
6597
+ "Output shape:",
6598
+ " cohort_size: people in the selected cohort",
6599
+ " cohort_company_target_count: distinct company targets in that cohort",
6600
+ " cohort_people_per_company_average: people per company target",
6601
+ " active_cohort_count and active_cohort_company_target_count: still-active campaign cohort counts",
6602
+ " pipeline_stage_counts[]: current stage distribution",
6603
+ "",
6604
+ "API:",
6605
+ " GET /api/v1/accounts/:account_id/analytics/dashboard.json"
5381
6606
  ].join("\n")],
5382
6607
 
5383
6608
  ["analytics prospects", [
@@ -5529,6 +6754,7 @@ const HELP_TOPICS = new Map([
5529
6754
  " audienti motions pause <motn_id>",
5530
6755
  " audienti motions delete <motn_id> --confirm yes",
5531
6756
  " audienti motions status <motn_id>",
6757
+ " audienti motions run-discovery <motn_id>",
5532
6758
  "",
5533
6759
  "3. Add a new prospect from LinkedIn and poll enrichment",
5534
6760
  " audienti lists create --name \"Target list\"",
@@ -5550,6 +6776,9 @@ const HELP_TOPICS = new Map([
5550
6776
  " audienti prospects add-profile <prsp_id> --url prospect@example.com",
5551
6777
  " audienti prospects report-bad-profile <prsp_id> <prof_id>",
5552
6778
  " audienti prospects add-note <prsp_id> --type steer --message \"Meeting will not happen\" --engagement-type action.meeting.canceled",
6779
+ " audienti prospects reject <prsp_id>",
6780
+ " audienti prospects nurture <prsp_id>",
6781
+ " audienti prospects restore <prsp_id>",
5553
6782
  " audienti prospects sequence-preview <prsp_id>",
5554
6783
  " audienti writer test-run <prsp_id>",
5555
6784
  " audienti prospects sequence-export <prsp_id> --csv",
@@ -5567,6 +6796,7 @@ const HELP_TOPICS = new Map([
5567
6796
  "7. Inspect account analytics",
5568
6797
  " audienti users activity me --window 7d",
5569
6798
  " audienti analytics prospects --window 24h",
6799
+ " audienti analytics dashboard --play-tag wine_campaign",
5570
6800
  " audienti analytics users --user me --window 30d",
5571
6801
  " audienti analytics visibility --window 24h --user me",
5572
6802
  " audienti analytics content --window week",
@@ -5578,7 +6808,6 @@ const HELP_TOPICS = new Map([
5578
6808
  " Prefer prospects import for new LinkedIn people and add-prospects commands for records that already exist.",
5579
6809
  "",
5580
6810
  "Current gaps to plan around:",
5581
- " Prospect disposition still lacks a dedicated CLI mutation.",
5582
6811
  " Operator outcome writeback is implemented for prospect rows, not visibility rows."
5583
6812
  ].join("\n")]
5584
6813
  ]);